@cortexkit/aft-opencode 0.28.2 → 0.29.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.
@@ -46,13 +46,28 @@ interface DrainContext {
46
46
  directory: string;
47
47
  sessionID: string;
48
48
  /**
49
- * Live OpenCode HTTP listener URL (from `input.serverUrl`). Used by the
50
- * wake path to build a separate `createOpencodeClient` that hits the
51
- * live listener via `globalThis.fetch` works around the OpenCode
52
- * promptAsync runner-split bug (anomalyco/opencode#28202). When absent,
53
- * the wake stays queued and existing retry-with-backoff fires; we do
54
- * NOT fall back to `input.client.session.promptAsync` because that
55
- * reopens the bug path.
49
+ * Plugin-provided OpenCode SDK client (`input.client`). The wake path
50
+ * uses this as a fallback when `useLiveServerWake()` is false — i.e.
51
+ * the live HTTP listener was unreachable when probed at plugin init,
52
+ * so `getLiveServerClient(...)` cannot be built. Falling back here
53
+ * accepts the upstream `promptAsync` runner-split bug
54
+ * (anomalyco/opencode#28202; duplicate "stop" messages) in exchange
55
+ * for wakes still arriving at all in plain-TUI sessions.
56
+ *
57
+ * Typed `unknown` because the real `@opencode-ai/sdk` `OpencodeClient`
58
+ * has a narrower, generated `promptAsync` signature than the loose
59
+ * structural `OpenCodeClient` shape used by the live-server factory
60
+ * and test stubs. The wake closure asserts to `OpenCodeClient` after
61
+ * deciding which transport to use.
62
+ */
63
+ client?: unknown;
64
+ /**
65
+ * Live OpenCode HTTP listener URL (from `input.serverUrl`). When the
66
+ * listener was reachable at startup, the wake path builds a separate
67
+ * `createOpencodeClient` from this URL so requests hit the same Effect
68
+ * memoMap as the live UI — works around the runner-split bug
69
+ * (anomalyco/opencode#28202). When the listener was unreachable, the
70
+ * wake path falls back to `client` above; this URL is unused.
56
71
  */
57
72
  serverUrl?: string;
58
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bg-notifications.d.ts","sourceRoot":"","sources":["../src/bg-notifications.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bg-notifications.d.ts","sourceRoot":"","sources":["../src/bg-notifications.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAWhD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAG3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,KAAK,cAAc,GAAG;IACpB,kBAAkB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAChC,kBAAkB,EAAE,YAAY,EAAE,CAAC;IACnC,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;IAC5C,aAAa,EAAE,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;IACrC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,wBAAwB,EAAE,MAAM,CAAC;IACjC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,kBAAkB,EAAE,KAAK,CAAC;QAAE,UAAU,EAAE,YAAY,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC5E,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAa,CAAC;AAGtE,eAAO,MAAM,4BAA4B,QAAiB,CAAC;AAS3D,UAAU,YAAY;IACpB,GAAG,EAAE,aAAa,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AASD,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAgB/E;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,WAAW,EAAE,OAAO,GACnB,YAAY,EAAE,CAoBhB;AAED,wBAAsB,wBAAwB,CAC5C,YAAY,EAAE,YAAY,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,EAChD,UAAU,EAAE,OAAO,GAClB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,YAAY,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,EAChD,QAAQ,EAAE,qBAAqB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,GACtC,OAAO,CAAC,IAAI,CAAC,CAsDf;AAED,wBAAsB,uBAAuB,CAC3C,YAAY,EAAE,YAAY,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAC/C,OAAO,CAAC,IAAI,CAAC,CAEf;AAmLD,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,CASjF;AAED,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,SAAS,qBAAqB,EAAE,GAAG,MAAM,CAQ7F;AAWD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAcnE;AAED,wBAAgB,kCAAkC,IAAI,IAAI,CAKzD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAoKlD;;;;;;;;;;;;;;;;;;GAkBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AAg0B5E,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAqKlD;;;;;;;;;;;;;;;;;;GAkBG;AAQH,QAAA,MAAM,MAAM,EAAE,MAA6D,CAAC;AAqyB5E,eAAe,MAAM,CAAC"}
package/dist/index.js CHANGED
@@ -31001,6 +31001,7 @@ import { chmodSync as chmodSync2, copyFileSync, existsSync as existsSync2, mkdir
31001
31001
  import { createRequire as createRequire2 } from "node:module";
31002
31002
  import { homedir as homedir3 } from "node:os";
31003
31003
  import { join as join3 } from "node:path";
31004
+ var ensureBinaryForResolver = ensureBinary;
31004
31005
  function readBinaryVersion(binaryPath) {
31005
31006
  try {
31006
31007
  const result = spawnSync(binaryPath, ["--version"], {
@@ -31141,7 +31142,7 @@ async function findBinary(expectedVersion) {
31141
31142
  return syncResult;
31142
31143
  }
31143
31144
  log("Binary not found locally, attempting auto-download...");
31144
- const downloaded = await ensureBinary(expectedVersion);
31145
+ const downloaded = await ensureBinaryForResolver(expectedVersion);
31145
31146
  if (downloaded)
31146
31147
  return downloaded;
31147
31148
  throw new Error([
@@ -31165,6 +31166,7 @@ async function findBinary(expectedVersion) {
31165
31166
  }
31166
31167
 
31167
31168
  // ../aft-bridge/dist/migration.js
31169
+ var spawnSyncForMigration = spawnSync2;
31168
31170
  var TARGET_MARKER = ".migrated_from_legacy";
31169
31171
  var DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
31170
31172
  function dataHome() {
@@ -31236,7 +31238,7 @@ async function ensureStorageMigrated(opts) {
31236
31238
 
31237
31239
  `);
31238
31240
  } catch {}
31239
- const result = spawnSync2(binaryPath, [
31241
+ const result = spawnSyncForMigration(binaryPath, [
31240
31242
  "migrate-storage",
31241
31243
  "--from",
31242
31244
  legacyRoot,
@@ -32692,9 +32694,6 @@ function getLiveServerClient(serverUrl, directory) {
32692
32694
  clientCache.set(key, client);
32693
32695
  return client;
32694
32696
  }
32695
- function isBunRuntime() {
32696
- return typeof globalThis.Bun !== "undefined";
32697
- }
32698
32697
  async function probeServerReachable(serverUrl, timeoutMs = 1500) {
32699
32698
  if (!serverUrl)
32700
32699
  return false;
@@ -32713,6 +32712,13 @@ async function probeServerReachable(serverUrl, timeoutMs = 1500) {
32713
32712
  clearTimeout(timer);
32714
32713
  }
32715
32714
  }
32715
+ var liveServerWakeAvailable = false;
32716
+ function setLiveServerWakeAvailable(available) {
32717
+ liveServerWakeAvailable = available;
32718
+ }
32719
+ function useLiveServerWake() {
32720
+ return liveServerWakeAvailable;
32721
+ }
32716
32722
 
32717
32723
  // src/bg-notifications.ts
32718
32724
  function hashReminder(text) {
@@ -32819,18 +32825,26 @@ async function triggerWakeIfPending(drainContext, skipDrain) {
32819
32825
  if (state.pendingCompletions.length === 0 && state.pendingLongRunning.length === 0)
32820
32826
  return;
32821
32827
  scheduleWake(state, async (reminder, deliveredCompletions) => {
32822
- if (!drainContext.serverUrl) {
32823
- sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake serverUrl unavailable`, {
32824
- event: "bash_completion_wake_serverUrl_unavailable",
32825
- task_ids: deliveredCompletions.map((c) => c.task_id),
32826
- directory: drainContext.directory,
32827
- attempt: state.wakeRetryAttempts + 1
32828
- });
32829
- throw new Error("input.serverUrl is unavailable; cannot reach live OpenCode listener");
32828
+ let client;
32829
+ let clientPath;
32830
+ if (useLiveServerWake() && drainContext.serverUrl) {
32831
+ client = getLiveServerClient(drainContext.serverUrl, drainContext.directory);
32832
+ clientPath = "live-server";
32833
+ } else {
32834
+ if (!drainContext.client) {
32835
+ sessionWarn(drainContext.sessionID, `${LOG_PREFIX} wake client unavailable`, {
32836
+ event: "bash_completion_wake_client_unavailable",
32837
+ task_ids: deliveredCompletions.map((c) => c.task_id),
32838
+ directory: drainContext.directory,
32839
+ attempt: state.wakeRetryAttempts + 1
32840
+ });
32841
+ throw new Error("no wake transport available: live-server unreachable and input.client absent");
32842
+ }
32843
+ client = drainContext.client;
32844
+ clientPath = "in-process-fallback";
32830
32845
  }
32831
- const client = getLiveServerClient(drainContext.serverUrl, drainContext.directory);
32832
32846
  if (typeof client.session?.promptAsync !== "function") {
32833
- throw new Error("live-server client.session.promptAsync is unavailable");
32847
+ throw new Error(`wake client.session.promptAsync is unavailable (path=${clientPath})`);
32834
32848
  }
32835
32849
  const promptContext = await resolvePromptContext(client, drainContext.sessionID);
32836
32850
  const body = {
@@ -32856,7 +32870,7 @@ async function triggerWakeIfPending(drainContext, skipDrain) {
32856
32870
  directory: drainContext.directory,
32857
32871
  reminder_sha256: hashReminder(reminder),
32858
32872
  reminder_chars: reminder.length,
32859
- wake_client_path: "live-server",
32873
+ wake_client_path: clientPath,
32860
32874
  prompt_context: promptContext ? {
32861
32875
  agent: promptContext.agent,
32862
32876
  model: promptContext.model ? {
@@ -49958,6 +49972,28 @@ async function cleanupWarnings(opts) {
49958
49972
  }
49959
49973
  }
49960
49974
 
49975
+ // src/shared/bash-hints.ts
49976
+ var CONFLICT_HINT = `
49977
+
49978
+ [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
49979
+ var GREP_HINT = `
49980
+
49981
+ [Hint] Use the grep tool instead of bash for faster indexed search.`;
49982
+ function maybeAppendConflictsHint(output) {
49983
+ if (!output.includes("Automatic merge failed; fix conflicts"))
49984
+ return output;
49985
+ if (!/^CONFLICT \(|^error: could not apply /m.test(output))
49986
+ return output;
49987
+ return output + CONFLICT_HINT;
49988
+ }
49989
+ function maybeAppendGrepHint(output) {
49990
+ const firstLine = output.slice(0, 300).split(`
49991
+ `)[0] ?? "";
49992
+ if (!/\b(rg|grep)\s/.test(firstLine))
49993
+ return output;
49994
+ return output + GREP_HINT;
49995
+ }
49996
+
49961
49997
  // src/shared/rpc-server.ts
49962
49998
  import { randomBytes as randomBytes2 } from "node:crypto";
49963
49999
  import { mkdirSync as mkdirSync11, renameSync as renameSync7, unlinkSync as unlinkSync7, writeFileSync as writeFileSync10 } from "node:fs";
@@ -53990,18 +54026,12 @@ ${lines}
53990
54026
  config: aftConfig,
53991
54027
  storageDir: configOverrides.storage_dir
53992
54028
  };
53993
- let tuiPortZeroNudgeNeeded = false;
53994
- if (isBunRuntime()) {
53995
- probeServerReachable(input.serverUrl?.toString()).then((reachable) => {
53996
- tuiPortZeroNudgeNeeded = !reachable;
53997
- if (!reachable) {
53998
- log2("Detected OpenCode TUI without --port 0; live HTTP listener unreachable. " + "Will nudge once per session to restart with --port 0 (anomalyco/opencode#28202).");
53999
- }
54000
- }).catch(() => {
54001
- tuiPortZeroNudgeNeeded = true;
54002
- });
54003
- }
54004
- const tuiNudgeSentSessions = new Set;
54029
+ probeServerReachable(input.serverUrl?.toString()).then((reachable) => {
54030
+ setLiveServerWakeAvailable(reachable);
54031
+ log2(reachable ? "Live OpenCode HTTP listener reachable; bg-notifications wake path = live-server (anomalyco/opencode#28202 workaround active)." : "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.");
54032
+ }).catch(() => {
54033
+ setLiveServerWakeAvailable(false);
54034
+ });
54005
54035
  if (onnxRuntimePromise) {
54006
54036
  onnxRuntimePromise.then((ortDylibDir) => {
54007
54037
  if (ortDylibDir) {
@@ -54215,13 +54245,6 @@ Install: ${getManualInstallHint()}`).catch(() => {});
54215
54245
  "chat.message": async (messageInput) => {
54216
54246
  const sid = messageInput.sessionID ?? messageInput.sessionId ?? messageInput.id;
54217
54247
  warmSessionDirectory(input.client, sid, input.directory);
54218
- if (tuiPortZeroNudgeNeeded && typeof sid === "string" && sid.length > 0) {
54219
- if (!tuiNudgeSentSessions.has(sid)) {
54220
- tuiNudgeSentSessions.add(sid);
54221
- const nudge = "Due to a bug in opencode (https://github.com/anomalyco/opencode/issues/28202), background bash notifications are now using a workaround which needs opencode to be run via `opencode --port 0` in TUI mode. Opencode Desktop doesn't need special treatment.";
54222
- sendIgnoredMessage2(input.client, sid, nudge).catch(() => {});
54223
- }
54224
- }
54225
54248
  },
54226
54249
  "command.execute.before": async (commandInput, _output) => {
54227
54250
  if (isTuiMode2() || commandInput.command !== STATUS_COMMAND) {
@@ -54254,19 +54277,9 @@ Install: ${getManualInstallHint()}`).catch(() => {});
54254
54277
  if (stored.metadata)
54255
54278
  output.metadata = { ...output.metadata, ...stored.metadata };
54256
54279
  }
54257
- if (toolInput.tool === "bash" && output.output?.includes("Automatic merge failed; fix conflicts")) {
54258
- output.output += `
54259
-
54260
- [Hint] Use aft_conflicts to see all conflict regions across files in a single call.`;
54261
- }
54262
54280
  if (toolInput.tool === "bash" && output.output) {
54263
- const firstLine = output.output.slice(0, 300).split(`
54264
- `)[0] ?? "";
54265
- if (/\b(rg|grep)\s/.test(firstLine)) {
54266
- output.output += `
54267
-
54268
- [Hint] Use the grep tool instead of bash for faster indexed search.`;
54269
- }
54281
+ output.output = maybeAppendConflictsHint(output.output);
54282
+ output.output = maybeAppendGrepHint(output.output);
54270
54283
  }
54271
54284
  const sessionDir = getSessionDirectoryCached(toolInput.sessionID) ?? input.directory;
54272
54285
  await appendInTurnBgCompletions({ ctx, directory: sessionDir, sessionID: toolInput.sessionID }, output);
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Append the `aft_conflicts` hint when the output indicates a real git merge
3
+ * or rebase produced conflicts.
4
+ *
5
+ * Gated on BOTH:
6
+ * - the "Automatic merge failed; fix conflicts" marker, AND
7
+ * - a git-conflict signal (`CONFLICT (...)` line or `error: could not apply`)
8
+ *
9
+ * Both conditions are required because `aft_conflicts` calls `git ls-files -u`,
10
+ * which fails with "not a git repository" outside a git working tree. The
11
+ * marker string can legitimately appear in docs, READMEs, test fixtures, and
12
+ * grep output, so we cannot key off it alone — a false-positive hint sends
13
+ * agents into a confusing error.
14
+ */
15
+ export declare function maybeAppendConflictsHint(output: string): string;
16
+ /**
17
+ * Append the grep-tool hint when the bash command was a grep/rg invocation.
18
+ *
19
+ * Detected by examining the FIRST LINE of output (which is the echoed command
20
+ * line on most shells in foreground mode). We cap the slice at 300 chars so
21
+ * a single huge first line doesn't slow this hook.
22
+ */
23
+ export declare function maybeAppendGrepHint(output: string): string;
24
+ //# sourceMappingURL=bash-hints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bash-hints.d.ts","sourceRoot":"","sources":["../../src/shared/bash-hints.ts"],"names":[],"mappings":"AAWA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAM/D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI1D"}
@@ -19,6 +19,16 @@
19
19
  * UI uses, so the active session's `SessionRunState` is the one that
20
20
  * resolves `ensureRunning` and overlapping turns coalesce correctly.
21
21
  *
22
+ * The workaround only works when the live HTTP listener is actually
23
+ * reachable. OpenCode Desktop (Electron+Node) and TUI launched with
24
+ * `opencode --port 0` bind a real API listener; plain TUI binds an
25
+ * internal-only listener that 404s for `/session/*`. We probe once at
26
+ * plugin init and cache the result. When the listener is unreachable
27
+ * the wake path silently uses the in-process `input.client.session.promptAsync`,
28
+ * which keeps wakes flowing (at the cost of the upstream duplicate-runner
29
+ * bug) instead of producing no notification at all or nagging the user
30
+ * to relaunch with a different flag.
31
+ *
22
32
  * Tracked upstream as anomalyco/opencode#28202. When OpenCode fixes the
23
33
  * runtime split, this helper and its single consumer in `bg-notifications.ts`
24
34
  * can be deleted and the wake path can go back to `input.client`.
@@ -38,23 +48,32 @@ export type LiveServerClient = ReturnType<typeof createOpencodeClient>;
38
48
  export declare function getLiveServerClient(serverUrl: string, directory: string): LiveServerClient;
39
49
  /** Test helper — drop the cache between cases so each test starts clean. */
40
50
  export declare function __resetLiveServerClientCacheForTests(): void;
41
- /**
42
- * True when the current runtime is Bun. OpenCode's TUI ships with Bun;
43
- * the Electron Desktop app ships with Node. We use this to gate the
44
- * `--port 0` nudge: Desktop is unaffected by anomalyco/opencode#28202
45
- * because Node's webidl polyfill is complete, so undici v8 (and the live
46
- * HTTP server) work without additional flags.
47
- */
48
- export declare function isBunRuntime(): boolean;
49
51
  /**
50
52
  * Probe whether `serverUrl` accepts a connection within `timeoutMs`.
51
53
  * Returns `true` for any HTTP response (including 4xx / 5xx) since the
52
54
  * goal is to confirm the listener exists. Returns `false` on connection
53
55
  * refused, DNS failure, timeout, or undefined URL.
54
56
  *
55
- * Used at plugin init under Bun to detect TUI sessions started without
56
- * `opencode --port 0` those bind an internal-only listener that 404s
57
- * for `/session/...` endpoints and breaks AFT's wake path.
57
+ * Used at plugin init to decide whether bg-notifications should use the
58
+ * live-server wake transport (workaround for anomalyco/opencode#28202)
59
+ * or fall back to the in-process `input.client.session.promptAsync`
60
+ * path. Plain TUI (no `--port 0`) binds an internal-only listener that
61
+ * 404s for `/session/...`, so this returns false there; OpenCode
62
+ * Desktop, `opencode run`, and `opencode --port 0` TUI return true.
58
63
  */
59
64
  export declare function probeServerReachable(serverUrl: string | undefined, timeoutMs?: number): Promise<boolean>;
65
+ /**
66
+ * Record the probe result. Idempotent; if you record twice, the latest
67
+ * value wins. The wake path reads through `useLiveServerWake()`.
68
+ */
69
+ export declare function setLiveServerWakeAvailable(available: boolean): void;
70
+ /**
71
+ * Read the cached probe decision. `true` means the wake path should use
72
+ * `getLiveServerClient(serverUrl, directory)` and POST through the live
73
+ * HTTP listener. `false` means fall back to the in-process client passed
74
+ * via plugin context (`input.client`).
75
+ */
76
+ export declare function useLiveServerWake(): boolean;
77
+ /** Test helper — reset the decision cache between cases. */
78
+ export declare function __resetLiveServerWakeForTests(): void;
60
79
  //# sourceMappingURL=live-server-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"live-server-client.d.ts","sourceRoot":"","sources":["../../src/shared/live-server-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA6BvE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAY1F;AAED,4EAA4E;AAC5E,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,SAAO,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB"}
1
+ {"version":3,"file":"live-server-client.d.ts","sourceRoot":"","sources":["../../src/shared/live-server-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA6BvE;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAY1F;AAED,4EAA4E;AAC5E,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,SAAO,GACf,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAmBD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,OAAO,GAAG,IAAI,CAEnE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C;AAED,4DAA4D;AAC5D,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD"}
package/dist/tui.js CHANGED
@@ -3,7 +3,7 @@ import { createMemo as createMemo2, createSignal as createSignal2, onCleanup as
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@cortexkit/aft-opencode",
6
- version: "0.28.2",
6
+ version: "0.29.1",
7
7
  type: "module",
8
8
  description: "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
9
9
  main: "dist/index.js",
@@ -32,7 +32,7 @@ var package_default = {
32
32
  },
33
33
  dependencies: {
34
34
  "@clack/prompts": "^1.2.0",
35
- "@cortexkit/aft-bridge": "0.28.2",
35
+ "@cortexkit/aft-bridge": "0.29.1",
36
36
  "@opencode-ai/plugin": "^1.15.5",
37
37
  "@opencode-ai/sdk": "^1.15.5",
38
38
  "comment-json": "^4.6.2",
@@ -40,12 +40,12 @@ var package_default = {
40
40
  zod: "^4.1.8"
41
41
  },
42
42
  optionalDependencies: {
43
- "@cortexkit/aft-darwin-arm64": "0.28.2",
44
- "@cortexkit/aft-darwin-x64": "0.28.2",
45
- "@cortexkit/aft-linux-arm64": "0.28.2",
46
- "@cortexkit/aft-linux-x64": "0.28.2",
47
- "@cortexkit/aft-win32-arm64": "0.28.2",
48
- "@cortexkit/aft-win32-x64": "0.28.2"
43
+ "@cortexkit/aft-darwin-arm64": "0.29.1",
44
+ "@cortexkit/aft-darwin-x64": "0.29.1",
45
+ "@cortexkit/aft-linux-arm64": "0.29.1",
46
+ "@cortexkit/aft-linux-x64": "0.29.1",
47
+ "@cortexkit/aft-win32-arm64": "0.29.1",
48
+ "@cortexkit/aft-win32-x64": "0.29.1"
49
49
  },
50
50
  devDependencies: {
51
51
  "@types/node": "^22.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.28.2",
3
+ "version": "0.29.1",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
6
6
  "main": "dist/index.js",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^1.2.0",
32
- "@cortexkit/aft-bridge": "0.28.2",
32
+ "@cortexkit/aft-bridge": "0.29.1",
33
33
  "@opencode-ai/plugin": "^1.15.5",
34
34
  "@opencode-ai/sdk": "^1.15.5",
35
35
  "comment-json": "^4.6.2",
@@ -37,12 +37,12 @@
37
37
  "zod": "^4.1.8"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@cortexkit/aft-darwin-arm64": "0.28.2",
41
- "@cortexkit/aft-darwin-x64": "0.28.2",
42
- "@cortexkit/aft-linux-arm64": "0.28.2",
43
- "@cortexkit/aft-linux-x64": "0.28.2",
44
- "@cortexkit/aft-win32-arm64": "0.28.2",
45
- "@cortexkit/aft-win32-x64": "0.28.2"
40
+ "@cortexkit/aft-darwin-arm64": "0.29.1",
41
+ "@cortexkit/aft-darwin-x64": "0.29.1",
42
+ "@cortexkit/aft-linux-arm64": "0.29.1",
43
+ "@cortexkit/aft-linux-x64": "0.29.1",
44
+ "@cortexkit/aft-win32-arm64": "0.29.1",
45
+ "@cortexkit/aft-win32-x64": "0.29.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.0.0",
@@ -0,0 +1,45 @@
1
+ // Pure helpers for the bash-output hint nudges fired from `tool.execute.after`.
2
+ //
3
+ // These run on every bash result and append agent-visible "[Hint] ..." lines
4
+ // that steer toward AFT tools. The functions return the new output string
5
+ // (or the original string when no hint should fire) so the hook can compose
6
+ // them without managing state.
7
+
8
+ const CONFLICT_HINT =
9
+ "\n\n[Hint] Use aft_conflicts to see all conflict regions across files in a single call.";
10
+ const GREP_HINT = "\n\n[Hint] Use the grep tool instead of bash for faster indexed search.";
11
+
12
+ /**
13
+ * Append the `aft_conflicts` hint when the output indicates a real git merge
14
+ * or rebase produced conflicts.
15
+ *
16
+ * Gated on BOTH:
17
+ * - the "Automatic merge failed; fix conflicts" marker, AND
18
+ * - a git-conflict signal (`CONFLICT (...)` line or `error: could not apply`)
19
+ *
20
+ * Both conditions are required because `aft_conflicts` calls `git ls-files -u`,
21
+ * which fails with "not a git repository" outside a git working tree. The
22
+ * marker string can legitimately appear in docs, READMEs, test fixtures, and
23
+ * grep output, so we cannot key off it alone — a false-positive hint sends
24
+ * agents into a confusing error.
25
+ */
26
+ export function maybeAppendConflictsHint(output: string): string {
27
+ if (!output.includes("Automatic merge failed; fix conflicts")) return output;
28
+ // git merge prints "CONFLICT (content|file|...): ..." per file.
29
+ // git rebase / git am print "error: could not apply <sha>" per failed pick.
30
+ if (!/^CONFLICT \(|^error: could not apply /m.test(output)) return output;
31
+ return output + CONFLICT_HINT;
32
+ }
33
+
34
+ /**
35
+ * Append the grep-tool hint when the bash command was a grep/rg invocation.
36
+ *
37
+ * Detected by examining the FIRST LINE of output (which is the echoed command
38
+ * line on most shells in foreground mode). We cap the slice at 300 chars so
39
+ * a single huge first line doesn't slow this hook.
40
+ */
41
+ export function maybeAppendGrepHint(output: string): string {
42
+ const firstLine = output.slice(0, 300).split("\n")[0] ?? "";
43
+ if (!/\b(rg|grep)\s/.test(firstLine)) return output;
44
+ return output + GREP_HINT;
45
+ }
@@ -19,6 +19,16 @@
19
19
  * UI uses, so the active session's `SessionRunState` is the one that
20
20
  * resolves `ensureRunning` and overlapping turns coalesce correctly.
21
21
  *
22
+ * The workaround only works when the live HTTP listener is actually
23
+ * reachable. OpenCode Desktop (Electron+Node) and TUI launched with
24
+ * `opencode --port 0` bind a real API listener; plain TUI binds an
25
+ * internal-only listener that 404s for `/session/*`. We probe once at
26
+ * plugin init and cache the result. When the listener is unreachable
27
+ * the wake path silently uses the in-process `input.client.session.promptAsync`,
28
+ * which keeps wakes flowing (at the cost of the upstream duplicate-runner
29
+ * bug) instead of producing no notification at all or nagging the user
30
+ * to relaunch with a different flag.
31
+ *
22
32
  * Tracked upstream as anomalyco/opencode#28202. When OpenCode fixes the
23
33
  * runtime split, this helper and its single consumer in `bg-notifications.ts`
24
34
  * can be deleted and the wake path can go back to `input.client`.
@@ -84,26 +94,18 @@ export function __resetLiveServerClientCacheForTests(): void {
84
94
  clientCache.clear();
85
95
  }
86
96
 
87
- /**
88
- * True when the current runtime is Bun. OpenCode's TUI ships with Bun;
89
- * the Electron Desktop app ships with Node. We use this to gate the
90
- * `--port 0` nudge: Desktop is unaffected by anomalyco/opencode#28202
91
- * because Node's webidl polyfill is complete, so undici v8 (and the live
92
- * HTTP server) work without additional flags.
93
- */
94
- export function isBunRuntime(): boolean {
95
- return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
96
- }
97
-
98
97
  /**
99
98
  * Probe whether `serverUrl` accepts a connection within `timeoutMs`.
100
99
  * Returns `true` for any HTTP response (including 4xx / 5xx) since the
101
100
  * goal is to confirm the listener exists. Returns `false` on connection
102
101
  * refused, DNS failure, timeout, or undefined URL.
103
102
  *
104
- * Used at plugin init under Bun to detect TUI sessions started without
105
- * `opencode --port 0` those bind an internal-only listener that 404s
106
- * for `/session/...` endpoints and breaks AFT's wake path.
103
+ * Used at plugin init to decide whether bg-notifications should use the
104
+ * live-server wake transport (workaround for anomalyco/opencode#28202)
105
+ * or fall back to the in-process `input.client.session.promptAsync`
106
+ * path. Plain TUI (no `--port 0`) binds an internal-only listener that
107
+ * 404s for `/session/...`, so this returns false there; OpenCode
108
+ * Desktop, `opencode run`, and `opencode --port 0` TUI return true.
107
109
  */
108
110
  export async function probeServerReachable(
109
111
  serverUrl: string | undefined,
@@ -130,3 +132,43 @@ export async function probeServerReachable(
130
132
  clearTimeout(timer);
131
133
  }
132
134
  }
135
+
136
+ /**
137
+ * Per-plugin-process decision: should bg-notifications use the live-server
138
+ * wake transport (workaround for anomalyco/opencode#28202), or fall back
139
+ * to the in-process `input.client.session.promptAsync` path?
140
+ *
141
+ * Set once at plugin init from the result of `probeServerReachable()`.
142
+ * Defaults to `false` if no decision has been recorded yet — that's the
143
+ * safe direction because `input.client.session.promptAsync` is always
144
+ * available (it's part of the plugin contract), whereas the live-server
145
+ * path needs both a probe-confirmed listener and the workaround code
146
+ * itself to be live.
147
+ *
148
+ * Read at wake time (not cached on a closure) so background probes that
149
+ * complete after plugin init still take effect on the next wake.
150
+ */
151
+ let liveServerWakeAvailable = false;
152
+
153
+ /**
154
+ * Record the probe result. Idempotent; if you record twice, the latest
155
+ * value wins. The wake path reads through `useLiveServerWake()`.
156
+ */
157
+ export function setLiveServerWakeAvailable(available: boolean): void {
158
+ liveServerWakeAvailable = available;
159
+ }
160
+
161
+ /**
162
+ * Read the cached probe decision. `true` means the wake path should use
163
+ * `getLiveServerClient(serverUrl, directory)` and POST through the live
164
+ * HTTP listener. `false` means fall back to the in-process client passed
165
+ * via plugin context (`input.client`).
166
+ */
167
+ export function useLiveServerWake(): boolean {
168
+ return liveServerWakeAvailable;
169
+ }
170
+
171
+ /** Test helper — reset the decision cache between cases. */
172
+ export function __resetLiveServerWakeForTests(): void {
173
+ liveServerWakeAvailable = false;
174
+ }