@cortexkit/aft-opencode 0.39.1 → 0.39.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.
package/dist/index.js CHANGED
@@ -11939,6 +11939,11 @@ var LONG_RUNNING_COMMAND_TIMEOUT_MS = {
11939
11939
  function timeoutForCommand(command) {
11940
11940
  return LONG_RUNNING_COMMAND_TIMEOUT_MS[command];
11941
11941
  }
11942
+ var PASSIVE_COMMANDS = new Set(["status"]);
11943
+ var PASSIVE_COMMAND_TIMEOUT_MS = 5000;
11944
+ function isPassiveCommand(command) {
11945
+ return PASSIVE_COMMANDS.has(command);
11946
+ }
11942
11947
 
11943
11948
  // ../aft-bridge/dist/status-bar.js
11944
11949
  var STATUS_BAR_HEARTBEAT_CALLS = 15;
@@ -12016,6 +12021,11 @@ function bashTaskIdFrom(response) {
12016
12021
  function tagStderrLine(line) {
12017
12022
  return /^\[aft(-\w+)?\] /.test(line) ? line : `[aft] ${line}`;
12018
12023
  }
12024
+ var BENIGN_CPUINFO_PROC_CPUINFO_PARSE_FAILURE = "failed to parse processor information from /proc/cpuinfo";
12025
+ function shouldSurfaceStderrLine(line) {
12026
+ const normalized = line.trim();
12027
+ return !(normalized === `Error in cpuinfo: ${BENIGN_CPUINFO_PROC_CPUINFO_PARSE_FAILURE}` || normalized === BENIGN_CPUINFO_PROC_CPUINFO_PARSE_FAILURE);
12028
+ }
12019
12029
  function compareSemver(a, b) {
12020
12030
  const [aMain, aPre] = a.split("-", 2);
12021
12031
  const [bMain, bPre] = b.split("-", 2);
@@ -12107,6 +12117,7 @@ class BinaryBridge {
12107
12117
  _restartCount = 0;
12108
12118
  _shuttingDown = false;
12109
12119
  timeoutMs;
12120
+ hangThreshold;
12110
12121
  maxRestarts;
12111
12122
  configured = false;
12112
12123
  _configurePromise = null;
@@ -12131,6 +12142,7 @@ class BinaryBridge {
12131
12142
  this.binaryPath = binaryPath;
12132
12143
  this.cwd = cwd;
12133
12144
  this.timeoutMs = options?.timeoutMs ?? DEFAULT_BRIDGE_TIMEOUT_MS;
12145
+ this.hangThreshold = options?.hangThreshold ?? BRIDGE_HANG_TIMEOUT_THRESHOLD;
12134
12146
  this.maxRestarts = options?.maxRestarts ?? 3;
12135
12147
  this.configOverrides = clampSemanticTimeout(configOverrides ?? {}, this.timeoutMs);
12136
12148
  this.minVersion = options?.minVersion;
@@ -12248,7 +12260,9 @@ class BinaryBridge {
12248
12260
  if (requestSessionId && options?.configureWarningClient !== undefined) {
12249
12261
  this.configureWarningClients.set(requestSessionId, options.configureWarningClient);
12250
12262
  }
12251
- const effectiveTimeoutMs = options?.transportTimeoutMs ?? options?.timeoutMs ?? this.timeoutMs;
12263
+ const passive = isPassiveCommand(command);
12264
+ const resolvedTimeoutMs = options?.transportTimeoutMs ?? options?.timeoutMs ?? this.timeoutMs;
12265
+ const effectiveTimeoutMs = passive ? Math.min(resolvedTimeoutMs, PASSIVE_COMMAND_TIMEOUT_MS) : resolvedTimeoutMs;
12252
12266
  const implicitTransportOptions = {
12253
12267
  ...options?.transportTimeoutMs !== undefined || options?.timeoutMs !== undefined ? { transportTimeoutMs: effectiveTimeoutMs } : {},
12254
12268
  markConfiguredOnSuccess: false
@@ -12298,7 +12312,7 @@ class BinaryBridge {
12298
12312
  }
12299
12313
  const line = `${JSON.stringify(request)}
12300
12314
  `;
12301
- const keepBridgeOnTimeout = options?.keepBridgeOnTimeout === true;
12315
+ const keepBridgeOnTimeout = passive || options?.keepBridgeOnTimeout === true;
12302
12316
  let requestSentAt = Date.now();
12303
12317
  const response = await new Promise((resolve2, reject) => {
12304
12318
  const timer = setTimeout(() => {
@@ -12320,7 +12334,7 @@ class BinaryBridge {
12320
12334
  const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
12321
12335
  const consecutiveTimeouts = this.consecutiveRequestTimeouts + 1;
12322
12336
  this.consecutiveRequestTimeouts = consecutiveTimeouts;
12323
- const keepWarm = childActiveSinceRequest || consecutiveTimeouts < BRIDGE_HANG_TIMEOUT_THRESHOLD;
12337
+ const keepWarm = childActiveSinceRequest || consecutiveTimeouts < this.hangThreshold;
12324
12338
  const restartSuffix = keepWarm ? " — bridge kept warm" : " — restarting bridge";
12325
12339
  const timeoutMsg = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms${restartSuffix}`;
12326
12340
  if (requestSessionId) {
@@ -12615,7 +12629,7 @@ class BinaryBridge {
12615
12629
  `)) !== -1) {
12616
12630
  const line = this.stderrBuffer.slice(0, newlineIdx).replace(/\r$/, "");
12617
12631
  this.stderrBuffer = this.stderrBuffer.slice(newlineIdx + 1);
12618
- if (!line)
12632
+ if (!line || !shouldSurfaceStderrLine(line))
12619
12633
  continue;
12620
12634
  const tagged = tagStderrLine(line);
12621
12635
  this.logVia(tagged);
@@ -12625,7 +12639,7 @@ class BinaryBridge {
12625
12639
  flushStderrBuffer() {
12626
12640
  const line = this.stderrBuffer.replace(/\r$/, "");
12627
12641
  this.stderrBuffer = "";
12628
- if (!line)
12642
+ if (!line || !shouldSurfaceStderrLine(line))
12629
12643
  return;
12630
12644
  const tagged = tagStderrLine(line);
12631
12645
  this.logVia(tagged);
@@ -12850,6 +12864,208 @@ class BinaryBridge {
12850
12864
  }
12851
12865
  }
12852
12866
  }
12867
+ // ../aft-bridge/dist/callgraph-format.js
12868
+ import { homedir as homedir3 } from "node:os";
12869
+ var PLAIN_CALLGRAPH_THEME = {
12870
+ fg: (_role, text) => text
12871
+ };
12872
+ function asRecord(value) {
12873
+ if (!value || typeof value !== "object" || Array.isArray(value))
12874
+ return;
12875
+ return value;
12876
+ }
12877
+ function asRecords(value) {
12878
+ return Array.isArray(value) ? value.map(asRecord).filter(Boolean) : [];
12879
+ }
12880
+ function asString(value) {
12881
+ return typeof value === "string" ? value : undefined;
12882
+ }
12883
+ function asNumber(value) {
12884
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
12885
+ }
12886
+ function asBoolean(value) {
12887
+ return typeof value === "boolean" ? value : undefined;
12888
+ }
12889
+ function shortenPath(path2) {
12890
+ const home = homedir3();
12891
+ if (path2.startsWith(home))
12892
+ return `~${path2.slice(home.length)}`;
12893
+ return path2;
12894
+ }
12895
+ function joinNonEmpty(parts, separator = " · ") {
12896
+ return parts.filter((part) => Boolean(part && part.length > 0)).join(separator);
12897
+ }
12898
+ function treeLine(depth, text) {
12899
+ return `${" ".repeat(depth)}${depth === 0 ? "" : "↳ "}${text}`;
12900
+ }
12901
+ function renderCallTreeNode(node, depth, lines, theme) {
12902
+ const name = asString(node.name) ?? "(unknown)";
12903
+ const file = shortenPath(asString(node.file) ?? "(unknown file)");
12904
+ const line = asNumber(node.line);
12905
+ const unresolved = node.resolved === false ? ` ${theme.fg("warning", "[unresolved]")}` : "";
12906
+ const location = line !== undefined ? `[${file}:${line}]` : `[${file}]`;
12907
+ lines.push(treeLine(depth, `${name} ${location}${unresolved}`));
12908
+ asRecords(node.children).forEach((child) => {
12909
+ renderCallTreeNode(child, depth + 1, lines, theme);
12910
+ });
12911
+ }
12912
+ function depthWarning(response, theme, depthField = "depth_limited", truncatedField = "truncated") {
12913
+ const limited = asBoolean(response[depthField]);
12914
+ const truncated = asNumber(response[truncatedField]) ?? 0;
12915
+ if (!limited && truncated === 0)
12916
+ return "";
12917
+ const detail = truncated > 0 ? `, ${truncated} truncated` : "";
12918
+ return theme.fg("warning", `(depth limited${detail})`);
12919
+ }
12920
+ function renderTracePath(path2, index, lines) {
12921
+ lines.push(`Path ${index + 1}`);
12922
+ asRecords(path2.hops).forEach((hop, hopIndex) => {
12923
+ const symbol = asString(hop.symbol) ?? "(unknown)";
12924
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
12925
+ const line = asNumber(hop.line);
12926
+ const entry = hop.is_entry_point === true ? " [entry]" : "";
12927
+ lines.push(treeLine(hopIndex + 1, `${symbol}${entry} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}`));
12928
+ });
12929
+ }
12930
+ function renderCallersGroupLines(group, theme) {
12931
+ const file = shortenPath(asString(group.file) ?? "(unknown file)");
12932
+ const lines = [theme.fg("accent", file)];
12933
+ const callers = asRecords(group.callers);
12934
+ const bySymbol = new Map;
12935
+ for (const caller of callers) {
12936
+ const symbol = asString(caller.symbol) ?? "(unknown)";
12937
+ const line = asNumber(caller.line);
12938
+ const bucket = bySymbol.get(symbol) ?? [];
12939
+ if (line !== undefined)
12940
+ bucket.push(line);
12941
+ bySymbol.set(symbol, bucket);
12942
+ }
12943
+ const symbols = [...bySymbol.keys()].sort((a, b) => a.localeCompare(b));
12944
+ for (const symbol of symbols) {
12945
+ const lineNums = (bySymbol.get(symbol) ?? []).sort((a, b) => a - b);
12946
+ const linePart = lineNums.length > 0 ? lineNums.map(String).join(", ") : "?";
12947
+ lines.push(` ↳ ${symbol}:${linePart}`);
12948
+ }
12949
+ return lines;
12950
+ }
12951
+ function formatCallgraphSections(op, response, theme = PLAIN_CALLGRAPH_THEME) {
12952
+ const record = asRecord(response);
12953
+ if (!record)
12954
+ return [theme.fg("muted", "No navigation result.")];
12955
+ if (op === "call_tree") {
12956
+ const lines = [];
12957
+ renderCallTreeNode(record, 0, lines, theme);
12958
+ const warning = depthWarning(record, theme);
12959
+ if (warning)
12960
+ lines.push(warning);
12961
+ return lines.length > 0 ? lines : [theme.fg("muted", "No call tree available.")];
12962
+ }
12963
+ if (op === "callers") {
12964
+ const groups = asRecords(record.callers);
12965
+ const warning = depthWarning(record, theme);
12966
+ const total = asNumber(record.total_callers) ?? 0;
12967
+ const sections2 = [
12968
+ joinNonEmpty([
12969
+ theme.fg("success", `${total} caller${total === 1 ? "" : "s"}`),
12970
+ theme.fg("muted", `${groups.length} file group${groups.length === 1 ? "" : "s"}`),
12971
+ warning
12972
+ ])
12973
+ ];
12974
+ groups.forEach((group) => {
12975
+ sections2.push(renderCallersGroupLines(group, theme).join(`
12976
+ `));
12977
+ });
12978
+ return sections2;
12979
+ }
12980
+ if (op === "trace_to_symbol") {
12981
+ const path2 = asRecords(record.path);
12982
+ const complete = asBoolean(record.complete);
12983
+ const reason = asString(record.reason);
12984
+ if (path2.length === 0) {
12985
+ const prefix = complete === false ? theme.fg("warning", "No complete path") : theme.fg("muted", "No path");
12986
+ return [`${prefix}${reason ? ` (${reason})` : ""}`];
12987
+ }
12988
+ const lines = [theme.fg("success", `${path2.length} hop${path2.length === 1 ? "" : "s"}`)];
12989
+ path2.forEach((hop, index) => {
12990
+ const symbol = asString(hop.symbol) ?? "(unknown)";
12991
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
12992
+ const line = asNumber(hop.line);
12993
+ lines.push(treeLine(index + 1, `${symbol} ${line !== undefined ? `[${file}:${line}]` : `[${file}]`}`));
12994
+ });
12995
+ return lines;
12996
+ }
12997
+ if (op === "trace_to") {
12998
+ const paths = asRecords(record.paths);
12999
+ const warning = depthWarning(record, theme, "max_depth_reached", "truncated_paths");
13000
+ const totalPaths = asNumber(record.total_paths) ?? paths.length;
13001
+ const entryPoints = asNumber(record.entry_points_found) ?? 0;
13002
+ const sections2 = [
13003
+ joinNonEmpty([
13004
+ theme.fg("success", `${totalPaths} path${totalPaths === 1 ? "" : "s"}`),
13005
+ theme.fg("muted", `${entryPoints} entry point${entryPoints === 1 ? "" : "s"}`),
13006
+ warning
13007
+ ])
13008
+ ];
13009
+ if (paths.length === 0)
13010
+ sections2.push(theme.fg("muted", "No entry paths found."));
13011
+ paths.forEach((path2, index) => {
13012
+ const lines = [];
13013
+ renderTracePath(path2, index, lines);
13014
+ sections2.push(lines.join(`
13015
+ `));
13016
+ });
13017
+ return sections2;
13018
+ }
13019
+ if (op === "impact") {
13020
+ const callers = asRecords(record.callers);
13021
+ const warning = depthWarning(record, theme);
13022
+ const totalAffected = asNumber(record.total_affected) ?? callers.length;
13023
+ const affectedFiles = asNumber(record.affected_files) ?? 0;
13024
+ const sections2 = [
13025
+ joinNonEmpty([
13026
+ theme.fg("warning", `${totalAffected} affected call site${totalAffected === 1 ? "" : "s"}`),
13027
+ theme.fg("muted", `${affectedFiles} file${affectedFiles === 1 ? "" : "s"}`),
13028
+ warning
13029
+ ])
13030
+ ];
13031
+ if (callers.length === 0)
13032
+ sections2.push(theme.fg("muted", "No impacted callers found."));
13033
+ callers.forEach((caller) => {
13034
+ const file = shortenPath(asString(caller.caller_file) ?? "(unknown file)");
13035
+ const symbol = asString(caller.caller_symbol) ?? "(unknown)";
13036
+ const line = asNumber(caller.line) ?? 0;
13037
+ const entry = caller.is_entry_point === true ? ` ${theme.fg("warning", "[entry]")}` : "";
13038
+ const expression = asString(caller.call_expression);
13039
+ const params = Array.isArray(caller.parameters) ? caller.parameters.map(String).join(", ") : "";
13040
+ sections2.push([
13041
+ `${theme.fg("accent", file)}:${line}`,
13042
+ ` ↳ ${symbol}${entry}`,
13043
+ expression ? ` ${theme.fg("muted", expression)}` : undefined,
13044
+ params ? ` ${theme.fg("muted", `params: ${params}`)}` : undefined
13045
+ ].filter(Boolean).join(`
13046
+ `));
13047
+ });
13048
+ return sections2;
13049
+ }
13050
+ const hops = asRecords(record.hops);
13051
+ const sections = [
13052
+ joinNonEmpty([
13053
+ theme.fg("success", `${hops.length} hop${hops.length === 1 ? "" : "s"}`),
13054
+ asBoolean(record.depth_limited) ? theme.fg("warning", "(depth limited)") : undefined
13055
+ ])
13056
+ ];
13057
+ if (hops.length === 0)
13058
+ sections.push(theme.fg("muted", "No data-flow hops found."));
13059
+ hops.forEach((hop, index) => {
13060
+ const file = shortenPath(asString(hop.file) ?? "(unknown file)");
13061
+ const symbol = asString(hop.symbol) ?? "(unknown)";
13062
+ const variable = asString(hop.variable) ?? "(unknown)";
13063
+ const line = asNumber(hop.line) ?? 0;
13064
+ const approximate = hop.approximate === true ? ` ${theme.fg("warning", "[approx]")}` : "";
13065
+ sections.push(treeLine(index, `${variable} ${theme.fg("muted", `${asString(hop.flow_type) ?? "flow"}`)} ${symbol} [${file}:${line}]${approximate}`));
13066
+ });
13067
+ return sections;
13068
+ }
12853
13069
  // ../aft-bridge/dist/coerce.js
12854
13070
  function coerceStringArray(value) {
12855
13071
  if (Array.isArray(value)) {
@@ -12886,7 +13102,7 @@ function isEmptyParam(value) {
12886
13102
  import { spawnSync } from "node:child_process";
12887
13103
  import { createHash, randomUUID } from "node:crypto";
12888
13104
  import { chmodSync, closeSync, copyFileSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeSync } from "node:fs";
12889
- import { homedir as homedir3 } from "node:os";
13105
+ import { homedir as homedir4 } from "node:os";
12890
13106
  import { join as join3 } from "node:path";
12891
13107
  import { Readable } from "node:stream";
12892
13108
  import { pipeline } from "node:stream/promises";
@@ -12944,10 +13160,10 @@ function isExpectedCachedBinary(binaryPath, tag) {
12944
13160
  function getCacheDir() {
12945
13161
  if (process.platform === "win32") {
12946
13162
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
12947
- const base2 = localAppData || join3(homedir3(), "AppData", "Local");
13163
+ const base2 = localAppData || join3(homedir4(), "AppData", "Local");
12948
13164
  return join3(base2, "aft", "bin");
12949
13165
  }
12950
- const base = process.env.XDG_CACHE_HOME || join3(homedir3(), ".cache");
13166
+ const base = process.env.XDG_CACHE_HOME || join3(homedir4(), ".cache");
12951
13167
  return join3(base, "aft", "bin");
12952
13168
  }
12953
13169
  function getBinaryName() {
@@ -13220,7 +13436,7 @@ function stripJsoncSymbols(value) {
13220
13436
  // ../aft-bridge/dist/migration.js
13221
13437
  import { spawnSync as spawnSync2 } from "node:child_process";
13222
13438
  import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "node:fs";
13223
- import { homedir as homedir5, tmpdir } from "node:os";
13439
+ import { homedir as homedir6, tmpdir } from "node:os";
13224
13440
  import { dirname as dirname2, join as join6 } from "node:path";
13225
13441
 
13226
13442
  // ../aft-bridge/dist/paths.js
@@ -13277,7 +13493,7 @@ function markAnnouncementSeen(storageRoot, harness, currentVersion) {
13277
13493
  import { execSync } from "node:child_process";
13278
13494
  import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync as openSync2, readSync, renameSync as renameSync3, unlinkSync as unlinkSync2 } from "node:fs";
13279
13495
  import { createRequire as createRequire2 } from "node:module";
13280
- import { homedir as homedir4 } from "node:os";
13496
+ import { homedir as homedir5 } from "node:os";
13281
13497
  import { join as join5 } from "node:path";
13282
13498
  var ensureBinaryForResolver = ensureBinary;
13283
13499
  function copyToVersionedCache(npmBinaryPath, knownVersion) {
@@ -13319,7 +13535,7 @@ function normalizeBareVersion(version) {
13319
13535
  return version.startsWith("v") ? version.slice(1) : version;
13320
13536
  }
13321
13537
  function homeDirFromEnv(env) {
13322
- return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir4();
13538
+ return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir5();
13323
13539
  }
13324
13540
  function cacheDirFromEnv(env) {
13325
13541
  if (process.platform === "win32") {
@@ -13501,8 +13717,8 @@ function dataHome() {
13501
13717
  }
13502
13718
  function homeDir() {
13503
13719
  if (process.platform === "win32")
13504
- return process.env.USERPROFILE || process.env.HOME || homedir5();
13505
- return process.env.HOME || homedir5();
13720
+ return process.env.USERPROFILE || process.env.HOME || homedir6();
13721
+ return process.env.HOME || homedir6();
13506
13722
  }
13507
13723
  function resolveLegacyStorageRoot(harness) {
13508
13724
  if (harness === "pi")
@@ -13592,13 +13808,13 @@ async function ensureStorageMigrated(opts) {
13592
13808
  }
13593
13809
  // ../aft-bridge/dist/npm-resolver.js
13594
13810
  import { readdirSync, statSync as statSync2 } from "node:fs";
13595
- import { homedir as homedir6 } from "node:os";
13811
+ import { homedir as homedir7 } from "node:os";
13596
13812
  import { delimiter, dirname as dirname3, isAbsolute as isAbsolute2, join as join7 } from "node:path";
13597
13813
  function defaultDeps() {
13598
13814
  return {
13599
13815
  platform: process.platform,
13600
13816
  env: process.env,
13601
- home: homedir6(),
13817
+ home: homedir7(),
13602
13818
  execPath: process.execPath
13603
13819
  };
13604
13820
  }
@@ -14961,6 +15177,7 @@ class BridgePool {
14961
15177
  this.projectConfigLoader = options.projectConfigLoader;
14962
15178
  this.bridgeOptions = {
14963
15179
  timeoutMs: options.timeoutMs,
15180
+ hangThreshold: options.hangThreshold,
14964
15181
  maxRestarts: options.maxRestarts,
14965
15182
  minVersion: options.minVersion,
14966
15183
  onVersionMismatch: options.onVersionMismatch,
@@ -15364,9 +15581,6 @@ function write(level, message, data, sessionId) {
15364
15581
  function log2(message, data) {
15365
15582
  write("INFO", message, data);
15366
15583
  }
15367
- function debug(message, data) {
15368
- write("DEBUG", message, data);
15369
- }
15370
15584
  function warn2(message, data) {
15371
15585
  write("WARN", message, data);
15372
15586
  }
@@ -15376,9 +15590,6 @@ function error2(message, data) {
15376
15590
  function sessionLog(sessionId, message, data) {
15377
15591
  write("INFO", message, data, sessionId);
15378
15592
  }
15379
- function sessionDebug(sessionId, message, data) {
15380
- write("DEBUG", message, data, sessionId);
15381
- }
15382
15593
  function sessionWarn(sessionId, message, data) {
15383
15594
  write("WARN", message, data, sessionId);
15384
15595
  }
@@ -15472,84 +15683,6 @@ async function resolvePromptContext(client, sessionId) {
15472
15683
  return result;
15473
15684
  }
15474
15685
 
15475
- // src/shared/live-server-client.ts
15476
- import { createOpencodeClient } from "@opencode-ai/sdk";
15477
- var clientCache = new Map;
15478
- function cacheKey(serverUrl, directory) {
15479
- return `${serverUrl}|${directory}`;
15480
- }
15481
- function normalizeServerUrl(serverUrl) {
15482
- try {
15483
- return new URL(serverUrl).toString();
15484
- } catch {
15485
- return serverUrl;
15486
- }
15487
- }
15488
- function serverAuthHeaders() {
15489
- const password = process.env.OPENCODE_SERVER_PASSWORD;
15490
- if (!password)
15491
- return;
15492
- const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
15493
- return {
15494
- Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
15495
- };
15496
- }
15497
- function getLiveServerClient(serverUrl, directory) {
15498
- const key = cacheKey(serverUrl, directory);
15499
- const cached = clientCache.get(key);
15500
- if (cached)
15501
- return cached;
15502
- const client = createOpencodeClient({
15503
- baseUrl: serverUrl,
15504
- directory,
15505
- headers: serverAuthHeaders(),
15506
- fetch: globalThis.fetch
15507
- });
15508
- clientCache.set(key, client);
15509
- return client;
15510
- }
15511
- var liveServerWakeAvailableByServerUrl = new Map;
15512
- var legacyLiveServerWakeAvailable = false;
15513
- async function probeServerReachable(serverUrl, timeoutMs = 1500) {
15514
- if (!serverUrl)
15515
- return false;
15516
- const normalizedServerUrl = normalizeServerUrl(serverUrl);
15517
- const controller = new AbortController;
15518
- const timer = setTimeout(() => controller.abort(), timeoutMs);
15519
- let reachable = false;
15520
- try {
15521
- const probeUrl = new URL("/session", serverUrl).toString();
15522
- const res = await globalThis.fetch(probeUrl, {
15523
- method: "GET",
15524
- headers: serverAuthHeaders(),
15525
- signal: controller.signal
15526
- });
15527
- reachable = res.ok || res.status === 401 || res.status === 403;
15528
- } catch {
15529
- reachable = false;
15530
- } finally {
15531
- clearTimeout(timer);
15532
- liveServerWakeAvailableByServerUrl.set(normalizedServerUrl, reachable);
15533
- }
15534
- return reachable;
15535
- }
15536
- function setLiveServerWakeAvailable(serverUrlOrAvailable, available) {
15537
- if (typeof serverUrlOrAvailable === "boolean") {
15538
- legacyLiveServerWakeAvailable = serverUrlOrAvailable;
15539
- return;
15540
- }
15541
- if (!serverUrlOrAvailable) {
15542
- legacyLiveServerWakeAvailable = available ?? false;
15543
- return;
15544
- }
15545
- liveServerWakeAvailableByServerUrl.set(normalizeServerUrl(serverUrlOrAvailable), available ?? false);
15546
- }
15547
- function useLiveServerWake(serverUrl) {
15548
- if (!serverUrl)
15549
- return legacyLiveServerWakeAvailable;
15550
- return liveServerWakeAvailableByServerUrl.get(normalizeServerUrl(serverUrl)) ?? false;
15551
- }
15552
-
15553
15686
  // src/bg-notifications.ts
15554
15687
  function hashReminder(text) {
15555
15688
  return createHash3("sha256").update(text).digest("hex").slice(0, 16);
@@ -15776,18 +15909,18 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15776
15909
  directory: drainContext.directory,
15777
15910
  attempt: state.wakeRetryAttempts + 1
15778
15911
  });
15779
- throw new Error("no wake transport available: live-server unreachable and input.client absent");
15912
+ throw new Error("in-process wake client unavailable: input.client absent");
15780
15913
  }
15781
15914
  return drainContext.client;
15782
15915
  };
15783
- const sendPrompt = async (client, clientPath) => {
15784
- if (typeof client.session?.promptAsync !== "function") {
15785
- throw new Error(`wake client.session.promptAsync is unavailable (path=${clientPath})`);
15916
+ const sendPrompt = async (client2) => {
15917
+ if (typeof client2.session?.promptAsync !== "function") {
15918
+ throw new Error("wake client.session.promptAsync is unavailable");
15786
15919
  }
15787
- const promptContext = await resolvePromptContext(client, drainContext.sessionID);
15920
+ const promptContext = await resolvePromptContext(client2, drainContext.sessionID);
15788
15921
  const body = {
15789
15922
  noReply: false,
15790
- parts: [{ type: "text", text: reminder }]
15923
+ parts: [{ type: "text", text: reminder, synthetic: true }]
15791
15924
  };
15792
15925
  if (promptContext?.agent)
15793
15926
  body.agent = promptContext.agent;
@@ -15807,7 +15940,6 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15807
15940
  directory: drainContext.directory,
15808
15941
  reminder_sha256: hashReminder(reminder),
15809
15942
  reminder_chars: reminder.length,
15810
- wake_client_path: clientPath,
15811
15943
  prompt_context: promptContext ? {
15812
15944
  agent: promptContext.agent,
15813
15945
  model: promptContext.model ? {
@@ -15822,18 +15954,16 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15822
15954
  ...wakeMeta
15823
15955
  });
15824
15956
  try {
15825
- await client.session.promptAsync({
15957
+ await client2.session.promptAsync({
15826
15958
  path: { id: drainContext.sessionID },
15827
15959
  body
15828
15960
  });
15829
15961
  } catch (err) {
15830
- const logPromptError = clientPath === "live-server" ? sessionDebug : sessionWarn;
15831
- logPromptError(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync error`, {
15962
+ sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync error`, {
15832
15963
  event: "bash_completion_wake_prompt_async_error",
15833
15964
  delivery_id: deliveryID2,
15834
15965
  attempt: state.wakeRetryAttempts + 1,
15835
15966
  task_ids: taskIDs,
15836
- wake_client_path: clientPath,
15837
15967
  error: err instanceof Error ? err.message : String(err)
15838
15968
  });
15839
15969
  throw err;
@@ -15842,38 +15972,12 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
15842
15972
  event: "bash_completion_wake_prompt_async_ok",
15843
15973
  delivery_id: deliveryID2,
15844
15974
  attempt: state.wakeRetryAttempts + 1,
15845
- task_ids: taskIDs,
15846
- wake_client_path: clientPath
15975
+ task_ids: taskIDs
15847
15976
  });
15848
15977
  return deliveryID2;
15849
15978
  };
15850
- if (useLiveServerWake(drainContext.serverUrl) && drainContext.serverUrl) {
15851
- try {
15852
- const liveClient = getLiveServerClient(drainContext.serverUrl, drainContext.directory);
15853
- const deliveryID2 = await sendPrompt(liveClient, "live-server");
15854
- await ackCompletions(drainContext, deliveredCompletions, deliveryID2);
15855
- return;
15856
- } catch (err) {
15857
- setLiveServerWakeAvailable(drainContext.serverUrl, false);
15858
- sessionDebug(drainContext.sessionID, `${LOG_PREFIX} live-server wake failed; falling back`, {
15859
- event: "bash_completion_wake_live_server_fallback",
15860
- task_ids: taskIDs,
15861
- directory: drainContext.directory,
15862
- server_url: drainContext.serverUrl,
15863
- attempt: state.wakeRetryAttempts + 1,
15864
- error: err instanceof Error ? err.message : String(err)
15865
- });
15866
- const fallbackClient2 = getInProcessClient();
15867
- const deliveryID2 = await sendPrompt(fallbackClient2, "in-process-fallback");
15868
- state.retryDelayMs = null;
15869
- state.wakeRetryAttempts = 0;
15870
- state.wakeHardStopped = false;
15871
- await ackCompletions(drainContext, deliveredCompletions, deliveryID2);
15872
- return;
15873
- }
15874
- }
15875
- const fallbackClient = getInProcessClient();
15876
- const deliveryID = await sendPrompt(fallbackClient, "in-process-fallback");
15979
+ const client = getInProcessClient();
15980
+ const deliveryID = await sendPrompt(client);
15877
15981
  await ackCompletions(drainContext, deliveredCompletions, deliveryID);
15878
15982
  }, (err, hardStopped) => {
15879
15983
  sessionWarn(drainContext.sessionID, hardStopped ? `${LOG_PREFIX} wake send failed ${MAX_WAKE_SEND_ATTEMPTS} times; stopping retries: ${err instanceof Error ? err.message : String(err)}` : `${LOG_PREFIX} wake send failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -16256,7 +16360,7 @@ function formatDuration(completion) {
16256
16360
 
16257
16361
  // src/config.ts
16258
16362
  import { existsSync as existsSync6, readFileSync as readFileSync4, renameSync as renameSync4, unlinkSync as unlinkSync4, writeFileSync as writeFileSync3 } from "node:fs";
16259
- import { homedir as homedir7 } from "node:os";
16363
+ import { homedir as homedir8 } from "node:os";
16260
16364
  import { join as join10 } from "node:path";
16261
16365
  var import_comment_json = __toESM(require_src2(), 1);
16262
16366
 
@@ -29871,6 +29975,10 @@ var BashFeaturesSchema = exports_external.object({
29871
29975
  foreground_wait_window_ms: exports_external.number().int().positive().optional()
29872
29976
  });
29873
29977
  var BashConfigSchema = exports_external.union([exports_external.boolean(), BashFeaturesSchema]);
29978
+ var BridgeConfigSchema = exports_external.object({
29979
+ request_timeout_ms: exports_external.number().int().min(1000, { message: "bridge.request_timeout_ms must be at least 1000" }).optional(),
29980
+ hang_threshold: exports_external.number().int().min(1, { message: "bridge.hang_threshold must be at least 1" }).optional()
29981
+ });
29874
29982
  var InspectConfigSchema = exports_external.object({
29875
29983
  enabled: exports_external.boolean().optional(),
29876
29984
  tier2_idle_minutes: exports_external.number().min(0).optional(),
@@ -29903,6 +30011,8 @@ var AftConfigSchema = exports_external.object({
29903
30011
  restrict_to_project_root: exports_external.boolean().optional(),
29904
30012
  search_index: exports_external.boolean().optional(),
29905
30013
  semantic_search: exports_external.boolean().optional(),
30014
+ callgraph_store: exports_external.boolean().optional(),
30015
+ callgraph_chunk_size: exports_external.number().optional(),
29906
30016
  inspect: InspectConfigSchema.optional(),
29907
30017
  bash: BashConfigSchema.optional(),
29908
30018
  experimental: ExperimentalConfigSchema.optional(),
@@ -29910,7 +30020,8 @@ var AftConfigSchema = exports_external.object({
29910
30020
  url_fetch_allow_private: exports_external.boolean().optional(),
29911
30021
  semantic: SemanticConfigSchema.optional(),
29912
30022
  max_callgraph_files: exports_external.number().int().positive().optional(),
29913
- auto_update: exports_external.boolean().optional()
30023
+ auto_update: exports_external.boolean().optional(),
30024
+ bridge: BridgeConfigSchema.optional()
29914
30025
  }).strict();
29915
30026
  function normalizeLspExtension(extension) {
29916
30027
  return extension.trim().replace(/^\.+/, "");
@@ -29980,6 +30091,10 @@ function resolveProjectOverridesForConfigure(config2) {
29980
30091
  overrides.search_index = config2.search_index;
29981
30092
  if (config2.semantic_search !== undefined)
29982
30093
  overrides.semantic_search = config2.semantic_search;
30094
+ if (config2.callgraph_store !== undefined)
30095
+ overrides.callgraph_store = config2.callgraph_store;
30096
+ if (config2.callgraph_chunk_size !== undefined)
30097
+ overrides.callgraph_chunk_size = config2.callgraph_chunk_size;
29983
30098
  Object.assign(overrides, resolveExperimentalConfigForConfigure(config2));
29984
30099
  Object.assign(overrides, resolveLspConfigForConfigure(config2));
29985
30100
  if (config2.semantic !== undefined)
@@ -30254,6 +30369,17 @@ function parseConfigPartially(rawConfig) {
30254
30369
  }
30255
30370
  return partialConfig;
30256
30371
  }
30372
+ var configLoadErrors = [];
30373
+ function getConfigLoadErrors() {
30374
+ return configLoadErrors;
30375
+ }
30376
+ function formatConfigParseFailureMessage(configPath, errorMessage) {
30377
+ return `AFT config at ${configPath} failed to parse and was ignored (running on defaults): ${errorMessage}. ` + "Fix the syntax or run `npx @cortexkit/aft doctor`.";
30378
+ }
30379
+ function recordConfigParseFailure(configPath, errorMessage) {
30380
+ configLoadErrors.push({ path: configPath, message: errorMessage });
30381
+ warn2(formatConfigParseFailureMessage(configPath, errorMessage));
30382
+ }
30257
30383
  function loadConfigFromPath(configPath) {
30258
30384
  try {
30259
30385
  if (!existsSync6(configPath)) {
@@ -30274,6 +30400,7 @@ function loadConfigFromPath(configPath) {
30274
30400
  } catch (err) {
30275
30401
  const errorMsg = err instanceof Error ? err.message : String(err);
30276
30402
  error2(`Error loading config from ${configPath}: ${errorMsg}`);
30403
+ recordConfigParseFailure(configPath, errorMsg);
30277
30404
  return null;
30278
30405
  }
30279
30406
  }
@@ -30397,6 +30524,8 @@ var PROJECT_SAFE_TOP_LEVEL_FIELDS = new Set([
30397
30524
  "configure_warnings_delivery",
30398
30525
  "search_index",
30399
30526
  "semantic_search",
30527
+ "callgraph_store",
30528
+ "callgraph_chunk_size",
30400
30529
  "inspect",
30401
30530
  "experimental",
30402
30531
  "bash"
@@ -30420,6 +30549,8 @@ function getStrippedTopLevelKeys(override) {
30420
30549
  stripped.push("max_callgraph_files");
30421
30550
  if (override.auto_update !== undefined)
30422
30551
  stripped.push("auto_update");
30552
+ if (override.bridge !== undefined)
30553
+ stripped.push("bridge");
30423
30554
  return stripped;
30424
30555
  }
30425
30556
  function mergeConfigs(base, override) {
@@ -30431,6 +30562,7 @@ function mergeConfigs(base, override) {
30431
30562
  const experimental = mergeExperimentalConfig(base.experimental, override.experimental);
30432
30563
  const bash = mergeBashConfig(base.bash, override.bash);
30433
30564
  const inspect = mergeInspectConfig(base.inspect, override.inspect);
30565
+ const bridge = base.bridge;
30434
30566
  const safeOverride = pickProjectSafeFields(override);
30435
30567
  delete safeOverride.bash;
30436
30568
  delete safeOverride.inspect;
@@ -30444,18 +30576,28 @@ function mergeConfigs(base, override) {
30444
30576
  ...inspect !== undefined ? { inspect } : {},
30445
30577
  experimental,
30446
30578
  semantic,
30579
+ ...bridge !== undefined ? { bridge } : {},
30447
30580
  ...disabledTools.length > 0 ? { disabled_tools: [...new Set(disabledTools)] } : {}
30448
30581
  };
30449
30582
  }
30583
+ var DEFAULT_BRIDGE_REQUEST_TIMEOUT_MS = 30000;
30584
+ var DEFAULT_BRIDGE_HANG_THRESHOLD = 2;
30585
+ function resolveBridgePoolTransportOptions(config2) {
30586
+ return {
30587
+ timeoutMs: config2.bridge?.request_timeout_ms ?? DEFAULT_BRIDGE_REQUEST_TIMEOUT_MS,
30588
+ hangThreshold: config2.bridge?.hang_threshold ?? DEFAULT_BRIDGE_HANG_THRESHOLD
30589
+ };
30590
+ }
30450
30591
  function getOpenCodeConfigDir() {
30451
30592
  const envDir = process.env.OPENCODE_CONFIG_DIR?.trim();
30452
30593
  if (envDir) {
30453
30594
  return envDir;
30454
30595
  }
30455
- const xdgConfig = process.env.XDG_CONFIG_HOME || join10(homedir7(), ".config");
30596
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join10(homedir8(), ".config");
30456
30597
  return join10(xdgConfig, "opencode");
30457
30598
  }
30458
30599
  function loadAftConfig(projectDirectory) {
30600
+ configLoadErrors = [];
30459
30601
  const configDir = getOpenCodeConfigDir();
30460
30602
  const userBasePath = join10(configDir, "aft");
30461
30603
  migrateAftConfigFile(`${userBasePath}.jsonc`);
@@ -30488,7 +30630,7 @@ function loadAftConfig(projectDirectory) {
30488
30630
 
30489
30631
  // src/notifications.ts
30490
30632
  import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
30491
- import { homedir as homedir8, platform } from "node:os";
30633
+ import { homedir as homedir9, platform } from "node:os";
30492
30634
  import { join as join11 } from "node:path";
30493
30635
  function isTuiMode() {
30494
30636
  return process.env.OPENCODE_CLIENT === "cli";
@@ -30510,7 +30652,7 @@ var WARNING_MARKER = `${AFT_MARKER} ⚠️`;
30510
30652
  var STATUS_MARKER = `${AFT_MARKER} ✅`;
30511
30653
  function getDesktopStatePath() {
30512
30654
  const os3 = platform();
30513
- const home = homedir8();
30655
+ const home = homedir9();
30514
30656
  if (os3 === "darwin") {
30515
30657
  return join11(home, "Library", "Application Support", "ai.opencode.desktop", "opencode.global.dat");
30516
30658
  }
@@ -30789,6 +30931,8 @@ function warningTitle(warning) {
30789
30931
  return "Checker is not installed";
30790
30932
  case "lsp_binary_missing":
30791
30933
  return "LSP binary is missing";
30934
+ case "config_parse_failed":
30935
+ return "Config failed to parse";
30792
30936
  }
30793
30937
  }
30794
30938
  function formatConfigureWarningLine(warning) {
@@ -30910,12 +31054,36 @@ async function cleanupWarnings(opts) {
30910
31054
 
30911
31055
  // src/configure-warnings.ts
30912
31056
  var pendingEagerWarnings = new Map;
31057
+ var pendingConfigParseWarnings = new Map;
30913
31058
  var pendingBySession = new Map;
30914
31059
  function isConfigureWarning(value) {
30915
31060
  if (!value || typeof value !== "object" || Array.isArray(value))
30916
31061
  return false;
30917
31062
  const warning = value;
30918
- return (warning.kind === "formatter_not_installed" || warning.kind === "checker_not_installed" || warning.kind === "lsp_binary_missing") && typeof warning.hint === "string";
31063
+ return (warning.kind === "formatter_not_installed" || warning.kind === "checker_not_installed" || warning.kind === "lsp_binary_missing" || warning.kind === "config_parse_failed") && typeof warning.hint === "string";
31064
+ }
31065
+ function configParseWarningsFromErrors(errors3) {
31066
+ return errors3.map((entry) => ({
31067
+ kind: "config_parse_failed",
31068
+ hint: formatConfigParseFailureMessage(entry.path, entry.message)
31069
+ }));
31070
+ }
31071
+ function enqueueConfigParseWarnings(projectRoot, errors3) {
31072
+ if (!projectRoot || errors3.length === 0)
31073
+ return;
31074
+ const incoming = configParseWarningsFromErrors(errors3);
31075
+ const existing = pendingConfigParseWarnings.get(projectRoot) ?? [];
31076
+ for (const warning of incoming) {
31077
+ if (!existing.some((item) => item.hint === warning.hint)) {
31078
+ existing.push(warning);
31079
+ }
31080
+ }
31081
+ pendingConfigParseWarnings.set(projectRoot, existing);
31082
+ }
31083
+ function drainPendingConfigParseWarnings(projectRoot) {
31084
+ const pending = pendingConfigParseWarnings.get(projectRoot) ?? [];
31085
+ pendingConfigParseWarnings.delete(projectRoot);
31086
+ return pending;
30919
31087
  }
30920
31088
  function coerceConfigureWarnings(warnings) {
30921
31089
  return warnings.filter(isConfigureWarning);
@@ -30926,7 +31094,10 @@ function drainPendingEagerWarnings(projectRoot) {
30926
31094
  return pending;
30927
31095
  }
30928
31096
  function enqueueConfigureWarningsForSession(context) {
30929
- const validWarnings = coerceConfigureWarnings(context.warnings);
31097
+ const validWarnings = [
31098
+ ...drainPendingConfigParseWarnings(context.projectRoot),
31099
+ ...coerceConfigureWarnings(context.warnings)
31100
+ ];
30930
31101
  if (!context.sessionId) {
30931
31102
  if (validWarnings.length === 0)
30932
31103
  return;
@@ -30996,27 +31167,27 @@ var import_comment_json3 = __toESM(require_src2(), 1);
30996
31167
  // src/hooks/auto-update-checker/checker.ts
30997
31168
  var import_comment_json2 = __toESM(require_src2(), 1);
30998
31169
  import { existsSync as existsSync8, readFileSync as readFileSync6, statSync as statSync4, writeFileSync as writeFileSync4 } from "node:fs";
30999
- import { homedir as homedir10 } from "node:os";
31170
+ import { homedir as homedir11 } from "node:os";
31000
31171
  import { dirname as dirname5, isAbsolute as isAbsolute4, join as join13, resolve as resolve3 } from "node:path";
31001
31172
  import { fileURLToPath } from "node:url";
31002
31173
 
31003
31174
  // src/hooks/auto-update-checker/constants.ts
31004
- import { homedir as homedir9, platform as platform2 } from "node:os";
31175
+ import { homedir as homedir10, platform as platform2 } from "node:os";
31005
31176
  import { join as join12 } from "node:path";
31006
31177
  var PACKAGE_NAME = "@cortexkit/aft-opencode";
31007
31178
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
31008
31179
  var NPM_FETCH_TIMEOUT = 1e4;
31009
31180
  function getOpenCodeCacheRoot() {
31010
31181
  if (platform2() === "win32") {
31011
- return join12(process.env.LOCALAPPDATA ?? homedir9(), "opencode");
31182
+ return join12(process.env.LOCALAPPDATA ?? homedir10(), "opencode");
31012
31183
  }
31013
- return join12(homedir9(), ".cache", "opencode");
31184
+ return join12(homedir10(), ".cache", "opencode");
31014
31185
  }
31015
31186
  function getOpenCodeConfigRoot() {
31016
31187
  if (platform2() === "win32") {
31017
- return join12(process.env.APPDATA ?? join12(homedir9(), "AppData", "Roaming"), "opencode");
31188
+ return join12(process.env.APPDATA ?? join12(homedir10(), "AppData", "Roaming"), "opencode");
31018
31189
  }
31019
- return join12(process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config"), "opencode");
31190
+ return join12(process.env.XDG_CONFIG_HOME ?? join12(homedir10(), ".config"), "opencode");
31020
31191
  }
31021
31192
  var CACHE_DIR = join12(getOpenCodeCacheRoot(), "packages");
31022
31193
  var USER_OPENCODE_CONFIG = join12(getOpenCodeConfigRoot(), "opencode.json");
@@ -31195,7 +31366,7 @@ function getCachedVersion(spec) {
31195
31366
  getCurrentRuntimePackageJsonPath(),
31196
31367
  spec ? getSpecCachePackageJsonPath(spec) : null,
31197
31368
  getSpecCachePackageJsonPath(`${PACKAGE_NAME}@latest`),
31198
- join13(homedir10(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
31369
+ join13(homedir11(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
31199
31370
  ].filter(isString);
31200
31371
  for (const packageJsonPath of candidates) {
31201
31372
  try {
@@ -31698,7 +31869,7 @@ import {
31698
31869
  unlinkSync as unlinkSync5,
31699
31870
  writeFileSync as writeFileSync7
31700
31871
  } from "node:fs";
31701
- import { homedir as homedir11 } from "node:os";
31872
+ import { homedir as homedir12 } from "node:os";
31702
31873
  import { join as join15 } from "node:path";
31703
31874
  function aftCacheBase() {
31704
31875
  const override = process.env.AFT_CACHE_DIR;
@@ -31706,10 +31877,10 @@ function aftCacheBase() {
31706
31877
  return override;
31707
31878
  if (process.platform === "win32") {
31708
31879
  const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA;
31709
- const base2 = localAppData || join15(homedir11(), "AppData", "Local");
31880
+ const base2 = localAppData || join15(homedir12(), "AppData", "Local");
31710
31881
  return join15(base2, "aft");
31711
31882
  }
31712
- const base = process.env.XDG_CACHE_HOME || join15(homedir11(), ".cache");
31883
+ const base = process.env.XDG_CACHE_HOME || join15(homedir12(), ".cache");
31713
31884
  return join15(base, "aft");
31714
31885
  }
31715
31886
  function lspCacheRoot() {
@@ -33792,7 +33963,7 @@ async function verifySessionDirectory(client, sessionId) {
33792
33963
  }
33793
33964
 
33794
33965
  // src/shared/status.ts
33795
- function asRecord(value) {
33966
+ function asRecord2(value) {
33796
33967
  return typeof value === "object" && value !== null ? value : {};
33797
33968
  }
33798
33969
  function readString(value, fallback = "") {
@@ -33811,7 +33982,7 @@ function readOptionalNumber(value) {
33811
33982
  return typeof value === "number" && Number.isFinite(value) ? value : null;
33812
33983
  }
33813
33984
  function readCompressionAggregate(value) {
33814
- const aggregate = asRecord(value);
33985
+ const aggregate = asRecord2(value);
33815
33986
  return {
33816
33987
  events: readNumber(aggregate.events),
33817
33988
  original_tokens: readNumber(aggregate.original_tokens),
@@ -33822,7 +33993,7 @@ function readCompressionAggregate(value) {
33822
33993
  function readCompression(value) {
33823
33994
  if (typeof value !== "object" || value === null)
33824
33995
  return;
33825
- const compression = asRecord(value);
33996
+ const compression = asRecord2(value);
33826
33997
  return {
33827
33998
  project: readCompressionAggregate(compression.project),
33828
33999
  session: readCompressionAggregate(compression.session)
@@ -33831,7 +34002,7 @@ function readCompression(value) {
33831
34002
  function readStatusBar(value) {
33832
34003
  if (typeof value !== "object" || value === null)
33833
34004
  return;
33834
- const bar = asRecord(value);
34005
+ const bar = asRecord2(value);
33835
34006
  return {
33836
34007
  errors: readNumber(bar.errors),
33837
34008
  warnings: readNumber(bar.warnings),
@@ -33875,16 +34046,16 @@ function formatBytes(bytes) {
33875
34046
  return `${value.toFixed(decimals)} ${units[unitIndex]}`;
33876
34047
  }
33877
34048
  function coerceAftStatus(response) {
33878
- const features = asRecord(response.features);
33879
- const searchIndex = asRecord(response.search_index);
33880
- const semanticIndex = asRecord(response.semantic_index);
34049
+ const features = asRecord2(response.features);
34050
+ const searchIndex = asRecord2(response.search_index);
34051
+ const semanticIndex = asRecord2(response.semantic_index);
33881
34052
  const semanticConfig = {
33882
- ...asRecord(response.semantic),
33883
- ...asRecord(response.semantic_config)
34053
+ ...asRecord2(response.semantic),
34054
+ ...asRecord2(response.semantic_config)
33884
34055
  };
33885
- const disk = asRecord(response.disk);
33886
- const symbolCache = asRecord(response.symbol_cache);
33887
- const session = asRecord(response.session);
34056
+ const disk = asRecord2(response.disk);
34057
+ const symbolCache = asRecord2(response.symbol_cache);
34058
+ const session = asRecord2(response.session);
33888
34059
  return {
33889
34060
  version: readString(response.version, "unknown"),
33890
34061
  project_root: readNullableString(response.project_root),
@@ -34006,7 +34177,7 @@ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as r
34006
34177
  import { dirname as dirname10, join as join22 } from "node:path";
34007
34178
 
34008
34179
  // src/shared/opencode-config-dir.ts
34009
- import { homedir as homedir12 } from "node:os";
34180
+ import { homedir as homedir13 } from "node:os";
34010
34181
  import { join as join21, resolve as resolve5 } from "node:path";
34011
34182
  function getCliConfigDir() {
34012
34183
  const envConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
@@ -34014,9 +34185,9 @@ function getCliConfigDir() {
34014
34185
  return resolve5(envConfigDir);
34015
34186
  }
34016
34187
  if (process.platform === "win32") {
34017
- return join21(homedir12(), ".config", "opencode");
34188
+ return join21(homedir13(), ".config", "opencode");
34018
34189
  }
34019
- return join21(process.env.XDG_CONFIG_HOME || join21(homedir12(), ".config"), "opencode");
34190
+ return join21(process.env.XDG_CONFIG_HOME || join21(homedir13(), ".config"), "opencode");
34020
34191
  }
34021
34192
  function getOpenCodeConfigDir2(_options) {
34022
34193
  return getCliConfigDir();
@@ -34856,18 +35027,43 @@ function resolveForegroundWaitMs(configured) {
34856
35027
  function bashToolDescription(aftSearchRegistered, compressionOn, backgroundOn) {
34857
35028
  const searchSteer = aftSearchRegistered ? "use aft_search (concepts, identifiers, regex, literals), read, aft_outline, or aft_zoom instead" : "use the grep tool, read, aft_outline, or aft_zoom instead";
34858
35029
  const compression = compressionOn ? " Output is compressed by default; pass compressed: false for raw output." : "";
34859
- const tasks = backgroundOn ? ' Pass background: true to run in the background and get a taskId for bash_status/bash_kill. Pass pty: true for interactive programs (REPLs, TUIs) and drive them with bash_status({ outputMode: "screen" }) plus bash_write (pty implies background automatically).' : " Commands that outlive the foreground wait window are promoted to background tasks; inspect them with bash_status({ taskId }) or terminate with bash_kill.";
34860
- return `Execute shell commands.${compression}${tasks} Use bash_watch to wait for output patterns or exit events.
35030
+ const tasks = backgroundOn ? ' Pass background: true to run in the background and get a taskId for bash_status/bash_watch/bash_kill. Pass pty: true for interactive programs (REPLs, TUIs) and drive them with bash_status({ outputMode: "screen" }) plus bash_write (pty implies background automatically). Use bash_watch to wait for exit or output patterns (sync blocks, async notifies). Do not loop bash_status to wait.' : " Commands run in the foreground to completion; timeout is the hard kill cap (default 30 minutes).";
35031
+ return `Execute shell commands.${compression}${tasks}
34861
35032
 
34862
35033
  DO NOT use bash for code search or code exploration. If you are about to run grep, rg, sed, awk, find, or cat through bash to locate or read code: STOP — ${searchSteer}.`;
34863
35034
  }
35035
+ function pushUnique(target, values) {
35036
+ for (const value of values) {
35037
+ if (!target.includes(value))
35038
+ target.push(value);
35039
+ }
35040
+ }
35041
+ function groupBashPermissionAsks(asks) {
35042
+ const grouped = [];
35043
+ let bashAsk;
35044
+ for (const ask of asks) {
35045
+ if (ask.kind === "bash") {
35046
+ if (!bashAsk) {
35047
+ bashAsk = { kind: "bash", patterns: [], always: [] };
35048
+ grouped.push(bashAsk);
35049
+ }
35050
+ pushUnique(bashAsk.patterns, ask.patterns);
35051
+ pushUnique(bashAsk.always, ask.always);
35052
+ continue;
35053
+ }
35054
+ grouped.push(ask);
35055
+ }
35056
+ return grouped;
35057
+ }
35058
+ function permissionsGrantedForRetry(asks) {
35059
+ return asks.flatMap((ask) => ask.always.length > 0 ? ask.always : ask.patterns);
35060
+ }
34864
35061
  async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
34865
35062
  const first = await bridgeCall(ctx, runtime, "bash", params, options);
34866
35063
  if (first.success !== false || first.code !== "permission_required")
34867
35064
  return first;
34868
35065
  const asks = Array.isArray(first.asks) ? first.asks : [];
34869
- const permissionsGranted = [];
34870
- for (const ask of asks) {
35066
+ for (const ask of groupBashPermissionAsks(asks)) {
34871
35067
  const permission = ask.kind === "external_directory" ? "external_directory" : "bash";
34872
35068
  await runAsk(runtime.ask({
34873
35069
  permission,
@@ -34875,54 +35071,60 @@ async function withPermissionLoop(ctx, runtime, params, bridgeCall, options) {
34875
35071
  always: ask.always,
34876
35072
  metadata: {}
34877
35073
  }));
34878
- permissionsGranted.push(...ask.always.length > 0 ? ask.always : ask.patterns);
34879
35074
  }
34880
- const second = await bridgeCall(ctx, runtime, "bash", { ...params, permissions_granted: permissionsGranted }, options);
35075
+ const second = await bridgeCall(ctx, runtime, "bash", { ...params, permissions_granted: permissionsGrantedForRetry(asks) }, options);
34881
35076
  if (second.success === false && second.code === "permission_required") {
34882
35077
  throw new Error("bash permission retry failed");
34883
35078
  }
34884
35079
  return second;
34885
35080
  }
34886
35081
  function createBashTool(ctx, aftSearchRegisteredOverride) {
35082
+ const initialBashCfg = resolveBashConfig(ctx.config);
35083
+ const backgroundFlagArg = initialBashCfg.background ? {
35084
+ background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false.")
35085
+ } : {};
35086
+ const ptyArgs = initialBashCfg.background ? {
35087
+ pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
35088
+ ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
35089
+ ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
35090
+ } : {};
35091
+ const args = {
35092
+ command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
35093
+ timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe(initialBashCfg.background ? "Hard kill cap in milliseconds (positive integer). When omitted, the task can run up to 30 minutes. Foreground bash returns inline if the command finishes within ~8s (configurable via bash.foreground_wait_window_ms); otherwise it's automatically promoted to background and a completion reminder is delivered when the task actually finishes." : "Hard kill cap in milliseconds (positive integer). When omitted, the foreground command can run up to 30 minutes and returns inline when it finishes."),
35094
+ workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
35095
+ description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
35096
+ ...backgroundFlagArg,
35097
+ compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
35098
+ ...ptyArgs
35099
+ };
34887
35100
  return {
34888
- description: (() => {
34889
- const cfg = resolveBashConfig(ctx.config);
34890
- return bashToolDescription(false, cfg.compress, cfg.background);
34891
- })(),
34892
- args: {
34893
- command: z4.string().describe("Shell command to execute. Supports pipes, redirection, and normal shell syntax."),
34894
- timeout: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("Hard kill cap in milliseconds (positive integer). When omitted, the task can run up to 30 minutes. Foreground bash returns inline if the command finishes within ~8s (configurable via bash.foreground_wait_window_ms); otherwise it's automatically promoted to background and a completion reminder is delivered when the task actually finishes."),
34895
- workdir: z4.string().optional().describe("Working directory for command execution. Relative paths resolve through the bridge; defaults to the current tool context/project root when omitted."),
34896
- description: z4.string().optional().describe("Short 5-10 word human-readable summary shown in OpenCode UI metadata instead of raw shell syntax."),
34897
- background: z4.boolean().optional().describe("When true, spawn the command in the background and return a taskId for bash_status/bash_kill instead of waiting for completion. Defaults to false."),
34898
- compressed: z4.boolean().optional().describe("When true or omitted, return compressed output with noisy terminal control sequences reduced. Set to false for raw output."),
34899
- pty: z4.boolean().optional().describe('When true, spawn the command in a real PTY for interactive programs (python/node/bash REPLs, vim). Implies background: true automatically. Unavailable in subagent sessions. Inspect with bash_status({ taskId, outputMode: "screen" }) and drive interactively with bash_write — its input accepts either a string OR an array like [ "iHello", { key: "esc" }, ":wq", { key: "enter" } ] for atomic text+key sequences.'),
34900
- ptyRows: optionalInt(1, 60).describe("PTY terminal height in rows — ignored when pty is false. Defaults to 24 when pty: true. Minimum 1, maximum 60."),
34901
- ptyCols: optionalInt(1, 140).describe("PTY terminal width in columns — ignored when pty is false. Defaults to 80 when pty: true. Minimum 1, maximum 140.")
34902
- },
34903
- execute: async (args, context) => {
35101
+ description: bashToolDescription(false, initialBashCfg.compress, initialBashCfg.background),
35102
+ args,
35103
+ execute: async (args2, context) => {
34904
35104
  const bashCfg = resolveBashConfig(ctx.config);
34905
35105
  const ctxAftSearchRegistered = ctx.aftSearchRegistered === true;
34906
35106
  const aftSearchRegistered = aftSearchRegisteredOverride ?? ctxAftSearchRegistered;
34907
35107
  let accumulatedOutput = "";
34908
- const description = args.description;
35108
+ const description = args2.description;
34909
35109
  const metadata = context.metadata;
34910
- const rawCommand = args.command;
34911
- const compressionEnabled = bashCfg.compress && args.compressed !== false;
35110
+ const rawCommand = args2.command;
35111
+ const compressionEnabled = bashCfg.compress && args2.compressed !== false;
34912
35112
  const pipeStrip = maybeStripCompressorPipe(rawCommand, compressionEnabled);
34913
35113
  const command = pipeStrip.command;
34914
- const cwd = args.workdir ?? context.directory;
35114
+ const cwd = args2.workdir ?? context.directory;
34915
35115
  const isSubagent = await resolveIsSubagent(ctx.client, context.sessionID, context.directory);
34916
- const requestedPty = args.pty === true;
34917
- const requestedBackground = args.background === true || requestedPty;
35116
+ const backgroundDisabled = !bashCfg.background;
35117
+ const requestedPty = !backgroundDisabled && args2.pty === true;
35118
+ const requestedBackground = !backgroundDisabled && (args2.background === true || requestedPty);
34918
35119
  if (requestedPty && isSubagent) {
34919
35120
  throw new Error("PTY mode is not available in subagent sessions; subagents cannot drive interactive terminals.");
34920
35121
  }
34921
35122
  const allowSubagentBg = bashCfg.subagent_background;
34922
35123
  const subagentForcedForeground = isSubagent && !allowSubagentBg;
34923
- const effectiveBackground = subagentForcedForeground ? false : requestedBackground;
34924
- const rawTimeout = args.timeout;
34925
- const effectiveTimeout = effectiveBackground ? rawTimeout : resolveBashKillTimeout(rawTimeout, bashCfg.foreground_wait_window_ms);
35124
+ const blockToCompletion = subagentForcedForeground || backgroundDisabled;
35125
+ const effectiveBackground = blockToCompletion ? false : requestedBackground;
35126
+ const rawTimeout = args2.timeout;
35127
+ const effectiveTimeout = effectiveBackground || backgroundDisabled ? rawTimeout : resolveBashKillTimeout(rawTimeout, bashCfg.foreground_wait_window_ms);
34926
35128
  if (subagentForcedForeground && requestedBackground) {
34927
35129
  sessionLog(context.sessionID, "[bash] subagent + background:true → converting to foreground (subagent would lose task_id)");
34928
35130
  }
@@ -34930,15 +35132,15 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
34930
35132
  const data = await withPermissionLoop(ctx, context, {
34931
35133
  command,
34932
35134
  timeout: effectiveTimeout,
34933
- workdir: args.workdir,
35135
+ workdir: args2.workdir,
34934
35136
  env: shellEnv?.env ?? {},
34935
35137
  description,
34936
35138
  background: effectiveBackground,
34937
35139
  notify_on_completion: effectiveBackground,
34938
- compressed: args.compressed,
35140
+ compressed: args2.compressed,
34939
35141
  pty: requestedPty,
34940
- pty_rows: args.ptyRows,
34941
- pty_cols: args.ptyCols,
35142
+ pty_rows: args2.ptyRows,
35143
+ pty_cols: args2.ptyCols,
34942
35144
  permissions_requested: true
34943
35145
  }, callBashBridge, {
34944
35146
  onProgress: ({ text }) => {
@@ -34963,7 +35165,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
34963
35165
  return { output: startedLine, title: uiTitle, metadata: metadataPayload2 };
34964
35166
  }
34965
35167
  const foregroundWaitMs = resolveForegroundWaitMs(bashCfg.foreground_wait_window_ms);
34966
- const waitTimeoutMs = subagentForcedForeground ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
35168
+ const waitTimeoutMs = blockToCompletion ? effectiveTimeout ?? DEFAULT_HARD_TIMEOUT_MS : effectiveTimeout !== undefined ? Math.min(effectiveTimeout, foregroundWaitMs) : foregroundWaitMs;
34967
35169
  const startedAt = Date.now();
34968
35170
  while (true) {
34969
35171
  const status = await callBashBridge(ctx, context, "bash_status", { task_id: taskId });
@@ -34977,7 +35179,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
34977
35179
  return { output: rendered2, title: uiTitle, metadata: metadataPayload2 };
34978
35180
  }
34979
35181
  if (Date.now() - startedAt >= waitTimeoutMs) {
34980
- if (subagentForcedForeground) {
35182
+ if (blockToCompletion) {
34981
35183
  await sleep(FOREGROUND_POLL_INTERVAL_MS);
34982
35184
  continue;
34983
35185
  }
@@ -35037,7 +35239,7 @@ function createBashTool(ctx, aftSearchRegisteredOverride) {
35037
35239
  }
35038
35240
  function createBashStatusTool(ctx) {
35039
35241
  return {
35040
- description: "Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. Use bash_watch to block on or register for pattern matches and exit events.",
35242
+ description: "Read-only snapshot of a background or PTY bash task's current state and output. Returns immediately. Never waits. One look to check on a task is fine — never loop it to wait for completion. To wait, use bash_watch.",
35041
35243
  args: {
35042
35244
  taskId: z4.string().describe("Background task ID returned by bash({ background: true }), e.g. bash-6b454047."),
35043
35245
  outputMode: z4.enum(["screen", "raw", "both"]).optional().describe("PTY output rendering mode. Defaults to screen for PTY tasks and preserves existing behavior for piped tasks when omitted.")
@@ -35515,16 +35717,16 @@ import { tool as tool6 } from "@opencode-ai/plugin";
35515
35717
  var z6 = tool6.schema;
35516
35718
  var BASH_WAIT_POLL_INTERVAL_MS = 100;
35517
35719
  var DEFAULT_BASH_STATUS_WAIT_TIMEOUT_MS = 30000;
35518
- var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 300000;
35720
+ var MAX_BASH_STATUS_WAIT_TIMEOUT_MS = 30 * 60 * 1000;
35519
35721
  var REGEX_WAIT_SCAN_WINDOW_BYTES = 64 * 1024;
35520
35722
  function createBashWatchTool(ctx) {
35521
35723
  return {
35522
- description: "Watch a background bash task. Two modes. Async (background:true, requires pattern) registers a non-blocking notification and returns immediately — use this to be pinged when a specific line appears or the task exits, without freezing your turn. Sync (default) blocks until a pattern matches/the task exits/timeout, and is ONLY for short bounded waits (seconds, e.g. a dev server printing a readiness line). Do NOT sync-wait for a long task (build/test/install): blocking locks the user out until it ends instead end your turn and let the automatic completion reminder arrive, or use async mode.",
35724
+ description: "Watch a background bash task. Sync (default) blocks until a pattern matches, the task exits, or timeout — use it when the result is the next thing you need, even for long builds/tests/installs (pass timeoutMs up to 30 min for those). The user can interrupt anytime; the wait auto-converts to an async notification. Async (background:true, requires pattern) registers a non-blocking notification and returns immediately use when you have parallel work or want to end your turn. Never loop bash_status to wait.",
35523
35725
  args: {
35524
35726
  taskId: z6.string().describe("Background task ID returned by bash({ background: true })."),
35525
35727
  pattern: z6.union([z6.string(), z6.object({ regex: z6.string() })]).optional().describe("Substring or regex pattern. Optional in sync mode; required with background:true. Sync substring watches keep only the overlap tail needed for boundary matches; sync regex watches use a 64 KB rolling output window."),
35526
35728
  background: z6.boolean().optional().describe("When true, register an async watch and return immediately. Defaults to false (sync wait)."),
35527
- timeoutMs: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS).describe("Sync-only timeout in milliseconds. Default 30000, max 300000."),
35729
+ timeoutMs: optionalInt(1, MAX_BASH_STATUS_WAIT_TIMEOUT_MS).describe("Sync-only timeout in milliseconds. Default 30000, max 1800000 (30 min)."),
35528
35730
  once: z6.boolean().optional().describe("Async-only. Defaults true; false keeps the watch sticky until task exit.")
35529
35731
  },
35530
35732
  execute: async (args, context) => {
@@ -37084,12 +37286,15 @@ function hoistedTools(ctx) {
37084
37286
  aft_delete: createDeleteTool(ctx),
37085
37287
  aft_move: createMoveTool(ctx)
37086
37288
  };
37087
- if (resolveBashConfig(ctx.config).enabled) {
37289
+ const bashCfg = resolveBashConfig(ctx.config);
37290
+ if (bashCfg.enabled) {
37088
37291
  tools.bash = createBashTool(ctx);
37089
- tools.bash_status = createBashStatusTool(ctx);
37090
- tools.bash_write = createBashWriteTool(ctx);
37091
- tools.bash_watch = createBashWatchTool(ctx);
37092
- tools.bash_kill = createBashKillTool(ctx);
37292
+ if (bashCfg.background) {
37293
+ tools.bash_status = createBashStatusTool(ctx);
37294
+ tools.bash_write = createBashWriteTool(ctx);
37295
+ tools.bash_watch = createBashWatchTool(ctx);
37296
+ tools.bash_kill = createBashKillTool(ctx);
37297
+ }
37093
37298
  }
37094
37299
  return tools;
37095
37300
  }
@@ -37143,12 +37348,15 @@ function aftPrefixedTools(ctx) {
37143
37348
  aft_delete: createDeleteTool(ctx),
37144
37349
  aft_move: createMoveTool(ctx)
37145
37350
  };
37146
- if (resolveBashConfig(ctx.config).enabled) {
37351
+ const bashCfg = resolveBashConfig(ctx.config);
37352
+ if (bashCfg.enabled) {
37147
37353
  tools.aft_bash = createBashTool(ctx);
37148
- tools.bash_status = createBashStatusTool(ctx);
37149
- tools.bash_write = createBashWriteTool(ctx);
37150
- tools.bash_watch = createBashWatchTool(ctx);
37151
- tools.bash_kill = createBashKillTool(ctx);
37354
+ if (bashCfg.background) {
37355
+ tools.bash_status = createBashStatusTool(ctx);
37356
+ tools.bash_write = createBashWriteTool(ctx);
37357
+ tools.bash_watch = createBashWatchTool(ctx);
37358
+ tools.bash_kill = createBashKillTool(ctx);
37359
+ }
37152
37360
  }
37153
37361
  return tools;
37154
37362
  }
@@ -37232,15 +37440,15 @@ function importTools(ctx) {
37232
37440
  // src/tools/inspect.ts
37233
37441
  import { tool as tool10 } from "@opencode-ai/plugin";
37234
37442
  var z10 = tool10.schema;
37235
- function asRecord2(value) {
37443
+ function asRecord3(value) {
37236
37444
  if (!value || typeof value !== "object" || Array.isArray(value))
37237
37445
  return;
37238
37446
  return value;
37239
37447
  }
37240
- function asNumber(value) {
37448
+ function asNumber2(value) {
37241
37449
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
37242
37450
  }
37243
- function asString(value) {
37451
+ function asString2(value) {
37244
37452
  return typeof value === "string" ? value : undefined;
37245
37453
  }
37246
37454
  function asStringArray(value) {
@@ -37257,16 +37465,16 @@ function diagnosticsServerSummary(section) {
37257
37465
  return parts.length > 0 ? parts.join("; ") : "none reported";
37258
37466
  }
37259
37467
  function formatDiagnosticsSummary(summary) {
37260
- const section = asRecord2(summary?.diagnostics);
37468
+ const section = asRecord3(summary?.diagnostics);
37261
37469
  if (!section)
37262
37470
  return;
37263
- const errors3 = asNumber(section.errors);
37264
- const warnings = asNumber(section.warnings);
37265
- const info = asNumber(section.info);
37266
- const hints = asNumber(section.hints);
37471
+ const errors3 = asNumber2(section.errors);
37472
+ const warnings = asNumber2(section.warnings);
37473
+ const info = asNumber2(section.info);
37474
+ const hints = asNumber2(section.hints);
37267
37475
  const hasCounts = [errors3, warnings, info, hints].some((value) => value !== undefined);
37268
37476
  const counts = `${errors3 ?? 0} errors, ${warnings ?? 0} warnings, ${info ?? 0} info, ${hints ?? 0} hints`;
37269
- const status = asString(section.status);
37477
+ const status = asString2(section.status);
37270
37478
  if (status === "pending") {
37271
37479
  return hasCounts ? `diagnostics: ${counts} so far — still pending (servers: ${diagnosticsServerSummary(section)})` : `diagnostics: pending (servers: ${diagnosticsServerSummary(section)})`;
37272
37480
  }
@@ -37279,9 +37487,9 @@ function formatDiagnosticsSummary(summary) {
37279
37487
  return;
37280
37488
  }
37281
37489
  function formatDiagnosticLocation(diagnostic) {
37282
- const file2 = asString(diagnostic.file) ?? "(unknown file)";
37283
- const line = asNumber(diagnostic.line);
37284
- const column = asNumber(diagnostic.column);
37490
+ const file2 = asString2(diagnostic.file) ?? "(unknown file)";
37491
+ const line = asNumber2(diagnostic.line);
37492
+ const column = asNumber2(diagnostic.column);
37285
37493
  if (line === undefined)
37286
37494
  return file2;
37287
37495
  if (column === undefined)
@@ -37289,21 +37497,21 @@ function formatDiagnosticLocation(diagnostic) {
37289
37497
  return `${file2}:${line}:${column}`;
37290
37498
  }
37291
37499
  function formatDiagnosticsDetails(details) {
37292
- const diagnostics = Array.isArray(details?.diagnostics) ? details.diagnostics.map(asRecord2).filter(Boolean) : [];
37500
+ const diagnostics = Array.isArray(details?.diagnostics) ? details.diagnostics.map(asRecord3).filter(Boolean) : [];
37293
37501
  return diagnostics.map((diagnostic) => {
37294
- const severity = asString(diagnostic.severity) ?? "information";
37295
- const message = asString(diagnostic.message) ?? "(no message)";
37296
- const source = asString(diagnostic.source);
37502
+ const severity = asString2(diagnostic.severity) ?? "information";
37503
+ const message = asString2(diagnostic.message) ?? "(no message)";
37504
+ const source = asString2(diagnostic.source);
37297
37505
  const suffix = source ? ` [${source}]` : "";
37298
37506
  return `${formatDiagnosticLocation(diagnostic)} ${severity} ${message}${suffix}`;
37299
37507
  });
37300
37508
  }
37301
37509
  function renderInspectDiagnostics(response) {
37302
37510
  const lines = [];
37303
- const summaryLine = formatDiagnosticsSummary(asRecord2(response.summary));
37511
+ const summaryLine = formatDiagnosticsSummary(asRecord3(response.summary));
37304
37512
  if (summaryLine)
37305
37513
  lines.push(summaryLine);
37306
- const detailLines = formatDiagnosticsDetails(asRecord2(response.details));
37514
+ const detailLines = formatDiagnosticsDetails(asRecord3(response.details));
37307
37515
  if (detailLines.length > 0) {
37308
37516
  lines.push("diagnostics details:", ...detailLines.map((line) => `- ${line}`));
37309
37517
  }
@@ -37488,7 +37696,8 @@ function navigationTools(ctx) {
37488
37696
  }
37489
37697
  throw new Error(message);
37490
37698
  }
37491
- return JSON.stringify(response);
37699
+ return formatCallgraphSections(args.op, response).join(`
37700
+ `);
37492
37701
  }
37493
37702
  }
37494
37703
  };
@@ -38207,13 +38416,42 @@ function searchPathExists(projectRoot, target) {
38207
38416
  }
38208
38417
  function splitSearchPathArg(projectRoot, raw) {
38209
38418
  if (searchPathExists(projectRoot, raw) || !/\s/.test(raw)) {
38210
- return [raw];
38419
+ return { paths: [raw], missing: [] };
38211
38420
  }
38212
38421
  const fragments = raw.trim().split(/\s+/).filter(Boolean);
38213
- if (fragments.length < 2 || !fragments.every((fragment) => searchPathExists(projectRoot, fragment))) {
38214
- return [raw];
38422
+ if (fragments.length < 2) {
38423
+ return { paths: [raw], missing: [] };
38424
+ }
38425
+ const existing = [];
38426
+ const missing = [];
38427
+ for (const fragment of fragments) {
38428
+ if (searchPathExists(projectRoot, fragment)) {
38429
+ existing.push(fragment);
38430
+ } else {
38431
+ missing.push(fragment);
38432
+ }
38433
+ }
38434
+ if (existing.length === 0) {
38435
+ return { paths: [raw], missing: [] };
38215
38436
  }
38216
- return fragments;
38437
+ return { paths: existing, missing };
38438
+ }
38439
+ function bridgeSearchPathArg(projectRoot, split) {
38440
+ return split.paths.map((target) => absoluteSearchPath(projectRoot, target)).join(" ");
38441
+ }
38442
+ function formatSkippedSearchPaths(missing) {
38443
+ if (missing.length === 0)
38444
+ return;
38445
+ const noun = missing.length === 1 ? "path" : "paths";
38446
+ return `Skipped ${missing.length} ${noun} not found: ${missing.join(", ")}`;
38447
+ }
38448
+ function appendSkippedSearchPaths(text, missing) {
38449
+ const note = formatSkippedSearchPaths(missing);
38450
+ if (!note)
38451
+ return text;
38452
+ return text.length > 0 ? `${text}
38453
+
38454
+ ${note}` : note;
38217
38455
  }
38218
38456
  function searchPathKind(projectRoot, target, defaultKind) {
38219
38457
  try {
@@ -38226,8 +38464,8 @@ function searchPathKind(projectRoot, target, defaultKind) {
38226
38464
  return defaultKind;
38227
38465
  }
38228
38466
  }
38229
- function searchPathTargets(projectRoot, raw, defaultKind) {
38230
- return splitSearchPathArg(projectRoot, raw).map((target) => {
38467
+ function searchPathTargets(projectRoot, split, defaultKind) {
38468
+ return split.paths.map((target) => {
38231
38469
  const absoluteTarget = absoluteSearchPath(projectRoot, target);
38232
38470
  return {
38233
38471
  target: absoluteTarget,
@@ -38277,16 +38515,17 @@ function searchTools(ctx) {
38277
38515
  const projectRoot = await resolveProjectRoot(ctx, context);
38278
38516
  const pattern = String(args.pattern);
38279
38517
  const includeArg = args.include ? String(args.include) : undefined;
38280
- const pathArg = args.path ? expandTilde2(String(args.path)) : undefined;
38281
- const bridgePath = pathArg ? absoluteSearchPath(projectRoot, pathArg) : undefined;
38518
+ const pathArg = args.path ? String(args.path) : undefined;
38519
+ const pathSplit = pathArg ? splitSearchPathArg(projectRoot, pathArg) : undefined;
38520
+ const bridgePath = pathSplit ? bridgeSearchPathArg(projectRoot, pathSplit) : undefined;
38282
38521
  const grepDenied = await askGrepPermission(context, pattern, {
38283
38522
  path: bridgePath,
38284
38523
  include: includeArg
38285
38524
  });
38286
38525
  if (grepDenied)
38287
38526
  return permissionDeniedResponse(grepDenied);
38288
- if (pathArg) {
38289
- for (const target of searchPathTargets(projectRoot, pathArg, "file")) {
38527
+ if (pathSplit) {
38528
+ for (const target of searchPathTargets(projectRoot, pathSplit, "file")) {
38290
38529
  const externalDenied = await assertExternalDirectoryPermission(context, target.target, {
38291
38530
  kind: target.kind
38292
38531
  });
@@ -38304,7 +38543,10 @@ function searchTools(ctx) {
38304
38543
  if (response.success === false) {
38305
38544
  throw new Error(response.message || "grep failed");
38306
38545
  }
38307
- return formatGrepOutput(response);
38546
+ if (pathSplit && pathSplit.missing.length > 0) {
38547
+ response.complete = false;
38548
+ }
38549
+ return appendSkippedSearchPaths(formatGrepOutput(response), pathSplit?.missing ?? []);
38308
38550
  }
38309
38551
  };
38310
38552
  const globTool = {
@@ -38316,7 +38558,7 @@ function searchTools(ctx) {
38316
38558
  execute: async (args, context) => {
38317
38559
  const projectRoot = await resolveProjectRoot(ctx, context);
38318
38560
  let globPattern = expandTilde2(String(args.pattern));
38319
- let globPath = args.path ? expandTilde2(String(args.path)) : undefined;
38561
+ let globPath = args.path ? String(args.path) : undefined;
38320
38562
  if (!globPath && globPattern.startsWith("/")) {
38321
38563
  const metaIdx = globPattern.search(/[*?{}[\]]/);
38322
38564
  if (metaIdx > 0) {
@@ -38330,12 +38572,13 @@ function searchTools(ctx) {
38330
38572
  globPattern = path7.basename(globPattern);
38331
38573
  }
38332
38574
  }
38333
- const globDenied = await askGlobPermission(context, globPattern, { path: globPath });
38575
+ const pathSplit = globPath ? splitSearchPathArg(projectRoot, globPath) : undefined;
38576
+ const bridgePath = pathSplit ? bridgeSearchPathArg(projectRoot, pathSplit) : undefined;
38577
+ const globDenied = await askGlobPermission(context, globPattern, { path: bridgePath });
38334
38578
  if (globDenied)
38335
38579
  return permissionDeniedResponse(globDenied);
38336
- const bridgePath = globPath ? absoluteSearchPath(projectRoot, globPath) : undefined;
38337
- if (globPath) {
38338
- for (const target of searchPathTargets(projectRoot, globPath, "directory")) {
38580
+ if (pathSplit) {
38581
+ for (const target of searchPathTargets(projectRoot, pathSplit, "directory")) {
38339
38582
  const externalDenied = await assertExternalDirectoryPermission(context, target.target, {
38340
38583
  kind: target.kind
38341
38584
  });
@@ -38350,14 +38593,17 @@ function searchTools(ctx) {
38350
38593
  if (response.success === false) {
38351
38594
  throw new Error(response.message || "glob failed");
38352
38595
  }
38596
+ if (pathSplit && pathSplit.missing.length > 0) {
38597
+ response.complete = false;
38598
+ }
38353
38599
  if (typeof response.text === "string") {
38354
- return response.text;
38600
+ return appendSkippedSearchPaths(response.text, pathSplit?.missing ?? []);
38355
38601
  }
38356
38602
  if (Array.isArray(response.files)) {
38357
- return response.files.join(`
38358
- `);
38603
+ return appendSkippedSearchPaths(response.files.join(`
38604
+ `), pathSplit?.missing ?? []);
38359
38605
  }
38360
- return response.text || JSON.stringify(response);
38606
+ return appendSkippedSearchPaths(response.text || JSON.stringify(response), pathSplit?.missing ?? []);
38361
38607
  }
38362
38608
  };
38363
38609
  const hoisting = ctx.config.hoist_builtin_tools !== false;
@@ -38499,11 +38745,11 @@ function buildWorkflowHints(opts) {
38499
38745
  }
38500
38746
  if (hasBash && hasBgBash) {
38501
38747
  sections.push([
38502
- `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`taskId\` immediately. Then do ONE of these:`,
38503
- "1. Keep working on something independent of the result.",
38504
- "2. End your turn a completion reminder with the result arrives automatically.",
38505
- `3. Need to react to a specific output line early? Register \`bash_watch({ taskId, pattern, background: true })\`, then end your turn — it pings you the moment the pattern appears.`,
38506
- `\`${bashStatusName}({ taskId })\` is for inspecting output AFTER the reminder arrives, or one quick look at a live task — never call it repeatedly to wait: the reminder already delivers the result, and each poll wastes a turn. Sync \`bash_watch\` (without \`background: true\`) blocks your turn and locks the user out reserve it for waits of a few seconds (a dev server printing its ready line), never for builds or test suites.`
38748
+ `**Long-running commands** (builds, installs, full test suites): \`${bashName}({ background: true })\` returns a \`taskId\` immediately.`,
38749
+ "1. Nothing else useful to do (a build/test/validation whose result is the next thing you need) → sync `bash_watch` to block until it exits (pass a longer timeoutMs for long commands; the user can interrupt).",
38750
+ "2. Useful parallel work available → end your turn; the completion reminder delivers the result. (Or spawn a subagent for the side work.)",
38751
+ "3. Want to react to a specific early output line async `bash_watch` (background:true + pattern).",
38752
+ "Never loop `bash_status` to wait — it's a one-shot inspector, not a wait primitive."
38507
38753
  ].join(`
38508
38754
  `));
38509
38755
  sections.push(`**PTY / interactive commands**: PTY mode is for interactive REPLs and terminal apps (python, node, bash itself, vim). Start with \`${bashName}({ command: "python", pty: true, background: true })\`, read the screen with \`${bashStatusName}({ taskId, outputMode: "screen" })\`, and send input with \`${bashWriteName}({ taskId, input: "..." })\`.`);
@@ -38588,6 +38834,7 @@ async function initializePluginForDirectory(input) {
38588
38834
  const binaryPath = await findBinary(PLUGIN_VERSION);
38589
38835
  await ensureStorageMigrated({ harness: "opencode", binaryPath, logger: bridgeLogger });
38590
38836
  const aftConfig = loadAftConfig(input.directory);
38837
+ enqueueConfigParseWarnings(input.directory, getConfigLoadErrors());
38591
38838
  const autoUpdateAbort = new AbortController;
38592
38839
  const configOverrides = {
38593
38840
  ...resolveProjectOverridesForConfigure(aftConfig),
@@ -38672,11 +38919,13 @@ ${lines}
38672
38919
  }
38673
38920
  const versionUpgradePromises = new Map;
38674
38921
  const poolOptions = {
38922
+ ...resolveBridgePoolTransportOptions(aftConfig),
38675
38923
  errorPrefix: "[aft-plugin]",
38676
38924
  minVersion: PLUGIN_VERSION,
38677
38925
  projectConfigLoader: (projectRoot) => {
38678
38926
  try {
38679
38927
  const projectConfig = loadAftConfig(projectRoot);
38928
+ enqueueConfigParseWarnings(projectRoot, getConfigLoadErrors());
38680
38929
  return resolveProjectOverridesForConfigure(projectConfig);
38681
38930
  } catch (err) {
38682
38931
  warn2(`loadAftConfig(${projectRoot}) failed; falling back to plugin-init config: ${err instanceof Error ? err.message : String(err)}`);
@@ -38735,8 +38984,7 @@ ${lines}
38735
38984
  ctx,
38736
38985
  directory: sessionDir,
38737
38986
  sessionID: completion.session_id,
38738
- client: input.client,
38739
- serverUrl: input.serverUrl?.toString()
38987
+ client: input.client
38740
38988
  }, completion);
38741
38989
  },
38742
38990
  onBashLongRunning: (reminder, bridge) => {
@@ -38745,8 +38993,7 @@ ${lines}
38745
38993
  ctx,
38746
38994
  directory: sessionDir,
38747
38995
  sessionID: reminder.session_id,
38748
- client: input.client,
38749
- serverUrl: input.serverUrl?.toString()
38996
+ client: input.client
38750
38997
  }, reminder);
38751
38998
  },
38752
38999
  onBashPatternMatch: (frame, bridge) => {
@@ -38755,8 +39002,7 @@ ${lines}
38755
39002
  ctx,
38756
39003
  directory: sessionDir,
38757
39004
  sessionID: frame.session_id,
38758
- client: input.client,
38759
- serverUrl: input.serverUrl?.toString()
39005
+ client: input.client
38760
39006
  }, frame);
38761
39007
  }
38762
39008
  };
@@ -38769,16 +39015,6 @@ ${lines}
38769
39015
  config: aftConfig,
38770
39016
  storageDir: configOverrides.storage_dir
38771
39017
  };
38772
- probeServerReachable(input.serverUrl?.toString()).then((reachable) => {
38773
- setLiveServerWakeAvailable(reachable);
38774
- if (reachable) {
38775
- log2("Live OpenCode HTTP listener reachable; bg-notifications wake path = live-server (anomalyco/opencode#28202 workaround active).");
38776
- } else {
38777
- debug("Live OpenCode HTTP listener unreachable; bg-notifications wake path = in-process-fallback. Wakes will still arrive but the upstream duplicate-runner bug (anomalyco/opencode#28202) is not worked around. Launch with `opencode --port 0` in TUI mode to activate the workaround.");
38778
- }
38779
- }).catch(() => {
38780
- setLiveServerWakeAvailable(false);
38781
- });
38782
39018
  if (onnxRuntimePromise) {
38783
39019
  onnxRuntimePromise.then((ortDylibDir) => {
38784
39020
  if (ortDylibDir) {
@@ -39013,9 +39249,24 @@ Install: ${getManualInstallHint()}`).catch(() => {});
39013
39249
  ctx,
39014
39250
  directory: sessionDir,
39015
39251
  sessionID,
39016
- client: input.client,
39017
- serverUrl: input.serverUrl?.toString()
39252
+ client: input.client
39018
39253
  });
39254
+ const configParseWarnings = drainPendingConfigParseWarnings(sessionDir);
39255
+ if (configParseWarnings.length > 0) {
39256
+ const bridge = pool.getActiveBridgeForRoot(sessionDir) ?? pool.getBridge(sessionDir);
39257
+ enqueueConfigureWarningsForSession({
39258
+ projectRoot: sessionDir,
39259
+ sessionId: sessionID,
39260
+ client: input.client,
39261
+ bridge,
39262
+ warnings: configParseWarnings,
39263
+ fallbackClient: input.client,
39264
+ storageDir: configOverrides.storage_dir,
39265
+ pluginVersion: PLUGIN_VERSION,
39266
+ serverUrl: input.serverUrl?.toString(),
39267
+ delivery: aftConfig.configure_warnings_delivery ?? "toast"
39268
+ });
39269
+ }
39019
39270
  await flushConfigureWarningsOnIdle(sessionID);
39020
39271
  },
39021
39272
  "chat.message": async (messageInput) => {