opencode-dux 1.0.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 (302) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +452 -0
  3. package/dist/agents/descriptions.d.ts +6 -0
  4. package/dist/agents/designer.d.ts +2 -0
  5. package/dist/agents/explorer.d.ts +2 -0
  6. package/dist/agents/fixer.d.ts +2 -0
  7. package/dist/agents/index.d.ts +22 -0
  8. package/dist/agents/interpreter.d.ts +2 -0
  9. package/dist/agents/librarian.d.ts +2 -0
  10. package/dist/agents/oracle.d.ts +2 -0
  11. package/dist/agents/orchestrator.d.ts +27 -0
  12. package/dist/agents/overrides.d.ts +18 -0
  13. package/dist/agents/prompt-blocks.d.ts +97 -0
  14. package/dist/agents/steward.d.ts +3 -0
  15. package/dist/cli/config-io.d.ts +24 -0
  16. package/dist/cli/config-manager.d.ts +4 -0
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +1006 -0
  19. package/dist/cli/install.d.ts +2 -0
  20. package/dist/cli/mcps.d.ts +13 -0
  21. package/dist/cli/model-key-normalization.d.ts +1 -0
  22. package/dist/cli/paths.d.ts +35 -0
  23. package/dist/cli/providers.d.ts +137 -0
  24. package/dist/cli/skills.d.ts +22 -0
  25. package/dist/cli/system.d.ts +5 -0
  26. package/dist/cli/types.d.ts +38 -0
  27. package/dist/config/constants.d.ts +12 -0
  28. package/dist/config/index.d.ts +4 -0
  29. package/dist/config/loader.d.ts +40 -0
  30. package/dist/config/runtime-preset.d.ts +12 -0
  31. package/dist/config/schema.d.ts +281 -0
  32. package/dist/config/utils.d.ts +10 -0
  33. package/dist/discovery/local/types.d.ts +79 -0
  34. package/dist/discovery/local.d.ts +73 -0
  35. package/dist/discovery/mcp-servers.d.ts +88 -0
  36. package/dist/discovery/skills.d.ts +94 -0
  37. package/dist/hooks/apply-patch/codec.d.ts +7 -0
  38. package/dist/hooks/apply-patch/errors.d.ts +25 -0
  39. package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
  40. package/dist/hooks/apply-patch/index.d.ts +15 -0
  41. package/dist/hooks/apply-patch/matching.d.ts +26 -0
  42. package/dist/hooks/apply-patch/operations.d.ts +3 -0
  43. package/dist/hooks/apply-patch/patch.d.ts +2 -0
  44. package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
  45. package/dist/hooks/apply-patch/resolution.d.ts +19 -0
  46. package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
  47. package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
  48. package/dist/hooks/apply-patch/types.d.ts +80 -0
  49. package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
  50. package/dist/hooks/auto-update-checker/checker.d.ts +32 -0
  51. package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
  52. package/dist/hooks/auto-update-checker/index.d.ts +18 -0
  53. package/dist/hooks/auto-update-checker/types.d.ts +22 -0
  54. package/dist/hooks/chat-headers.d.ts +16 -0
  55. package/dist/hooks/context-pressure-reminder/index.d.ts +33 -0
  56. package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
  57. package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
  58. package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
  59. package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
  60. package/dist/hooks/filter-available-skills/index.d.ts +32 -0
  61. package/dist/hooks/foreground-fallback/index.d.ts +72 -0
  62. package/dist/hooks/image-hook.d.ts +5 -0
  63. package/dist/hooks/index.d.ts +14 -0
  64. package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
  65. package/dist/hooks/json-error-recovery/index.d.ts +1 -0
  66. package/dist/hooks/phase-reminder/index.d.ts +26 -0
  67. package/dist/hooks/post-file-tool-nudge/index.d.ts +19 -0
  68. package/dist/hooks/task-session-manager/index.d.ts +52 -0
  69. package/dist/hooks/todo-continuation/index.d.ts +53 -0
  70. package/dist/hooks/todo-continuation/todo-hygiene.d.ts +35 -0
  71. package/dist/index.d.ts +5 -0
  72. package/dist/index.js +31782 -0
  73. package/dist/mcp/context7.d.ts +6 -0
  74. package/dist/mcp/grep-app.d.ts +6 -0
  75. package/dist/mcp/index.d.ts +13 -0
  76. package/dist/mcp/types.d.ts +12 -0
  77. package/dist/mcp/websearch.d.ts +9 -0
  78. package/dist/skills/registry.d.ts +29 -0
  79. package/dist/subscriptions/accounts-store.d.ts +57 -0
  80. package/dist/subscriptions/index.d.ts +13 -0
  81. package/dist/subscriptions/neuralwatt-scraper.d.ts +14 -0
  82. package/dist/subscriptions/opencode-go-scraper.d.ts +27 -0
  83. package/dist/subscriptions/types.d.ts +115 -0
  84. package/dist/subscriptions/usage-service.d.ts +74 -0
  85. package/dist/tools/ast-grep/cli.d.ts +15 -0
  86. package/dist/tools/ast-grep/constants.d.ts +25 -0
  87. package/dist/tools/ast-grep/downloader.d.ts +5 -0
  88. package/dist/tools/ast-grep/index.d.ts +10 -0
  89. package/dist/tools/ast-grep/tools.d.ts +3 -0
  90. package/dist/tools/ast-grep/types.d.ts +30 -0
  91. package/dist/tools/ast-grep/utils.d.ts +4 -0
  92. package/dist/tools/delegate.d.ts +14 -0
  93. package/dist/tools/index.d.ts +5 -0
  94. package/dist/tools/preset-manager.d.ts +27 -0
  95. package/dist/tools/smartfetch/binary.d.ts +3 -0
  96. package/dist/tools/smartfetch/cache.d.ts +6 -0
  97. package/dist/tools/smartfetch/constants.d.ts +12 -0
  98. package/dist/tools/smartfetch/index.d.ts +3 -0
  99. package/dist/tools/smartfetch/network.d.ts +38 -0
  100. package/dist/tools/smartfetch/secondary-model.d.ts +28 -0
  101. package/dist/tools/smartfetch/tool.d.ts +3 -0
  102. package/dist/tools/smartfetch/types.d.ts +122 -0
  103. package/dist/tools/smartfetch/utils.d.ts +18 -0
  104. package/dist/tui-state.d.ts +168 -0
  105. package/dist/tui.d.ts +37 -0
  106. package/dist/tui.js +1896 -0
  107. package/dist/utils/agent-variant.d.ts +63 -0
  108. package/dist/utils/compat.d.ts +30 -0
  109. package/dist/utils/env.d.ts +1 -0
  110. package/dist/utils/index.d.ts +9 -0
  111. package/dist/utils/internal-initiator.d.ts +6 -0
  112. package/dist/utils/logger.d.ts +8 -0
  113. package/dist/utils/polling.d.ts +21 -0
  114. package/dist/utils/session-manager.d.ts +55 -0
  115. package/dist/utils/session.d.ts +90 -0
  116. package/dist/utils/subagent-depth.d.ts +35 -0
  117. package/dist/utils/system-collapse.d.ts +6 -0
  118. package/dist/utils/task.d.ts +4 -0
  119. package/dist/utils/zip-extractor.d.ts +1 -0
  120. package/index.ts +1 -0
  121. package/opencode-dux.schema.json +634 -0
  122. package/package.json +103 -0
  123. package/src/agents/descriptions.ts +55 -0
  124. package/src/agents/designer.test.ts +86 -0
  125. package/src/agents/designer.ts +154 -0
  126. package/src/agents/display-name.test.ts +186 -0
  127. package/src/agents/explorer.test.ts +79 -0
  128. package/src/agents/explorer.ts +144 -0
  129. package/src/agents/fixer.test.ts +79 -0
  130. package/src/agents/fixer.ts +145 -0
  131. package/src/agents/index.test.ts +472 -0
  132. package/src/agents/index.ts +248 -0
  133. package/src/agents/interpreter.ts +136 -0
  134. package/src/agents/librarian.test.ts +80 -0
  135. package/src/agents/librarian.ts +145 -0
  136. package/src/agents/oracle.test.ts +89 -0
  137. package/src/agents/oracle.ts +184 -0
  138. package/src/agents/orchestrator.test.ts +116 -0
  139. package/src/agents/orchestrator.ts +574 -0
  140. package/src/agents/overrides.ts +95 -0
  141. package/src/agents/prompt-blocks.test.ts +114 -0
  142. package/src/agents/prompt-blocks.ts +640 -0
  143. package/src/agents/steward.ts +146 -0
  144. package/src/cli/config-io.test.ts +536 -0
  145. package/src/cli/config-io.ts +473 -0
  146. package/src/cli/config-manager.test.ts +141 -0
  147. package/src/cli/config-manager.ts +4 -0
  148. package/src/cli/index.ts +88 -0
  149. package/src/cli/install.ts +282 -0
  150. package/src/cli/mcps.test.ts +62 -0
  151. package/src/cli/mcps.ts +39 -0
  152. package/src/cli/model-key-normalization.test.ts +21 -0
  153. package/src/cli/model-key-normalization.ts +60 -0
  154. package/src/cli/paths.test.ts +167 -0
  155. package/src/cli/paths.ts +144 -0
  156. package/src/cli/providers.test.ts +118 -0
  157. package/src/cli/providers.ts +141 -0
  158. package/src/cli/skills.test.ts +111 -0
  159. package/src/cli/skills.ts +103 -0
  160. package/src/cli/system.test.ts +91 -0
  161. package/src/cli/system.ts +180 -0
  162. package/src/cli/types.ts +43 -0
  163. package/src/config/constants.ts +58 -0
  164. package/src/config/index.ts +4 -0
  165. package/src/config/loader.test.ts +1194 -0
  166. package/src/config/loader.ts +269 -0
  167. package/src/config/model-resolution.test.ts +176 -0
  168. package/src/config/runtime-preset.test.ts +61 -0
  169. package/src/config/runtime-preset.ts +37 -0
  170. package/src/config/schema.ts +248 -0
  171. package/src/config/utils.test.ts +41 -0
  172. package/src/config/utils.ts +23 -0
  173. package/src/discovery/local/types.ts +85 -0
  174. package/src/discovery/local.ts +322 -0
  175. package/src/discovery/mcp-servers.ts +804 -0
  176. package/src/discovery/skills.ts +959 -0
  177. package/src/hooks/apply-patch/codec.test.ts +184 -0
  178. package/src/hooks/apply-patch/codec.ts +352 -0
  179. package/src/hooks/apply-patch/errors.ts +117 -0
  180. package/src/hooks/apply-patch/execution-context.ts +432 -0
  181. package/src/hooks/apply-patch/hook.test.ts +768 -0
  182. package/src/hooks/apply-patch/index.ts +126 -0
  183. package/src/hooks/apply-patch/matching.test.ts +215 -0
  184. package/src/hooks/apply-patch/matching.ts +586 -0
  185. package/src/hooks/apply-patch/operations.test.ts +1535 -0
  186. package/src/hooks/apply-patch/operations.ts +3 -0
  187. package/src/hooks/apply-patch/patch.ts +9 -0
  188. package/src/hooks/apply-patch/prepared-changes.ts +400 -0
  189. package/src/hooks/apply-patch/resolution.test.ts +420 -0
  190. package/src/hooks/apply-patch/resolution.ts +437 -0
  191. package/src/hooks/apply-patch/rewrite.ts +496 -0
  192. package/src/hooks/apply-patch/test-helpers.ts +52 -0
  193. package/src/hooks/apply-patch/types.ts +111 -0
  194. package/src/hooks/auto-update-checker/cache.test.ts +179 -0
  195. package/src/hooks/auto-update-checker/cache.ts +188 -0
  196. package/src/hooks/auto-update-checker/checker.test.ts +159 -0
  197. package/src/hooks/auto-update-checker/checker.ts +308 -0
  198. package/src/hooks/auto-update-checker/constants.ts +33 -0
  199. package/src/hooks/auto-update-checker/index.test.ts +282 -0
  200. package/src/hooks/auto-update-checker/index.ts +225 -0
  201. package/src/hooks/auto-update-checker/types.ts +26 -0
  202. package/src/hooks/chat-headers.test.ts +236 -0
  203. package/src/hooks/chat-headers.ts +97 -0
  204. package/src/hooks/context-pressure-reminder/index.test.ts +179 -0
  205. package/src/hooks/context-pressure-reminder/index.ts +137 -0
  206. package/src/hooks/delegate-task-retry/guidance.ts +41 -0
  207. package/src/hooks/delegate-task-retry/hook.ts +23 -0
  208. package/src/hooks/delegate-task-retry/index.test.ts +38 -0
  209. package/src/hooks/delegate-task-retry/index.ts +7 -0
  210. package/src/hooks/delegate-task-retry/patterns.ts +79 -0
  211. package/src/hooks/filter-available-skills/index.test.ts +297 -0
  212. package/src/hooks/filter-available-skills/index.ts +160 -0
  213. package/src/hooks/foreground-fallback/index.test.ts +624 -0
  214. package/src/hooks/foreground-fallback/index.ts +374 -0
  215. package/src/hooks/image-hook.ts +6 -0
  216. package/src/hooks/index.ts +17 -0
  217. package/src/hooks/json-error-recovery/hook.ts +73 -0
  218. package/src/hooks/json-error-recovery/index.test.ts +111 -0
  219. package/src/hooks/json-error-recovery/index.ts +6 -0
  220. package/src/hooks/phase-reminder/index.test.ts +74 -0
  221. package/src/hooks/phase-reminder/index.ts +85 -0
  222. package/src/hooks/post-file-tool-nudge/index.test.ts +94 -0
  223. package/src/hooks/post-file-tool-nudge/index.ts +63 -0
  224. package/src/hooks/task-session-manager/index.test.ts +833 -0
  225. package/src/hooks/task-session-manager/index.ts +434 -0
  226. package/src/hooks/todo-continuation/index.test.ts +3026 -0
  227. package/src/hooks/todo-continuation/index.ts +878 -0
  228. package/src/hooks/todo-continuation/todo-hygiene.test.ts +204 -0
  229. package/src/hooks/todo-continuation/todo-hygiene.ts +207 -0
  230. package/src/index.ts +1672 -0
  231. package/src/mcp/context7.ts +14 -0
  232. package/src/mcp/grep-app.ts +11 -0
  233. package/src/mcp/index.test.ts +96 -0
  234. package/src/mcp/index.ts +66 -0
  235. package/src/mcp/types.ts +16 -0
  236. package/src/mcp/websearch.ts +47 -0
  237. package/src/skills/codemap/README.md +60 -0
  238. package/src/skills/codemap/SKILL.md +174 -0
  239. package/src/skills/codemap/scripts/codemap.mjs +483 -0
  240. package/src/skills/codemap/scripts/codemap.test.ts +129 -0
  241. package/src/skills/registry.ts +218 -0
  242. package/src/skills/simplify/README.md +19 -0
  243. package/src/skills/simplify/SKILL.md +138 -0
  244. package/src/subscriptions/accounts-store.test.ts +236 -0
  245. package/src/subscriptions/accounts-store.ts +184 -0
  246. package/src/subscriptions/index.ts +30 -0
  247. package/src/subscriptions/neuralwatt-scraper.ts +108 -0
  248. package/src/subscriptions/opencode-go-scraper.ts +301 -0
  249. package/src/subscriptions/types.ts +145 -0
  250. package/src/subscriptions/usage-service.test.ts +202 -0
  251. package/src/subscriptions/usage-service.ts +651 -0
  252. package/src/tools/ast-grep/cli.ts +257 -0
  253. package/src/tools/ast-grep/constants.ts +214 -0
  254. package/src/tools/ast-grep/downloader.ts +131 -0
  255. package/src/tools/ast-grep/index.ts +24 -0
  256. package/src/tools/ast-grep/tools.ts +117 -0
  257. package/src/tools/ast-grep/types.ts +51 -0
  258. package/src/tools/ast-grep/utils.ts +126 -0
  259. package/src/tools/delegate-handoff.test.ts +18 -0
  260. package/src/tools/delegate.ts +508 -0
  261. package/src/tools/index.ts +8 -0
  262. package/src/tools/preset-manager.test.ts +795 -0
  263. package/src/tools/preset-manager.ts +332 -0
  264. package/src/tools/smartfetch/binary.ts +58 -0
  265. package/src/tools/smartfetch/cache.test.ts +34 -0
  266. package/src/tools/smartfetch/cache.ts +112 -0
  267. package/src/tools/smartfetch/constants.ts +29 -0
  268. package/src/tools/smartfetch/index.ts +8 -0
  269. package/src/tools/smartfetch/network.test.ts +178 -0
  270. package/src/tools/smartfetch/network.ts +614 -0
  271. package/src/tools/smartfetch/secondary-model.test.ts +85 -0
  272. package/src/tools/smartfetch/secondary-model.ts +276 -0
  273. package/src/tools/smartfetch/tool.test.ts +60 -0
  274. package/src/tools/smartfetch/tool.ts +832 -0
  275. package/src/tools/smartfetch/types.ts +135 -0
  276. package/src/tools/smartfetch/utils.test.ts +24 -0
  277. package/src/tools/smartfetch/utils.ts +456 -0
  278. package/src/tui-state.test.ts +867 -0
  279. package/src/tui-state.ts +1255 -0
  280. package/src/tui.test.ts +336 -0
  281. package/src/tui.ts +1539 -0
  282. package/src/utils/agent-variant.test.ts +244 -0
  283. package/src/utils/agent-variant.ts +187 -0
  284. package/src/utils/compat.ts +91 -0
  285. package/src/utils/env.ts +12 -0
  286. package/src/utils/index.ts +9 -0
  287. package/src/utils/internal-initiator.ts +28 -0
  288. package/src/utils/logger.test.ts +220 -0
  289. package/src/utils/logger.ts +136 -0
  290. package/src/utils/polling.test.ts +191 -0
  291. package/src/utils/polling.ts +67 -0
  292. package/src/utils/session-manager.test.ts +173 -0
  293. package/src/utils/session-manager.ts +356 -0
  294. package/src/utils/session.test.ts +110 -0
  295. package/src/utils/session.ts +389 -0
  296. package/src/utils/subagent-depth.test.ts +170 -0
  297. package/src/utils/subagent-depth.ts +75 -0
  298. package/src/utils/system-collapse.test.ts +86 -0
  299. package/src/utils/system-collapse.ts +24 -0
  300. package/src/utils/task.test.ts +24 -0
  301. package/src/utils/task.ts +20 -0
  302. package/src/utils/zip-extractor.ts +102 -0
@@ -0,0 +1,322 @@
1
+ import type { PluginInput } from '@opencode-ai/plugin';
2
+ import { log } from '../utils/logger';
3
+ import { SDK_DISCOVERY_TIMEOUT_MS } from '../config/constants';
4
+
5
+ /**
6
+ * A single MCP server discovered via the OpenCode SDK.
7
+ */
8
+ export interface DiscoveredMcp {
9
+ /** Unique name used as the config key (e.g., 'playwright', 'github'). */
10
+ name: string;
11
+ /** Current connection status reported by OpenCode. */
12
+ status:
13
+ | 'connected'
14
+ | 'disabled'
15
+ | 'failed'
16
+ | 'needs_auth'
17
+ | 'needs_client_registration';
18
+ /** Human-readable description, may include error details when status is 'failed'. */
19
+ description?: string;
20
+ /** Categorization tags derived from the MCP name. */
21
+ tags: string[];
22
+ /** Agents that benefit most from this MCP based on defaults. */
23
+ recommendedAgents: string[];
24
+ }
25
+
26
+ /**
27
+ * A single skill discovered via the OpenCode SDK.
28
+ */
29
+ export interface DiscoveredSkill {
30
+ /** Unique skill name. */
31
+ name: string;
32
+ /** Human-readable description. */
33
+ description?: string;
34
+ /** Filesystem location of the skill (SKILL.md path). */
35
+ location: string;
36
+ /** Categorization tags derived from the skill name. */
37
+ tags: string[];
38
+ /** Agents that benefit most from this skill based on defaults. */
39
+ recommendedAgents: string[];
40
+ /** Whether the skill ships with the plugin or was added by the user. */
41
+ source: 'bundled' | 'user';
42
+ }
43
+
44
+ /**
45
+ * Complete result of an SDK-based local discovery scan.
46
+ */
47
+ export interface LocalDiscoveryResult {
48
+ /** Discovered skills. */
49
+ skills: DiscoveredSkill[];
50
+ /** Discovered MCP servers. */
51
+ mcps: DiscoveredMcp[];
52
+ /** Unix timestamp (ms) when the scan was performed. */
53
+ scannedAt: number;
54
+ /** How long the scan took in milliseconds. */
55
+ scanDurationMs: number;
56
+ }
57
+
58
+ // ── Tag derivation ──────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Known MCP/skill name patterns mapped to tags.
62
+ * Used to derive tags from names dynamically at scan time.
63
+ */
64
+ const NAME_TAG_MAP: Record<string, string[]> = {
65
+ playwright: ['browser', 'ui', 'testing'],
66
+ github: ['github', 'git'],
67
+ filesystem: ['filesystem', 'files'],
68
+ websearch: ['web', 'search'],
69
+ context7: ['docs', 'search'],
70
+ grep_app: ['search', 'code'],
71
+ ast_grep: ['search', 'code'],
72
+ };
73
+
74
+ /**
75
+ * Derive tags from a name string (MCP or skill name).
76
+ *
77
+ * Checks known patterns in a case-insensitive manner and returns the first
78
+ * matching tag set. Falls back to an empty array when no pattern matches.
79
+ *
80
+ * @param name - The MCP or skill name to derive tags from
81
+ * @returns Array of tag strings
82
+ */
83
+ function deriveTags(name: string): string[] {
84
+ const lower = name.toLowerCase();
85
+ for (const [key, tags] of Object.entries(NAME_TAG_MAP)) {
86
+ if (lower.includes(key)) {
87
+ return [...tags];
88
+ }
89
+ }
90
+ return [];
91
+ }
92
+
93
+ // ── Recommended agents derivation ───────────────────────────────────────────
94
+
95
+ /**
96
+ * Derive recommended agent names for a given MCP or skill name.
97
+ *
98
+ * Agent-MCP recommendations have been removed; always returns an empty
99
+ * array. MCPs are auto-managed by the plugin with no user config control.
100
+ *
101
+ * @returns Empty array
102
+ */
103
+ function deriveRecommendedAgents(_name: string): string[] {
104
+ return [];
105
+ }
106
+
107
+ // ── Skill source determination ──────────────────────────────────────────────
108
+
109
+ /**
110
+ * Determine whether a skill is bundled or user-added based on its location.
111
+ *
112
+ * Skills located within the plugin's own `src/skills` directory are considered
113
+ * bundled; all others are treated as user-added.
114
+ *
115
+ * @param location - The filesystem location of the skill
116
+ * @returns `'bundled'` if the skill ships with the plugin, `'user'` otherwise
117
+ */
118
+ function determineSource(location: string): 'bundled' | 'user' {
119
+ // Normalise path separators before checking
120
+ const normalised = location.replace(/\\/g, '/');
121
+ return normalised.includes('src/skills') ? 'bundled' : 'user';
122
+ }
123
+
124
+ // ── Timeout helper ──────────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Race a promise against a configurable timeout.
128
+ * If the timeout fires first, the returned promise rejects with an error
129
+ * that includes the label, giving downstream try/catch a clear indication
130
+ * of which SDK call hung.
131
+ *
132
+ * @param promise - The async operation to protect
133
+ * @param ms - Timeout in milliseconds
134
+ * @param label - Human-readable label for error messages
135
+ */
136
+ function withTimeout<T>(
137
+ promise: Promise<T>,
138
+ ms: number,
139
+ label: string,
140
+ ): Promise<T> {
141
+ return Promise.race([
142
+ promise,
143
+ new Promise<T>((_, reject) =>
144
+ setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms),
145
+ ),
146
+ ]);
147
+ }
148
+
149
+ // ── Internal SDK call helpers ───────────────────────────────────────────────
150
+
151
+ /**
152
+ * Scan MCP servers via `ctx.client.mcp.status()`.
153
+ *
154
+ * Maps each entry's status value from the SDK's format to the
155
+ * `DiscoveredMcp.status` union, derives tags and recommended agents from
156
+ * the server name.
157
+ *
158
+ * @param ctx - OpenCode plugin input
159
+ * @returns Array of discovered MCPs (empty on failure)
160
+ */
161
+ async function scanMcpStatuses(ctx: PluginInput): Promise<DiscoveredMcp[]> {
162
+ const response = await withTimeout(
163
+ ctx.client.mcp.status(),
164
+ SDK_DISCOVERY_TIMEOUT_MS,
165
+ 'MCP status scan',
166
+ );
167
+ // McpStatusResponses returns { [key: string]: McpStatus } inside `data`
168
+ const data = response.data as
169
+ | Record<string, { status: string; error?: string }>
170
+ | undefined;
171
+
172
+ if (!data) {
173
+ return [];
174
+ }
175
+
176
+ return Object.entries(data).map(([name, statusInfo]) => ({
177
+ name,
178
+ status: statusInfo.status as DiscoveredMcp['status'],
179
+ description: statusInfo.error,
180
+ tags: deriveTags(name),
181
+ recommendedAgents: deriveRecommendedAgents(name),
182
+ }));
183
+ }
184
+
185
+ /**
186
+ * Scan skills via `ctx.client.instance.skill()`.
187
+ *
188
+ * Uses a type-asserted call because `instance.skill()` may not be reflected
189
+ * in the published SDK type definitions yet. Returns an empty array when the
190
+ * method is unavailable or the call fails.
191
+ *
192
+ * @param ctx - OpenCode plugin input
193
+ * @returns Array of discovered skills (empty on failure)
194
+ */
195
+ async function scanInstanceSkills(
196
+ ctx: PluginInput,
197
+ ): Promise<DiscoveredSkill[]> {
198
+ const skillFn = (
199
+ ctx.client.instance as unknown as {
200
+ skill?: (opts?: Record<string, unknown>) => Promise<{
201
+ data?: Array<{
202
+ name: string;
203
+ description?: string;
204
+ location: string;
205
+ }>;
206
+ }>;
207
+ }
208
+ ).skill;
209
+
210
+ if (typeof skillFn !== 'function') {
211
+ log(
212
+ '[discovery] ctx.client.instance.skill() is not available, returning empty skills',
213
+ );
214
+ return [];
215
+ }
216
+
217
+ const response = await withTimeout(
218
+ skillFn(),
219
+ SDK_DISCOVERY_TIMEOUT_MS,
220
+ 'Instance skill scan',
221
+ );
222
+ const items = Array.isArray(response?.data) ? response.data : [];
223
+
224
+ return items.map((s) => ({
225
+ name: s.name,
226
+ description: s.description,
227
+ location: s.location,
228
+ tags: deriveTags(s.name),
229
+ recommendedAgents: deriveRecommendedAgents(s.name),
230
+ source: determineSource(s.location),
231
+ }));
232
+ }
233
+
234
+ // ── Cache ───────────────────────────────────────────────────────────────────
235
+
236
+ /** Internal cache entry for a completed scan result. */
237
+ interface CacheEntry {
238
+ result: LocalDiscoveryResult;
239
+ timestamp: number;
240
+ }
241
+
242
+ /** Cache TTL in milliseconds (5 minutes). */
243
+ const CACHE_TTL_MS = 5 * 60 * 1_000;
244
+
245
+ /** Module-level scan result cache. */
246
+ let cache: CacheEntry | null = null;
247
+
248
+ // ── Exported API ────────────────────────────────────────────────────────────
249
+
250
+ /**
251
+ * Scan locally configured MCP servers and skills using the OpenCode SDK.
252
+ *
253
+ * Calls `ctx.client.mcp.status()` and `ctx.client.instance.skill()` for fast,
254
+ * authoritative local discovery. Each SDK call is independently wrapped in a
255
+ * try/catch so that a failure in one does not prevent the other from
256
+ * succeeding. Empty arrays are returned for any failing SDK call.
257
+ *
258
+ * Results are **not** cached by this function – use
259
+ * {@link getLocalDiscovery} for caching support.
260
+ *
261
+ * @param ctx - The OpenCode plugin input context
262
+ * @returns A {@link LocalDiscoveryResult} with discovered MCPs and skills
263
+ */
264
+ export async function scanLocal(
265
+ ctx: PluginInput,
266
+ ): Promise<LocalDiscoveryResult> {
267
+ const start = performance.now();
268
+
269
+ let mcps: DiscoveredMcp[] = [];
270
+ let skills: DiscoveredSkill[] = [];
271
+
272
+ // Scan MCPs – failure is non-fatal, results default to empty
273
+ try {
274
+ mcps = await scanMcpStatuses(ctx);
275
+ } catch (err) {
276
+ log('[discovery] MCP scan failed', String(err));
277
+ }
278
+
279
+ // Scan skills – failure is non-fatal, results default to empty
280
+ try {
281
+ skills = await scanInstanceSkills(ctx);
282
+ } catch (err) {
283
+ log('[discovery] skill scan failed', String(err));
284
+ }
285
+
286
+ const scanDurationMs = performance.now() - start;
287
+
288
+ return {
289
+ skills,
290
+ mcps,
291
+ scannedAt: Date.now(),
292
+ scanDurationMs,
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Get locally discovered resources, using a cached result when available.
298
+ *
299
+ * Results are cached for 5 minutes. Call with `forceRefresh = true` to
300
+ * bypass the cache and perform a fresh scan. The cache lives at module
301
+ * scope and is shared across all callers within the same plugin instance.
302
+ *
303
+ * @param ctx - The OpenCode plugin input context
304
+ * @param forceRefresh - If `true`, bypass the cache and force a fresh scan
305
+ * @returns A {@link LocalDiscoveryResult}
306
+ */
307
+ export async function getLocalDiscovery(
308
+ ctx: PluginInput,
309
+ forceRefresh?: boolean,
310
+ ): Promise<LocalDiscoveryResult> {
311
+ if (
312
+ !forceRefresh &&
313
+ cache !== null &&
314
+ Date.now() - cache.timestamp < CACHE_TTL_MS
315
+ ) {
316
+ return cache.result;
317
+ }
318
+
319
+ const result = await scanLocal(ctx);
320
+ cache = { result, timestamp: Date.now() };
321
+ return result;
322
+ }