bosun 0.41.9 → 0.42.0

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/.env.example CHANGED
@@ -201,7 +201,7 @@ VOICE_DELEGATE_EXECUTOR=codex-sdk
201
201
  # earlier: it summarizes large, noisy command outputs before they ever land in
202
202
  # the active turn, while preserving a `bosun --tool-log <id>` retrieval path.
203
203
  # CONTEXT_SHREDDING_ENABLED=true
204
- # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=false
204
+ # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_ENABLED=true
205
205
  # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MODE=auto
206
206
  # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_MIN_CHARS=4000
207
207
  # CONTEXT_SHREDDING_LIVE_TOOL_COMPACTION_TARGET_CHARS=1800
@@ -144,7 +144,7 @@ export const HOOK_EVENTS = Object.freeze([
144
144
  * Canonical SDK names.
145
145
  * @type {readonly string[]}
146
146
  */
147
- const VALID_SDKS = Object.freeze(["codex", "copilot", "claude", "opencode"]);
147
+ const VALID_SDKS = Object.freeze(["codex", "copilot", "claude", "opencode", "gemini"]);
148
148
 
149
149
  /**
150
150
  * Wildcard indicating a hook applies to all SDKs.
@@ -1198,3 +1198,58 @@ function _truncate(str, maxLen) {
1198
1198
  if (!str || str.length <= maxLen) return str ?? "";
1199
1199
  return str.slice(0, maxLen) + "\n... (truncated)";
1200
1200
  }
1201
+
1202
+ // ── Hook Library Integration ────────────────────────────────────────────────
1203
+
1204
+ /**
1205
+ * Register hooks from the hook-library into the agent-hooks runtime registry.
1206
+ * This bridges the declarative hook catalog with the execution engine.
1207
+ *
1208
+ * @param {Record<string, Array<{id: string, command: string, description?: string, timeout?: number, blocking?: boolean, sdks?: string[], builtin?: boolean, retryable?: boolean, maxRetries?: number, env?: Record<string,string>}>>} hooksByEvent
1209
+ * - Output of getHooksForRegistration() from hook-library.mjs
1210
+ * @returns {{ registered: number, skipped: number }}
1211
+ */
1212
+ export function registerLibraryHooks(hooksByEvent) {
1213
+ let registered = 0;
1214
+ let skipped = 0;
1215
+
1216
+ if (!hooksByEvent || typeof hooksByEvent !== "object") {
1217
+ return { registered, skipped };
1218
+ }
1219
+
1220
+ for (const [event, hooks] of Object.entries(hooksByEvent)) {
1221
+ if (!HOOK_EVENTS.includes(event)) {
1222
+ console.warn(`${TAG} unknown hook event from library: ${event}`);
1223
+ skipped += hooks.length;
1224
+ continue;
1225
+ }
1226
+
1227
+ for (const hook of hooks) {
1228
+ // Skip if a hook with this ID is already registered (built-in takes priority)
1229
+ const existing = (_registry.get(event) ?? []).find((h) => h.id === hook.id);
1230
+ if (existing) {
1231
+ skipped++;
1232
+ continue;
1233
+ }
1234
+
1235
+ registerHook(event, {
1236
+ id: hook.id,
1237
+ command: hook.command,
1238
+ description: hook.description ?? "",
1239
+ timeout: hook.timeout ?? DEFAULT_TIMEOUT_MS,
1240
+ blocking: hook.blocking ?? false,
1241
+ sdks: hook.sdks ?? [SDK_WILDCARD],
1242
+ builtin: hook.builtin ?? false,
1243
+ retryable: hook.retryable ?? false,
1244
+ maxRetries: hook.maxRetries,
1245
+ env: hook.env,
1246
+ });
1247
+ registered++;
1248
+ }
1249
+ }
1250
+
1251
+ if (registered > 0) {
1252
+ console.log(`${TAG} registered ${registered} hook(s) from hook library`);
1253
+ }
1254
+ return { registered, skipped };
1255
+ }
@@ -55,8 +55,7 @@ import {
55
55
  MAX_STREAM_RETRIES,
56
56
  } from "../infra/stream-resilience.mjs";
57
57
  import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
58
- import { compressAllItems, estimateSavings, estimateContextUsagePct, recordShreddingEvent } from "../workspace/context-cache.mjs";
59
- import { resolveContextShreddingOptions } from "../config/context-shredding-config.mjs";
58
+ import { maybeCompressSessionItems } from "../workspace/context-cache.mjs";
60
59
 
61
60
  // Lazy-load MCP registry to avoid circular dependencies.
62
61
  // Cached at module scope per AGENTS.md hard rules.
@@ -110,7 +109,15 @@ function hasOptionalModule(specifier) {
110
109
  require.resolve(specifier);
111
110
  ok = true;
112
111
  } catch {
113
- ok = false;
112
+ // ESM-only packages have no CJS "require" export so require.resolve
113
+ // throws even when the package is installed. Fall back to checking
114
+ // whether the package directory exists on disk.
115
+ try {
116
+ const pkgDir = resolve(__dirname, "..", "node_modules", ...specifier.split("/"));
117
+ ok = existsSync(resolve(pkgDir, "package.json"));
118
+ } catch {
119
+ ok = false;
120
+ }
114
121
  }
115
122
  MODULE_PRESENCE_CACHE.set(specifier, ok);
116
123
  return ok;
@@ -244,29 +251,12 @@ async function maybeCompressResultItems(
244
251
 
245
252
  const resolvedSessionType = normalizeSessionType(sessionType, "task");
246
253
  const agentType = normalizeSdkForShredding(sdk);
247
- const shreddingOpts = resolveContextShreddingOptions(
248
- resolvedSessionType,
254
+ return maybeCompressSessionItems(items, {
255
+ sessionType: resolvedSessionType,
249
256
  agentType,
250
- );
251
- if (shreddingOpts?._skip === true) return items;
252
-
253
- const usagePct = estimateContextUsagePct(items);
254
- const threshold = Number.isFinite(shreddingOpts?.contextUsageThreshold)
255
- ? Number(shreddingOpts.contextUsageThreshold)
256
- : 0.5;
257
- if (usagePct < threshold) return items;
258
-
259
- shreddingOpts.contextUsagePct = usagePct;
260
- const compressedItems = await compressAllItems(items, shreddingOpts);
261
- try {
262
- const savings = estimateSavings(items, compressedItems);
263
- if (savings.savedChars > 0) {
264
- recordShreddingEvent({ ...savings, agentType: agentType || sdk });
265
- }
266
- } catch {
267
- /* non-fatal */
268
- }
269
- return compressedItems;
257
+ force: forceCompression,
258
+ skip: skipCompression,
259
+ });
270
260
  }
271
261
 
272
262
  function resolveCodexStreamSafety(totalTimeoutMs) {
@@ -762,13 +752,14 @@ async function withTemporaryEnv(overrides, fn) {
762
752
  * Otherwise strips OPENAI_BASE_URL so the SDK uses its default auth.
763
753
  */
764
754
  function buildCodexSdkOptions(envInput = process.env) {
765
- const { env: resolvedEnv } = resolveCodexProfileRuntime(envInput);
755
+ const resolved = resolveCodexProfileRuntime(envInput);
756
+ const { env: resolvedEnv, configProvider } = resolved;
766
757
  const baseUrl = resolvedEnv.OPENAI_BASE_URL || "";
767
758
  const isAzure = (() => {
768
759
  try {
769
760
  const parsed = new URL(baseUrl);
770
761
  const host = String(parsed.hostname || "").toLowerCase();
771
- return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
762
+ return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
772
763
  } catch {
773
764
  return false;
774
765
  }
@@ -783,16 +774,20 @@ function buildCodexSdkOptions(envInput = process.env) {
783
774
  if (env.OPENAI_API_KEY && !env.AZURE_OPENAI_API_KEY) {
784
775
  env.AZURE_OPENAI_API_KEY = env.OPENAI_API_KEY;
785
776
  }
777
+ // Use the config.toml provider section name and env_key when available,
778
+ // so the SDK config override is consistent with the user's config.toml.
779
+ const providerSectionName = configProvider?.name || "azure";
780
+ const providerEnvKey = configProvider?.envKey || "AZURE_OPENAI_API_KEY";
786
781
  const azureModel = env.CODEX_MODEL || undefined;
787
782
  return {
788
783
  env,
789
784
  config: {
790
- model_provider: "azure",
785
+ model_provider: providerSectionName,
791
786
  model_providers: {
792
- azure: {
787
+ [providerSectionName]: {
793
788
  name: "Azure OpenAI",
794
789
  base_url: baseUrl,
795
- env_key: "AZURE_OPENAI_API_KEY",
790
+ env_key: providerEnvKey,
796
791
  wire_api: "responses",
797
792
  },
798
793
  },
@@ -10,8 +10,12 @@ function normalizeTemplateValue(value) {
10
10
  if (value == null) return "";
11
11
  if (typeof value === "string") {
12
12
  const text = value.trim();
13
- if (/^\{\{\s*[\w.-]+\s*\}\}$/.test(text)) return "";
14
- return value;
13
+ if (!text) return "";
14
+ const sanitized = text
15
+ .replace(/\{\{\s*[\w.-]+\s*\}\}/g, " ")
16
+ .replace(/[ \t]{2,}/g, " ")
17
+ .trim();
18
+ return sanitized;
15
19
  }
16
20
  if (typeof value === "number" || typeof value === "boolean") {
17
21
  return String(value);
package/agent/autofix.mjs CHANGED
@@ -545,7 +545,7 @@ export function runCodexExec(
545
545
  try {
546
546
  const parsed = new URL(baseUrl);
547
547
  const host = String(parsed.hostname || "").toLowerCase();
548
- return host === "openai.azure.com" || host.endsWith(".openai.azure.com");
548
+ return host === "openai.azure.com" || host.endsWith(".openai.azure.com") || host.endsWith(".cognitiveservices.azure.com");
549
549
  } catch {
550
550
  return false;
551
551
  }