bosun 0.36.2 → 0.36.4

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 (57) hide show
  1. package/agent-prompts.mjs +95 -0
  2. package/analyze-agent-work-helpers.mjs +308 -0
  3. package/analyze-agent-work.mjs +926 -0
  4. package/autofix.mjs +2 -0
  5. package/bosun.schema.json +101 -3
  6. package/codex-shell.mjs +85 -10
  7. package/desktop/main.mjs +871 -48
  8. package/desktop/preload.mjs +54 -1
  9. package/desktop-shortcut.mjs +90 -11
  10. package/git-editor-fix.mjs +273 -0
  11. package/mcp-registry.mjs +579 -0
  12. package/meeting-workflow-service.mjs +631 -0
  13. package/monitor.mjs +18 -103
  14. package/package.json +21 -2
  15. package/primary-agent.mjs +32 -12
  16. package/session-tracker.mjs +68 -0
  17. package/setup-web-server.mjs +20 -10
  18. package/setup.mjs +376 -83
  19. package/startup-service.mjs +51 -6
  20. package/stream-resilience.mjs +17 -7
  21. package/ui/app.js +164 -4
  22. package/ui/components/agent-selector.js +145 -1
  23. package/ui/components/chat-view.js +161 -15
  24. package/ui/components/session-list.js +2 -2
  25. package/ui/components/shared.js +188 -15
  26. package/ui/modules/icons.js +13 -0
  27. package/ui/modules/utils.js +44 -0
  28. package/ui/modules/voice-client-sdk.js +733 -0
  29. package/ui/modules/voice-overlay.js +128 -15
  30. package/ui/modules/voice.js +15 -6
  31. package/ui/setup.html +281 -81
  32. package/ui/styles/components.css +99 -3
  33. package/ui/styles/sessions.css +122 -14
  34. package/ui/styles.css +14 -0
  35. package/ui/tabs/agents.js +1 -1
  36. package/ui/tabs/chat.js +123 -14
  37. package/ui/tabs/control.js +16 -22
  38. package/ui/tabs/dashboard.js +85 -8
  39. package/ui/tabs/library.js +113 -17
  40. package/ui/tabs/settings.js +116 -2
  41. package/ui/tabs/tasks.js +388 -39
  42. package/ui/tabs/telemetry.js +0 -1
  43. package/ui/tabs/workflows.js +4 -0
  44. package/ui-server.mjs +400 -22
  45. package/update-check.mjs +41 -13
  46. package/voice-action-dispatcher.mjs +844 -0
  47. package/voice-agents-sdk.mjs +664 -0
  48. package/voice-auth-manager.mjs +164 -0
  49. package/voice-relay.mjs +1194 -0
  50. package/voice-tools.mjs +914 -0
  51. package/workflow-templates/agents.mjs +6 -2
  52. package/workflow-templates/github.mjs +154 -12
  53. package/workflow-templates.mjs +3 -0
  54. package/github-reconciler.mjs +0 -506
  55. package/merge-strategy.mjs +0 -1210
  56. package/pr-cleanup-daemon.mjs +0 -992
  57. package/workspace-reaper.mjs +0 -405
@@ -1,5 +1,58 @@
1
- import { contextBridge } from "electron";
1
+ import { contextBridge, ipcRenderer } from "electron";
2
2
 
3
3
  contextBridge.exposeInMainWorld("veDesktop", {
4
4
  platform: process.platform,
5
+
6
+ follow: {
7
+ open: async (detail = {}) => {
8
+ return ipcRenderer.invoke("bosun:desktop:follow:open", detail || {});
9
+ },
10
+ hide: async () => {
11
+ return ipcRenderer.invoke("bosun:desktop:follow:hide");
12
+ },
13
+ restore: async () => {
14
+ return ipcRenderer.invoke("bosun:desktop:follow:restore");
15
+ },
16
+ },
17
+
18
+ /**
19
+ * Keyboard shortcuts API.
20
+ * Available in the renderer via `window.veDesktop.shortcuts.*`
21
+ */
22
+ shortcuts: {
23
+ /**
24
+ * Returns the full shortcuts catalog with current effective accelerators.
25
+ * @returns {Promise<ShortcutEntry[]>}
26
+ */
27
+ list: () => ipcRenderer.invoke("bosun:shortcuts:list"),
28
+
29
+ /**
30
+ * Set a custom accelerator for a shortcut.
31
+ * Pass `null` as accelerator to disable the shortcut.
32
+ * @param {string} id
33
+ * @param {string|null} accelerator Electron accelerator string or null.
34
+ * @returns {Promise<{ ok: boolean, error?: string }>}
35
+ */
36
+ set: (id, accelerator) =>
37
+ ipcRenderer.invoke("bosun:shortcuts:set", { id, accelerator }),
38
+
39
+ /**
40
+ * Reset a single shortcut to its default accelerator.
41
+ * @param {string} id
42
+ * @returns {Promise<{ ok: boolean, error?: string }>}
43
+ */
44
+ reset: (id) => ipcRenderer.invoke("bosun:shortcuts:reset", { id }),
45
+
46
+ /**
47
+ * Reset all shortcuts to their defaults.
48
+ * @returns {Promise<{ ok: boolean }>}
49
+ */
50
+ resetAll: () => ipcRenderer.invoke("bosun:shortcuts:resetAll"),
51
+
52
+ /**
53
+ * Show the native keyboard shortcuts reference dialog.
54
+ * @returns {Promise<{ ok: boolean }>}
55
+ */
56
+ showDialog: () => ipcRenderer.invoke("bosun:shortcuts:showDialog"),
57
+ },
5
58
  });
@@ -47,10 +47,91 @@ function getCliPath() {
47
47
  return resolve(__dirname, "cli.mjs");
48
48
  }
49
49
 
50
+ function getDesktopMainPath() {
51
+ return resolve(__dirname, "desktop", "main.mjs");
52
+ }
53
+
50
54
  function getWorkingDirectory() {
51
55
  return __dirname;
52
56
  }
53
57
 
58
+ /**
59
+ * Attempt to locate the Electron binary that should be used to launch the
60
+ * Bosun desktop app.
61
+ *
62
+ * Search order:
63
+ * 1. BOSUN_ELECTRON_PATH env var — explicit override
64
+ * 2. <bosun-dir>/node_modules/.bin/electron(.cmd)
65
+ * 3. <bosun-dir>/../node_modules/.bin/electron(.cmd) (repo root)
66
+ * 4. electron(.cmd) anywhere on $PATH
67
+ *
68
+ * Returns the resolved absolute path to the electron binary, or null when
69
+ * none could be located (falls back to node + cli.mjs --desktop).
70
+ */
71
+ function findElectronBinary() {
72
+ // 1. Explicit env override
73
+ const envPath = process.env.BOSUN_ELECTRON_PATH;
74
+ if (envPath) {
75
+ const resolved = resolve(envPath);
76
+ if (existsSync(resolved)) return resolved;
77
+ }
78
+
79
+ const isWin = process.platform === "win32";
80
+ const candidates = isWin
81
+ ? [
82
+ resolve(__dirname, "node_modules", ".bin", "electron.cmd"),
83
+ resolve(__dirname, "..", "node_modules", ".bin", "electron.cmd"),
84
+ resolve(__dirname, "node_modules", ".bin", "electron.exe"),
85
+ resolve(__dirname, "..", "node_modules", ".bin", "electron.exe"),
86
+ ]
87
+ : [
88
+ resolve(__dirname, "node_modules", ".bin", "electron"),
89
+ resolve(__dirname, "..", "node_modules", ".bin", "electron"),
90
+ ];
91
+
92
+ for (const c of candidates) {
93
+ if (existsSync(c)) return c;
94
+ }
95
+
96
+ // 4. Search $PATH
97
+ try {
98
+ const cmd = isWin ? "where electron.cmd 2>nul" : "which electron 2>/dev/null";
99
+ const found = execSync(cmd, { encoding: "utf8", stdio: "pipe", timeout: 2000 })
100
+ .trim()
101
+ .split("\n")[0]
102
+ .trim();
103
+ if (found && existsSync(found)) return found;
104
+ } catch {
105
+ /* not found on PATH */
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Return `{ executable, args }` for launching the Bosun desktop app.
113
+ *
114
+ * - If an Electron binary is found: launches `electron desktop/main.mjs`
115
+ * - Otherwise: falls back to `node cli.mjs --desktop`
116
+ */
117
+ export function resolveElectronLauncher() {
118
+ const electronPath = findElectronBinary();
119
+ if (electronPath) {
120
+ return { executable: electronPath, args: [getDesktopMainPath()] };
121
+ }
122
+ return { executable: getNodePath(), args: [getCliPath(), "--desktop"] };
123
+ }
124
+
125
+ /**
126
+ * Build a quoted shell command string for the launcher.
127
+ * Used by macOS .command fallback and Linux .desktop Exec= field.
128
+ */
129
+ function buildShellCommand() {
130
+ const { executable, args } = resolveElectronLauncher();
131
+ const quotedArgs = args.map((a) => `"${a}"`).join(" ");
132
+ return `"${executable}" ${quotedArgs}`;
133
+ }
134
+
54
135
  function parseLinuxDesktopDir() {
55
136
  try {
56
137
  const configPath = resolve(homedir(), ".config", "user-dirs.dirs");
@@ -93,12 +174,6 @@ function ensureDesktopDir(dir) {
93
174
  }
94
175
  }
95
176
 
96
- function buildShellCommand() {
97
- const nodePath = getNodePath();
98
- const cliPath = getCliPath();
99
- return `"${nodePath}" "${cliPath}" --desktop`;
100
- }
101
-
102
177
  function escapePowerShell(value) {
103
178
  return String(value).replace(/'/g, "''");
104
179
  }
@@ -109,17 +184,21 @@ function escapeAppleScript(value) {
109
184
 
110
185
  function installWindowsShortcut(desktopDir) {
111
186
  const shortcutPath = resolve(desktopDir, `${APP_NAME}.lnk`);
112
- const nodePath = getNodePath();
113
- const cliPath = getCliPath();
114
- const args = `"${cliPath}" --desktop`;
187
+ const { executable, args } = resolveElectronLauncher();
188
+ const quotedArgs = args.map((a) => `"${a}"`).join(" ");
115
189
  const workingDir = getWorkingDirectory();
190
+ const iconPath = resolve(__dirname, "logo.png");
191
+ const iconLine = existsSync(iconPath)
192
+ ? `$Shortcut.IconLocation = '${escapePowerShell(iconPath)}'`
193
+ : "";
116
194
  const psScript = `
117
195
  $WshShell = New-Object -ComObject WScript.Shell
118
196
  $Shortcut = $WshShell.CreateShortcut('${escapePowerShell(shortcutPath)}')
119
- $Shortcut.TargetPath = '${escapePowerShell(nodePath)}'
120
- $Shortcut.Arguments = '${escapePowerShell(args)}'
197
+ $Shortcut.TargetPath = '${escapePowerShell(executable)}'
198
+ $Shortcut.Arguments = '${escapePowerShell(quotedArgs)}'
121
199
  $Shortcut.WorkingDirectory = '${escapePowerShell(workingDir)}'
122
200
  $Shortcut.Description = 'Bosun Desktop Portal'
201
+ ${iconLine}
123
202
  $Shortcut.Save()
124
203
  `.trim();
125
204
 
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * git-editor-fix.mjs — Prevent agents from opening interactive editors
4
+ *
5
+ * Problem: Agents inherit user's git config which uses VSCode (`code --wait`)
6
+ * Result: Git operations block waiting for editor, freezing agents
7
+ *
8
+ * Solution: Set GIT_EDITOR=true (or GIT_EDITOR=:) for non-interactive mode
9
+ *
10
+ * This script ensures all agent workspaces have non-blocking git config.
11
+ * Covers: main repo, tmpclaude-* workspaces, git worktrees (ve/*),
12
+ * and VK task worktrees under $TEMP/vibe-kanban/worktrees/.
13
+ */
14
+
15
+ import { execSync } from "child_process";
16
+ import { existsSync, readdirSync } from "fs";
17
+ import { resolve, basename } from "path";
18
+ import { tmpdir } from "os";
19
+ import { fileURLToPath } from "url";
20
+ import { resolveRepoRoot } from "./repo-root.mjs";
21
+
22
+ const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
23
+ const REPO_ROOT = resolveRepoRoot();
24
+
25
+ /**
26
+ * Configure git to never open interactive editors
27
+ * @param {string} workspacePath - Path to workspace directory
28
+ * @returns {boolean} true if configured successfully
29
+ */
30
+ function configureNonInteractiveGit(workspacePath) {
31
+ const gitDir = resolve(workspacePath, ".git");
32
+
33
+ // .git can be a file (worktree link) or a directory — both are valid
34
+ if (!existsSync(gitDir)) {
35
+ console.warn(`[git-editor-fix] No .git entry at ${workspacePath}`);
36
+ return false;
37
+ }
38
+
39
+ try {
40
+ // Set local git config for this workspace
41
+ // Use ':' (colon) as no-op editor — POSIX standard that always succeeds
42
+ execSync("git config --local core.editor :", {
43
+ cwd: workspacePath,
44
+ stdio: "pipe",
45
+ });
46
+
47
+ // Also disable merge commit editor prompts
48
+ execSync("git config --local merge.commit.autoEdit no", {
49
+ cwd: workspacePath,
50
+ stdio: "pipe",
51
+ });
52
+
53
+ console.log(
54
+ `[git-editor-fix] ✓ Configured ${workspacePath} for non-interactive git`,
55
+ );
56
+ return true;
57
+ } catch (err) {
58
+ console.error(
59
+ `[git-editor-fix] Failed to configure ${workspacePath}:`,
60
+ err.message,
61
+ );
62
+ return false;
63
+ }
64
+ }
65
+
66
+ // ── Workspace discovery helpers ──────────────────────────────────────────────
67
+
68
+ /**
69
+ * Collect tmpclaude-* directories under REPO_ROOT
70
+ * @returns {string[]}
71
+ */
72
+ function findTmpclaudeWorkspaces() {
73
+ /** @type {string[]} */
74
+ const results = [];
75
+ try {
76
+ const entries = readdirSync(REPO_ROOT, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ if (entry.isDirectory() && entry.name.startsWith("tmpclaude-")) {
79
+ results.push(resolve(REPO_ROOT, entry.name));
80
+ }
81
+ }
82
+ } catch (err) {
83
+ console.error(
84
+ "[git-editor-fix] Failed to scan tmpclaude workspaces:",
85
+ err.message,
86
+ );
87
+ }
88
+ return results;
89
+ }
90
+
91
+ /**
92
+ * Parse `git worktree list --porcelain` and return paths of ve/* worktrees
93
+ * @returns {string[]}
94
+ */
95
+ function findGitWorktrees() {
96
+ /** @type {string[]} */
97
+ const results = [];
98
+ try {
99
+ const raw = execSync("git worktree list --porcelain", {
100
+ cwd: REPO_ROOT,
101
+ stdio: ["pipe", "pipe", "pipe"],
102
+ encoding: "utf-8",
103
+ });
104
+
105
+ // Porcelain output has blocks separated by blank lines.
106
+ // Each block starts with "worktree <path>".
107
+ // We also look for "branch refs/heads/ve/..." to identify VK worktrees,
108
+ // but we include ALL worktrees — they all need the fix.
109
+ for (const line of raw.split("\n")) {
110
+ const match = line.match(/^worktree\s+(.+)$/);
111
+ if (match) {
112
+ const wtPath = match[1].trim();
113
+ // Skip the bare repo entry if present
114
+ if (existsSync(resolve(wtPath, ".git"))) {
115
+ results.push(wtPath);
116
+ }
117
+ }
118
+ }
119
+ } catch (err) {
120
+ // git worktree list fails if the repo has no worktrees — that's fine
121
+ if (!String(err.message).includes("is not a git repository")) {
122
+ console.warn(
123
+ "[git-editor-fix] Could not enumerate git worktrees:",
124
+ err.message,
125
+ );
126
+ }
127
+ }
128
+ return results;
129
+ }
130
+
131
+ /**
132
+ * Scan $TEMP/vibe-kanban/worktrees/ for VK task worktree directories
133
+ * @returns {string[]}
134
+ */
135
+ function findVKWorktrees() {
136
+ /** @type {string[]} */
137
+ const results = [];
138
+ const vkBase = resolve(tmpdir(), "vibe-kanban", "worktrees");
139
+
140
+ if (!existsSync(vkBase)) {
141
+ return results;
142
+ }
143
+
144
+ try {
145
+ const entries = readdirSync(vkBase, { withFileTypes: true });
146
+ for (const entry of entries) {
147
+ if (entry.isDirectory()) {
148
+ const candidate = resolve(vkBase, entry.name);
149
+ if (existsSync(resolve(candidate, ".git"))) {
150
+ results.push(candidate);
151
+ }
152
+ }
153
+ }
154
+ } catch (err) {
155
+ console.error("[git-editor-fix] Failed to scan VK worktrees:", err.message);
156
+ }
157
+ return results;
158
+ }
159
+
160
+ // ── Main functions ───────────────────────────────────────────────────────────
161
+
162
+ /**
163
+ * Scan for all agent workspaces and fix git config.
164
+ * Sources:
165
+ * 1. Main repo root (REPO_ROOT)
166
+ * 2. tmpclaude-* directories
167
+ * 3. git worktrees (parsed from `git worktree list --porcelain`)
168
+ * 4. VK task worktrees under $TEMP/vibe-kanban/worktrees/
169
+ *
170
+ * Paths are deduplicated before configuration.
171
+ */
172
+ function fixAllWorkspaces() {
173
+ console.log("[git-editor-fix] Scanning for agent workspaces...");
174
+
175
+ /** @type {Set<string>} */
176
+ const seen = new Set();
177
+
178
+ /** @param {string} p */
179
+ const add = (p) => {
180
+ const normalized = resolve(p);
181
+ seen.add(normalized);
182
+ };
183
+
184
+ // 1. Main repo root
185
+ add(REPO_ROOT);
186
+
187
+ // 2. tmpclaude-* directories
188
+ for (const ws of findTmpclaudeWorkspaces()) {
189
+ add(ws);
190
+ }
191
+
192
+ // 3. Git worktrees (includes ve/* branches)
193
+ for (const ws of findGitWorktrees()) {
194
+ add(ws);
195
+ }
196
+
197
+ // 4. VK worktrees under $TEMP
198
+ for (const ws of findVKWorktrees()) {
199
+ add(ws);
200
+ }
201
+
202
+ const workspaces = [...seen];
203
+ console.log(
204
+ `[git-editor-fix] Found ${workspaces.length} workspace(s) to configure`,
205
+ );
206
+
207
+ let fixed = 0;
208
+ for (const ws of workspaces) {
209
+ if (configureNonInteractiveGit(ws)) {
210
+ fixed++;
211
+ }
212
+ }
213
+
214
+ console.log(
215
+ `[git-editor-fix] ✓ Fixed ${fixed}/${workspaces.length} workspaces`,
216
+ );
217
+ }
218
+
219
+ /**
220
+ * Convenience wrapper: configure the main repo and all discoverable worktrees
221
+ * in a single call. Suitable for use from other modules.
222
+ * @returns {{ fixed: number, total: number }}
223
+ */
224
+ function configureRepoAndWorktrees() {
225
+ console.log("[git-editor-fix] Configuring repo and all worktrees...");
226
+
227
+ /** @type {Set<string>} */
228
+ const seen = new Set();
229
+
230
+ const add = (/** @type {string} */ p) => seen.add(resolve(p));
231
+
232
+ add(REPO_ROOT);
233
+ findTmpclaudeWorkspaces().forEach(add);
234
+ findGitWorktrees().forEach(add);
235
+ findVKWorktrees().forEach(add);
236
+
237
+ const workspaces = [...seen];
238
+ let fixed = 0;
239
+ for (const ws of workspaces) {
240
+ if (configureNonInteractiveGit(ws)) {
241
+ fixed++;
242
+ }
243
+ }
244
+
245
+ console.log(
246
+ `[git-editor-fix] ✓ Configured ${fixed}/${workspaces.length} workspace(s)`,
247
+ );
248
+ return { fixed, total: workspaces.length };
249
+ }
250
+
251
+ // ── CLI Entry Point ──────────────────────────────────────────────────────────
252
+
253
+ const isMainModule = () => {
254
+ try {
255
+ const modulePath = fileURLToPath(import.meta.url);
256
+ return process.argv[1] === modulePath;
257
+ } catch {
258
+ return false;
259
+ }
260
+ };
261
+
262
+ if (isMainModule()) {
263
+ fixAllWorkspaces();
264
+ }
265
+
266
+ export {
267
+ configureNonInteractiveGit,
268
+ fixAllWorkspaces,
269
+ configureRepoAndWorktrees,
270
+ findGitWorktrees,
271
+ findVKWorktrees,
272
+ findTmpclaudeWorkspaces,
273
+ };