oh-my-agent-web 0.3.0 → 0.5.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 (279) hide show
  1. package/README.md +108 -450
  2. package/dist/server/approval/read-only.js +107 -0
  3. package/dist/server/approval/rules.js +85 -0
  4. package/dist/server/approval/scope.js +150 -0
  5. package/dist/server/brand.js +2 -0
  6. package/dist/server/cli-config.js +52 -0
  7. package/dist/server/cli-info.js +101 -0
  8. package/dist/server/cli.js +304 -0
  9. package/dist/server/engine/builtin.js +84 -0
  10. package/dist/server/engine/catalog.js +8 -0
  11. package/dist/server/engine/codex-approvals.js +45 -0
  12. package/dist/server/engine/codex-map.js +298 -0
  13. package/dist/server/engine/codex-sessions.js +159 -0
  14. package/dist/server/engine/codex-transport.js +49 -0
  15. package/dist/server/engine/codex.js +314 -0
  16. package/dist/server/engine/dsh-history.js +195 -0
  17. package/dist/server/engine/dsh-log.js +245 -0
  18. package/dist/server/engine/dsh-map.js +221 -0
  19. package/dist/server/engine/dsh-sessions.js +207 -0
  20. package/dist/server/engine/dsh-transport.js +37 -0
  21. package/dist/server/engine/dsh.js +284 -0
  22. package/dist/server/engine/fake.js +231 -0
  23. package/dist/server/engine/jsonrpc-stdio.js +241 -0
  24. package/dist/server/engine/model-registry-reload.js +115 -0
  25. package/dist/server/engine/narrate-guidance.js +22 -0
  26. package/dist/server/engine/opencode-map.js +499 -0
  27. package/dist/server/engine/opencode-projects.js +61 -0
  28. package/dist/server/engine/opencode-sessions.js +195 -0
  29. package/dist/server/engine/opencode-transport.js +48 -0
  30. package/dist/server/engine/opencode.js +394 -0
  31. package/dist/server/engine/pi-events.js +148 -0
  32. package/dist/server/engine/pi-plugins.js +310 -0
  33. package/dist/server/engine/pi-rename.js +14 -0
  34. package/dist/server/engine/pi-sdk.js +503 -0
  35. package/dist/server/engine/pi-session-model.js +23 -0
  36. package/dist/server/engine/pi-subagent-tool.js +166 -0
  37. package/dist/server/engine/pi-subagents.js +372 -0
  38. package/dist/server/engine/pi-thinking.js +34 -0
  39. package/dist/server/engine/pi-transcript.js +225 -0
  40. package/dist/server/engine/pi-version.js +11 -0
  41. package/dist/server/engine/port.js +1 -0
  42. package/dist/server/engine/registry.js +47 -0
  43. package/dist/server/engine/scratch.js +13 -0
  44. package/dist/server/engine/subagent-guidance.js +22 -0
  45. package/dist/server/engine/suggestion-guidance.js +9 -0
  46. package/dist/server/engine/switchable.js +22 -0
  47. package/dist/server/engine/wiring.js +166 -0
  48. package/dist/server/files/dir-routes.js +40 -0
  49. package/dist/server/files/file-search.js +63 -0
  50. package/dist/server/files/file-service.js +232 -0
  51. package/dist/server/files/files-routes.js +210 -0
  52. package/dist/server/files/git-info.js +361 -0
  53. package/dist/server/http/auth-routes.js +99 -0
  54. package/dist/server/http/auth.js +191 -0
  55. package/dist/server/http/aux-routes.js +40 -0
  56. package/dist/server/http/compress.js +77 -0
  57. package/dist/server/http/health-and-static.js +33 -0
  58. package/dist/server/http/live-routes.js +113 -0
  59. package/dist/server/http/login-page.js +200 -0
  60. package/dist/server/index.js +363 -520
  61. package/dist/server/markers/actions.js +262 -0
  62. package/dist/server/mcp/mcp-auth.js +184 -0
  63. package/dist/server/mcp/mcp-command.js +94 -0
  64. package/dist/server/mcp/mcp-import.js +290 -0
  65. package/dist/server/mcp/mcp-market-sources.js +192 -0
  66. package/dist/server/mcp/mcp-market-types.js +5 -0
  67. package/dist/server/mcp/mcp-market.js +175 -0
  68. package/dist/server/mcp/mcp-probe.js +134 -0
  69. package/dist/server/mcp/mcp-routes.js +70 -0
  70. package/dist/server/mcp/mcp-store.js +286 -0
  71. package/dist/server/mcp/pi-mcp-config.js +280 -0
  72. package/dist/server/plugins/plugin-routes.js +21 -0
  73. package/dist/server/protocol-parse.js +262 -0
  74. package/dist/server/protocol-settings.js +1 -0
  75. package/dist/server/protocol.js +6 -0
  76. package/dist/server/providers/catalog-store.js +85 -0
  77. package/dist/server/providers/catalog-writes.js +197 -0
  78. package/dist/server/providers/catalog.js +77 -0
  79. package/dist/server/providers/codex-models.js +96 -0
  80. package/dist/server/providers/codex-test.js +124 -0
  81. package/dist/server/providers/enabled-models.js +118 -0
  82. package/dist/server/providers/import-models.js +69 -0
  83. package/dist/server/providers/import-routes.js +203 -0
  84. package/dist/server/providers/model-test.js +52 -0
  85. package/dist/server/providers/probe.js +64 -0
  86. package/dist/server/providers/provider-handlers.js +223 -0
  87. package/dist/server/providers/provider-store.js +359 -0
  88. package/dist/server/providers/request-headers.js +7 -0
  89. package/dist/server/providers/subscription-routes.js +181 -0
  90. package/dist/server/providers/validate.js +241 -0
  91. package/dist/server/push/push-routes.js +114 -0
  92. package/dist/server/push/push.js +120 -0
  93. package/dist/server/session/approval-bridge.js +103 -0
  94. package/dist/server/session/attachment-resolve.js +21 -0
  95. package/dist/server/session/client-view.js +82 -0
  96. package/dist/server/session/conversation-create.js +65 -0
  97. package/dist/server/session/delete-session.js +84 -0
  98. package/dist/server/session/engine-switch.js +108 -0
  99. package/dist/server/session/lock.js +82 -0
  100. package/dist/server/session/model-controller.js +119 -0
  101. package/dist/server/session/notices.js +31 -0
  102. package/dist/server/session/pi-opener.js +123 -0
  103. package/dist/server/session/pi-refresh.js +51 -0
  104. package/dist/server/session/pi-sessions-provider.js +46 -0
  105. package/dist/server/session/pi-sessions.js +272 -0
  106. package/dist/server/session/refresh-actions.js +42 -0
  107. package/dist/server/session/rename-session.js +23 -0
  108. package/dist/server/session/runtime-services.js +28 -0
  109. package/dist/server/session/runtime-types.js +1 -0
  110. package/dist/server/session/runtime.js +380 -0
  111. package/dist/server/session/session-stats.js +91 -0
  112. package/dist/server/session/settings.js +367 -0
  113. package/dist/server/session/snapshot-for.js +7 -0
  114. package/dist/server/session/stats-refresher.js +50 -0
  115. package/dist/server/session/subagents.js +116 -0
  116. package/dist/server/session/switch-conversation.js +86 -0
  117. package/dist/server/session/throttle.js +14 -0
  118. package/dist/server/session/turn-events.js +126 -0
  119. package/dist/server/session/turn-finish.js +79 -0
  120. package/dist/server/session/turn-markers.js +26 -0
  121. package/dist/server/session/turn-queue.js +111 -0
  122. package/dist/server/session/turn-send.js +108 -0
  123. package/dist/server/session/turn-watchdog.js +77 -0
  124. package/dist/server/sessions/session-routes.js +17 -0
  125. package/dist/server/skills/skill-install.js +186 -0
  126. package/dist/server/skills/skill-routes.js +134 -0
  127. package/dist/server/skills/skill-store.js +163 -0
  128. package/dist/server/skills/skillhub.js +214 -0
  129. package/dist/server/skills/zip.js +121 -0
  130. package/dist/server/snapshot/fan-out.js +10 -0
  131. package/dist/server/snapshot/messages.js +75 -0
  132. package/dist/server/snapshot/sender.js +62 -0
  133. package/dist/server/snapshot/state.js +202 -0
  134. package/dist/server/subagents/profile-store.js +287 -0
  135. package/dist/server/terminal/terminal-routes.js +176 -0
  136. package/dist/server/terminal/terminal-session.js +140 -0
  137. package/dist/server/uploads/upload-routes.js +64 -0
  138. package/dist/server/uploads/upload-store.js +115 -0
  139. package/dist/server/util/data-dir.js +36 -0
  140. package/dist/server/util/env.js +146 -0
  141. package/dist/server/util/ids.js +28 -0
  142. package/dist/server/util/log-file.js +67 -0
  143. package/dist/server/util/log.js +52 -0
  144. package/dist/server/util/origin.js +75 -0
  145. package/dist/server/util/paths.js +60 -0
  146. package/dist/server/util/pricing-index.js +121 -0
  147. package/dist/server/util/pricing.js +214 -0
  148. package/dist/server/util/startup.js +34 -0
  149. package/dist/server/util/upstream-version.js +140 -0
  150. package/dist/server/util/usage.js +19 -0
  151. package/dist/server/util/version.js +50 -0
  152. package/dist/server/ws/handle-message.js +107 -0
  153. package/package.json +50 -63
  154. package/web/dist/apple-touch-icon-precomposed.png +0 -0
  155. package/web/dist/apple-touch-icon.png +0 -0
  156. package/web/dist/assets/abnfDiagram-WEYOIA3C-VUKLHHSn.js +1 -0
  157. package/web/dist/assets/arc-BeUkaHhk.js +1 -0
  158. package/web/dist/assets/architecture-WOLXFQ4H-B9I4s0NV.js +1 -0
  159. package/web/dist/assets/architectureDiagram-ZHVYKT53-CaEkLhfq.js +36 -0
  160. package/web/dist/assets/array-BifhSqXX.js +1 -0
  161. package/web/dist/assets/blockDiagram-RJAE7QA3-IR49UNE_.js +140 -0
  162. package/web/dist/assets/c4Diagram-XDPPPE54-B-81CloB.js +38 -0
  163. package/web/dist/assets/channel-B_mTTjGt.js +1 -0
  164. package/web/dist/assets/chunk-2E7WHVMD-CaWVF7qv.js +88 -0
  165. package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  166. package/web/dist/assets/chunk-4EMNKR2T-BvXf2tq-.js +1 -0
  167. package/web/dist/assets/chunk-55XCU76K-Bz_d2e2D.js +1 -0
  168. package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  169. package/web/dist/assets/chunk-7S6XSTFO-B_SPMiU8.js +2 -0
  170. package/web/dist/assets/chunk-AQDTSKZB-Cen-_xej.js +72 -0
  171. package/web/dist/assets/chunk-DSNLSHJZ-DEw1kFwj.js +129 -0
  172. package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  173. package/web/dist/assets/chunk-HAGFOTED-BwpOOcw8.js +62 -0
  174. package/web/dist/assets/chunk-HNE4Q36P-BjCwi2uW.js +1 -0
  175. package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  176. package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  177. package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  178. package/web/dist/assets/chunk-NQOKRVIF-WcqhWFS5.js +213 -0
  179. package/web/dist/assets/chunk-UHW7PFBP-quDvQby8.js +1 -0
  180. package/web/dist/assets/chunk-VQ5W62QI-DZBwmhzq.js +1 -0
  181. package/web/dist/assets/chunk-WQQL4CDK-wYyHYuui.js +10 -0
  182. package/web/dist/assets/chunk-XXDRQBXY-DfJzC7nt.js +1 -0
  183. package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  184. package/web/dist/assets/chunk-YZO67HFG-CWLraGk_.js +1 -0
  185. package/web/dist/assets/chunk-ZAUX2LTX-ChBUt_FJ.js +1 -0
  186. package/web/dist/assets/chunk-ZCHMQSOR-C57UFT60.js +2 -0
  187. package/web/dist/assets/classDiagram-v2-4MYWR3UA-Bo4i7SNY.js +217 -0
  188. package/web/dist/assets/cose-bilkent-JH36ORCC-CK360NdH.js +1 -0
  189. package/web/dist/assets/cynefin-EF2NZ3EQ-DlXFjIFm.js +1 -0
  190. package/web/dist/assets/cynefinDiagram-WF24UDRP-zvZIqA1c.js +62 -0
  191. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  192. package/web/dist/assets/dagre-NK4GIZVX-CnKhCu-v.js +4 -0
  193. package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  194. package/web/dist/assets/diagram-CBK3MWTC-3QTR8Fjx.js +24 -0
  195. package/web/dist/assets/diagram-I5XIIV2D-D9lSd7z6.js +24 -0
  196. package/web/dist/assets/diagram-ODIL2UCW-DWJD6lPM.js +30 -0
  197. package/web/dist/assets/diagram-PFBEJUXE-CWJmoUg4.js +3 -0
  198. package/web/dist/assets/diagram-RJA3I4VV-D1SZHQcQ.js +41 -0
  199. package/web/dist/assets/diagram-TYUNFSNR-DCCjGi1x.js +200 -0
  200. package/web/dist/assets/dist-VOk_Iv17.js +1 -0
  201. package/web/dist/assets/ebnfDiagram-V4P3TTPT-CQLdqEiW.js +1 -0
  202. package/web/dist/assets/elk-QONTBCTQ-Dp2pEp6b.js +27 -0
  203. package/web/dist/assets/erDiagram-KYQIGQAA-BFGNoFQ-.js +99 -0
  204. package/web/dist/assets/eventmodeling-K75KTNOO-CFfAemGw.js +1 -0
  205. package/web/dist/assets/flowDiagram-IUGLLWBU-DmSp3cbW.js +1 -0
  206. package/web/dist/assets/ganttDiagram-R7GPNTJY-vK3jPGDq.js +292 -0
  207. package/web/dist/assets/gitGraph-VSP46ZUC-rXBgHTdq.js +1 -0
  208. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-CBbGhago.js +106 -0
  209. package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  210. package/web/dist/assets/index-DBOF_OJe.js +104 -0
  211. package/web/dist/assets/index-nlY_-OaR.css +1 -0
  212. package/web/dist/assets/info-OHQRW6UA-Cd9vp-6C.js +1 -0
  213. package/web/dist/assets/infoDiagram-MVDRKAX5-Ce-Sg_qu.js +2 -0
  214. package/web/dist/assets/init-C-OQMol4.js +1 -0
  215. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-DWlmR6BY.js +70 -0
  216. package/web/dist/assets/journeyDiagram-BOSHW7O7-DmDG97ae.js +139 -0
  217. package/web/dist/assets/kanban-definition-FNSA4HC5-1Z4sDrgV.js +89 -0
  218. package/web/dist/assets/katex-B_JcIwqj.js +257 -0
  219. package/web/dist/assets/line-DGM1-9KW.js +1 -0
  220. package/web/dist/assets/linear-B6hyeGHu.js +1 -0
  221. package/web/dist/assets/mermaid-parser.core-D1HmdkA4.js +7 -0
  222. package/web/dist/assets/mermaid.core-DeUJrhO1.js +44 -0
  223. package/web/dist/assets/mindmap-definition-6OY57UG3-C3-FByy8.js +98 -0
  224. package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  225. package/web/dist/assets/packet-JDAUHWVQ-BoOr7DaW.js +1 -0
  226. package/web/dist/assets/path-fybaL0A-.js +1 -0
  227. package/web/dist/assets/pegDiagram-ECIUJNBX-Bn7m75TZ.js +1 -0
  228. package/web/dist/assets/pie-XZMESJXO-CGuMTDBp.js +1 -0
  229. package/web/dist/assets/pieDiagram-GNMPY74T-DHhaPwZe.js +39 -0
  230. package/web/dist/assets/quadrantDiagram-D57K4MC3-DxTgFHLK.js +7 -0
  231. package/web/dist/assets/radar-ABXABTNO-BiKxmALL.js +1 -0
  232. package/web/dist/assets/railroad-I3PHUGI6-BKYufgg4.js +1 -0
  233. package/web/dist/assets/railroad-abnf-LNEFI6M7-DmOrjlyL.js +1 -0
  234. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-BgMyVDd1.js +1 -0
  235. package/web/dist/assets/railroad-peg-JR7BVNR7-BN6NL8S-.js +1 -0
  236. package/web/dist/assets/railroadDiagram-Y72NRBL7-BsbA-9gP.js +1 -0
  237. package/web/dist/assets/requirementDiagram-EQYSW5RA-HUw4YqWd.js +84 -0
  238. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  239. package/web/dist/assets/sankeyDiagram-SY24JHO5-Dx1MOFRu.js +40 -0
  240. package/web/dist/assets/sequenceDiagram-BLEHNHQU-C0-Rzg3g.js +169 -0
  241. package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  242. package/web/dist/assets/src-CuHygcpj.js +1 -0
  243. package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnEICGtg.js +291 -0
  244. package/web/dist/assets/swimlanes-B3ENRKFQ-CwDRCHZD.js +1 -0
  245. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-DZ_aAXmG.js +8 -0
  246. package/web/dist/assets/timeline-definition-JXYKGLGB-C4BVpgzc.js +120 -0
  247. package/web/dist/assets/treeView-D4JQ5SDB-BBsvEMP3.js +1 -0
  248. package/web/dist/assets/treemap-SAJKECNS-BYw6cxC8.js +1 -0
  249. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkOXLgwP.js +328 -0
  250. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Kgjk_n6B.js +34 -0
  251. package/web/dist/assets/wardley-7MLQ67FV-BGVObhvJ.js +1 -0
  252. package/web/dist/assets/wardleyDiagram-IVAIDKWA-i97uD6tL.js +78 -0
  253. package/web/dist/assets/xychartDiagram-4URNHDU4-awJi5TfJ.js +7 -0
  254. package/web/dist/favicon-32.png +0 -0
  255. package/web/dist/favicon.ico +0 -0
  256. package/web/dist/favicon.svg +20 -8
  257. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  258. package/web/dist/icons/icon-192.png +0 -0
  259. package/web/dist/icons/icon-512.png +0 -0
  260. package/web/dist/icons/icon-maskable-512.png +0 -0
  261. package/web/dist/index.html +31 -24
  262. package/web/dist/manifest.webmanifest +17 -0
  263. package/web/dist/maskable.svg +25 -0
  264. package/web/dist/sw.js +40 -0
  265. package/bin/oh-my-agent-web-ps.mjs +0 -203
  266. package/bin/oh-my-agent-web-ws.mjs +0 -782
  267. package/bin/oh-my-agent-web.mjs +0 -321
  268. package/bin/update-check.mjs +0 -142
  269. package/dist/server/codex-client.js +0 -310
  270. package/dist/server/codex-supervisor.js +0 -446
  271. package/dist/server/fs-service.js +0 -144
  272. package/dist/server/request-inspector.js +0 -458
  273. package/dist/shared/protocol.js +0 -112
  274. package/scripts/manage-server.mjs +0 -155
  275. package/scripts/omaw.mjs +0 -74
  276. package/shared/protocol.ts +0 -228
  277. package/web/dist/assets/index-5LJ5fRcO.js +0 -42
  278. package/web/dist/assets/index-QiDfPKFo.css +0 -1
  279. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -0,0 +1,186 @@
1
+ /**
2
+ * 技能的三种安装方式 + 删除。
3
+ *
4
+ * 参考 `/Users/dds/data/git/oh-my-skills`(`skillhub-install.ts` / `repo-install.ts`),
5
+ * 但有一处**刻意不同**:参考实现装 Git 仓库里的技能时只写一份 `SKILL.md`,
6
+ * 那样 `scripts/`、`references/` 全丢 —— 而现在的技能大量靠这两样干活。
7
+ * 这里一律**整目录复制**。
8
+ *
9
+ * 三条纪律:
10
+ * - **只往 `~/.agents/skills` 写**(目录由调用方给,默认就是它):这是 pi 真正扫的那份,
11
+ * 也是界面上「默认列出来」的那份,安装完不用第二处同步;
12
+ * - **不覆盖同名**:目标目录已存在就报错(`skill.exists`),让用户自己去删 ——
13
+ * 静默覆盖会把人家手改过的技能冲掉,这比多一步麻烦糟得多;
14
+ * - **临时目录一定会清**(`finally`),下载/克隆失败不留下几百 MB 垃圾。
15
+ */
16
+ import { execFile } from "node:child_process";
17
+ import { cp, mkdir, mkdtemp, readdir, rm, stat, writeFile } from "node:fs/promises";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { promisify } from "node:util";
21
+ import { hasSkillFile, safeSkillDirName } from "./skillhub.js";
22
+ const run = promisify(execFile);
23
+ /** git clone 的上限:技能仓库一般很小,卡住不收就是事故 */
24
+ const GIT_TIMEOUT_MS = 120_000;
25
+ async function exists(path) {
26
+ return stat(path)
27
+ .then(() => true)
28
+ .catch(() => false);
29
+ }
30
+ /** 目标目录已有技能时的统一报错(README 里也这么写,用户知道下一步干什么) */
31
+ function existsError(id) {
32
+ return { ok: false, error: `这个技能目录已经存在:${id}(要重装先删掉它,我不想覆盖你手改过的东西)` };
33
+ }
34
+ /** 把一个技能目录整份复制到 `skillsRoot/<id>` */
35
+ async function copyInto(skillsRoot, sourceDir, id) {
36
+ const target = join(skillsRoot, id);
37
+ if (await exists(target))
38
+ return existsError(id);
39
+ await mkdir(skillsRoot, { recursive: true });
40
+ // `cp` 的 recursive 会连 `scripts/`、`references/`、`assets/` 一起带过去
41
+ await cp(sourceDir, target, { recursive: true, errorOnExist: true, force: false });
42
+ return { ok: true, id, path: target };
43
+ }
44
+ function gitUrlLooksSafe(url) {
45
+ const value = url.trim();
46
+ if (value === "")
47
+ return false;
48
+ // 允许 https / ssh / git@host:path / 本地绝对路径(自己机器上的仓库也该能装)
49
+ if (/^https?:\/\//i.test(value))
50
+ return true;
51
+ if (/^git@[^:\s]+:.+$/i.test(value))
52
+ return true;
53
+ if (/^ssh:\/\//i.test(value))
54
+ return true;
55
+ return value.startsWith("/") && !value.includes("..");
56
+ }
57
+ /** 克隆到一个临时目录;返回目录与清理函数 */
58
+ async function cloneRepo(repoUrl) {
59
+ const workDir = await mkdtemp(join(tmpdir(), "omaw-repo-"));
60
+ const dir = join(workDir, "repo");
61
+ await run("git", ["clone", "--depth", "1", repoUrl, dir], {
62
+ timeout: GIT_TIMEOUT_MS,
63
+ maxBuffer: 8 * 1024 * 1024,
64
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
65
+ });
66
+ return { dir, cleanup: () => rm(workDir, { recursive: true, force: true }).catch(() => undefined) };
67
+ }
68
+ /** 仓库里的技能可能在根目录,也可能在 `skills/` 下(两种都认,跟参考实现一致) */
69
+ async function findSkillInRepo(repoDir, skill) {
70
+ const candidates = [join(repoDir, "skills", skill), join(repoDir, skill)];
71
+ for (const candidate of candidates) {
72
+ if (await hasSkillFile(candidate))
73
+ return candidate;
74
+ }
75
+ return null;
76
+ }
77
+ export function createSkillInstaller(options) {
78
+ const { skillsRoot, hub } = options;
79
+ return {
80
+ async fromHub(input) {
81
+ const id = safeSkillDirName(input.name ?? input.slug);
82
+ if (await exists(join(skillsRoot, id)))
83
+ return existsError(id);
84
+ try {
85
+ const pkg = await hub.fetchPackage(input.slug);
86
+ try {
87
+ return await copyInto(skillsRoot, pkg.dir, id);
88
+ }
89
+ finally {
90
+ await pkg.cleanup();
91
+ }
92
+ }
93
+ catch (error) {
94
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
95
+ }
96
+ },
97
+ async listRepo(repoUrl) {
98
+ if (!gitUrlLooksSafe(repoUrl))
99
+ return { ok: false, error: "仓库地址看起来不对(支持 https / ssh / git@host:path / 本地绝对路径)" };
100
+ let cloned;
101
+ try {
102
+ cloned = await cloneRepo(repoUrl.trim());
103
+ const found = new Set();
104
+ for (const base of [join(cloned.dir, "skills"), cloned.dir]) {
105
+ const entries = await readdir(base, { withFileTypes: true }).catch(() => []);
106
+ for (const entry of entries) {
107
+ if (!entry.isDirectory() || entry.name.startsWith("."))
108
+ continue;
109
+ if (await hasSkillFile(join(base, entry.name)))
110
+ found.add(entry.name);
111
+ }
112
+ }
113
+ if (found.size === 0)
114
+ return { ok: false, error: "这个仓库里没找到技能(根目录或 skills/ 下要有 SKILL.md)" };
115
+ return { ok: true, skills: [...found].sort() };
116
+ }
117
+ catch (error) {
118
+ return { ok: false, error: describeGit(error) };
119
+ }
120
+ finally {
121
+ await cloned?.cleanup();
122
+ }
123
+ },
124
+ async fromRepo(input) {
125
+ if (!gitUrlLooksSafe(input.repoUrl))
126
+ return { ok: false, error: "仓库地址看起来不对(支持 https / ssh / git@host:path / 本地绝对路径)" };
127
+ const id = safeSkillDirName(input.name ?? input.skill);
128
+ if (await exists(join(skillsRoot, id)))
129
+ return existsError(id);
130
+ let cloned;
131
+ try {
132
+ cloned = await cloneRepo(input.repoUrl.trim());
133
+ const dir = await findSkillInRepo(cloned.dir, input.skill);
134
+ if (dir === null)
135
+ return { ok: false, error: `仓库里没找到技能 "${input.skill}"` };
136
+ return await copyInto(skillsRoot, dir, id);
137
+ }
138
+ catch (error) {
139
+ return { ok: false, error: describeGit(error) };
140
+ }
141
+ finally {
142
+ await cloned?.cleanup();
143
+ }
144
+ },
145
+ async manual(input) {
146
+ const id = safeSkillDirName(input.name);
147
+ const target = join(skillsRoot, id);
148
+ if (await exists(target))
149
+ return existsError(id);
150
+ await mkdir(target, { recursive: true });
151
+ const name = input.name.trim() === "" ? id : input.name.trim();
152
+ const body = input.raw !== undefined && input.raw.trim() !== ""
153
+ ? input.raw
154
+ : `---\nname: ${name}\ndescription: (一句话说明这个技能什么时候用)\n---\n\n# ${name}\n\n写清楚:什么时候用它、怎么做、做完怎么确认。\n`;
155
+ await writeFile(join(target, "SKILL.md"), body, "utf8");
156
+ return { ok: true, id, path: target };
157
+ },
158
+ async remove(id) {
159
+ const safe = safeSkillDirName(id);
160
+ if (safe !== id)
161
+ return { ok: false, error: `非法技能 id:${id}` };
162
+ const target = join(skillsRoot, id);
163
+ if (!(await hasSkillFile(target)))
164
+ return { ok: false, error: `"${id}" 不是一个技能目录` };
165
+ try {
166
+ // 整目录删:SKILL.md(或禁用态的 .SKILL.md.disabled)与 scripts/references 一起走
167
+ await rm(target, { recursive: true, force: true });
168
+ return { ok: true };
169
+ }
170
+ catch (error) {
171
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
172
+ }
173
+ },
174
+ };
175
+ }
176
+ /** git 的报错尾巴通常很有用(`Repository not found` / `could not resolve host`),别吞掉 */
177
+ function describeGit(error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ const stderr = error.stderr;
180
+ const text = (stderr !== undefined && stderr.trim() !== "" ? stderr : message).trim();
181
+ const lines = text
182
+ .split("\n")
183
+ .map((line) => line.trim())
184
+ .filter((line) => line !== "");
185
+ return `克隆失败:${lines.slice(-2).join(" / ").slice(0, 400)}`;
186
+ }
@@ -0,0 +1,134 @@
1
+ import { HUB_CATEGORIES, createSkillHub, listSkillDirs } from "./skillhub.js";
2
+ import { createSkillInstaller } from "./skill-install.js";
3
+ import { createSkillStore } from "./skill-store.js";
4
+ export function registerSkillRoutes(app, options = {}) {
5
+ const skills = options.skills ?? createSkillStore();
6
+ const skillsRoot = skills.dir;
7
+ const hub = createSkillHub();
8
+ const installer = createSkillInstaller({ skillsRoot, hub });
9
+ /** 安装成功后刷新列表:界面一次请求拿到「新列表 + 提示」,不用再打一次 */
10
+ const afterInstall = async (result, notice) => ({
11
+ ...(result.ok ? { notice } : { installError: result.error ?? "安装失败" }),
12
+ skills: await skills.list(),
13
+ });
14
+ app.get("/api/skills", async (_request, response) => {
15
+ response.json({ skills: await skills.list(), root: skillsRoot });
16
+ });
17
+ app.post("/api/skills/enabled", async (request, response) => {
18
+ const body = request.body;
19
+ const ids = Array.isArray(body?.ids) ? body.ids.filter((item) => typeof item === "string") : [];
20
+ if (ids.length === 0) {
21
+ response.status(400).json({ error: { code: "skills.no_ids", message: "没有指定要操作的技能" } });
22
+ return;
23
+ }
24
+ const enabled = body?.enabled === true;
25
+ const result = await skills.setEnabled(ids, enabled);
26
+ options.onChange?.();
27
+ response.json({ ...result, skills: await skills.list() });
28
+ });
29
+ // ---- 技能市场 ----
30
+ app.get("/api/skills/hub", async (request, response) => {
31
+ const query = typeof request.query.q === "string" ? request.query.q : "";
32
+ const rawCategory = typeof request.query.category === "string" ? request.query.category : "";
33
+ const category = HUB_CATEGORIES.some((item) => item.key === rawCategory) ? rawCategory : "";
34
+ const page = Number.parseInt(typeof request.query.page === "string" ? request.query.page : "1", 10) || 1;
35
+ try {
36
+ const result = await hub.search({
37
+ query,
38
+ category,
39
+ page,
40
+ pageSize: 24,
41
+ refresh: request.query.refresh === "1",
42
+ });
43
+ // 已安装的标记:按目录名比(安装时目录名就是 slug / 技能名,见 skill-install.ts)
44
+ const installed = new Set(await listSkillDirs(skillsRoot));
45
+ response.json({
46
+ ...result,
47
+ skills: result.skills.map((skill) => ({
48
+ ...skill,
49
+ installed: installed.has(skill.slug) || installed.has(skill.name),
50
+ })),
51
+ categories: HUB_CATEGORIES,
52
+ });
53
+ }
54
+ catch (error) {
55
+ response.status(502).json({
56
+ error: { code: "skills.hub_failed", message: error instanceof Error ? error.message : String(error) },
57
+ });
58
+ }
59
+ });
60
+ app.post("/api/skills/install", async (request, response) => {
61
+ const body = request.body;
62
+ const source = body?.source;
63
+ const text = (value) => (typeof value === "string" ? value : undefined);
64
+ if (source === "hub") {
65
+ const slug = text(body?.slug) ?? "";
66
+ if (slug === "") {
67
+ response.status(400).json({ error: { code: "skills.no_slug", message: "没有指定要装的技能" } });
68
+ return;
69
+ }
70
+ const result = await installer.fromHub({ slug, name: text(body?.name) });
71
+ response.json(await afterInstall(result, `已安装「${text(body?.name) ?? slug}」`));
72
+ return;
73
+ }
74
+ if (source === "git") {
75
+ const repoUrl = text(body?.repoUrl) ?? "";
76
+ const skill = text(body?.skill);
77
+ // 没给 skill = 先看仓库里有什么(两段式),给了才真装
78
+ if (skill === undefined || skill === "") {
79
+ response.json(await installer.listRepo(repoUrl));
80
+ return;
81
+ }
82
+ const result = await installer.fromRepo({ repoUrl, skill, name: text(body?.name) });
83
+ response.json(await afterInstall(result, `已从仓库安装「${skill}」`));
84
+ return;
85
+ }
86
+ if (source === "manual") {
87
+ const name = text(body?.name) ?? "";
88
+ const result = await installer.manual({ name, raw: text(body?.raw) });
89
+ response.json(await afterInstall(result, `已创建「${name}」`));
90
+ return;
91
+ }
92
+ response.status(400).json({ error: { code: "skills.bad_source", message: "不认识的安装方式" } });
93
+ });
94
+ app.post("/api/skills/remove", async (request, response) => {
95
+ const body = request.body;
96
+ // 单个(老的调用点)与批量(左栏多选)都走这一条:删除是「一个目录一个目录地删」,
97
+ // 批量只是循环,没必要再加一个端点
98
+ const ids = Array.isArray(body?.ids)
99
+ ? body.ids.filter((item) => typeof item === "string")
100
+ : typeof body?.id === "string"
101
+ ? [body.id]
102
+ : [];
103
+ if (ids.length === 0) {
104
+ response.status(400).json({ error: { code: "skills.no_id", message: "没有指定要删除的技能" } });
105
+ return;
106
+ }
107
+ const removed = [];
108
+ const failures = [];
109
+ for (const id of ids) {
110
+ const result = await installer.remove(id);
111
+ if (result.ok)
112
+ removed.push(id);
113
+ else
114
+ failures.push(`${id}:${result.error ?? "删除失败"}`);
115
+ }
116
+ options.onChange?.();
117
+ response.json({
118
+ ...(removed.length > 0
119
+ ? { notice: removed.length === 1 ? `已删除「${removed[0]}」` : `已删除 ${removed.length} 个技能` }
120
+ : {}),
121
+ ...(failures.length > 0 ? { removeError: failures.join(";") } : {}),
122
+ skills: await skills.list(),
123
+ });
124
+ });
125
+ app.get("/api/skills/:id/file", async (request, response) => {
126
+ const relPath = typeof request.query.path === "string" ? request.query.path : "";
127
+ const result = await skills.readFile(request.params.id, relPath);
128
+ if (!result.ok) {
129
+ response.status(400).json({ error: { code: "skills.read_failed", message: result.error } });
130
+ return;
131
+ }
132
+ response.json(result);
133
+ });
134
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * 技能列表与启停(P8)。
3
+ *
4
+ * 技能目录:`~/.agents/skills/<目录名>/SKILL.md`(frontmatter 里有 name / description),
5
+ * 目录里还可能有 `scripts/`、`references/` 等。
6
+ *
7
+ * **启停的实现方式(刻意选择)**:把 `SKILL.md` 改名成 `.SKILL.md.disabled`。
8
+ * - pi 扫描技能只认 `SKILL.md`,改名即"看不见",这是真正生效的禁用;
9
+ * - 不动其它文件、不动目录结构,一条 mv 就能复原,**不产生第二份状态文件**
10
+ * (两处状态必然不一致,这是 ADR-0021 的教训)。
11
+ */
12
+ import { readdir, readFile, rename, stat } from "node:fs/promises";
13
+ import { homedir } from "node:os";
14
+ import { join } from "node:path";
15
+ import { readPreview } from "../files/file-service.js";
16
+ export const SKILL_ENTRY = "SKILL.md";
17
+ export const SKILL_ENTRY_DISABLED = ".SKILL.md.disabled";
18
+ export function defaultSkillsDir() {
19
+ return join(homedir(), ".agents", "skills");
20
+ }
21
+ /**
22
+ * 纯函数:从 SKILL.md 的 frontmatter 里抠 name / description(不引 yaml 依赖)。
23
+ *
24
+ * 两种写法都要认 —— 真实技能里用折叠标量的很多(`anthropics/skills` 的 academy-guide 就是):
25
+ *
26
+ * description: 一句话
27
+ * description: >
28
+ * 折成一行的一段
29
+ * 可以有缩进的续行
30
+ *
31
+ * 只认折叠(`>`)与字面(`|`)两种块标量:这两种占真实用法的全部,
32
+ * 为它们引一个 YAML 库不划算(ADR-0008)。
33
+ */
34
+ export function parseFrontmatter(text) {
35
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
36
+ if (!match)
37
+ return { name: "", description: "" };
38
+ const body = match[1] ?? "";
39
+ const lines = body.split(/\r?\n/);
40
+ const read = (key) => {
41
+ const start = lines.findIndex((line) => new RegExp(`^${key}:`).test(line));
42
+ if (start === -1)
43
+ return "";
44
+ const rawValue = (lines[start] ?? "").replace(new RegExp(`^${key}:\\s*`), "").trim();
45
+ // 块标量:`>` 折叠成空格,`|` 保留换行;续行是后面那些「缩进或空行」的行
46
+ const scalar = /^([>|])([+-]?)$/.exec(rawValue);
47
+ if (scalar) {
48
+ const collected = [];
49
+ for (let index = start + 1; index < lines.length; index += 1) {
50
+ const line = lines[index] ?? "";
51
+ if (line.trim() !== "" && !/^\s/.test(line))
52
+ break; // 顶格 = 下一个键
53
+ collected.push(line.trim());
54
+ }
55
+ const text = collected.join(scalar[1] === "|" ? "\n" : " ").trim();
56
+ return text.replace(/\s+/g, " ");
57
+ }
58
+ // 单行:去掉包裹的引号
59
+ return rawValue.replace(/^["']/, "").replace(/["']$/, "");
60
+ };
61
+ return { name: read("name"), description: read("description") };
62
+ }
63
+ /** 递归列目录里的文件(相对路径),有上限,避免某个技能塞几千个文件把界面拖死 */
64
+ export async function listFiles(root, prefix = "", out = []) {
65
+ if (out.length >= 200)
66
+ return out;
67
+ let entries;
68
+ try {
69
+ entries = await readdir(join(root, prefix), { withFileTypes: true });
70
+ }
71
+ catch {
72
+ return out;
73
+ }
74
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
75
+ if (out.length >= 200)
76
+ break;
77
+ if (entry.name === "cache" || entry.name.startsWith("."))
78
+ continue;
79
+ const rel = join(prefix, entry.name);
80
+ if (entry.isDirectory())
81
+ await listFiles(root, rel, out);
82
+ else
83
+ out.push(rel);
84
+ }
85
+ return out;
86
+ }
87
+ function safeId(id) {
88
+ // 目录名白名单:不允许穿越、不允许以点开头(那是内部文件)
89
+ return /^[A-Za-z0-9._-]{1,80}$/.test(id) && !id.startsWith(".");
90
+ }
91
+ export function createSkillStore(options = {}) {
92
+ const root = options.dir ?? defaultSkillsDir();
93
+ const resolve = (id) => (safeId(id) ? join(root, id) : null);
94
+ return {
95
+ dir: root,
96
+ async list() {
97
+ let names = [];
98
+ try {
99
+ names = (await readdir(root, { withFileTypes: true }))
100
+ .filter((entry) => entry.isDirectory())
101
+ .map((entry) => entry.name);
102
+ }
103
+ catch {
104
+ return [];
105
+ }
106
+ const skills = [];
107
+ for (const id of names.sort()) {
108
+ const dir = join(root, id);
109
+ const enabled = await stat(join(dir, SKILL_ENTRY))
110
+ .then(() => true)
111
+ .catch(() => false);
112
+ const entryPath = join(dir, enabled ? SKILL_ENTRY : SKILL_ENTRY_DISABLED);
113
+ const text = await readFile(entryPath, "utf8").catch(() => "");
114
+ if (text === "")
115
+ continue; // 既没有 SKILL.md 也没有 .disabled 的目录不是技能
116
+ const meta = parseFrontmatter(text);
117
+ skills.push({
118
+ id,
119
+ name: meta.name !== "" ? meta.name : id,
120
+ description: meta.description,
121
+ enabled,
122
+ files: await listFiles(dir),
123
+ });
124
+ }
125
+ return skills;
126
+ },
127
+ async setEnabled(ids, enabled) {
128
+ const changed = [];
129
+ const errors = [];
130
+ for (const id of ids) {
131
+ const dir = resolve(id);
132
+ if (!dir) {
133
+ errors.push(`非法技能 id:${id}`);
134
+ continue;
135
+ }
136
+ const from = join(dir, enabled ? SKILL_ENTRY_DISABLED : SKILL_ENTRY);
137
+ const to = join(dir, enabled ? SKILL_ENTRY : SKILL_ENTRY_DISABLED);
138
+ const exists = await stat(from)
139
+ .then(() => true)
140
+ .catch(() => false);
141
+ if (!exists)
142
+ continue; // 已经是目标状态
143
+ try {
144
+ await rename(from, to);
145
+ changed.push(id);
146
+ }
147
+ catch (error) {
148
+ errors.push(`${id}:${error instanceof Error ? error.message : String(error)}`);
149
+ }
150
+ }
151
+ return { changed, errors };
152
+ },
153
+ async readFile(id, relPath) {
154
+ const dir = resolve(id);
155
+ if (!dir)
156
+ return { ok: false, error: "非法技能 id" };
157
+ const result = await readPreview(dir, relPath);
158
+ if (!result.ok)
159
+ return { ok: false, error: result.error };
160
+ return { ok: true, text: result.text, binary: result.binary, bytes: result.bytes };
161
+ },
162
+ };
163
+ }