bosun 0.38.1 → 0.38.12

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/bosun-skills.mjs CHANGED
@@ -790,12 +790,12 @@ runtime tokens and dramatically reduce exploration time.
790
790
  Use structured comment headers that agents are trained to recognize:
791
791
 
792
792
  \\\`\\\`\\\`
793
- // CLAUDE:SUMMARY — <module-name>
793
+ // BOSUN:SUMMARY — <module-name>
794
794
  // <1–3 sentence summary of purpose, key types, and public API>
795
795
  \\\`\\\`\\\`
796
796
 
797
797
  \\\`\\\`\\\`
798
- // CLAUDE:WARN — <module-name>
798
+ // BOSUN:WARN — <module-name>
799
799
  // <non-obvious pitfall, race condition, or constraint agents MUST know>
800
800
  \\\`\\\`\\\`
801
801
 
@@ -821,7 +821,7 @@ Output: \\\`.bosun/audit/inventory.json\\\`
821
821
  ### Phase 2 — Summaries
822
822
  For every file where \\\`has_summary === false\\\` and \\\`category !== "generated"\\\`:
823
823
  1. Read the file.
824
- 2. Write a \\\`CLAUDE:SUMMARY\\\` comment at the top.
824
+ 2. Write a \\\`BOSUN:SUMMARY\\\` comment at the top.
825
825
  3. Stage the file.
826
826
 
827
827
  ### Phase 3 — Warnings
@@ -831,7 +831,7 @@ For every file, check for non-obvious constraints:
831
831
  - Order-dependent initialization
832
832
  - Platform-specific behavior (Windows paths, etc.)
833
833
 
834
- Add \\\`CLAUDE:WARN\\\` comments where found.
834
+ Add \\\`BOSUN:WARN\\\` comments where found.
835
835
 
836
836
  ### Phase 4 — Manifest Audit
837
837
  Ensure \\\`AGENTS.md\\\` (or equivalent) at repo root is accurate:
@@ -845,8 +845,8 @@ If the file is outdated or missing sections, append corrections.
845
845
 
846
846
  ### Phase 5 — Conformity Check
847
847
  Re-scan all annotations and validate:
848
- - \\\`CLAUDE:SUMMARY\\\` is present in every non-trivial source file.
849
- - \\\`CLAUDE:WARN\\\` exists for files with known pitfalls.
848
+ - \\\`BOSUN:SUMMARY\\\` is present in every non-trivial source file.
849
+ - \\\`BOSUN:WARN\\\` exists for files with known pitfalls.
850
850
  - No stale annotations reference symbols/functions that no longer exist.
851
851
 
852
852
  Output: \\\`.bosun/audit/conformity-report.json\\\`
package/bosun.schema.json CHANGED
@@ -888,7 +888,37 @@
888
888
  },
889
889
  "weight": { "type": "number" },
890
890
  "role": { "type": "string" },
891
- "enabled": { "type": "boolean" }
891
+ "enabled": { "type": "boolean" },
892
+ "provider": {
893
+ "type": "string",
894
+ "description": "AI provider for this executor (e.g. anthropic, openai, ollama, nebius, scaleway, deepinfra, qiniu, cloudflare, xiaomi, synthetic, perplexity)"
895
+ },
896
+ "providerConfig": {
897
+ "type": "object",
898
+ "description": "Provider-specific configuration for this executor",
899
+ "properties": {
900
+ "apiKey": {
901
+ "type": "string",
902
+ "description": "API key for this provider (overrides env)"
903
+ },
904
+ "baseUrl": {
905
+ "type": "string",
906
+ "description": "API base URL for this provider"
907
+ },
908
+ "model": {
909
+ "type": "string",
910
+ "description": "Model ID to use with this provider (e.g. claude-sonnet-4-20250514)"
911
+ },
912
+ "port": {
913
+ "type": "number",
914
+ "description": "Port for local server (OpenCode)"
915
+ },
916
+ "timeoutMs": {
917
+ "type": "number",
918
+ "description": "Request timeout in milliseconds"
919
+ }
920
+ }
921
+ }
892
922
  }
893
923
  },
894
924
  "failover": {
package/config.mjs CHANGED
@@ -530,6 +530,12 @@ function normalizeExecutorEntry(entry, index = 0, total = 1) {
530
530
  entry.codexProfile || entry.modelProfile || "",
531
531
  ).trim();
532
532
 
533
+ // Provider configuration for the executor (e.g. opencode with specific provider)
534
+ const provider = String(entry.provider || "").trim() || null;
535
+ const providerConfig = entry.providerConfig && typeof entry.providerConfig === "object"
536
+ ? { ...entry.providerConfig }
537
+ : null;
538
+
533
539
  return {
534
540
  name,
535
541
  executor: executorType,
@@ -539,6 +545,8 @@ function normalizeExecutorEntry(entry, index = 0, total = 1) {
539
545
  enabled: entry.enabled !== false,
540
546
  models,
541
547
  codexProfile,
548
+ provider,
549
+ providerConfig,
542
550
  };
543
551
  }
544
552
 
package/desktop/main.mjs CHANGED
@@ -13,11 +13,11 @@ import {
13
13
  } from "electron";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { fileURLToPath, pathToFileURL } from "node:url";
16
- import { existsSync, readFileSync } from "node:fs";
16
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { execFileSync, spawn } from "node:child_process";
18
18
  import { request as httpRequest } from "node:http";
19
19
  import { request as httpsRequest } from "node:https";
20
- import { homedir } from "node:os";
20
+ import { homedir, tmpdir } from "node:os";
21
21
 
22
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
23
 
@@ -921,9 +921,8 @@ function buildAppMenu() {
921
921
  },
922
922
  { type: /** @type {const} */ ("separator") },
923
923
  {
924
- label: "Check for Updates",
925
- enabled: app.isPackaged,
926
- click: () => maybeAutoUpdate().catch(() => {}),
924
+ label: "Check for Updates\u2026",
925
+ click: () => checkForUpdateDesktop().catch(() => {}),
927
926
  },
928
927
  ],
929
928
  },
@@ -1200,12 +1199,36 @@ async function createMainWindow() {
1200
1199
  });
1201
1200
 
1202
1201
  mainWindow.on("close", (event) => {
1203
- // User close should not quit the app; minimize to taskbar/dock.
1202
+ // Programmatic quit let it through.
1204
1203
  if (shuttingDown) return;
1205
1204
  event.preventDefault();
1206
1205
  if (mainWindow?.isMinimized()) return;
1207
- mainWindow?.setSkipTaskbar(false);
1208
- mainWindow?.minimize();
1206
+
1207
+ // Show an exit dialog so the user can choose what to do.
1208
+ dialog
1209
+ .showMessageBox(mainWindow, {
1210
+ type: "question",
1211
+ title: "Close Bosun",
1212
+ message: "What would you like to do?",
1213
+ buttons: ["Hide to Taskbar", "Exit Bosun", "Cancel"],
1214
+ defaultId: 0,
1215
+ cancelId: 2,
1216
+ noLink: true,
1217
+ })
1218
+ .then(({ response }) => {
1219
+ if (response === 0) {
1220
+ // Hide to taskbar / system tray
1221
+ mainWindow?.hide();
1222
+ if (trayMode && process.platform === "darwin") {
1223
+ app.dock?.hide();
1224
+ }
1225
+ } else if (response === 1) {
1226
+ // Full exit
1227
+ void shutdown("user_exit");
1228
+ }
1229
+ // response === 2 → Cancel — do nothing
1230
+ })
1231
+ .catch(() => {});
1209
1232
  });
1210
1233
 
1211
1234
  mainWindow.on("closed", () => {
@@ -1473,6 +1496,10 @@ function refreshTrayMenu() {
1473
1496
  },
1474
1497
  { type: /** @type {const} */ ("separator") },
1475
1498
 
1499
+ {
1500
+ label: "Check for Updates\u2026",
1501
+ click: () => checkForUpdateDesktop().catch(() => {}),
1502
+ },
1476
1503
  {
1477
1504
  label: "Restart to Apply Update",
1478
1505
  enabled: app.isPackaged,
@@ -1897,18 +1924,337 @@ async function bootstrap() {
1897
1924
  }
1898
1925
 
1899
1926
  async function maybeAutoUpdate() {
1900
- if (!app.isPackaged) return;
1901
- if (process.env.BOSUN_DESKTOP_AUTO_UPDATE !== "1") return;
1927
+ // Packaged Electron builds use electron-updater (Squirrel / NSIS).
1928
+ if (app.isPackaged && process.env.BOSUN_DESKTOP_AUTO_UPDATE === "1") {
1929
+ try {
1930
+ const { autoUpdater } = await import("electron-updater");
1931
+ const feedUrl = process.env.BOSUN_DESKTOP_UPDATE_URL;
1932
+ if (feedUrl) {
1933
+ autoUpdater.setFeedURL({ url: feedUrl });
1934
+ }
1935
+ autoUpdater.autoDownload = true;
1936
+ autoUpdater.checkForUpdatesAndNotify().catch(() => {});
1937
+ } catch (err) {
1938
+ console.warn("[desktop] auto-update unavailable", err?.message || err);
1939
+ }
1940
+ return;
1941
+ }
1942
+
1943
+ // For dev / npm-global installs: silent background check on launch.
1944
+ // Just logs — no dialogs. The user can trigger interactive flow from the menu.
1902
1945
  try {
1903
- const { autoUpdater } = await import("electron-updater");
1904
- const feedUrl = process.env.BOSUN_DESKTOP_UPDATE_URL;
1905
- if (feedUrl) {
1906
- autoUpdater.setFeedURL({ url: feedUrl });
1946
+ const currentVersion = readCurrentBosunVersion();
1947
+ const latest = await fetchLatestVersionFromRegistry();
1948
+ if (latest && isNewerVersion(latest, currentVersion)) {
1949
+ console.log(
1950
+ `[desktop] Update available: v${currentVersion} → v${latest}. ` +
1951
+ `Use Bosun → Check for Updates to install.`,
1952
+ );
1907
1953
  }
1908
- autoUpdater.autoDownload = true;
1909
- autoUpdater.checkForUpdatesAndNotify().catch(() => {});
1954
+ } catch {
1955
+ // silent — never block startup
1956
+ }
1957
+ }
1958
+
1959
+ // ── Desktop Update Helpers ────────────────────────────────────────────────────
1960
+
1961
+ /**
1962
+ * Read the current bosun version from the on-disk package.json.
1963
+ * Works in both packaged and dev setups.
1964
+ */
1965
+ function readCurrentBosunVersion() {
1966
+ try {
1967
+ const pkg = JSON.parse(
1968
+ readFileSync(resolveBosunRuntimePath("package.json"), "utf8"),
1969
+ );
1970
+ return pkg.version || "0.0.0";
1971
+ } catch {
1972
+ return "0.0.0";
1973
+ }
1974
+ }
1975
+
1976
+ /** Simple semver comparison (major.minor.patch). */
1977
+ function isNewerVersion(remote, local) {
1978
+ const parse = (v) => {
1979
+ const parts = String(v).replace(/^v/, "").split(".").map(Number);
1980
+ return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
1981
+ };
1982
+ const r = parse(remote);
1983
+ const l = parse(local);
1984
+ if (r.major !== l.major) return r.major > l.major;
1985
+ if (r.minor !== l.minor) return r.minor > l.minor;
1986
+ return r.patch > l.patch;
1987
+ }
1988
+
1989
+ /**
1990
+ * Fetch the latest published version of `bosun` from the npm registry.
1991
+ * Uses raw https to avoid undici handle leaks on Windows.
1992
+ */
1993
+ function fetchLatestVersionFromRegistry() {
1994
+ return new Promise((res) => {
1995
+ const req = httpsRequest(
1996
+ "https://registry.npmjs.org/bosun/latest",
1997
+ { headers: { Accept: "application/json" }, timeout: 10_000 },
1998
+ (response) => {
1999
+ if (response.statusCode < 200 || response.statusCode >= 300) {
2000
+ response.resume();
2001
+ return res(null);
2002
+ }
2003
+ let body = "";
2004
+ response.setEncoding("utf8");
2005
+ response.on("data", (chunk) => { body += chunk; });
2006
+ response.on("end", () => {
2007
+ try { res(JSON.parse(body).version || null); } catch { res(null); }
2008
+ });
2009
+ },
2010
+ );
2011
+ req.on("error", () => res(null));
2012
+ req.on("timeout", () => { req.destroy(); res(null); });
2013
+ req.end();
2014
+ });
2015
+ }
2016
+
2017
+ /**
2018
+ * Interactive "Check for Updates" flow for the desktop app.
2019
+ * Fetches the latest version from npm, shows a dialog, and — if the user
2020
+ * confirms — spawns a resilient updater script that survives EBUSY locks.
2021
+ */
2022
+ async function checkForUpdateDesktop() {
2023
+ const parentWin = mainWindow && !mainWindow.isDestroyed() ? mainWindow : null;
2024
+
2025
+ const currentVersion = readCurrentBosunVersion();
2026
+
2027
+ let latestVersion;
2028
+ try {
2029
+ latestVersion = await fetchLatestVersionFromRegistry();
2030
+ } catch {
2031
+ latestVersion = null;
2032
+ }
2033
+
2034
+ if (!latestVersion) {
2035
+ await dialog.showMessageBox(parentWin, {
2036
+ type: "warning",
2037
+ title: "Check for Updates",
2038
+ message: "Could not reach the npm registry.",
2039
+ detail:
2040
+ "Make sure you are connected to the internet and try again.\n\n" +
2041
+ "You can also update manually:\n npm install -g bosun@latest",
2042
+ buttons: ["OK"],
2043
+ });
2044
+ return;
2045
+ }
2046
+
2047
+ if (!isNewerVersion(latestVersion, currentVersion)) {
2048
+ await dialog.showMessageBox(parentWin, {
2049
+ type: "info",
2050
+ title: "Check for Updates",
2051
+ message: "Bosun is up to date!",
2052
+ detail: `Current version: v${currentVersion}\nLatest version: v${latestVersion}`,
2053
+ buttons: ["OK"],
2054
+ });
2055
+ return;
2056
+ }
2057
+
2058
+ // Update is available — ask the user whether to install.
2059
+ const { response } = await dialog.showMessageBox(parentWin, {
2060
+ type: "info",
2061
+ title: "Update Available",
2062
+ message: `A new version of Bosun is available!`,
2063
+ detail:
2064
+ `Current version: v${currentVersion}\n` +
2065
+ `New version: v${latestVersion}\n\n` +
2066
+ "Bosun will close, install the update, and relaunch automatically.\n" +
2067
+ "The update typically takes 10–30 seconds.",
2068
+ buttons: ["Install Update & Restart", "Later"],
2069
+ defaultId: 0,
2070
+ cancelId: 1,
2071
+ });
2072
+
2073
+ if (response !== 0) return;
2074
+
2075
+ // Spawn a detached updater script that waits for the app to exit,
2076
+ // retries npm install (surviving EBUSY), and relaunches.
2077
+ try {
2078
+ spawnResilientUpdater(latestVersion);
2079
+ // Brief pause so the updater process is established before we quit.
2080
+ await new Promise((r) => setTimeout(r, 500));
2081
+ void shutdown("update_install");
1910
2082
  } catch (err) {
1911
- console.warn("[desktop] auto-update unavailable", err?.message || err);
2083
+ await dialog.showMessageBox(parentWin, {
2084
+ type: "error",
2085
+ title: "Update Failed",
2086
+ message: "Could not start the update process.",
2087
+ detail:
2088
+ `${err?.message || err}\n\n` +
2089
+ "You can update manually by running:\n npm install -g bosun@latest",
2090
+ buttons: ["OK"],
2091
+ });
2092
+ }
2093
+ }
2094
+
2095
+ /**
2096
+ * Spawn a detached OS-native script that:
2097
+ * 1. Waits for the Bosun desktop process to fully exit.
2098
+ * 2. Retries `npm install -g bosun@<version>` with exponential back-off
2099
+ * to survive EBUSY / file-lock errors on Windows.
2100
+ * 3. Attempts to relaunch Bosun desktop after a successful install.
2101
+ * 4. Self-deletes the temp script.
2102
+ */
2103
+ function spawnResilientUpdater(version) {
2104
+ const safeVersion = String(version).replaceAll(/[^0-9.a-zA-Z-]/g, "");
2105
+ const scriptId = `bosun-update-${Date.now()}`;
2106
+
2107
+ if (process.platform === "win32") {
2108
+ const scriptPath = resolve(tmpdir(), `${scriptId}.ps1`);
2109
+ // Resolve the bosun CLI so we can relaunch after install.
2110
+ const bosunCliPath = resolveBosunRuntimePath("cli.mjs").replaceAll("\\", "\\\\");
2111
+ const nodeExePath = process.execPath.replaceAll("\\", "\\\\");
2112
+ const script = [
2113
+ `# Bosun Desktop Auto-Updater — generated ${new Date().toISOString()}`,
2114
+ `$ErrorActionPreference = 'Stop'`,
2115
+ `$parentPid = ${process.pid}`,
2116
+ ``,
2117
+ `# ── Wait for the desktop process to exit ──────────────────────`,
2118
+ `$maxWait = 30`,
2119
+ `$waited = 0`,
2120
+ `while ($waited -lt $maxWait) {`,
2121
+ ` try {`,
2122
+ ` Get-Process -Id $parentPid -ErrorAction Stop | Out-Null`,
2123
+ ` Start-Sleep -Seconds 1`,
2124
+ ` $waited++`,
2125
+ ` } catch {`,
2126
+ ` break`,
2127
+ ` }`,
2128
+ `}`,
2129
+ ``,
2130
+ `# ── Retry npm install with exponential back-off ───────────────`,
2131
+ `$maxRetries = 6`,
2132
+ `$delay = 2`,
2133
+ `$success = $false`,
2134
+ ``,
2135
+ `for ($i = 1; $i -le $maxRetries; $i++) {`,
2136
+ ` Write-Host "[bosun-updater] Attempt $i / $maxRetries ..."`,
2137
+ ` try {`,
2138
+ ` $output = & npm install -g bosun@${safeVersion} 2>&1 | Out-String`,
2139
+ ` if ($LASTEXITCODE -eq 0) {`,
2140
+ ` Write-Host "[bosun-updater] Success!"`,
2141
+ ` $success = $true`,
2142
+ ` break`,
2143
+ ` }`,
2144
+ ` Write-Host $output`,
2145
+ ` } catch {`,
2146
+ ` Write-Host "[bosun-updater] Error: $_"`,
2147
+ ` }`,
2148
+ ` if ($i -lt $maxRetries) {`,
2149
+ ` Write-Host "[bosun-updater] Retrying in $delay seconds ..."`,
2150
+ ` Start-Sleep -Seconds $delay`,
2151
+ ` $delay = [math]::Min($delay * 2, 30)`,
2152
+ ` }`,
2153
+ `}`,
2154
+ ``,
2155
+ `# ── Last-resort: --force ──────────────────────────────────────`,
2156
+ `if (-not $success) {`,
2157
+ ` Write-Host "[bosun-updater] Trying with --force ..."`,
2158
+ ` try {`,
2159
+ ` & npm install -g bosun@${safeVersion} --force 2>&1 | Out-String`,
2160
+ ` if ($LASTEXITCODE -eq 0) { $success = $true }`,
2161
+ ` } catch {`,
2162
+ ` Write-Host "[bosun-updater] Force install also failed: $_"`,
2163
+ ` }`,
2164
+ `}`,
2165
+ ``,
2166
+ `# ── Relaunch Bosun desktop ────────────────────────────────────`,
2167
+ `if ($success) {`,
2168
+ ` Write-Host "[bosun-updater] Relaunching Bosun desktop ..."`,
2169
+ ` try {`,
2170
+ ` Start-Process -FilePath "bosun" -ArgumentList "--desktop" -WindowStyle Hidden`,
2171
+ ` } catch {`,
2172
+ ` # Fallback: launch via node directly`,
2173
+ ` try {`,
2174
+ ` Start-Process -FilePath "${nodeExePath}" -ArgumentList "${bosunCliPath}", "--desktop" -WindowStyle Hidden`,
2175
+ ` } catch {`,
2176
+ ` Write-Host "[bosun-updater] Could not relaunch: $_"`,
2177
+ ` }`,
2178
+ ` }`,
2179
+ `} else {`,
2180
+ ` Write-Host "[bosun-updater] Update failed. Please run manually: npm install -g bosun@latest"`,
2181
+ `}`,
2182
+ ``,
2183
+ `# ── Clean up ─────────────────────────────────────────────────`,
2184
+ `Start-Sleep -Seconds 2`,
2185
+ `Remove-Item -Path $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue`,
2186
+ ].join("\n");
2187
+
2188
+ writeFileSync(scriptPath, script, "utf8");
2189
+
2190
+ const child = spawn(
2191
+ "powershell.exe",
2192
+ ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
2193
+ { detached: true, stdio: "ignore", windowsHide: false },
2194
+ );
2195
+ child.unref();
2196
+ console.log(`[desktop] spawned updater script: ${scriptPath} (pid ${child.pid})`);
2197
+ } else {
2198
+ // ── macOS / Linux ─────────────────────────────────────────────────
2199
+ const scriptPath = resolve(tmpdir(), `${scriptId}.sh`);
2200
+ const script = [
2201
+ `#!/usr/bin/env bash`,
2202
+ `# Bosun Desktop Auto-Updater — generated ${new Date().toISOString()}`,
2203
+ `set -e`,
2204
+ `PARENT_PID=${process.pid}`,
2205
+ `MAX_WAIT=30`,
2206
+ `WAITED=0`,
2207
+ ``,
2208
+ `# Wait for parent to exit`,
2209
+ `while [ $WAITED -lt $MAX_WAIT ]; do`,
2210
+ ` if kill -0 $PARENT_PID 2>/dev/null; then`,
2211
+ ` sleep 1`,
2212
+ ` WAITED=$((WAITED + 1))`,
2213
+ ` else`,
2214
+ ` break`,
2215
+ ` fi`,
2216
+ `done`,
2217
+ ``,
2218
+ `# Retry npm install with back-off`,
2219
+ `MAX_RETRIES=6`,
2220
+ `DELAY=2`,
2221
+ `SUCCESS=0`,
2222
+ `for i in $(seq 1 $MAX_RETRIES); do`,
2223
+ ` echo "[bosun-updater] Attempt $i / $MAX_RETRIES ..."`,
2224
+ ` if npm install -g bosun@${safeVersion} 2>&1; then`,
2225
+ ` SUCCESS=1`,
2226
+ ` break`,
2227
+ ` fi`,
2228
+ ` echo "[bosun-updater] Retrying in \${DELAY}s ..."`,
2229
+ ` sleep $DELAY`,
2230
+ ` DELAY=$((DELAY * 2))`,
2231
+ ` [ $DELAY -gt 30 ] && DELAY=30`,
2232
+ `done`,
2233
+ ``,
2234
+ `# Last resort: --force`,
2235
+ `if [ $SUCCESS -eq 0 ]; then`,
2236
+ ` echo "[bosun-updater] Trying with --force ..."`,
2237
+ ` npm install -g bosun@${safeVersion} --force 2>&1 && SUCCESS=1 || true`,
2238
+ `fi`,
2239
+ ``,
2240
+ `# Relaunch`,
2241
+ `if [ $SUCCESS -eq 1 ]; then`,
2242
+ ` echo "[bosun-updater] Relaunching Bosun desktop ..."`,
2243
+ ` nohup bosun --desktop </dev/null >/dev/null 2>&1 &`,
2244
+ `fi`,
2245
+ ``,
2246
+ `# Clean up`,
2247
+ `rm -f "$0"`,
2248
+ ].join("\n");
2249
+
2250
+ writeFileSync(scriptPath, script, { encoding: "utf8", mode: 0o755 });
2251
+
2252
+ const child = spawn("bash", [scriptPath], {
2253
+ detached: true,
2254
+ stdio: "ignore",
2255
+ });
2256
+ child.unref();
2257
+ console.log(`[desktop] spawned updater script: ${scriptPath} (pid ${child.pid})`);
1912
2258
  }
1913
2259
  }
1914
2260
 
@@ -34,12 +34,12 @@ export const RESOURCE_TYPES = Object.freeze(["prompt", "agent", "skill", "mcp",
34
34
 
35
35
  // ── Helpers ───────────────────────────────────────────────────────────────────
36
36
 
37
- function getBosunHome() {
38
- return (
39
- process.env.BOSUN_HOME ||
40
- process.env.BOSUN_DIR ||
41
- resolve(homedir(), ".bosun")
42
- );
37
+ export function getBosunHomeDir() {
38
+ const explicit = process.env.BOSUN_HOME || process.env.BOSUN_DIR;
39
+ if (explicit) return resolve(String(explicit));
40
+ const modernDefault = resolve(homedir(), "bosun");
41
+ if (existsSync(modernDefault)) return modernDefault;
42
+ return resolve(homedir(), ".bosun");
43
43
  }
44
44
 
45
45
  function ensureDir(dir) {
@@ -111,7 +111,7 @@ function nowISO() {
111
111
  * Get the manifest path for a workspace (or global).
112
112
  */
113
113
  export function getManifestPath(rootDir) {
114
- return resolve(rootDir || getBosunHome(), ".bosun", LIBRARY_MANIFEST);
114
+ return resolve(rootDir || getBosunHomeDir(), ".bosun", LIBRARY_MANIFEST);
115
115
  }
116
116
 
117
117
  /**
@@ -139,7 +139,7 @@ export function saveManifest(rootDir, manifest) {
139
139
  // ── CRUD operations ──────────────────────────────────────────────────────────
140
140
 
141
141
  function dirForType(rootDir, type) {
142
- const root = rootDir || getBosunHome();
142
+ const root = rootDir || getBosunHomeDir();
143
143
  switch (type) {
144
144
  case "prompt": return resolve(root, PROMPT_DIR);
145
145
  case "skill": return resolve(root, SKILL_DIR);
@@ -613,7 +613,7 @@ export function resolveEntry(workspaceRoot, id) {
613
613
  }
614
614
 
615
615
  // 2. Check global
616
- const globalRoot = getBosunHome();
616
+ const globalRoot = getBosunHomeDir();
617
617
  if (globalRoot !== workspaceRoot) {
618
618
  const entry = getEntry(globalRoot, id);
619
619
  if (entry) {