@xopcai/xopc 0.0.113 → 0.0.114

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 (327) hide show
  1. package/dist/browser-ext/manifest.json +1 -1
  2. package/dist/extensions/telegram/xopc.extension.json +1 -1
  3. package/dist/gateway/static/root/assets/{agent-avatar-dicebear-CBlklyNr.js → agent-avatar-dicebear-Fp-p7bP1.js} +1 -1
  4. package/dist/gateway/static/root/assets/agent-browser-settings-page-CWh7trjd.js +6 -0
  5. package/dist/gateway/static/root/assets/agent-defaults-tabbed-page-kC8OuLoj.js +1 -0
  6. package/dist/gateway/static/root/assets/agents-DJWG26Si.js +71 -0
  7. package/dist/gateway/static/root/assets/{app-management-settings-panel-28E_WfMe.js → app-management-settings-panel-v24i25Oj.js} +1 -1
  8. package/dist/gateway/static/root/assets/{appearance-settings-Do1z0XgJ.js → appearance-settings-ycbdJRuZ.js} +1 -1
  9. package/dist/gateway/static/root/assets/apps-page-0V3DXZ7J.js +1 -0
  10. package/dist/gateway/static/root/assets/{archive-plugin-BAFlsov_.js → archive-plugin-CJzSADJo.js} +1 -1
  11. package/dist/gateway/static/root/assets/{attachment-load-V3qGPC3x.js → attachment-load-BTAB4zGS.js} +1 -1
  12. package/dist/gateway/static/root/assets/{binary-plugins-DT8NvA6W.js → binary-plugins-DXR1-mct.js} +1 -1
  13. package/dist/gateway/static/root/assets/{block-editor-CdLMEcHd.js → block-editor-CzQri9h2.js} +44 -44
  14. package/dist/gateway/static/root/assets/channels-settings-CIKOkYdO.js +1 -0
  15. package/dist/gateway/static/root/assets/{config-api-Bf5mjpu7.js → config-api-DRqjEvAx.js} +1 -1
  16. package/dist/gateway/static/root/assets/connectors-settings-ClrDS5jH.js +2 -0
  17. package/dist/gateway/static/root/assets/{cron-api--pAB_rps.js → cron-api-BXaZXv1o.js} +1 -1
  18. package/dist/gateway/static/root/assets/cron-page-Chi_4u4P.js +3 -0
  19. package/dist/gateway/static/root/assets/dreaming-settings-B0_rs2tj.js +1 -0
  20. package/dist/gateway/static/root/assets/{extension-debug-page-BGhE7msg.js → extension-debug-page-DSkqMYo7.js} +1 -1
  21. package/dist/gateway/static/root/assets/{extension-page-CvpI45WG.js → extension-page-h2dGDbMN.js} +1 -1
  22. package/dist/gateway/static/root/assets/{extension-settings-page-DAmR5FUe.js → extension-settings-page-hN99RhOi.js} +1 -1
  23. package/dist/gateway/static/root/assets/{fetch-NZmjSXLl.js → fetch-D9QqUtzZ.js} +1 -1
  24. package/dist/gateway/static/root/assets/{format-interval-label-qhth3BMV.js → format-interval-label-D6E687rG.js} +1 -1
  25. package/dist/gateway/static/root/assets/{gateway-config-api-CaXBt-9R.js → gateway-config-api-CLt_SKTC.js} +1 -1
  26. package/dist/gateway/static/root/assets/{gateway-settings-DphpbLBc.js → gateway-settings-1PDi4lI0.js} +1 -1
  27. package/dist/gateway/static/root/assets/{gateway-startup-retry-Be1omjh3.js → gateway-startup-retry-N7Z_sz7p.js} +1 -1
  28. package/dist/gateway/static/root/assets/goal-detail-page-D_wwXL-t.js +1 -0
  29. package/dist/gateway/static/root/assets/goals-page-DZ2toZ8y.js +2 -0
  30. package/dist/gateway/static/root/assets/{goals-settings-ceEBjnfc.js → goals-settings-LFlwtIKE.js} +1 -1
  31. package/dist/gateway/static/root/assets/{heartbeat-config-api-BzO6G4fD.js → heartbeat-config-api-DGPadTw-.js} +1 -1
  32. package/dist/gateway/static/root/assets/{heartbeat-settings-DCST9-du.js → heartbeat-settings-BHHAs8ZX.js} +1 -1
  33. package/dist/gateway/static/root/assets/index-DefIdW9q.js +122 -0
  34. package/dist/gateway/static/root/assets/index-DzmWBkq-.css +1 -0
  35. package/dist/gateway/static/root/assets/{interval-presets-DJmshV8n.js → interval-presets-CEklPQ_r.js} +1 -1
  36. package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-9AUUlQ5Q.js → keyboard-shortcuts-settings-Ce_aQ8Iv.js} +1 -1
  37. package/dist/gateway/static/root/assets/logs-page-j_D2QTuQ.js +2 -0
  38. package/dist/gateway/static/root/assets/{markdown-split-CYYELqAr.js → markdown-split-BMz3xPXO.js} +1 -1
  39. package/dist/gateway/static/root/assets/{media-plugins-BEcMR2M8.js → media-plugins-BalOSoz9.js} +1 -1
  40. package/dist/gateway/static/root/assets/messages-BN_WiUg0.js +3 -0
  41. package/dist/gateway/static/root/assets/models-hub-panel-CMH_TmZz.js +2 -0
  42. package/dist/gateway/static/root/assets/note-detail-page-O18BrQPq.js +2 -0
  43. package/dist/gateway/static/root/assets/{note-image-lightbox-DNAoPcmm.js → note-image-lightbox-C5D1Q5mY.js} +1 -1
  44. package/dist/gateway/static/root/assets/notes-page-B_ZYCmY9.js +1 -0
  45. package/dist/gateway/static/root/assets/{preview-open-alternatives-BPLz0AIv.js → preview-open-alternatives-CYFR5U9l.js} +1 -1
  46. package/dist/gateway/static/root/assets/remote-access-hub-CwKgWixY.js +1 -0
  47. package/dist/gateway/static/root/assets/{save-bar-controls-CrWwWRVK.js → save-bar-controls-BQ79Z5IP.js} +1 -1
  48. package/dist/gateway/static/root/assets/{schedule-field-CF2sadvN.js → schedule-field-Bgk6Emue.js} +1 -1
  49. package/dist/gateway/static/root/assets/{schema-form-Di5cKjYE.js → schema-form-DzS36TMa.js} +1 -1
  50. package/dist/gateway/static/root/assets/sessions-page-C4fcD-7B.js +1 -0
  51. package/dist/gateway/static/root/assets/settings-advanced-gate-Ad2OJ86w.js +1 -0
  52. package/dist/gateway/static/root/assets/{settings-collapsible-section-Chh282jW.js → settings-collapsible-section-CdjFnSAy.js} +1 -1
  53. package/dist/gateway/static/root/assets/{settings-form-section-Dq-83cQd.js → settings-form-section-DZLy_exu.js} +1 -1
  54. package/dist/gateway/static/root/assets/settings-page-D2BMY5wo.js +1 -0
  55. package/dist/gateway/static/root/assets/setup-status-panel-aWwxl_E1.js +1 -0
  56. package/dist/gateway/static/root/assets/{share-preview-page-0FXYnSUA.js → share-preview-page-_qRDr4g_.js} +1 -1
  57. package/dist/gateway/static/root/assets/shares-settings-DUE83uIf.js +1 -0
  58. package/dist/gateway/static/root/assets/skill-reload-api-mWKJcrog.js +1 -0
  59. package/dist/gateway/static/root/assets/skills-page-0Sx1Q58n.js +2 -0
  60. package/dist/gateway/static/root/assets/{system-settings-panel-W1pTcojB.js → system-settings-panel-hqaBF5aG.js} +1 -1
  61. package/dist/gateway/static/root/assets/{use-agent-defaults-form-BVRvkK7m.js → use-agent-defaults-form-zbVIQLOA.js} +1 -1
  62. package/dist/gateway/static/root/assets/{use-channel-catalog-CgJibfMM.js → use-channel-catalog-CbXq97rT.js} +1 -1
  63. package/dist/gateway/static/root/assets/{use-settings-tab-guard-Bw_qkRQE.js → use-settings-tab-guard-DwS3drWK.js} +1 -1
  64. package/dist/gateway/static/root/assets/voice-api-key-field-D6HR0hR2.js +1 -0
  65. package/dist/gateway/static/root/assets/{voice-player-store-C7lziaAx.js → voice-player-store-DSuOd9zy.js} +1 -1
  66. package/dist/gateway/static/root/assets/workflow-page.utils-C0Uq3xyq.js +1 -0
  67. package/dist/gateway/static/root/assets/workflows-page-DW0fmWhQ.js +18 -0
  68. package/dist/gateway/static/root/index.html +5 -5
  69. package/dist/package.js +1 -1
  70. package/dist/src/agent/agent-manager.d.ts +8 -0
  71. package/dist/src/agent/agent-manager.js +57 -22
  72. package/dist/src/agent/agent-manager.js.map +1 -1
  73. package/dist/src/agent/bootstrap/project-agents-context.d.ts +2 -0
  74. package/dist/src/agent/bootstrap/project-agents-context.js +32 -0
  75. package/dist/src/agent/bootstrap/project-agents-context.js.map +1 -0
  76. package/dist/src/agent/goals/persistent-goal-service.js +2 -4
  77. package/dist/src/agent/goals/persistent-goal-service.js.map +1 -1
  78. package/dist/src/agent/inbound/inbound-loop.js +5 -1
  79. package/dist/src/agent/inbound/inbound-loop.js.map +1 -1
  80. package/dist/src/agent/inbound/turn-dispatcher.d.ts +2 -2
  81. package/dist/src/agent/inbound/turn-dispatcher.js +2 -2
  82. package/dist/src/agent/inbound/turn-dispatcher.js.map +1 -1
  83. package/dist/src/agent/mcp/bundle-mcp-materialize.d.ts +27 -0
  84. package/dist/src/agent/mcp/bundle-mcp-materialize.js +49 -1
  85. package/dist/src/agent/mcp/bundle-mcp-materialize.js.map +1 -1
  86. package/dist/src/agent/mcp/bundle-mcp-runtime.js +78 -3
  87. package/dist/src/agent/mcp/bundle-mcp-runtime.js.map +1 -1
  88. package/dist/src/agent/mcp/bundle-mcp-types.d.ts +19 -0
  89. package/dist/src/agent/service/btw-query.js +11 -3
  90. package/dist/src/agent/service/btw-query.js.map +1 -1
  91. package/dist/src/agent/service/parse-outbound-session-key.js +13 -8
  92. package/dist/src/agent/service/parse-outbound-session-key.js.map +1 -1
  93. package/dist/src/agent/service/process-direct-one-shot.d.ts +2 -2
  94. package/dist/src/agent/service/process-direct-one-shot.js +1 -1
  95. package/dist/src/agent/service/process-direct-one-shot.js.map +1 -1
  96. package/dist/src/agent/service/process-direct-streaming.d.ts +2 -2
  97. package/dist/src/agent/service/process-direct-streaming.js +1 -1
  98. package/dist/src/agent/service/process-direct-streaming.js.map +1 -1
  99. package/dist/src/agent/service.d.ts +1 -1
  100. package/dist/src/agent/service.js +9 -5
  101. package/dist/src/agent/service.js.map +1 -1
  102. package/dist/src/agent/service.types.d.ts +5 -0
  103. package/dist/src/agent/skills/filesystem-watcher.js +4 -3
  104. package/dist/src/agent/skills/filesystem-watcher.js.map +1 -1
  105. package/dist/src/agent/skills/index.js +2 -1
  106. package/dist/src/agent/skills/index.js.map +1 -1
  107. package/dist/src/agent/skills/skill-manage-ops.js +2 -1
  108. package/dist/src/agent/skills/skill-manage-ops.js.map +1 -1
  109. package/dist/src/agent/skills/types.d.ts +1 -1
  110. package/dist/src/agent/skills/watcher.js +2 -5
  111. package/dist/src/agent/skills/watcher.js.map +1 -1
  112. package/dist/src/agent/skills/workspace-skills-dir.d.ts +1 -0
  113. package/dist/src/agent/skills/workspace-skills-dir.js +11 -0
  114. package/dist/src/agent/skills/workspace-skills-dir.js.map +1 -0
  115. package/dist/src/agent/tool-manuals/browser.d.ts +1 -1
  116. package/dist/src/agent/tool-manuals/browser.js +62 -3
  117. package/dist/src/agent/tool-manuals/browser.js.map +1 -1
  118. package/dist/src/agent/tools/browser/tool/browser-use-tool.d.ts +4 -1
  119. package/dist/src/agent/tools/browser/tool/browser-use-tool.js +13 -4
  120. package/dist/src/agent/tools/browser/tool/browser-use-tool.js.map +1 -1
  121. package/dist/src/agent/tools/browser/tool/schemas.d.ts +0 -2
  122. package/dist/src/agent/tools/browser/tool/schemas.js +1 -2
  123. package/dist/src/agent/tools/browser/tool/schemas.js.map +1 -1
  124. package/dist/src/agent/tools/factory.js +1 -10
  125. package/dist/src/agent/tools/factory.js.map +1 -1
  126. package/dist/src/agent/tools/skill-manage-tool.js +1 -1
  127. package/dist/src/agent/tools/skill-manage-tool.js.map +1 -1
  128. package/dist/src/agent/workflow/subagent-runner.js +18 -1
  129. package/dist/src/agent/workflow/subagent-runner.js.map +1 -1
  130. package/dist/src/browser/actions/extension-registry-bridge.js +91 -7
  131. package/dist/src/browser/actions/extension-registry-bridge.js.map +1 -1
  132. package/dist/src/browser/actions/registry.js +383 -7
  133. package/dist/src/browser/actions/registry.js.map +1 -1
  134. package/dist/src/browser/actions/types.d.ts +11 -3
  135. package/dist/src/browser/index.js +2 -2
  136. package/dist/src/browser/pipeline/output.d.ts +18 -16
  137. package/dist/src/browser/pipeline/output.js +28 -20
  138. package/dist/src/browser/pipeline/output.js.map +1 -1
  139. package/dist/src/browser/pipeline/runner.d.ts +7 -14
  140. package/dist/src/browser/pipeline/runner.js +349 -102
  141. package/dist/src/browser/pipeline/runner.js.map +1 -1
  142. package/dist/src/browser/pipeline/schema.d.ts +1 -0
  143. package/dist/src/browser/pipeline/schema.js +35 -58
  144. package/dist/src/browser/pipeline/schema.js.map +1 -1
  145. package/dist/src/browser/pipeline/source.d.ts +2 -0
  146. package/dist/src/browser/pipeline/source.js +10 -1
  147. package/dist/src/browser/pipeline/source.js.map +1 -1
  148. package/dist/src/browser/pipeline/template.d.ts +10 -2
  149. package/dist/src/browser/pipeline/template.js +50 -8
  150. package/dist/src/browser/pipeline/template.js.map +1 -1
  151. package/dist/src/chat-commands/builtins/config.js +3 -5
  152. package/dist/src/chat-commands/builtins/config.js.map +1 -1
  153. package/dist/src/chat-commands/context.d.ts +5 -0
  154. package/dist/src/chat-commands/context.js +18 -9
  155. package/dist/src/chat-commands/context.js.map +1 -1
  156. package/dist/src/chat-commands/index.d.ts +1 -1
  157. package/dist/src/chat-commands/index.js +2 -2
  158. package/dist/src/chat-commands/index.js.map +1 -1
  159. package/dist/src/chat-commands/processor.js +22 -21
  160. package/dist/src/chat-commands/processor.js.map +1 -1
  161. package/dist/src/chat-commands/session-key.d.ts +3 -15
  162. package/dist/src/chat-commands/session-key.js +3 -49
  163. package/dist/src/chat-commands/session-key.js.map +1 -1
  164. package/dist/src/chat-commands/types.d.ts +4 -0
  165. package/dist/src/cli/commands/browser-cli-helpers.js +11 -5
  166. package/dist/src/cli/commands/browser-cli-helpers.js.map +1 -1
  167. package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
  168. package/dist/src/cli/commands/doctor/checks/session-integrity.js +8 -8
  169. package/dist/src/cli/commands/doctor/checks/session-integrity.js.map +1 -1
  170. package/dist/src/config/rules.js +5 -0
  171. package/dist/src/config/rules.js.map +1 -1
  172. package/dist/src/connectors/builtin-catalog.js +180 -1
  173. package/dist/src/connectors/builtin-catalog.js.map +1 -1
  174. package/dist/src/connectors/health.js +18 -9
  175. package/dist/src/connectors/health.js.map +1 -1
  176. package/dist/src/connectors/types.d.ts +19 -0
  177. package/dist/src/connectors/usage.js +12 -4
  178. package/dist/src/connectors/usage.js.map +1 -1
  179. package/dist/src/extensions/types/tui.d.ts +2 -0
  180. package/dist/src/gateway/heartbeat/service.js +13 -1
  181. package/dist/src/gateway/heartbeat/service.js.map +1 -1
  182. package/dist/src/gateway/hono/app.js +13 -4
  183. package/dist/src/gateway/hono/app.js.map +1 -1
  184. package/dist/src/gateway/hono/oauth-async.js +1 -1
  185. package/dist/src/gateway/hono/routes/agent-stream.js +6 -6
  186. package/dist/src/gateway/hono/routes/agent-stream.js.map +1 -1
  187. package/dist/src/gateway/hono/routes/config-patch/gateway.d.ts +1 -1
  188. package/dist/src/gateway/hono/routes/config-patch/gateway.js +6 -0
  189. package/dist/src/gateway/hono/routes/config-patch/gateway.js.map +1 -1
  190. package/dist/src/gateway/hono/routes/goals.js +16 -3
  191. package/dist/src/gateway/hono/routes/goals.js.map +1 -1
  192. package/dist/src/gateway/hono/routes/logs.js +1 -0
  193. package/dist/src/gateway/hono/routes/logs.js.map +1 -1
  194. package/dist/src/gateway/hono/routes/mcp.js +27 -8
  195. package/dist/src/gateway/hono/routes/mcp.js.map +1 -1
  196. package/dist/src/gateway/hono/routes/notes.js +14 -2
  197. package/dist/src/gateway/hono/routes/notes.js.map +1 -1
  198. package/dist/src/gateway/hono/routes/sessions.js +62 -4
  199. package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
  200. package/dist/src/gateway/hono/routes/workspace.js +3 -66
  201. package/dist/src/gateway/hono/routes/workspace.js.map +1 -1
  202. package/dist/src/gateway/hono/sse.d.ts +2 -2
  203. package/dist/src/gateway/hono/sse.js +7 -15
  204. package/dist/src/gateway/hono/sse.js.map +1 -1
  205. package/dist/src/gateway/resolve-webchat-session-key.d.ts +2 -6
  206. package/dist/src/gateway/resolve-webchat-session-key.js +5 -31
  207. package/dist/src/gateway/resolve-webchat-session-key.js.map +1 -1
  208. package/dist/src/gateway/service/agent-runner.d.ts +2 -3
  209. package/dist/src/gateway/service/agent-runner.js +9 -22
  210. package/dist/src/gateway/service/agent-runner.js.map +1 -1
  211. package/dist/src/gateway/service/run-gateway-agent.js +9 -7
  212. package/dist/src/gateway/service/run-gateway-agent.js.map +1 -1
  213. package/dist/src/gateway/service/sessions-api.d.ts +9 -0
  214. package/dist/src/gateway/service/sessions-api.js +11 -0
  215. package/dist/src/gateway/service/sessions-api.js.map +1 -1
  216. package/dist/src/gateway/service.d.ts +1 -1
  217. package/dist/src/gateway/service.js +19 -5
  218. package/dist/src/gateway/service.js.map +1 -1
  219. package/dist/src/gateway/workspace-file-search.d.ts +9 -0
  220. package/dist/src/gateway/workspace-file-search.js +76 -0
  221. package/dist/src/gateway/workspace-file-search.js.map +1 -0
  222. package/dist/src/goals/goal-notifications.d.ts +2 -0
  223. package/dist/src/goals/goal-notifications.js +16 -11
  224. package/dist/src/goals/goal-notifications.js.map +1 -1
  225. package/dist/src/goals/goal-service.js +1 -6
  226. package/dist/src/goals/goal-service.js.map +1 -1
  227. package/dist/src/project-trust/trust-store.js +2 -0
  228. package/dist/src/project-trust/trust-store.js.map +1 -1
  229. package/dist/src/session/index.d.ts +1 -1
  230. package/dist/src/session/index.js +2 -2
  231. package/dist/src/session/init-session-turn.js +6 -9
  232. package/dist/src/session/init-session-turn.js.map +1 -1
  233. package/dist/src/session/manager.d.ts +5 -1
  234. package/dist/src/session/manager.js +5 -2
  235. package/dist/src/session/manager.js.map +1 -1
  236. package/dist/src/session/reset-type.d.ts +0 -2
  237. package/dist/src/session/reset-type.js +3 -9
  238. package/dist/src/session/reset-type.js.map +1 -1
  239. package/dist/src/session/resolve-session.js +9 -20
  240. package/dist/src/session/resolve-session.js.map +1 -1
  241. package/dist/src/session/store.d.ts +11 -3
  242. package/dist/src/session/store.js +29 -13
  243. package/dist/src/session/store.js.map +1 -1
  244. package/dist/src/session/types.d.ts +2 -4
  245. package/dist/src/session/types.js.map +1 -1
  246. package/dist/src/storage/sqlite/index.d.ts +2 -2
  247. package/dist/src/storage/sqlite/index.js +3 -3
  248. package/dist/src/storage/sqlite/migrations/010_session_id_identity.sql +30 -0
  249. package/dist/src/storage/sqlite/migrations/runner.d.ts +2 -2
  250. package/dist/src/storage/sqlite/migrations/runner.js +4 -4
  251. package/dist/src/storage/sqlite/migrations/runner.js.map +1 -1
  252. package/dist/src/storage/sqlite/migrations/runner.ts +2 -2
  253. package/dist/src/storage/sqlite/row-mappers.d.ts +4 -4
  254. package/dist/src/storage/sqlite/row-mappers.js +11 -14
  255. package/dist/src/storage/sqlite/row-mappers.js.map +1 -1
  256. package/dist/src/storage/sqlite/schema.js +3 -3
  257. package/dist/src/storage/sqlite/schema.js.map +1 -1
  258. package/dist/src/storage/sqlite/schema.sql +37 -10
  259. package/dist/src/storage/sqlite/session-metadata.d.ts +2 -7
  260. package/dist/src/storage/sqlite/session-metadata.js +18 -72
  261. package/dist/src/storage/sqlite/session-metadata.js.map +1 -1
  262. package/dist/src/storage/sqlite/session-repository.d.ts +6 -5
  263. package/dist/src/storage/sqlite/session-repository.js +34 -40
  264. package/dist/src/storage/sqlite/session-repository.js.map +1 -1
  265. package/dist/src/storage/sqlite/transcript-repository.d.ts +2 -2
  266. package/dist/src/storage/sqlite/transcript-repository.js +52 -52
  267. package/dist/src/storage/sqlite/transcript-repository.js.map +1 -1
  268. package/dist/src/tui/backends/embedded-backend.d.ts +11 -4
  269. package/dist/src/tui/backends/embedded-backend.js +27 -1
  270. package/dist/src/tui/backends/embedded-backend.js.map +1 -1
  271. package/dist/src/tui/backends/gateway-sse-backend.d.ts +4 -1
  272. package/dist/src/tui/backends/gateway-sse-backend.js +21 -2
  273. package/dist/src/tui/backends/gateway-sse-backend.js.map +1 -1
  274. package/dist/src/tui/extension-host/autocomplete.js +13 -10
  275. package/dist/src/tui/extension-host/autocomplete.js.map +1 -1
  276. package/dist/src/tui/extension-host/runtime.d.ts +7 -0
  277. package/dist/src/tui/extension-host/runtime.js +6 -4
  278. package/dist/src/tui/extension-host/runtime.js.map +1 -1
  279. package/dist/src/tui/tui-backend.d.ts +9 -0
  280. package/dist/src/tui/tui-commands.js +85 -6
  281. package/dist/src/tui/tui-commands.js.map +1 -1
  282. package/dist/src/tui/tui-file-autocomplete.d.ts +5 -0
  283. package/dist/src/tui/tui-file-autocomplete.js +26 -0
  284. package/dist/src/tui/tui-file-autocomplete.js.map +1 -0
  285. package/dist/src/tui/tui-picker-overlay.js +1 -1
  286. package/dist/src/tui/tui.js +29 -11
  287. package/dist/src/tui/tui.js.map +1 -1
  288. package/dist/src/utils/logger/context.d.ts +2 -2
  289. package/dist/src/utils/logger/context.js +2 -1
  290. package/dist/src/utils/logger/context.js.map +1 -1
  291. package/dist/src/utils/logger/log-store.js +1 -0
  292. package/dist/src/utils/logger/log-store.js.map +1 -1
  293. package/dist/src/utils/logger/pino-record.js +3 -0
  294. package/dist/src/utils/logger/pino-record.js.map +1 -1
  295. package/dist/src/utils/logger/types.d.ts +5 -0
  296. package/dist/src/utils/logger/types.js.map +1 -1
  297. package/dist/src/workflows/service/workflow-session-bridge.js +13 -1
  298. package/dist/src/workflows/service/workflow-session-bridge.js.map +1 -1
  299. package/package.json +1 -1
  300. package/dist/gateway/static/root/assets/agent-browser-settings-page-rGbKc2Hh.js +0 -6
  301. package/dist/gateway/static/root/assets/agent-defaults-tabbed-page-CtwVbjOA.js +0 -1
  302. package/dist/gateway/static/root/assets/agents-CCBmGcwc.js +0 -71
  303. package/dist/gateway/static/root/assets/apps-page-xwCsJS15.js +0 -1
  304. package/dist/gateway/static/root/assets/channels-settings-C044DcYk.js +0 -1
  305. package/dist/gateway/static/root/assets/connectors-settings-8VEWUOTj.js +0 -2
  306. package/dist/gateway/static/root/assets/cron-page-DqmNGNOy.js +0 -3
  307. package/dist/gateway/static/root/assets/dreaming-settings-6UZE1FSK.js +0 -1
  308. package/dist/gateway/static/root/assets/goal-detail-page-bv0VRDaf.js +0 -1
  309. package/dist/gateway/static/root/assets/goals-page-1Ywnt4Cb.js +0 -2
  310. package/dist/gateway/static/root/assets/index-Cxm6xsps.js +0 -122
  311. package/dist/gateway/static/root/assets/index-b_R4tKqt.css +0 -1
  312. package/dist/gateway/static/root/assets/logs-page-B_wrxhee.js +0 -2
  313. package/dist/gateway/static/root/assets/messages-CkT-lcgP.js +0 -3
  314. package/dist/gateway/static/root/assets/models-hub-panel-D0JDSbNu.js +0 -2
  315. package/dist/gateway/static/root/assets/note-detail-page-DpFV-EO6.js +0 -2
  316. package/dist/gateway/static/root/assets/notes-page-CdkjTch8.js +0 -1
  317. package/dist/gateway/static/root/assets/remote-access-hub-DSYVAi96.js +0 -1
  318. package/dist/gateway/static/root/assets/sessions-page-CenkcVjF.js +0 -1
  319. package/dist/gateway/static/root/assets/settings-advanced-gate-D9sva86V.js +0 -1
  320. package/dist/gateway/static/root/assets/settings-page-DL35U0x0.js +0 -1
  321. package/dist/gateway/static/root/assets/setup-status-panel-Ctwbp9Er.js +0 -1
  322. package/dist/gateway/static/root/assets/shares-settings-DIM3JNsr.js +0 -1
  323. package/dist/gateway/static/root/assets/skill-reload-api-DAlNaNzL.js +0 -1
  324. package/dist/gateway/static/root/assets/skills-page-CLrHVzAt.js +0 -2
  325. package/dist/gateway/static/root/assets/voice-api-key-field-vYAO71Gn.js +0 -1
  326. package/dist/gateway/static/root/assets/workflow-page.utils-sgHjOXOm.js +0 -1
  327. package/dist/gateway/static/root/assets/workflows-page-DYyOTmFx.js +0 -18
@@ -3,6 +3,7 @@ import { init_paths, resolveSkillsDir } from "../../config/paths.js";
3
3
  import { createLogger } from "../../utils/logger/index.js";
4
4
  import { init_logger } from "../../utils/logger.js";
5
5
  import { createSkillConfigManager } from "./config.js";
6
+ import { resolveWorkspaceSkillsDir } from "./workspace-skills-dir.js";
6
7
  import { isManagedSkillTransientDirName } from "./managed-store.js";
7
8
  import path from "node:path";
8
9
  import chokidar from "chokidar";
@@ -40,11 +41,11 @@ function isSameOrInside(parent, child) {
40
41
  return rel === "" || !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
41
42
  }
42
43
  function isWorkspaceSkillPath(workspaceRoot, candidate) {
43
- const skillsRoot = path.join(workspaceRoot, "skills");
44
- return candidate === workspaceRoot || isSameOrInside(skillsRoot, candidate);
44
+ const skillsRoot = resolveWorkspaceSkillsDir(workspaceRoot);
45
+ return candidate === workspaceRoot || isSameOrInside(skillsRoot, candidate) || isSameOrInside(candidate, skillsRoot);
45
46
  }
46
47
  function isWorkspaceSkillsRoot(workspaceRoot, candidate) {
47
- return candidate === path.join(workspaceRoot, "skills");
48
+ return candidate === resolveWorkspaceSkillsDir(workspaceRoot);
48
49
  }
49
50
  function isSkillMdPath(candidate) {
50
51
  return path.basename(candidate).toLowerCase() === "skill.md";
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-watcher.js","names":[],"sources":["../../../../src/agent/skills/filesystem-watcher.ts"],"sourcesContent":["import path from 'node:path';\n\nimport chokidar, { type FSWatcher } from 'chokidar';\n\nimport { resolveSkillsDir, resolveStateDir } from '../../config/paths.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { createSkillConfigManager } from './config.js';\nimport { isManagedSkillTransientDirName } from './managed-store.js';\n\nconst log = createLogger('SkillFilesystemWatcher');\n\nexport interface SkillFilesystemWatcherOptions {\n onChange: (event: { reason: 'watch'; changedPath?: string }) => void;\n}\n\ntype WatchTarget = {\n key: string;\n root: string;\n kind: 'global' | 'workspace';\n watcher: FSWatcher;\n timer?: NodeJS.Timeout;\n pendingPath?: string;\n};\n\nconst DEFAULT_DEBOUNCE_MS = 1000;\nconst WATCH_DEPTH = 8;\nconst IGNORED_SEGMENTS = new Set([\n '.git',\n 'node_modules',\n 'dist',\n 'build',\n '.next',\n '.nuxt',\n '.venv',\n 'venv',\n '__pycache__',\n '.cache',\n '.mypy_cache',\n '.pytest_cache',\n]);\n\nfunction normalizePath(p: string): string {\n return path.resolve(p);\n}\n\nfunction pathSegments(p: string): string[] {\n return p.split(/[\\\\/]+/).filter(Boolean);\n}\n\nfunction hasIgnoredSegment(p: string): boolean {\n return pathSegments(p).some(\n (segment) => IGNORED_SEGMENTS.has(segment) || isManagedSkillTransientDirName(segment),\n );\n}\n\nfunction isSameOrInside(parent: string, child: string): boolean {\n const rel = path.relative(parent, child);\n return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));\n}\n\nfunction isWorkspaceSkillPath(workspaceRoot: string, candidate: string): boolean {\n const skillsRoot = path.join(workspaceRoot, 'skills');\n return candidate === workspaceRoot || isSameOrInside(skillsRoot, candidate);\n}\n\nfunction isWorkspaceSkillsRoot(workspaceRoot: string, candidate: string): boolean {\n return candidate === path.join(workspaceRoot, 'skills');\n}\n\nfunction isSkillMdPath(candidate: string): boolean {\n return path.basename(candidate).toLowerCase() === 'skill.md';\n}\n\nfunction readWatcherConfig(): { enabled: boolean; debounceMs: number } {\n const cfg = createSkillConfigManager(resolveStateDir()).load();\n const rawDebounce = cfg.load?.watchDebounceMs;\n return {\n enabled: cfg.load?.watch !== false,\n debounceMs:\n typeof rawDebounce === 'number' && Number.isFinite(rawDebounce)\n ? Math.max(0, rawDebounce)\n : DEFAULT_DEBOUNCE_MS,\n };\n}\n\nexport class SkillFilesystemWatcher {\n private readonly targets = new Map<string, WatchTarget>();\n private readonly onChange: SkillFilesystemWatcherOptions['onChange'];\n private disposed = false;\n\n constructor(options: SkillFilesystemWatcherOptions) {\n this.onChange = options.onChange;\n this.watchGlobalSkillsDir();\n }\n\n watchGlobalSkillsDir(): void {\n this.watchRoot(resolveSkillsDir(), 'global');\n }\n\n watchWorkspace(workspaceDir: string): void {\n const resolved = normalizePath(workspaceDir);\n if (!resolved) return;\n this.watchRoot(resolved, 'workspace');\n }\n\n refreshPrimaryWorkspace(workspaceDir: string): void {\n this.watchGlobalSkillsDir();\n this.watchWorkspace(workspaceDir);\n }\n\n dispose(): void {\n this.disposed = true;\n for (const target of this.targets.values()) {\n if (target.timer) clearTimeout(target.timer);\n void target.watcher.close().catch((err) => {\n log.warn({ err, root: target.root }, 'Skill watcher close failed');\n });\n }\n this.targets.clear();\n }\n\n private watchRoot(root: string, kind: WatchTarget['kind']): void {\n if (this.disposed) return;\n const { enabled, debounceMs } = readWatcherConfig();\n const resolvedRoot = normalizePath(root);\n const key = `${kind}:${resolvedRoot}`;\n\n if (!enabled) {\n this.unwatch(key);\n return;\n }\n\n const existing = this.targets.get(key);\n if (existing) return;\n\n const ignored = (watchPath: string, stats?: { isDirectory?: () => boolean }) => {\n const candidate = normalizePath(watchPath);\n if (candidate === resolvedRoot) return false;\n if (hasIgnoredSegment(candidate)) return true;\n\n if (kind === 'workspace' && !isWorkspaceSkillPath(resolvedRoot, candidate)) {\n return true;\n }\n\n if (kind === 'workspace' && isWorkspaceSkillsRoot(resolvedRoot, candidate)) return false;\n if (!stats) return false;\n if (stats?.isDirectory?.()) return false;\n return !isSkillMdPath(candidate);\n };\n\n const watcher = chokidar.watch(resolvedRoot, {\n ignoreInitial: true,\n followSymlinks: false,\n depth: WATCH_DEPTH,\n awaitWriteFinish: {\n stabilityThreshold: debounceMs,\n pollInterval: 100,\n },\n ignored,\n });\n\n const target: WatchTarget = { key, root: resolvedRoot, kind, watcher };\n const schedule = (changedPath?: string) => this.scheduleRefresh(target, changedPath);\n\n watcher.on('addDir', schedule);\n watcher.on('add', schedule);\n watcher.on('change', schedule);\n watcher.on('unlink', schedule);\n watcher.on('unlinkDir', schedule);\n watcher.on('error', (err) => {\n log.warn({ err, root: resolvedRoot, kind }, 'Skill watcher error');\n });\n\n this.targets.set(key, target);\n log.info({ root: resolvedRoot, kind }, 'Watching skill filesystem');\n }\n\n private unwatch(key: string): void {\n const target = this.targets.get(key);\n if (!target) return;\n if (target.timer) clearTimeout(target.timer);\n void target.watcher.close().catch((err) => {\n log.warn({ err, root: target.root }, 'Skill watcher close failed');\n });\n this.targets.delete(key);\n }\n\n private scheduleRefresh(target: WatchTarget, changedPath?: string): void {\n const { enabled, debounceMs } = readWatcherConfig();\n if (!enabled) {\n this.unwatch(target.key);\n return;\n }\n\n target.pendingPath = changedPath ?? target.pendingPath;\n if (target.timer) clearTimeout(target.timer);\n target.timer = setTimeout(() => {\n target.timer = undefined;\n const pendingPath = target.pendingPath;\n target.pendingPath = undefined;\n this.onChange({ reason: 'watch', changedPath: pendingPath });\n }, debounceMs);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"filesystem-watcher.js","names":[],"sources":["../../../../src/agent/skills/filesystem-watcher.ts"],"sourcesContent":["import path from 'node:path';\n\nimport chokidar, { type FSWatcher } from 'chokidar';\n\nimport { resolveSkillsDir, resolveStateDir } from '../../config/paths.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { createSkillConfigManager } from './config.js';\nimport { isManagedSkillTransientDirName } from './managed-store.js';\nimport { resolveWorkspaceSkillsDir } from './workspace-skills-dir.js';\n\nconst log = createLogger('SkillFilesystemWatcher');\n\nexport interface SkillFilesystemWatcherOptions {\n onChange: (event: { reason: 'watch'; changedPath?: string }) => void;\n}\n\ntype WatchTarget = {\n key: string;\n root: string;\n kind: 'global' | 'workspace';\n watcher: FSWatcher;\n timer?: NodeJS.Timeout;\n pendingPath?: string;\n};\n\nconst DEFAULT_DEBOUNCE_MS = 1000;\nconst WATCH_DEPTH = 8;\nconst IGNORED_SEGMENTS = new Set([\n '.git',\n 'node_modules',\n 'dist',\n 'build',\n '.next',\n '.nuxt',\n '.venv',\n 'venv',\n '__pycache__',\n '.cache',\n '.mypy_cache',\n '.pytest_cache',\n]);\n\nfunction normalizePath(p: string): string {\n return path.resolve(p);\n}\n\nfunction pathSegments(p: string): string[] {\n return p.split(/[\\\\/]+/).filter(Boolean);\n}\n\nfunction hasIgnoredSegment(p: string): boolean {\n return pathSegments(p).some(\n (segment) => IGNORED_SEGMENTS.has(segment) || isManagedSkillTransientDirName(segment),\n );\n}\n\nfunction isSameOrInside(parent: string, child: string): boolean {\n const rel = path.relative(parent, child);\n return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));\n}\n\nfunction isWorkspaceSkillPath(workspaceRoot: string, candidate: string): boolean {\n const skillsRoot = resolveWorkspaceSkillsDir(workspaceRoot);\n return (\n candidate === workspaceRoot ||\n isSameOrInside(skillsRoot, candidate) ||\n isSameOrInside(candidate, skillsRoot)\n );\n}\n\nfunction isWorkspaceSkillsRoot(workspaceRoot: string, candidate: string): boolean {\n return candidate === resolveWorkspaceSkillsDir(workspaceRoot);\n}\n\nfunction isSkillMdPath(candidate: string): boolean {\n return path.basename(candidate).toLowerCase() === 'skill.md';\n}\n\nfunction readWatcherConfig(): { enabled: boolean; debounceMs: number } {\n const cfg = createSkillConfigManager(resolveStateDir()).load();\n const rawDebounce = cfg.load?.watchDebounceMs;\n return {\n enabled: cfg.load?.watch !== false,\n debounceMs:\n typeof rawDebounce === 'number' && Number.isFinite(rawDebounce)\n ? Math.max(0, rawDebounce)\n : DEFAULT_DEBOUNCE_MS,\n };\n}\n\nexport class SkillFilesystemWatcher {\n private readonly targets = new Map<string, WatchTarget>();\n private readonly onChange: SkillFilesystemWatcherOptions['onChange'];\n private disposed = false;\n\n constructor(options: SkillFilesystemWatcherOptions) {\n this.onChange = options.onChange;\n this.watchGlobalSkillsDir();\n }\n\n watchGlobalSkillsDir(): void {\n this.watchRoot(resolveSkillsDir(), 'global');\n }\n\n watchWorkspace(workspaceDir: string): void {\n const resolved = normalizePath(workspaceDir);\n if (!resolved) return;\n this.watchRoot(resolved, 'workspace');\n }\n\n refreshPrimaryWorkspace(workspaceDir: string): void {\n this.watchGlobalSkillsDir();\n this.watchWorkspace(workspaceDir);\n }\n\n dispose(): void {\n this.disposed = true;\n for (const target of this.targets.values()) {\n if (target.timer) clearTimeout(target.timer);\n void target.watcher.close().catch((err) => {\n log.warn({ err, root: target.root }, 'Skill watcher close failed');\n });\n }\n this.targets.clear();\n }\n\n private watchRoot(root: string, kind: WatchTarget['kind']): void {\n if (this.disposed) return;\n const { enabled, debounceMs } = readWatcherConfig();\n const resolvedRoot = normalizePath(root);\n const key = `${kind}:${resolvedRoot}`;\n\n if (!enabled) {\n this.unwatch(key);\n return;\n }\n\n const existing = this.targets.get(key);\n if (existing) return;\n\n const ignored = (watchPath: string, stats?: { isDirectory?: () => boolean }) => {\n const candidate = normalizePath(watchPath);\n if (candidate === resolvedRoot) return false;\n if (hasIgnoredSegment(candidate)) return true;\n\n if (kind === 'workspace' && !isWorkspaceSkillPath(resolvedRoot, candidate)) {\n return true;\n }\n\n if (kind === 'workspace' && isWorkspaceSkillsRoot(resolvedRoot, candidate)) return false;\n if (!stats) return false;\n if (stats?.isDirectory?.()) return false;\n return !isSkillMdPath(candidate);\n };\n\n const watcher = chokidar.watch(resolvedRoot, {\n ignoreInitial: true,\n followSymlinks: false,\n depth: WATCH_DEPTH,\n awaitWriteFinish: {\n stabilityThreshold: debounceMs,\n pollInterval: 100,\n },\n ignored,\n });\n\n const target: WatchTarget = { key, root: resolvedRoot, kind, watcher };\n const schedule = (changedPath?: string) => this.scheduleRefresh(target, changedPath);\n\n watcher.on('addDir', schedule);\n watcher.on('add', schedule);\n watcher.on('change', schedule);\n watcher.on('unlink', schedule);\n watcher.on('unlinkDir', schedule);\n watcher.on('error', (err) => {\n log.warn({ err, root: resolvedRoot, kind }, 'Skill watcher error');\n });\n\n this.targets.set(key, target);\n log.info({ root: resolvedRoot, kind }, 'Watching skill filesystem');\n }\n\n private unwatch(key: string): void {\n const target = this.targets.get(key);\n if (!target) return;\n if (target.timer) clearTimeout(target.timer);\n void target.watcher.close().catch((err) => {\n log.warn({ err, root: target.root }, 'Skill watcher close failed');\n });\n this.targets.delete(key);\n }\n\n private scheduleRefresh(target: WatchTarget, changedPath?: string): void {\n const { enabled, debounceMs } = readWatcherConfig();\n if (!enabled) {\n this.unwatch(target.key);\n return;\n }\n\n target.pendingPath = changedPath ?? target.pendingPath;\n if (target.timer) clearTimeout(target.timer);\n target.timer = setTimeout(() => {\n target.timer = undefined;\n const pendingPath = target.pendingPath;\n target.pendingPath = undefined;\n this.onChange({ reason: 'watch', changedPath: pendingPath });\n }, debounceMs);\n }\n}\n"],"mappings":"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"}
@@ -6,6 +6,7 @@ import { parseFrontmatter } from "../../markdown/frontmatter.js";
6
6
  import { findInstallSpec, getDefaultInstallerPreferences, hasBinary, installSkill } from "./installer.js";
7
7
  import { applySkillEnvOverrides, createSkillConfigManager, getSkillEnvironment, isSkillEnabled, resolveSkillConfig, validateSkillConfig } from "./config.js";
8
8
  import { collectSkillInstallWarnings, formatScanSummary, scanSkillDirectory } from "./scanner.js";
9
+ import { resolveWorkspaceSkillsDir } from "./workspace-skills-dir.js";
9
10
  import { parseSkillToolConditions } from "./skill-tool-gating.js";
10
11
  import { parseRequiredEnvVarNames } from "./required-env-vars.js";
11
12
  import { formatSkillsForPrompt } from "./format-skills-prompt.js";
@@ -210,7 +211,7 @@ function loadSkills(options) {
210
211
  }
211
212
  }
212
213
  if (workspaceDir) {
213
- const discovered = discoverSkills(join(workspaceDir, "skills"), "workspace");
214
+ const discovered = discoverSkills(resolveWorkspaceSkillsDir(workspaceDir), "workspace");
214
215
  diagnostics.push(...discovered.diagnostics);
215
216
  const workspaceSkills = discovered.skills;
216
217
  for (const skill of workspaceSkills) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/agent/skills/index.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'fs';\nimport { basename, dirname, join, relative, sep } from 'path';\nimport { parseFrontmatter } from '../../markdown/frontmatter.js';\nimport { resolveStateDir } from '../../config/paths.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { createSkillConfigManager, isSkillEnabled } from './config.js';\nimport { formatSkillsForPrompt } from './format-skills-prompt.js';\nimport { parseRequiredEnvVarNames } from './required-env-vars.js';\nimport { parseSkillMetadata } from './parse-skill-metadata.js';\nimport { parseSkillToolConditions } from './skill-tool-gating.js';\nimport type { Skill, SkillDiagnostic, LoadSkillsResult, SkillConfig, SkillsConfig } from './types.js';\n\nconst log = createLogger('SkillLoader');\n\nconst IGNORE_FILES = ['.gitignore', '.ignore', '.fdignore'];\n\nfunction toPosixPath(p: string): string {\n return p.split(sep).join('/');\n}\n\nfunction prefixIgnorePattern(line: string, prefix: string): string | null {\n const trimmed = line.trim();\n if (!trimmed) return null;\n if (trimmed.startsWith('#') && !trimmed.startsWith('\\\\#')) return null;\n\n let pattern = line;\n let negated = false;\n\n if (pattern.startsWith('!')) {\n negated = true;\n pattern = pattern.slice(1);\n } else if (pattern.startsWith('\\\\!')) {\n pattern = pattern.slice(1);\n }\n\n if (pattern.startsWith('/')) {\n pattern = pattern.slice(1);\n }\n\n const prefixed = prefix ? `${prefix}${pattern}` : pattern;\n return negated ? `!${prefixed}` : prefixed;\n}\n\nfunction loadIgnoreRules(dir: string, rootDir: string): Set<string> {\n const ignoredPaths = new Set<string>();\n const relativeDir = relative(rootDir, dir);\n const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : '';\n\n for (const filename of IGNORE_FILES) {\n const ignorePath = join(dir, filename);\n if (!existsSync(ignorePath)) continue;\n\n try {\n const content = readFileSync(ignorePath, 'utf-8');\n for (const line of content.split(/\\r?\\n/)) {\n const pattern = prefixIgnorePattern(line, prefix);\n if (pattern) {\n const fullPattern = pattern.startsWith('!')\n ? `!${prefix}${pattern.slice(1)}`\n : `${prefix}${pattern}`;\n ignoredPaths.add(fullPattern);\n }\n }\n } catch {}\n }\n\n return ignoredPaths;\n}\n\nfunction shouldIgnore(path: string, ignoredPaths: Set<string>): boolean {\n for (const pattern of ignoredPaths) {\n if (pattern.startsWith('!')) {\n const positive = pattern.slice(1);\n if (path === positive || path.startsWith(`${positive}/`)) {\n return false;\n }\n } else {\n if (path === pattern || path.startsWith(`${pattern}/`)) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction discoverSkills(\n dir: string,\n source: 'builtin' | 'workspace' | 'global',\n): { skills: Skill[]; diagnostics: SkillDiagnostic[] } {\n const skills: Skill[] = [];\n const diagnostics: SkillDiagnostic[] = [];\n if (!existsSync(dir)) return { skills, diagnostics };\n\n function scan(currentDir: string, currentIgnoredPaths: Set<string>) {\n try {\n const entries = readdirSync(currentDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;\n\n const fullPath = join(currentDir, entry.name);\n const relPath = toPosixPath(relative(dir, fullPath));\n\n if (shouldIgnore(relPath, currentIgnoredPaths)) continue;\n\n if (entry.isDirectory()) {\n const skillMdPath = join(fullPath, 'SKILL.md');\n const skillRelPath = `${relPath}/`;\n\n const subIgnoredPaths = new Set(currentIgnoredPaths);\n const subIgnoreFile = join(fullPath, '.gitignore');\n if (existsSync(subIgnoreFile)) {\n const subRules = loadIgnoreRules(fullPath, dir);\n for (const rule of subRules) {\n subIgnoredPaths.add(`${skillRelPath}${rule}`);\n }\n }\n\n if (existsSync(skillMdPath) && !shouldIgnore(skillRelPath, currentIgnoredPaths)) {\n const { skill, diagnostic } = loadSkillFromFile(skillMdPath, source, dir);\n if (diagnostic) diagnostics.push(diagnostic);\n if (skill) skills.push(skill);\n }\n\n scan(fullPath, subIgnoredPaths);\n }\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n diagnostics.push({\n type: 'error',\n message: `Failed to scan skills directory: ${message}`,\n path: currentDir,\n });\n }\n }\n\n scan(dir, loadIgnoreRules(dir, dir));\n return { skills, diagnostics };\n}\n\nfunction deriveDescriptionFromMarkdown(content: string): string | undefined {\n let fallbackHeading = '';\n\n for (const rawLine of content.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (!line) continue;\n\n const heading = line.match(/^#{1,6}\\s+(.+)$/);\n if (heading) {\n fallbackHeading ||= heading[1]?.trim() ?? '';\n continue;\n }\n\n return line\n .replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, '$1')\n .replace(/[*_`~]/g, '')\n .trim() || undefined;\n }\n\n return fallbackHeading || undefined;\n}\n\nfunction loadSkillFromFile(\n filePath: string,\n source: 'builtin' | 'workspace' | 'global',\n rootDir?: string,\n): { skill: Skill | null; diagnostic?: SkillDiagnostic } {\n try {\n const rawContent = readFileSync(filePath, 'utf-8');\n const { frontmatter, content } = parseFrontmatter(rawContent);\n const skillDir = dirname(filePath);\n const parentDirName = basename(skillDir);\n\n const name = (frontmatter.name as string | undefined) || parentDirName;\n const description =\n (frontmatter.description as string | undefined)?.trim() || deriveDescriptionFromMarkdown(content);\n if (!description?.trim()) {\n return {\n skill: null,\n diagnostic: {\n type: 'error',\n skillName: name,\n message: `Skill \"${name}\" is missing a description`,\n path: filePath,\n },\n };\n }\n\n // Derive category from directory path: skills/creative/algorithmic-art → 'creative'\n // Only assign a category when the skill is nested at least two levels below rootDir.\n let category: string | undefined;\n if (rootDir) {\n const relDir = toPosixPath(relative(rootDir, skillDir));\n const segments = relDir.split('/');\n if (segments.length > 1) {\n category = segments[0];\n }\n }\n\n const metadata = parseSkillMetadata(frontmatter);\n metadata.name ||= name;\n metadata.description ||= description.trim();\n const toolConditions = parseSkillToolConditions(frontmatter);\n const requiredEnvVarNames = parseRequiredEnvVarNames(frontmatter);\n\n return {\n skill: {\n name,\n description: description.trim(),\n category,\n filePath,\n baseDir: skillDir,\n source,\n disableModelInvocation: frontmatter['disable-model-invocation'] === true,\n metadata,\n toolConditions,\n requiredEnvVarNames: requiredEnvVarNames.length > 0 ? requiredEnvVarNames : undefined,\n content,\n },\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n skill: null,\n diagnostic: {\n type: 'error',\n message: `Failed to load skill file: ${message}`,\n path: filePath,\n },\n };\n }\n}\n\nexport function loadSkills(options: {\n workspaceDir?: string;\n globalDir?: string;\n builtinDir?: string;\n extraDirs?: string[];\n}): LoadSkillsResult {\n const { workspaceDir, builtinDir, extraDirs = [] } = options;\n\n const skillMap = new Map<string, Skill>();\n const diagnostics: SkillDiagnostic[] = [];\n\n if (builtinDir) {\n const discovered = discoverSkills(builtinDir, 'builtin');\n diagnostics.push(...discovered.diagnostics);\n for (const skill of discovered.skills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${existing.source} overrides ${skill.source}`,\n path: skill.filePath,\n });\n } else {\n skillMap.set(skill.name, skill);\n }\n }\n }\n\n const globalDirs = [\n options.globalDir,\n join(resolveStateDir(), 'skills'),\n ].filter((d): d is string => !!d && existsSync(d));\n\n for (const dir of globalDirs) {\n const discovered = discoverSkills(dir, 'global');\n diagnostics.push(...discovered.diagnostics);\n for (const skill of discovered.skills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${skill.source} overrides ${existing.source}`,\n path: skill.filePath,\n });\n }\n // Global must win over bundled when names match (Workspace > Global > Bundled).\n skillMap.set(skill.name, skill);\n }\n }\n\n if (workspaceDir) {\n const discovered = discoverSkills(join(workspaceDir, 'skills'), 'workspace');\n diagnostics.push(...discovered.diagnostics);\n const workspaceSkills = discovered.skills;\n for (const skill of workspaceSkills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${skill.source} overrides ${existing.source}`,\n path: skill.filePath,\n });\n }\n skillMap.set(skill.name, skill);\n }\n }\n\n // Scan extra directories\n for (const extraDir of extraDirs) {\n if (existsSync(extraDir)) {\n const discovered = discoverSkills(extraDir, 'global');\n diagnostics.push(...discovered.diagnostics);\n for (const skill of discovered.skills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${existing.source} overrides ${skill.source}`,\n path: skill.filePath,\n });\n } else {\n skillMap.set(skill.name, skill);\n }\n }\n }\n }\n\n const skillsConfig = createSkillConfigManager(resolveStateDir()).load();\n const merged = Array.from(skillMap.values());\n\n return {\n skills: merged,\n prompt: formatSkillsForPrompt(merged, skillsConfig),\n diagnostics,\n };\n}\n\nexport interface SkillLoader {\n init: (workspace: string, builtin: string | null) => LoadSkillsResult;\n load: () => LoadSkillsResult;\n reload: () => LoadSkillsResult;\n /** Recompute `<available_skills>` from disk-loaded skills and current ~/.xopc/skills.json (no filesystem rescan). */\n refreshPromptFromConfig: () => void;\n getSkills: () => Skill[];\n getPrompt: () => string;\n getDiagnostics: () => SkillDiagnostic[];\n getLastLoadTime: () => number;\n getSkillByName: (name: string) => Skill | undefined;\n getEnabledSkills: (config?: Record<string, SkillConfig>) => Skill[];\n}\n\nexport function createSkillLoader(): SkillLoader {\n let cachedSkills: Skill[] = [];\n let cachedPrompt: string = '';\n let cachedDiagnostics: SkillDiagnostic[] = [];\n let lastLoadTime = 0;\n let workspaceDir: string | undefined;\n let builtinDir: string | undefined;\n let extraDirs: string[] = [];\n\n function updateCache(result: LoadSkillsResult): LoadSkillsResult {\n cachedSkills = result.skills;\n cachedPrompt = result.prompt;\n cachedDiagnostics = result.diagnostics;\n lastLoadTime = Date.now();\n return result;\n }\n\n return {\n init: (workspace: string, builtin: string | null) => {\n workspaceDir = workspace;\n builtinDir = builtin || undefined;\n return updateCache(loadSkills({ workspaceDir, builtinDir, extraDirs }));\n },\n \n load: () => {\n return updateCache(loadSkills({ workspaceDir, builtinDir, extraDirs }));\n },\n \n reload: () => {\n log.info('Reloading skills');\n return updateCache(loadSkills({ workspaceDir, builtinDir, extraDirs }));\n },\n\n refreshPromptFromConfig: () => {\n const skillsConfig = createSkillConfigManager(resolveStateDir()).load();\n cachedPrompt = formatSkillsForPrompt(cachedSkills, skillsConfig);\n lastLoadTime = Date.now();\n },\n\n getSkills: () => cachedSkills,\n getPrompt: () => cachedPrompt,\n getDiagnostics: () => cachedDiagnostics,\n getLastLoadTime: () => lastLoadTime,\n \n getSkillByName: (name: string) => {\n return cachedSkills.find(s => s.name === name);\n },\n \n getEnabledSkills: (entries?: Record<string, SkillConfig>) => {\n const skillsConfig: SkillsConfig =\n entries !== undefined ? { entries } : createSkillConfigManager(resolveStateDir()).load();\n return cachedSkills.filter(\n (skill) => !skill.disableModelInvocation && isSkillEnabled(skill, skillsConfig),\n );\n },\n };\n}\n\n// `SkillManager` + sibling types removed from this barrel — `skill-manager.ts`\n// imports `createSkillLoader` from THIS file, so re-exporting it back here\n// formed a circular cycle. External callers should import directly from\n// `./skill-manager.js`.\n\n// Re-export types for convenience\nexport type { \n Skill, \n SkillMetadata, \n SkillConfig,\n SkillInstallSpec,\n SkillInstallResult,\n SkillInstallRequest,\n LoadSkillsResult,\n // Note: SkillDiagnostic is re-exported from skill-manager.ts above\n SkillSnapshot,\n} from './types.js';\n\n// Re-export installer\nexport { \n installSkill, \n findInstallSpec,\n hasBinary,\n getDefaultInstallerPreferences,\n type InstallerPreferences,\n type InstallContext,\n} from './installer.js';\n\n// Re-export scanner\nexport {\n scanSkillDirectory,\n formatScanSummary,\n collectSkillInstallWarnings,\n type ScanSummary,\n type SecurityFinding,\n type Severity,\n} from './scanner.js';\n\n// Re-export config manager\nexport {\n resolveSkillConfig,\n applySkillEnvOverrides,\n getSkillEnvironment,\n createSkillConfigManager,\n isSkillEnabled,\n validateSkillConfig,\n type SkillConfigFile,\n} from './config.js';\n\n// `watcher.ts` re-exports removed — it imports `SkillLoader` from THIS file, so\n// re-exporting back here formed a circular cycle. External callers should\n// import directly from `./watcher.js`.\n\n// Re-export test framework\nexport {\n SkillTestFramework,\n SkillTestRunner,\n formatTestResults,\n formatTestResultsJson,\n formatTestResultsTap,\n type TestResult,\n type TestStatus,\n type SkillTestReport,\n type TestOptions,\n type TestRunnerOptions,\n} from './test-framework.js';\n"],"mappings":";;;;;;;;;;;;;;;;YAGwD;aACH;AAQrD,MAAM,MAAM,aAAa,cAAc;AAEvC,MAAM,eAAe;CAAC;CAAc;CAAW;CAAY;AAE3D,SAAS,YAAY,GAAmB;AACtC,QAAO,EAAE,MAAM,IAAI,CAAC,KAAK,IAAI;;AAG/B,SAAS,oBAAoB,MAAc,QAA+B;CACxE,MAAM,UAAU,KAAK,MAAM;AAC3B,KAAI,CAAC,QAAS,QAAO;AACrB,KAAI,QAAQ,WAAW,IAAI,IAAI,CAAC,QAAQ,WAAW,MAAM,CAAE,QAAO;CAElE,IAAI,UAAU;CACd,IAAI,UAAU;AAEd,KAAI,QAAQ,WAAW,IAAI,EAAE;AAC3B,YAAU;AACV,YAAU,QAAQ,MAAM,EAAE;YACjB,QAAQ,WAAW,MAAM,CAClC,WAAU,QAAQ,MAAM,EAAE;AAG5B,KAAI,QAAQ,WAAW,IAAI,CACzB,WAAU,QAAQ,MAAM,EAAE;CAG5B,MAAM,WAAW,SAAS,GAAG,SAAS,YAAY;AAClD,QAAO,UAAU,IAAI,aAAa;;AAGpC,SAAS,gBAAgB,KAAa,SAA8B;CAClE,MAAM,+BAAe,IAAI,KAAa;CACtC,MAAM,cAAc,SAAS,SAAS,IAAI;CAC1C,MAAM,SAAS,cAAc,GAAG,YAAY,YAAY,CAAC,KAAK;AAE9D,MAAK,MAAM,YAAY,cAAc;EACnC,MAAM,aAAa,KAAK,KAAK,SAAS;AACtC,MAAI,CAAC,WAAW,WAAW,CAAE;AAE7B,MAAI;GACF,MAAM,UAAU,aAAa,YAAY,QAAQ;AACjD,QAAK,MAAM,QAAQ,QAAQ,MAAM,QAAQ,EAAE;IACzC,MAAM,UAAU,oBAAoB,MAAM,OAAO;AACjD,QAAI,SAAS;KACX,MAAM,cAAc,QAAQ,WAAW,IAAI,GACvC,IAAI,SAAS,QAAQ,MAAM,EAAE,KAC7B,GAAG,SAAS;AAChB,kBAAa,IAAI,YAAY;;;UAG3B;;AAGV,QAAO;;AAGT,SAAS,aAAa,MAAc,cAAoC;AACtE,MAAK,MAAM,WAAW,aACpB,KAAI,QAAQ,WAAW,IAAI,EAAE;EAC3B,MAAM,WAAW,QAAQ,MAAM,EAAE;AACjC,MAAI,SAAS,YAAY,KAAK,WAAW,GAAG,SAAS,GAAG,CACtD,QAAO;YAGL,SAAS,WAAW,KAAK,WAAW,GAAG,QAAQ,GAAG,CACpD,QAAO;AAIb,QAAO;;AAGT,SAAS,eACP,KACA,QACqD;CACrD,MAAM,SAAkB,EAAE;CAC1B,MAAM,cAAiC,EAAE;AACzC,KAAI,CAAC,WAAW,IAAI,CAAE,QAAO;EAAE;EAAQ;EAAa;CAEpD,SAAS,KAAK,YAAoB,qBAAkC;AAClE,MAAI;GACF,MAAM,UAAU,YAAY,YAAY,EAAE,eAAe,MAAM,CAAC;AAEhE,QAAK,MAAM,SAAS,SAAS;AAC3B,QAAI,MAAM,KAAK,WAAW,IAAI,IAAI,MAAM,SAAS,eAAgB;IAEjE,MAAM,WAAW,KAAK,YAAY,MAAM,KAAK;IAC7C,MAAM,UAAU,YAAY,SAAS,KAAK,SAAS,CAAC;AAEpD,QAAI,aAAa,SAAS,oBAAoB,CAAE;AAEhD,QAAI,MAAM,aAAa,EAAE;KACvB,MAAM,cAAc,KAAK,UAAU,WAAW;KAC9C,MAAM,eAAe,GAAG,QAAQ;KAEhC,MAAM,kBAAkB,IAAI,IAAI,oBAAoB;AAEpD,SAAI,WADkB,KAAK,UAAU,aACT,CAAC,EAAE;MAC7B,MAAM,WAAW,gBAAgB,UAAU,IAAI;AAC/C,WAAK,MAAM,QAAQ,SACjB,iBAAgB,IAAI,GAAG,eAAe,OAAO;;AAIjD,SAAI,WAAW,YAAY,IAAI,CAAC,aAAa,cAAc,oBAAoB,EAAE;MAC/E,MAAM,EAAE,OAAO,eAAe,kBAAkB,aAAa,QAAQ,IAAI;AACzE,UAAI,WAAY,aAAY,KAAK,WAAW;AAC5C,UAAI,MAAO,QAAO,KAAK,MAAM;;AAG/B,UAAK,UAAU,gBAAgB;;;WAG5B,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAChE,eAAY,KAAK;IACf,MAAM;IACN,SAAS,oCAAoC;IAC7C,MAAM;IACP,CAAC;;;AAIN,MAAK,KAAK,gBAAgB,KAAK,IAAI,CAAC;AACpC,QAAO;EAAE;EAAQ;EAAa;;AAGhC,SAAS,8BAA8B,SAAqC;CAC1E,IAAI,kBAAkB;AAEtB,MAAK,MAAM,WAAW,QAAQ,MAAM,QAAQ,EAAE;EAC5C,MAAM,OAAO,QAAQ,MAAM;AAC3B,MAAI,CAAC,KAAM;EAEX,MAAM,UAAU,KAAK,MAAM,kBAAkB;AAC7C,MAAI,SAAS;AACX,uBAAoB,QAAQ,IAAI,MAAM,IAAI;AAC1C;;AAGF,SAAO,KACJ,QAAQ,0BAA0B,KAAK,CACvC,QAAQ,WAAW,GAAG,CACtB,MAAM,IAAI,KAAA;;AAGf,QAAO,mBAAmB,KAAA;;AAG5B,SAAS,kBACP,UACA,QACA,SACuD;AACvD,KAAI;EAEF,MAAM,EAAE,aAAa,YAAY,iBADd,aAAa,UAAU,QACkB,CAAC;EAC7D,MAAM,WAAW,QAAQ,SAAS;EAClC,MAAM,gBAAgB,SAAS,SAAS;EAExC,MAAM,OAAQ,YAAY,QAA+B;EACzD,MAAM,cACH,YAAY,aAAoC,MAAM,IAAI,8BAA8B,QAAQ;AACnG,MAAI,CAAC,aAAa,MAAM,CACtB,QAAO;GACL,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW;IACX,SAAS,UAAU,KAAK;IACxB,MAAM;IACP;GACF;EAKH,IAAI;AACJ,MAAI,SAAS;GAEX,MAAM,WADS,YAAY,SAAS,SAAS,SAAS,CAC/B,CAAC,MAAM,IAAI;AAClC,OAAI,SAAS,SAAS,EACpB,YAAW,SAAS;;EAIxB,MAAM,WAAW,mBAAmB,YAAY;AAChD,WAAS,SAAS;AAClB,WAAS,gBAAgB,YAAY,MAAM;EAC3C,MAAM,iBAAiB,yBAAyB,YAAY;EAC5D,MAAM,sBAAsB,yBAAyB,YAAY;AAEjE,SAAO,EACL,OAAO;GACL;GACA,aAAa,YAAY,MAAM;GAC/B;GACA;GACA,SAAS;GACT;GACA,wBAAwB,YAAY,gCAAgC;GACpE;GACA;GACA,qBAAqB,oBAAoB,SAAS,IAAI,sBAAsB,KAAA;GAC5E;GACD,EACF;UACM,KAAK;AAEZ,SAAO;GACL,OAAO;GACP,YAAY;IACV,MAAM;IACN,SAAS,8BALG,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAM5D,MAAM;IACP;GACF;;;AAIL,SAAgB,WAAW,SAKN;CACnB,MAAM,EAAE,cAAc,YAAY,YAAY,EAAE,KAAK;CAErD,MAAM,2BAAW,IAAI,KAAoB;CACzC,MAAM,cAAiC,EAAE;AAEzC,KAAI,YAAY;EACd,MAAM,aAAa,eAAe,YAAY,UAAU;AACxD,cAAY,KAAK,GAAG,WAAW,YAAY;AAC3C,OAAK,MAAM,SAAS,WAAW,QAAQ;GACrC,MAAM,WAAW,SAAS,IAAI,MAAM,KAAK;AACzC,OAAI,SACF,aAAY,KAAK;IACf,MAAM;IACN,WAAW,MAAM;IACjB,SAAS,UAAU,MAAM,KAAK,eAAe,SAAS,OAAO,aAAa,MAAM;IAChF,MAAM,MAAM;IACb,CAAC;OAEF,UAAS,IAAI,MAAM,MAAM,MAAM;;;CAKrC,MAAM,aAAa,CACjB,QAAQ,WACR,KAAK,iBAAiB,EAAE,SAAS,CAClC,CAAC,QAAQ,MAAmB,CAAC,CAAC,KAAK,WAAW,EAAE,CAAC;AAElD,MAAK,MAAM,OAAO,YAAY;EAC5B,MAAM,aAAa,eAAe,KAAK,SAAS;AAChD,cAAY,KAAK,GAAG,WAAW,YAAY;AAC3C,OAAK,MAAM,SAAS,WAAW,QAAQ;GACrC,MAAM,WAAW,SAAS,IAAI,MAAM,KAAK;AACzC,OAAI,SACF,aAAY,KAAK;IACf,MAAM;IACN,WAAW,MAAM;IACjB,SAAS,UAAU,MAAM,KAAK,eAAe,MAAM,OAAO,aAAa,SAAS;IAChF,MAAM,MAAM;IACb,CAAC;AAGJ,YAAS,IAAI,MAAM,MAAM,MAAM;;;AAInC,KAAI,cAAc;EAChB,MAAM,aAAa,eAAe,KAAK,cAAc,SAAS,EAAE,YAAY;AAC5E,cAAY,KAAK,GAAG,WAAW,YAAY;EAC3C,MAAM,kBAAkB,WAAW;AACnC,OAAK,MAAM,SAAS,iBAAiB;GACnC,MAAM,WAAW,SAAS,IAAI,MAAM,KAAK;AACzC,OAAI,SACF,aAAY,KAAK;IACf,MAAM;IACN,WAAW,MAAM;IACjB,SAAS,UAAU,MAAM,KAAK,eAAe,MAAM,OAAO,aAAa,SAAS;IAChF,MAAM,MAAM;IACb,CAAC;AAEJ,YAAS,IAAI,MAAM,MAAM,MAAM;;;AAKnC,MAAK,MAAM,YAAY,UACrB,KAAI,WAAW,SAAS,EAAE;EACxB,MAAM,aAAa,eAAe,UAAU,SAAS;AACrD,cAAY,KAAK,GAAG,WAAW,YAAY;AAC3C,OAAK,MAAM,SAAS,WAAW,QAAQ;GACrC,MAAM,WAAW,SAAS,IAAI,MAAM,KAAK;AACzC,OAAI,SACF,aAAY,KAAK;IACf,MAAM;IACN,WAAW,MAAM;IACjB,SAAS,UAAU,MAAM,KAAK,eAAe,SAAS,OAAO,aAAa,MAAM;IAChF,MAAM,MAAM;IACb,CAAC;OAEF,UAAS,IAAI,MAAM,MAAM,MAAM;;;CAMvC,MAAM,eAAe,yBAAyB,iBAAiB,CAAC,CAAC,MAAM;CACvE,MAAM,SAAS,MAAM,KAAK,SAAS,QAAQ,CAAC;AAE5C,QAAO;EACL,QAAQ;EACR,QAAQ,sBAAsB,QAAQ,aAAa;EACnD;EACD;;AAiBH,SAAgB,oBAAiC;CAC/C,IAAI,eAAwB,EAAE;CAC9B,IAAI,eAAuB;CAC3B,IAAI,oBAAuC,EAAE;CAC7C,IAAI,eAAe;CACnB,IAAI;CACJ,IAAI;CACJ,IAAI,YAAsB,EAAE;CAE5B,SAAS,YAAY,QAA4C;AAC/D,iBAAe,OAAO;AACtB,iBAAe,OAAO;AACtB,sBAAoB,OAAO;AAC3B,iBAAe,KAAK,KAAK;AACzB,SAAO;;AAGT,QAAO;EACL,OAAO,WAAmB,YAA2B;AACnD,kBAAe;AACf,gBAAa,WAAW,KAAA;AACxB,UAAO,YAAY,WAAW;IAAE;IAAc;IAAY;IAAW,CAAC,CAAC;;EAGzE,YAAY;AACV,UAAO,YAAY,WAAW;IAAE;IAAc;IAAY;IAAW,CAAC,CAAC;;EAGzE,cAAc;AACZ,OAAI,KAAK,mBAAmB;AAC5B,UAAO,YAAY,WAAW;IAAE;IAAc;IAAY;IAAW,CAAC,CAAC;;EAGzE,+BAA+B;GAC7B,MAAM,eAAe,yBAAyB,iBAAiB,CAAC,CAAC,MAAM;AACvE,kBAAe,sBAAsB,cAAc,aAAa;AAChE,kBAAe,KAAK,KAAK;;EAG3B,iBAAiB;EACjB,iBAAiB;EACjB,sBAAsB;EACtB,uBAAuB;EAEvB,iBAAiB,SAAiB;AAChC,UAAO,aAAa,MAAK,MAAK,EAAE,SAAS,KAAK;;EAGhD,mBAAmB,YAA0C;GAC3D,MAAM,eACJ,YAAY,KAAA,IAAY,EAAE,SAAS,GAAG,yBAAyB,iBAAiB,CAAC,CAAC,MAAM;AAC1F,UAAO,aAAa,QACjB,UAAU,CAAC,MAAM,0BAA0B,eAAe,OAAO,aAAa,CAChF;;EAEJ"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/agent/skills/index.ts"],"sourcesContent":["import { existsSync, readdirSync, readFileSync } from 'fs';\nimport { basename, dirname, join, relative, sep } from 'path';\nimport { parseFrontmatter } from '../../markdown/frontmatter.js';\nimport { resolveStateDir } from '../../config/paths.js';\nimport { createLogger } from '../../utils/logger.js';\nimport { createSkillConfigManager, isSkillEnabled } from './config.js';\nimport { formatSkillsForPrompt } from './format-skills-prompt.js';\nimport { parseRequiredEnvVarNames } from './required-env-vars.js';\nimport { parseSkillMetadata } from './parse-skill-metadata.js';\nimport { parseSkillToolConditions } from './skill-tool-gating.js';\nimport { resolveWorkspaceSkillsDir } from './workspace-skills-dir.js';\nimport type { Skill, SkillDiagnostic, LoadSkillsResult, SkillConfig, SkillsConfig } from './types.js';\n\nconst log = createLogger('SkillLoader');\n\nconst IGNORE_FILES = ['.gitignore', '.ignore', '.fdignore'];\n\nfunction toPosixPath(p: string): string {\n return p.split(sep).join('/');\n}\n\nfunction prefixIgnorePattern(line: string, prefix: string): string | null {\n const trimmed = line.trim();\n if (!trimmed) return null;\n if (trimmed.startsWith('#') && !trimmed.startsWith('\\\\#')) return null;\n\n let pattern = line;\n let negated = false;\n\n if (pattern.startsWith('!')) {\n negated = true;\n pattern = pattern.slice(1);\n } else if (pattern.startsWith('\\\\!')) {\n pattern = pattern.slice(1);\n }\n\n if (pattern.startsWith('/')) {\n pattern = pattern.slice(1);\n }\n\n const prefixed = prefix ? `${prefix}${pattern}` : pattern;\n return negated ? `!${prefixed}` : prefixed;\n}\n\nfunction loadIgnoreRules(dir: string, rootDir: string): Set<string> {\n const ignoredPaths = new Set<string>();\n const relativeDir = relative(rootDir, dir);\n const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : '';\n\n for (const filename of IGNORE_FILES) {\n const ignorePath = join(dir, filename);\n if (!existsSync(ignorePath)) continue;\n\n try {\n const content = readFileSync(ignorePath, 'utf-8');\n for (const line of content.split(/\\r?\\n/)) {\n const pattern = prefixIgnorePattern(line, prefix);\n if (pattern) {\n const fullPattern = pattern.startsWith('!')\n ? `!${prefix}${pattern.slice(1)}`\n : `${prefix}${pattern}`;\n ignoredPaths.add(fullPattern);\n }\n }\n } catch {}\n }\n\n return ignoredPaths;\n}\n\nfunction shouldIgnore(path: string, ignoredPaths: Set<string>): boolean {\n for (const pattern of ignoredPaths) {\n if (pattern.startsWith('!')) {\n const positive = pattern.slice(1);\n if (path === positive || path.startsWith(`${positive}/`)) {\n return false;\n }\n } else {\n if (path === pattern || path.startsWith(`${pattern}/`)) {\n return true;\n }\n }\n }\n return false;\n}\n\nfunction discoverSkills(\n dir: string,\n source: 'builtin' | 'workspace' | 'global',\n): { skills: Skill[]; diagnostics: SkillDiagnostic[] } {\n const skills: Skill[] = [];\n const diagnostics: SkillDiagnostic[] = [];\n if (!existsSync(dir)) return { skills, diagnostics };\n\n function scan(currentDir: string, currentIgnoredPaths: Set<string>) {\n try {\n const entries = readdirSync(currentDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;\n\n const fullPath = join(currentDir, entry.name);\n const relPath = toPosixPath(relative(dir, fullPath));\n\n if (shouldIgnore(relPath, currentIgnoredPaths)) continue;\n\n if (entry.isDirectory()) {\n const skillMdPath = join(fullPath, 'SKILL.md');\n const skillRelPath = `${relPath}/`;\n\n const subIgnoredPaths = new Set(currentIgnoredPaths);\n const subIgnoreFile = join(fullPath, '.gitignore');\n if (existsSync(subIgnoreFile)) {\n const subRules = loadIgnoreRules(fullPath, dir);\n for (const rule of subRules) {\n subIgnoredPaths.add(`${skillRelPath}${rule}`);\n }\n }\n\n if (existsSync(skillMdPath) && !shouldIgnore(skillRelPath, currentIgnoredPaths)) {\n const { skill, diagnostic } = loadSkillFromFile(skillMdPath, source, dir);\n if (diagnostic) diagnostics.push(diagnostic);\n if (skill) skills.push(skill);\n }\n\n scan(fullPath, subIgnoredPaths);\n }\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n diagnostics.push({\n type: 'error',\n message: `Failed to scan skills directory: ${message}`,\n path: currentDir,\n });\n }\n }\n\n scan(dir, loadIgnoreRules(dir, dir));\n return { skills, diagnostics };\n}\n\nfunction deriveDescriptionFromMarkdown(content: string): string | undefined {\n let fallbackHeading = '';\n\n for (const rawLine of content.split(/\\r?\\n/)) {\n const line = rawLine.trim();\n if (!line) continue;\n\n const heading = line.match(/^#{1,6}\\s+(.+)$/);\n if (heading) {\n fallbackHeading ||= heading[1]?.trim() ?? '';\n continue;\n }\n\n return line\n .replace(/\\[([^\\]]+)\\]\\([^)]*\\)/g, '$1')\n .replace(/[*_`~]/g, '')\n .trim() || undefined;\n }\n\n return fallbackHeading || undefined;\n}\n\nfunction loadSkillFromFile(\n filePath: string,\n source: 'builtin' | 'workspace' | 'global',\n rootDir?: string,\n): { skill: Skill | null; diagnostic?: SkillDiagnostic } {\n try {\n const rawContent = readFileSync(filePath, 'utf-8');\n const { frontmatter, content } = parseFrontmatter(rawContent);\n const skillDir = dirname(filePath);\n const parentDirName = basename(skillDir);\n\n const name = (frontmatter.name as string | undefined) || parentDirName;\n const description =\n (frontmatter.description as string | undefined)?.trim() || deriveDescriptionFromMarkdown(content);\n if (!description?.trim()) {\n return {\n skill: null,\n diagnostic: {\n type: 'error',\n skillName: name,\n message: `Skill \"${name}\" is missing a description`,\n path: filePath,\n },\n };\n }\n\n // Derive category from directory path: .xopc/skills/creative/algorithmic-art → 'creative'\n // Only assign a category when the skill is nested at least two levels below rootDir.\n let category: string | undefined;\n if (rootDir) {\n const relDir = toPosixPath(relative(rootDir, skillDir));\n const segments = relDir.split('/');\n if (segments.length > 1) {\n category = segments[0];\n }\n }\n\n const metadata = parseSkillMetadata(frontmatter);\n metadata.name ||= name;\n metadata.description ||= description.trim();\n const toolConditions = parseSkillToolConditions(frontmatter);\n const requiredEnvVarNames = parseRequiredEnvVarNames(frontmatter);\n\n return {\n skill: {\n name,\n description: description.trim(),\n category,\n filePath,\n baseDir: skillDir,\n source,\n disableModelInvocation: frontmatter['disable-model-invocation'] === true,\n metadata,\n toolConditions,\n requiredEnvVarNames: requiredEnvVarNames.length > 0 ? requiredEnvVarNames : undefined,\n content,\n },\n };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return {\n skill: null,\n diagnostic: {\n type: 'error',\n message: `Failed to load skill file: ${message}`,\n path: filePath,\n },\n };\n }\n}\n\nexport function loadSkills(options: {\n workspaceDir?: string;\n globalDir?: string;\n builtinDir?: string;\n extraDirs?: string[];\n}): LoadSkillsResult {\n const { workspaceDir, builtinDir, extraDirs = [] } = options;\n\n const skillMap = new Map<string, Skill>();\n const diagnostics: SkillDiagnostic[] = [];\n\n if (builtinDir) {\n const discovered = discoverSkills(builtinDir, 'builtin');\n diagnostics.push(...discovered.diagnostics);\n for (const skill of discovered.skills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${existing.source} overrides ${skill.source}`,\n path: skill.filePath,\n });\n } else {\n skillMap.set(skill.name, skill);\n }\n }\n }\n\n const globalDirs = [\n options.globalDir,\n join(resolveStateDir(), 'skills'),\n ].filter((d): d is string => !!d && existsSync(d));\n\n for (const dir of globalDirs) {\n const discovered = discoverSkills(dir, 'global');\n diagnostics.push(...discovered.diagnostics);\n for (const skill of discovered.skills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${skill.source} overrides ${existing.source}`,\n path: skill.filePath,\n });\n }\n // Global must win over bundled when names match (Workspace > Global > Bundled).\n skillMap.set(skill.name, skill);\n }\n }\n\n if (workspaceDir) {\n const discovered = discoverSkills(resolveWorkspaceSkillsDir(workspaceDir), 'workspace');\n diagnostics.push(...discovered.diagnostics);\n const workspaceSkills = discovered.skills;\n for (const skill of workspaceSkills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${skill.source} overrides ${existing.source}`,\n path: skill.filePath,\n });\n }\n skillMap.set(skill.name, skill);\n }\n }\n\n // Scan extra directories\n for (const extraDir of extraDirs) {\n if (existsSync(extraDir)) {\n const discovered = discoverSkills(extraDir, 'global');\n diagnostics.push(...discovered.diagnostics);\n for (const skill of discovered.skills) {\n const existing = skillMap.get(skill.name);\n if (existing) {\n diagnostics.push({\n type: 'collision',\n skillName: skill.name,\n message: `Skill \"${skill.name}\" collision: ${existing.source} overrides ${skill.source}`,\n path: skill.filePath,\n });\n } else {\n skillMap.set(skill.name, skill);\n }\n }\n }\n }\n\n const skillsConfig = createSkillConfigManager(resolveStateDir()).load();\n const merged = Array.from(skillMap.values());\n\n return {\n skills: merged,\n prompt: formatSkillsForPrompt(merged, skillsConfig),\n diagnostics,\n };\n}\n\nexport interface SkillLoader {\n init: (workspace: string, builtin: string | null) => LoadSkillsResult;\n load: () => LoadSkillsResult;\n reload: () => LoadSkillsResult;\n /** Recompute `<available_skills>` from disk-loaded skills and current ~/.xopc/skills.json (no filesystem rescan). */\n refreshPromptFromConfig: () => void;\n getSkills: () => Skill[];\n getPrompt: () => string;\n getDiagnostics: () => SkillDiagnostic[];\n getLastLoadTime: () => number;\n getSkillByName: (name: string) => Skill | undefined;\n getEnabledSkills: (config?: Record<string, SkillConfig>) => Skill[];\n}\n\nexport function createSkillLoader(): SkillLoader {\n let cachedSkills: Skill[] = [];\n let cachedPrompt: string = '';\n let cachedDiagnostics: SkillDiagnostic[] = [];\n let lastLoadTime = 0;\n let workspaceDir: string | undefined;\n let builtinDir: string | undefined;\n let extraDirs: string[] = [];\n\n function updateCache(result: LoadSkillsResult): LoadSkillsResult {\n cachedSkills = result.skills;\n cachedPrompt = result.prompt;\n cachedDiagnostics = result.diagnostics;\n lastLoadTime = Date.now();\n return result;\n }\n\n return {\n init: (workspace: string, builtin: string | null) => {\n workspaceDir = workspace;\n builtinDir = builtin || undefined;\n return updateCache(loadSkills({ workspaceDir, builtinDir, extraDirs }));\n },\n \n load: () => {\n return updateCache(loadSkills({ workspaceDir, builtinDir, extraDirs }));\n },\n \n reload: () => {\n log.info('Reloading skills');\n return updateCache(loadSkills({ workspaceDir, builtinDir, extraDirs }));\n },\n\n refreshPromptFromConfig: () => {\n const skillsConfig = createSkillConfigManager(resolveStateDir()).load();\n cachedPrompt = formatSkillsForPrompt(cachedSkills, skillsConfig);\n lastLoadTime = Date.now();\n },\n\n getSkills: () => cachedSkills,\n getPrompt: () => cachedPrompt,\n getDiagnostics: () => cachedDiagnostics,\n getLastLoadTime: () => lastLoadTime,\n \n getSkillByName: (name: string) => {\n return cachedSkills.find(s => s.name === name);\n },\n \n getEnabledSkills: (entries?: Record<string, SkillConfig>) => {\n const skillsConfig: SkillsConfig =\n entries !== undefined ? { entries } : createSkillConfigManager(resolveStateDir()).load();\n return cachedSkills.filter(\n (skill) => !skill.disableModelInvocation && isSkillEnabled(skill, skillsConfig),\n );\n },\n };\n}\n\n// `SkillManager` + sibling types removed from this barrel — `skill-manager.ts`\n// imports `createSkillLoader` from THIS file, so re-exporting it back here\n// formed a circular cycle. External callers should import directly from\n// `./skill-manager.js`.\n\n// Re-export types for convenience\nexport type { \n Skill, \n SkillMetadata, \n SkillConfig,\n SkillInstallSpec,\n SkillInstallResult,\n SkillInstallRequest,\n LoadSkillsResult,\n // Note: SkillDiagnostic is re-exported from skill-manager.ts above\n SkillSnapshot,\n} from './types.js';\n\n// Re-export installer\nexport { \n installSkill, \n findInstallSpec,\n hasBinary,\n getDefaultInstallerPreferences,\n type InstallerPreferences,\n type InstallContext,\n} from './installer.js';\n\n// Re-export scanner\nexport {\n scanSkillDirectory,\n formatScanSummary,\n collectSkillInstallWarnings,\n type ScanSummary,\n type SecurityFinding,\n type Severity,\n} from './scanner.js';\n\n// Re-export config manager\nexport {\n resolveSkillConfig,\n applySkillEnvOverrides,\n getSkillEnvironment,\n createSkillConfigManager,\n isSkillEnabled,\n validateSkillConfig,\n type SkillConfigFile,\n} from './config.js';\n\n// `watcher.ts` re-exports removed — it imports `SkillLoader` from THIS file, so\n// re-exporting back here formed a circular cycle. External callers should\n// import directly from `./watcher.js`.\n\n// Re-export test framework\nexport {\n SkillTestFramework,\n SkillTestRunner,\n formatTestResults,\n formatTestResultsJson,\n formatTestResultsTap,\n type TestResult,\n type TestStatus,\n type SkillTestReport,\n type TestOptions,\n type TestRunnerOptions,\n} from './test-framework.js';\n"],"mappings":"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"}
@@ -2,6 +2,7 @@ import { init_paths, resolveSkillsDir } from "../../config/paths.js";
2
2
  import { parseFrontmatter } from "../../markdown/frontmatter.js";
3
3
  import { fuzzyFindText, normalizeForFuzzyMatch, normalizeToLF, restoreLineEndings } from "../tools/edit-diff.js";
4
4
  import { formatScanSummary, scanSkillDirectory } from "./scanner.js";
5
+ import { resolveWorkspaceSkillsDir } from "./workspace-skills-dir.js";
5
6
  import { existsSync, realpathSync } from "fs";
6
7
  import { join, resolve, sep } from "path";
7
8
  import { mkdir, writeFile } from "fs/promises";
@@ -97,7 +98,7 @@ function resolveGlobalSkillsRoot() {
97
98
  return resolveSkillsDir();
98
99
  }
99
100
  function resolveWorkspaceSkillsRoot(workspace) {
100
- return resolve(workspace, "skills");
101
+ return resolveWorkspaceSkillsDir(workspace);
101
102
  }
102
103
  function canWriteToPath(skillBaseDir, workspace, policy) {
103
104
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"skill-manage-ops.js","names":[],"sources":["../../../../src/agent/skills/skill-manage-ops.ts"],"sourcesContent":["/**\n * Shared logic for the skill_manage agent tool (create / edit / patch / delete / write_file / remove_file).\n */\n\nimport { existsSync, realpathSync } from 'fs';\nimport { join, resolve, sep } from 'path';\nimport { mkdir, writeFile } from 'fs/promises';\n\nimport { parseFrontmatter } from '../../markdown/frontmatter.js';\nimport { resolveSkillsDir } from '../../config/paths.js';\nimport type { Skill } from './types.js';\nimport type { SkillsConfig } from './types.js';\nimport { scanSkillDirectory, formatScanSummary } from './scanner.js';\n\nexport const SKILL_MANAGE_ALLOWED_SUBDIRS = new Set(['references', 'templates', 'scripts', 'assets']);\n\nexport const SKILL_MANAGE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;\nexport const DEFAULT_MAX_SKILL_MD_CHARS = 100_000;\nexport const DEFAULT_MAX_SUPPORT_FILE_BYTES = 1_048_576;\n\nexport type AgentWritePolicy = 'global' | 'workspace' | 'both';\n\nexport function effectiveAgentWritePolicy(cfg?: SkillsConfig): AgentWritePolicy {\n const p = cfg?.agentWritePolicy;\n if (p === 'workspace' || p === 'both') return p;\n return 'global';\n}\n\nexport function maxSkillMdChars(_cfg?: SkillsConfig): number {\n return DEFAULT_MAX_SKILL_MD_CHARS;\n}\n\nexport function maxSupportFileBytes(cfg?: SkillsConfig): number {\n const n = cfg?.limits?.maxSkillFileBytes;\n if (typeof n === 'number' && n > 0) return n;\n return DEFAULT_MAX_SUPPORT_FILE_BYTES;\n}\n\nexport function validateSkillNameSegment(name: string, label: string): string | null {\n if (!name?.trim()) return `${label} is required.`;\n if (name.length > 64) return `${label} must be at most 64 characters.`;\n if (!SKILL_MANAGE_NAME_RE.test(name)) {\n return `${label} must match ${SKILL_MANAGE_NAME_RE}: lowercase letters, digits, ., _, -`;\n }\n return null;\n}\n\nexport function validateSkillMdContent(\n content: string,\n expectedName: string,\n maxChars: number,\n): { ok: true } | { ok: false; error: string } {\n if (!content.trim()) return { ok: false, error: 'Content cannot be empty.' };\n if (content.length > maxChars) {\n return { ok: false, error: `SKILL.md exceeds limit (${maxChars} characters).` };\n }\n if (!content.startsWith('---')) {\n return { ok: false, error: 'SKILL.md must start with YAML frontmatter (---).' };\n }\n const close = /\\r?\\n---\\s*\\r?\\n/.exec(content.slice(3));\n if (!close) {\n return { ok: false, error: 'Frontmatter must be closed with a line --- before the body.' };\n }\n const { frontmatter, content: body } = parseFrontmatter<Record<string, unknown>>(content);\n const name = frontmatter.name;\n const desc = frontmatter.description;\n if (typeof name !== 'string' || !name.trim()) {\n return { ok: false, error: 'Frontmatter must include a string name field.' };\n }\n if (typeof desc !== 'string' || !desc.trim()) {\n return { ok: false, error: 'Frontmatter must include a string description field.' };\n }\n if (name.trim() !== expectedName) {\n return {\n ok: false,\n error: `Frontmatter name \"${name.trim()}\" must match skill name \"${expectedName}\".`,\n };\n }\n if (!body?.trim()) {\n return { ok: false, error: 'SKILL.md must have a non-empty body after frontmatter.' };\n }\n return { ok: true };\n}\n\nfunction resolvedPathPrefix(dir: string): string {\n const r = resolve(dir);\n return r.endsWith(sep) ? r : r + sep;\n}\n\n/** Whether `filePath` is equal to `dir` or strictly inside it (resolved). */\nexport function isPathInsideDir(dir: string, filePath: string): boolean {\n try {\n const base = resolve(dir);\n const target = resolve(filePath);\n const baseReal = existsSync(base) ? realpathSync(base) : base;\n const targetReal = existsSync(target) ? realpathSync(target) : target;\n const prefix = resolvedPathPrefix(baseReal);\n return targetReal === baseReal || targetReal.startsWith(prefix);\n } catch {\n return false;\n }\n}\n\nexport function resolveGlobalSkillsRoot(): string {\n return resolveSkillsDir();\n}\n\nexport function resolveWorkspaceSkillsRoot(workspace: string): string {\n return resolve(workspace, 'skills');\n}\n\nexport function canWriteToPath(\n skillBaseDir: string,\n workspace: string,\n policy: AgentWritePolicy,\n): boolean {\n try {\n const baseResolved = existsSync(skillBaseDir) ? realpathSync(skillBaseDir) : resolve(skillBaseDir);\n const globalRoot = resolveGlobalSkillsRoot();\n const wsRoot = resolveWorkspaceSkillsRoot(workspace);\n const underGlobal = isPathInsideDir(globalRoot, baseResolved);\n const underWs = isPathInsideDir(wsRoot, baseResolved);\n if (policy === 'global') return underGlobal;\n if (policy === 'workspace') return underWs;\n return underGlobal || underWs;\n } catch {\n return false;\n }\n}\n\nexport function mutatableSkillOrNull(\n skill: Skill | undefined,\n workspace: string,\n policy: AgentWritePolicy,\n): Skill | null {\n if (!skill) return null;\n if (skill.source === 'builtin' || skill.source === 'extra') return null;\n if (!canWriteToPath(skill.baseDir, workspace, policy)) return null;\n return skill;\n}\n\nexport function validateSupportingRelativePath(filePath: string): string | null {\n if (!filePath?.trim()) return 'file_path is required.';\n const norm = filePath.replace(/\\\\/g, '/');\n if (norm.includes('..')) return \"Path traversal ('..') is not allowed.\";\n if (norm.startsWith('/') || /^[A-Za-z]:/.test(norm)) {\n return 'file_path must be relative to the skill directory.';\n }\n const parts = norm.split('/').filter(Boolean);\n if (parts.length < 2) {\n return 'Provide a file path with a subdirectory, e.g. references/notes.md';\n }\n if (!SKILL_MANAGE_ALLOWED_SUBDIRS.has(parts[0]!)) {\n return `File must be under one of: ${[...SKILL_MANAGE_ALLOWED_SUBDIRS].join(', ')}.`;\n }\n return null;\n}\n\nexport async function atomicWriteUtf8(filePath: string, content: string): Promise<void> {\n const dir = resolve(filePath, '..');\n await mkdir(dir, { recursive: true });\n await writeFile(filePath, content, 'utf-8');\n}\n\n/** Scan skill dir; returns error message if critical findings (caller rolls back). */\nexport async function scanSkillDirOrError(skillDir: string, skillName: string): Promise<string | null> {\n const summary = await scanSkillDirectory(skillDir);\n if (summary.critical > 0) {\n return `Security scan blocked this change:\\n${formatScanSummary(summary, skillName)}`;\n }\n return null;\n}\n\nexport function resolveCreateSkillDir(\n name: string,\n category: string | undefined,\n writeTarget: 'global' | 'workspace',\n workspace: string,\n policy: AgentWritePolicy,\n): { ok: true; dir: string } | { ok: false; error: string } {\n if (writeTarget === 'global' && policy === 'workspace') {\n return { ok: false, error: 'Creating under global skills is not allowed (skills.agentWritePolicy).' };\n }\n if (writeTarget === 'workspace' && policy === 'global') {\n return { ok: false, error: 'Creating under workspace skills is not allowed (skills.agentWritePolicy).' };\n }\n const root =\n writeTarget === 'global' ? resolveGlobalSkillsRoot() : resolveWorkspaceSkillsRoot(workspace);\n const dir = category?.trim() ? join(root, category.trim(), name) : join(root, name);\n return { ok: true, dir };\n}\n\nexport function ensureCategorySegment(category: string | undefined): string | null {\n if (category === undefined || category === null || category === '') return null;\n if (typeof category !== 'string') return 'category must be a string.';\n const c = category.trim();\n if (!c) return null;\n if (c.includes('/') || c.includes('\\\\')) return 'category must be a single path segment.';\n return validateSkillNameSegment(c, 'category');\n}\n\nimport { normalizeToLF, restoreLineEndings, fuzzyFindText, normalizeForFuzzyMatch } from '../tools/edit-diff.js';\n\nfunction detectLineEnding(content: string): '\\r\\n' | '\\n' {\n const crlfIdx = content.indexOf('\\r\\n');\n const lfIdx = content.indexOf('\\n');\n if (lfIdx === -1) return '\\n';\n if (crlfIdx === -1) return '\\n';\n return crlfIdx < lfIdx ? '\\r\\n' : '\\n';\n}\n\nexport function applyPatchToContent(\n content: string,\n oldString: string,\n newString: string,\n replaceAll: boolean,\n): { ok: true; next: string; replacements: number } | { ok: false; error: string } {\n if (!oldString) return { ok: false, error: 'old_string is required for patch.' };\n if (newString === undefined) {\n return { ok: false, error: 'new_string is required (use empty string to delete matched text).' };\n }\n\n const lineEnding = detectLineEnding(content);\n const normalized = normalizeToLF(content);\n const oldN = normalizeToLF(oldString);\n const newN = normalizeToLF(newString);\n\n if (replaceAll) {\n const parts = normalized.split(oldN);\n const count = parts.length - 1;\n if (count === 0) {\n return { ok: false, error: 'old_string not found for replace_all.' };\n }\n const merged = parts.join(newN);\n return { ok: true, next: restoreLineEndings(merged, lineEnding), replacements: count };\n }\n\n const match = fuzzyFindText(normalized, oldN);\n if (!match.found) {\n return { ok: false, error: 'old_string not found in file.' };\n }\n const base = match.contentForReplacement;\n const fuzzyContent = normalizeForFuzzyMatch(normalizeToLF(base));\n const fuzzyOld = normalizeForFuzzyMatch(oldN);\n const occurrences = fuzzyOld ? fuzzyContent.split(fuzzyOld).length - 1 : 0;\n if (occurrences > 1) {\n return { ok: false, error: `Found ${occurrences} matches; narrow old_string or use replace_all.` };\n }\n const next =\n base.slice(0, match.index) + newN + base.slice(match.index + match.matchLength);\n return { ok: true, next: restoreLineEndings(next, lineEnding), replacements: 1 };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"skill-manage-ops.js","names":[],"sources":["../../../../src/agent/skills/skill-manage-ops.ts"],"sourcesContent":["/**\n * Shared logic for the skill_manage agent tool (create / edit / patch / delete / write_file / remove_file).\n */\n\nimport { existsSync, realpathSync } from 'fs';\nimport { join, resolve, sep } from 'path';\nimport { mkdir, writeFile } from 'fs/promises';\n\nimport { parseFrontmatter } from '../../markdown/frontmatter.js';\nimport { resolveSkillsDir } from '../../config/paths.js';\nimport type { Skill } from './types.js';\nimport type { SkillsConfig } from './types.js';\nimport { scanSkillDirectory, formatScanSummary } from './scanner.js';\nimport { resolveWorkspaceSkillsDir } from './workspace-skills-dir.js';\n\nexport const SKILL_MANAGE_ALLOWED_SUBDIRS = new Set(['references', 'templates', 'scripts', 'assets']);\n\nexport const SKILL_MANAGE_NAME_RE = /^[a-z0-9][a-z0-9._-]*$/;\nexport const DEFAULT_MAX_SKILL_MD_CHARS = 100_000;\nexport const DEFAULT_MAX_SUPPORT_FILE_BYTES = 1_048_576;\n\nexport type AgentWritePolicy = 'global' | 'workspace' | 'both';\n\nexport function effectiveAgentWritePolicy(cfg?: SkillsConfig): AgentWritePolicy {\n const p = cfg?.agentWritePolicy;\n if (p === 'workspace' || p === 'both') return p;\n return 'global';\n}\n\nexport function maxSkillMdChars(_cfg?: SkillsConfig): number {\n return DEFAULT_MAX_SKILL_MD_CHARS;\n}\n\nexport function maxSupportFileBytes(cfg?: SkillsConfig): number {\n const n = cfg?.limits?.maxSkillFileBytes;\n if (typeof n === 'number' && n > 0) return n;\n return DEFAULT_MAX_SUPPORT_FILE_BYTES;\n}\n\nexport function validateSkillNameSegment(name: string, label: string): string | null {\n if (!name?.trim()) return `${label} is required.`;\n if (name.length > 64) return `${label} must be at most 64 characters.`;\n if (!SKILL_MANAGE_NAME_RE.test(name)) {\n return `${label} must match ${SKILL_MANAGE_NAME_RE}: lowercase letters, digits, ., _, -`;\n }\n return null;\n}\n\nexport function validateSkillMdContent(\n content: string,\n expectedName: string,\n maxChars: number,\n): { ok: true } | { ok: false; error: string } {\n if (!content.trim()) return { ok: false, error: 'Content cannot be empty.' };\n if (content.length > maxChars) {\n return { ok: false, error: `SKILL.md exceeds limit (${maxChars} characters).` };\n }\n if (!content.startsWith('---')) {\n return { ok: false, error: 'SKILL.md must start with YAML frontmatter (---).' };\n }\n const close = /\\r?\\n---\\s*\\r?\\n/.exec(content.slice(3));\n if (!close) {\n return { ok: false, error: 'Frontmatter must be closed with a line --- before the body.' };\n }\n const { frontmatter, content: body } = parseFrontmatter<Record<string, unknown>>(content);\n const name = frontmatter.name;\n const desc = frontmatter.description;\n if (typeof name !== 'string' || !name.trim()) {\n return { ok: false, error: 'Frontmatter must include a string name field.' };\n }\n if (typeof desc !== 'string' || !desc.trim()) {\n return { ok: false, error: 'Frontmatter must include a string description field.' };\n }\n if (name.trim() !== expectedName) {\n return {\n ok: false,\n error: `Frontmatter name \"${name.trim()}\" must match skill name \"${expectedName}\".`,\n };\n }\n if (!body?.trim()) {\n return { ok: false, error: 'SKILL.md must have a non-empty body after frontmatter.' };\n }\n return { ok: true };\n}\n\nfunction resolvedPathPrefix(dir: string): string {\n const r = resolve(dir);\n return r.endsWith(sep) ? r : r + sep;\n}\n\n/** Whether `filePath` is equal to `dir` or strictly inside it (resolved). */\nexport function isPathInsideDir(dir: string, filePath: string): boolean {\n try {\n const base = resolve(dir);\n const target = resolve(filePath);\n const baseReal = existsSync(base) ? realpathSync(base) : base;\n const targetReal = existsSync(target) ? realpathSync(target) : target;\n const prefix = resolvedPathPrefix(baseReal);\n return targetReal === baseReal || targetReal.startsWith(prefix);\n } catch {\n return false;\n }\n}\n\nexport function resolveGlobalSkillsRoot(): string {\n return resolveSkillsDir();\n}\n\nexport function resolveWorkspaceSkillsRoot(workspace: string): string {\n return resolveWorkspaceSkillsDir(workspace);\n}\n\nexport function canWriteToPath(\n skillBaseDir: string,\n workspace: string,\n policy: AgentWritePolicy,\n): boolean {\n try {\n const baseResolved = existsSync(skillBaseDir) ? realpathSync(skillBaseDir) : resolve(skillBaseDir);\n const globalRoot = resolveGlobalSkillsRoot();\n const wsRoot = resolveWorkspaceSkillsRoot(workspace);\n const underGlobal = isPathInsideDir(globalRoot, baseResolved);\n const underWs = isPathInsideDir(wsRoot, baseResolved);\n if (policy === 'global') return underGlobal;\n if (policy === 'workspace') return underWs;\n return underGlobal || underWs;\n } catch {\n return false;\n }\n}\n\nexport function mutatableSkillOrNull(\n skill: Skill | undefined,\n workspace: string,\n policy: AgentWritePolicy,\n): Skill | null {\n if (!skill) return null;\n if (skill.source === 'builtin' || skill.source === 'extra') return null;\n if (!canWriteToPath(skill.baseDir, workspace, policy)) return null;\n return skill;\n}\n\nexport function validateSupportingRelativePath(filePath: string): string | null {\n if (!filePath?.trim()) return 'file_path is required.';\n const norm = filePath.replace(/\\\\/g, '/');\n if (norm.includes('..')) return \"Path traversal ('..') is not allowed.\";\n if (norm.startsWith('/') || /^[A-Za-z]:/.test(norm)) {\n return 'file_path must be relative to the skill directory.';\n }\n const parts = norm.split('/').filter(Boolean);\n if (parts.length < 2) {\n return 'Provide a file path with a subdirectory, e.g. references/notes.md';\n }\n if (!SKILL_MANAGE_ALLOWED_SUBDIRS.has(parts[0]!)) {\n return `File must be under one of: ${[...SKILL_MANAGE_ALLOWED_SUBDIRS].join(', ')}.`;\n }\n return null;\n}\n\nexport async function atomicWriteUtf8(filePath: string, content: string): Promise<void> {\n const dir = resolve(filePath, '..');\n await mkdir(dir, { recursive: true });\n await writeFile(filePath, content, 'utf-8');\n}\n\n/** Scan skill dir; returns error message if critical findings (caller rolls back). */\nexport async function scanSkillDirOrError(skillDir: string, skillName: string): Promise<string | null> {\n const summary = await scanSkillDirectory(skillDir);\n if (summary.critical > 0) {\n return `Security scan blocked this change:\\n${formatScanSummary(summary, skillName)}`;\n }\n return null;\n}\n\nexport function resolveCreateSkillDir(\n name: string,\n category: string | undefined,\n writeTarget: 'global' | 'workspace',\n workspace: string,\n policy: AgentWritePolicy,\n): { ok: true; dir: string } | { ok: false; error: string } {\n if (writeTarget === 'global' && policy === 'workspace') {\n return { ok: false, error: 'Creating under global skills is not allowed (skills.agentWritePolicy).' };\n }\n if (writeTarget === 'workspace' && policy === 'global') {\n return { ok: false, error: 'Creating under workspace skills is not allowed (skills.agentWritePolicy).' };\n }\n const root =\n writeTarget === 'global' ? resolveGlobalSkillsRoot() : resolveWorkspaceSkillsRoot(workspace);\n const dir = category?.trim() ? join(root, category.trim(), name) : join(root, name);\n return { ok: true, dir };\n}\n\nexport function ensureCategorySegment(category: string | undefined): string | null {\n if (category === undefined || category === null || category === '') return null;\n if (typeof category !== 'string') return 'category must be a string.';\n const c = category.trim();\n if (!c) return null;\n if (c.includes('/') || c.includes('\\\\')) return 'category must be a single path segment.';\n return validateSkillNameSegment(c, 'category');\n}\n\nimport { normalizeToLF, restoreLineEndings, fuzzyFindText, normalizeForFuzzyMatch } from '../tools/edit-diff.js';\n\nfunction detectLineEnding(content: string): '\\r\\n' | '\\n' {\n const crlfIdx = content.indexOf('\\r\\n');\n const lfIdx = content.indexOf('\\n');\n if (lfIdx === -1) return '\\n';\n if (crlfIdx === -1) return '\\n';\n return crlfIdx < lfIdx ? '\\r\\n' : '\\n';\n}\n\nexport function applyPatchToContent(\n content: string,\n oldString: string,\n newString: string,\n replaceAll: boolean,\n): { ok: true; next: string; replacements: number } | { ok: false; error: string } {\n if (!oldString) return { ok: false, error: 'old_string is required for patch.' };\n if (newString === undefined) {\n return { ok: false, error: 'new_string is required (use empty string to delete matched text).' };\n }\n\n const lineEnding = detectLineEnding(content);\n const normalized = normalizeToLF(content);\n const oldN = normalizeToLF(oldString);\n const newN = normalizeToLF(newString);\n\n if (replaceAll) {\n const parts = normalized.split(oldN);\n const count = parts.length - 1;\n if (count === 0) {\n return { ok: false, error: 'old_string not found for replace_all.' };\n }\n const merged = parts.join(newN);\n return { ok: true, next: restoreLineEndings(merged, lineEnding), replacements: count };\n }\n\n const match = fuzzyFindText(normalized, oldN);\n if (!match.found) {\n return { ok: false, error: 'old_string not found in file.' };\n }\n const base = match.contentForReplacement;\n const fuzzyContent = normalizeForFuzzyMatch(normalizeToLF(base));\n const fuzzyOld = normalizeForFuzzyMatch(oldN);\n const occurrences = fuzzyOld ? fuzzyContent.split(fuzzyOld).length - 1 : 0;\n if (occurrences > 1) {\n return { ok: false, error: `Found ${occurrences} matches; narrow old_string or use replace_all.` };\n }\n const next =\n base.slice(0, match.index) + newN + base.slice(match.index + match.matchLength);\n return { ok: true, next: restoreLineEndings(next, lineEnding), replacements: 1 };\n}\n"],"mappings":"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"}
@@ -114,7 +114,7 @@ export interface SkillsConfig {
114
114
  */
115
115
  toolGating?: boolean;
116
116
  /**
117
- * Where `skill_manage` may create or edit skills: global `~/.xopc/skills`, workspace `skills/`, or both.
117
+ * Where `skill_manage` may create or edit skills: global `~/.xopc/skills`, workspace `.xopc/skills/`, or both.
118
118
  * Default `global`.
119
119
  */
120
120
  agentWritePolicy?: 'global' | 'workspace' | 'both';
@@ -1,7 +1,7 @@
1
1
  import { createLogger } from "../../utils/logger/index.js";
2
2
  import { init_logger } from "../../utils/logger.js";
3
+ import { resolveWorkspaceSkillsDir } from "./workspace-skills-dir.js";
3
4
  import { watch } from "fs";
4
- import { join } from "path";
5
5
  //#region src/agent/skills/watcher.ts
6
6
  /**
7
7
  * Skill Watcher
@@ -102,10 +102,7 @@ function createSkillWatcher(loader, options) {
102
102
  */
103
103
  function createWatcherFromLoader(loader, options) {
104
104
  const watchDirs = [];
105
- if (options.workspaceDir) {
106
- const workspaceSkillsDir = join(options.workspaceDir, "skills");
107
- watchDirs.push(workspaceSkillsDir);
108
- }
105
+ if (options.workspaceDir) watchDirs.push(resolveWorkspaceSkillsDir(options.workspaceDir));
109
106
  if (options.builtinDir) watchDirs.push(options.builtinDir);
110
107
  if (options.globalDirs) watchDirs.push(...options.globalDirs);
111
108
  if (options.extraDirs) watchDirs.push(...options.extraDirs);
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.js","names":[],"sources":["../../../../src/agent/skills/watcher.ts"],"sourcesContent":["/**\n * Skill Watcher\n * \n * Watches skill directories for changes and triggers reloads.\n *\n */\n\nimport { watch } from 'fs';\nimport { join } from 'path';\nimport { createLogger } from '../../utils/logger.js';\nimport type { SkillLoader } from './index.js';\n\nconst log = createLogger('SkillWatcher');\n\nexport interface SkillWatcherOptions {\n /** Debounce time in milliseconds */\n debounceMs?: number;\n /** Watch these directories */\n watchDirs: string[];\n}\n\nexport interface SkillWatcher {\n /** Start watching */\n start: () => void;\n /** Stop watching */\n stop: () => void;\n /** Check if watching is active */\n isWatching: () => boolean;\n /** Get last refresh time */\n getLastRefreshTime: () => number;\n}\n\n/**\n * Create a debounced reload function\n */\nfunction createDebouncedReload(\n loader: SkillLoader,\n debounceMs: number\n): () => void {\n let timeout: NodeJS.Timeout | null = null;\n let pendingReload = false;\n\n return () => {\n if (timeout) {\n pendingReload = true;\n return;\n }\n\n timeout = setTimeout(() => {\n timeout = null;\n \n if (pendingReload) {\n pendingReload = false;\n loader.reload();\n log.info('Skills reloaded after directory changes');\n }\n }, debounceMs);\n };\n}\n\n/**\n * Create a skill watcher\n */\nexport function createSkillWatcher(\n loader: SkillLoader,\n options: SkillWatcherOptions\n): SkillWatcher {\n const { watchDirs, debounceMs = 1000 } = options;\n \n const watchers: Array<{ watcher: ReturnType<typeof watch>; dir: string }> = [];\n const debouncedReload = createDebouncedReload(loader, debounceMs);\n let isWatching = false;\n let lastRefreshTime = Date.now();\n\n function watchDirectory(dir: string): void {\n try {\n const watcher = watch(dir, { recursive: true }, (eventType, filename) => {\n if (!filename) return;\n\n // Only care about SKILL.md files\n if (filename.toLowerCase() === 'skill.md') {\n log.debug({ eventType, filename, dir }, 'Skill file changed');\n lastRefreshTime = Date.now();\n debouncedReload();\n }\n\n // Also watch for new directories (potential new skills)\n if (eventType === 'rename') {\n log.debug({ eventType, filename, dir }, 'Directory changed, may be new skill');\n lastRefreshTime = Date.now();\n debouncedReload();\n }\n });\n\n watchers.push({ watcher, dir });\n log.info({ dir }, 'Watching skill directory');\n } catch (err) {\n log.warn({ dir, error: err }, 'Failed to watch skill directory');\n }\n }\n\n return {\n start: () => {\n if (isWatching) {\n log.warn('Watcher already started');\n return;\n }\n\n for (const dir of watchDirs) {\n watchDirectory(dir);\n }\n\n isWatching = true;\n log.info({ dirs: watchDirs }, 'Skill watcher started');\n },\n\n stop: () => {\n if (!isWatching) {\n return;\n }\n\n for (const { watcher } of watchers) {\n watcher.close();\n }\n\n watchers.length = 0;\n isWatching = false;\n log.info('Skill watcher stopped');\n },\n\n isWatching: () => isWatching,\n\n getLastRefreshTime: () => lastRefreshTime,\n };\n}\n\n/**\n * Create a skill watcher from loader configuration\n */\nexport function createWatcherFromLoader(\n loader: SkillLoader,\n options: {\n workspaceDir?: string;\n builtinDir?: string;\n globalDirs?: string[];\n extraDirs?: string[];\n debounceMs?: number;\n }\n): SkillWatcher | null {\n const watchDirs: string[] = [];\n\n // Add workspace skills directory\n if (options.workspaceDir) {\n const workspaceSkillsDir = join(options.workspaceDir, 'skills');\n watchDirs.push(workspaceSkillsDir);\n }\n\n // Add builtin directory\n if (options.builtinDir) {\n watchDirs.push(options.builtinDir);\n }\n\n // Add global directories\n if (options.globalDirs) {\n watchDirs.push(...options.globalDirs);\n }\n\n // Add extra directories\n if (options.extraDirs) {\n watchDirs.push(...options.extraDirs);\n }\n\n if (watchDirs.length === 0) {\n log.warn('No directories to watch');\n return null;\n }\n\n return createSkillWatcher(loader, {\n watchDirs,\n debounceMs: options.debounceMs ?? 1000,\n });\n}\n"],"mappings":";;;;;;;;;;;aASqD;AAGrD,MAAM,MAAM,aAAa,eAAe;;;;AAuBxC,SAAS,sBACP,QACA,YACY;CACZ,IAAI,UAAiC;CACrC,IAAI,gBAAgB;AAEpB,cAAa;AACX,MAAI,SAAS;AACX,mBAAgB;AAChB;;AAGF,YAAU,iBAAiB;AACzB,aAAU;AAEV,OAAI,eAAe;AACjB,oBAAgB;AAChB,WAAO,QAAQ;AACf,QAAI,KAAK,0CAA0C;;KAEpD,WAAW;;;;;;AAOlB,SAAgB,mBACd,QACA,SACc;CACd,MAAM,EAAE,WAAW,aAAa,QAAS;CAEzC,MAAM,WAAsE,EAAE;CAC9E,MAAM,kBAAkB,sBAAsB,QAAQ,WAAW;CACjE,IAAI,aAAa;CACjB,IAAI,kBAAkB,KAAK,KAAK;CAEhC,SAAS,eAAe,KAAmB;AACzC,MAAI;GACF,MAAM,UAAU,MAAM,KAAK,EAAE,WAAW,MAAM,GAAG,WAAW,aAAa;AACvE,QAAI,CAAC,SAAU;AAGf,QAAI,SAAS,aAAa,KAAK,YAAY;AACzC,SAAI,MAAM;MAAE;MAAW;MAAU;MAAK,EAAE,qBAAqB;AAC7D,uBAAkB,KAAK,KAAK;AAC5B,sBAAiB;;AAInB,QAAI,cAAc,UAAU;AAC1B,SAAI,MAAM;MAAE;MAAW;MAAU;MAAK,EAAE,sCAAsC;AAC9E,uBAAkB,KAAK,KAAK;AAC5B,sBAAiB;;KAEnB;AAEF,YAAS,KAAK;IAAE;IAAS;IAAK,CAAC;AAC/B,OAAI,KAAK,EAAE,KAAK,EAAE,2BAA2B;WACtC,KAAK;AACZ,OAAI,KAAK;IAAE;IAAK,OAAO;IAAK,EAAE,kCAAkC;;;AAIpE,QAAO;EACL,aAAa;AACX,OAAI,YAAY;AACd,QAAI,KAAK,0BAA0B;AACnC;;AAGF,QAAK,MAAM,OAAO,UAChB,gBAAe,IAAI;AAGrB,gBAAa;AACb,OAAI,KAAK,EAAE,MAAM,WAAW,EAAE,wBAAwB;;EAGxD,YAAY;AACV,OAAI,CAAC,WACH;AAGF,QAAK,MAAM,EAAE,aAAa,SACxB,SAAQ,OAAO;AAGjB,YAAS,SAAS;AAClB,gBAAa;AACb,OAAI,KAAK,wBAAwB;;EAGnC,kBAAkB;EAElB,0BAA0B;EAC3B;;;;;AAMH,SAAgB,wBACd,QACA,SAOqB;CACrB,MAAM,YAAsB,EAAE;AAG9B,KAAI,QAAQ,cAAc;EACxB,MAAM,qBAAqB,KAAK,QAAQ,cAAc,SAAS;AAC/D,YAAU,KAAK,mBAAmB;;AAIpC,KAAI,QAAQ,WACV,WAAU,KAAK,QAAQ,WAAW;AAIpC,KAAI,QAAQ,WACV,WAAU,KAAK,GAAG,QAAQ,WAAW;AAIvC,KAAI,QAAQ,UACV,WAAU,KAAK,GAAG,QAAQ,UAAU;AAGtC,KAAI,UAAU,WAAW,GAAG;AAC1B,MAAI,KAAK,0BAA0B;AACnC,SAAO;;AAGT,QAAO,mBAAmB,QAAQ;EAChC;EACA,YAAY,QAAQ,cAAc;EACnC,CAAC"}
1
+ {"version":3,"file":"watcher.js","names":[],"sources":["../../../../src/agent/skills/watcher.ts"],"sourcesContent":["/**\n * Skill Watcher\n * \n * Watches skill directories for changes and triggers reloads.\n *\n */\n\nimport { watch } from 'fs';\nimport { createLogger } from '../../utils/logger.js';\nimport type { SkillLoader } from './index.js';\nimport { resolveWorkspaceSkillsDir } from './workspace-skills-dir.js';\n\nconst log = createLogger('SkillWatcher');\n\nexport interface SkillWatcherOptions {\n /** Debounce time in milliseconds */\n debounceMs?: number;\n /** Watch these directories */\n watchDirs: string[];\n}\n\nexport interface SkillWatcher {\n /** Start watching */\n start: () => void;\n /** Stop watching */\n stop: () => void;\n /** Check if watching is active */\n isWatching: () => boolean;\n /** Get last refresh time */\n getLastRefreshTime: () => number;\n}\n\n/**\n * Create a debounced reload function\n */\nfunction createDebouncedReload(\n loader: SkillLoader,\n debounceMs: number\n): () => void {\n let timeout: NodeJS.Timeout | null = null;\n let pendingReload = false;\n\n return () => {\n if (timeout) {\n pendingReload = true;\n return;\n }\n\n timeout = setTimeout(() => {\n timeout = null;\n \n if (pendingReload) {\n pendingReload = false;\n loader.reload();\n log.info('Skills reloaded after directory changes');\n }\n }, debounceMs);\n };\n}\n\n/**\n * Create a skill watcher\n */\nexport function createSkillWatcher(\n loader: SkillLoader,\n options: SkillWatcherOptions\n): SkillWatcher {\n const { watchDirs, debounceMs = 1000 } = options;\n \n const watchers: Array<{ watcher: ReturnType<typeof watch>; dir: string }> = [];\n const debouncedReload = createDebouncedReload(loader, debounceMs);\n let isWatching = false;\n let lastRefreshTime = Date.now();\n\n function watchDirectory(dir: string): void {\n try {\n const watcher = watch(dir, { recursive: true }, (eventType, filename) => {\n if (!filename) return;\n\n // Only care about SKILL.md files\n if (filename.toLowerCase() === 'skill.md') {\n log.debug({ eventType, filename, dir }, 'Skill file changed');\n lastRefreshTime = Date.now();\n debouncedReload();\n }\n\n // Also watch for new directories (potential new skills)\n if (eventType === 'rename') {\n log.debug({ eventType, filename, dir }, 'Directory changed, may be new skill');\n lastRefreshTime = Date.now();\n debouncedReload();\n }\n });\n\n watchers.push({ watcher, dir });\n log.info({ dir }, 'Watching skill directory');\n } catch (err) {\n log.warn({ dir, error: err }, 'Failed to watch skill directory');\n }\n }\n\n return {\n start: () => {\n if (isWatching) {\n log.warn('Watcher already started');\n return;\n }\n\n for (const dir of watchDirs) {\n watchDirectory(dir);\n }\n\n isWatching = true;\n log.info({ dirs: watchDirs }, 'Skill watcher started');\n },\n\n stop: () => {\n if (!isWatching) {\n return;\n }\n\n for (const { watcher } of watchers) {\n watcher.close();\n }\n\n watchers.length = 0;\n isWatching = false;\n log.info('Skill watcher stopped');\n },\n\n isWatching: () => isWatching,\n\n getLastRefreshTime: () => lastRefreshTime,\n };\n}\n\n/**\n * Create a skill watcher from loader configuration\n */\nexport function createWatcherFromLoader(\n loader: SkillLoader,\n options: {\n workspaceDir?: string;\n builtinDir?: string;\n globalDirs?: string[];\n extraDirs?: string[];\n debounceMs?: number;\n }\n): SkillWatcher | null {\n const watchDirs: string[] = [];\n\n // Add project-local workspace skills directory\n if (options.workspaceDir) {\n watchDirs.push(resolveWorkspaceSkillsDir(options.workspaceDir));\n }\n\n // Add builtin directory\n if (options.builtinDir) {\n watchDirs.push(options.builtinDir);\n }\n\n // Add global directories\n if (options.globalDirs) {\n watchDirs.push(...options.globalDirs);\n }\n\n // Add extra directories\n if (options.extraDirs) {\n watchDirs.push(...options.extraDirs);\n }\n\n if (watchDirs.length === 0) {\n log.warn('No directories to watch');\n return null;\n }\n\n return createSkillWatcher(loader, {\n watchDirs,\n debounceMs: options.debounceMs ?? 1000,\n });\n}\n"],"mappings":";;;;;;;;;;;aAQqD;AAIrD,MAAM,MAAM,aAAa,eAAe;;;;AAuBxC,SAAS,sBACP,QACA,YACY;CACZ,IAAI,UAAiC;CACrC,IAAI,gBAAgB;AAEpB,cAAa;AACX,MAAI,SAAS;AACX,mBAAgB;AAChB;;AAGF,YAAU,iBAAiB;AACzB,aAAU;AAEV,OAAI,eAAe;AACjB,oBAAgB;AAChB,WAAO,QAAQ;AACf,QAAI,KAAK,0CAA0C;;KAEpD,WAAW;;;;;;AAOlB,SAAgB,mBACd,QACA,SACc;CACd,MAAM,EAAE,WAAW,aAAa,QAAS;CAEzC,MAAM,WAAsE,EAAE;CAC9E,MAAM,kBAAkB,sBAAsB,QAAQ,WAAW;CACjE,IAAI,aAAa;CACjB,IAAI,kBAAkB,KAAK,KAAK;CAEhC,SAAS,eAAe,KAAmB;AACzC,MAAI;GACF,MAAM,UAAU,MAAM,KAAK,EAAE,WAAW,MAAM,GAAG,WAAW,aAAa;AACvE,QAAI,CAAC,SAAU;AAGf,QAAI,SAAS,aAAa,KAAK,YAAY;AACzC,SAAI,MAAM;MAAE;MAAW;MAAU;MAAK,EAAE,qBAAqB;AAC7D,uBAAkB,KAAK,KAAK;AAC5B,sBAAiB;;AAInB,QAAI,cAAc,UAAU;AAC1B,SAAI,MAAM;MAAE;MAAW;MAAU;MAAK,EAAE,sCAAsC;AAC9E,uBAAkB,KAAK,KAAK;AAC5B,sBAAiB;;KAEnB;AAEF,YAAS,KAAK;IAAE;IAAS;IAAK,CAAC;AAC/B,OAAI,KAAK,EAAE,KAAK,EAAE,2BAA2B;WACtC,KAAK;AACZ,OAAI,KAAK;IAAE;IAAK,OAAO;IAAK,EAAE,kCAAkC;;;AAIpE,QAAO;EACL,aAAa;AACX,OAAI,YAAY;AACd,QAAI,KAAK,0BAA0B;AACnC;;AAGF,QAAK,MAAM,OAAO,UAChB,gBAAe,IAAI;AAGrB,gBAAa;AACb,OAAI,KAAK,EAAE,MAAM,WAAW,EAAE,wBAAwB;;EAGxD,YAAY;AACV,OAAI,CAAC,WACH;AAGF,QAAK,MAAM,EAAE,aAAa,SACxB,SAAQ,OAAO;AAGjB,YAAS,SAAS;AAClB,gBAAa;AACb,OAAI,KAAK,wBAAwB;;EAGnC,kBAAkB;EAElB,0BAA0B;EAC3B;;;;;AAMH,SAAgB,wBACd,QACA,SAOqB;CACrB,MAAM,YAAsB,EAAE;AAG9B,KAAI,QAAQ,aACV,WAAU,KAAK,0BAA0B,QAAQ,aAAa,CAAC;AAIjE,KAAI,QAAQ,WACV,WAAU,KAAK,QAAQ,WAAW;AAIpC,KAAI,QAAQ,WACV,WAAU,KAAK,GAAG,QAAQ,WAAW;AAIvC,KAAI,QAAQ,UACV,WAAU,KAAK,GAAG,QAAQ,UAAU;AAGtC,KAAI,UAAU,WAAW,GAAG;AAC1B,MAAI,KAAK,0BAA0B;AACnC,SAAO;;AAGT,QAAO,mBAAmB,QAAQ;EAChC;EACA,YAAY,QAAQ,cAAc;EACnC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function resolveWorkspaceSkillsDir(workspaceDir: string): string;
@@ -0,0 +1,11 @@
1
+ import { join } from "node:path";
2
+ //#region src/agent/skills/workspace-skills-dir.ts
3
+ const PROJECT_XOPC_DIR = ".xopc";
4
+ const PROJECT_SKILLS_DIR = "skills";
5
+ function resolveWorkspaceSkillsDir(workspaceDir) {
6
+ return join(workspaceDir, PROJECT_XOPC_DIR, PROJECT_SKILLS_DIR);
7
+ }
8
+ //#endregion
9
+ export { resolveWorkspaceSkillsDir };
10
+
11
+ //# sourceMappingURL=workspace-skills-dir.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-skills-dir.js","names":[],"sources":["../../../../src/agent/skills/workspace-skills-dir.ts"],"sourcesContent":["import { join } from 'node:path';\n\nconst PROJECT_XOPC_DIR = '.xopc';\nconst PROJECT_SKILLS_DIR = 'skills';\n\nexport function resolveWorkspaceSkillsDir(workspaceDir: string): string {\n return join(workspaceDir, PROJECT_XOPC_DIR, PROJECT_SKILLS_DIR);\n}\n"],"mappings":";;AAEA,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAE3B,SAAgB,0BAA0B,cAA8B;AACtE,QAAO,KAAK,cAAc,kBAAkB,mBAAmB"}
@@ -1 +1 @@
1
- export declare const browserUseManual = "# Browser Tool Manual\n\n## Overview\n\n`browser_use` is XOPC's unified browser tool. It supports four modes:\n\n| Mode | Purpose |\n|------|---------|\n| `command` | Run a single browser action |\n| `pipeline` | Run a multi-step YAML flow |\n| `inspect` | Read current page state |\n| `close` | Close the browser session |\n\n## When to use\n\n- Open pages, click controls, fill forms\n- Capture screenshots or page content\n- Run multi-step automation flows\n- Extract data from pages\n\n## Command mode\n\n### Basic shape\n\n```json\n{\n \"mode\": \"command\",\n \"command\": \"<action>\",\n \"args\": { ... }\n}\n```\n\n### Available commands\n\n| Command | Description | Key arguments |\n|---------|-------------|---------------|\n| `open` / `navigate` | Open a URL | `url`, `wait_until` |\n| `state` / `snapshot` | ARIA snapshot of the page | `selector`, `maxLength` |\n| `click` | Click an element | `selector` / `text` / `role` |\n| `type` / `input` | Type text | `selector` / `label`, `text`, `pressEnter` |\n| `scroll` | Scroll the page | `direction`, `amount` |\n| `screenshot` | Screenshot | `selector`, `full_page`, `path` |\n| `back` | Go back | `waitFor` |\n| `keys` / `press` | Keyboard input | `key` |\n| `console` / `eval` | Run JavaScript | `javascript` |\n| `images` | Collect images on the page | `selector`, `maxImages` |\n| `wait` | Wait for element/time | `selector` / `text` / `ms`, `timeout_ms` |\n| `dialog` | Handle dialogs | `action` (accept/dismiss) |\n| `close` | Close the current page | \u2014 |\n\n### Element targeting\n\nPrefer, in order:\n\n1. **ARIA role**: most stable, e.g. `\"role\": \"button:Submit\"`\n2. **Visible text**, e.g. `\"text\": \"Sign In\"`\n3. **CSS selector**, e.g. `\"selector\": \"#login-btn\"`\n\nGuidelines:\n\n- Use `inspect` or `state` first to get a snapshot\n- Pick locators from ARIA info in the snapshot\n- Avoid dynamic class names when possible\n\n## Inspect mode\n\nBefore acting, understand the current page:\n\n```json\n{ \"mode\": \"inspect\" }\n```\n\nReturns: current URL, title, ARIA snapshot.\n\n**Best practice**: after each navigation, inspect, then choose the next step.\n\n## Pipeline mode\n\nFor multi-step, reusable flows. Uses a brocli-style YAML DSL.\n\n### Run from file\n\n```json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"path\": \"./browser-flow.yaml\",\n \"args\": { \"url\": \"https://example.com\" }\n }\n}\n```\n\n### Run inline YAML\n\n```json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"name: quick-check\\npipeline:\\n - navigate:\\n url: https://example.com\\n - screenshot:\\n full_page: true\",\n \"dryRun\": false\n }\n}\n```\n\n### YAML layout\n\n```yaml\nname: pipeline-name\ndescription: What this flow does\nargs:\n url:\n type: string\n required: true\n query:\n type: string\n default: \"hello\"\npipeline:\n - navigate:\n url: ${{ args.url }}\n wait_until: domcontentloaded\n - wait:\n selector: input[name=\"q\"]\n - type:\n selector: input[name=\"q\"]\n text: ${{ args.query }}\n - press:\n key: Enter\n - wait:\n selector: body\n timeout_ms: 10000\n - screenshot:\n path: ./artifacts/result.png\n full_page: true\n - output:\n value:\n screenshot: ./artifacts/result.png\non_error:\n - screenshot:\n path: ./artifacts/error.png\n full_page: true\n```\n\n### Authoring rules\n\n1. One action per step\n2. Use `${{ args.xxx }}` for parameters\n3. Use `${{ data }}` or `${{ data | json }}` for the previous step's result\n4. Use `on_error` to collect diagnostics after failure\n\n### Validate without running\n\n```json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"...\",\n \"dryRun\": true\n }\n}\n```\n\n## Recovery when something fails\n\n1. `inspect` to see current state\n2. Confirm the URL (redirects may have changed it)\n3. Use the snapshot to see if the page finished loading\n4. `screenshot` for visual debugging\n5. `wait` for elements, then retry\n\n## Safety\n\n- Do not put API keys or tokens in URLs\n- Do not target private IPs / localhost unless configuration allows it\n- Cloud metadata endpoints are always blocked\n- For destructive actions (submit forms, delete data), confirm intent with the user first\n";
1
+ export declare const browserUseManual = "# Browser Tool Manual\n\n## Overview\n\n`browser_use` is XOPC's unified browser tool. It supports four modes:\n\n| Mode | Purpose |\n|------|---------|\n| `command` | Run a single browser action |\n| `pipeline` | Run a multi-step YAML flow |\n| `inspect` | Read current page state |\n| `close` | Close the browser session |\n\n## When to use\n\n- Open pages, click controls, fill forms\n- Capture screenshots or page content\n- Run multi-step automation flows\n- Extract data from pages\n\n## Command mode\n\n### Basic shape\n\n```json\n{\n \"mode\": \"command\",\n \"command\": \"<action>\",\n \"args\": { ... }\n}\n```\n\n### Available commands\n\n| Command | Description | Key arguments |\n|---------|-------------|---------------|\n| `open` / `navigate` | Open a URL | `url`, `wait_until` |\n| `state` / `snapshot` | ARIA snapshot of the page | `selector`, `maxLength` |\n| `click` | Click an element | `selector` / `text` / `role` |\n| `type` / `input` | Type text | `selector` / `label`, `text`, `pressEnter` |\n| `scroll` | Scroll the page | `direction`, `amount` |\n| `screenshot` | Screenshot | `selector`, `full_page`, `path` |\n| `back` | Go back | `waitFor` |\n| `keys` / `press` | Keyboard input | `key` |\n| `console` / `eval` | Run JavaScript | `javascript` |\n| `images` | Collect images on the page | `selector`, `maxImages` |\n| `wait` | Wait for element/text/function/time | `selector` / `text` / `function` / `ms`, `state`, `timeout_ms` |\n| `wait_for_navigation` | Wait for page load state | `wait_until`, `timeout_ms` |\n| `wait_for_network_idle` | Wait until network is idle | `timeout_ms` |\n| `element_text` | Read element text | `selector` |\n| `element_attribute` | Read element attribute | `selector`, `attribute` |\n| `bounding_box` | Read element box | `selector` |\n| `network_start` / `network_events` / `network_stop` | Capture network events | `url_pattern`, `include_body` |\n| `dialog` | Handle dialogs | `action` (accept/dismiss) |\n| `close` | Close the current page | \u2014 |\n\n### Element targeting\n\nPrefer, in order:\n\n1. **ARIA role**: most stable, e.g. `\"role\": \"button:Submit\"`\n2. **Visible text**, e.g. `\"text\": \"Sign In\"`\n3. **CSS selector**, e.g. `\"selector\": \"#login-btn\"`\n\nGuidelines:\n\n- Use `inspect` or `state` first to get a snapshot\n- Pick locators from ARIA info in the snapshot\n- Avoid dynamic class names when possible\n\n## Inspect mode\n\nBefore acting, understand the current page:\n\n```json\n{ \"mode\": \"inspect\" }\n```\n\nReturns: current URL, title, ARIA snapshot.\n\n**Best practice**: after each navigation, inspect, then choose the next step.\n\n## Pipeline mode\n\nFor multi-step, reusable flows. Uses a brocli-style YAML DSL.\n\n### Run from file\n\n```json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"path\": \"./browser-flow.yaml\",\n \"args\": { \"url\": \"https://example.com\" }\n }\n}\n```\n\n### Run inline YAML\n\n```json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"name: quick-check\\npipeline:\\n - navigate:\\n url: https://example.com\\n - screenshot:\\n full_page: true\",\n \"dryRun\": false\n }\n}\n```\n\n### YAML layout\n\n```yaml\nname: pipeline-name\ndescription: What this flow does\ntimeoutSeconds: 60\ninclude:\n - ./shared-login.yaml\nargs:\n url:\n type: string\n required: true\n query:\n type: string\n default: \"hello\"\npipeline:\n - navigate:\n url: ${{ args.url }}\n wait_until: domcontentloaded\n - wait:\n selector: input[name=\"q\"]\n - type:\n selector: input[name=\"q\"]\n text: ${{ args.query }}\n - press:\n key: Enter\n - wait:\n selector: body\n timeout_ms: 10000\n - element_text:\n selector: h1\n - assert:\n contains: Results\n - screenshot:\n path: ./artifacts/result.png\n full_page: true\n - output:\n value:\n screenshot: ./artifacts/result.png\non_error:\n - screenshot:\n path: ./artifacts/error.png\n full_page: true\n```\n\n### Authoring rules\n\n1. One action per step\n2. Use `${{ args.xxx }}` for parameters\n3. Use `${{ last }}` or `${{ last | json }}` for the previous step result\n4. Use `${{ outputs.0 }}` for prior `output` values and `${{ vars.name }}` for values from `set_var`\n5. Use `include` for shared YAML fragments; relative includes resolve from the current pipeline file\n6. Use `on_error` to collect diagnostics after failure\n\n### Stable pipeline actions\n\n```yaml\npipeline:\n - network_start:\n url_pattern: /api/search\n include_body: true\n - click:\n text: Search\n - wait_for_network_idle:\n timeout_ms: 10000\n - collect: {}\n - filter:\n path: type\n equals: response\n - limit:\n count: 3\n - output:\n value: ${{ last }}\n```\n\nControl flow:\n\n```yaml\npipeline:\n - retry:\n times: 3\n delay_ms: 500\n pipeline:\n - wait:\n selector: button.submit\n timeout_ms: 3000\n - click:\n selector: button.submit\n - if:\n condition: ${{ last }}\n then:\n - output:\n value: clicked\n else:\n - output:\n value: missing\n```\n\n### Validate without running\n\n```json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"...\",\n \"dryRun\": true\n }\n}\n```\n\n## Recovery when something fails\n\n1. `inspect` to see current state\n2. Confirm the URL (redirects may have changed it)\n3. Use the snapshot to see if the page finished loading\n4. `screenshot` for visual debugging\n5. `wait` for elements, then retry\n\n## Safety\n\n- Do not put API keys or tokens in URLs\n- Do not target private IPs / localhost unless configuration allows it\n- Cloud metadata endpoints are always blocked\n- For destructive actions (submit forms, delete data), confirm intent with the user first\n";
@@ -45,7 +45,13 @@ const browserUseManual = `# Browser Tool Manual
45
45
  | \`keys\` / \`press\` | Keyboard input | \`key\` |
46
46
  | \`console\` / \`eval\` | Run JavaScript | \`javascript\` |
47
47
  | \`images\` | Collect images on the page | \`selector\`, \`maxImages\` |
48
- | \`wait\` | Wait for element/time | \`selector\` / \`text\` / \`ms\`, \`timeout_ms\` |
48
+ | \`wait\` | Wait for element/text/function/time | \`selector\` / \`text\` / \`function\` / \`ms\`, \`state\`, \`timeout_ms\` |
49
+ | \`wait_for_navigation\` | Wait for page load state | \`wait_until\`, \`timeout_ms\` |
50
+ | \`wait_for_network_idle\` | Wait until network is idle | \`timeout_ms\` |
51
+ | \`element_text\` | Read element text | \`selector\` |
52
+ | \`element_attribute\` | Read element attribute | \`selector\`, \`attribute\` |
53
+ | \`bounding_box\` | Read element box | \`selector\` |
54
+ | \`network_start\` / \`network_events\` / \`network_stop\` | Capture network events | \`url_pattern\`, \`include_body\` |
49
55
  | \`dialog\` | Handle dialogs | \`action\` (accept/dismiss) |
50
56
  | \`close\` | Close the current page | — |
51
57
 
@@ -108,6 +114,9 @@ For multi-step, reusable flows. Uses a brocli-style YAML DSL.
108
114
  \`\`\`yaml
109
115
  name: pipeline-name
110
116
  description: What this flow does
117
+ timeoutSeconds: 60
118
+ include:
119
+ - ./shared-login.yaml
111
120
  args:
112
121
  url:
113
122
  type: string
@@ -129,6 +138,10 @@ pipeline:
129
138
  - wait:
130
139
  selector: body
131
140
  timeout_ms: 10000
141
+ - element_text:
142
+ selector: h1
143
+ - assert:
144
+ contains: Results
132
145
  - screenshot:
133
146
  path: ./artifacts/result.png
134
147
  full_page: true
@@ -145,8 +158,54 @@ on_error:
145
158
 
146
159
  1. One action per step
147
160
  2. Use \`\${{ args.xxx }}\` for parameters
148
- 3. Use \`\${{ data }}\` or \`\${{ data | json }}\` for the previous step's result
149
- 4. Use \`on_error\` to collect diagnostics after failure
161
+ 3. Use \`\${{ last }}\` or \`\${{ last | json }}\` for the previous step result
162
+ 4. Use \`\${{ outputs.0 }}\` for prior \`output\` values and \`\${{ vars.name }}\` for values from \`set_var\`
163
+ 5. Use \`include\` for shared YAML fragments; relative includes resolve from the current pipeline file
164
+ 6. Use \`on_error\` to collect diagnostics after failure
165
+
166
+ ### Stable pipeline actions
167
+
168
+ \`\`\`yaml
169
+ pipeline:
170
+ - network_start:
171
+ url_pattern: /api/search
172
+ include_body: true
173
+ - click:
174
+ text: Search
175
+ - wait_for_network_idle:
176
+ timeout_ms: 10000
177
+ - collect: {}
178
+ - filter:
179
+ path: type
180
+ equals: response
181
+ - limit:
182
+ count: 3
183
+ - output:
184
+ value: \${{ last }}
185
+ \`\`\`
186
+
187
+ Control flow:
188
+
189
+ \`\`\`yaml
190
+ pipeline:
191
+ - retry:
192
+ times: 3
193
+ delay_ms: 500
194
+ pipeline:
195
+ - wait:
196
+ selector: button.submit
197
+ timeout_ms: 3000
198
+ - click:
199
+ selector: button.submit
200
+ - if:
201
+ condition: \${{ last }}
202
+ then:
203
+ - output:
204
+ value: clicked
205
+ else:
206
+ - output:
207
+ value: missing
208
+ \`\`\`
150
209
 
151
210
  ### Validate without running
152
211
 
@@ -1 +1 @@
1
- {"version":3,"file":"browser.js","names":[],"sources":["../../../../src/agent/tool-manuals/browser.ts"],"sourcesContent":["export const browserUseManual = `# Browser Tool Manual\n\n## Overview\n\n\\`browser_use\\` is XOPC's unified browser tool. It supports four modes:\n\n| Mode | Purpose |\n|------|---------|\n| \\`command\\` | Run a single browser action |\n| \\`pipeline\\` | Run a multi-step YAML flow |\n| \\`inspect\\` | Read current page state |\n| \\`close\\` | Close the browser session |\n\n## When to use\n\n- Open pages, click controls, fill forms\n- Capture screenshots or page content\n- Run multi-step automation flows\n- Extract data from pages\n\n## Command mode\n\n### Basic shape\n\n\\`\\`\\`json\n{\n \"mode\": \"command\",\n \"command\": \"<action>\",\n \"args\": { ... }\n}\n\\`\\`\\`\n\n### Available commands\n\n| Command | Description | Key arguments |\n|---------|-------------|---------------|\n| \\`open\\` / \\`navigate\\` | Open a URL | \\`url\\`, \\`wait_until\\` |\n| \\`state\\` / \\`snapshot\\` | ARIA snapshot of the page | \\`selector\\`, \\`maxLength\\` |\n| \\`click\\` | Click an element | \\`selector\\` / \\`text\\` / \\`role\\` |\n| \\`type\\` / \\`input\\` | Type text | \\`selector\\` / \\`label\\`, \\`text\\`, \\`pressEnter\\` |\n| \\`scroll\\` | Scroll the page | \\`direction\\`, \\`amount\\` |\n| \\`screenshot\\` | Screenshot | \\`selector\\`, \\`full_page\\`, \\`path\\` |\n| \\`back\\` | Go back | \\`waitFor\\` |\n| \\`keys\\` / \\`press\\` | Keyboard input | \\`key\\` |\n| \\`console\\` / \\`eval\\` | Run JavaScript | \\`javascript\\` |\n| \\`images\\` | Collect images on the page | \\`selector\\`, \\`maxImages\\` |\n| \\`wait\\` | Wait for element/time | \\`selector\\` / \\`text\\` / \\`ms\\`, \\`timeout_ms\\` |\n| \\`dialog\\` | Handle dialogs | \\`action\\` (accept/dismiss) |\n| \\`close\\` | Close the current page | — |\n\n### Element targeting\n\nPrefer, in order:\n\n1. **ARIA role**: most stable, e.g. \\`\"role\": \"button:Submit\"\\`\n2. **Visible text**, e.g. \\`\"text\": \"Sign In\"\\`\n3. **CSS selector**, e.g. \\`\"selector\": \"#login-btn\"\\`\n\nGuidelines:\n\n- Use \\`inspect\\` or \\`state\\` first to get a snapshot\n- Pick locators from ARIA info in the snapshot\n- Avoid dynamic class names when possible\n\n## Inspect mode\n\nBefore acting, understand the current page:\n\n\\`\\`\\`json\n{ \"mode\": \"inspect\" }\n\\`\\`\\`\n\nReturns: current URL, title, ARIA snapshot.\n\n**Best practice**: after each navigation, inspect, then choose the next step.\n\n## Pipeline mode\n\nFor multi-step, reusable flows. Uses a brocli-style YAML DSL.\n\n### Run from file\n\n\\`\\`\\`json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"path\": \"./browser-flow.yaml\",\n \"args\": { \"url\": \"https://example.com\" }\n }\n}\n\\`\\`\\`\n\n### Run inline YAML\n\n\\`\\`\\`json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"name: quick-check\\\\npipeline:\\\\n - navigate:\\\\n url: https://example.com\\\\n - screenshot:\\\\n full_page: true\",\n \"dryRun\": false\n }\n}\n\\`\\`\\`\n\n### YAML layout\n\n\\`\\`\\`yaml\nname: pipeline-name\ndescription: What this flow does\nargs:\n url:\n type: string\n required: true\n query:\n type: string\n default: \"hello\"\npipeline:\n - navigate:\n url: \\${{ args.url }}\n wait_until: domcontentloaded\n - wait:\n selector: input[name=\"q\"]\n - type:\n selector: input[name=\"q\"]\n text: \\${{ args.query }}\n - press:\n key: Enter\n - wait:\n selector: body\n timeout_ms: 10000\n - screenshot:\n path: ./artifacts/result.png\n full_page: true\n - output:\n value:\n screenshot: ./artifacts/result.png\non_error:\n - screenshot:\n path: ./artifacts/error.png\n full_page: true\n\\`\\`\\`\n\n### Authoring rules\n\n1. One action per step\n2. Use \\`\\${{ args.xxx }}\\` for parameters\n3. Use \\`\\${{ data }}\\` or \\`\\${{ data | json }}\\` for the previous step's result\n4. Use \\`on_error\\` to collect diagnostics after failure\n\n### Validate without running\n\n\\`\\`\\`json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"...\",\n \"dryRun\": true\n }\n}\n\\`\\`\\`\n\n## Recovery when something fails\n\n1. \\`inspect\\` to see current state\n2. Confirm the URL (redirects may have changed it)\n3. Use the snapshot to see if the page finished loading\n4. \\`screenshot\\` for visual debugging\n5. \\`wait\\` for elements, then retry\n\n## Safety\n\n- Do not put API keys or tokens in URLs\n- Do not target private IPs / localhost unless configuration allows it\n- Cloud metadata endpoints are always blocked\n- For destructive actions (submit forms, delete data), confirm intent with the user first\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB"}
1
+ {"version":3,"file":"browser.js","names":[],"sources":["../../../../src/agent/tool-manuals/browser.ts"],"sourcesContent":["export const browserUseManual = `# Browser Tool Manual\n\n## Overview\n\n\\`browser_use\\` is XOPC's unified browser tool. It supports four modes:\n\n| Mode | Purpose |\n|------|---------|\n| \\`command\\` | Run a single browser action |\n| \\`pipeline\\` | Run a multi-step YAML flow |\n| \\`inspect\\` | Read current page state |\n| \\`close\\` | Close the browser session |\n\n## When to use\n\n- Open pages, click controls, fill forms\n- Capture screenshots or page content\n- Run multi-step automation flows\n- Extract data from pages\n\n## Command mode\n\n### Basic shape\n\n\\`\\`\\`json\n{\n \"mode\": \"command\",\n \"command\": \"<action>\",\n \"args\": { ... }\n}\n\\`\\`\\`\n\n### Available commands\n\n| Command | Description | Key arguments |\n|---------|-------------|---------------|\n| \\`open\\` / \\`navigate\\` | Open a URL | \\`url\\`, \\`wait_until\\` |\n| \\`state\\` / \\`snapshot\\` | ARIA snapshot of the page | \\`selector\\`, \\`maxLength\\` |\n| \\`click\\` | Click an element | \\`selector\\` / \\`text\\` / \\`role\\` |\n| \\`type\\` / \\`input\\` | Type text | \\`selector\\` / \\`label\\`, \\`text\\`, \\`pressEnter\\` |\n| \\`scroll\\` | Scroll the page | \\`direction\\`, \\`amount\\` |\n| \\`screenshot\\` | Screenshot | \\`selector\\`, \\`full_page\\`, \\`path\\` |\n| \\`back\\` | Go back | \\`waitFor\\` |\n| \\`keys\\` / \\`press\\` | Keyboard input | \\`key\\` |\n| \\`console\\` / \\`eval\\` | Run JavaScript | \\`javascript\\` |\n| \\`images\\` | Collect images on the page | \\`selector\\`, \\`maxImages\\` |\n| \\`wait\\` | Wait for element/text/function/time | \\`selector\\` / \\`text\\` / \\`function\\` / \\`ms\\`, \\`state\\`, \\`timeout_ms\\` |\n| \\`wait_for_navigation\\` | Wait for page load state | \\`wait_until\\`, \\`timeout_ms\\` |\n| \\`wait_for_network_idle\\` | Wait until network is idle | \\`timeout_ms\\` |\n| \\`element_text\\` | Read element text | \\`selector\\` |\n| \\`element_attribute\\` | Read element attribute | \\`selector\\`, \\`attribute\\` |\n| \\`bounding_box\\` | Read element box | \\`selector\\` |\n| \\`network_start\\` / \\`network_events\\` / \\`network_stop\\` | Capture network events | \\`url_pattern\\`, \\`include_body\\` |\n| \\`dialog\\` | Handle dialogs | \\`action\\` (accept/dismiss) |\n| \\`close\\` | Close the current page | — |\n\n### Element targeting\n\nPrefer, in order:\n\n1. **ARIA role**: most stable, e.g. \\`\"role\": \"button:Submit\"\\`\n2. **Visible text**, e.g. \\`\"text\": \"Sign In\"\\`\n3. **CSS selector**, e.g. \\`\"selector\": \"#login-btn\"\\`\n\nGuidelines:\n\n- Use \\`inspect\\` or \\`state\\` first to get a snapshot\n- Pick locators from ARIA info in the snapshot\n- Avoid dynamic class names when possible\n\n## Inspect mode\n\nBefore acting, understand the current page:\n\n\\`\\`\\`json\n{ \"mode\": \"inspect\" }\n\\`\\`\\`\n\nReturns: current URL, title, ARIA snapshot.\n\n**Best practice**: after each navigation, inspect, then choose the next step.\n\n## Pipeline mode\n\nFor multi-step, reusable flows. Uses a brocli-style YAML DSL.\n\n### Run from file\n\n\\`\\`\\`json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"path\": \"./browser-flow.yaml\",\n \"args\": { \"url\": \"https://example.com\" }\n }\n}\n\\`\\`\\`\n\n### Run inline YAML\n\n\\`\\`\\`json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"name: quick-check\\\\npipeline:\\\\n - navigate:\\\\n url: https://example.com\\\\n - screenshot:\\\\n full_page: true\",\n \"dryRun\": false\n }\n}\n\\`\\`\\`\n\n### YAML layout\n\n\\`\\`\\`yaml\nname: pipeline-name\ndescription: What this flow does\ntimeoutSeconds: 60\ninclude:\n - ./shared-login.yaml\nargs:\n url:\n type: string\n required: true\n query:\n type: string\n default: \"hello\"\npipeline:\n - navigate:\n url: \\${{ args.url }}\n wait_until: domcontentloaded\n - wait:\n selector: input[name=\"q\"]\n - type:\n selector: input[name=\"q\"]\n text: \\${{ args.query }}\n - press:\n key: Enter\n - wait:\n selector: body\n timeout_ms: 10000\n - element_text:\n selector: h1\n - assert:\n contains: Results\n - screenshot:\n path: ./artifacts/result.png\n full_page: true\n - output:\n value:\n screenshot: ./artifacts/result.png\non_error:\n - screenshot:\n path: ./artifacts/error.png\n full_page: true\n\\`\\`\\`\n\n### Authoring rules\n\n1. One action per step\n2. Use \\`\\${{ args.xxx }}\\` for parameters\n3. Use \\`\\${{ last }}\\` or \\`\\${{ last | json }}\\` for the previous step result\n4. Use \\`\\${{ outputs.0 }}\\` for prior \\`output\\` values and \\`\\${{ vars.name }}\\` for values from \\`set_var\\`\n5. Use \\`include\\` for shared YAML fragments; relative includes resolve from the current pipeline file\n6. Use \\`on_error\\` to collect diagnostics after failure\n\n### Stable pipeline actions\n\n\\`\\`\\`yaml\npipeline:\n - network_start:\n url_pattern: /api/search\n include_body: true\n - click:\n text: Search\n - wait_for_network_idle:\n timeout_ms: 10000\n - collect: {}\n - filter:\n path: type\n equals: response\n - limit:\n count: 3\n - output:\n value: \\${{ last }}\n\\`\\`\\`\n\nControl flow:\n\n\\`\\`\\`yaml\npipeline:\n - retry:\n times: 3\n delay_ms: 500\n pipeline:\n - wait:\n selector: button.submit\n timeout_ms: 3000\n - click:\n selector: button.submit\n - if:\n condition: \\${{ last }}\n then:\n - output:\n value: clicked\n else:\n - output:\n value: missing\n\\`\\`\\`\n\n### Validate without running\n\n\\`\\`\\`json\n{\n \"mode\": \"pipeline\",\n \"pipeline\": {\n \"yaml\": \"...\",\n \"dryRun\": true\n }\n}\n\\`\\`\\`\n\n## Recovery when something fails\n\n1. \\`inspect\\` to see current state\n2. Confirm the URL (redirects may have changed it)\n3. Use the snapshot to see if the page finished loading\n4. \\`screenshot\\` for visual debugging\n5. \\`wait\\` for elements, then retry\n\n## Safety\n\n- Do not put API keys or tokens in URLs\n- Do not target private IPs / localhost unless configuration allows it\n- Cloud metadata endpoints are always blocked\n- For destructive actions (submit forms, delete data), confirm intent with the user first\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB"}
@@ -25,6 +25,9 @@ export interface CreateBrowserUseToolDeps {
25
25
  */
26
26
  getReadiness?: () => Promise<BrowserNotReadyError | null>;
27
27
  /** Pipeline runner (injected to avoid circular deps; lazy-loaded if not provided). */
28
- runPipeline?: (yaml: string, args: Record<string, unknown>, ctx: BrowserActionContext, dryRun: boolean) => Promise<BrowserActionResult>;
28
+ runPipeline?: (yaml: string, args: Record<string, unknown>, ctx: BrowserActionContext, options: {
29
+ dryRun?: boolean;
30
+ sourceLocation?: string;
31
+ }) => Promise<BrowserActionResult>;
29
32
  }
30
33
  export declare function createBrowserUseTool(deps: CreateBrowserUseToolDeps): AgentTool<any, any>;
@@ -185,9 +185,12 @@ function createBrowserUseTool(deps) {
185
185
  mode: "pipeline"
186
186
  }
187
187
  };
188
- let yamlSource = pipelineParams.yaml ?? pipelineParams.script ?? "";
188
+ let yamlSource = pipelineParams.yaml ?? "";
189
+ let sourceLocation;
189
190
  if (!yamlSource && pipelineParams.path) try {
190
- yamlSource = (await loadBrowserPipelineSource(pipelineParams.path)).source;
191
+ const loaded = await loadBrowserPipelineSource(pipelineParams.path);
192
+ yamlSource = loaded.source;
193
+ sourceLocation = loaded.location;
191
194
  } catch (e) {
192
195
  const msg = e instanceof Error ? e.message : String(e);
193
196
  return {
@@ -215,10 +218,16 @@ function createBrowserUseTool(deps) {
215
218
  const pipelineArgs = pipelineParams.args ?? {};
216
219
  const dryRun = pipelineParams.dryRun === true;
217
220
  const ctx = buildContext(await deps.getPageForTask(), signal);
218
- if (deps.runPipeline) return formatResult(await deps.runPipeline(yamlSource, pipelineArgs, ctx, dryRun));
221
+ if (deps.runPipeline) return formatResult(await deps.runPipeline(yamlSource, pipelineArgs, ctx, {
222
+ dryRun,
223
+ sourceLocation
224
+ }));
219
225
  try {
220
226
  const { runBrowserPipeline } = await import("../../../../browser/pipeline/runner.js");
221
- return formatResult(await runBrowserPipeline(yamlSource, pipelineArgs, ctx, registry, dryRun));
227
+ return formatResult(await runBrowserPipeline(yamlSource, pipelineArgs, ctx, registry, {
228
+ dryRun,
229
+ sourceLocation
230
+ }));
222
231
  } catch (e) {
223
232
  const msg = e instanceof Error ? e.message : String(e);
224
233
  log.error({ err: e }, `Pipeline execution failed: ${msg}`);