@goondocks/myco 0.16.2 → 0.17.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.
Files changed (172) hide show
  1. package/CONTRIBUTING.md +3 -3
  2. package/README.md +4 -3
  3. package/dist/{agent-run-MNU2QWHR.js → agent-run-BGW4TY3D.js} +7 -7
  4. package/dist/{agent-tasks-NCRKUU6E.js → agent-tasks-XUJ6FTPL.js} +7 -7
  5. package/dist/{chunk-P6C6ADBU.js → chunk-2IJ6C63F.js} +2 -2
  6. package/dist/{chunk-V2ZBYKDU.js → chunk-2WRXLYG6.js} +3 -3
  7. package/dist/{chunk-34NHDRWI.js → chunk-6SDC6V3N.js} +2 -2
  8. package/dist/{chunk-34NHDRWI.js.map → chunk-6SDC6V3N.js.map} +1 -1
  9. package/dist/{chunk-6JZEAOLG.js → chunk-CPL76CYD.js} +3 -3
  10. package/dist/{chunk-VWXDSDJU.js → chunk-DKSQMH5X.js} +2 -2
  11. package/dist/{chunk-GSKXOCFG.js → chunk-EBIYONNZ.js} +21 -1
  12. package/dist/chunk-EBIYONNZ.js.map +1 -0
  13. package/dist/{chunk-XAXQ72L3.js → chunk-FEX6ALLH.js} +2 -2
  14. package/dist/{chunk-UILSK6DK.js → chunk-GBYLHPML.js} +2 -2
  15. package/dist/{chunk-DZWSHCAC.js → chunk-KGL5QSDN.js} +2 -2
  16. package/dist/{chunk-4JVHWBZF.js → chunk-KKEMVH6D.js} +2 -2
  17. package/dist/{chunk-ZMODJWI5.js → chunk-KOTFMGL5.js} +4 -4
  18. package/dist/{chunk-4U6X35TH.js → chunk-KQOII5RU.js} +3 -3
  19. package/dist/{chunk-2E7YGLLN.js → chunk-LA7NDX3J.js} +2 -2
  20. package/dist/{chunk-RJRRHTAA.js → chunk-N6JNOSBQ.js} +3 -3
  21. package/dist/{chunk-CJ2KTRWI.js → chunk-OGNEW5CN.js} +2 -2
  22. package/dist/{chunk-C3GNF7RJ.js → chunk-QQ7CXA7Q.js} +5 -5
  23. package/dist/{chunk-I3S6L7QC.js → chunk-U3SSOSIR.js} +2 -2
  24. package/dist/{chunk-IRSNOBGD.js → chunk-UDBCAFXS.js} +2 -2
  25. package/dist/{chunk-SGYYOTNM.js → chunk-VGVRBSLC.js} +2 -2
  26. package/dist/{chunk-W7ZOOZMK.js → chunk-VQQ57UPG.js} +3 -3
  27. package/dist/{chunk-TIAYBVSI.js → chunk-VVGZL2HX.js} +171 -58
  28. package/dist/chunk-VVGZL2HX.js.map +1 -0
  29. package/dist/{chunk-BNAYBGPH.js → chunk-XQHL4GMO.js} +6 -5
  30. package/dist/{chunk-BNAYBGPH.js.map → chunk-XQHL4GMO.js.map} +1 -1
  31. package/dist/{chunk-RPILIIYT.js → chunk-Y7QCKCEJ.js} +2 -2
  32. package/dist/{chunk-D63XTGBV.js → chunk-YTOD6L6N.js} +6 -6
  33. package/dist/{chunk-D2NTFSVO.js → chunk-ZUSTCXHT.js} +3 -3
  34. package/dist/{cli-RYYABF2X.js → cli-353VNZIY.js} +40 -40
  35. package/dist/{client-5VXKGNN2.js → client-7KJ453V4.js} +4 -4
  36. package/dist/{config-VHHCGE4F.js → config-K3CJEFFO.js} +3 -3
  37. package/dist/{detect-6FNYONJF.js → detect-NJ2OREDP.js} +2 -2
  38. package/dist/{detect-providers-R7QOB3H6.js → detect-providers-OE6HWW3M.js} +4 -4
  39. package/dist/{doctor-M4Q7VCDO.js → doctor-RYFP7ABA.js} +14 -13
  40. package/dist/doctor-RYFP7ABA.js.map +1 -0
  41. package/dist/{executor-ULRFWJCH.js → executor-YOKYS7OT.js} +17 -17
  42. package/dist/{init-AEHAQFPK.js → init-XR2JZWY2.js} +17 -17
  43. package/dist/{init-wizard-SVKDS3LR.js → init-wizard-5CH2FD76.js} +7 -7
  44. package/dist/{installer-AARSFXI6.js → installer-45ZLP2RP.js} +2 -2
  45. package/dist/{llm-LS7U7BHC.js → llm-PGETQHZ2.js} +7 -7
  46. package/dist/{loader-QDWQTBX4.js → loader-AVWL7PNO.js} +3 -3
  47. package/dist/{loader-YQDG5GI5.js → loader-J56KP27U.js} +3 -3
  48. package/dist/{main-GAGOE6XB.js → main-6DGPZXRF.js} +170 -60
  49. package/dist/main-6DGPZXRF.js.map +1 -0
  50. package/dist/{open-4QMAL32X.js → open-P7YEH7UJ.js} +7 -7
  51. package/dist/{openai-embeddings-FUW6CSN2.js → openai-embeddings-LZKY6RV5.js} +4 -4
  52. package/dist/{openrouter-YSIUSUQL.js → openrouter-UTOZG6Z5.js} +4 -4
  53. package/dist/{post-compact-OAWEBEDK.js → post-compact-WPS4SONO.js} +7 -7
  54. package/dist/{post-tool-use-B3KOEOIM.js → post-tool-use-5WLLRGZ5.js} +6 -6
  55. package/dist/{post-tool-use-failure-2I5ELTTN.js → post-tool-use-failure-6C6HSBHI.js} +7 -7
  56. package/dist/{pre-compact-NOXNJ5EV.js → pre-compact-Z4E4JLAK.js} +7 -7
  57. package/dist/{provider-check-VEYONGNU.js → provider-check-CESRPIY5.js} +4 -4
  58. package/dist/{registry-5R3DLJQH.js → registry-SPKP2WLI.js} +4 -4
  59. package/dist/{remove-LX4G6KP7.js → remove-B2PFVQXK.js} +9 -9
  60. package/dist/{resolution-events-CHOKR35X.js → resolution-events-CLDXZF67.js} +4 -4
  61. package/dist/{restart-WSNBSALP.js → restart-XAJDOL3E.js} +8 -8
  62. package/dist/{search-Q6N3SHKP.js → search-ERTCTAQ3.js} +8 -8
  63. package/dist/{server-OFRKA6N7.js → server-LXUA7XUQ.js} +5 -5
  64. package/dist/{server-OFRKA6N7.js.map → server-LXUA7XUQ.js.map} +1 -1
  65. package/dist/{session-SKXJLJYH.js → session-433T6V3C.js} +9 -9
  66. package/dist/{session-end-5EIVRCPS.js → session-end-4Y5VY4OI.js} +6 -6
  67. package/dist/{session-start-OL2ICLED.js → session-start-3STH4HFL.js} +11 -11
  68. package/dist/{setup-llm-BRNQW7K2.js → setup-llm-UBBSQWX5.js} +8 -8
  69. package/dist/src/cli.js +1 -1
  70. package/dist/src/daemon/main.js +1 -1
  71. package/dist/src/hooks/post-tool-use.js +1 -1
  72. package/dist/src/hooks/session-end.js +1 -1
  73. package/dist/src/hooks/session-start.js +1 -1
  74. package/dist/src/hooks/stop.js +1 -1
  75. package/dist/src/hooks/user-prompt-submit.js +1 -1
  76. package/dist/src/mcp/server.js +1 -1
  77. package/dist/src/symbionts/manifests/claude-code.yaml +4 -0
  78. package/dist/src/symbionts/manifests/opencode.yaml +26 -0
  79. package/dist/src/symbionts/templates/claude-code/hooks.json +12 -12
  80. package/dist/src/symbionts/templates/codex/hooks.json +4 -4
  81. package/dist/src/symbionts/templates/cursor/hooks.json +9 -9
  82. package/dist/src/symbionts/templates/gemini/hooks.json +6 -6
  83. package/dist/src/symbionts/templates/opencode/mcp.json +6 -0
  84. package/dist/src/symbionts/templates/opencode/package.json +5 -0
  85. package/dist/src/symbionts/templates/opencode/plugin.ts +733 -0
  86. package/dist/src/symbionts/templates/opencode/settings.json +8 -0
  87. package/dist/src/symbionts/templates/vscode-copilot/hooks.json +7 -7
  88. package/dist/src/symbionts/templates/windsurf/hooks.json +4 -4
  89. package/dist/{stats-U5FHDIR7.js → stats-3NW7PGQK.js} +9 -9
  90. package/dist/{stop-YUZNQBRQ.js → stop-L7BLMHUD.js} +6 -6
  91. package/dist/{stop-failure-6WFAKH2U.js → stop-failure-P5MYHGAZ.js} +7 -7
  92. package/dist/{subagent-start-GWJXAAH3.js → subagent-start-AIEFG4HA.js} +7 -7
  93. package/dist/{subagent-stop-B44SMV2R.js → subagent-stop-TZ62BSNI.js} +7 -7
  94. package/dist/{task-completed-GIUFSRTP.js → task-completed-ZKVCUBCP.js} +7 -7
  95. package/dist/{team-3YI3UWB3.js → team-WHZW6IFU.js} +5 -5
  96. package/dist/ui/assets/{index-RYHXSJv1.js → index-2UyTdjlV.js} +12 -12
  97. package/dist/ui/assets/index-Cts1wLEW.css +1 -0
  98. package/dist/ui/index.html +2 -2
  99. package/dist/{update-QPRTLGYU.js → update-P7GIQLIV.js} +9 -9
  100. package/dist/{user-prompt-submit-FSYEPW7W.js → user-prompt-submit-4J7ZW6X3.js} +6 -6
  101. package/dist/{verify-ITBMLK67.js → verify-PSERIZPF.js} +8 -8
  102. package/dist/{version-VS2EDHBG.js → version-OHJ5ZLHX.js} +2 -2
  103. package/package.json +1 -1
  104. package/skills/rules/SKILL.md +1 -1
  105. package/dist/chunk-GSKXOCFG.js.map +0 -1
  106. package/dist/chunk-TIAYBVSI.js.map +0 -1
  107. package/dist/doctor-M4Q7VCDO.js.map +0 -1
  108. package/dist/main-GAGOE6XB.js.map +0 -1
  109. package/dist/ui/assets/index-Bjv_ck3c.css +0 -1
  110. /package/dist/{agent-run-MNU2QWHR.js.map → agent-run-BGW4TY3D.js.map} +0 -0
  111. /package/dist/{agent-tasks-NCRKUU6E.js.map → agent-tasks-XUJ6FTPL.js.map} +0 -0
  112. /package/dist/{chunk-P6C6ADBU.js.map → chunk-2IJ6C63F.js.map} +0 -0
  113. /package/dist/{chunk-V2ZBYKDU.js.map → chunk-2WRXLYG6.js.map} +0 -0
  114. /package/dist/{chunk-6JZEAOLG.js.map → chunk-CPL76CYD.js.map} +0 -0
  115. /package/dist/{chunk-VWXDSDJU.js.map → chunk-DKSQMH5X.js.map} +0 -0
  116. /package/dist/{chunk-XAXQ72L3.js.map → chunk-FEX6ALLH.js.map} +0 -0
  117. /package/dist/{chunk-UILSK6DK.js.map → chunk-GBYLHPML.js.map} +0 -0
  118. /package/dist/{chunk-DZWSHCAC.js.map → chunk-KGL5QSDN.js.map} +0 -0
  119. /package/dist/{chunk-4JVHWBZF.js.map → chunk-KKEMVH6D.js.map} +0 -0
  120. /package/dist/{chunk-ZMODJWI5.js.map → chunk-KOTFMGL5.js.map} +0 -0
  121. /package/dist/{chunk-4U6X35TH.js.map → chunk-KQOII5RU.js.map} +0 -0
  122. /package/dist/{chunk-2E7YGLLN.js.map → chunk-LA7NDX3J.js.map} +0 -0
  123. /package/dist/{chunk-RJRRHTAA.js.map → chunk-N6JNOSBQ.js.map} +0 -0
  124. /package/dist/{chunk-CJ2KTRWI.js.map → chunk-OGNEW5CN.js.map} +0 -0
  125. /package/dist/{chunk-C3GNF7RJ.js.map → chunk-QQ7CXA7Q.js.map} +0 -0
  126. /package/dist/{chunk-I3S6L7QC.js.map → chunk-U3SSOSIR.js.map} +0 -0
  127. /package/dist/{chunk-IRSNOBGD.js.map → chunk-UDBCAFXS.js.map} +0 -0
  128. /package/dist/{chunk-SGYYOTNM.js.map → chunk-VGVRBSLC.js.map} +0 -0
  129. /package/dist/{chunk-W7ZOOZMK.js.map → chunk-VQQ57UPG.js.map} +0 -0
  130. /package/dist/{chunk-RPILIIYT.js.map → chunk-Y7QCKCEJ.js.map} +0 -0
  131. /package/dist/{chunk-D63XTGBV.js.map → chunk-YTOD6L6N.js.map} +0 -0
  132. /package/dist/{chunk-D2NTFSVO.js.map → chunk-ZUSTCXHT.js.map} +0 -0
  133. /package/dist/{cli-RYYABF2X.js.map → cli-353VNZIY.js.map} +0 -0
  134. /package/dist/{client-5VXKGNN2.js.map → client-7KJ453V4.js.map} +0 -0
  135. /package/dist/{config-VHHCGE4F.js.map → config-K3CJEFFO.js.map} +0 -0
  136. /package/dist/{detect-6FNYONJF.js.map → detect-NJ2OREDP.js.map} +0 -0
  137. /package/dist/{detect-providers-R7QOB3H6.js.map → detect-providers-OE6HWW3M.js.map} +0 -0
  138. /package/dist/{executor-ULRFWJCH.js.map → executor-YOKYS7OT.js.map} +0 -0
  139. /package/dist/{init-AEHAQFPK.js.map → init-XR2JZWY2.js.map} +0 -0
  140. /package/dist/{init-wizard-SVKDS3LR.js.map → init-wizard-5CH2FD76.js.map} +0 -0
  141. /package/dist/{installer-AARSFXI6.js.map → installer-45ZLP2RP.js.map} +0 -0
  142. /package/dist/{llm-LS7U7BHC.js.map → llm-PGETQHZ2.js.map} +0 -0
  143. /package/dist/{loader-QDWQTBX4.js.map → loader-AVWL7PNO.js.map} +0 -0
  144. /package/dist/{loader-YQDG5GI5.js.map → loader-J56KP27U.js.map} +0 -0
  145. /package/dist/{open-4QMAL32X.js.map → open-P7YEH7UJ.js.map} +0 -0
  146. /package/dist/{openai-embeddings-FUW6CSN2.js.map → openai-embeddings-LZKY6RV5.js.map} +0 -0
  147. /package/dist/{openrouter-YSIUSUQL.js.map → openrouter-UTOZG6Z5.js.map} +0 -0
  148. /package/dist/{post-compact-OAWEBEDK.js.map → post-compact-WPS4SONO.js.map} +0 -0
  149. /package/dist/{post-tool-use-B3KOEOIM.js.map → post-tool-use-5WLLRGZ5.js.map} +0 -0
  150. /package/dist/{post-tool-use-failure-2I5ELTTN.js.map → post-tool-use-failure-6C6HSBHI.js.map} +0 -0
  151. /package/dist/{pre-compact-NOXNJ5EV.js.map → pre-compact-Z4E4JLAK.js.map} +0 -0
  152. /package/dist/{provider-check-VEYONGNU.js.map → provider-check-CESRPIY5.js.map} +0 -0
  153. /package/dist/{registry-5R3DLJQH.js.map → registry-SPKP2WLI.js.map} +0 -0
  154. /package/dist/{remove-LX4G6KP7.js.map → remove-B2PFVQXK.js.map} +0 -0
  155. /package/dist/{resolution-events-CHOKR35X.js.map → resolution-events-CLDXZF67.js.map} +0 -0
  156. /package/dist/{restart-WSNBSALP.js.map → restart-XAJDOL3E.js.map} +0 -0
  157. /package/dist/{search-Q6N3SHKP.js.map → search-ERTCTAQ3.js.map} +0 -0
  158. /package/dist/{session-SKXJLJYH.js.map → session-433T6V3C.js.map} +0 -0
  159. /package/dist/{session-end-5EIVRCPS.js.map → session-end-4Y5VY4OI.js.map} +0 -0
  160. /package/dist/{session-start-OL2ICLED.js.map → session-start-3STH4HFL.js.map} +0 -0
  161. /package/dist/{setup-llm-BRNQW7K2.js.map → setup-llm-UBBSQWX5.js.map} +0 -0
  162. /package/dist/{stats-U5FHDIR7.js.map → stats-3NW7PGQK.js.map} +0 -0
  163. /package/dist/{stop-YUZNQBRQ.js.map → stop-L7BLMHUD.js.map} +0 -0
  164. /package/dist/{stop-failure-6WFAKH2U.js.map → stop-failure-P5MYHGAZ.js.map} +0 -0
  165. /package/dist/{subagent-start-GWJXAAH3.js.map → subagent-start-AIEFG4HA.js.map} +0 -0
  166. /package/dist/{subagent-stop-B44SMV2R.js.map → subagent-stop-TZ62BSNI.js.map} +0 -0
  167. /package/dist/{task-completed-GIUFSRTP.js.map → task-completed-ZKVCUBCP.js.map} +0 -0
  168. /package/dist/{team-3YI3UWB3.js.map → team-WHZW6IFU.js.map} +0 -0
  169. /package/dist/{update-QPRTLGYU.js.map → update-P7GIQLIV.js.map} +0 -0
  170. /package/dist/{user-prompt-submit-FSYEPW7W.js.map → user-prompt-submit-4J7ZW6X3.js.map} +0 -0
  171. /package/dist/{verify-ITBMLK67.js.map → verify-PSERIZPF.js.map} +0 -0
  172. /package/dist/{version-VS2EDHBG.js.map → version-OHJ5ZLHX.js.map} +0 -0
@@ -0,0 +1,733 @@
1
+ // Managed by Myco. Regenerated on `myco update`. Edit src/symbionts/templates/opencode/plugin.ts in the Myco repo instead.
2
+ // myco:plugin-marker:opencode
3
+ //
4
+ // Myco Codebase Intelligence Plugin for OpenCode.
5
+ //
6
+ // This plugin runs inside opencode's Bun runtime and communicates with the local
7
+ // Myco daemon over HTTP — no subprocess spawns, no hook CLI, no stdin piping.
8
+ //
9
+ // Capture: POST /sessions/register, /sessions/unregister, /events, /events/stop
10
+ // Context: GET /api/digest
11
+ // Inject: client.session.prompt({ noReply: true, parts: [{ synthetic: true }] })
12
+ //
13
+ // See https://opencode.ai/docs/plugins/
14
+ //
15
+ // Degraded-mode safety: this plugin ships committed inside any project that has
16
+ // run `myco init` — the file lives at .opencode/plugins/myco.ts in that project's
17
+ // repo. When a teammate clones such a project WITHOUT having Myco installed
18
+ // locally, opencode will still load this plugin (the file is right there in the
19
+ // cloned repo). To stay invisible in that case, the plugin has NO external
20
+ // runtime imports — only node:fs and node:path, which are always available in
21
+ // Bun's runtime. Every path that would contact the Myco daemon gracefully no-ops
22
+ // when `.myco/daemon.json` is absent or the daemon is unreachable, so the plugin
23
+ // becomes invisible rather than throwing. Do NOT add runtime imports from
24
+ // @opencode-ai/plugin or any other package — that would break this guarantee.
25
+
26
+ import { readFileSync, appendFileSync, mkdirSync } from "node:fs";
27
+ import { join } from "node:path";
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Constants
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Keep in sync with `TOOL_OUTPUT_PREVIEW_CHARS` in src/constants.ts (currently 200).
35
+ * The plugin file is standalone and cannot import from Myco — this value is copied
36
+ * so every symbiont records tool_output previews at the same length.
37
+ */
38
+ const TOOL_OUTPUT_PREVIEW_CHARS = 200;
39
+
40
+ /** Timeout for daemon HTTP calls — must be short so we never block opencode. */
41
+ const MYCO_FETCH_TIMEOUT_MS = 3000;
42
+
43
+ /** Tail window read from opencode when building the end-of-turn assistant summary. */
44
+ const SESSION_IDLE_TAIL_LIMIT = 12;
45
+
46
+ /** Max size of resume context injection to keep resumed sessions lean. */
47
+ const RESUME_CONTEXT_MAX_CHARS = 4000;
48
+
49
+ /** Heading prefix for compaction context — makes Myco's contribution recognizable in the compacted summary. */
50
+ const COMPACTION_HEADING = "## Myco — Project Context (preserved across compaction)\n\n";
51
+
52
+ /**
53
+ * Marker set on the `metadata` field of every synthetic TextPartInput this
54
+ * plugin injects via `client.session.prompt({ noReply: true, ... })`. The
55
+ * `chat.message` handler checks for this marker and skips matching messages
56
+ * so the injection doesn't re-enter as if it were a new user prompt.
57
+ *
58
+ * Why not the `synthetic` flag? opencode's own prompt.ts uses `synthetic: true`
59
+ * for ~20 distinct internal purposes (plan-mode prompts, build-switch
60
+ * transitions, subagent task summaries, shell-impl wrappers). Filtering on
61
+ * the synthetic flag rejects legitimate user messages whenever opencode has
62
+ * appended one of its own synthetic parts — which caused real user prompts
63
+ * to silently drop in live testing.
64
+ */
65
+ const MYCO_METADATA_MARKER = "myco";
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Types
69
+ // ---------------------------------------------------------------------------
70
+
71
+ type MessagePart = { type?: string; text?: string };
72
+ type SessionMessage = { info?: { role?: string }; parts?: MessagePart[] };
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Small helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ function isRecord(value: unknown): value is Record<string, unknown> {
79
+ return typeof value === "object" && value !== null;
80
+ }
81
+
82
+ function pickString(
83
+ record: Record<string, unknown>,
84
+ keys: readonly string[],
85
+ ): string | undefined {
86
+ for (const key of keys) {
87
+ const value = record[key];
88
+ if (typeof value === "string" && value.length > 0) return value;
89
+ }
90
+ return undefined;
91
+ }
92
+
93
+ export function normalizeToolInput(toolInput: unknown): unknown {
94
+ if (!isRecord(toolInput)) return toolInput;
95
+
96
+ const filePath = pickString(toolInput, ["file_path", "filePath", "path"]);
97
+ const workdir = pickString(toolInput, ["workdir", "cwd"]);
98
+ const command = pickString(toolInput, ["command", "cmd"]);
99
+
100
+ return {
101
+ ...toolInput,
102
+ ...(filePath ? { file_path: filePath } : {}),
103
+ ...(workdir ? { workdir } : {}),
104
+ ...(command ? { command } : {}),
105
+ };
106
+ }
107
+
108
+ export function collectAssistantSummaryFromMessages(messages: SessionMessage[]): string {
109
+ const summaryParts: string[] = [];
110
+ let foundAssistantBlock = false;
111
+
112
+ for (let i = messages.length - 1; i >= 0; i--) {
113
+ const message = messages[i];
114
+ if (message?.info?.role !== "assistant") {
115
+ if (foundAssistantBlock) break;
116
+ continue;
117
+ }
118
+
119
+ foundAssistantBlock = true;
120
+ const text = (message.parts ?? [])
121
+ .filter((part) => part.type === "text" && part.text)
122
+ .map((part) => part.text as string)
123
+ .join("\n")
124
+ .trim();
125
+ if (text) summaryParts.unshift(text);
126
+ }
127
+
128
+ return summaryParts.join("\n").trim();
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Daemon HTTP transport — all communication with the local Myco daemon.
133
+ // Every function is best-effort: failures are swallowed so the plugin cannot
134
+ // interfere with opencode when Myco is absent or the daemon is unreachable.
135
+ // ---------------------------------------------------------------------------
136
+
137
+ /**
138
+ * Port cache for `.myco/daemon.json`. Read once on first access; refreshed on
139
+ * the next call that follows a failed HTTP request (handles daemon restarts
140
+ * mid-session). `undefined` = never loaded, `null` = loaded but absent.
141
+ */
142
+ let cachedDaemonPort: number | null | undefined = undefined;
143
+
144
+ /**
145
+ * Active opencode sessions tracked by this plugin instance. Populated on
146
+ * `session.created` and drained on `session.deleted` / `server.instance.disposed`.
147
+ *
148
+ * Opencode has no `session.end` event — when the TUI exits normally (Ctrl+C,
149
+ * close terminal), the session stays "active" from the daemon's perspective
150
+ * until the session-maintenance job sweeps it (1-hour threshold). To close
151
+ * sessions cleanly on TUI exit, we track them locally and call unregister
152
+ * for each one when `server.instance.disposed` fires.
153
+ */
154
+ const activeOpencodeSessions = new Set<string>();
155
+
156
+ /** Resume injections are process-local and should run at most once per session. */
157
+ const resumeInjectedSessions = new Set<string>();
158
+
159
+ /** Read the Myco daemon port from .myco/daemon.json in the project directory. */
160
+ function readDaemonPortFromDisk(directory: string): number | null {
161
+ try {
162
+ const raw = readFileSync(join(directory, ".myco", "daemon.json"), "utf-8");
163
+ const info = JSON.parse(raw) as { port?: number };
164
+ return typeof info.port === "number" ? info.port : null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
169
+
170
+ /** Get the cached daemon port, loading from disk on first access. */
171
+ function getDaemonPort(directory: string): number | null {
172
+ if (cachedDaemonPort === undefined) cachedDaemonPort = readDaemonPortFromDisk(directory);
173
+ return cachedDaemonPort;
174
+ }
175
+
176
+ /** Force-refresh the daemon port from disk — used after a fetch failure in case the daemon restarted. */
177
+ function refreshDaemonPort(directory: string): number | null {
178
+ cachedDaemonPort = readDaemonPortFromDisk(directory);
179
+ return cachedDaemonPort;
180
+ }
181
+
182
+ /** Fetch with a short timeout. Returns the Response on success, null on failure. */
183
+ async function fetchWithTimeout(url: string, init?: RequestInit): Promise<Response | null> {
184
+ const controller = new AbortController();
185
+ const timer = setTimeout(() => controller.abort(), MYCO_FETCH_TIMEOUT_MS);
186
+ try {
187
+ const res = await fetch(url, { ...init, signal: controller.signal });
188
+ return res.ok ? res : null;
189
+ } catch {
190
+ return null;
191
+ } finally {
192
+ clearTimeout(timer);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Fetch from a daemon endpoint with a single retry after refreshing the port.
198
+ * The retry handles the case where the daemon restarted on a different port
199
+ * mid-session; the cache hot-path avoids a sync disk read on every HTTP call.
200
+ */
201
+ async function fetchFromDaemon(
202
+ directory: string,
203
+ path: string,
204
+ init?: RequestInit,
205
+ ): Promise<Response | null> {
206
+ const port = getDaemonPort(directory);
207
+ if (!port) return null;
208
+
209
+ const first = await fetchWithTimeout(`http://localhost:${port}${path}`, init);
210
+ if (first) return first;
211
+
212
+ // Retry once with a refreshed port — the daemon may have restarted.
213
+ const freshPort = refreshDaemonPort(directory);
214
+ if (!freshPort || freshPort === port) return null;
215
+ return fetchWithTimeout(`http://localhost:${freshPort}${path}`, init);
216
+ }
217
+
218
+ /**
219
+ * POST JSON to a daemon endpoint.
220
+ * Returns `{ ok, data }` — `ok` is true when the HTTP call succeeded, `data`
221
+ * is the parsed response body (may be absent if the body was empty or not JSON).
222
+ */
223
+ async function postJson(
224
+ directory: string,
225
+ path: string,
226
+ body: Record<string, unknown>,
227
+ ): Promise<{ ok: boolean; data?: unknown }> {
228
+ const res = await fetchFromDaemon(directory, path, {
229
+ method: "POST",
230
+ headers: { "Content-Type": "application/json" },
231
+ body: JSON.stringify(body),
232
+ });
233
+ if (!res) return { ok: false };
234
+ try {
235
+ return { ok: true, data: await res.json() };
236
+ } catch {
237
+ return { ok: true };
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Append an event to the local buffer at .myco/buffer/<session-id>.jsonl.
243
+ *
244
+ * Used as a fallback when the daemon HTTP POST fails (daemon down, network
245
+ * error, timeout). The daemon replays buffered events via reconcileBufferBatches
246
+ * at startup, so events captured here are NOT lost — they land in the vault
247
+ * the next time the daemon comes back up. Mirrors the fallback pattern in
248
+ * src/hooks/send-event.ts + src/capture/buffer.ts that every other symbiont
249
+ * uses (claude-code, cursor, codex, etc.) via the hook CLI path.
250
+ *
251
+ * Without this fallback, opencode would have a significant parity gap — all
252
+ * other symbionts preserve events across daemon downtime, but opencode events
253
+ * would be silently dropped the moment the daemon was unreachable.
254
+ *
255
+ * The entry shape matches EventBuffer.append(): event payload with session_id
256
+ * stripped (it's in the filename) and an auto-injected ISO timestamp.
257
+ */
258
+ function bufferEvent(
259
+ directory: string,
260
+ sessionId: string,
261
+ event: Record<string, unknown>,
262
+ ): void {
263
+ try {
264
+ const bufferDir = join(directory, ".myco", "buffer");
265
+ mkdirSync(bufferDir, { recursive: true });
266
+ const filePath = join(bufferDir, `${sessionId}.jsonl`);
267
+ // Strip session_id from the entry — it's encoded in the filename
268
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
269
+ const { session_id: _sid, ...payload } = event;
270
+ const line = JSON.stringify({
271
+ ...payload,
272
+ timestamp: payload.timestamp ?? new Date().toISOString(),
273
+ });
274
+ appendFileSync(filePath, line + "\n");
275
+ } catch (err) {
276
+ // eslint-disable-next-line no-console
277
+ console.error("[myco] Failed to buffer event:", err);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * POST a capture event to the daemon with buffer fallback on failure.
283
+ * Used for user_prompt and tool_use events — both are replayed from the
284
+ * buffer by reconcileBufferBatches when the daemon restarts.
285
+ */
286
+ async function postEventWithBuffer(
287
+ directory: string,
288
+ sessionId: string,
289
+ event: Record<string, unknown>,
290
+ ): Promise<void> {
291
+ const result = await postJson(directory, "/events", event);
292
+ if (!result.ok) {
293
+ bufferEvent(directory, sessionId, event);
294
+ }
295
+ }
296
+
297
+ /** Register an opencode session with the daemon. */
298
+ async function mycoRegisterSession(
299
+ directory: string,
300
+ sessionId: string,
301
+ parentSessionId: string | undefined,
302
+ ): Promise<void> {
303
+ await postJson(directory, "/sessions/register", {
304
+ session_id: sessionId,
305
+ agent: "opencode",
306
+ parent_session_id: parentSessionId,
307
+ started_at: new Date().toISOString(),
308
+ });
309
+ }
310
+
311
+ /** Unregister an opencode session. */
312
+ async function mycoUnregisterSession(directory: string, sessionId: string): Promise<void> {
313
+ await postJson(directory, "/sessions/unregister", { session_id: sessionId });
314
+ }
315
+
316
+ /** Post a user prompt event. Images, if any, are shipped as an array of
317
+ * `{ data: base64, mediaType }` objects — the daemon's event dispatcher persists
318
+ * them as attachments keyed to the newly-opened prompt batch.
319
+ *
320
+ * Opencode has no on-disk transcript for Myco to mine, so images attached by
321
+ * the user in the TUI must travel with the prompt event itself. Other symbionts
322
+ * (claude-code, cursor) extract images from their JSONL transcripts at stop time.
323
+ *
324
+ * Falls back to the local buffer if the daemon is unreachable so events are
325
+ * replayed on daemon restart (same resilience the other symbionts get via
326
+ * src/hooks/send-event.ts).
327
+ */
328
+ async function mycoPostUserPrompt(
329
+ directory: string,
330
+ sessionId: string,
331
+ prompt: string,
332
+ images: Array<{ data: string; mediaType: string }>,
333
+ ): Promise<void> {
334
+ await postEventWithBuffer(directory, sessionId, {
335
+ type: "user_prompt",
336
+ session_id: sessionId,
337
+ agent: "opencode",
338
+ prompt,
339
+ ...(images.length > 0 ? { images } : {}),
340
+ });
341
+ }
342
+
343
+ /** Post a tool use event. Falls back to the local buffer on failure. */
344
+ async function mycoPostToolUse(
345
+ directory: string,
346
+ sessionId: string,
347
+ toolName: string,
348
+ toolInput: unknown,
349
+ toolOutput: string,
350
+ ): Promise<void> {
351
+ await postEventWithBuffer(directory, sessionId, {
352
+ type: "tool_use",
353
+ session_id: sessionId,
354
+ agent: "opencode",
355
+ tool_name: toolName,
356
+ tool_input: toolInput,
357
+ output_preview: toolOutput,
358
+ });
359
+ }
360
+
361
+ /** Post a stop event with the last assistant message as the response summary. */
362
+ async function mycoPostStop(
363
+ directory: string,
364
+ sessionId: string,
365
+ lastAssistantMessage: string | undefined,
366
+ ): Promise<void> {
367
+ await postJson(directory, "/events/stop", {
368
+ session_id: sessionId,
369
+ agent: "opencode",
370
+ last_assistant_message: lastAssistantMessage,
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Fetch the session-start context for a new opencode session. Hits the daemon's
376
+ * config-aware `POST /context` endpoint, which selects the digest tier the user
377
+ * has configured (`config.context.digest_tier`, default 5000) and returns the
378
+ * full session context (digest + branch + session ID lines).
379
+ *
380
+ * This is the same endpoint Claude Code's session-start hook uses, so opencode
381
+ * sessions receive the same context the user has configured for every other agent.
382
+ */
383
+ async function fetchMycoSessionContext(
384
+ directory: string,
385
+ sessionId: string,
386
+ ): Promise<string | null> {
387
+ const result = await postJson(directory, "/context", { session_id: sessionId });
388
+ if (!result.ok) return null;
389
+ const data = result.data as { text?: string } | undefined;
390
+ const text = data?.text?.trim() ?? "";
391
+ return text.length > 0 ? text : null;
392
+ }
393
+
394
+ /** Fetch a small resume recap for a resumed opencode session. */
395
+ async function fetchMycoResumeContext(
396
+ directory: string,
397
+ sessionId: string,
398
+ parentSessionId: string,
399
+ ): Promise<string | null> {
400
+ const result = await postJson(directory, "/context/resume", {
401
+ session_id: sessionId,
402
+ parent_session_id: parentSessionId,
403
+ });
404
+ if (!result.ok) return null;
405
+ const data = result.data as { text?: string } | undefined;
406
+ const text = data?.text?.trim() ?? "";
407
+ if (!text || text.length > RESUME_CONTEXT_MAX_CHARS) return null;
408
+ return text;
409
+ }
410
+
411
+ /** Post a compaction telemetry event. */
412
+ async function mycoPostCompact(
413
+ directory: string,
414
+ sessionId: string,
415
+ trigger: string | undefined,
416
+ ): Promise<void> {
417
+ await postEventWithBuffer(directory, sessionId, {
418
+ type: "pre_compact",
419
+ session_id: sessionId,
420
+ agent: "opencode",
421
+ ...(trigger ? { trigger } : {}),
422
+ });
423
+ }
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // Opencode session injection — push synthetic context into session history.
427
+ // ---------------------------------------------------------------------------
428
+
429
+ /**
430
+ * Inject text into an opencode session as a synthetic (plugin-authored) user turn
431
+ * without triggering an AI response. The text part carries:
432
+ * - `synthetic: true` so opencode's TUI hides it from the chat log
433
+ * - `metadata.myco: true` so our own `chat.message` handler can distinguish
434
+ * this re-entry from a real user message (see MYCO_METADATA_MARKER)
435
+ * Errors are swallowed — injection is best-effort.
436
+ */
437
+ async function injectSyntheticContext(
438
+ client: unknown,
439
+ sessionId: string,
440
+ text: string,
441
+ ): Promise<void> {
442
+ try {
443
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
444
+ const c = client as any;
445
+ await c.session.prompt({
446
+ path: { id: sessionId },
447
+ body: {
448
+ parts: [
449
+ {
450
+ type: "text",
451
+ text,
452
+ synthetic: true,
453
+ metadata: { [MYCO_METADATA_MARKER]: true },
454
+ },
455
+ ],
456
+ noReply: true,
457
+ },
458
+ });
459
+ } catch (error) {
460
+ // eslint-disable-next-line no-console
461
+ console.error("[myco] Failed to inject synthetic context:", error);
462
+ }
463
+ }
464
+
465
+ /** Flatten todo items into a newline-separated summary. */
466
+ function formatTodos(
467
+ todos: Array<{ id?: string; content?: string; status?: string }>,
468
+ ): string {
469
+ if (!todos || todos.length === 0) return "";
470
+ return todos
471
+ .map((t) => `[${t.status || "pending"}] ${t.content || ""}`)
472
+ .join("\n");
473
+ }
474
+
475
+ /** Truncate tool output for storage. */
476
+ function summarizeToolOutput(output: unknown): string {
477
+ if (typeof output !== "string") return "";
478
+ return output.length > TOOL_OUTPUT_PREVIEW_CHARS
479
+ ? output.slice(0, TOOL_OUTPUT_PREVIEW_CHARS) + "..."
480
+ : output;
481
+ }
482
+
483
+ // ---------------------------------------------------------------------------
484
+ // Plugin entry
485
+ // ---------------------------------------------------------------------------
486
+
487
+ /**
488
+ * Opencode plugin entry. The function signature matches opencode's Plugin type
489
+ * via duck typing — we deliberately do NOT import the Plugin type from
490
+ * @opencode-ai/plugin so this file has zero external runtime dependencies.
491
+ * That guarantee lets teammates who clone a project that uses Myco still run
492
+ * opencode cleanly even when they don't have Myco installed locally.
493
+ *
494
+ * @param {{ client: any, directory: string, worktree: string }} ctx
495
+ */
496
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
497
+ export const MycoPlugin = async ({ client, directory, worktree }: { client: any; directory: string; worktree: string }) => {
498
+ // Best-effort init log. Wrapped in try-catch so a future SDK shape change in
499
+ // opencode (e.g. client.app.log moving) cannot prevent the plugin from
500
+ // registering its handlers.
501
+ try {
502
+ await client.app.log({
503
+ service: "myco",
504
+ level: "info",
505
+ message: "Myco plugin initialized",
506
+ extra: { directory, worktree },
507
+ });
508
+ } catch {
509
+ // Swallow — init log is diagnostic only.
510
+ }
511
+
512
+ return {
513
+ /**
514
+ * Generic event handler: session lifecycle, todos.
515
+ */
516
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
517
+ event: async ({ event }: { event: any }) => {
518
+ if (event.type === "session.created") {
519
+ const info = event.properties?.info ?? {};
520
+ const sessionId: string | undefined = info.id;
521
+ if (!sessionId) return;
522
+
523
+ activeOpencodeSessions.add(sessionId);
524
+
525
+ const parentSessionId = info.parentID || undefined;
526
+ const contextPromise = parentSessionId
527
+ ? (resumeInjectedSessions.has(sessionId)
528
+ ? Promise.resolve(null)
529
+ : fetchMycoResumeContext(directory, sessionId, parentSessionId))
530
+ : fetchMycoSessionContext(directory, sessionId);
531
+
532
+ // Run registration and context fetch concurrently — they don't depend
533
+ // on each other, and parallelizing saves one round-trip of latency.
534
+ const [, sessionContext] = await Promise.all([
535
+ mycoRegisterSession(directory, sessionId, parentSessionId),
536
+ contextPromise,
537
+ ]);
538
+
539
+ if (sessionContext) {
540
+ await injectSyntheticContext(client, sessionId, sessionContext);
541
+ if (parentSessionId) resumeInjectedSessions.add(sessionId);
542
+ }
543
+ return;
544
+ }
545
+
546
+ if (event.type === "session.deleted") {
547
+ const info = event.properties?.info ?? {};
548
+ if (info.id) {
549
+ activeOpencodeSessions.delete(info.id);
550
+ resumeInjectedSessions.delete(info.id);
551
+ await mycoUnregisterSession(directory, info.id);
552
+ }
553
+ return;
554
+ }
555
+
556
+ if (event.type === "server.instance.disposed") {
557
+ // Opencode TUI is shutting down. Flush all tracked sessions so the
558
+ // daemon can mark them completed immediately rather than waiting for
559
+ // the stale-session maintenance sweep (1-hour threshold).
560
+ //
561
+ // Fire-and-forget-parallel: the Bun process is about to exit, so we
562
+ // can't rely on awaited fetches completing. Promise.all gives the
563
+ // unregister calls their best shot at landing before teardown; any
564
+ // that don't make it fall back to the session-maintenance job.
565
+ if (activeOpencodeSessions.size === 0) return;
566
+ const toClose = Array.from(activeOpencodeSessions);
567
+ activeOpencodeSessions.clear();
568
+ for (const id of toClose) resumeInjectedSessions.delete(id);
569
+ await Promise.all(toClose.map((id) => mycoUnregisterSession(directory, id)));
570
+ return;
571
+ }
572
+
573
+ if (event.type === "session.idle") {
574
+ const sessionId = event.properties?.sessionID;
575
+ if (!sessionId) return;
576
+
577
+ // Fetch the last assistant message for a response summary.
578
+ let responseSummary = "";
579
+ try {
580
+ // `limit` is a tail-limit in opencode's server (returns the last N
581
+ // messages in chronological order — verified empirically against
582
+ // opencode v1.4.1 via `opencode serve`). The tail window is large
583
+ // enough to capture a multi-message assistant block at end-of-turn
584
+ // without reading the full session history on every idle event.
585
+ const result = await client.session.messages({
586
+ path: { id: sessionId },
587
+ query: { directory, limit: SESSION_IDLE_TAIL_LIMIT },
588
+ });
589
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
590
+ const messages = ((result as any)?.data ?? []) as SessionMessage[];
591
+ responseSummary = collectAssistantSummaryFromMessages(messages);
592
+ } catch (err) {
593
+ // eslint-disable-next-line no-console
594
+ console.error("[myco] Failed to fetch messages for summary:", err);
595
+ }
596
+
597
+ await mycoPostStop(directory, sessionId, responseSummary || undefined);
598
+ return;
599
+ }
600
+
601
+ if (event.type === "todo.updated") {
602
+ const sessionId = event.properties?.sessionID;
603
+ if (!sessionId) return;
604
+ const todos = event.properties?.todos ?? [];
605
+ await mycoPostToolUse(
606
+ directory,
607
+ sessionId,
608
+ "TodoUpdate",
609
+ { todos, count: todos.length },
610
+ formatTodos(todos),
611
+ );
612
+ }
613
+ },
614
+
615
+ /**
616
+ * Chat message: capture the user prompt + any image attachments.
617
+ *
618
+ * Per-turn spore injection is intentionally not done here. A previous iteration
619
+ * injected spores via session.prompt({ noReply: true }) inside this handler, but
620
+ * opencode re-fires chat.message for the synthetic turn and the first real user
621
+ * message landed during the re-entrancy window. Agents can fetch context on
622
+ * demand via the myco_context and myco_search MCP tools.
623
+ *
624
+ * Re-entrancy guard: we check for `metadata.myco === true` on any part to
625
+ * detect our session-start digest injection coming back around. Opencode
626
+ * itself sets `synthetic: true` for many internal purposes (plan-mode
627
+ * prompts, build-switch transitions, subagent task summaries), so the
628
+ * `synthetic` flag alone is NOT reliable as a re-entrancy signal — it
629
+ * would silently drop any user prompt that opencode touched for one of
630
+ * those internal reasons.
631
+ */
632
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
633
+ "chat.message": async (input: any, output: any) => {
634
+ const sessionId = input?.sessionID;
635
+ if (!sessionId) return;
636
+
637
+ // Part shapes we care about: text (for the prompt string) and file
638
+ // (for image attachments encoded as data URLs — FilePart.url is
639
+ // `data:<mime>;base64,<data>` per
640
+ // packages/app/src/components/prompt-input/attachments.ts in opencode).
641
+ const allParts = (output?.parts ?? []) as Array<{
642
+ type?: string;
643
+ text?: string;
644
+ mime?: string;
645
+ url?: string;
646
+ synthetic?: boolean;
647
+ metadata?: { [key: string]: unknown };
648
+ }>;
649
+ // Skip if any part carries the Myco metadata marker — that means
650
+ // chat.message is firing for our own injectSyntheticContext call.
651
+ if (allParts.some((p) => p.metadata?.[MYCO_METADATA_MARKER] === true)) return;
652
+
653
+ // Prompt text = user's real text only. opencode emits `synthetic: true`
654
+ // text parts for internal scaffolding when the message contains file
655
+ // mentions, plan-mode switches, subagent tasks, and similar — see
656
+ // packages/opencode/src/session/prompt.ts. Those parts include full
657
+ // file contents, tool-call scaffolding, plan instructions, etc. Joining
658
+ // them into prompt_text would bloat every captured user prompt with
659
+ // system-level content that the user never typed.
660
+ const textParts = allParts
661
+ .filter((p) => p.type === "text" && p.text && p.synthetic !== true)
662
+ .map((p) => p.text as string);
663
+ const prompt = textParts.join("\n");
664
+ if (!prompt) return;
665
+
666
+ // Extract any image attachments from FilePart data URLs. Non-image file
667
+ // parts (code snippets, documents) are ignored here — only images travel
668
+ // to Myco as binary attachments via the existing attachment pipeline.
669
+ const images: Array<{ data: string; mediaType: string }> = [];
670
+ for (const part of allParts) {
671
+ if (
672
+ part.type !== "file" ||
673
+ !part.mime?.startsWith("image/") ||
674
+ typeof part.url !== "string" ||
675
+ !part.url.startsWith("data:")
676
+ ) {
677
+ continue;
678
+ }
679
+ const commaIdx = part.url.indexOf(",");
680
+ if (commaIdx <= 0) continue;
681
+ const base64 = part.url.slice(commaIdx + 1);
682
+ if (base64) images.push({ data: base64, mediaType: part.mime });
683
+ }
684
+
685
+ await mycoPostUserPrompt(directory, sessionId, prompt, images);
686
+ },
687
+
688
+ /**
689
+ * Post-tool execution: ship tool usage to Myco.
690
+ *
691
+ * We forward `input.args` as `tool_input` — NOT `output.metadata` — because
692
+ * `args` carries the tool invocation arguments (including `filePath` for
693
+ * write/edit/patch tools), which Myco's plan-capture matcher needs to detect
694
+ * writes to .opencode/plans/*.md.
695
+ */
696
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
697
+ "tool.execute.after": async (input: any, output: any) => {
698
+ const sessionId = input?.sessionID;
699
+ if (!sessionId) return;
700
+
701
+ const toolName = input?.tool ?? "unknown";
702
+ const toolInput = normalizeToolInput(input?.args ?? output?.metadata ?? {});
703
+ const toolOutput = summarizeToolOutput(output?.output);
704
+
705
+ await mycoPostToolUse(directory, sessionId, toolName, toolInput, toolOutput);
706
+ },
707
+
708
+ /**
709
+ * Compaction hook: fires BEFORE opencode generates a continuation summary
710
+ * during session compaction. Pushing the session context into output.context
711
+ * ensures Myco's project knowledge survives compaction rather than being
712
+ * dropped. The fetched context respects the user's configured digest tier.
713
+ *
714
+ * See https://opencode.ai/docs/plugins/#compaction-hooks
715
+ */
716
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
717
+ "experimental.session.compacting": async (input: any, output: any) => {
718
+ const sessionId = input?.sessionID;
719
+ if (!sessionId) return;
720
+
721
+ await mycoPostCompact(directory, sessionId, typeof input?.trigger === "string" ? input.trigger : undefined);
722
+
723
+ const sessionContext = await fetchMycoSessionContext(directory, sessionId);
724
+ if (!sessionContext) return;
725
+
726
+ if (Array.isArray(output?.context)) {
727
+ output.context.push(COMPACTION_HEADING + sessionContext);
728
+ }
729
+ },
730
+ };
731
+ };
732
+
733
+ export default MycoPlugin;