@cortexkit/aft-opencode 0.58.1 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/bash-wait-detach.d.ts.map +1 -1
  2. package/dist/bg-notifications.d.ts +8 -0
  3. package/dist/bg-notifications.d.ts.map +1 -1
  4. package/dist/bridge-bootstrap.d.ts +4 -4
  5. package/dist/config-live-reload.d.ts +44 -8
  6. package/dist/config-live-reload.d.ts.map +1 -1
  7. package/dist/config.d.ts +85 -13
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/configure-warnings.d.ts.map +1 -1
  10. package/dist/entry/server.js +5329 -2607
  11. package/dist/hashline-tag-sources.d.ts +18 -0
  12. package/dist/hashline-tag-sources.d.ts.map +1 -0
  13. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  14. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4511 -2373
  17. package/dist/lsp-auto-install.d.ts +30 -0
  18. package/dist/lsp-auto-install.d.ts.map +1 -1
  19. package/dist/lsp-github-install.d.ts +5 -2
  20. package/dist/lsp-github-install.d.ts.map +1 -1
  21. package/dist/lsp-github-table.d.ts +4 -3
  22. package/dist/lsp-github-table.d.ts.map +1 -1
  23. package/dist/lsp-npm-table.d.ts +10 -0
  24. package/dist/lsp-npm-table.d.ts.map +1 -1
  25. package/dist/lsp-registry-probe.d.ts +8 -4
  26. package/dist/lsp-registry-probe.d.ts.map +1 -1
  27. package/dist/notifications.d.ts +1 -1
  28. package/dist/notifications.d.ts.map +1 -1
  29. package/dist/permissions/v2-service.d.ts +110 -5
  30. package/dist/permissions/v2-service.d.ts.map +1 -1
  31. package/dist/rpc/contract.d.ts +1 -1
  32. package/dist/rpc/contract.d.ts.map +1 -1
  33. package/dist/rpc/register.d.ts +3 -5
  34. package/dist/rpc/register.d.ts.map +1 -1
  35. package/dist/shared/ignored-message.d.ts.map +1 -1
  36. package/dist/shared/read-vision.d.ts.map +1 -1
  37. package/dist/shared/rpc-client.d.ts +8 -0
  38. package/dist/shared/rpc-client.d.ts.map +1 -1
  39. package/dist/shared/rpc-server.d.ts.map +1 -1
  40. package/dist/shared/session-directory.d.ts +9 -0
  41. package/dist/shared/session-directory.d.ts.map +1 -1
  42. package/dist/shared/status.d.ts +1 -0
  43. package/dist/shared/status.d.ts.map +1 -1
  44. package/dist/shared/subagent-detect.d.ts +11 -4
  45. package/dist/shared/subagent-detect.d.ts.map +1 -1
  46. package/dist/shared/v2-context.d.ts +3 -0
  47. package/dist/shared/v2-context.d.ts.map +1 -0
  48. package/dist/subc-tool-schemas.d.ts +23 -1
  49. package/dist/subc-tool-schemas.d.ts.map +1 -1
  50. package/dist/tool-registration.d.ts +11 -8
  51. package/dist/tool-registration.d.ts.map +1 -1
  52. package/dist/tools/_shared.d.ts +16 -15
  53. package/dist/tools/_shared.d.ts.map +1 -1
  54. package/dist/tools/bash.d.ts +41 -1
  55. package/dist/tools/bash.d.ts.map +1 -1
  56. package/dist/tools/bash_watch.d.ts +14 -4
  57. package/dist/tools/bash_watch.d.ts.map +1 -1
  58. package/dist/tools/bash_write.d.ts.map +1 -1
  59. package/dist/tools/definitions/v2.d.ts +4 -0
  60. package/dist/tools/definitions/v2.d.ts.map +1 -1
  61. package/dist/tools/hoisted/v2.d.ts +8 -1
  62. package/dist/tools/hoisted/v2.d.ts.map +1 -1
  63. package/dist/tools/hoisted.d.ts +27 -7
  64. package/dist/tools/hoisted.d.ts.map +1 -1
  65. package/dist/tools/inspect.d.ts +10 -1
  66. package/dist/tools/inspect.d.ts.map +1 -1
  67. package/dist/tools/permissions.d.ts +2 -1
  68. package/dist/tools/permissions.d.ts.map +1 -1
  69. package/dist/tools/reading.d.ts +0 -3
  70. package/dist/tools/reading.d.ts.map +1 -1
  71. package/dist/tools/search.d.ts.map +1 -1
  72. package/dist/tools/semantic.d.ts.map +1 -1
  73. package/dist/tui/aft-rpc.d.ts +14 -0
  74. package/dist/tui/aft-rpc.d.ts.map +1 -1
  75. package/dist/tui/v2-status.d.ts.map +1 -1
  76. package/dist/v2-host-tool-overlap.d.ts +66 -0
  77. package/dist/v2-host-tool-overlap.d.ts.map +1 -0
  78. package/dist/v2-prompt-detach.d.ts +70 -0
  79. package/dist/v2-prompt-detach.d.ts.map +1 -0
  80. package/dist/v2-tool-hooks.d.ts +6 -0
  81. package/dist/v2-tool-hooks.d.ts.map +1 -0
  82. package/dist/v2-workflow-hints.d.ts +4 -0
  83. package/dist/v2-workflow-hints.d.ts.map +1 -0
  84. package/dist/wakes/runtime-consumer.d.ts +2 -0
  85. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  86. package/dist/wakes/session-delivery.d.ts +9 -0
  87. package/dist/wakes/session-delivery.d.ts.map +1 -1
  88. package/dist/workflow-hints.d.ts +20 -7
  89. package/dist/workflow-hints.d.ts.map +1 -1
  90. package/package.json +9 -9
  91. package/src/entry/server-runtime.mjs +41 -5
  92. package/src/shared/ignored-message.ts +18 -0
  93. package/src/shared/read-vision.ts +14 -4
  94. package/src/shared/rpc-client.ts +46 -5
  95. package/src/shared/rpc-server.ts +10 -0
  96. package/src/shared/session-directory.ts +147 -7
  97. package/src/shared/status.ts +15 -5
  98. package/src/shared/subagent-detect.ts +89 -28
  99. package/src/shared/v2-context.ts +7 -0
  100. package/src/tui/aft-rpc.ts +11 -0
  101. package/src/tui/index.tsx +20 -16
  102. package/src/tui/sidebar-view.tsx +9 -1
  103. package/src/tui/v2-status.ts +7 -5
  104. package/src/tui/v2.tsx +47 -0
  105. package/src/tui-compiled/aft-rpc.ts +11 -0
  106. package/src/tui-compiled/index.tsx +27 -16
  107. package/src/tui-compiled/sidebar-view.tsx +11 -2
  108. package/src/tui-compiled/v2-status.ts +7 -5
  109. package/src/tui-compiled/v2.tsx +26 -0
@@ -0,0 +1,70 @@
1
+ import { Effect } from "effect";
2
+ import { interruptBashWaitsForChatMessage } from "./bash-wait-detach.js";
3
+ import type { AftConfig } from "./config.js";
4
+ /** An offset range into `prompt.text`; `end` is exclusive. */
5
+ interface PromptMention {
6
+ start: number;
7
+ end: number;
8
+ text: string;
9
+ }
10
+ interface MentionCarrier {
11
+ mention?: PromptMention;
12
+ }
13
+ /** The fields of OpenCode 2's `SessionHooks["prompt"]` event this hook reads or edits. */
14
+ export interface V2PromptHookEvent {
15
+ readonly sessionID: string;
16
+ prompt: {
17
+ text: string;
18
+ files?: MentionCarrier[];
19
+ agents?: MentionCarrier[];
20
+ skills?: MentionCarrier[];
21
+ };
22
+ metadata?: Record<string, unknown>;
23
+ }
24
+ type ChatMessagePool = Parameters<typeof interruptBashWaitsForChatMessage>[0];
25
+ export interface V2PromptDetachRuntime {
26
+ /** The Location's bridge pool, where a session's waits live. */
27
+ readonly pool: ChatMessagePool;
28
+ /**
29
+ * The root this Location's tools run on (the Location's own directory); its
30
+ * bridge is asked to detach before any other.
31
+ */
32
+ readonly projectRoot: string;
33
+ /**
34
+ * Read on every prompt so a live config reload of
35
+ * `bash.detach_on_user_message` applies to the next message.
36
+ */
37
+ getConfig(): AftConfig;
38
+ registerSession?: (sessionID: string) => void;
39
+ }
40
+ /**
41
+ * Run the shared chat-message decision on one OpenCode 2 prompt and write the
42
+ * stripped text (and moved mention offsets) back onto it. Returns whether the
43
+ * session's waits were interrupted.
44
+ *
45
+ * A prompt AFT admitted itself (a completion wake) is passed to the shared
46
+ * decision as a synthetic part, exactly as OpenCode 1 marks such parts: it
47
+ * interrupts a wait whenever `bash.detach_on_user_message` is on, but its text
48
+ * is never read for `&detach` and never edited.
49
+ *
50
+ * All edits are computed before any is applied, so a throw leaves the prompt
51
+ * exactly as it arrived.
52
+ */
53
+ export declare function interruptBashWaitsForV2Prompt(runtime: V2PromptDetachRuntime, event: V2PromptHookEvent): boolean;
54
+ /**
55
+ * The callback registered as OpenCode 2's session `prompt` hook. The host
56
+ * waits for it before admitting the message and a failure would reject the
57
+ * message, so it never fails and never waits: errors are caught, logged once
58
+ * per distinct reason, and the prompt passes through unchanged; the bridge
59
+ * detach request it may start is not awaited.
60
+ */
61
+ export declare function createV2PromptDetachHook(runtime: V2PromptDetachRuntime): (event: V2PromptHookEvent) => Effect.Effect<void>;
62
+ /**
63
+ * Register the prompt hook on an OpenCode 2 host context. The registration is
64
+ * scoped by the host, so it ends with the Location. A host context without
65
+ * `session.hook` gets no hook, which leaves waits un-detachable by messages
66
+ * but otherwise working.
67
+ */
68
+ export declare function registerV2PromptDetachHook(context: unknown, runtime: V2PromptDetachRuntime): Effect.Effect<boolean, never, unknown>;
69
+ export {};
70
+ //# sourceMappingURL=v2-prompt-detach.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-prompt-detach.d.ts","sourceRoot":"","sources":["../src/v2-prompt-detach.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,8DAA8D;AAC9D,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;QACzB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;QAC1B,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,SAAS,IAAI,SAAS,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAqCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,qBAAqB,EAC9B,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAuBT;AAMD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,GAC7B,CAAC,KAAK,EAAE,iBAAiB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAgBnD;AAOD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,qBAAqB,GAC7B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAYxC"}
@@ -0,0 +1,6 @@
1
+ import { Effect } from "effect";
2
+ /** Port the V1 raw-argument, result-text and current-model seams to V2. */
3
+ export declare function registerV2ToolHooks(context: unknown, runtime: {
4
+ hashlineEffective?: boolean;
5
+ }, registeredTools: ReadonlySet<string>): Effect.Effect<void, never, import("effect/Scope").Scope>;
6
+ //# sourceMappingURL=v2-tool-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-tool-hooks.d.ts","sourceRoot":"","sources":["../src/v2-tool-hooks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAqBxC,2EAA2E;AAC3E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,EACxC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,4DA6DrC"}
@@ -0,0 +1,4 @@
1
+ import { Effect } from "effect";
2
+ /** V2 consumes session system hooks, not V1's chat.system.transform. */
3
+ export declare function registerV2WorkflowHints(context: unknown, hintsBlock: string | null): Effect.Effect<boolean, never, import("effect/Scope").Scope>;
4
+ //# sourceMappingURL=v2-workflow-hints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-workflow-hints.d.ts","sourceRoot":"","sources":["../src/v2-workflow-hints.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,wEAAwE;AACxE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,+DAelF"}
@@ -4,6 +4,8 @@ import { type V2SessionAdmission } from "./session-delivery.js";
4
4
  export interface V2WakeHostContext {
5
5
  readonly session: V2SessionAdmission;
6
6
  }
7
+ /** Test-only: forget every route and held completion in the process-wide table. */
8
+ export declare function __resetV2WakeRoutesForTests(): void;
7
9
  declare const sharedBridgeOptions: Pick<BridgeOptions, "onBashCompletion" | "onBashLongRunning">;
8
10
  export interface V2RuntimeConsumer extends V2ToolConsumers {
9
11
  readonly bridgeOptions: typeof sharedBridgeOptions;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-consumer.d.ts","sourceRoot":"","sources":["../../src/wakes/runtime-consumer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,aAAa,EACd,MAAM,uBAAuB,CAAC;AAK/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AAEnF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;CACtC;AA0DD,QAAA,MAAM,mBAAmB,EAAE,IAAI,CAAC,aAAa,EAAE,kBAAkB,GAAG,mBAAmB,CAGtF,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,QAAQ,CAAC,aAAa,EAAE,OAAO,mBAAmB,CAAC;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAwBrF"}
1
+ {"version":3,"file":"runtime-consumer.d.ts","sourceRoot":"","sources":["../../src/wakes/runtime-consumer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,aAAa,EACd,MAAM,uBAAuB,CAAC;AAM/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AAEnF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;CACtC;AAyJD,mFAAmF;AACnF,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED,QAAA,MAAM,mBAAmB,EAAE,IAAI,CAAC,aAAa,EAAE,kBAAkB,GAAG,mBAAmB,CAGtF,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,QAAQ,CAAC,aAAa,EAAE,OAAO,mBAAmB,CAAC;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAiErF"}
@@ -1,4 +1,13 @@
1
1
  import { Effect } from "effect";
2
+ /**
3
+ * The `metadata.source` value on every prompt AFT admits into a session itself
4
+ * (background-completion wakes). OpenCode 2 runs every prompt, AFT's own
5
+ * included, through the session `prompt` hook; the hook reads this marker to
6
+ * tell AFT's machine-written text from text an operator typed, never the text.
7
+ */
8
+ export declare const AFT_PROMPT_SOURCE = "aft";
9
+ /** True when a prompt's metadata marks it as one AFT admitted itself. */
10
+ export declare function isAftOriginatedPrompt(metadata: unknown): boolean;
2
11
  export interface V2SessionPromptInput {
3
12
  readonly sessionID: string;
4
13
  readonly text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"session-delivery.d.ts","sourceRoot":"","sources":["../../src/wakes/session-delivery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACrE,SAAS,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAIhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAS;gBAEI,OAAO,EAAE,kBAAkB;IAExD,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;IAuBlE,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC;IAe7D,OAAO,IAAI,IAAI;CAKhB"}
1
+ {"version":3,"file":"session-delivery.d.ts","sourceRoot":"","sources":["../../src/wakes/session-delivery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAMhE;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACrE,SAAS,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAIhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAS;gBAEI,OAAO,EAAE,kBAAkB;IAExD,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;IAqClE,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC;IAe7D,OAAO,IAAI,IAAI;CAKhB"}
@@ -4,20 +4,25 @@ export interface WorkflowHintsOpts {
4
4
  bashBackgroundEnabled: boolean;
5
5
  /** Resolved bash compression flag. */
6
6
  bashCompressionEnabled: boolean;
7
+ /**
8
+ * Resolved `bash.rewrite` flag. Only rewritten `cat`/`head`/`tail` commands
9
+ * mint hashline tags through bash, so the hashline hint names them only when
10
+ * rewriting is on. Omitted means on.
11
+ */
12
+ bashRewriteEnabled?: boolean;
7
13
  /** Set of tool names that are not registered. */
8
14
  disabledTools: Set<string>;
9
15
  /** Whether the hashline `edit` arm is the one actually registered. */
10
16
  hashlineEffective?: boolean;
11
17
  }
12
18
  /**
13
- * Routing rule for hashline sessions.
14
- *
15
- * Navigation tools return source that looks edit-ready but never publishes a
16
- * snapshot, so an agent that inspects a symbol and then patches it is refused
17
- * for a tag it believes it already has. Naming the tag-minting tools is the
18
- * only way to distinguish "I have seen this code" from "I can address it".
19
+ * Routing rule for hashline sessions: which calls mint the tags a patch
20
+ * addresses (see `hashlineTagSourceSentence`). The bash rewrite path and each
21
+ * AFT navigation tool are named only when present.
19
22
  */
20
- export declare const HASHLINE_TAG_SOURCE_HINT = "**Hashline edit tags**: Only `read` (and accepted AFT `cat`/`head`/`tail` rewrites) mint hashline tags. `aft_zoom`, `aft_outline`, `grep`, `aft_search`, and conflict snippets do not. After navigation, call `read` on every file and range the patch addresses.";
23
+ export declare function hashlineTagSourceHint(bashRewritesMintTags: boolean, isRegistered?: (toolName: string) => boolean): string;
24
+ /** The hashline hint for a session whose bash rewrites `cat`/`head`/`tail`. */
25
+ export declare const HASHLINE_TAG_SOURCE_HINT: string;
21
26
  /**
22
27
  * Build the workflow hints block. Returns `null` when no hints are
23
28
  * applicable for the configured surface (e.g. `tool_surface: "minimal"`
@@ -32,6 +37,14 @@ export declare function buildWorkflowHints(opts: WorkflowHintsOpts): string | nu
32
37
  * `bash.background` setting controls whether the hint appears.
33
38
  */
34
39
  export declare function buildHintsFromConfig(config: AftConfig, disabledTools: Set<string>, hashlineEffective?: boolean): string | null;
40
+ /** Every tool name a workflow-hints section can mention. */
41
+ export declare const WORKFLOW_HINT_TOOL_NAMES: readonly string[];
42
+ /**
43
+ * Build the hints block for the tools that are actually registered. Every
44
+ * hint-relevant name missing from `registeredTools` is passed as absent, so
45
+ * sections for tools the agent cannot call are dropped.
46
+ */
47
+ export declare function buildHintsForRegisteredTools(config: AftConfig, registeredTools: ReadonlySet<string>, hashlineEffective?: boolean): string | null;
35
48
  /**
36
49
  * Fold the hints block into a host `system[]` array in place.
37
50
  *
@@ -1 +1 @@
1
- {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAEhE,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sCAAsC;IACtC,sBAAsB,EAAE,OAAO,CAAC;IAChC,iDAAiD;IACjD,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAID;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,sQACgO,CAAC;AAEtQ;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAwIzE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,iBAAiB,UAAQ,GACxB,MAAM,GAAG,IAAI,CAOf;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQ9E"}
1
+ {"version":3,"file":"workflow-hints.d.ts","sourceRoot":"","sources":["../src/workflow-hints.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,KAAK,SAAS,EAAqB,MAAM,aAAa,CAAC;AAGhE,MAAM,WAAW,iBAAiB;IAChC,2DAA2D;IAC3D,qBAAqB,EAAE,OAAO,CAAC;IAC/B,sCAAsC;IACtC,sBAAsB,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iDAAiD;IACjD,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC3B,sEAAsE;IACtE,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAID;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,oBAAoB,EAAE,OAAO,EAC7B,YAAY,GAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAoB,GACvD,MAAM,CAER;AAED,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,QAA8B,CAAC;AAEpE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,GAAG,IAAI,CAkJzE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,EACjB,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,EAC1B,iBAAiB,UAAQ,GACxB,MAAM,GAAG,IAAI,CASf;AAED,4DAA4D;AAC5D,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAYrD,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,SAAS,EACjB,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,EACpC,iBAAiB,UAAQ,GACxB,MAAM,GAAG,IAAI,CAGf;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAQ9E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.58.1",
3
+ "version": "0.59.0",
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",
@@ -33,13 +33,13 @@
33
33
  "schema": "bun scripts/build-schema.ts",
34
34
  "build:tool-schemas": "bun run scripts/build-tool-schemas.ts",
35
35
  "prepublishOnly": "bun run build",
36
- "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'",
36
+ "test:unit": "bun test src/__tests__ ./test/ ./__tests__/ --path-ignore-patterns 'src/__tests__/e2e/**'",
37
37
  "build:tui": "bun scripts/build-tui.ts",
38
38
  "smoke:tui": "bun test ./scripts/smoke-tui-pack-install.ts"
39
39
  },
40
40
  "dependencies": {
41
41
  "@clack/prompts": "^1.6.0",
42
- "@cortexkit/aft-bridge": "0.58.1",
42
+ "@cortexkit/aft-bridge": "0.59.0",
43
43
  "@opencode/client": "2.0.11",
44
44
  "@opentui/core": "0.4.3",
45
45
  "@opentui/solid": "0.4.3",
@@ -50,12 +50,12 @@
50
50
  "zod": "^4.4.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@cortexkit/aft-darwin-arm64": "0.58.1",
54
- "@cortexkit/aft-darwin-x64": "0.58.1",
55
- "@cortexkit/aft-linux-arm64": "0.58.1",
56
- "@cortexkit/aft-linux-x64": "0.58.1",
57
- "@cortexkit/aft-win32-arm64": "0.58.1",
58
- "@cortexkit/aft-win32-x64": "0.58.1"
53
+ "@cortexkit/aft-darwin-arm64": "0.59.0",
54
+ "@cortexkit/aft-darwin-x64": "0.59.0",
55
+ "@cortexkit/aft-linux-arm64": "0.59.0",
56
+ "@cortexkit/aft-linux-x64": "0.59.0",
57
+ "@cortexkit/aft-win32-arm64": "0.59.0",
58
+ "@cortexkit/aft-win32-x64": "0.59.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@opencode-ai/plugin": "^1.17.11",
@@ -17,8 +17,13 @@ import { startOpenCodeLiveConfigReload } from "../config-live-reload.js";
17
17
  import { debug, log, warn } from "../logger.js";
18
18
  import { resolvePluginVersion } from "../plugin-version.js";
19
19
  import { registerAftConfigErrorRpc, registerAftRpc } from "../rpc/register.js";
20
- import { hoistedV2ToolConsumers } from "../tools/hoisted/v2.js";
20
+ import { hoistedV2ToolConsumers, v2PromptChannelFor } from "../tools/hoisted/v2.js";
21
+ import { registerV2HostToolOverlapNotice } from "../v2-host-tool-overlap.js";
22
+ import { registerV2PromptDetachHook } from "../v2-prompt-detach.js";
23
+ import { registerV2ToolHooks } from "../v2-tool-hooks.js";
24
+ import { registerV2WorkflowHints } from "../v2-workflow-hints.js";
21
25
  import { createV2RuntimeConsumer } from "../wakes/runtime-consumer.js";
26
+ import { buildHintsForRegisteredTools } from "../workflow-hints.js";
22
27
  import {
23
28
  buildAftToolDefinitions,
24
29
  openCodeHashlineEffective,
@@ -34,8 +39,14 @@ const defaults = {
34
39
  registerTools: registerAftTools,
35
40
  registerRpc: registerAftRpc,
36
41
  registerConfigErrorRpc: registerAftConfigErrorRpc,
37
- toolConsumers: (context) => ({
38
- ...hoistedV2ToolConsumers(context),
42
+ registerPromptHook: registerV2PromptDetachHook,
43
+ registerToolHooks: registerV2ToolHooks,
44
+ registerWorkflowHints: registerV2WorkflowHints,
45
+ registerHostToolOverlapNotice: registerV2HostToolOverlapNotice,
46
+ // The prompt server comes from the user-only `opencode` block of the
47
+ // Location's config; without it AFT finds the server it runs inside.
48
+ toolConsumers: (context, config) => ({
49
+ ...hoistedV2ToolConsumers(context, v2PromptChannelFor(config?.opencode)),
39
50
  ...createV2RuntimeConsumer(context),
40
51
  }),
41
52
  acquireBridge,
@@ -75,7 +86,7 @@ async function bootLocation(context, location, dependencies) {
75
86
  dependencies,
76
87
  );
77
88
  const canonicalDirectory = location.project?.canonical ?? directory;
78
- const consumers = dependencies.toolConsumers(context);
89
+ const consumers = dependencies.toolConsumers(context, config);
79
90
  // Only a rejected configuration keeps AFT out of a project; there is no
80
91
  // config switch that turns it off.
81
92
  const isProjectEnabled = createProjectAcceptance(directory, dependencies);
@@ -115,6 +126,7 @@ async function bootLocation(context, location, dependencies) {
115
126
  const { hashlineEditRegistered } = applyToolSurfaceOverrides(pool, config, registeredTools);
116
127
  reportHashlineDowngrade(config, registeredTools, notify);
117
128
  toolContext.hashlineEffective = hashlineEditRegistered;
129
+ const hintsBlock = buildHintsForRegisteredTools(config, registeredTools, hashlineEditRegistered);
118
130
  // Keep this Location's live config keys current when a config file changes.
119
131
  // The finalizer that releases the bridge also stops the watch.
120
132
  const liveConfigReload = dependencies.startLiveConfigReload({
@@ -127,7 +139,7 @@ async function bootLocation(context, location, dependencies) {
127
139
  },
128
140
  notify,
129
141
  });
130
- return { consumers, pool, tools, liveConfigReload };
142
+ return { consumers, pool, tools, liveConfigReload, toolContext, directory, hintsBlock };
131
143
  }
132
144
 
133
145
  /**
@@ -206,12 +218,36 @@ export function makeServerEffect(overrides = {}) {
206
218
  );
207
219
  const rpc = yield* dependencies.registerRpc(context, location, runtime.pool);
208
220
  yield* Effect.addFinalizer(() => Effect.promise(() => rpc.dispose()));
221
+ // OpenCode 2 has no chat.message hook; its session prompt hook is where a
222
+ // new message detaches a waiting bash, as chat.message does on OpenCode 1.
223
+ // The config is read through the tool context so live reloads apply.
224
+ // The bridge asked first is the one this Location's tools run on, the
225
+ // Location's own directory; for a Location in a linked git worktree that
226
+ // is the worktree, not the canonical main checkout the pool was acquired
227
+ // for (issue #387). Other bridges are still tried after it.
228
+ yield* dependencies.registerPromptHook(context, {
229
+ pool: runtime.pool,
230
+ projectRoot: runtime.directory,
231
+ getConfig: () => runtime.toolContext.config,
232
+ registerSession: runtime.consumers.registerSession,
233
+ });
209
234
  yield* dependencies.registerTools(
210
235
  context,
211
236
  location,
212
237
  runtime.tools,
213
238
  runtime.consumers,
214
239
  );
240
+ yield* dependencies.registerToolHooks(context, runtime.toolContext, new Set(Object.keys(runtime.tools)));
241
+ yield* dependencies.registerWorkflowHints(context, runtime.hintsBlock);
242
+ // Tell the user once when the host's own patch or shell tool still runs
243
+ // beside AFT's apply_patch or bash. Checked on the first prompt, once
244
+ // every plugin has registered its tools, and shown by AFT's TUI plugin
245
+ // as a toast; nothing is written into the session, which the model reads.
246
+ yield* dependencies.registerHostToolOverlapNotice(
247
+ context,
248
+ new Set(Object.keys(runtime.tools)),
249
+ (message) => rpc.emitHostToolOverlap({ message }),
250
+ );
215
251
  });
216
252
  };
217
253
  }
@@ -1,4 +1,7 @@
1
+ import { Effect } from "effect";
2
+
1
3
  import { resolvePromptContext } from "./last-assistant-model.js";
4
+ import { isV2PluginContext } from "./v2-context.js";
2
5
 
3
6
  /**
4
7
  * Append an `ignored: true` synthetic user message to a session.
@@ -24,6 +27,21 @@ export async function sendIgnoredMessage(
24
27
  sessionID: string,
25
28
  text: string,
26
29
  ): Promise<void> {
30
+ if (isV2PluginContext(client)) {
31
+ const context = client as {
32
+ session: {
33
+ synthetic(input: {
34
+ sessionID: string;
35
+ text: string;
36
+ resume: false;
37
+ }): Effect.Effect<unknown, unknown>;
38
+ };
39
+ };
40
+ // V2 owns the current agent/model itself. Synthetic inbox entries neither
41
+ // switch that context nor resume a turn, and must actually run as Effects.
42
+ await Effect.runPromise(context.session.synthetic({ sessionID, text, resume: false }));
43
+ return;
44
+ }
27
45
  const typedClient = client as {
28
46
  session?: {
29
47
  prompt?: (input: unknown) => unknown;
@@ -1,4 +1,7 @@
1
+ import { Effect } from "effect";
2
+
1
3
  import { resolvePromptContext } from "./last-assistant-model.js";
4
+ import { isV2PluginContext } from "./v2-context.js";
2
5
 
3
6
  export const VISION_HOST_TIMEOUT_MS = 1500;
4
7
  type Model = { providerID: string; modelID: string };
@@ -69,12 +72,19 @@ export async function currentSessionVisionCapability(
69
72
  let capability = state.capabilities.get(key);
70
73
  if (!capability) {
71
74
  capability = (async () => {
72
- const api = (client as { provider?: { list?: () => Promise<unknown> } }).provider;
75
+ const api = (client as { provider?: { list?: () => unknown } }).provider;
73
76
  if (!api?.list) return undefined;
74
- const list = api.list.bind(api);
77
+ const list = async () => {
78
+ const result = api.list?.();
79
+ return isV2PluginContext(client)
80
+ ? Effect.runPromise(result as Effect.Effect<unknown, unknown>)
81
+ : await result;
82
+ };
75
83
  const listed = record(await bounded(list));
76
84
  const catalog = record(listed?.data) ?? listed;
77
- const providers = catalog?.all ?? catalog?.providers;
85
+ const providers = Array.isArray(listed?.data)
86
+ ? listed.data
87
+ : (catalog?.all ?? catalog?.providers);
78
88
  if (!Array.isArray(providers)) return undefined;
79
89
  const provider = providers.map(record).find((entry) => entry?.id === currentModel.providerID);
80
90
  const models = provider?.models;
@@ -83,7 +93,7 @@ export async function currentSessionVisionCapability(
83
93
  ? models.find((entry) => record(entry)?.id === currentModel.modelID)
84
94
  : record(models)?.[currentModel.modelID],
85
95
  );
86
- const inputs = record(entry?.modalities)?.input;
96
+ const inputs = record(entry?.capabilities)?.input ?? record(entry?.modalities)?.input;
87
97
  return Array.isArray(inputs)
88
98
  ? inputs.includes("image")
89
99
  : typeof entry?.attachment === "boolean"
@@ -133,19 +133,46 @@ export class AftRpcClient {
133
133
  const { signal } = options;
134
134
  throwIfAborted(signal);
135
135
 
136
+ // Fast path: reuse the endpoint that served the last warm response while
137
+ // it is still the newest live port file, skipping discovery's sequential
138
+ // health checks. Anything other than an accepted warm response falls back
139
+ // to the full scan below.
140
+ let placeholder: T | null = null;
141
+ let lastError: unknown = null;
142
+ let answeredPort: number | null = null;
143
+ const cached = this.cachedEndpoint();
144
+ if (cached) {
145
+ try {
146
+ const result = await this.callOne<T>(method, params, cached, signal);
147
+ if (this.looksLikePlaceholder(result)) {
148
+ placeholder = result;
149
+ answeredPort = cached.port;
150
+ } else if (!options.accept || options.accept(result)) {
151
+ return result;
152
+ } else {
153
+ answeredPort = cached.port;
154
+ }
155
+ } catch {
156
+ throwIfAborted(signal);
157
+ }
158
+ this.port = null;
159
+ this.token = null;
160
+ }
161
+
136
162
  // Try ALL discovered ports for this project (OpenCode TUI under --port 0
137
163
  // loads our plugin twice in the same process, so two RPC servers listen
138
- // and we have to try both — only one's bridge is actually warm).
139
- const infos = await this.resolvePortInfos(signal);
140
- if (infos.length === 0) {
164
+ // and we have to try both — only one's bridge is actually warm). A port
165
+ // that already answered on the fast path is not asked again.
166
+ const infos = (await this.resolvePortInfos(signal)).filter(
167
+ (info) => info.port !== answeredPort,
168
+ );
169
+ if (infos.length === 0 && placeholder === null) {
141
170
  throw new Error("AFT RPC server not available");
142
171
  }
143
172
 
144
173
  // First pass: try every port. Prefer responses that look like "warm
145
174
  // bridge" output (i.e. not the synthetic `status: "not_initialized"`
146
175
  // placeholder served when this instance's bridge hasn't been spawned).
147
- let placeholder: T | null = null;
148
- let lastError: unknown = null;
149
176
  for (const info of infos) {
150
177
  throwIfAborted(signal);
151
178
  try {
@@ -250,6 +277,20 @@ export class AftRpcClient {
250
277
  return status === "not_initialized";
251
278
  }
252
279
 
280
+ /**
281
+ * The endpoint that served the last warm response, if it is still the newest
282
+ * live port file for this project. Discovery tries ports newest first and
283
+ * returns the first warm answer, so while this holds the full scan would
284
+ * reach the same server first. Reading the port files also drops entries
285
+ * whose owning process has died, so a dead server is never reused.
286
+ */
287
+ private cachedEndpoint(): PortInfo | null {
288
+ if (this.port === null) return null;
289
+ const [newest] = this.readAllPortFiles();
290
+ if (!newest || newest.port !== this.port || newest.token !== this.token) return null;
291
+ return newest;
292
+ }
293
+
253
294
  /** Check if any RPC server is reachable. */
254
295
  async isAvailable(): Promise<boolean> {
255
296
  try {
@@ -319,6 +319,10 @@ export class AftRpcServer {
319
319
  return json({ error: "Invalid JSON" }, 400);
320
320
  }
321
321
 
322
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
323
+ return json({ error: "Expected a JSON object" }, 400);
324
+ }
325
+
322
326
  if (!tokensMatch(typeof params.token === "string" ? params.token : "", this.token)) {
323
327
  return json({ error: "Forbidden" }, 403);
324
328
  }
@@ -453,6 +457,12 @@ export class AftRpcServer {
453
457
  return;
454
458
  }
455
459
 
460
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
461
+ res.writeHead(400, { "Content-Type": "application/json" });
462
+ res.end(JSON.stringify({ error: "Expected a JSON object" }));
463
+ return;
464
+ }
465
+
456
466
  if (!tokensMatch(typeof params.token === "string" ? params.token : "", this.token)) {
457
467
  res.writeHead(403, { "Content-Type": "application/json" });
458
468
  res.end(JSON.stringify({ error: "Forbidden" }));