@jack200714/mafw 4.5.1 → 4.8.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 (330) hide show
  1. package/README.md +5 -2
  2. package/bin/mafw.js +16 -14
  3. package/gateway/dist/index.js +52 -38
  4. package/gateway/dist/media/media-plugin-loader.js +0 -12
  5. package/gateway/dist/plugins/hub.js +153 -19
  6. package/gateway/dist/routes/plugins.js +15 -5
  7. package/gateway/dist/runtime/event-broadcast.js +33 -0
  8. package/gateway/dist/runtime/loader.js +23 -13
  9. package/gateway/dist/usage/builtin-plugins/gateway.js +25 -3
  10. package/gateway/dist/usage/plugin-loader.js +0 -11
  11. package/gateway/package.json +2 -2
  12. package/package.json +1 -7
  13. package/packages/tui/dist/cli.js +8 -3
  14. package/.opencode/mafw/.backup-v61/parametric/base-skill-manifest.yaml +0 -2
  15. package/.opencode/mafw/STATUS.md +0 -6
  16. package/.opencode/mafw/ledger.md +0 -20
  17. package/.opencode/mafw/memory/.harmonic_index.json +0 -28
  18. package/.opencode/mafw/memory/.review_queue.json +0 -1
  19. package/.opencode/mafw/memory/tier3/parametric.json +0 -13
  20. package/.opencode/mafw/memory/tier3/test-goal.json +0 -19
  21. package/.opencode/mafw/memory-index.json +0 -6
  22. package/.opencode/mafw/parametric/base-skill-manifest.yaml +0 -2
  23. package/.opencode/mafw/waves.json +0 -4
  24. package/.opencode/opencode.json +0 -3
  25. package/.opencode/plugins/mafw-plugin.ts +0 -8
  26. package/.opencode/skills/banner-design/SKILL.md +0 -196
  27. package/.opencode/skills/banner-design/references/banner-sizes-and-styles.md +0 -118
  28. package/.opencode/skills/brand/SKILL.md +0 -97
  29. package/.opencode/skills/brand/references/approval-checklist.md +0 -169
  30. package/.opencode/skills/brand/references/asset-organization.md +0 -157
  31. package/.opencode/skills/brand/references/brand-guideline-template.md +0 -140
  32. package/.opencode/skills/brand/references/color-palette-management.md +0 -186
  33. package/.opencode/skills/brand/references/consistency-checklist.md +0 -94
  34. package/.opencode/skills/brand/references/logo-usage-rules.md +0 -185
  35. package/.opencode/skills/brand/references/messaging-framework.md +0 -85
  36. package/.opencode/skills/brand/references/typography-specifications.md +0 -214
  37. package/.opencode/skills/brand/references/update.md +0 -118
  38. package/.opencode/skills/brand/references/visual-identity.md +0 -96
  39. package/.opencode/skills/brand/references/voice-framework.md +0 -88
  40. package/.opencode/skills/brand/scripts/extract-colors.cjs +0 -341
  41. package/.opencode/skills/brand/scripts/inject-brand-context.cjs +0 -349
  42. package/.opencode/skills/brand/scripts/sync-brand-to-tokens.cjs +0 -248
  43. package/.opencode/skills/brand/scripts/tests/test_sync_brand_to_tokens.py +0 -52
  44. package/.opencode/skills/brand/scripts/validate-asset.cjs +0 -387
  45. package/.opencode/skills/brand/templates/brand-guidelines-starter.md +0 -275
  46. package/.opencode/skills/design/SKILL.md +0 -313
  47. package/.opencode/skills/design/data/cip/deliverables.csv +0 -51
  48. package/.opencode/skills/design/data/cip/industries.csv +0 -21
  49. package/.opencode/skills/design/data/cip/mockup-contexts.csv +0 -21
  50. package/.opencode/skills/design/data/cip/styles.csv +0 -21
  51. package/.opencode/skills/design/data/icon/styles.csv +0 -16
  52. package/.opencode/skills/design/data/logo/colors.csv +0 -56
  53. package/.opencode/skills/design/data/logo/industries.csv +0 -56
  54. package/.opencode/skills/design/data/logo/styles.csv +0 -56
  55. package/.opencode/skills/design/references/banner-sizes-and-styles.md +0 -118
  56. package/.opencode/skills/design/references/cip-deliverable-guide.md +0 -95
  57. package/.opencode/skills/design/references/cip-design.md +0 -121
  58. package/.opencode/skills/design/references/cip-prompt-engineering.md +0 -84
  59. package/.opencode/skills/design/references/cip-style-guide.md +0 -68
  60. package/.opencode/skills/design/references/design-routing.md +0 -207
  61. package/.opencode/skills/design/references/icon-design.md +0 -122
  62. package/.opencode/skills/design/references/logo-color-psychology.md +0 -101
  63. package/.opencode/skills/design/references/logo-design.md +0 -92
  64. package/.opencode/skills/design/references/logo-prompt-engineering.md +0 -158
  65. package/.opencode/skills/design/references/logo-style-guide.md +0 -109
  66. package/.opencode/skills/design/references/slides-copywriting-formulas.md +0 -84
  67. package/.opencode/skills/design/references/slides-create.md +0 -4
  68. package/.opencode/skills/design/references/slides-html-template.md +0 -295
  69. package/.opencode/skills/design/references/slides-layout-patterns.md +0 -137
  70. package/.opencode/skills/design/references/slides-strategies.md +0 -94
  71. package/.opencode/skills/design/references/slides.md +0 -42
  72. package/.opencode/skills/design/references/social-photos-design.md +0 -329
  73. package/.opencode/skills/design/scripts/cip/core.py +0 -215
  74. package/.opencode/skills/design/scripts/cip/generate.py +0 -484
  75. package/.opencode/skills/design/scripts/cip/render-html.py +0 -424
  76. package/.opencode/skills/design/scripts/cip/search.py +0 -127
  77. package/.opencode/skills/design/scripts/icon/generate.py +0 -487
  78. package/.opencode/skills/design/scripts/logo/core.py +0 -175
  79. package/.opencode/skills/design/scripts/logo/generate.py +0 -362
  80. package/.opencode/skills/design/scripts/logo/search.py +0 -114
  81. package/.opencode/skills/design-system/SKILL.md +0 -244
  82. package/.opencode/skills/design-system/data/slide-backgrounds.csv +0 -11
  83. package/.opencode/skills/design-system/data/slide-charts.csv +0 -26
  84. package/.opencode/skills/design-system/data/slide-color-logic.csv +0 -14
  85. package/.opencode/skills/design-system/data/slide-copy.csv +0 -26
  86. package/.opencode/skills/design-system/data/slide-layout-logic.csv +0 -16
  87. package/.opencode/skills/design-system/data/slide-layouts.csv +0 -26
  88. package/.opencode/skills/design-system/data/slide-strategies.csv +0 -16
  89. package/.opencode/skills/design-system/data/slide-typography.csv +0 -15
  90. package/.opencode/skills/design-system/references/component-specs.md +0 -236
  91. package/.opencode/skills/design-system/references/component-tokens.md +0 -214
  92. package/.opencode/skills/design-system/references/primitive-tokens.md +0 -203
  93. package/.opencode/skills/design-system/references/semantic-tokens.md +0 -215
  94. package/.opencode/skills/design-system/references/states-and-variants.md +0 -241
  95. package/.opencode/skills/design-system/references/tailwind-integration.md +0 -251
  96. package/.opencode/skills/design-system/references/token-architecture.md +0 -224
  97. package/.opencode/skills/design-system/scripts/embed-tokens.cjs +0 -99
  98. package/.opencode/skills/design-system/scripts/fetch-background.py +0 -317
  99. package/.opencode/skills/design-system/scripts/generate-slide.py +0 -770
  100. package/.opencode/skills/design-system/scripts/generate-tokens.cjs +0 -205
  101. package/.opencode/skills/design-system/scripts/html-token-validator.py +0 -327
  102. package/.opencode/skills/design-system/scripts/search-slides.py +0 -218
  103. package/.opencode/skills/design-system/scripts/slide-token-validator.py +0 -35
  104. package/.opencode/skills/design-system/scripts/slide_search_core.py +0 -453
  105. package/.opencode/skills/design-system/scripts/tests/test_validate_tokens.py +0 -48
  106. package/.opencode/skills/design-system/scripts/validate-tokens.cjs +0 -246
  107. package/.opencode/skills/design-system/templates/design-tokens-starter.json +0 -143
  108. package/.opencode/skills/frontend-design/LICENSE.txt +0 -21
  109. package/.opencode/skills/frontend-design/SKILL.md +0 -115
  110. package/.opencode/skills/mafw-automation-interview/SKILL.md +0 -43
  111. package/.opencode/skills/mafw-cli-scanner/SKILL.md +0 -37
  112. package/.opencode/skills/mafw-compression-verifier/SKILL.md +0 -52
  113. package/.opencode/skills/mafw-desktop-inspector/SKILL.md +0 -170
  114. package/.opencode/skills/mafw-execute/SKILL.md +0 -62
  115. package/.opencode/skills/mafw-gateway-restart/SKILL.md +0 -87
  116. package/.opencode/skills/mafw-github-scanner/SKILL.md +0 -38
  117. package/.opencode/skills/mafw-goal/SKILL.md +0 -76
  118. package/.opencode/skills/mafw-interview/SKILL.md +0 -68
  119. package/.opencode/skills/mafw-memory-extractor/SKILL.md +0 -65
  120. package/.opencode/skills/mafw-plan/SKILL.md +0 -58
  121. package/.opencode/skills/mafw-review/SKILL.md +0 -85
  122. package/.opencode/skills/runtime-plugin-authoring/SKILL.md +0 -791
  123. package/.opencode/skills/slides/SKILL.md +0 -40
  124. package/.opencode/skills/slides/references/copywriting-formulas.md +0 -84
  125. package/.opencode/skills/slides/references/create.md +0 -4
  126. package/.opencode/skills/slides/references/html-template.md +0 -295
  127. package/.opencode/skills/slides/references/layout-patterns.md +0 -137
  128. package/.opencode/skills/slides/references/slide-strategies.md +0 -94
  129. package/.opencode/skills/ui-styling/LICENSE.txt +0 -202
  130. package/.opencode/skills/ui-styling/SKILL.md +0 -324
  131. package/.opencode/skills/ui-styling/references/canvas-design-system.md +0 -320
  132. package/.opencode/skills/ui-styling/references/shadcn-accessibility.md +0 -471
  133. package/.opencode/skills/ui-styling/references/shadcn-components.md +0 -424
  134. package/.opencode/skills/ui-styling/references/shadcn-theming.md +0 -373
  135. package/.opencode/skills/ui-styling/references/tailwind-customization.md +0 -483
  136. package/.opencode/skills/ui-styling/references/tailwind-responsive.md +0 -382
  137. package/.opencode/skills/ui-styling/references/tailwind-utilities.md +0 -455
  138. package/.opencode/skills/ui-styling/scripts/requirements.txt +0 -17
  139. package/.opencode/skills/ui-styling/scripts/shadcn_add.py +0 -308
  140. package/.opencode/skills/ui-styling/scripts/tailwind_config_gen.py +0 -473
  141. package/.opencode/skills/ui-styling/scripts/tests/coverage-ui.json +0 -1
  142. package/.opencode/skills/ui-styling/scripts/tests/requirements.txt +0 -3
  143. package/.opencode/skills/ui-styling/scripts/tests/test_shadcn_add.py +0 -266
  144. package/.opencode/skills/ui-styling/scripts/tests/test_tailwind_config_gen.py +0 -394
  145. package/.opencode/skills/ui-ux-pro-max/SKILL.md +0 -388
  146. package/.opencode/skills/ui-ux-pro-max/data/app-interface.csv +0 -31
  147. package/.opencode/skills/ui-ux-pro-max/data/charts.csv +0 -26
  148. package/.opencode/skills/ui-ux-pro-max/data/colors.csv +0 -193
  149. package/.opencode/skills/ui-ux-pro-max/data/google-fonts.csv +0 -1924
  150. package/.opencode/skills/ui-ux-pro-max/data/icons.csv +0 -106
  151. package/.opencode/skills/ui-ux-pro-max/data/landing.csv +0 -35
  152. package/.opencode/skills/ui-ux-pro-max/data/motion.csv +0 -17
  153. package/.opencode/skills/ui-ux-pro-max/data/products.csv +0 -193
  154. package/.opencode/skills/ui-ux-pro-max/data/react-performance.csv +0 -45
  155. package/.opencode/skills/ui-ux-pro-max/data/stacks/angular.csv +0 -51
  156. package/.opencode/skills/ui-ux-pro-max/data/stacks/astro.csv +0 -54
  157. package/.opencode/skills/ui-ux-pro-max/data/stacks/avalonia.csv +0 -57
  158. package/.opencode/skills/ui-ux-pro-max/data/stacks/flutter.csv +0 -53
  159. package/.opencode/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +0 -56
  160. package/.opencode/skills/ui-ux-pro-max/data/stacks/javafx.csv +0 -76
  161. package/.opencode/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +0 -53
  162. package/.opencode/skills/ui-ux-pro-max/data/stacks/laravel.csv +0 -51
  163. package/.opencode/skills/ui-ux-pro-max/data/stacks/nextjs.csv +0 -53
  164. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +0 -71
  165. package/.opencode/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +0 -59
  166. package/.opencode/skills/ui-ux-pro-max/data/stacks/react-native.csv +0 -52
  167. package/.opencode/skills/ui-ux-pro-max/data/stacks/react.csv +0 -54
  168. package/.opencode/skills/ui-ux-pro-max/data/stacks/shadcn.csv +0 -61
  169. package/.opencode/skills/ui-ux-pro-max/data/stacks/svelte.csv +0 -54
  170. package/.opencode/skills/ui-ux-pro-max/data/stacks/swiftui.csv +0 -51
  171. package/.opencode/skills/ui-ux-pro-max/data/stacks/threejs.csv +0 -54
  172. package/.opencode/skills/ui-ux-pro-max/data/stacks/uno.csv +0 -60
  173. package/.opencode/skills/ui-ux-pro-max/data/stacks/uwp.csv +0 -56
  174. package/.opencode/skills/ui-ux-pro-max/data/stacks/vue.csv +0 -50
  175. package/.opencode/skills/ui-ux-pro-max/data/stacks/winui.csv +0 -60
  176. package/.opencode/skills/ui-ux-pro-max/data/stacks/wpf.csv +0 -57
  177. package/.opencode/skills/ui-ux-pro-max/data/styles.csv +0 -85
  178. package/.opencode/skills/ui-ux-pro-max/data/typography.csv +0 -75
  179. package/.opencode/skills/ui-ux-pro-max/data/ui-reasoning.csv +0 -162
  180. package/.opencode/skills/ui-ux-pro-max/data/ux-guidelines.csv +0 -100
  181. package/.opencode/skills/ui-ux-pro-max/scripts/core.py +0 -464
  182. package/.opencode/skills/ui-ux-pro-max/scripts/design_system.py +0 -1479
  183. package/.opencode/skills/ui-ux-pro-max/scripts/search.py +0 -162
  184. package/.opencode/skills/ui-ux-pro-max/scripts/tests/test_core.py +0 -134
  185. package/.opencode/skills/ui-ux-pro-max/scripts/tests/test_design_system_mode.py +0 -159
  186. package/.opencode/skills/ui-ux-pro-max/scripts/validate_data.py +0 -114
  187. package/dist/hooks/bash-python-guide.d.ts +0 -4
  188. package/dist/hooks/bash-python-guide.d.ts.map +0 -1
  189. package/dist/hooks/bash-python-guide.js +0 -67
  190. package/dist/hooks/bash-python-guide.js.map +0 -1
  191. package/dist/hooks/handoff.d.ts +0 -9
  192. package/dist/hooks/handoff.d.ts.map +0 -1
  193. package/dist/hooks/handoff.js +0 -8
  194. package/dist/hooks/handoff.js.map +0 -1
  195. package/dist/hooks/hook-manager.d.ts +0 -32
  196. package/dist/hooks/hook-manager.d.ts.map +0 -1
  197. package/dist/hooks/hook-manager.js +0 -91
  198. package/dist/hooks/hook-manager.js.map +0 -1
  199. package/dist/hooks/llm-after.d.ts +0 -8
  200. package/dist/hooks/llm-after.d.ts.map +0 -1
  201. package/dist/hooks/llm-after.js +0 -8
  202. package/dist/hooks/llm-after.js.map +0 -1
  203. package/dist/hooks/media-ingest.d.ts +0 -43
  204. package/dist/hooks/media-ingest.d.ts.map +0 -1
  205. package/dist/hooks/media-ingest.js +0 -269
  206. package/dist/hooks/media-ingest.js.map +0 -1
  207. package/dist/hooks/memory-guide.d.ts +0 -3
  208. package/dist/hooks/memory-guide.d.ts.map +0 -1
  209. package/dist/hooks/memory-guide.js +0 -47
  210. package/dist/hooks/memory-guide.js.map +0 -1
  211. package/dist/hooks/observation-capture.d.ts +0 -17
  212. package/dist/hooks/observation-capture.d.ts.map +0 -1
  213. package/dist/hooks/observation-capture.js +0 -20
  214. package/dist/hooks/observation-capture.js.map +0 -1
  215. package/dist/hooks/session-compacting.d.ts +0 -6
  216. package/dist/hooks/session-compacting.d.ts.map +0 -1
  217. package/dist/hooks/session-compacting.js +0 -6
  218. package/dist/hooks/session-compacting.js.map +0 -1
  219. package/dist/hooks/session-ending.d.ts +0 -20
  220. package/dist/hooks/session-ending.d.ts.map +0 -1
  221. package/dist/hooks/session-ending.js +0 -113
  222. package/dist/hooks/session-ending.js.map +0 -1
  223. package/dist/hooks/session-recall.d.ts +0 -2
  224. package/dist/hooks/session-recall.d.ts.map +0 -1
  225. package/dist/hooks/session-recall.js +0 -103
  226. package/dist/hooks/session-recall.js.map +0 -1
  227. package/dist/hooks/session-start.d.ts +0 -9
  228. package/dist/hooks/session-start.d.ts.map +0 -1
  229. package/dist/hooks/session-start.js +0 -42
  230. package/dist/hooks/session-start.js.map +0 -1
  231. package/dist/hooks/tool-before.d.ts +0 -10
  232. package/dist/hooks/tool-before.d.ts.map +0 -1
  233. package/dist/hooks/tool-before.js +0 -6
  234. package/dist/hooks/tool-before.js.map +0 -1
  235. package/dist/hooks/tool-executed.d.ts +0 -19
  236. package/dist/hooks/tool-executed.d.ts.map +0 -1
  237. package/dist/hooks/tool-executed.js +0 -12
  238. package/dist/hooks/tool-executed.js.map +0 -1
  239. package/dist/hooks/user-profile.d.ts +0 -2
  240. package/dist/hooks/user-profile.d.ts.map +0 -1
  241. package/dist/hooks/user-profile.js +0 -30
  242. package/dist/hooks/user-profile.js.map +0 -1
  243. package/dist/hooks/user-prompt.d.ts +0 -9
  244. package/dist/hooks/user-prompt.d.ts.map +0 -1
  245. package/dist/hooks/user-prompt.js +0 -8
  246. package/dist/hooks/user-prompt.js.map +0 -1
  247. package/dist/hooks/voice-guide.d.ts +0 -8
  248. package/dist/hooks/voice-guide.d.ts.map +0 -1
  249. package/dist/hooks/voice-guide.js +0 -101
  250. package/dist/hooks/voice-guide.js.map +0 -1
  251. package/dist/plugin.d.ts +0 -55
  252. package/dist/plugin.d.ts.map +0 -1
  253. package/dist/plugin.js +0 -342
  254. package/dist/plugin.js.map +0 -1
  255. package/dist/tools/add-memory.d.ts +0 -17
  256. package/dist/tools/add-memory.d.ts.map +0 -1
  257. package/dist/tools/add-memory.js +0 -60
  258. package/dist/tools/add-memory.js.map +0 -1
  259. package/dist/tools/media-ask.d.ts +0 -14
  260. package/dist/tools/media-ask.d.ts.map +0 -1
  261. package/dist/tools/media-ask.js +0 -224
  262. package/dist/tools/media-ask.js.map +0 -1
  263. package/dist/tools/media-speak.d.ts +0 -16
  264. package/dist/tools/media-speak.d.ts.map +0 -1
  265. package/dist/tools/media-speak.js +0 -78
  266. package/dist/tools/media-speak.js.map +0 -1
  267. package/dist/tools/media-upload.d.ts +0 -14
  268. package/dist/tools/media-upload.d.ts.map +0 -1
  269. package/dist/tools/media-upload.js +0 -155
  270. package/dist/tools/media-upload.js.map +0 -1
  271. package/dist/tools/python-exec.d.ts +0 -13
  272. package/dist/tools/python-exec.d.ts.map +0 -1
  273. package/dist/tools/python-exec.js +0 -99
  274. package/dist/tools/python-exec.js.map +0 -1
  275. package/dist/tools/python-restart.d.ts +0 -11
  276. package/dist/tools/python-restart.d.ts.map +0 -1
  277. package/dist/tools/python-restart.js +0 -33
  278. package/dist/tools/python-restart.js.map +0 -1
  279. package/dist/utils/circuit-breaker.d.ts +0 -23
  280. package/dist/utils/circuit-breaker.d.ts.map +0 -1
  281. package/dist/utils/circuit-breaker.js +0 -75
  282. package/dist/utils/circuit-breaker.js.map +0 -1
  283. package/dist/utils/config-loader.d.ts +0 -20
  284. package/dist/utils/config-loader.d.ts.map +0 -1
  285. package/dist/utils/config-loader.js +0 -194
  286. package/dist/utils/config-loader.js.map +0 -1
  287. package/dist/utils/fallback.d.ts +0 -7
  288. package/dist/utils/fallback.d.ts.map +0 -1
  289. package/dist/utils/fallback.js +0 -20
  290. package/dist/utils/fallback.js.map +0 -1
  291. package/dist/utils/git.d.ts +0 -24
  292. package/dist/utils/git.d.ts.map +0 -1
  293. package/dist/utils/git.js +0 -71
  294. package/dist/utils/git.js.map +0 -1
  295. package/dist/utils/github.d.ts +0 -37
  296. package/dist/utils/github.d.ts.map +0 -1
  297. package/dist/utils/github.js +0 -53
  298. package/dist/utils/github.js.map +0 -1
  299. package/dist/utils/global-path.d.ts +0 -2
  300. package/dist/utils/global-path.d.ts.map +0 -1
  301. package/dist/utils/global-path.js +0 -32
  302. package/dist/utils/global-path.js.map +0 -1
  303. package/dist/utils/logger.d.ts +0 -6
  304. package/dist/utils/logger.d.ts.map +0 -1
  305. package/dist/utils/logger.js +0 -69
  306. package/dist/utils/logger.js.map +0 -1
  307. package/dist/utils/obs-capture.d.ts +0 -7
  308. package/dist/utils/obs-capture.d.ts.map +0 -1
  309. package/dist/utils/obs-capture.js +0 -49
  310. package/dist/utils/obs-capture.js.map +0 -1
  311. package/dist/utils/retry.d.ts +0 -11
  312. package/dist/utils/retry.d.ts.map +0 -1
  313. package/dist/utils/retry.js +0 -45
  314. package/dist/utils/retry.js.map +0 -1
  315. package/dist/utils/self-wiring.d.ts +0 -13
  316. package/dist/utils/self-wiring.d.ts.map +0 -1
  317. package/dist/utils/self-wiring.js +0 -86
  318. package/dist/utils/self-wiring.js.map +0 -1
  319. package/dist/utils/state.d.ts +0 -106
  320. package/dist/utils/state.d.ts.map +0 -1
  321. package/dist/utils/state.js +0 -166
  322. package/dist/utils/state.js.map +0 -1
  323. package/dist/utils/status.d.ts +0 -47
  324. package/dist/utils/status.d.ts.map +0 -1
  325. package/dist/utils/status.js +0 -147
  326. package/dist/utils/status.js.map +0 -1
  327. package/dist/utils/ttl-map.d.ts +0 -17
  328. package/dist/utils/ttl-map.d.ts.map +0 -1
  329. package/dist/utils/ttl-map.js +0 -67
  330. package/dist/utils/ttl-map.js.map +0 -1
package/README.md CHANGED
@@ -146,14 +146,17 @@ npx electron-vite build
146
146
 
147
147
  ### 3. opencode 接入(可选)
148
148
 
149
+ > npm 包仅含 **Gateway + TUI**(`bin: mafw`)。opencode 插件入口(`dist/plugin.js`)不在
150
+ > npm 包内——插件方式接入请从源码构建(`npm run build` 后本地安装 tgz),或仅用下方
151
+ > MCP remote 接线(无需插件)。
152
+
149
153
  ```json
150
154
  {
151
- "plugin": ["@jack200714/mafw"],
152
155
  "mcp": { "mafw": { "type": "remote", "url": "http://127.0.0.1:3000/mcp", "enabled": true } }
153
156
  }
154
157
  ```
155
158
 
156
- 插件激活时自动补写缺失的 MCP 接线并备份原配置(fail-open)。
159
+ 源码构建后插件激活时会自动补写缺失的 MCP 接线并备份原配置(fail-open)。
157
160
 
158
161
  ## 仓库结构
159
162
 
package/bin/mafw.js CHANGED
@@ -125,8 +125,23 @@ function startGateway(background) {
125
125
  process.exit(1);
126
126
  }
127
127
 
128
+ // Background mode stdio: stdin/stdout 'ignore' (devnull), stderr redirected
129
+ // straight to a file fd. All three must avoid live pipes: a pipe's write end
130
+ // breaks once this short-lived CLI exits, and any later console.log in the
131
+ // gateway (e.g. mdns-advertiser) then crashes it with an unhandled EPIPE; a
132
+ // piped stderr would also keep THIS CLI's event loop alive and make
133
+ // `mafw daemon` hang. File fd = crash traces persist with zero plumbing.
134
+ let stdio = background ? ['ignore', 'ignore', 'ignore'] : 'inherit';
135
+ if (background) {
136
+ try {
137
+ const errLog = path.join(LOG_DIR, 'gateway-stderr.log');
138
+ try { if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true }); } catch {}
139
+ stdio = ['ignore', 'ignore', fs.openSync(errLog, 'a')];
140
+ } catch { /* fall back to full ignore */ }
141
+ }
142
+
128
143
  const child = spawn(process.execPath, [GATEWAY_SCRIPT], {
129
- stdio: background ? ['ignore', 'pipe', 'pipe'] : 'inherit',
144
+ stdio,
130
145
  detached: background,
131
146
  windowsHide: true,
132
147
  });
@@ -134,19 +149,6 @@ function startGateway(background) {
134
149
  writePid(child.pid);
135
150
 
136
151
  if (background) {
137
- // Diagnostics: capture stderr to a file (a detached gateway that crashes
138
- // silently otherwise leaves no trace). stdout is discarded; the gateway's
139
- // own file logging covers normal logs.
140
- // NOTE: use a raw fd + writeSync — a fs.WriteStream here never ends, which
141
- // keeps the parent's event loop alive so `mafw daemon` hangs (and any
142
- // tree-kill then takes the detached gateway down with it).
143
- const errLog = path.join(LOG_DIR, 'gateway-stderr.log');
144
- try { if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true }); } catch {}
145
- const errFd = fs.openSync(errLog, 'a');
146
- if (child.stderr) {
147
- child.stderr.on('data', (chunk) => { try { fs.writeSync(errFd, chunk); } catch {} });
148
- }
149
- if (child.stdout) child.stdout.destroy();
150
152
  child.unref();
151
153
  console.log(`Gateway started in background (PID: ${child.pid})`);
152
154
  console.log(`Logs: ${LOG_FILE}`);
@@ -99,6 +99,7 @@ const self_update_1 = require("./self-update");
99
99
  const step_inject_1 = require("./recall/step-inject");
100
100
  const inject_format_1 = require("./recall/inject-format");
101
101
  const normalize_1 = require("./runtime/normalize");
102
+ const event_broadcast_1 = require("./runtime/event-broadcast");
102
103
  const budget_guard_1 = require("./core/budget-guard");
103
104
  const goal_budget_1 = require("./core/goal-budget");
104
105
  const contract_1 = require("./runtime/contract");
@@ -107,6 +108,7 @@ const pi_runtime_1 = require("./runtime/plugins/pi-runtime");
107
108
  const permission_1 = require("./routes/permission");
108
109
  const runtime_switch_1 = require("./routes/runtime-switch");
109
110
  const plugins_1 = require("./routes/plugins");
111
+ const hub_1 = require("./plugins/hub");
110
112
  const restart_agent_1 = require("./routes/restart-agent");
111
113
  const session_mutations_1 = require("./routes/session-mutations");
112
114
  const serve_supervisor_1 = require("./runtime/serve-supervisor");
@@ -157,6 +159,10 @@ class MafwScheduler {
157
159
  serveOwned = false;
158
160
  serveExitStreak = 0;
159
161
  serveRecovering = false;
162
+ // External runtimes (pi) do not own serve: the watchdog can only warn and
163
+ // reconnect the event stream. Flag keeps that warning a one-shot ERROR per
164
+ // outage instead of an unbounded WARN loop.
165
+ serveExternalDownNotified = false;
160
166
  serveWatchdogTimer = null;
161
167
  serveRetryTimer = null;
162
168
  serveStableTimer = null;
@@ -206,8 +212,6 @@ class MafwScheduler {
206
212
  registeredProjects = new Map();
207
213
  registryPath;
208
214
  registryWriteQueue = Promise.resolve();
209
- configPath;
210
- configWriteQueue = Promise.resolve();
211
215
  running = true;
212
216
  // private dashboard?: DashboardServer;
213
217
  mcpEndpoint;
@@ -252,7 +256,6 @@ class MafwScheduler {
252
256
  this.serveUrl = config_1.config.server.serveUrl;
253
257
  this.apiPort = config_1.config.server.apiPort;
254
258
  this.pollInterval = config_1.config.timeouts.backupPollInterval;
255
- this.configPath = config_1.config.paths.globalConfig;
256
259
  this.registryPath = config_1.config.paths.registryFile;
257
260
  this.chatSessions = new chat_sessions_1.ChatSessionManager();
258
261
  this.serveSupervisor = (0, serve_supervisor_1.createServeSupervisor)({
@@ -423,8 +426,7 @@ class MafwScheduler {
423
426
  // 4. Dashboard is now served via the API server on the same port
424
427
  // this.dashboard = new DashboardServer(3001, this.projectDir, this);
425
428
  // this.dashboard.start();
426
- // 5. 恢复配置和注册表
427
- await this.recoverConfig();
429
+ // 5. 恢复注册表(权威源:gateway DB kv_store + legacy 文件兜底)
428
430
  await this.recoverRegistry();
429
431
  // 5.0 Data-directory migration: move memory store + pipeline files from
430
432
  // the previously-fixed gateway package .mafw (and any project-relative
@@ -773,7 +775,8 @@ class MafwScheduler {
773
775
  registeredAt: new Date().toISOString()
774
776
  });
775
777
  this.persistRegistry();
776
- this.persistConfig();
778
+ // 桌面 renderer 依此事件刷新 Rail 项目列表(否则只在 gateway ready 时拉一次)。
779
+ this.broadcast((0, event_broadcast_1.projectRegisteredEvent)(projectDir));
777
780
  logger_1.log.info(`[Scheduler] Project registered via filesystem: ${projectDir}`);
778
781
  }
779
782
  catch {
@@ -952,13 +955,15 @@ class MafwScheduler {
952
955
  catch (err) {
953
956
  logger_1.log.warn(`[Trajectory] idle aggregation failed (non-fatal): ${err.message}`);
954
957
  }
955
- this.broadcast({ type: 'opencode_event', data: { type: 'message.complete', sessionID, ...(memoryWorker ? { internal: true } : {}) } });
958
+ this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type: 'message.complete', sessionID }, memoryWorker));
956
959
  }
957
960
  else if (f.broadcast === 'error') {
958
- this.broadcast({ type: 'opencode_event', data: { type: 'message.error', sessionID, error: props?.error instanceof Error ? props.error.message : String(props?.error ?? 'Unknown error'), ...(memoryWorker ? { internal: true } : {}) } });
961
+ this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type: 'message.error', sessionID, error: props?.error instanceof Error ? props.error.message : String(props?.error ?? 'Unknown error') }, memoryWorker));
959
962
  }
960
963
  else {
961
- this.broadcast({ type: 'opencode_event', data: { type, properties: props, sessionID, ...(memoryWorker ? { internal: true } : {}) } });
964
+ // directory 透传:session.created/updated/deleted 的消费方(桌面 Rail)
965
+ // 据此定位所属项目做定向刷新(normalize 已从 GlobalEvent 信封提取)。
966
+ this.broadcast((0, event_broadcast_1.opencodeBroadcast)({ type, properties: props, sessionID, directory: f.directory }, memoryWorker));
962
967
  }
963
968
  }
964
969
  /** 能力门:runtime 未声明该能力时以 503 显式拒绝(fail-open 的声明式降级)。 */
@@ -2122,6 +2127,10 @@ class MafwScheduler {
2122
2127
  if (!this.running || this.serveRecovering)
2123
2128
  return;
2124
2129
  if (await this.isServeHealthy()) {
2130
+ if (failures > 0 || this.serveExternalDownNotified) {
2131
+ logger_1.log.info('[Scheduler] Serve back online');
2132
+ }
2133
+ this.serveExternalDownNotified = false;
2125
2134
  failures = 0;
2126
2135
  return;
2127
2136
  }
@@ -2130,7 +2139,10 @@ class MafwScheduler {
2130
2139
  if (failures >= this.serveWatchdogFailures) {
2131
2140
  failures = 0;
2132
2141
  if (this.opencodeClient?.external) {
2133
- logger_1.log.warn('[Scheduler] External serve unreachable; reconnecting event stream only (not killing external process)');
2142
+ if (!this.serveExternalDownNotified) {
2143
+ this.serveExternalDownNotified = true;
2144
+ logger_1.log.error('[Scheduler] External serve unreachable — the active runtime does not own the serve process, so it will NOT be respawned. Serve-dependent features (provider list, sessions, approvals) are degraded.');
2145
+ }
2134
2146
  try {
2135
2147
  await this.subscribeToEvents();
2136
2148
  }
@@ -3600,7 +3612,8 @@ class MafwScheduler {
3600
3612
  });
3601
3613
  // 持久化到磁盘(写队列防并发覆盖)
3602
3614
  await this.persistRegistry();
3603
- await this.persistConfig();
3615
+ // 桌面 renderer 依此事件刷新 Rail 项目列表(否则只在 gateway ready 时拉一次)。
3616
+ this.broadcast((0, event_broadcast_1.projectRegisteredEvent)(projectDir));
3604
3617
  if (this.opencodeClient) {
3605
3618
  try {
3606
3619
  await this.ensureManagerSession(projectDir, mafwDir);
@@ -4089,6 +4102,9 @@ class MafwScheduler {
4089
4102
  if (this.automationEngine)
4090
4103
  this.automationEngine.setRuntimeClient(rt);
4091
4104
  await this.resubscribeEvents(`runtime switched to '${rt.name}'`);
4105
+ // Desktop hint: a runtime switch swaps the session storage backend
4106
+ // (opencode SQLite vs pi), so cached session lists are stale.
4107
+ this.broadcast({ type: 'runtime_switched', runtime: rt.name, previous: prev?.name ?? null });
4092
4108
  if (prev && prev.dispose) {
4093
4109
  try {
4094
4110
  await prev.dispose();
@@ -4144,6 +4160,21 @@ class MafwScheduler {
4144
4160
  usage: config_1.config.resolvePath('usage-plugins'),
4145
4161
  ui: process.env.MAFW_UI_PLUGINS_DIR || path.join(os.homedir(), '.mafw', 'ui-plugins'),
4146
4162
  },
4163
+ builtinEntries: () => {
4164
+ const entries = [];
4165
+ const rt = (name) => ({ type: 'runtime', name, file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
4166
+ entries.push(rt('opencode'));
4167
+ for (const name of this.runtimeLoader?.getBuiltinNames?.() ?? [])
4168
+ entries.push(rt(name));
4169
+ entries.push({ type: 'media', name: 'pi', file: '(builtin)', status: 'enabled', size: 0, mtime: '' });
4170
+ const usageState = this.pluginLoader?.getState?.() ?? [];
4171
+ for (const s of usageState) {
4172
+ if (s.builtin && s.status === 'ok' && s.name) {
4173
+ entries.push({ type: 'usage', name: s.name, file: s.file, status: 'enabled', size: 0, mtime: '', pluginType: s.pluginType });
4174
+ }
4175
+ }
4176
+ return entries;
4177
+ },
4147
4178
  getErrors: (type) => {
4148
4179
  const stateOf = (loader) => (loader && typeof loader.getState === 'function' ? loader.getState() : []);
4149
4180
  const source = type === 'runtime' ? this.runtimeLoader : type === 'media' ? this.mediaPluginLoader : type === 'usage' ? this.pluginLoader : null;
@@ -4166,6 +4197,16 @@ class MafwScheduler {
4166
4197
  },
4167
4198
  },
4168
4199
  };
4200
+ try {
4201
+ const cleaned = (0, hub_1.cleanupExamples)(pluginHubDeps.hub);
4202
+ if (cleaned.removed.length)
4203
+ logger_1.log.info(`[PluginsHub] removed stale examples: ${cleaned.removed.length}`);
4204
+ if (cleaned.failed.length)
4205
+ logger_1.log.warn(`[PluginsHub] cleanupExamples failed: ${cleaned.failed.join(', ')}`);
4206
+ }
4207
+ catch (err) {
4208
+ logger_1.log.warn(`[PluginsHub] cleanupExamples error: ${err.message}`);
4209
+ }
4169
4210
  if (req.method === 'GET' && req.url?.match(/^\/api\/plugins(?:\?|$)/)) {
4170
4211
  await (0, plugins_1.handlePluginsList)(req, res, pluginHubDeps);
4171
4212
  return;
@@ -5433,33 +5474,6 @@ class MafwScheduler {
5433
5474
  logger_1.log.info(`[Scheduler] Recovered ${filtered.length} registered projects`);
5434
5475
  }
5435
5476
  }
5436
- async persistConfig() {
5437
- this.configWriteQueue = this.configWriteQueue.then(async () => {
5438
- const dir = path.dirname(this.configPath);
5439
- if (!fs.existsSync(dir))
5440
- fs.mkdirSync(dir, { recursive: true });
5441
- let config = {};
5442
- if (fs.existsSync(this.configPath)) {
5443
- config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
5444
- }
5445
- config.projects = Object.fromEntries(this.registeredProjects);
5446
- fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2));
5447
- });
5448
- await this.configWriteQueue;
5449
- }
5450
- async recoverConfig() {
5451
- if (fs.existsSync(this.configPath)) {
5452
- try {
5453
- const config = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
5454
- const entries = Object.entries((config.projects || {}))
5455
- .filter(([dir]) => !this.isUserDataDir(dir));
5456
- this.registeredProjects = new Map(entries);
5457
- }
5458
- catch (err) {
5459
- logger_1.log.error(`[Scheduler] Failed to recover config: ${err.message}`);
5460
- }
5461
- }
5462
- }
5463
5477
  // ── 4. 轮询(降级兜底 + autoresume) ──
5464
5478
  startBackupPolling() {
5465
5479
  const interval = config_1.config.timeouts.backupPollInterval;
@@ -60,7 +60,6 @@ class MediaPluginLoader {
60
60
  if (!fs.existsSync(this.pluginsDir)) {
61
61
  fs.mkdirSync(this.pluginsDir, { recursive: true });
62
62
  fs.writeFileSync(path.join(this.pluginsDir, 'README.md'), README_CONTENT);
63
- fs.writeFileSync(path.join(this.pluginsDir, 'example.js.disabled'), EXAMPLE_CONTENT);
64
63
  logger_1.log.info(`[MediaPluginLoader] Created ${this.pluginsDir}`);
65
64
  }
66
65
  }
@@ -241,14 +240,3 @@ media:
241
240
  engine: qwen-vl # override for video
242
241
  \`\`\`
243
242
  `;
244
- const EXAMPLE_CONTENT = `// Rename to example.js to activate
245
- module.exports = {
246
- name: "example",
247
- modalities: ["image"],
248
- async createPrompt(ctx) {
249
- return async (parts, opts) => {
250
- return "Example analysis result";
251
- };
252
- },
253
- };
254
- `;
@@ -36,11 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.HubError = void 0;
37
37
  exports.parseType = parseType;
38
38
  exports.listPlugins = listPlugins;
39
+ exports.cleanupExamples = cleanupExamples;
39
40
  exports.installPlugin = installPlugin;
40
41
  exports.setPluginEnabled = setPluginEnabled;
41
42
  exports.deletePlugin = deletePlugin;
42
43
  const fs = __importStar(require("fs"));
44
+ const os = __importStar(require("os"));
43
45
  const path = __importStar(require("path"));
46
+ const logger_1 = require("../core/utils/logger");
44
47
  class HubError extends Error {
45
48
  status;
46
49
  constructor(status, message) {
@@ -87,6 +90,19 @@ function statEntry(type, dir, file) {
87
90
  };
88
91
  }
89
92
  function listPlugins(deps) {
93
+ const userEntries = collectUserEntries(deps);
94
+ const builtins = (deps.builtinEntries?.() ?? []).map((e) => ({ ...e, builtin: true }));
95
+ const userKeys = new Set(userEntries.map((e) => `${e.type}/${e.name}`));
96
+ const configDisabled = deps.configDisabledUsage?.() ?? new Set();
97
+ for (const b of builtins) {
98
+ if (userKeys.has(`${b.type}/${b.name}`))
99
+ b.overridden = true;
100
+ if (b.type === 'usage' && configDisabled.has(b.name) && b.status === 'enabled')
101
+ b.status = 'config-disabled';
102
+ }
103
+ return [...builtins, ...userEntries];
104
+ }
105
+ function collectUserEntries(deps) {
90
106
  const entries = [];
91
107
  for (const type of PLUGIN_TYPES) {
92
108
  const dir = deps.dirs[type];
@@ -130,24 +146,52 @@ function listPlugins(deps) {
130
146
  }
131
147
  return entries;
132
148
  }
133
- function decodeContent(contentBase64, maxBytes) {
134
- if (typeof contentBase64 !== 'string' || contentBase64.length === 0)
135
- throw new HubError(400, 'empty content');
136
- const buf = Buffer.from(contentBase64, 'base64');
137
- if (buf.length === 0)
138
- throw new HubError(400, 'empty content');
139
- // round-trip check rejects non-base64 garbage
140
- if (buf.toString('base64') !== contentBase64)
141
- throw new HubError(400, 'content is not valid base64');
142
- if (buf.length > maxBytes)
143
- throw new HubError(413, `content exceeds ${maxBytes} bytes`);
144
- return buf;
149
+ function cleanupExamples(deps) {
150
+ const removed = [];
151
+ const failed = [];
152
+ for (const dir of Object.values(deps.dirs)) {
153
+ const target = path.join(dir, 'example.js.disabled');
154
+ try {
155
+ if (fs.existsSync(target)) {
156
+ fs.unlinkSync(target);
157
+ removed.push(target);
158
+ }
159
+ }
160
+ catch {
161
+ failed.push(target);
162
+ }
163
+ }
164
+ return { removed, failed };
145
165
  }
146
166
  async function installPlugin(deps, input) {
147
- const type = parseType(input.type);
148
167
  const filename = validateFilename(input.filename);
149
168
  if (!filename.endsWith('.js'))
150
169
  throw new HubError(400, `install filename must end with .js: ${filename}`);
170
+ const maxBytes = deps.maxBytes ?? DEFAULT_MAX_BYTES;
171
+ if (!Buffer.isBuffer(input.bytes) || input.bytes.length === 0)
172
+ throw new HubError(400, 'empty content');
173
+ if (input.bytes.length > maxBytes)
174
+ throw new HubError(413, `content exceeds ${maxBytes} bytes`);
175
+ const explicitType = input.type ? parseType(input.type) : undefined;
176
+ const { matches, mod } = inspectPlugin(filename, input.bytes);
177
+ validateName(mod, filename);
178
+ let type;
179
+ if (matches.length === 0)
180
+ throw new HubError(400, 'unrecognized plugin interface: export createRuntime / createPrompt / fetch / tools');
181
+ if (explicitType) {
182
+ if (!matches.includes(explicitType)) {
183
+ const listing = matches.map((m) => `${m} (${MATCH_IFACE[m]})`).join('/');
184
+ throw new HubError(400, `plugin interface mismatch: selected ${explicitType}, exports ${listing}`);
185
+ }
186
+ type = explicitType;
187
+ }
188
+ else {
189
+ if (matches.length > 1)
190
+ throw new HubError(400, `ambiguous plugin interface: ${matches.join('/')}`);
191
+ type = matches[0];
192
+ }
193
+ if (type === 'media')
194
+ await validateMediaActivation(mod);
151
195
  const dir = deps.dirs[type];
152
196
  fs.mkdirSync(dir, { recursive: true });
153
197
  const target = resolveInDir(dir, filename);
@@ -156,23 +200,113 @@ async function installPlugin(deps, input) {
156
200
  if (!input.overwrite && (fs.existsSync(dup) || fs.existsSync(dupDisabled))) {
157
201
  throw new HubError(409, `plugin already exists: ${filename}`);
158
202
  }
159
- const buf = decodeContent(input.contentBase64, deps.maxBytes ?? DEFAULT_MAX_BYTES);
160
203
  const tmp = `${target}.tmp`;
161
- fs.writeFileSync(tmp, buf);
204
+ fs.writeFileSync(tmp, input.bytes);
162
205
  fs.renameSync(tmp, target);
163
206
  await deps.reload?.(type);
164
207
  return statEntry(type, dir, filename);
165
208
  }
209
+ const MATCH_IFACE = {
210
+ runtime: 'createRuntime',
211
+ media: 'createPrompt',
212
+ usage: 'fetch',
213
+ ui: 'tools',
214
+ };
215
+ function computeMatches(mod) {
216
+ const matches = [];
217
+ if (typeof mod?.createRuntime === 'function')
218
+ matches.push('runtime');
219
+ if (typeof mod?.createPrompt === 'function' || typeof mod?.fixPayload === 'function'
220
+ || typeof mod?.engine === 'string' || Array.isArray(mod?.modalities))
221
+ matches.push('media');
222
+ if (typeof mod?.fetch === 'function'
223
+ && (mod.type === undefined || mod.type === 'api' || mod.type === 'token-plan' || mod.type === 'local'))
224
+ matches.push('usage');
225
+ if (mod?.tools && typeof mod.tools === 'object' && Object.keys(mod.tools).length > 0)
226
+ matches.push('ui');
227
+ return matches;
228
+ }
229
+ function inspectPlugin(filename, bytes) {
230
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mafw-hub-sniff-'));
231
+ const tmpFile = path.join(tmpDir, filename);
232
+ try {
233
+ fs.writeFileSync(tmpFile, bytes);
234
+ try {
235
+ delete require.cache[require.resolve(tmpFile)];
236
+ }
237
+ catch { /* first load */ }
238
+ let mod;
239
+ try {
240
+ mod = require(tmpFile);
241
+ }
242
+ catch (err) {
243
+ throw new HubError(400, `plugin failed to load: ${err.message}`);
244
+ }
245
+ return { matches: computeMatches(mod), mod };
246
+ }
247
+ finally {
248
+ try {
249
+ fs.rmSync(tmpDir, { recursive: true, force: true });
250
+ }
251
+ catch { /* best effort */ }
252
+ }
253
+ }
254
+ function validateName(mod, filename) {
255
+ const name = mod?.name;
256
+ if (typeof name !== 'string' || name.length === 0)
257
+ throw new HubError(400, 'missing plugin name');
258
+ const stem = baseName(filename);
259
+ if (name !== stem) {
260
+ throw new HubError(400, `plugin name mismatch: exports '${name}', filename '${filename}' (must match)`);
261
+ }
262
+ }
263
+ const VALID_MODALITIES = new Set(['image', 'video', 'audio']);
264
+ const STUB_MEDIA_CTX = {
265
+ apiKey: () => null,
266
+ fetch: (url, opts) => fetch(url, opts),
267
+ pluginConfig: () => null,
268
+ log: logger_1.log,
269
+ };
270
+ async function validateMediaActivation(mod) {
271
+ const modalities = mod?.modalities;
272
+ if (!Array.isArray(modalities) || modalities.length === 0
273
+ || !modalities.every((m) => VALID_MODALITIES.has(m))) {
274
+ throw new HubError(400, 'invalid modalities');
275
+ }
276
+ const hasCreatePrompt = typeof mod?.createPrompt === 'function';
277
+ const hasPiEngine = mod?.engine === 'pi';
278
+ if (!hasCreatePrompt && !hasPiEngine)
279
+ throw new HubError(400, 'missing createPrompt() or engine:"pi"');
280
+ if (hasCreatePrompt && hasPiEngine)
281
+ throw new HubError(400, 'createPrompt and engine:"pi" are mutually exclusive');
282
+ if (hasCreatePrompt) {
283
+ const fn = await mod.createPrompt(STUB_MEDIA_CTX);
284
+ if (typeof fn !== 'function')
285
+ throw new HubError(400, 'createPrompt did not return a function');
286
+ }
287
+ }
166
288
  async function setPluginEnabled(deps, input) {
167
289
  const type = parseType(input.type);
168
290
  const filename = validateFilename(input.filename);
169
291
  const dir = deps.dirs[type];
170
- const current = resolveInDir(dir, filename);
171
- if (!fs.existsSync(current))
292
+ // Accept either on-disk state: a stale client snapshot (double-toggled
293
+ // switch, unrefreshed list) sends the opposite-state filename, which must
294
+ // resolve idempotently instead of 404ing ("激活 not found").
295
+ const primary = resolveInDir(dir, filename);
296
+ const variant = /\.js\.disabled$/.test(filename)
297
+ ? filename.replace(/\.js\.disabled$/, '.js')
298
+ : filename.replace(/\.js$/, '.js.disabled');
299
+ const variantPath = resolveInDir(dir, variant);
300
+ const current = fs.existsSync(primary) ? primary : fs.existsSync(variantPath) ? variantPath : null;
301
+ if (!current)
172
302
  throw new HubError(404, `plugin file not found: ${filename}`);
173
- const nextName = input.enabled ? filename.replace(/\.js\.disabled$/, '.js') : filename.replace(/\.js$/, '.js.disabled');
303
+ const currentName = path.basename(current);
304
+ const nextName = input.enabled
305
+ ? currentName.replace(/\.js\.disabled$/, '.js')
306
+ : currentName.replace(/\.js$/, '.js.disabled');
174
307
  const next = resolveInDir(dir, nextName);
175
- fs.renameSync(current, next);
308
+ if (next !== current)
309
+ fs.renameSync(current, next);
176
310
  await deps.reload?.(type);
177
311
  return statEntry(type, dir, nextName);
178
312
  }
@@ -15,6 +15,14 @@ function readBody(req) {
15
15
  req.on('error', reject);
16
16
  });
17
17
  }
18
+ function readBodyBuffer(req) {
19
+ return new Promise((resolve, reject) => {
20
+ const chunks = [];
21
+ req.on('data', (c) => chunks.push(c));
22
+ req.on('end', () => resolve(Buffer.concat(chunks)));
23
+ req.on('error', reject);
24
+ });
25
+ }
18
26
  async function readJsonBody(req) {
19
27
  const raw = await readBody(req);
20
28
  try {
@@ -47,11 +55,13 @@ async function handlePluginsList(_req, res, deps) {
47
55
  }
48
56
  async function handlePluginsInstall(req, res, deps) {
49
57
  await guarded(res, async () => {
50
- const body = await readJsonBody(req);
51
- const entry = await (0, hub_1.installPlugin)(deps.hub, {
52
- type: body.type, filename: body.filename, contentBase64: body.contentBase64, overwrite: !!body.overwrite,
53
- });
54
- logger_1.log.info(`[PluginsHub] installed ${body.type}/${body.filename}`);
58
+ const url = new URL(req.url || '/', 'http://localhost');
59
+ const filename = url.searchParams.get('filename') || '';
60
+ const type = (url.searchParams.get('type') || undefined);
61
+ const overwrite = url.searchParams.get('overwrite') === '1' || url.searchParams.get('overwrite') === 'true';
62
+ const bytes = await readBodyBuffer(req);
63
+ const entry = await (0, hub_1.installPlugin)(deps.hub, { filename, type, bytes, overwrite });
64
+ logger_1.log.info(`[PluginsHub] installed ${entry.type}/${filename}`);
55
65
  return entry;
56
66
  });
57
67
  }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Mode A 全局广播信封构造器(MafwScheduler.broadcast 的载荷形状)。
4
+ *
5
+ * wire 契约(桌面 renderer / TUI 均按此解析):
6
+ * { type: 'opencode_event', data: { type, properties?, sessionID?, directory?, error?, internal? } }
7
+ *
8
+ * 规则:可选字段缺失时不得出现在 data 上(key 稳定性,消费方用
9
+ * `'x' in data` 判定时不受缺省值污染)。
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.opencodeBroadcast = opencodeBroadcast;
13
+ exports.projectRegisteredEvent = projectRegisteredEvent;
14
+ function opencodeBroadcast(data, internal) {
15
+ return {
16
+ type: 'opencode_event',
17
+ data: {
18
+ ...data,
19
+ ...(data.directory === undefined ? {} : { directory: data.directory }),
20
+ ...(internal ? { internal: true } : {}),
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * 项目注册广播(桌面 renderer 依此刷新 Rail 项目列表)。
26
+ *
27
+ * 顶层广播必须扁平(无 `data` 键):desktop 剥壳逻辑 `event = raw?.data || raw`
28
+ * 会把 `data` 当作 opencode_event 的内层载荷,`type` 随之丢失——与
29
+ * runtime_switched / user_question 同一约定。
30
+ */
31
+ function projectRegisteredEvent(projectDir) {
32
+ return { type: 'project_registered', projectDir };
33
+ }
@@ -69,7 +69,6 @@ class RuntimePluginLoader {
69
69
  if (!fs.existsSync(this.pluginsDir)) {
70
70
  fs.mkdirSync(this.pluginsDir, { recursive: true });
71
71
  fs.writeFileSync(path.join(this.pluginsDir, 'README.md'), README_CONTENT);
72
- fs.writeFileSync(path.join(this.pluginsDir, 'example.js.disabled'), EXAMPLE_CONTENT);
73
72
  logger_1.log.info(`[RuntimePluginLoader] Created ${this.pluginsDir}`);
74
73
  }
75
74
  }
@@ -90,6 +89,10 @@ class RuntimePluginLoader {
90
89
  this.factories.delete(prev.name);
91
90
  this.meta.delete(prev.name);
92
91
  }
92
+ if (prev?.alias) {
93
+ this.factories.delete(prev.alias);
94
+ this.meta.delete(prev.alias);
95
+ }
93
96
  this.state.delete(file);
94
97
  }
95
98
  }
@@ -123,9 +126,22 @@ class RuntimePluginLoader {
123
126
  const external = mod.external !== false;
124
127
  this.factories.set(name, mod.createRuntime);
125
128
  this.meta.set(name, { capabilities, external });
126
- this.state.set(file, { file, name, status: 'ok', capabilities });
127
129
  loadedNames.add(name);
128
- logger_1.log.info(`[RuntimePluginLoader] Loaded ${file} (${name})`);
130
+ // Filename-stem alias: the plugin hub addresses runtime plugins by
131
+ // filename stem (statEntry baseName) while switch/createRuntime
132
+ // validate by module.exports.name — register both so hub "激活" works
133
+ // regardless of the declared name. The real module name always wins
134
+ // (unconditional set above overwrites an earlier stem alias), and an
135
+ // alias never shadows a builtin.
136
+ const stem = file.replace(/\.js$/, '');
137
+ let alias;
138
+ if (stem !== name && !this.factories.has(stem) && !this.builtins.has(stem)) {
139
+ this.factories.set(stem, mod.createRuntime);
140
+ this.meta.set(stem, { capabilities, external });
141
+ alias = stem;
142
+ }
143
+ this.state.set(file, { file, name, status: 'ok', capabilities, alias });
144
+ logger_1.log.info(`[RuntimePluginLoader] Loaded ${file} (${name})${alias ? ` [alias: ${alias}]` : ''}`);
129
145
  }
130
146
  catch (err) {
131
147
  const prev = this.state.get(file);
@@ -144,6 +160,10 @@ class RuntimePluginLoader {
144
160
  return { createRuntime: builtin.factory, ...builtin };
145
161
  return undefined;
146
162
  }
163
+ /** 已注册内置件名(builtin 文件件不在其中,opencode 为恒等默认由 index.ts 注入 hub)。 */
164
+ getBuiltinNames() {
165
+ return [...this.builtins.keys()];
166
+ }
147
167
  getState() {
148
168
  for (const [name, b] of this.builtins) {
149
169
  this.state.set(`builtin:${name}`, { file: `builtin:${name}`, name, status: 'ok', capabilities: b.capabilities });
@@ -228,13 +248,3 @@ Capabilities declared here gate gateway features declaratively: missing
228
248
  capabilities disable the corresponding features (503 on gated endpoints,
229
249
  skipped event subscription) — they never crash.
230
250
  `;
231
- const EXAMPLE_CONTENT = `// Rename to example.js to activate
232
- module.exports = {
233
- name: "example",
234
- capabilities: { eventStream: false },
235
- async createRuntime(ctx) {
236
- ctx.log.info("[example-runtime] created");
237
- throw new Error("example plugin: implement createRuntime before activating");
238
- },
239
- };
240
- `;