@cortexkit/aft-opencode 0.54.0 → 0.55.1

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
@@ -8842,6 +8842,21 @@ class BinaryBridge {
8842
8842
  }
8843
8843
  return this.sendWithVersionMismatchRetry(command, dispatchParams, options, true);
8844
8844
  }
8845
+ listenForRequestAbort(requestId, signal) {
8846
+ if (!signal)
8847
+ return () => {};
8848
+ let fired = false;
8849
+ const onAbort = () => {
8850
+ if (fired)
8851
+ return;
8852
+ fired = true;
8853
+ this.send("cancel_request", { id: requestId }, { keepBridgeOnTimeout: true }).catch(() => {});
8854
+ };
8855
+ signal.addEventListener("abort", onAbort, { once: true });
8856
+ if (signal.aborted)
8857
+ onAbort();
8858
+ return () => signal.removeEventListener("abort", onAbort);
8859
+ }
8845
8860
  async toolCall(sessionId, name, rawArgs = {}, options) {
8846
8861
  const params = { name, arguments: rawArgs };
8847
8862
  if (sessionId)
@@ -8864,7 +8879,7 @@ class BinaryBridge {
8864
8879
  if (this._shuttingDown) {
8865
8880
  throw new BridgeTransportUnavailableError(`${this.errorPrefix} Bridge is shutting down, cannot send "${command}"`);
8866
8881
  }
8867
- if (Object.hasOwn(params, "id")) {
8882
+ if (Object.hasOwn(params, "id") && command !== "cancel_request") {
8868
8883
  throw new Error("params cannot contain reserved key 'id'");
8869
8884
  }
8870
8885
  const requestSessionId = typeof params.session_id === "string" && params.session_id.length > 0 ? params.session_id : undefined;
@@ -8918,7 +8933,7 @@ class BinaryBridge {
8918
8933
  }
8919
8934
  const id = String(this.nextId++);
8920
8935
  let request;
8921
- if (Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
8936
+ if (command === "cancel_request" || Object.hasOwn(params, "command") || Object.hasOwn(params, "method")) {
8922
8937
  const nested = { ...params };
8923
8938
  const reserved = {};
8924
8939
  for (const key of ["session_id", "lsp_hints"]) {
@@ -8938,11 +8953,12 @@ class BinaryBridge {
8938
8953
  const child = this.process;
8939
8954
  const response = await new Promise((resolve3, reject) => {
8940
8955
  const timer = setTimeout(() => {
8941
- const entry = this.pending.get(id);
8942
- if (!entry)
8956
+ const entry2 = this.pending.get(id);
8957
+ if (!entry2)
8943
8958
  return;
8944
8959
  this.pending.delete(id);
8945
- clearTimeout(entry.timer);
8960
+ clearTimeout(entry2.timer);
8961
+ entry2.onSettled?.();
8946
8962
  if (keepBridgeOnTimeout) {
8947
8963
  const timeoutMsg2 = `Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`;
8948
8964
  if (requestSessionId) {
@@ -8950,7 +8966,7 @@ class BinaryBridge {
8950
8966
  } else {
8951
8967
  this.warnVia(timeoutMsg2);
8952
8968
  }
8953
- entry.reject(new BridgeTransportTimeoutError(command, effectiveTimeoutMs, `${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8969
+ entry2.reject(new BridgeTransportTimeoutError(command, effectiveTimeoutMs, `${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8954
8970
  return;
8955
8971
  }
8956
8972
  const childActiveSinceRequest = this.lastChildActivityAt > requestSentAt;
@@ -8965,13 +8981,20 @@ class BinaryBridge {
8965
8981
  this.warnVia(timeoutMsg);
8966
8982
  }
8967
8983
  if (keepWarm) {
8968
- entry.reject(new Error(`${this.errorPrefix} request "${command}" timed out after ${effectiveTimeoutMs}ms (bridge busy/under load); bridge kept warm — retry`));
8984
+ entry2.reject(new Error(`${this.errorPrefix} request "${command}" timed out after ${effectiveTimeoutMs}ms (bridge busy/under load); bridge kept warm — retry`));
8969
8985
  return;
8970
8986
  }
8971
- entry.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8987
+ entry2.reject(new Error(`${this.errorPrefix} Request "${command}" (id=${id}) timed out after ${effectiveTimeoutMs}ms`));
8972
8988
  this.handleTimeout(requestSessionId);
8973
8989
  }, effectiveTimeoutMs);
8974
- this.pending.set(id, { resolve: resolve3, reject, timer, onProgress: options?.onProgress, command });
8990
+ const entry = {
8991
+ resolve: resolve3,
8992
+ reject,
8993
+ timer,
8994
+ onProgress: options?.onProgress,
8995
+ command
8996
+ };
8997
+ this.pending.set(id, entry);
8975
8998
  if (!child?.stdin?.writable) {
8976
8999
  this.pending.delete(id);
8977
9000
  clearTimeout(timer);
@@ -8985,11 +9008,12 @@ class BinaryBridge {
8985
9008
  const handleWriteFailure = (cause) => {
8986
9009
  const writeError = cause instanceof Error ? cause : new Error(String(cause));
8987
9010
  const error2 = new BridgeTransportUnknownOutcomeError(`${this.errorPrefix} Failed to write to stdin: ${writeError.message}`, { cause: writeError });
8988
- const entry = this.pending.get(id);
8989
- if (entry) {
9011
+ const entry2 = this.pending.get(id);
9012
+ if (entry2) {
8990
9013
  this.pending.delete(id);
8991
- clearTimeout(entry.timer);
8992
- entry.reject(error2);
9014
+ clearTimeout(entry2.timer);
9015
+ entry2.onSettled?.();
9016
+ entry2.reject(error2);
8993
9017
  }
8994
9018
  if (this.process === child)
8995
9019
  this.invalidateTransportProcess(error2);
@@ -9002,6 +9026,9 @@ class BinaryBridge {
9002
9026
  } catch (err) {
9003
9027
  handleWriteFailure(err);
9004
9028
  }
9029
+ if (this.pending.has(id)) {
9030
+ entry.onSettled = this.listenForRequestAbort(id, options?.abortSignal);
9031
+ }
9005
9032
  });
9006
9033
  if (command === "configure" && response.success === true && options?.markConfiguredOnSuccess !== false) {
9007
9034
  this.configured = true;
@@ -9370,6 +9397,7 @@ class BinaryBridge {
9370
9397
  if (requestId && entry) {
9371
9398
  this.pending.delete(requestId);
9372
9399
  clearTimeout(entry.timer);
9400
+ entry.onSettled?.();
9373
9401
  entry.resolve({
9374
9402
  success: false,
9375
9403
  code: "permission_required",
@@ -9411,6 +9439,7 @@ class BinaryBridge {
9411
9439
  return;
9412
9440
  this.pending.delete(id);
9413
9441
  clearTimeout(entry.timer);
9442
+ entry.onSettled?.();
9414
9443
  this.consecutiveRequestTimeouts = 0;
9415
9444
  this.scheduleRestartCountReset();
9416
9445
  this.accountForBashTaskResponse(entry.command, response);
@@ -9501,6 +9530,7 @@ class BinaryBridge {
9501
9530
  rejectAllPending(error2) {
9502
9531
  for (const [_id, entry] of this.pending) {
9503
9532
  clearTimeout(entry.timer);
9533
+ entry.onSettled?.();
9504
9534
  entry.reject(error2);
9505
9535
  }
9506
9536
  this.pending.clear();
@@ -9525,6 +9555,12 @@ import { join as join5 } from "node:path";
9525
9555
  function homeDir2(env) {
9526
9556
  return (process.platform === "win32" ? env.USERPROFILE || env.HOME : env.HOME) || homedir4();
9527
9557
  }
9558
+ function getOpenCodeCacheRoot(env = process.env, home = homedir4()) {
9559
+ return join5(env.XDG_CACHE_HOME || join5(home, ".cache"), "opencode");
9560
+ }
9561
+ function getOpenCodeConfigRoot(env = process.env, home = homedir4()) {
9562
+ return join5(env.XDG_CONFIG_HOME || join5(home, ".config"), "opencode");
9563
+ }
9528
9564
  function getAftCacheRoot(env = process.env) {
9529
9565
  if (env.AFT_CACHE_DIR)
9530
9566
  return env.AFT_CACHE_DIR;
@@ -14284,6 +14320,7 @@ async function ensureStorageMigrated(opts) {
14284
14320
  throw new Error(`AFT storage migration failed (${detail}). ` + `Harness: ${opts.harness}. Legacy: ${legacyRoot}. Target: ${newRoot}. ` + `See log: ${logPath}. ` + `Plugin load aborted to prevent legacy/new state divergence.` + (stderrTail ? ` Stderr tail: ${stderrTail}` : "") + (stdoutTail ? ` Stdout tail: ${stdoutTail}` : ""));
14285
14321
  }
14286
14322
  // ../aft-bridge/dist/npm-resolver.js
14323
+ import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
14287
14324
  import { readdirSync as readdirSync2, statSync as statSync5 } from "node:fs";
14288
14325
  import { homedir as homedir8 } from "node:os";
14289
14326
  import { delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute3, join as join10 } from "node:path";
@@ -14390,6 +14427,189 @@ function resolveNpm(deps = defaultDeps()) {
14390
14427
  }
14391
14428
  return null;
14392
14429
  }
14430
+ function quoteCmdArgument(value) {
14431
+ if (/[\0\r\n"%]/.test(value)) {
14432
+ throw new Error(`npm argument cannot be represented safely for cmd.exe: ${JSON.stringify(value)}`);
14433
+ }
14434
+ return `"${value}"`;
14435
+ }
14436
+ function npmInvocation(resolved, npmArgs, platform = process.platform, env = process.env) {
14437
+ if (platform !== "win32" || !/\.(?:cmd|bat)$/i.test(resolved.command)) {
14438
+ return { command: resolved.command, args: [...npmArgs] };
14439
+ }
14440
+ if (/[\0\r\n"]/.test(resolved.command)) {
14441
+ throw new Error(`npm command cannot be represented safely for cmd.exe: ${JSON.stringify(resolved.command)}`);
14442
+ }
14443
+ const commandEnvName = "AFT_NPM_COMMAND";
14444
+ const quotedArgs = npmArgs.map(quoteCmdArgument);
14445
+ const commandLine = `""%${commandEnvName}%"${quotedArgs.length > 0 ? ` ${quotedArgs.join(" ")}` : ""}"`;
14446
+ return {
14447
+ command: env.ComSpec ?? env.COMSPEC ?? "cmd.exe",
14448
+ args: ["/d", "/s", "/v:off", "/c", commandLine],
14449
+ env: { [commandEnvName]: resolved.command },
14450
+ windowsVerbatimArguments: true,
14451
+ windowsCmdShim: true
14452
+ };
14453
+ }
14454
+
14455
+ class NpmTerminationUnknownError extends Error {
14456
+ code = "npm_termination_unknown";
14457
+ constructor(detail) {
14458
+ super(`npm process-tree termination could not be confirmed: ${detail}`);
14459
+ this.name = "NpmTerminationUnknownError";
14460
+ }
14461
+ }
14462
+ function terminateDirectNpmChild(child, gracePeriodMs) {
14463
+ if (child.exitCode !== null || child.signalCode !== null)
14464
+ return Promise.resolve();
14465
+ return new Promise((resolve7, reject) => {
14466
+ let settled = false;
14467
+ let forceTimer = null;
14468
+ let confirmationTimer = null;
14469
+ let signalFailure = null;
14470
+ const cleanup = () => {
14471
+ if (forceTimer)
14472
+ clearTimeout(forceTimer);
14473
+ if (confirmationTimer)
14474
+ clearTimeout(confirmationTimer);
14475
+ child.removeListener("exit", finish);
14476
+ child.removeListener("error", onChildError);
14477
+ };
14478
+ const finish = () => {
14479
+ if (settled)
14480
+ return;
14481
+ settled = true;
14482
+ cleanup();
14483
+ resolve7();
14484
+ };
14485
+ const fail = () => {
14486
+ if (settled)
14487
+ return;
14488
+ if (child.exitCode !== null || child.signalCode !== null) {
14489
+ finish();
14490
+ return;
14491
+ }
14492
+ settled = true;
14493
+ cleanup();
14494
+ reject(new NpmTerminationUnknownError(signalFailure ?? `direct npm child did not exit after SIGKILL within ${gracePeriodMs}ms`));
14495
+ };
14496
+ const onChildError = (error2) => {
14497
+ signalFailure = `direct npm child error during termination: ${String(error2)}`;
14498
+ };
14499
+ const signal = (value) => {
14500
+ try {
14501
+ if (!child.kill(value)) {
14502
+ signalFailure = `direct npm child rejected ${value ?? "SIGTERM"}`;
14503
+ }
14504
+ } catch (error2) {
14505
+ signalFailure = `direct npm child ${value ?? "SIGTERM"} failed: ${String(error2)}`;
14506
+ }
14507
+ };
14508
+ child.once("exit", finish);
14509
+ child.on("error", onChildError);
14510
+ forceTimer = setTimeout(() => {
14511
+ signal("SIGKILL");
14512
+ confirmationTimer = setTimeout(fail, gracePeriodMs);
14513
+ }, gracePeriodMs);
14514
+ signal();
14515
+ if (child.exitCode !== null || child.signalCode !== null)
14516
+ finish();
14517
+ });
14518
+ }
14519
+ function terminateNpmProcessTree(child, invocation, env = process.env, gracePeriodMs = 5000) {
14520
+ if (!invocation.windowsCmdShim)
14521
+ return terminateDirectNpmChild(child, gracePeriodMs);
14522
+ const exitedSuccessfully = () => child.exitCode === 0 && child.signalCode === null;
14523
+ if (exitedSuccessfully())
14524
+ return Promise.resolve();
14525
+ if (child.pid === undefined) {
14526
+ return Promise.reject(new NpmTerminationUnknownError("cmd.exe child has no process ID"));
14527
+ }
14528
+ return new Promise((resolve7, reject) => {
14529
+ let settled = false;
14530
+ let childExited = child.exitCode !== null || child.signalCode !== null;
14531
+ let treeKillConfirmed = false;
14532
+ let treeKillFailure = null;
14533
+ let killer = null;
14534
+ const timeout = setTimeout(() => {
14535
+ try {
14536
+ killer?.kill();
14537
+ } catch {}
14538
+ if (treeKillConfirmed || exitedSuccessfully()) {
14539
+ settled = true;
14540
+ cleanup();
14541
+ resolve7();
14542
+ return;
14543
+ }
14544
+ fail(treeKillFailure ?? `taskkill.exe did not finish within ${gracePeriodMs}ms`);
14545
+ }, gracePeriodMs);
14546
+ const cleanup = () => {
14547
+ clearTimeout(timeout);
14548
+ child.removeListener("exit", onChildExit);
14549
+ };
14550
+ const succeedIfConfirmed = () => {
14551
+ if (settled || !childExited || !treeKillConfirmed)
14552
+ return;
14553
+ settled = true;
14554
+ cleanup();
14555
+ resolve7();
14556
+ };
14557
+ const fail = (detail) => {
14558
+ if (settled)
14559
+ return;
14560
+ settled = true;
14561
+ cleanup();
14562
+ reject(new NpmTerminationUnknownError(detail));
14563
+ };
14564
+ function onChildExit() {
14565
+ childExited = true;
14566
+ if (treeKillFailure !== null) {
14567
+ if (exitedSuccessfully()) {
14568
+ settled = true;
14569
+ cleanup();
14570
+ resolve7();
14571
+ } else {
14572
+ fail(treeKillFailure);
14573
+ }
14574
+ return;
14575
+ }
14576
+ succeedIfConfirmed();
14577
+ }
14578
+ const recordTreeKillFailure = (detail) => {
14579
+ if (settled)
14580
+ return;
14581
+ if (exitedSuccessfully()) {
14582
+ settled = true;
14583
+ cleanup();
14584
+ resolve7();
14585
+ return;
14586
+ }
14587
+ treeKillFailure = detail;
14588
+ if (childExited)
14589
+ fail(detail);
14590
+ };
14591
+ child.once("exit", onChildExit);
14592
+ const systemRoot = env.SystemRoot ?? env.SYSTEMROOT;
14593
+ const taskkill = systemRoot ? join10(systemRoot, "System32", "taskkill.exe") : "taskkill.exe";
14594
+ try {
14595
+ killer = spawn3(taskkill, ["/pid", String(child.pid), "/t", "/f"], {
14596
+ stdio: "ignore",
14597
+ windowsHide: true
14598
+ });
14599
+ killer.once("error", (error2) => recordTreeKillFailure(`taskkill.exe failed to start: ${String(error2)}`));
14600
+ killer.once("exit", (code) => {
14601
+ if (code !== 0) {
14602
+ recordTreeKillFailure(`taskkill.exe exited with code ${code ?? "unknown"}`);
14603
+ return;
14604
+ }
14605
+ treeKillConfirmed = true;
14606
+ succeedIfConfirmed();
14607
+ });
14608
+ } catch (error2) {
14609
+ recordTreeKillFailure(`taskkill.exe failed to start: ${String(error2)}`);
14610
+ }
14611
+ });
14612
+ }
14393
14613
  function npmSpawnEnv(resolved, baseEnv = process.env) {
14394
14614
  if (!resolved.binDir)
14395
14615
  return { ...baseEnv };
@@ -15142,6 +15362,10 @@ function pathValue(record, property) {
15142
15362
  return value;
15143
15363
  }
15144
15364
  function normalizeAliasPair(record, canonical, legacy, required) {
15365
+ if (isNullOrEmptyString(record[canonical]))
15366
+ delete record[canonical];
15367
+ if (isNullOrEmptyString(record[legacy]))
15368
+ delete record[legacy];
15145
15369
  const hasCanonical = hasOwn(record, canonical);
15146
15370
  const hasLegacy = hasOwn(record, legacy);
15147
15371
  if (!hasCanonical && !hasLegacy) {
@@ -15173,8 +15397,13 @@ function normalizeAliasPair(record, canonical, legacy, required) {
15173
15397
  delete record[legacy];
15174
15398
  }
15175
15399
  function validateOptionalCanonicalPath(record, property) {
15176
- if (hasOwn(record, property))
15177
- pathValue(record, property);
15400
+ if (!hasOwn(record, property))
15401
+ return;
15402
+ if (isNullOrEmptyString(record[property])) {
15403
+ delete record[property];
15404
+ return;
15405
+ }
15406
+ pathValue(record, property);
15178
15407
  }
15179
15408
  function normalizeZoomTargets(record) {
15180
15409
  if (!hasOwn(record, "targets"))
@@ -15330,6 +15559,10 @@ function prepareCanonicalEditArguments(toolName, rawArguments) {
15330
15559
  return record;
15331
15560
  }
15332
15561
  function normalizeEditPathAlias(record) {
15562
+ if (isNullOrEmptyString(record.path))
15563
+ delete record.path;
15564
+ if (isNullOrEmptyString(record.filePath))
15565
+ delete record.filePath;
15333
15566
  const hasCanonical = hasOwn(record, "path");
15334
15567
  const hasLegacy = hasOwn(record, "filePath");
15335
15568
  if (!hasCanonical && !hasLegacy)
@@ -15355,7 +15588,7 @@ function normalizeEditPathAlias(record) {
15355
15588
  }
15356
15589
  }
15357
15590
  function validateEditPath(record) {
15358
- if (!hasOwn(record, "path")) {
15591
+ if (!hasOwn(record, "path") || isNullOrEmptyString(record.path)) {
15359
15592
  throw new InvalidRequestError("'path' is required");
15360
15593
  }
15361
15594
  pathValue(record, "path");
@@ -15416,10 +15649,13 @@ function isEditSentinelItem(item) {
15416
15649
  if (record.oldString === null && ["startLine", "endLine"].some((key) => hasOwn(record, key) && record[key] !== null)) {
15417
15650
  return false;
15418
15651
  }
15419
- const newStringEmpty = !hasOwn(record, "newString") || isNullOrEmptyString(record.newString);
15420
- if (!newStringEmpty)
15421
- return false;
15422
- return !hasOwn(record, "content") || isNullOrEmptyString(record.content);
15652
+ return (!hasOwn(record, "newString") || isNullOrEmptyString(record.newString)) && (!hasOwn(record, "content") || isNullOrEmptyString(record.content)) && (!hasOwn(record, "replaceAll") || record.replaceAll === null || record.replaceAll === false) && isDefaultOccurrence(record.occurrence);
15653
+ }
15654
+ function hasMeaningfulFindPayload(item) {
15655
+ return isNonEmptyString(item.oldString);
15656
+ }
15657
+ function isDefaultOccurrence(value) {
15658
+ return value === undefined || value === null || value === 1;
15423
15659
  }
15424
15660
  function normalizeEditArraySentinels(record) {
15425
15661
  const value = record.edits;
@@ -15472,8 +15708,8 @@ function parseEditArray(value) {
15472
15708
  }
15473
15709
  return value;
15474
15710
  }
15475
- function stripLineRangeSentinels(item) {
15476
- const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15711
+ function normalizeEditItemSentinels(item) {
15712
+ const hadRangeFields = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15477
15713
  for (const key of [
15478
15714
  "oldString",
15479
15715
  "newString",
@@ -15486,7 +15722,19 @@ function stripLineRangeSentinels(item) {
15486
15722
  if (item[key] === null)
15487
15723
  delete item[key];
15488
15724
  }
15489
- if (!hasRangeField)
15725
+ const contentIsEmpty = item.content === "";
15726
+ if (hasMeaningfulFindPayload(item) && (!hasOwn(item, "content") || contentIsEmpty)) {
15727
+ for (const key of ["startLine", "endLine", "content"])
15728
+ delete item[key];
15729
+ if (hadRangeFields) {
15730
+ if (item.replaceAll === false)
15731
+ delete item.replaceAll;
15732
+ if (hasOwn(item, "occurrence") && isDefaultOccurrence(item.occurrence))
15733
+ delete item.occurrence;
15734
+ }
15735
+ return;
15736
+ }
15737
+ if (!isNonEmptyString(item.content))
15490
15738
  return;
15491
15739
  if (item.oldString === "")
15492
15740
  delete item.oldString;
@@ -15494,7 +15742,7 @@ function stripLineRangeSentinels(item) {
15494
15742
  delete item.newString;
15495
15743
  if (item.replaceAll === false)
15496
15744
  delete item.replaceAll;
15497
- if (item.occurrence === 1)
15745
+ if (hasOwn(item, "occurrence") && isDefaultOccurrence(item.occurrence))
15498
15746
  delete item.occurrence;
15499
15747
  }
15500
15748
  function normalizeEditItem(value, index) {
@@ -15505,7 +15753,7 @@ function normalizeEditItem(value, index) {
15505
15753
  const item = copyOwnProperties(source);
15506
15754
  normalizeItemAlias(item, "oldString", "oldText");
15507
15755
  normalizeItemAlias(item, "newString", "newText");
15508
- stripLineRangeSentinels(item);
15756
+ normalizeEditItemSentinels(item);
15509
15757
  const hasFindField = ["oldString", "newString", "replaceAll", "occurrence"].some((key) => hasOwn(item, key));
15510
15758
  const hasRangeField = ["startLine", "endLine", "content"].some((key) => hasOwn(item, key));
15511
15759
  if (hasFindField && hasRangeField) {
@@ -30707,6 +30955,9 @@ var GhShimConfigSchema = exports_external.object({
30707
30955
  enabled: exports_external.boolean().optional(),
30708
30956
  binary_path: exports_external.string().trim().refine(isAbsolute6, "gh_shim.binary_path must be absolute").optional()
30709
30957
  });
30958
+ var GhReadConfigSchema = exports_external.object({
30959
+ enabled: exports_external.boolean().optional()
30960
+ });
30710
30961
  var GitCoAuthorSchema = exports_external.string().trim().refine((value) => value === "off" || value === "auto" || /^[^<>\r\n]+\s+<[^<>\s@]+@[^<>\s@]+>$/.test(value), "git.co_author must be 'off', 'auto', or an explicit 'Name <email>' identity");
30711
30962
  var GitConfigSchema = exports_external.object({
30712
30963
  co_author: GitCoAuthorSchema.optional()
@@ -30768,6 +31019,7 @@ var AftConfigFieldsSchema = exports_external.object({
30768
31019
  bridge: BridgeConfigSchema.optional(),
30769
31020
  subc: SubcConfigSchema.optional(),
30770
31021
  gh_shim: GhShimConfigSchema.optional(),
31022
+ gh_read: GhReadConfigSchema.optional(),
30771
31023
  git: GitConfigSchema.optional()
30772
31024
  });
30773
31025
  var HarnessOverrideSchema = exports_external.preprocess((value) => {
@@ -31246,6 +31498,8 @@ function getStrippedTopLevelKeys(override) {
31246
31498
  stripped.push("subc");
31247
31499
  if (override.gh_shim !== undefined)
31248
31500
  stripped.push("gh_shim");
31501
+ if (override.gh_read !== undefined)
31502
+ stripped.push("gh_read");
31249
31503
  if (override.disabled_tools?.includes("aft_safety"))
31250
31504
  stripped.push("disabled_tools.aft_safety");
31251
31505
  return stripped;
@@ -31463,6 +31717,8 @@ var UNKNOWN_COMPLETION_CAP = 32;
31463
31717
  var DEFAULT_SESSION_ID2 = "__default__";
31464
31718
  var LOG_PREFIX = "[aft-plugin] bg-notifications:";
31465
31719
  var SUBC_NUDGE_LOG_INTERVAL_MS = 60000;
31720
+ var DEFAULT_BG_HOP_TIMEOUT_MS = 15000;
31721
+ var bgHopTimeoutMs = DEFAULT_BG_HOP_TIMEOUT_MS;
31466
31722
  var subcNudgesInFlight = new Map;
31467
31723
  var subcNudgeLogState = new Map;
31468
31724
  function consumeBgCompletion(sessionID, taskId) {
@@ -31690,11 +31946,20 @@ function handleSubcBgEventsNudge(drainContext) {
31690
31946
  }
31691
31947
  async function handleSubcBgEventsNudgeOnce(drainContext) {
31692
31948
  bridgeForDrain(drainContext);
31693
- stateFor(drainContext.sessionID).wakeDeferredTaskIds.clear();
31949
+ const state = stateFor(drainContext.sessionID);
31950
+ state.wakeDeferredTaskIds.clear();
31951
+ rearmHardStoppedWake(state, drainContext);
31694
31952
  await triggerWakeIfPending(drainContext, false, true, true);
31695
31953
  }
31696
31954
  function logSubcNudgeLifecycle(drainContext, kind, message) {
31697
- const key = `${kind}\x00${drainContext.directory}\x00${drainContext.sessionID}`;
31955
+ logDeliveryHop(drainContext, kind, message, {
31956
+ event: "subc_bg_nudge_delivery",
31957
+ cause: kind
31958
+ });
31959
+ }
31960
+ function logDeliveryHop(drainContext, kind, message, data, level = "info") {
31961
+ const taskKey = typeof data.task_id === "string" ? data.task_id : Array.isArray(data.task_ids) ? data.task_ids.join(",") : "";
31962
+ const key = `${kind}\x00${taskKey}\x00${drainContext.directory}\x00${drainContext.sessionID}`;
31698
31963
  const now = Date.now();
31699
31964
  const state = subcNudgeLogState.get(key);
31700
31965
  if (state && now - state.lastEmittedAt < SUBC_NUDGE_LOG_INTERVAL_MS) {
@@ -31703,13 +31968,59 @@ function logSubcNudgeLifecycle(drainContext, kind, message) {
31703
31968
  }
31704
31969
  const suppressed = state?.suppressed ?? 0;
31705
31970
  subcNudgeLogState.set(key, { lastEmittedAt: now, suppressed: 0 });
31706
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} ${message}`, {
31707
- event: "subc_bg_nudge_delivery",
31708
- cause: kind,
31971
+ const payload = {
31972
+ ...data,
31709
31973
  canonical_root: drainContext.directory,
31710
31974
  suppressed
31975
+ };
31976
+ if (level === "warn") {
31977
+ sessionWarn(drainContext.sessionID, `${LOG_PREFIX} ${message}`, payload);
31978
+ } else {
31979
+ sessionLog(drainContext.sessionID, `${LOG_PREFIX} ${message}`, payload);
31980
+ }
31981
+ }
31982
+ function logPerTaskDeliveryHop(drainContext, kind, message, taskIDs, data, level = "info") {
31983
+ for (const taskID of taskIDs) {
31984
+ logDeliveryHop(drainContext, kind, message, { ...data, task_id: taskID, task_ids: taskIDs }, level);
31985
+ }
31986
+ }
31987
+ function rearmHardStoppedWake(state, drainContext) {
31988
+ if (!state.wakeHardStopped)
31989
+ return;
31990
+ state.wakeHardStopped = false;
31991
+ state.wakeRetryAttempts = 0;
31992
+ state.retryDelayMs = null;
31993
+ logDeliveryHop(drainContext, "inject-rearmed", "delivery retry re-armed", {
31994
+ event: "bash_completion_inject_rearmed",
31995
+ task_ids: state.pendingCompletions.map((completion) => completion.task_id)
31711
31996
  });
31712
31997
  }
31998
+ async function withBgHopTimeout(operation, label) {
31999
+ let timer;
32000
+ try {
32001
+ return await Promise.race([
32002
+ operation,
32003
+ new Promise((_, reject) => {
32004
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${bgHopTimeoutMs}ms`)), bgHopTimeoutMs);
32005
+ timer.unref?.();
32006
+ })
32007
+ ]);
32008
+ } finally {
32009
+ if (timer)
32010
+ clearTimeout(timer);
32011
+ }
32012
+ }
32013
+ function promptAsyncFailure(response) {
32014
+ if (!response || typeof response !== "object" || Array.isArray(response))
32015
+ return null;
32016
+ const record2 = response;
32017
+ if (record2.error != null) {
32018
+ return typeof record2.error === "string" ? record2.error : JSON.stringify(record2.error);
32019
+ }
32020
+ if (record2.success === false)
32021
+ return String(record2.message ?? "promptAsync returned success=false");
32022
+ return null;
32023
+ }
31713
32024
  async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredCompletions = true, forceDrain = false) {
31714
32025
  const state = stateFor(drainContext.sessionID);
31715
32026
  if (!skipDrain && (forceDrain || state.outstandingTaskIds.size > 0 || !state.forcedDrainCompleted)) {
@@ -31732,11 +32043,12 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
31732
32043
  }
31733
32044
  return drainContext.client;
31734
32045
  };
31735
- const sendPrompt = async (client2) => {
31736
- if (typeof client2.session?.promptAsync !== "function") {
32046
+ const sendPrompt = async (client) => {
32047
+ const promptAsync = client.session?.promptAsync;
32048
+ if (typeof promptAsync !== "function") {
31737
32049
  throw new Error("wake client.session.promptAsync is unavailable");
31738
32050
  }
31739
- const promptContext = await resolvePromptContext(client2, drainContext.sessionID);
32051
+ const promptContext = await resolvePromptContext(client, drainContext.sessionID);
31740
32052
  const body = {
31741
32053
  noReply: false,
31742
32054
  parts: [{ type: "text", text: reminder, synthetic: true }]
@@ -31751,52 +32063,73 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
31751
32063
  }
31752
32064
  if (promptContext?.variant)
31753
32065
  body.variant = promptContext.variant;
31754
- const deliveryID2 = `aftdel_${randomUUID2()}`;
31755
- const wakeMeta = {
31756
- delivery_id: deliveryID2,
31757
- attempt: state.wakeRetryAttempts + 1,
31758
- task_ids: taskIDs,
31759
- directory: drainContext.directory,
31760
- reminder_sha256: hashReminder(reminder),
31761
- reminder_chars: reminder.length,
31762
- prompt_context: promptContext ? {
31763
- agent: promptContext.agent,
31764
- model: promptContext.model ? {
31765
- providerID: promptContext.model.providerID,
31766
- modelID: promptContext.model.modelID
31767
- } : null,
31768
- variant: promptContext.variant ?? null
31769
- } : null
31770
- };
31771
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync start`, {
31772
- event: "bash_completion_wake_prompt_async_start",
31773
- ...wakeMeta
31774
- });
31775
- try {
31776
- await client2.session.promptAsync({
31777
- path: { id: drainContext.sessionID },
31778
- body
31779
- });
31780
- } catch (err) {
31781
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync error`, {
31782
- event: "bash_completion_wake_prompt_async_error",
32066
+ const attemptPrompt = async (attemptBody, mode) => {
32067
+ const deliveryID2 = `aftdel_${randomUUID2()}`;
32068
+ const wakeMeta = {
31783
32069
  delivery_id: deliveryID2,
31784
32070
  attempt: state.wakeRetryAttempts + 1,
31785
32071
  task_ids: taskIDs,
31786
- error: err instanceof Error ? err.message : String(err)
32072
+ directory: drainContext.directory,
32073
+ reminder_sha256: hashReminder(reminder),
32074
+ reminder_chars: reminder.length,
32075
+ injection_mode: mode,
32076
+ prompt_context: promptContext ? {
32077
+ agent: promptContext.agent,
32078
+ model: promptContext.model ? {
32079
+ providerID: promptContext.model.providerID,
32080
+ modelID: promptContext.model.modelID
32081
+ } : null,
32082
+ variant: promptContext.variant ?? null
32083
+ } : null
32084
+ };
32085
+ logPerTaskDeliveryHop(drainContext, `inject-start-${mode}`, "session inject start", taskIDs, { event: "bash_completion_wake_prompt_async_start", ...wakeMeta });
32086
+ try {
32087
+ const response = await withBgHopTimeout(Promise.resolve(promptAsync({
32088
+ path: { id: drainContext.sessionID },
32089
+ body: attemptBody
32090
+ })), "session promptAsync injection");
32091
+ const failure = promptAsyncFailure(response);
32092
+ if (failure)
32093
+ throw new Error(failure);
32094
+ } catch (err) {
32095
+ logPerTaskDeliveryHop(drainContext, "inject-error", "session inject failed", taskIDs, {
32096
+ event: "bash_completion_wake_prompt_async_error",
32097
+ delivery_id: deliveryID2,
32098
+ attempt: state.wakeRetryAttempts + 1,
32099
+ injection_mode: mode,
32100
+ cause: err instanceof Error ? err.message : String(err)
32101
+ }, "warn");
32102
+ throw err;
32103
+ }
32104
+ logPerTaskDeliveryHop(drainContext, `inject-ok-${mode}`, "session inject ok", taskIDs, {
32105
+ event: "bash_completion_wake_prompt_async_ok",
32106
+ delivery_id: deliveryID2,
32107
+ attempt: state.wakeRetryAttempts + 1,
32108
+ injection_mode: mode
31787
32109
  });
31788
- throw err;
32110
+ return deliveryID2;
32111
+ };
32112
+ try {
32113
+ return await attemptPrompt(body, "mirrored-context");
32114
+ } catch (err) {
32115
+ if (!promptContext?.model)
32116
+ throw err;
32117
+ const fallbackBody = { ...body };
32118
+ delete fallbackBody.model;
32119
+ delete fallbackBody.variant;
32120
+ return attemptPrompt(fallbackBody, "model-free-fallback");
31789
32121
  }
31790
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} wake promptAsync ok`, {
31791
- event: "bash_completion_wake_prompt_async_ok",
31792
- delivery_id: deliveryID2,
31793
- attempt: state.wakeRetryAttempts + 1,
31794
- task_ids: taskIDs
31795
- });
31796
- return deliveryID2;
31797
32122
  };
31798
- const client = getInProcessClient();
31799
- const deliveryID = await sendPrompt(client);
32123
+ let deliveryID;
32124
+ try {
32125
+ deliveryID = await sendPrompt(getInProcessClient());
32126
+ } catch (err) {
32127
+ logPerTaskDeliveryHop(drainContext, "inject-error", "session inject failed", taskIDs, {
32128
+ event: "bash_completion_wake_prompt_async_error",
32129
+ cause: err instanceof Error ? err.message : String(err)
32130
+ }, "warn");
32131
+ throw err;
32132
+ }
31800
32133
  markDeliveredAwaitingAck(state, taskIDs);
31801
32134
  const acked = await ackCompletions(drainContext, deliveredCompletions, deliveryID);
31802
32135
  if (acked)
@@ -31812,7 +32145,12 @@ async function triggerWakeIfPending(drainContext, skipDrain, includeDeferredComp
31812
32145
  });
31813
32146
  return;
31814
32147
  }
31815
- 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)}`);
32148
+ if (hardStopped) {
32149
+ logDeliveryHop(drainContext, "inject-hard-stopped", "delivery retries paused", {
32150
+ event: "bash_completion_inject_hard_stopped",
32151
+ cause: err instanceof Error ? err.message : String(err)
32152
+ });
32153
+ }
31816
32154
  }, drainContext.sessionID, includeDeferredCompletions);
31817
32155
  }
31818
32156
  function formatSystemReminder(completions) {
@@ -31899,14 +32237,21 @@ async function drainCompletions(drainContext) {
31899
32237
  try {
31900
32238
  const bridge = bridgeForDrain(drainContext);
31901
32239
  const state = stateFor(drainContext.sessionID);
31902
- const response = await bridge.send("bash_drain_completions", {
31903
- session_id: drainContext.sessionID
31904
- });
32240
+ const response = await withBgHopTimeout(bridge.send("bash_drain_completions", { session_id: drainContext.sessionID }, { timeoutMs: bgHopTimeoutMs }), "bash_drain_completions");
31905
32241
  if (response.success === false) {
31906
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} drain failed: ${String(response.message ?? "unknown error")}`);
32242
+ logDeliveryHop(drainContext, "drain-error", "drain failed", {
32243
+ event: "bash_completion_drain_error",
32244
+ cause: String(response.message ?? "unknown error")
32245
+ }, "warn");
31907
32246
  return;
31908
32247
  }
31909
32248
  state.forcedDrainCompleted = true;
32249
+ const drainedTaskIds = Array.isArray(response.bg_completions) ? response.bg_completions.filter(isBgCompletion).map((completion) => completion.task_id) : [];
32250
+ logDeliveryHop(drainContext, "drain-ok", "drain ok", {
32251
+ event: "bash_completion_drain_ok",
32252
+ count: drainedTaskIds.length,
32253
+ task_ids: drainedTaskIds
32254
+ });
31910
32255
  const reackTaskIds = reconcileAwaitingAck(state, response.bg_completions);
31911
32256
  ingestDrainedBgCompletions(drainContext.sessionID, response.bg_completions);
31912
32257
  if (reackTaskIds.length > 0) {
@@ -31922,7 +32267,10 @@ async function drainCompletions(drainContext) {
31922
32267
  } catch (err) {
31923
32268
  if (drainContext.nudgeRef && isTerminalNudgeError(err))
31924
32269
  throw err;
31925
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} drain failed: ${err instanceof Error ? err.message : String(err)}`);
32270
+ logDeliveryHop(drainContext, "drain-error", "drain failed", {
32271
+ event: "bash_completion_drain_error",
32272
+ cause: err instanceof Error ? err.message : String(err)
32273
+ }, "warn");
31926
32274
  }
31927
32275
  }
31928
32276
  async function ackCompletions(drainContext, completions, deliveryID) {
@@ -31931,24 +32279,26 @@ async function ackCompletions(drainContext, completions, deliveryID) {
31931
32279
  return true;
31932
32280
  try {
31933
32281
  const bridge = bridgeForDrain(drainContext);
31934
- const response = await bridge.send("bash_ack_completions", {
31935
- session_id: drainContext.sessionID,
31936
- task_ids: taskIds
31937
- });
32282
+ const response = await withBgHopTimeout(bridge.send("bash_ack_completions", { session_id: drainContext.sessionID, task_ids: taskIds }, { timeoutMs: bgHopTimeoutMs }), "bash_ack_completions");
31938
32283
  if (response.success === false) {
31939
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} ack failed: ${String(response.message ?? "unknown error")}`);
32284
+ logPerTaskDeliveryHop(drainContext, "ack-error", "ack failed", taskIds, {
32285
+ event: "bash_completion_ack_error",
32286
+ cause: String(response.message ?? "unknown error")
32287
+ }, "warn");
31940
32288
  return false;
31941
32289
  }
31942
- sessionLog(drainContext.sessionID, `${LOG_PREFIX} ack ok`, {
32290
+ logPerTaskDeliveryHop(drainContext, "ack-ok", "ack ok", taskIds, {
31943
32291
  event: "bash_completion_ack_ok",
31944
- delivery_id: deliveryID ?? null,
31945
- task_ids: taskIds
32292
+ delivery_id: deliveryID ?? null
31946
32293
  });
31947
32294
  return true;
31948
32295
  } catch (err) {
31949
32296
  if (drainContext.nudgeRef && isTerminalNudgeError(err))
31950
32297
  throw err;
31951
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} ack failed: ${err instanceof Error ? err.message : String(err)}`);
32298
+ logPerTaskDeliveryHop(drainContext, "ack-error", "ack failed", taskIds, {
32299
+ event: "bash_completion_ack_error",
32300
+ cause: err instanceof Error ? err.message : String(err)
32301
+ }, "warn");
31952
32302
  return false;
31953
32303
  }
31954
32304
  }
@@ -32540,6 +32890,15 @@ async function callBashBridge(ctx, runtime, command, params = {}, options) {
32540
32890
  // src/bash-wait-detach.ts
32541
32891
  var BASH_WAIT_DETACH_MAGIC_KEYWORD = "&detach";
32542
32892
  var EMPTY_DETACH_MESSAGE = "(requested background detach)";
32893
+ var STANDALONE_DETACH_KEYWORD_SOURCE = `(^|[^\\p{L}\\p{N}_])${BASH_WAIT_DETACH_MAGIC_KEYWORD}(?![\\p{L}\\p{N}_])`;
32894
+ var STANDALONE_DETACH_KEYWORD_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SOURCE, "u");
32895
+ var STANDALONE_DETACH_KEYWORDS_PATTERN = new RegExp(STANDALONE_DETACH_KEYWORD_SOURCE, "gu");
32896
+ function containsStandaloneDetachKeyword(messageText) {
32897
+ return STANDALONE_DETACH_KEYWORD_PATTERN.test(messageText);
32898
+ }
32899
+ function stripStandaloneDetachKeywords(messageText) {
32900
+ return messageText.replace(STANDALONE_DETACH_KEYWORDS_PATTERN, "$1");
32901
+ }
32543
32902
  function userTextParts(output) {
32544
32903
  if (!output || typeof output !== "object")
32545
32904
  return [];
@@ -32561,9 +32920,9 @@ function stripUserMessageDetachKeyword(output) {
32561
32920
  const parts = userTextParts(output);
32562
32921
  const original = parts.map((part) => part.text).join(`
32563
32922
  `);
32564
- if (!original.includes(BASH_WAIT_DETACH_MAGIC_KEYWORD))
32923
+ if (!containsStandaloneDetachKeyword(original))
32565
32924
  return original;
32566
- const stripped = parts.map((part) => part.text.replaceAll(BASH_WAIT_DETACH_MAGIC_KEYWORD, "")).map((text) => text.replace(/[ \t]{2,}/g, " "));
32925
+ const stripped = parts.map((part) => stripStandaloneDetachKeywords(part.text)).map((text) => text.replace(/[ \t]{2,}/g, " "));
32567
32926
  if (stripped.join(`
32568
32927
  `).trim() === "") {
32569
32928
  if (parts[0])
@@ -32579,7 +32938,7 @@ function stripUserMessageDetachKeyword(output) {
32579
32938
  `);
32580
32939
  }
32581
32940
  function shouldDetachBashWaitOnUserMessage(config2, messageText) {
32582
- return resolveBashConfig(config2).detach_on_user_message || messageText.includes(BASH_WAIT_DETACH_MAGIC_KEYWORD);
32941
+ return resolveBashConfig(config2).detach_on_user_message || containsStandaloneDetachKeyword(messageText);
32583
32942
  }
32584
32943
  async function sendBashWaitDetach(bridge, sessionID) {
32585
32944
  const response = await bridge.send("bash_wait_detach", { session_id: sessionID }, { keepBridgeOnTimeout: true, transportTimeoutMs: BASH_TRANSPORT_TIMEOUT_MS });
@@ -33169,40 +33528,32 @@ import {
33169
33528
  import { dirname as dirname8 } from "node:path";
33170
33529
 
33171
33530
  // src/hooks/auto-update-checker/cache.ts
33172
- import { spawn as spawn3 } from "node:child_process";
33531
+ import { spawn as spawn4 } from "node:child_process";
33173
33532
  import { cpSync, existsSync as existsSync14, mkdtempSync, readFileSync as readFileSync10, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
33174
33533
  import { tmpdir as tmpdir2 } from "node:os";
33175
33534
  import { basename as basename3, dirname as dirname7, join as join16 } from "node:path";
33176
33535
  var import_comment_json3 = __toESM(require_src2(), 1);
33177
33536
 
33178
33537
  // src/hooks/auto-update-checker/checker.ts
33179
- var import_comment_json2 = __toESM(require_src2(), 1);
33180
33538
  import { existsSync as existsSync13, readFileSync as readFileSync9, statSync as statSync7, writeFileSync as writeFileSync5 } from "node:fs";
33181
- import { homedir as homedir14 } from "node:os";
33182
33539
  import { dirname as dirname6, isAbsolute as isAbsolute8, join as join15, resolve as resolve9 } from "node:path";
33183
33540
  import { fileURLToPath } from "node:url";
33541
+ var import_comment_json2 = __toESM(require_src2(), 1);
33184
33542
 
33185
33543
  // src/hooks/auto-update-checker/constants.ts
33186
- import { homedir as homedir13, platform as platform2 } from "node:os";
33187
33544
  import { join as join14 } from "node:path";
33188
33545
  var PACKAGE_NAME = "@cortexkit/aft-opencode";
33189
33546
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
33190
33547
  var NPM_FETCH_TIMEOUT = 1e4;
33191
- function getOpenCodeCacheRoot() {
33192
- if (platform2() === "win32") {
33193
- return join14(process.env.LOCALAPPDATA ?? homedir13(), "opencode");
33194
- }
33195
- return join14(homedir13(), ".cache", "opencode");
33548
+ function cacheDir() {
33549
+ return join14(getOpenCodeCacheRoot(), "packages");
33196
33550
  }
33197
- function getOpenCodeConfigRoot() {
33198
- if (platform2() === "win32") {
33199
- return join14(process.env.APPDATA ?? join14(homedir13(), "AppData", "Roaming"), "opencode");
33200
- }
33201
- return join14(process.env.XDG_CONFIG_HOME ?? join14(homedir13(), ".config"), "opencode");
33551
+ function userOpenCodeConfig() {
33552
+ return join14(getOpenCodeConfigRoot(), "opencode.json");
33553
+ }
33554
+ function userOpenCodeConfigJsonc() {
33555
+ return join14(getOpenCodeConfigRoot(), "opencode.jsonc");
33202
33556
  }
33203
- var CACHE_DIR = join14(getOpenCodeCacheRoot(), "packages");
33204
- var USER_OPENCODE_CONFIG = join14(getOpenCodeConfigRoot(), "opencode.json");
33205
- var USER_OPENCODE_CONFIG_JSONC = join14(getOpenCodeConfigRoot(), "opencode.jsonc");
33206
33557
 
33207
33558
  // src/hooks/auto-update-checker/types.ts
33208
33559
  var NpmPackageEnvelopeSchema = exports_external.object({
@@ -33262,8 +33613,8 @@ function getConfigPaths(directory) {
33262
33613
  return [
33263
33614
  join15(directory, ".opencode", "opencode.json"),
33264
33615
  join15(directory, ".opencode", "opencode.jsonc"),
33265
- USER_OPENCODE_CONFIG,
33266
- USER_OPENCODE_CONFIG_JSONC
33616
+ userOpenCodeConfig(),
33617
+ userOpenCodeConfigJsonc()
33267
33618
  ];
33268
33619
  }
33269
33620
  function resolvePathPluginSpec(spec, configPath) {
@@ -33368,7 +33719,7 @@ function findPluginEntry(directory) {
33368
33719
  }
33369
33720
  var cachedPackageVersion = null;
33370
33721
  function getSpecCachePackageJsonPath(spec) {
33371
- return join15(CACHE_DIR, spec, "node_modules", PACKAGE_NAME, "package.json");
33722
+ return join15(cacheDir(), spec, "node_modules", PACKAGE_NAME, "package.json");
33372
33723
  }
33373
33724
  function getCachedVersion(spec) {
33374
33725
  if (!spec && cachedPackageVersion)
@@ -33377,7 +33728,7 @@ function getCachedVersion(spec) {
33377
33728
  getCurrentRuntimePackageJsonPath(),
33378
33729
  spec ? getSpecCachePackageJsonPath(spec) : null,
33379
33730
  getSpecCachePackageJsonPath(`${PACKAGE_NAME}@latest`),
33380
- join15(homedir14(), ".cache", "opencode", "node_modules", PACKAGE_NAME, "package.json")
33731
+ join15(getOpenCodeCacheRoot(), "node_modules", PACKAGE_NAME, "package.json")
33381
33732
  ].filter(isString);
33382
33733
  for (const packageJsonPath of candidates) {
33383
33734
  try {
@@ -33424,6 +33775,7 @@ async function getLatestVersion(channel = "latest", options = {}) {
33424
33775
 
33425
33776
  // src/hooks/auto-update-checker/cache.ts
33426
33777
  var pendingSnapshots = new Map;
33778
+ var quarantinedInstallDirs = new Set;
33427
33779
  function createAutoUpdateSnapshot(installDir, packageJsonPath, packageName) {
33428
33780
  const packageDir = join16(installDir, "node_modules", packageName);
33429
33781
  const lockfilePath = join16(installDir, "package-lock.json");
@@ -33517,6 +33869,13 @@ function ensureDependencyVersion(packageJsonPath, packageName, version2) {
33517
33869
  return false;
33518
33870
  }
33519
33871
  }
33872
+ function restorePendingSnapshot(installDir) {
33873
+ const snapshot = pendingSnapshots.get(installDir);
33874
+ if (!snapshot)
33875
+ return;
33876
+ pendingSnapshots.delete(installDir);
33877
+ restoreAutoUpdateSnapshot(snapshot);
33878
+ }
33520
33879
  function removeInstalledPackage(installDir, packageName) {
33521
33880
  const packageDir = join16(installDir, "node_modules", packageName);
33522
33881
  if (!existsSync14(packageDir))
@@ -33537,9 +33896,10 @@ function resolveInstallContext(runtimePackageJsonPath = getCurrentRuntimePackage
33537
33896
  }
33538
33897
  return null;
33539
33898
  }
33540
- const legacyPackageJsonPath = join16(dirname7(CACHE_DIR), "package.json");
33899
+ const cacheRoot = dirname7(cacheDir());
33900
+ const legacyPackageJsonPath = join16(cacheRoot, "package.json");
33541
33901
  if (existsSync14(legacyPackageJsonPath)) {
33542
- return { installDir: dirname7(CACHE_DIR), packageJsonPath: legacyPackageJsonPath };
33902
+ return { installDir: cacheRoot, packageJsonPath: legacyPackageJsonPath };
33543
33903
  }
33544
33904
  return null;
33545
33905
  }
@@ -33550,6 +33910,12 @@ function preparePackageUpdate(version2, packageName = PACKAGE_NAME, runtimePacka
33550
33910
  warn2("[auto-update-checker] No install context found for auto-update");
33551
33911
  return null;
33552
33912
  }
33913
+ if (quarantinedInstallDirs.has(installContext.installDir)) {
33914
+ const recoverySnapshot = pendingSnapshots.get(installContext.installDir);
33915
+ const recoveryDetail = recoverySnapshot ? ` Recovery snapshot: ${recoverySnapshot.tempDir}` : "";
33916
+ warn2(`[auto-update-checker] Auto-update blocked after unconfirmed npm termination; ` + `restart OpenCode to retry.${recoveryDetail}`);
33917
+ return null;
33918
+ }
33553
33919
  const snapshot = createAutoUpdateSnapshot(installContext.installDir, installContext.packageJsonPath, packageName);
33554
33920
  pendingSnapshots.set(installContext.installDir, snapshot);
33555
33921
  if (!ensureDependencyVersion(installContext.packageJsonPath, packageName, version2)) {
@@ -33573,18 +33939,29 @@ async function runNpmInstallSafe(installDir, options = {}) {
33573
33939
  let timeout = null;
33574
33940
  let stderrTail = "";
33575
33941
  try {
33576
- if (options.signal?.aborted)
33942
+ if (options.signal?.aborted) {
33943
+ restorePendingSnapshot(installDir);
33577
33944
  return { ok: false, reason: "aborted" };
33945
+ }
33578
33946
  const npm = resolveNpm();
33579
33947
  if (!npm) {
33948
+ restorePendingSnapshot(installDir);
33580
33949
  const reason = "npm not found on PATH or in known version-manager locations";
33581
33950
  warnNpmInstallFailure(reason, stderrTail);
33582
33951
  return { ok: false, reason };
33583
33952
  }
33584
- const proc = spawn3(npm.command, ["install", "--no-audit", "--no-fund", "--no-progress", "--ignore-scripts"], {
33953
+ const invocation = npmInvocation(npm, [
33954
+ "install",
33955
+ "--no-audit",
33956
+ "--no-fund",
33957
+ "--no-progress",
33958
+ "--ignore-scripts"
33959
+ ]);
33960
+ const proc = spawn4(invocation.command, invocation.args, {
33585
33961
  cwd: installDir,
33586
33962
  stdio: ["ignore", "pipe", "pipe"],
33587
- env: npmSpawnEnv(npm)
33963
+ env: { ...npmSpawnEnv(npm), ...invocation.env },
33964
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
33588
33965
  });
33589
33966
  proc.stderr?.on("data", (chunk) => {
33590
33967
  stderrTail += chunk.toString("utf8");
@@ -33593,12 +33970,11 @@ async function runNpmInstallSafe(installDir, options = {}) {
33593
33970
  }
33594
33971
  });
33595
33972
  proc.stdout?.on("data", () => {});
33973
+ let terminationPromise = null;
33596
33974
  const abortProcess = () => {
33597
- try {
33598
- proc.kill();
33599
- } catch {}
33975
+ terminationPromise ??= terminateNpmProcessTree(proc, invocation);
33976
+ return terminationPromise;
33600
33977
  };
33601
- options.signal?.addEventListener("abort", abortProcess, { once: true });
33602
33978
  const exitPromise = new Promise((resolveExit) => {
33603
33979
  proc.on("error", (err) => resolveExit({ ok: false, reason: `spawn error: ${String(err)}` }));
33604
33980
  proc.on("exit", (code) => resolveExit(code === 0 ? { ok: true } : { ok: false, reason: `npm install exited with code ${code ?? "signal/unknown"}` }));
@@ -33606,16 +33982,29 @@ async function runNpmInstallSafe(installDir, options = {}) {
33606
33982
  const timeoutPromise = new Promise((resolveTimeout) => {
33607
33983
  timeout = setTimeout(() => resolveTimeout("timeout"), options.timeoutMs ?? 60000);
33608
33984
  });
33609
- const result = await Promise.race([exitPromise, timeoutPromise]);
33610
- options.signal?.removeEventListener("abort", abortProcess);
33611
- if (result === "timeout" || options.signal?.aborted) {
33612
- abortProcess();
33613
- const snapshot2 = pendingSnapshots.get(installDir);
33614
- if (snapshot2) {
33615
- pendingSnapshots.delete(installDir);
33616
- restoreAutoUpdateSnapshot(snapshot2);
33617
- }
33618
- const reason = options.signal?.aborted ? "aborted" : "timeout";
33985
+ let resolveAbort;
33986
+ const abortPromise = new Promise((resolve10) => {
33987
+ resolveAbort = resolve10;
33988
+ });
33989
+ const onAbort = () => resolveAbort("abort");
33990
+ options.signal?.addEventListener("abort", onAbort, { once: true });
33991
+ if (options.signal?.aborted)
33992
+ onAbort();
33993
+ const result = await Promise.race([exitPromise, timeoutPromise, abortPromise]);
33994
+ options.signal?.removeEventListener("abort", onAbort);
33995
+ if (result === "timeout" || result === "abort") {
33996
+ try {
33997
+ await abortProcess();
33998
+ } catch (error53) {
33999
+ quarantinedInstallDirs.add(installDir);
34000
+ const recoverySnapshot = pendingSnapshots.get(installDir);
34001
+ const recoveryDetail = recoverySnapshot ? `; auto-update quarantined for this session; recovery snapshot: ${recoverySnapshot.tempDir}` : "; auto-update quarantined for this session";
34002
+ const reason2 = `termination outcome unknown: ${String(error53)}${recoveryDetail}`;
34003
+ warnNpmInstallFailure(reason2, stderrTail);
34004
+ return { ok: false, reason: reason2, stderrTail: stderrTail || undefined };
34005
+ }
34006
+ restorePendingSnapshot(installDir);
34007
+ const reason = result === "abort" ? "aborted" : "timeout";
33619
34008
  warnNpmInstallFailure(reason, stderrTail);
33620
34009
  return { ok: false, reason, stderrTail: stderrTail || undefined };
33621
34010
  }
@@ -33631,11 +34020,7 @@ async function runNpmInstallSafe(installDir, options = {}) {
33631
34020
  }
33632
34021
  return { ...result, stderrTail: stderrTail || undefined };
33633
34022
  } catch (err) {
33634
- const snapshot = pendingSnapshots.get(installDir);
33635
- if (snapshot) {
33636
- pendingSnapshots.delete(installDir);
33637
- restoreAutoUpdateSnapshot(snapshot);
33638
- }
34023
+ restorePendingSnapshot(installDir);
33639
34024
  const reason = `exception: ${String(err)}`;
33640
34025
  warnNpmInstallFailure(reason, stderrTail);
33641
34026
  return { ok: false, reason, stderrTail: stderrTail || undefined };
@@ -33910,7 +34295,7 @@ function showToast(ctx, title, message, variant = "info", duration3 = 3000) {
33910
34295
  }
33911
34296
 
33912
34297
  // src/lsp-auto-install.ts
33913
- import { spawn as spawn4 } from "node:child_process";
34298
+ import { spawn as spawn5 } from "node:child_process";
33914
34299
  import { createHash as createHash6 } from "node:crypto";
33915
34300
  import {
33916
34301
  createReadStream,
@@ -33925,6 +34310,7 @@ import {
33925
34310
  import { join as join19 } from "node:path";
33926
34311
 
33927
34312
  // src/lsp-cache.ts
34313
+ import { randomUUID as randomUUID3 } from "node:crypto";
33928
34314
  import {
33929
34315
  closeSync as closeSync6,
33930
34316
  mkdirSync as mkdirSync7,
@@ -34006,27 +34392,46 @@ var STALE_LOCK_MS2 = 30 * 60 * 1000;
34006
34392
  function acquireInstallLock(lockKey) {
34007
34393
  mkdirSync7(lspPackageDir(lockKey), { recursive: true });
34008
34394
  const lock = lockPath(lockKey);
34395
+ const token = randomUUID3();
34009
34396
  const tryClaim = () => {
34010
34397
  try {
34011
34398
  const fd = openSync6(lock, "wx");
34012
34399
  try {
34013
34400
  writeFileSync8(fd, `${process.pid}
34014
34401
  ${new Date().toISOString()}
34402
+ ${token}
34015
34403
  `);
34016
34404
  } finally {
34017
34405
  closeSync6(fd);
34018
34406
  }
34019
- return true;
34407
+ let retained = false;
34408
+ let released = false;
34409
+ return {
34410
+ retain() {
34411
+ retained = true;
34412
+ },
34413
+ release() {
34414
+ if (released)
34415
+ return;
34416
+ released = true;
34417
+ if (retained) {
34418
+ warn2(`[lsp] retaining install lock for ${lockKey} after unconfirmed npm termination; ` + `cross-process retries remain blocked until stale-lock recovery`);
34419
+ return;
34420
+ }
34421
+ releaseInstallLock(lockKey, token);
34422
+ }
34423
+ };
34020
34424
  } catch (err) {
34021
34425
  const code = err.code;
34022
34426
  if (code === "EEXIST")
34023
- return false;
34427
+ return null;
34024
34428
  warn2(`[lsp] unexpected error acquiring install lock for ${lockKey}: ${err}`);
34025
- return false;
34429
+ return null;
34026
34430
  }
34027
34431
  };
34028
- if (tryClaim())
34029
- return true;
34432
+ const claimed = tryClaim();
34433
+ if (claimed)
34434
+ return claimed;
34030
34435
  let owningPid = null;
34031
34436
  let lockMtimeMs = 0;
34032
34437
  try {
@@ -34044,7 +34449,7 @@ ${new Date().toISOString()}
34044
34449
  const skipLiveness = process.platform === "win32";
34045
34450
  const ownerAlive = !skipLiveness && owningPid !== null && isProcessAlive3(owningPid);
34046
34451
  if (skipLiveness ? ageWithinFresh : ownerAlive && ageWithinFresh) {
34047
- return false;
34452
+ return null;
34048
34453
  }
34049
34454
  log2(`[lsp] reclaiming install lock for ${lockKey} (owner_pid=${owningPid ?? "unknown"}, alive=${ownerAlive}, age_ms=${age})`);
34050
34455
  try {
@@ -34063,16 +34468,18 @@ function isProcessAlive3(pid) {
34063
34468
  return true;
34064
34469
  }
34065
34470
  }
34066
- function releaseInstallLock(lockKey) {
34471
+ function releaseInstallLock(lockKey, acquisitionToken) {
34067
34472
  const lock = lockPath(lockKey);
34068
34473
  try {
34069
34474
  let owningPid = null;
34475
+ let owningToken = "";
34070
34476
  try {
34071
34477
  const raw = readFileSync12(lock, "utf8");
34072
- const firstLine = raw.split(/\r?\n/, 1)[0]?.trim() ?? "";
34073
- const parsed = Number.parseInt(firstLine, 10);
34478
+ const lines = raw.split(/\r?\n/);
34479
+ const parsed = Number.parseInt(lines[0]?.trim() ?? "", 10);
34074
34480
  if (Number.isFinite(parsed) && parsed > 0)
34075
34481
  owningPid = parsed;
34482
+ owningToken = lines[2]?.trim() ?? "";
34076
34483
  } catch (readErr) {
34077
34484
  const code = readErr.code;
34078
34485
  if (code === "ENOENT")
@@ -34080,8 +34487,8 @@ function releaseInstallLock(lockKey) {
34080
34487
  warn2(`[lsp] could not read install lock for ${lockKey} during release: ${readErr}`);
34081
34488
  return;
34082
34489
  }
34083
- if (owningPid !== process.pid) {
34084
- log2(`[lsp] not releasing install lock for ${lockKey}: owned by pid ${owningPid ?? "unknown"} (we are ${process.pid})`);
34490
+ if (owningPid !== process.pid || owningToken !== acquisitionToken) {
34491
+ log2(`[lsp] not releasing install lock for ${lockKey}: acquisition ownership changed`);
34085
34492
  return;
34086
34493
  }
34087
34494
  try {
@@ -34097,12 +34504,13 @@ function releaseInstallLock(lockKey) {
34097
34504
  }
34098
34505
  }
34099
34506
  async function withInstallLock(lockKey, task) {
34100
- if (!acquireInstallLock(lockKey))
34507
+ const lease = acquireInstallLock(lockKey);
34508
+ if (!lease)
34101
34509
  return null;
34102
34510
  try {
34103
- return await task();
34511
+ return await task(lease);
34104
34512
  } finally {
34105
- releaseInstallLock(lockKey);
34513
+ lease.release();
34106
34514
  }
34107
34515
  }
34108
34516
  var VERSION_CHECK_FILE = ".aft-version-check";
@@ -34447,6 +34855,7 @@ function buildExtensionMap(specs) {
34447
34855
  return byExt;
34448
34856
  }
34449
34857
  var inFlightAutoInstalls = new Set;
34858
+ var quarantinedNpmInstalls = new Set;
34450
34859
  function trackInFlightAutoInstall(controller, promise2) {
34451
34860
  const entry = { controller, promise: promise2 };
34452
34861
  inFlightAutoInstalls.add(entry);
@@ -34494,7 +34903,7 @@ function ensureInstallAnchor(cwd) {
34494
34903
  warn2(`[lsp] could not write package.json stub in ${cwd}: ${err}`);
34495
34904
  }
34496
34905
  }
34497
- function runInstall(spec, version2, cwd, signal) {
34906
+ function runInstall(spec, version2, cwd, installLock, signal) {
34498
34907
  return new Promise((resolve10) => {
34499
34908
  const target = `${spec.npm}@${version2}`;
34500
34909
  log2(`[lsp] installing ${target} to ${cwd}`);
@@ -34510,19 +34919,25 @@ function runInstall(spec, version2, cwd, signal) {
34510
34919
  return;
34511
34920
  }
34512
34921
  ensureInstallAnchor(cwd);
34513
- const child = spawn4(npm.command, ["install", "--no-save", "--ignore-scripts", "--silent", target], {
34922
+ const invocation = npmInvocation(npm, [
34923
+ "install",
34924
+ "--no-save",
34925
+ "--ignore-scripts",
34926
+ "--silent",
34927
+ target
34928
+ ]);
34929
+ const child = spawn5(invocation.command, invocation.args, {
34514
34930
  stdio: ["ignore", "pipe", "pipe"],
34515
34931
  cwd,
34516
- env: npmSpawnEnv(npm)
34932
+ env: { ...npmSpawnEnv(npm), ...invocation.env },
34933
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments
34517
34934
  });
34518
34935
  child.unref();
34519
34936
  let stderrBuf = "";
34520
34937
  let settled = false;
34521
- let killTimer = null;
34938
+ let terminationPromise = null;
34522
34939
  const cleanup = () => {
34523
34940
  signal?.removeEventListener("abort", onAbort);
34524
- if (killTimer)
34525
- clearTimeout(killTimer);
34526
34941
  };
34527
34942
  const finish = (ok) => {
34528
34943
  if (settled)
@@ -34531,14 +34946,31 @@ function runInstall(spec, version2, cwd, signal) {
34531
34946
  cleanup();
34532
34947
  resolve10(ok);
34533
34948
  };
34949
+ const childCompletedSuccessfully = () => child.exitCode === 0 && child.signalCode === null;
34950
+ const finishAfterConfirmedTermination = () => {
34951
+ const completedSuccessfully = childCompletedSuccessfully();
34952
+ if (completedSuccessfully)
34953
+ log2(`[lsp] installed ${target}`);
34954
+ finish(completedSuccessfully);
34955
+ };
34534
34956
  const onAbort = () => {
34957
+ if (childCompletedSuccessfully()) {
34958
+ finishAfterConfirmedTermination();
34959
+ return;
34960
+ }
34535
34961
  warn2(`[lsp] install ${target} aborted during shutdown`);
34536
- child.kill("SIGTERM");
34537
- killTimer = setTimeout(() => {
34538
- if (!settled)
34539
- child.kill("SIGKILL");
34540
- }, 5000);
34541
- killTimer.unref?.();
34962
+ terminationPromise ??= terminateNpmProcessTree(child, invocation);
34963
+ terminationPromise.then(finishAfterConfirmedTermination, (terminationError) => {
34964
+ if (child.pid === undefined) {
34965
+ error52(`[lsp] install ${target} failed to spawn during shutdown: ${terminationError}`);
34966
+ finish(false);
34967
+ return;
34968
+ }
34969
+ quarantinedNpmInstalls.add(spec.npm);
34970
+ installLock.retain();
34971
+ error52(`[lsp] install ${target} termination outcome unknown; quarantining retries for this session: ${String(terminationError)}`);
34972
+ finish(false);
34973
+ });
34542
34974
  };
34543
34975
  signal?.addEventListener("abort", onAbort, { once: true });
34544
34976
  if (signal?.aborted)
@@ -34552,10 +34984,16 @@ function runInstall(spec, version2, cwd, signal) {
34552
34984
  }
34553
34985
  });
34554
34986
  child.on("error", (err) => {
34555
- error52(`[lsp] install ${target} failed to spawn: ${err}`);
34987
+ if (settled)
34988
+ return;
34989
+ error52(`[lsp] install ${target} child-process error: ${err}`);
34990
+ if (terminationPromise && child.pid !== undefined)
34991
+ return;
34556
34992
  finish(false);
34557
34993
  });
34558
34994
  child.on("exit", (code) => {
34995
+ if (settled || terminationPromise)
34996
+ return;
34559
34997
  if (code === 0) {
34560
34998
  log2(`[lsp] installed ${target}`);
34561
34999
  finish(true);
@@ -34568,7 +35006,13 @@ ${stderrBuf.trim()}`);
34568
35006
  });
34569
35007
  }
34570
35008
  async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
34571
- const outcome = await withInstallLock(spec.npm, async () => {
35009
+ if (quarantinedNpmInstalls.has(spec.npm)) {
35010
+ return {
35011
+ started: false,
35012
+ reason: "install blocked after unconfirmed npm termination; restart the host to retry"
35013
+ };
35014
+ }
35015
+ const outcome = await withInstallLock(spec.npm, async (installLock) => {
34572
35016
  const { version: version2, probe } = await resolveTargetVersion(spec, config2, fetchImpl);
34573
35017
  if (!version2) {
34574
35018
  const installed = isInstalled(spec.npm, spec.binary);
@@ -34604,7 +35048,7 @@ async function ensureServerInstalled(spec, config2, fetchImpl, signal) {
34604
35048
  log2(`[lsp] reinstalling ${spec.npm}@${version2}: no installed-version metadata recorded`);
34605
35049
  }
34606
35050
  }
34607
- const ok = await runInstall(spec, version2, cachedPackageDir(spec.npm), signal).catch((err) => {
35051
+ const ok = await runInstall(spec, version2, cachedPackageDir(spec.npm), installLock, signal).catch((err) => {
34608
35052
  error52(`[lsp] background install ${spec.npm} crashed: ${err}`);
34609
35053
  return false;
34610
35054
  });
@@ -34787,69 +35231,69 @@ import { Readable as Readable3 } from "node:stream";
34787
35231
  import { pipeline as pipeline3 } from "node:stream/promises";
34788
35232
 
34789
35233
  // src/lsp-github-table.ts
34790
- function exe(platform3, name) {
34791
- return platform3 === "win32" ? `${name}.exe` : name;
35234
+ function exe(platform2, name) {
35235
+ return platform2 === "win32" ? `${name}.exe` : name;
34792
35236
  }
34793
35237
  var CLANGD = {
34794
35238
  id: "clangd",
34795
35239
  githubRepo: "clangd/clangd",
34796
35240
  binary: "clangd",
34797
- resolveAsset: (platform3, _arch, version2) => {
34798
- const platformName = platform3 === "darwin" ? "mac" : platform3 === "linux" ? "linux" : "windows";
35241
+ resolveAsset: (platform2, _arch, version2) => {
35242
+ const platformName = platform2 === "darwin" ? "mac" : platform2 === "linux" ? "linux" : "windows";
34799
35243
  return { name: `clangd-${platformName}-${version2}.zip`, archive: "zip" };
34800
35244
  },
34801
- binaryPathInArchive: (platform3, _arch, version2) => `clangd_${version2}/bin/${exe(platform3, "clangd")}`
35245
+ binaryPathInArchive: (platform2, _arch, version2) => `clangd_${version2}/bin/${exe(platform2, "clangd")}`
34802
35246
  };
34803
35247
  var LUA_LS = {
34804
35248
  id: "lua-ls",
34805
35249
  githubRepo: "LuaLS/lua-language-server",
34806
35250
  binary: "lua-language-server",
34807
- resolveAsset: (platform3, arch, version2) => {
34808
- const ext = platform3 === "win32" ? "zip" : "tar.gz";
34809
- const platformName = platform3 === "darwin" ? "darwin" : platform3 === "linux" ? "linux" : "win32";
35251
+ resolveAsset: (platform2, arch, version2) => {
35252
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
35253
+ const platformName = platform2 === "darwin" ? "darwin" : platform2 === "linux" ? "linux" : "win32";
34810
35254
  const archName = arch === "arm64" ? "arm64" : "x64";
34811
35255
  return {
34812
35256
  name: `lua-language-server-${version2}-${platformName}-${archName}.${ext}`,
34813
35257
  archive: ext
34814
35258
  };
34815
35259
  },
34816
- binaryPathInArchive: (platform3, _arch, _version) => `bin/${exe(platform3, "lua-language-server")}`
35260
+ binaryPathInArchive: (platform2, _arch, _version) => `bin/${exe(platform2, "lua-language-server")}`
34817
35261
  };
34818
35262
  var ZLS = {
34819
35263
  id: "zls",
34820
35264
  githubRepo: "zigtools/zls",
34821
35265
  binary: "zls",
34822
- resolveAsset: (platform3, arch, _version) => {
34823
- const ext = platform3 === "win32" ? "zip" : "tar.xz";
35266
+ resolveAsset: (platform2, arch, _version) => {
35267
+ const ext = platform2 === "win32" ? "zip" : "tar.xz";
34824
35268
  const archName = arch === "arm64" ? "aarch64" : "x86_64";
34825
- const platformName = platform3 === "darwin" ? "macos" : platform3 === "linux" ? "linux" : "windows";
35269
+ const platformName = platform2 === "darwin" ? "macos" : platform2 === "linux" ? "linux" : "windows";
34826
35270
  return { name: `zls-${archName}-${platformName}.${ext}`, archive: ext };
34827
35271
  },
34828
- binaryPathInArchive: (platform3, _arch, _version) => exe(platform3, "zls")
35272
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "zls")
34829
35273
  };
34830
35274
  var TINYMIST = {
34831
35275
  id: "tinymist",
34832
35276
  githubRepo: "Myriad-Dreamin/tinymist",
34833
35277
  binary: "tinymist",
34834
- resolveAsset: (platform3, arch, _version) => {
35278
+ resolveAsset: (platform2, arch, _version) => {
34835
35279
  const archName = arch === "arm64" ? "aarch64" : "x86_64";
34836
- const triple = platform3 === "darwin" ? "apple-darwin" : platform3 === "linux" ? "unknown-linux-gnu" : "pc-windows-msvc";
34837
- const ext = platform3 === "win32" ? "zip" : "tar.gz";
35280
+ const triple = platform2 === "darwin" ? "apple-darwin" : platform2 === "linux" ? "unknown-linux-gnu" : "pc-windows-msvc";
35281
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
34838
35282
  return { name: `tinymist-${archName}-${triple}.${ext}`, archive: ext };
34839
35283
  },
34840
- binaryPathInArchive: (platform3, _arch, _version) => exe(platform3, "tinymist")
35284
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "tinymist")
34841
35285
  };
34842
35286
  var TEXLAB = {
34843
35287
  id: "texlab",
34844
35288
  githubRepo: "latex-lsp/texlab",
34845
35289
  binary: "texlab",
34846
- resolveAsset: (platform3, arch, _version) => {
35290
+ resolveAsset: (platform2, arch, _version) => {
34847
35291
  const archName = arch === "arm64" ? "aarch64" : "x86_64";
34848
- const platformName = platform3 === "darwin" ? "macos" : platform3 === "linux" ? "linux" : "windows";
34849
- const ext = platform3 === "win32" ? "zip" : "tar.gz";
35292
+ const platformName = platform2 === "darwin" ? "macos" : platform2 === "linux" ? "linux" : "windows";
35293
+ const ext = platform2 === "win32" ? "zip" : "tar.gz";
34850
35294
  return { name: `texlab-${archName}-${platformName}.${ext}`, archive: ext };
34851
35295
  },
34852
- binaryPathInArchive: (platform3, _arch, _version) => exe(platform3, "texlab")
35296
+ binaryPathInArchive: (platform2, _arch, _version) => exe(platform2, "texlab")
34853
35297
  };
34854
35298
  var GITHUB_LSP_TABLE = [
34855
35299
  CLANGD,
@@ -34859,14 +35303,14 @@ var GITHUB_LSP_TABLE = [
34859
35303
  TEXLAB
34860
35304
  ];
34861
35305
  function detectHostPlatform() {
34862
- const platform3 = process.platform;
34863
- if (platform3 !== "darwin" && platform3 !== "linux" && platform3 !== "win32")
35306
+ const platform2 = process.platform;
35307
+ if (platform2 !== "darwin" && platform2 !== "linux" && platform2 !== "win32")
34864
35308
  return null;
34865
35309
  const arch = process.arch;
34866
35310
  if (arch === "x64")
34867
- return { platform: platform3, arch: "x64" };
35311
+ return { platform: platform2, arch: "x64" };
34868
35312
  if (arch === "arm64")
34869
- return { platform: platform3, arch: "arm64" };
35313
+ return { platform: platform2, arch: "arm64" };
34870
35314
  return null;
34871
35315
  }
34872
35316
 
@@ -34884,12 +35328,12 @@ function ghExtractDir(spec) {
34884
35328
  return join20(ghPackageDir(spec), "extracted");
34885
35329
  }
34886
35330
  var INSTALLED_META_FILE2 = ".aft-installed";
34887
- function ghBinaryPath(spec, platform3) {
34888
- const ext = platform3 === "win32" ? ".exe" : "";
35331
+ function ghBinaryPath(spec, platform2) {
35332
+ const ext = platform2 === "win32" ? ".exe" : "";
34889
35333
  return join20(ghBinDir(spec), `${spec.binary}${ext}`);
34890
35334
  }
34891
- function isGithubInstalled(spec, platform3) {
34892
- for (const candidate of ghBinaryCandidates(spec, platform3)) {
35335
+ function isGithubInstalled(spec, platform2) {
35336
+ for (const candidate of ghBinaryCandidates(spec, platform2)) {
34893
35337
  try {
34894
35338
  if (statSync11(join20(ghBinDir(spec), candidate)).isFile())
34895
35339
  return true;
@@ -34897,8 +35341,8 @@ function isGithubInstalled(spec, platform3) {
34897
35341
  }
34898
35342
  return false;
34899
35343
  }
34900
- function ghBinaryCandidates(spec, platform3) {
34901
- if (platform3 !== "win32")
35344
+ function ghBinaryCandidates(spec, platform2) {
35345
+ if (platform2 !== "win32")
34902
35346
  return [spec.binary];
34903
35347
  return [spec.binary, `${spec.binary}.cmd`, `${spec.binary}.exe`, `${spec.binary}.bat`];
34904
35348
  }
@@ -35278,10 +35722,10 @@ function quarantineCachedGithubInstall(spec, reason) {
35278
35722
  warn2(`[lsp] tofu_mismatch ${spec.id}: failed to quarantine cache entry: ${err}`);
35279
35723
  }
35280
35724
  }
35281
- function validateCachedGithubInstall(spec, platform3) {
35725
+ function validateCachedGithubInstall(spec, platform2) {
35282
35726
  const packageDir = ghPackageDir(spec);
35283
35727
  const meta3 = readGithubInstalledMetaIn(packageDir);
35284
- const binaryPath = ghBinaryCandidates(spec, platform3).map((candidate) => join20(ghBinDir(spec), candidate)).find((candidate) => {
35728
+ const binaryPath = ghBinaryCandidates(spec, platform2).map((candidate) => join20(ghBinDir(spec), candidate)).find((candidate) => {
35285
35729
  try {
35286
35730
  return statSync11(candidate).isFile();
35287
35731
  } catch {
@@ -35345,11 +35789,11 @@ function runPlatformExtractor(archivePath, destDir, archiveType) {
35345
35789
  }
35346
35790
  throw new Error(`unsupported archive type: ${archiveType}`);
35347
35791
  }
35348
- async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl, signal) {
35792
+ async function downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal) {
35349
35793
  const version2 = stripTagV(tag);
35350
- const expected = spec.resolveAsset(platform3, arch, version2);
35794
+ const expected = spec.resolveAsset(platform2, arch, version2);
35351
35795
  if (!expected) {
35352
- warn2(`[lsp] ${spec.id}: unsupported platform/arch combo ${platform3}/${arch}`);
35796
+ warn2(`[lsp] ${spec.id}: unsupported platform/arch combo ${platform2}/${arch}`);
35353
35797
  return null;
35354
35798
  }
35355
35799
  const matchingAsset = assets.find((a) => a.name === expected.name);
@@ -35399,16 +35843,16 @@ async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl,
35399
35843
  unlinkSync7(archivePath);
35400
35844
  } catch {}
35401
35845
  }
35402
- const innerBinaryPath = join20(extractDir, spec.binaryPathInArchive(platform3, arch, version2));
35846
+ const innerBinaryPath = join20(extractDir, spec.binaryPathInArchive(platform2, arch, version2));
35403
35847
  if (!existsSync18(innerBinaryPath)) {
35404
35848
  error52(`[lsp] ${spec.id}: extracted binary not found at ${innerBinaryPath}`);
35405
35849
  return null;
35406
35850
  }
35407
- const targetBinary = ghBinaryPath(spec, platform3);
35851
+ const targetBinary = ghBinaryPath(spec, platform2);
35408
35852
  mkdirSync9(dirname9(targetBinary), { recursive: true });
35409
35853
  try {
35410
35854
  copyFileSync4(innerBinaryPath, targetBinary);
35411
- if (platform3 !== "win32") {
35855
+ if (platform2 !== "win32") {
35412
35856
  const { chmodSync: chmodSync4 } = await import("node:fs");
35413
35857
  chmodSync4(targetBinary, 493);
35414
35858
  }
@@ -35421,11 +35865,11 @@ async function downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl,
35421
35865
  log2(`[lsp] ${spec.id} ${tag} binary_sha256=${binarySha256}`);
35422
35866
  return { archiveSha256, binarySha256 };
35423
35867
  }
35424
- async function ensureGithubInstalled(spec, config2, fetchImpl, platform3, arch, signal) {
35868
+ async function ensureGithubInstalled(spec, config2, fetchImpl, platform2, arch, signal) {
35425
35869
  const outcome = await withInstallLock(spec.githubRepo, async () => {
35426
35870
  const { tag, assets, blockedByGrace, reason } = await resolveTargetTag(spec, config2, fetchImpl, signal);
35427
35871
  if (!tag) {
35428
- const installed = isGithubInstalled(spec, platform3);
35872
+ const installed = isGithubInstalled(spec, platform2);
35429
35873
  if (installed) {
35430
35874
  warn2(`[lsp] no eligible release of ${spec.githubRepo} (grace=${config2.graceDays}d); keeping existing install`);
35431
35875
  return { started: false, reason: "kept existing install" };
@@ -35434,7 +35878,7 @@ async function ensureGithubInstalled(spec, config2, fetchImpl, platform3, arch,
35434
35878
  warn2(`[lsp] skipping ${spec.id}: ${fallbackReason}`);
35435
35879
  return { started: false, reason: fallbackReason };
35436
35880
  }
35437
- if (isGithubInstalled(spec, platform3)) {
35881
+ if (isGithubInstalled(spec, platform2)) {
35438
35882
  const installedMeta = readInstalledMetaIn(ghPackageDir(spec));
35439
35883
  if (installedMeta && installedMeta.version === tag) {
35440
35884
  return { started: false, reason: "already installed" };
@@ -35445,7 +35889,7 @@ async function ensureGithubInstalled(spec, config2, fetchImpl, platform3, arch,
35445
35889
  log2(`[lsp] reinstalling ${spec.id}@${tag}: no installed-version metadata recorded`);
35446
35890
  }
35447
35891
  }
35448
- const hashes = await downloadAndInstall(spec, tag, assets, platform3, arch, fetchImpl, signal).catch((err) => {
35892
+ const hashes = await downloadAndInstall(spec, tag, assets, platform2, arch, fetchImpl, signal).catch((err) => {
35449
35893
  error52(`[lsp] github install ${spec.id} crashed: ${err}`);
35450
35894
  return null;
35451
35895
  });
@@ -37825,6 +38269,48 @@ function relativeToWorktree(fp, worktree) {
37825
38269
  function readAttachments(data) {
37826
38270
  return Array.isArray(data.attachments) ? data.attachments : [];
37827
38271
  }
38272
+ var ISSUE_AND_PR_READ_DESCRIPTION = "GitHub issues and pull requests can be read with `issue://NUMBER` and `pr://NUMBER` (or `issue://OWNER/REPO/NUMBER` and `pr://OWNER/REPO/NUMBER`).";
38273
+ function asRecord2(value) {
38274
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
38275
+ }
38276
+ function visionCapabilityForOpenCodeModel(model) {
38277
+ const entry = model;
38278
+ if (Array.isArray(entry?.modalities?.input)) {
38279
+ return entry.modalities.input.includes("image");
38280
+ }
38281
+ return typeof entry?.attachment === "boolean" ? entry.attachment : undefined;
38282
+ }
38283
+ function modelFromOpenCodeProvider(provider, modelID) {
38284
+ const models = provider.models;
38285
+ if (Array.isArray(models)) {
38286
+ return models.find((model) => asRecord2(model)?.id === modelID);
38287
+ }
38288
+ return asRecord2(models)?.[modelID];
38289
+ }
38290
+ async function currentSessionVisionCapability(client, sessionID) {
38291
+ if (!sessionID)
38292
+ return;
38293
+ const promptContext = await resolvePromptContext(client, sessionID);
38294
+ const currentModel = promptContext?.model;
38295
+ if (!currentModel)
38296
+ return;
38297
+ const providerApi = client.provider;
38298
+ if (typeof providerApi?.list !== "function")
38299
+ return;
38300
+ let listed;
38301
+ try {
38302
+ listed = await providerApi.list();
38303
+ } catch {
38304
+ return;
38305
+ }
38306
+ const result = asRecord2(listed);
38307
+ const catalog = asRecord2(result?.data) ?? result;
38308
+ const providers = Array.isArray(catalog?.all) ? catalog.all : Array.isArray(catalog?.providers) ? catalog.providers : [];
38309
+ const provider = providers.map(asRecord2).find((entry) => entry?.id === currentModel.providerID);
38310
+ if (!provider)
38311
+ return;
38312
+ return visionCapabilityForOpenCodeModel(modelFromOpenCodeProvider(provider, currentModel.modelID));
38313
+ }
37828
38314
  function persistFilePathAlias(args, context) {
37829
38315
  if (typeof args.path === "string" && !Object.hasOwn(args, "filePath")) {
37830
38316
  args.filePath = args.path;
@@ -38012,10 +38498,19 @@ Examples:
38012
38498
  Read 30 lines from line 200: { "path": "src/app.ts", "offset": 200, "limit": 30 }
38013
38499
  List directory: { "path": "src/" }
38014
38500
  `;
38501
+ function readDescription(ghReadEnabled) {
38502
+ if (!ghReadEnabled)
38503
+ return READ_DESCRIPTION;
38504
+ return READ_DESCRIPTION.replace(`
38505
+ Examples:`, `
38506
+ ${ISSUE_AND_PR_READ_DESCRIPTION}
38507
+
38508
+ Examples:`);
38509
+ }
38015
38510
  function createReadTool(ctx) {
38016
38511
  return prepareToolMap({
38017
38512
  read: {
38018
- description: READ_DESCRIPTION,
38513
+ description: readDescription(ctx.config.gh_read?.enabled === true),
38019
38514
  args: {
38020
38515
  filePath: z8.string().describe("Path to file or directory (absolute or relative to project root)"),
38021
38516
  startLine: optionalInt(1, Number.MAX_SAFE_INTEGER).describe("1-based line to start reading from"),
@@ -38066,6 +38561,9 @@ function createReadTool(ctx) {
38066
38561
  rawArgs.endLine = endLine;
38067
38562
  if (rawLimit !== undefined && rawOffset === undefined)
38068
38563
  rawArgs.limit = rawLimit;
38564
+ const visionCapability = await currentSessionVisionCapability(ctx.client, context.sessionID);
38565
+ if (visionCapability !== undefined)
38566
+ rawArgs.vision_capability = visionCapability;
38069
38567
  const response = await callToolCall(ctx, context, "read", rawArgs);
38070
38568
  if (response.success === false) {
38071
38569
  throw new Error(response.message || "read failed");
@@ -38700,7 +39198,7 @@ async function resolveAndGateScope(ctx, context, scope) {
38700
39198
  }
38701
39199
  return { scope: Array.isArray(scope) ? resolved : resolved[0] };
38702
39200
  }
38703
- function asRecord2(value) {
39201
+ function asRecord3(value) {
38704
39202
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
38705
39203
  }
38706
39204
  function asString(value) {
@@ -38708,7 +39206,7 @@ function asString(value) {
38708
39206
  }
38709
39207
  function firstRecord(...values) {
38710
39208
  for (const value of values) {
38711
- const record2 = asRecord2(value);
39209
+ const record2 = asRecord3(value);
38712
39210
  if (record2)
38713
39211
  return record2;
38714
39212
  }
@@ -38743,7 +39241,7 @@ function parseInspectPhaseEntries(value) {
38743
39241
  if (!Array.isArray(value))
38744
39242
  return [];
38745
39243
  return value.flatMap((candidate) => {
38746
- const entry = asRecord2(candidate);
39244
+ const entry = asRecord3(candidate);
38747
39245
  const id = asString(entry?.id);
38748
39246
  if (!id)
38749
39247
  return [];
@@ -38759,7 +39257,7 @@ function parseInspectPhaseEntries(value) {
38759
39257
  });
38760
39258
  }
38761
39259
  function parseInspectTerminal(payload) {
38762
- const response = asRecord2(payload);
39260
+ const response = asRecord3(payload);
38763
39261
  if (!response)
38764
39262
  return;
38765
39263
  const kind = terminalKind(response);
@@ -38774,7 +39272,7 @@ function parseInspectTerminal(payload) {
38774
39272
  waitStampText: kind === "FRESH" ? firstString(waitStamp?.text, waitStamp?.human_text) : undefined
38775
39273
  };
38776
39274
  }
38777
- const failedPhaseRecord = asRecord2(response.failed_phase);
39275
+ const failedPhaseRecord = asRecord3(response.failed_phase);
38778
39276
  const failedPhaseId = firstString(failedPhaseRecord?.id, response.failed_phase);
38779
39277
  const failedPhase = failedPhaseId ? parseInspectPhaseEntries([
38780
39278
  {
@@ -38870,12 +39368,12 @@ function createInspectTier2IdleScheduler(options) {
38870
39368
  }
38871
39369
  function inspectTools(ctx) {
38872
39370
  const inspectTool = {
38873
- description: "Blocking-fresh codebase health inspection. Each call completes current analysis and produces exactly one terminal result: FRESH includes a wait-stamp and completed phases; INTERRUPTED and PHASE-FAILED retain completed phases, with PHASE-FAILED also reporting its phase attribution and failure reason. `sections` selects drill-down detail, not the categories verified.\n\n" + "Use `scope=` to narrow returned results. Scope filters rendered diagnostics; it does not change collection work. Scoped files no producer has authoritatively analyzed are reported as named gaps (complete: false). Passive health changes use the alert channel; do not infer inspect completion from that channel.\n\n" + `Use when: starting work on unfamiliar code, after multi-edit batches to check diagnostics, before a refactor, before review, or to verify cleanup completeness.
39371
+ description: "Blocking-fresh codebase health inspection. Each call completes current analysis and produces exactly one terminal result: FRESH includes a wait-stamp and completed phases; INTERRUPTED and PHASE-FAILED retain completed phases, with PHASE-FAILED also reporting its phase attribution and failure reason. `sections` selects drill-down detail, not the categories verified.\n\n" + "Use `scope=` to narrow returned results. Scope filters rendered diagnostics and limits Rust LSP startup to Cargo workspaces owning the scoped paths; it does not trigger per-file collection work. Scoped files no producer has authoritatively analyzed are reported as named gaps (complete: false). Passive health changes use the alert channel; do not infer inspect completion from that channel.\n\n" + `Use when: starting work on unfamiliar code, after multi-edit batches to check diagnostics, before a refactor, before review, or to verify cleanup completeness.
38874
39372
 
38875
39373
  ` + "Treat `dead_code` as a hint, not proof: reachability is call-based, so symbols reached only via method dispatch or referenced only in type position may be false positives — verify before deleting.",
38876
39374
  args: {
38877
39375
  sections: arg(z10.union([z10.string(), z10.array(z10.string())]).optional().describe("Categories to include in detailed drill-down (e.g. 'todos' or ['todos', 'dead_code', 'cycles']). Use 'all' for every active category. Omit for summary-only mode. `sections` changes detail, not the categories verified.")),
38878
- scope: arg(z10.union([z10.string(), z10.array(z10.string())]).optional().describe("Restrict returned results to paths under this scope (file or directory, absolute or relative to project root). `scope=` narrows results; it does not change the diagnostic collection work. Scoped files no producer has authoritatively analyzed are reported as named gaps (complete: false), never as a clean empty result.")),
39376
+ scope: arg(z10.union([z10.string(), z10.array(z10.string())]).optional().describe("Restrict returned results to paths under this scope (file or directory, absolute or relative to project root). `scope=` narrows results and limits Rust LSP startup to owning Cargo workspaces; it does not trigger per-file diagnostic collection. Scoped files no producer has authoritatively analyzed are reported as named gaps (complete: false), never as a clean empty result.")),
38879
39377
  topK: arg(z10.number().int().positive().max(100).optional().describe("Max drill-down items per category. Default 20, max 100."))
38880
39378
  },
38881
39379
  execute: async (args, context) => {
@@ -39724,7 +40222,9 @@ function semanticTools(ctx) {
39724
40222
  rawArgs.includeTests = args.includeTests;
39725
40223
  if (pathArg)
39726
40224
  rawArgs.path = pathArg;
39727
- const response = await callToolCall(ctx, context, "search", rawArgs);
40225
+ const response = await callToolCall(ctx, context, "search", rawArgs, {
40226
+ abortSignal: context.abort
40227
+ });
39728
40228
  if (response.success === false) {
39729
40229
  const message = typeof response.text === "string" && response.text.length > 0 ? response.text : typeof response.message === "string" && response.message.length > 0 ? response.message : "semantic_search failed";
39730
40230
  throw new Error(message);
@@ -39903,7 +40403,7 @@ var PLUGIN_VERSION = (() => {
39903
40403
  return "0.0.0";
39904
40404
  }
39905
40405
  })();
39906
- var ANNOUNCEMENT_VERSION = "0.54.0";
40406
+ var ANNOUNCEMENT_VERSION = "0.55.1";
39907
40407
  var ANNOUNCEMENT_FEATURES = [
39908
40408
  "Callgraph stays consistent under load: edits arriving mid-rebuild can no longer be silently lost from navigation results.",
39909
40409
  "aft_zoom handles real-world .jsonc: files with comment banners above the opening brace now resolve, and not-found errors point at the segment that actually failed (thanks @iceteaSA).",