plugin-agent-orchestrator 1.0.13 → 1.0.15

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 (255) hide show
  1. package/README.md +16 -291
  2. package/dist/client/AIEmployeesContext.d.ts +7 -0
  3. package/dist/client/OrchestratorSettings.d.ts +2 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/client/plugin.d.ts +1 -0
  6. package/dist/client/skill-hub/components/ExecutionHistory.d.ts +2 -0
  7. package/dist/client/skill-hub/components/ExecutionProgress.d.ts +20 -0
  8. package/dist/client/skill-hub/components/GitSkillImport.d.ts +7 -0
  9. package/dist/client/skill-hub/components/SkillEditor.d.ts +7 -0
  10. package/dist/client/skill-hub/components/SkillManager.d.ts +2 -0
  11. package/dist/client/skill-hub/components/SkillMetrics.d.ts +2 -0
  12. package/dist/client/skill-hub/components/SkillTestPanel.d.ts +7 -0
  13. package/dist/client/skill-hub/index.d.ts +10 -0
  14. package/dist/client/skill-hub/locale.d.ts +3 -0
  15. package/dist/client/skill-hub/tools/InteractionSchemasProvider.d.ts +19 -0
  16. package/dist/client/skill-hub/tools/SkillHubCard.d.ts +3 -0
  17. package/dist/client/skill-hub/utils/jsonFields.d.ts +3 -0
  18. package/dist/externalVersion.js +6 -6
  19. package/dist/node_modules/adm-zip/LICENSE +21 -0
  20. package/dist/node_modules/adm-zip/adm-zip.js +1 -0
  21. package/dist/node_modules/adm-zip/headers/entryHeader.js +377 -0
  22. package/dist/node_modules/adm-zip/headers/index.js +2 -0
  23. package/dist/node_modules/adm-zip/headers/mainHeader.js +130 -0
  24. package/dist/node_modules/adm-zip/methods/deflater.js +33 -0
  25. package/dist/node_modules/adm-zip/methods/index.js +3 -0
  26. package/dist/node_modules/adm-zip/methods/inflater.js +34 -0
  27. package/dist/node_modules/adm-zip/methods/zipcrypto.js +175 -0
  28. package/dist/node_modules/adm-zip/package.json +1 -0
  29. package/dist/node_modules/adm-zip/util/constants.js +142 -0
  30. package/dist/node_modules/adm-zip/util/decoder.js +5 -0
  31. package/dist/node_modules/adm-zip/util/errors.js +63 -0
  32. package/dist/node_modules/adm-zip/util/fattr.js +76 -0
  33. package/dist/node_modules/adm-zip/util/index.js +5 -0
  34. package/dist/node_modules/adm-zip/util/utils.js +339 -0
  35. package/dist/node_modules/adm-zip/zipEntry.js +405 -0
  36. package/dist/node_modules/adm-zip/zipFile.js +446 -0
  37. package/dist/node_modules/simple-git/dist/cjs/index.js +7399 -0
  38. package/dist/node_modules/simple-git/dist/esm/index.js +4745 -0
  39. package/dist/node_modules/simple-git/dist/esm/package.json +3 -0
  40. package/dist/node_modules/simple-git/dist/src/lib/api.d.ts +13 -0
  41. package/dist/node_modules/simple-git/dist/src/lib/args/log-format.d.ts +9 -0
  42. package/dist/node_modules/simple-git/dist/src/lib/errors/git-construct-error.d.ts +15 -0
  43. package/dist/node_modules/simple-git/dist/src/lib/errors/git-error.d.ts +30 -0
  44. package/dist/node_modules/simple-git/dist/src/lib/errors/git-plugin-error.d.ts +7 -0
  45. package/dist/node_modules/simple-git/dist/src/lib/errors/git-response-error.d.ts +32 -0
  46. package/dist/node_modules/simple-git/dist/src/lib/errors/task-configuration-error.d.ts +12 -0
  47. package/dist/node_modules/simple-git/dist/src/lib/git-factory.d.ts +15 -0
  48. package/dist/node_modules/simple-git/dist/src/lib/git-logger.d.ts +21 -0
  49. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch-delete.d.ts +5 -0
  50. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-branch.d.ts +2 -0
  51. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-commit.d.ts +2 -0
  52. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-diff-summary.d.ts +3 -0
  53. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-fetch.d.ts +2 -0
  54. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-list-log-summary.d.ts +6 -0
  55. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-merge.d.ts +11 -0
  56. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-move.d.ts +2 -0
  57. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-pull.d.ts +6 -0
  58. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-push.d.ts +4 -0
  59. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-messages.d.ts +5 -0
  60. package/dist/node_modules/simple-git/dist/src/lib/parsers/parse-remote-objects.d.ts +3 -0
  61. package/dist/node_modules/simple-git/dist/src/lib/plugins/abort-plugin.d.ts +3 -0
  62. package/dist/node_modules/simple-git/dist/src/lib/plugins/block-unsafe-operations-plugin.d.ts +3 -0
  63. package/dist/node_modules/simple-git/dist/src/lib/plugins/command-config-prefixing-plugin.d.ts +2 -0
  64. package/dist/node_modules/simple-git/dist/src/lib/plugins/completion-detection.plugin.d.ts +3 -0
  65. package/dist/node_modules/simple-git/dist/src/lib/plugins/custom-binary.plugin.d.ts +3 -0
  66. package/dist/node_modules/simple-git/dist/src/lib/plugins/error-detection.plugin.d.ts +7 -0
  67. package/dist/node_modules/simple-git/dist/src/lib/plugins/index.d.ts +11 -0
  68. package/dist/node_modules/simple-git/dist/src/lib/plugins/plugin-store.d.ts +11 -0
  69. package/dist/node_modules/simple-git/dist/src/lib/plugins/progress-monitor-plugin.d.ts +3 -0
  70. package/dist/node_modules/simple-git/dist/src/lib/plugins/simple-git-plugin.d.ts +48 -0
  71. package/dist/node_modules/simple-git/dist/src/lib/plugins/spawn-options-plugin.d.ts +3 -0
  72. package/dist/node_modules/simple-git/dist/src/lib/plugins/suffix-paths.plugin.d.ts +2 -0
  73. package/dist/node_modules/simple-git/dist/src/lib/plugins/timout-plugin.d.ts +3 -0
  74. package/dist/node_modules/simple-git/dist/src/lib/responses/BranchDeleteSummary.d.ts +12 -0
  75. package/dist/node_modules/simple-git/dist/src/lib/responses/BranchSummary.d.ts +14 -0
  76. package/dist/node_modules/simple-git/dist/src/lib/responses/CheckIgnore.d.ts +4 -0
  77. package/dist/node_modules/simple-git/dist/src/lib/responses/CleanSummary.d.ts +9 -0
  78. package/dist/node_modules/simple-git/dist/src/lib/responses/ConfigList.d.ts +13 -0
  79. package/dist/node_modules/simple-git/dist/src/lib/responses/DiffSummary.d.ts +10 -0
  80. package/dist/node_modules/simple-git/dist/src/lib/responses/FileStatusSummary.d.ts +9 -0
  81. package/dist/node_modules/simple-git/dist/src/lib/responses/GetRemoteSummary.d.ts +11 -0
  82. package/dist/node_modules/simple-git/dist/src/lib/responses/InitSummary.d.ts +9 -0
  83. package/dist/node_modules/simple-git/dist/src/lib/responses/MergeSummary.d.ts +16 -0
  84. package/dist/node_modules/simple-git/dist/src/lib/responses/PullSummary.d.ts +25 -0
  85. package/dist/node_modules/simple-git/dist/src/lib/responses/StatusSummary.d.ts +19 -0
  86. package/dist/node_modules/simple-git/dist/src/lib/responses/TagList.d.ts +7 -0
  87. package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor-chain.d.ts +25 -0
  88. package/dist/node_modules/simple-git/dist/src/lib/runners/git-executor.d.ts +14 -0
  89. package/dist/node_modules/simple-git/dist/src/lib/runners/promise-wrapped.d.ts +2 -0
  90. package/dist/node_modules/simple-git/dist/src/lib/runners/scheduler.d.ts +11 -0
  91. package/dist/node_modules/simple-git/dist/src/lib/runners/tasks-pending-queue.d.ts +23 -0
  92. package/dist/node_modules/simple-git/dist/src/lib/simple-git-api.d.ts +20 -0
  93. package/dist/node_modules/simple-git/dist/src/lib/task-callback.d.ts +2 -0
  94. package/dist/node_modules/simple-git/dist/src/lib/tasks/apply-patch.d.ts +3 -0
  95. package/dist/node_modules/simple-git/dist/src/lib/tasks/branch.d.ts +7 -0
  96. package/dist/node_modules/simple-git/dist/src/lib/tasks/change-working-directory.d.ts +2 -0
  97. package/dist/node_modules/simple-git/dist/src/lib/tasks/check-ignore.d.ts +2 -0
  98. package/dist/node_modules/simple-git/dist/src/lib/tasks/check-is-repo.d.ts +9 -0
  99. package/dist/node_modules/simple-git/dist/src/lib/tasks/checkout.d.ts +2 -0
  100. package/dist/node_modules/simple-git/dist/src/lib/tasks/clean.d.ts +25 -0
  101. package/dist/node_modules/simple-git/dist/src/lib/tasks/clone.d.ts +9 -0
  102. package/dist/node_modules/simple-git/dist/src/lib/tasks/commit.d.ts +4 -0
  103. package/dist/node_modules/simple-git/dist/src/lib/tasks/config.d.ts +8 -0
  104. package/dist/node_modules/simple-git/dist/src/lib/tasks/count-objects.d.ts +12 -0
  105. package/dist/node_modules/simple-git/dist/src/lib/tasks/diff-name-status.d.ts +12 -0
  106. package/dist/node_modules/simple-git/dist/src/lib/tasks/diff.d.ts +5 -0
  107. package/dist/node_modules/simple-git/dist/src/lib/tasks/fetch.d.ts +4 -0
  108. package/dist/node_modules/simple-git/dist/src/lib/tasks/first-commit.d.ts +2 -0
  109. package/dist/node_modules/simple-git/dist/src/lib/tasks/grep.d.ts +12 -0
  110. package/dist/node_modules/simple-git/dist/src/lib/tasks/hash-object.d.ts +5 -0
  111. package/dist/node_modules/simple-git/dist/src/lib/tasks/init.d.ts +3 -0
  112. package/dist/node_modules/simple-git/dist/src/lib/tasks/log.d.ts +32 -0
  113. package/dist/node_modules/simple-git/dist/src/lib/tasks/merge.d.ts +4 -0
  114. package/dist/node_modules/simple-git/dist/src/lib/tasks/move.d.ts +3 -0
  115. package/dist/node_modules/simple-git/dist/src/lib/tasks/pull.d.ts +3 -0
  116. package/dist/node_modules/simple-git/dist/src/lib/tasks/push.d.ts +9 -0
  117. package/dist/node_modules/simple-git/dist/src/lib/tasks/remote.d.ts +8 -0
  118. package/dist/node_modules/simple-git/dist/src/lib/tasks/reset.d.ts +11 -0
  119. package/dist/node_modules/simple-git/dist/src/lib/tasks/show.d.ts +2 -0
  120. package/dist/node_modules/simple-git/dist/src/lib/tasks/stash-list.d.ts +4 -0
  121. package/dist/node_modules/simple-git/dist/src/lib/tasks/status.d.ts +3 -0
  122. package/dist/node_modules/simple-git/dist/src/lib/tasks/sub-module.d.ts +5 -0
  123. package/dist/node_modules/simple-git/dist/src/lib/tasks/tag.d.ts +18 -0
  124. package/dist/node_modules/simple-git/dist/src/lib/tasks/task.d.ts +14 -0
  125. package/dist/node_modules/simple-git/dist/src/lib/tasks/version.d.ts +9 -0
  126. package/dist/node_modules/simple-git/dist/src/lib/types/handlers.d.ts +21 -0
  127. package/dist/node_modules/simple-git/dist/src/lib/types/index.d.ts +136 -0
  128. package/dist/node_modules/simple-git/dist/src/lib/types/tasks.d.ts +19 -0
  129. package/dist/node_modules/simple-git/dist/src/lib/utils/argument-filters.d.ts +14 -0
  130. package/dist/node_modules/simple-git/dist/src/lib/utils/exit-codes.d.ts +10 -0
  131. package/dist/node_modules/simple-git/dist/src/lib/utils/git-output-streams.d.ts +7 -0
  132. package/dist/node_modules/simple-git/dist/src/lib/utils/index.d.ts +8 -0
  133. package/dist/node_modules/simple-git/dist/src/lib/utils/line-parser.d.ts +15 -0
  134. package/dist/node_modules/simple-git/dist/src/lib/utils/simple-git-options.d.ts +2 -0
  135. package/dist/node_modules/simple-git/dist/src/lib/utils/task-options.d.ts +13 -0
  136. package/dist/node_modules/simple-git/dist/src/lib/utils/task-parser.d.ts +5 -0
  137. package/dist/node_modules/simple-git/dist/src/lib/utils/util.d.ts +47 -0
  138. package/dist/node_modules/simple-git/dist/typings/errors.d.ts +5 -0
  139. package/dist/node_modules/simple-git/dist/typings/index.d.ts +14 -0
  140. package/dist/node_modules/simple-git/dist/typings/response.d.ts +556 -0
  141. package/dist/node_modules/simple-git/dist/typings/simple-git.d.ts +1033 -0
  142. package/dist/node_modules/simple-git/dist/typings/types.d.ts +22 -0
  143. package/dist/node_modules/simple-git/node_modules/debug/package.json +64 -0
  144. package/dist/node_modules/simple-git/node_modules/debug/src/browser.js +272 -0
  145. package/dist/node_modules/simple-git/node_modules/debug/src/common.js +292 -0
  146. package/dist/node_modules/simple-git/node_modules/debug/src/index.js +10 -0
  147. package/dist/node_modules/simple-git/node_modules/debug/src/node.js +263 -0
  148. package/dist/node_modules/simple-git/package.json +1 -0
  149. package/dist/node_modules/simple-git/promise.js +17 -0
  150. package/dist/server/collections/agent-execution-spans.d.ts +9 -0
  151. package/dist/server/collections/agent-execution-spans.js +152 -0
  152. package/dist/server/collections/orchestrator-config.d.ts +1 -1
  153. package/dist/server/collections/orchestrator-config.js +6 -0
  154. package/dist/server/collections/orchestrator-logs.d.ts +1 -1
  155. package/dist/server/collections/skill-definitions.d.ts +2 -0
  156. package/dist/server/collections/skill-definitions.js +158 -0
  157. package/dist/server/collections/skill-executions.d.ts +2 -0
  158. package/dist/server/collections/skill-executions.js +123 -0
  159. package/dist/server/collections/skill-worker-configs.d.ts +2 -0
  160. package/dist/server/collections/skill-worker-configs.js +115 -0
  161. package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +4 -0
  162. package/dist/server/migrations/20260423000000-add-progress-fields.js +69 -0
  163. package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +4 -0
  164. package/dist/server/migrations/20260425000000-add-interaction-schema.js +61 -0
  165. package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +4 -0
  166. package/dist/server/migrations/20260427000000-change-packages-to-text.js +70 -0
  167. package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +4 -0
  168. package/dist/server/migrations/20260427000001-change-other-json-to-text.js +80 -0
  169. package/dist/server/migrations/20260429000000-add-llm-fields.js +8 -0
  170. package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +16 -0
  171. package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.js +51 -0
  172. package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +7 -0
  173. package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.js +57 -0
  174. package/dist/server/plugin.d.ts +3 -0
  175. package/dist/server/plugin.js +37 -1
  176. package/dist/server/resources/tracing.js +154 -11
  177. package/dist/server/services/CodeValidator.d.ts +32 -0
  178. package/dist/server/services/CodeValidator.js +206 -0
  179. package/dist/server/services/ExecutionSpanService.d.ts +44 -0
  180. package/dist/server/services/ExecutionSpanService.js +104 -0
  181. package/dist/server/services/FileManager.d.ts +28 -0
  182. package/dist/server/services/FileManager.js +151 -0
  183. package/dist/server/services/SandboxRunner.d.ts +41 -0
  184. package/dist/server/services/SandboxRunner.js +167 -0
  185. package/dist/server/services/SkillManager.d.ts +6 -0
  186. package/dist/server/services/SkillManager.js +640 -0
  187. package/dist/server/services/SkillRepositoryService.d.ts +22 -0
  188. package/dist/server/services/SkillRepositoryService.js +157 -0
  189. package/dist/server/services/WorkerEnvManager.d.ts +26 -0
  190. package/dist/server/services/WorkerEnvManager.js +120 -0
  191. package/dist/server/skill-hub/actions/git-import.d.ts +21 -0
  192. package/dist/server/skill-hub/actions/git-import.js +413 -0
  193. package/dist/server/skill-hub/mcp/McpController.d.ts +15 -0
  194. package/dist/server/skill-hub/mcp/McpController.js +111 -0
  195. package/dist/server/skill-hub/plugin.d.ts +58 -0
  196. package/dist/server/skill-hub/plugin.js +694 -0
  197. package/dist/server/skill-hub/sandbox-config.json +6 -0
  198. package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +16 -0
  199. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +389 -0
  200. package/dist/server/skill-hub/utils/json-fields.d.ts +7 -0
  201. package/dist/server/skill-hub/utils/json-fields.js +88 -0
  202. package/dist/server/tools/delegate-task.d.ts +4 -0
  203. package/dist/server/tools/delegate-task.js +606 -104
  204. package/dist/server/tools/skill-execute.d.ts +36 -0
  205. package/dist/server/tools/skill-execute.js +167 -0
  206. package/package.json +3 -1
  207. package/src/client/AIEmployeeSelect.tsx +1 -3
  208. package/src/client/AIEmployeesContext.tsx +28 -13
  209. package/src/client/OrchestratorSettings.tsx +43 -5
  210. package/src/client/RulesTab.tsx +253 -32
  211. package/src/client/TracingTab.tsx +277 -213
  212. package/src/client/index.tsx +1 -1
  213. package/src/client/plugin.tsx +54 -15
  214. package/src/client/skill-hub/components/ExecutionHistory.tsx +201 -0
  215. package/src/client/skill-hub/components/ExecutionProgress.tsx +55 -0
  216. package/src/client/skill-hub/components/GitSkillImport.tsx +555 -0
  217. package/src/client/skill-hub/components/SkillEditor.tsx +456 -0
  218. package/src/client/skill-hub/components/SkillManager.tsx +181 -0
  219. package/src/client/skill-hub/components/SkillMetrics.tsx +124 -0
  220. package/src/client/skill-hub/components/SkillTestPanel.tsx +144 -0
  221. package/src/client/skill-hub/index.tsx +75 -0
  222. package/src/client/skill-hub/locale.ts +16 -0
  223. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +59 -0
  224. package/src/client/skill-hub/tools/SkillHubCard.tsx +78 -0
  225. package/src/client/skill-hub/utils/jsonFields.ts +37 -0
  226. package/src/server/collections/agent-execution-spans.ts +129 -0
  227. package/src/server/collections/orchestrator-config.ts +7 -0
  228. package/src/server/collections/skill-definitions.ts +128 -0
  229. package/src/server/collections/skill-executions.ts +94 -0
  230. package/src/server/collections/skill-worker-configs.ts +86 -0
  231. package/src/server/migrations/20260423000000-add-progress-fields.ts +50 -0
  232. package/src/server/migrations/20260425000000-add-interaction-schema.ts +35 -0
  233. package/src/server/migrations/20260427000000-add-tracing-detail-fields.ts +5 -5
  234. package/src/server/migrations/20260427000000-change-packages-to-text.ts +47 -0
  235. package/src/server/migrations/20260427000001-change-other-json-to-text.ts +57 -0
  236. package/src/server/migrations/20260429000000-add-llm-fields.ts +11 -2
  237. package/src/server/migrations/20260429000000-fix-inputargs-json-to-text.ts +38 -0
  238. package/src/server/migrations/20260503000000-add-orchestrator-trace-fields.ts +32 -0
  239. package/src/server/plugin.ts +94 -46
  240. package/src/server/resources/tracing.ts +182 -15
  241. package/src/server/services/CodeValidator.ts +159 -0
  242. package/src/server/services/ExecutionSpanService.ts +106 -0
  243. package/src/server/services/FileManager.ts +144 -0
  244. package/src/server/services/SandboxRunner.ts +205 -0
  245. package/src/server/services/SkillManager.ts +623 -0
  246. package/src/server/services/SkillRepositoryService.ts +142 -0
  247. package/src/server/services/WorkerEnvManager.ts +113 -0
  248. package/src/server/skill-hub/actions/git-import.ts +486 -0
  249. package/src/server/skill-hub/mcp/McpController.ts +86 -0
  250. package/src/server/skill-hub/plugin.ts +771 -0
  251. package/src/server/skill-hub/sandbox-config.json +6 -0
  252. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +443 -0
  253. package/src/server/skill-hub/utils/json-fields.ts +57 -0
  254. package/src/server/tools/delegate-task.ts +803 -127
  255. package/src/server/tools/skill-execute.ts +157 -0
@@ -0,0 +1,771 @@
1
+ import { resolve } from 'path';
2
+ import { createReadStream, createWriteStream, unlinkSync } from 'fs';
3
+ import * as os from 'os';
4
+ import { SandboxRunner } from '../services/SandboxRunner';
5
+ import { FileManager } from '../services/FileManager';
6
+ import { SkillManager } from '../services/SkillManager';
7
+ import { WorkerEnvManager } from '../services/WorkerEnvManager';
8
+ import { SkillExecutionTask } from './tasks/SkillExecutionTask';
9
+ import { createSkillExecuteTool } from '../tools/skill-execute';
10
+ import { McpController } from './mcp/McpController';
11
+ import { SkillRepositoryService } from '../services/SkillRepositoryService';
12
+ import { gitListSkills, gitSyncSkills } from './actions/git-import';
13
+ import { parseJsonText, stringifyJsonText, parseJsonLike } from './utils/json-fields';
14
+
15
+ /**
16
+ * Simple in-memory rate limiter per user.
17
+ * Tracks execution counts within a sliding time window.
18
+ */
19
+ class RateLimiter {
20
+ private userExecutions = new Map<string, number[]>();
21
+
22
+ constructor(
23
+ private readonly maxExecutions: number = 10,
24
+ private readonly windowMs: number = 60 * 1000, // 1 minute
25
+ ) {}
26
+
27
+ /**
28
+ * Check if the user is allowed to execute.
29
+ * @returns true if allowed, false if rate limited.
30
+ */
31
+ check(userId: string): boolean {
32
+ const now = Date.now();
33
+ const executions = this.userExecutions.get(userId) || [];
34
+
35
+ // Remove expired entries
36
+ const valid = executions.filter((t) => now - t < this.windowMs);
37
+ this.userExecutions.set(userId, valid);
38
+
39
+ if (valid.length >= this.maxExecutions) {
40
+ return false;
41
+ }
42
+
43
+ valid.push(now);
44
+ return true;
45
+ }
46
+
47
+ /** Get remaining executions for a user */
48
+ remaining(userId: string): number {
49
+ const now = Date.now();
50
+ const executions = (this.userExecutions.get(userId) || []).filter(
51
+ (t) => now - t < this.windowMs,
52
+ );
53
+ return Math.max(0, this.maxExecutions - executions.length);
54
+ }
55
+
56
+ /** Periodically clean up expired entries (call from interval) */
57
+ cleanup() {
58
+ const now = Date.now();
59
+ for (const [userId, executions] of this.userExecutions) {
60
+ const valid = executions.filter((t) => now - t < this.windowMs);
61
+ if (valid.length === 0) {
62
+ this.userExecutions.delete(userId);
63
+ } else {
64
+ this.userExecutions.set(userId, valid);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ export class SkillHubSubFeature {
71
+ sandboxRunner: SandboxRunner;
72
+ fileManager: FileManager;
73
+ skillManager: SkillManager;
74
+ workerEnvManager: WorkerEnvManager;
75
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
76
+ private initEnvDoneCallback: any = null;
77
+ private initEnvProgressCallback: any = null;
78
+ private mcpController: McpController;
79
+ private skillRepoService: SkillRepositoryService;
80
+ private rateLimiter = new RateLimiter(
81
+ parseInt(process.env.SKILL_HUB_RATE_LIMIT_MAX || '10', 10),
82
+ parseInt(process.env.SKILL_HUB_RATE_LIMIT_WINDOW_MS || '60000', 10),
83
+ );
84
+ private skillTemplates = new Map<string, any>();
85
+
86
+ constructor(private plugin: any) {}
87
+
88
+ get app() { return this.plugin.app; }
89
+ get db() { return this.plugin.db; }
90
+ get name() { return this.plugin.name; }
91
+
92
+ async load() {
93
+ // 1. Collections and migrations are now handled by parent orchestrator plugin
94
+
95
+ // 2. Init services
96
+ const storagePath = resolve(process.cwd(), 'storage', 'plugin-skill-hub'); // Keep old storage path for backwards compatibility
97
+ this.fileManager = new FileManager(storagePath);
98
+ this.sandboxRunner = new SandboxRunner(this.fileManager, (this as any).app.logger, storagePath);
99
+ this.skillManager = new SkillManager((this as any).db);
100
+ this.workerEnvManager = new WorkerEnvManager((this as any).app, (this as any).db, storagePath);
101
+ this.skillRepoService = new SkillRepositoryService(storagePath);
102
+ this.mcpController = new McpController(this);
103
+
104
+ // 3. Register REST actions
105
+ (this as any).app.resourceManager.define({
106
+ name: 'skillHub',
107
+ actions: {
108
+ download: this.handleDownload.bind(this),
109
+ test: this.handleTest.bind(this),
110
+ initEnv: this.handleInitEnv.bind(this),
111
+ clearStorage: this.handleClearStorage.bind(this),
112
+ mcpListTools: this.mcpController.listTools.bind(this.mcpController),
113
+ mcpCallTool: this.mcpController.callTool.bind(this.mcpController),
114
+ listTemplates: this.handleListTemplates.bind(this),
115
+ gitListSkills,
116
+ gitSyncSkills,
117
+ },
118
+ });
119
+
120
+
121
+ // 4.5. Register DB hooks for automatic storage physical cleanup
122
+ (this as any).db.on('skillExecutions.afterDestroy', async (model, options) => {
123
+ const execId = model.get('id');
124
+ try {
125
+ const dir = this.fileManager.getExecDir(String(execId));
126
+ if (require('fs').existsSync(dir)) {
127
+ require('fs').rmSync(dir, { recursive: true, force: true });
128
+ }
129
+ } catch (err) {
130
+ (this as any).app.logger.error(`[skill-hub] Failed to cleanup physical storage for execId ${execId}`, { error: err });
131
+ }
132
+ });
133
+
134
+ (this as any).db.on('skillDefinitions.afterSave', async (model, options) => {
135
+ // If a zip file was uploaded, extract it and update the skill record
136
+ if (model.changed('fileId') && model.get('fileId')) {
137
+ try {
138
+ const attachment = await (this as any).db.getRepository('attachments').findOne({
139
+ filter: { id: model.get('fileId') },
140
+ transaction: options.transaction,
141
+ });
142
+
143
+ if (attachment) {
144
+ const fileManager = (this as any).app.pm.get('@nocobase/plugin-file-manager') as any;
145
+ if (!fileManager) {
146
+ (this as any).app.logger.warn('[skill-hub] plugin-file-manager not found, cannot extract skill package');
147
+ return;
148
+ }
149
+
150
+ const streamData = await fileManager.getFileStream(attachment);
151
+ if (!streamData || !streamData.stream) {
152
+ (this as any).app.logger.warn(`[skill-hub] Could not get file stream for attachment ${attachment.get('id')}`);
153
+ return;
154
+ }
155
+
156
+ const tempZipPath = resolve(os.tmpdir(), `skill_${Date.now()}_${model.get('id')}.zip`);
157
+
158
+ await new Promise((resolvePipe, rejectPipe) => {
159
+ const writeStream = createWriteStream(tempZipPath);
160
+ streamData.stream.pipe(writeStream);
161
+ writeStream.on('finish', resolvePipe);
162
+ writeStream.on('error', rejectPipe);
163
+ streamData.stream.on('error', rejectPipe);
164
+ });
165
+
166
+ if (require('fs').existsSync(tempZipPath)) {
167
+ const skillName = model.get('name');
168
+ const { metadata, instructions } = await this.skillRepoService.extractSkillPackage(skillName, tempZipPath);
169
+ const code = this.skillRepoService.getSkillCode(skillName);
170
+
171
+ const updateValues: any = { storageType: attachment.get('storageId') ? `storage-${attachment.get('storageId')}` : 'local' };
172
+ if (code) updateValues.codeTemplate = code;
173
+ if (metadata.description) updateValues.description = metadata.description;
174
+ if (metadata.title) updateValues.title = metadata.title;
175
+ if (metadata.language) updateValues.language = metadata.language;
176
+ if (metadata.inputSchema) updateValues.inputSchema = stringifyJsonText(parseJsonLike(metadata.inputSchema, null));
177
+ if (metadata.interactionSchema) updateValues.interactionSchema = stringifyJsonText(parseJsonLike(metadata.interactionSchema, null));
178
+ if (metadata.packages) updateValues.packages = stringifyJsonText(parseJsonLike(metadata.packages, []), []);
179
+ if (metadata.timeoutSeconds) updateValues.timeoutSeconds = metadata.timeoutSeconds;
180
+ if (instructions) updateValues.instructions = instructions;
181
+
182
+ await (this as any).db.getRepository('skillDefinitions').update({
183
+ filter: { id: model.get('id') },
184
+ values: updateValues,
185
+ transaction: options.transaction,
186
+ });
187
+
188
+ unlinkSync(tempZipPath);
189
+ (this as any).app.logger.info(`[skill-hub] Successfully extracted zip and updated skill: ${skillName}`);
190
+ }
191
+ }
192
+ } catch (err) {
193
+ (this as any).app.logger.error(`[skill-hub] Failed to unpack skill zip`, { error: err });
194
+ }
195
+ }
196
+ });
197
+
198
+ // 5. Subscribe PubSub — worker processes skill execution tasks
199
+ (this as any).app.pubSubManager.subscribe('skill-hub.task', async (payload: any) => {
200
+ if (process.env.SKILL_HUB_SANDBOX === 'false') return;
201
+ await this.onQueueTask(payload);
202
+ });
203
+
204
+ // 5b. Subscribe PubSub — worker processes init-env tasks
205
+ (this as any).app.pubSubManager.subscribe('skill-hub.init-env', async (payload: any) => {
206
+ if (process.env.SKILL_HUB_SANDBOX === 'false') return;
207
+ await this.workerEnvManager.executeInit(payload);
208
+ });
209
+
210
+ // 6. Register AI tools + subscriptions (deferred — after all plugins loaded)
211
+ (this as any).app.on('afterStart', async () => {
212
+ this.registerAITools();
213
+ this.startCleanupInterval();
214
+ await this.subscribeInitEnvDone();
215
+ // Ensure any newly added built-in skills are seeded automatically on upgrade/restart
216
+ await this.skillManager.seedDefaults().catch((e) => {
217
+ (this as any).app.logger.error(`[skill-hub] Failed to seed default skills: ${e.message}`);
218
+ });
219
+ });
220
+ }
221
+
222
+ private async onQueueTask(message: { id: string }) {
223
+ (this as any).app.logger.info(`[skill-hub] Worker received queue task: ${message.id}`);
224
+ const execution = await (this as any).db.getRepository('skillExecutions').findOne({
225
+ filter: { id: message.id },
226
+ appends: ['skill'],
227
+ });
228
+ if (!execution) {
229
+ (this as any).app.logger.warn(`[skill-hub] Task ${message.id} ignored: execution record not found.`);
230
+ return;
231
+ }
232
+
233
+ const task = new SkillExecutionTask(
234
+ execution,
235
+ this.sandboxRunner,
236
+ this.fileManager,
237
+ this.skillRepoService,
238
+ (this as any).app,
239
+ );
240
+ await task.run();
241
+ }
242
+
243
+ /**
244
+ * Execute skill — called by both AI tool and REST test endpoint.
245
+ * Dispatches to worker via EventQueue, waits for result via PubSub.
246
+ * Pushes progress to SSE via runtime.writer (if within AI tool context).
247
+ * Includes rate limiting and graceful abort propagation.
248
+ */
249
+ async executeSkill(skill: any, inputArgs: Record<string, any>, ctx?: any): Promise<any> {
250
+ // ── Rate limiting ──
251
+ const userId = ctx?.state?.currentUser?.id;
252
+ if (userId) {
253
+ if (!this.rateLimiter.check(String(userId))) {
254
+ const remaining = this.rateLimiter.remaining(String(userId));
255
+ throw new Error(
256
+ `Rate limit exceeded. You can execute up to ${this.rateLimiter['maxExecutions']} ` +
257
+ `skills per minute. Remaining: ${remaining}. Please wait and try again.`,
258
+ );
259
+ }
260
+ }
261
+
262
+ const execution = await (this as any).db.getRepository('skillExecutions').create({
263
+ values: {
264
+ skillId: skill.id,
265
+ status: 'pending',
266
+ inputArgs: stringifyJsonText(inputArgs, {}),
267
+ sessionId: ctx?.state?.sessionId,
268
+ triggeredById: ctx?.state?.currentUser?.id,
269
+ },
270
+ });
271
+
272
+ const execId = String(execution.id);
273
+
274
+ (this as any).app.logger.info(
275
+ `[skill-hub] Queued execution ${execId}: skill=${skill.get ? skill.get('name') : skill.name}, ` +
276
+ `user=${userId || 'system'}`,
277
+ );
278
+
279
+ // Track PubSub subscriptions for cleanup
280
+ const cleanups: Array<{ channel: string; callback: any }> = [];
281
+
282
+ // Define callbacks with references for unsubscribe
283
+ const progressChannel = `skill-hub.progress.${execId}`;
284
+ const doneChannel = `skill-hub.done.${execId}`;
285
+ const abortChannel = `skill-hub.abort.${execId}`;
286
+
287
+ const progressCallback = async (progress: any) => {
288
+ try {
289
+ ctx?.runtime?.writer?.({
290
+ action: 'skillProgress',
291
+ body: { execId, skillName: skill.name || skill.get?.('name'), ...progress },
292
+ });
293
+ } catch {
294
+ // Ignore SSE write errors (connection may have closed)
295
+ }
296
+ };
297
+
298
+ // Wait for result via PubSub (progress streaming + completion)
299
+ let result: any;
300
+ try {
301
+ let resolvePromise: any;
302
+ let rejectPromise: any;
303
+
304
+ const resultPromise = new Promise<any>((resolve, reject) => {
305
+ resolvePromise = resolve;
306
+ rejectPromise = reject;
307
+ });
308
+
309
+ const timeoutMs = ((skill.timeoutSeconds || skill.get?.('timeoutSeconds') || 60) + 15) * 1000;
310
+ const timeout = setTimeout(() => {
311
+ rejectPromise(new Error(`Skill execution timeout after ${skill.timeoutSeconds || 60}s`));
312
+ }, timeoutMs);
313
+
314
+ const doneCallback = async (data: any) => {
315
+ clearTimeout(timeout);
316
+ resolvePromise(data);
317
+ };
318
+
319
+ // Subscribe progress and completion FIRST (before dispatching)
320
+ await (this as any).app.pubSubManager.subscribe(progressChannel, progressCallback);
321
+ cleanups.push({ channel: progressChannel, callback: progressCallback });
322
+
323
+ await (this as any).app.pubSubManager.subscribe(doneChannel, doneCallback);
324
+ cleanups.push({ channel: doneChannel, callback: doneCallback });
325
+
326
+ // Handle user abort (cancel chat) → propagate to worker
327
+ if (ctx?.req?.signal || ctx?.signal) {
328
+ const signal = ctx.req?.signal || ctx.signal;
329
+ signal.addEventListener?.('abort', () => {
330
+ clearTimeout(timeout);
331
+ // Publish abort to worker via PubSub
332
+ (this as any).app.pubSubManager.publish(abortChannel, { reason: 'user_cancel' }).catch(() => {});
333
+ // Also update the execution status
334
+ (this as any).db.getRepository('skillExecutions').update({
335
+ filter: { id: execId },
336
+ values: { status: 'canceled' },
337
+ }).catch(() => {});
338
+ rejectPromise(new Error('Canceled by user'));
339
+ });
340
+ }
341
+
342
+ // NOW Dispatch to worker via EventQueue
343
+ await (this as any).app.pubSubManager.publish('skill-hub.task', { id: execId });
344
+
345
+ // Wait for completion
346
+ result = await resultPromise;
347
+ } finally {
348
+ // Cleanup all PubSub subscriptions
349
+ for (const { channel, callback } of cleanups) {
350
+ try {
351
+ await (this as any).app.pubSubManager.unsubscribe(channel, callback);
352
+ } catch {
353
+ // ignore cleanup errors
354
+ }
355
+ }
356
+ }
357
+
358
+ // Build download URLs for output files (use base64 filename to prevent Markdown link breaks if LLM decodes spaces)
359
+ const filesWithUrls = (result.files || []).map((f: any) => {
360
+ const b64name = Buffer.from(f.name).toString('base64url');
361
+ return {
362
+ ...f,
363
+ downloadUrl: `/api/skillHub:download?execId=${execId}&f=${b64name}`,
364
+ };
365
+ });
366
+
367
+ return { ...result, files: filesWithUrls, execId };
368
+ }
369
+
370
+ private async handleDownload(ctx: any, next: any) {
371
+ const { execId, filename, f } = ctx.action.params;
372
+ let targetFile = filename;
373
+ if (f) {
374
+ targetFile = Buffer.from(f, 'base64url').toString('utf8');
375
+ }
376
+
377
+ if (!execId || !targetFile) {
378
+ ctx.throw(400, 'Missing execId or filename');
379
+ }
380
+
381
+ const currentUser = ctx?.state?.currentUser;
382
+ if (!currentUser) {
383
+ ctx.throw(401, 'Unauthorized');
384
+ }
385
+
386
+ const execution = await (this as any).db.getRepository('skillExecutions').findOne({
387
+ filter: { id: execId },
388
+ });
389
+
390
+ if (!execution) {
391
+ ctx.throw(404, 'Execution not found');
392
+ }
393
+
394
+ const isOwner = execution.triggeredById === currentUser.id;
395
+ const isAdmin = currentUser.roles?.some((r: any) => r.name === 'root' || r === 'root' || r.name === 'admin');
396
+
397
+ if (!isOwner && !isAdmin) {
398
+ ctx.throw(403, 'Permission denied: you cannot view files from this execution');
399
+ }
400
+
401
+ const filePath = this.fileManager.getOutputFilePath(execId, targetFile);
402
+ if (!filePath) {
403
+ ctx.throw(404, 'File not found');
404
+ }
405
+
406
+ ctx.attachment(targetFile);
407
+ ctx.body = createReadStream(filePath);
408
+ await next();
409
+ }
410
+
411
+ private async handleTest(ctx: any, next: any) {
412
+ const { skillId, input } = ctx.action.params.values || {};
413
+ if (!skillId) {
414
+ ctx.throw(400, 'Missing skillId');
415
+ }
416
+
417
+ const skill = await (this as any).db.getRepository('skillDefinitions').findOne({
418
+ filter: { id: skillId },
419
+ });
420
+ if (!skill) {
421
+ ctx.throw(404, 'Skill not found');
422
+ }
423
+
424
+ const result = await this.executeSkill(skill, input || {}, ctx);
425
+ ctx.body = result;
426
+ await next();
427
+ }
428
+
429
+ /**
430
+ * Handle Init Environment request from admin UI.
431
+ * Dispatches init task to all workers via EventQueue.
432
+ */
433
+ private async handleInitEnv(ctx: any, next: any) {
434
+ const config = await this.workerEnvManager.getOrCreateConfig();
435
+ const customPackages = parseJsonText(config.get?.('customPackages') ?? config.customPackages, {
436
+ python: [],
437
+ node: [],
438
+ });
439
+ const message = await this.workerEnvManager.initEnvironment(
440
+ config.get ? {
441
+ npmRegistryUrl: config.get('npmRegistryUrl'),
442
+ npmAuthToken: config.get('npmAuthToken'),
443
+ pypiIndexUrl: config.get('pypiIndexUrl'),
444
+ pypiTrustedHost: config.get('pypiTrustedHost'),
445
+ aptMirrorUrl: config.get('aptMirrorUrl'),
446
+ aptGpgKeyUrl: config.get('aptGpgKeyUrl'),
447
+ customPackages,
448
+ } : config,
449
+ );
450
+ ctx.body = { message };
451
+ await next();
452
+ }
453
+
454
+ /**
455
+ * Subscribe to init-env done AND progress PubSub channels.
456
+ * When a worker finishes init, auto-update the DB with status.
457
+ */
458
+ private async subscribeInitEnvDone() {
459
+ this.initEnvDoneCallback = async (data: any) => {
460
+ try {
461
+ const values: any = {
462
+ initStatus: data.status,
463
+ lastInitLog: data.log,
464
+ };
465
+ if (data.status === 'succeeded' && data.whitelist) {
466
+ values.packageWhitelist = stringifyJsonText(data.whitelist, { python: [], node: [], apt: [] });
467
+ }
468
+ await (this as any).db.getRepository('skillWorkerConfigs').update({
469
+ filter: {},
470
+ values,
471
+ forceUpdate: true,
472
+ });
473
+ (this as any).app.logger.info(`[skill-hub] Init env ${data.status}`);
474
+ } catch (err) {
475
+ (this as any).app.logger.warn('[skill-hub] Failed to update init env status:', err);
476
+ }
477
+ };
478
+
479
+ this.initEnvProgressCallback = async (data: any) => {
480
+ try {
481
+ await (this as any).db.getRepository('skillWorkerConfigs').update({
482
+ filter: {},
483
+ values: {
484
+ initProgressPercent: data.percent,
485
+ initProgressLog: data.log,
486
+ },
487
+ forceUpdate: true,
488
+ });
489
+ } catch (err) {
490
+ // ignore progress update errors
491
+ }
492
+ };
493
+
494
+ await (this as any).app.pubSubManager.subscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
495
+ await (this as any).app.pubSubManager.subscribe('skill-hub.init-env.progress', this.initEnvProgressCallback);
496
+ }
497
+
498
+ private registerAITools() {
499
+ try {
500
+ const aiPlugin = (this as any).app.pm.get('@nocobase/plugin-ai') as any;
501
+ if (!aiPlugin?.ai?.toolsManager) {
502
+ (this as any).app.logger.warn('[skill-hub] plugin-ai not available, skip AI tool registration.');
503
+ return;
504
+ }
505
+
506
+ // 1. General tool (list + execute)
507
+ aiPlugin.ai.toolsManager.registerTools(createSkillExecuteTool(this));
508
+
509
+ // 2. Dynamic tools — each enabled skill becomes a separate AI tool.
510
+ aiPlugin.ai.toolsManager.registerDynamicTools(async (register: { registerTools: (options: any) => void }) => {
511
+ try {
512
+ const skills = await (this as any).db.getRepository('skillDefinitions').find({
513
+ filter: { enabled: true },
514
+ });
515
+
516
+ if (!skills || skills.length === 0) return;
517
+
518
+ const tools = await Promise.all(skills.map(async (skill: any) => {
519
+ const sanitizedToolName = skill.get('name').toLowerCase().replace(/[^a-z0-9_]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
520
+ const autoCall = !!skill.get('autoCall');
521
+ const interactionSchema = parseJsonText(skill.get('interactionSchema'), null);
522
+ const fullDescription = await this.getSkillDescriptionForAI(skill);
523
+ const baseDescription = `${fullDescription || skill.get('description')}\nLanguage: ${skill.get('language')}`;
524
+ const description = !autoCall && interactionSchema
525
+ ? `${baseDescription}\n\nIMPORTANT: This skill requires human confirmation. Pass best-effort args; the user will adjust them in UI before execution.`
526
+ : baseDescription;
527
+ return {
528
+ scope: 'CUSTOM' as const,
529
+ execution: 'backend' as const,
530
+ defaultPermission: (autoCall ? 'ALLOW' : 'ASK') as 'ALLOW' | 'ASK',
531
+ introduction: {
532
+ title: `Skill Hub: ${skill.get('title')}`,
533
+ about: skill.get('description') || `Thực thi kỹ năng ${skill.get('title')}`,
534
+ },
535
+ definition: {
536
+ name: `skill_hub_${sanitizedToolName}`,
537
+ description,
538
+ schema: parseJsonText(skill.get('inputSchema'), { type: 'object', properties: {} }),
539
+ },
540
+ invoke: async (toolCtx: any, args: any) => {
541
+ // Re-fetch skill to get latest version (hot-reload support)
542
+ const latestSkill = await (this as any).db.getRepository('skillDefinitions').findOne({
543
+ filter: { id: skill.get('id'), enabled: true },
544
+ });
545
+ if (!latestSkill) {
546
+ return { error: `Skill "${skill.get('name')}" is no longer available` };
547
+ }
548
+ const result = await this.executeSkill(latestSkill, args, toolCtx);
549
+ return {
550
+ status: result.status === 'succeeded' ? 'success' : 'error',
551
+ result: result, // Attach raw result
552
+ };
553
+ },
554
+ };
555
+ }));
556
+
557
+ register.registerTools(tools);
558
+ } catch (err) {
559
+ (this as any).app.logger.warn('[skill-hub] Failed to provide dynamic tools', err);
560
+ }
561
+ });
562
+
563
+ (this as any).app.logger.info('[skill-hub] AI tools registered (dynamic provider + general tool).');
564
+ } catch (error) {
565
+ (this as any).app.logger.warn('[skill-hub] Failed to register AI tools:', error);
566
+ }
567
+ }
568
+
569
+ private startCleanupInterval() {
570
+ // Check old execution files every hour, rate limiter every 5 minutes
571
+ const CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour
572
+
573
+ this.cleanupInterval = setInterval(async () => {
574
+ // 1. Storage Retention Cleanup
575
+ try {
576
+ const config = await (this as any).db.getRepository('skillWorkerConfigs').findOne();
577
+ const hours = config ? config.get('retentionHours') : 24;
578
+
579
+ if (hours && hours > 0) {
580
+ const MAX_AGE_MS = hours * 60 * 60 * 1000;
581
+ const cutoff = new Date(Date.now() - MAX_AGE_MS);
582
+ const repo = (this as any).db.getRepository('skillExecutions');
583
+
584
+ const outdated = await repo.find({
585
+ where: { createdAt: { $lt: cutoff } }
586
+ });
587
+
588
+ if (outdated.length > 0) {
589
+ for (const record of outdated) {
590
+ await record.destroy(); // Fires afterDestroy hook which removes physical folder
591
+ }
592
+ (this as any).app.logger.info(`[skill-hub] Auto-cleaned up ${outdated.length} expired execution records`);
593
+ }
594
+ }
595
+ } catch (err) {
596
+ (this as any).app.logger.warn('[skill-hub] Auto Cleanup error:', err);
597
+ }
598
+
599
+ // 2. Cleanup rate limiter stale entries
600
+ this.rateLimiter.cleanup();
601
+ }, CLEANUP_INTERVAL);
602
+ }
603
+
604
+ async beforeStop() {
605
+ // Unsubscribe PubSub
606
+ if (this.initEnvDoneCallback) {
607
+ try {
608
+ await (this as any).app.pubSubManager.unsubscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
609
+ } catch { /* ignore */ }
610
+ }
611
+ if (this.initEnvProgressCallback) {
612
+ try {
613
+ await (this as any).app.pubSubManager.unsubscribe('skill-hub.init-env.progress', this.initEnvProgressCallback);
614
+ } catch { /* ignore */ }
615
+ }
616
+
617
+ // Clear cleanup interval
618
+ if (this.cleanupInterval) {
619
+ clearInterval(this.cleanupInterval);
620
+ this.cleanupInterval = null;
621
+ }
622
+ }
623
+
624
+ // --- Handlers ---
625
+ private async handleClearStorage(ctx: any, next: () => Promise<any>) {
626
+ const { type } = ctx.request.body || ctx.action.params.values;
627
+ const repo = (this as any).db.getRepository('skillExecutions');
628
+ let count = 0;
629
+
630
+ if (type === 'all') {
631
+ const results = await repo.find({ fields: ['id'] });
632
+ for (const rec of results) {
633
+ await rec.destroy();
634
+ }
635
+ count = results.length;
636
+ } else if (type === 'expired') {
637
+ const config = await (this as any).db.getRepository('skillWorkerConfigs').findOne();
638
+ const hours = config ? config.get('retentionHours') : 24;
639
+ if (hours > 0) {
640
+ const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
641
+ const results = await repo.find({ where: { createdAt: { $lt: cutoff } }, fields: ['id'] });
642
+ for (const rec of results) {
643
+ await rec.destroy();
644
+ }
645
+ count = results.length;
646
+ }
647
+ }
648
+
649
+ ctx.body = { count };
650
+ await next();
651
+ }
652
+
653
+ private async handleListTemplates(ctx: any, next: () => Promise<any>) {
654
+ // Dynamic Pull: discover templates from all active plugins in the system
655
+ try {
656
+ const allPlugins = (this as any).app.pm.getPlugins();
657
+ for (const [, pluginInstance] of allPlugins) {
658
+ if (typeof (pluginInstance as any).getSkillTemplates === 'function') {
659
+ const pluginSkills = (pluginInstance as any).getSkillTemplates();
660
+ if (Array.isArray(pluginSkills)) {
661
+ for (const s of pluginSkills) {
662
+ if (!this.skillTemplates.has(s.name)) {
663
+ this.skillTemplates.set(s.name, this.hydrateSkillTemplate(pluginInstance.name, s));
664
+ }
665
+ }
666
+ }
667
+ }
668
+ }
669
+ } catch (e) {
670
+ (this as any).app.logger.warn(`[skill-hub] Failed to discover some plugin skills: ${e.message}`);
671
+ }
672
+
673
+ ctx.body = { data: Array.from(this.skillTemplates.values()) };
674
+ await next();
675
+ }
676
+
677
+ // ─── Extension API: other plugins register/unregister skills ───
678
+
679
+
680
+
681
+ /**
682
+ * Register a skill template into memory for UI importing.
683
+ */
684
+ registerSkillTemplate(pluginName: string, skillDef: any) {
685
+ this.skillTemplates.set(skillDef.name, this.hydrateSkillTemplate(pluginName, skillDef));
686
+ (this as any).app.logger.info(`[skill-hub] Registered skill template "${skillDef.name}" from plugin "${pluginName}"`);
687
+ }
688
+
689
+ resolveSkillTemplate(templateName: string) {
690
+ if (!templateName) return null;
691
+ const cached = this.skillTemplates.get(templateName);
692
+ if (cached) return cached;
693
+
694
+ try {
695
+ const allPlugins = (this as any).app.pm.getPlugins();
696
+ for (const [, pluginInstance] of allPlugins) {
697
+ if (typeof (pluginInstance as any).getSkillTemplates !== 'function') continue;
698
+ const pluginSkills = (pluginInstance as any).getSkillTemplates();
699
+ if (!Array.isArray(pluginSkills)) continue;
700
+ const found = pluginSkills.find((s: any) => s?.name === templateName);
701
+ if (found) {
702
+ const hydrated = this.hydrateSkillTemplate(pluginInstance.name, found);
703
+ this.skillTemplates.set(templateName, hydrated);
704
+ return hydrated;
705
+ }
706
+ }
707
+ } catch (e: any) {
708
+ (this as any).app.logger.warn(`[skill-hub] Failed to resolve plugin skill "${templateName}": ${e.message}`);
709
+ }
710
+
711
+ return null;
712
+ }
713
+
714
+ async getSkillDescriptionForAI(skill: any) {
715
+ const description = skill.get ? skill.get('description') : skill.description;
716
+ const instructions = await this.getSkillInstructions(skill);
717
+ const maxInlineInstructionChars = 24000;
718
+ const inlineInstructions = instructions && instructions.length > maxInlineInstructionChars
719
+ ? `${instructions.slice(0, maxInlineInstructionChars)}\n\n[Instructions truncated in tool description. Call skill_hub_execute with action="describe" and this skillName to load the complete workflow.]`
720
+ : instructions;
721
+ return [description, inlineInstructions ? `Instructions:\n${inlineInstructions}` : ''].filter(Boolean).join('\n\n');
722
+ }
723
+
724
+ async getSkillInstructions(skill: any) {
725
+ const storedInstructions = skill.get ? skill.get('instructions') : skill.instructions;
726
+ if (storedInstructions) return storedInstructions;
727
+
728
+ const storageType = skill.get ? skill.get('storageType') : skill.storageType;
729
+ if (storageType !== 'plugin') return '';
730
+
731
+ const templateName = (skill.get ? skill.get('pluginSource') : skill.pluginSource) ||
732
+ (skill.get ? skill.get('name') : skill.name);
733
+ const template = this.resolveSkillTemplate(templateName);
734
+ return template?.instructions || '';
735
+ }
736
+
737
+ private hydrateSkillTemplate(pluginName: string, skillDef: any) {
738
+ const skillName = skillDef.name;
739
+ const packageRoot = skillDef.skillPackage?.rootDir;
740
+ let packageInfo: any = null;
741
+
742
+ if (packageRoot && this.skillRepoService) {
743
+ packageInfo = this.skillRepoService.readSkillPackage(packageRoot);
744
+ }
745
+
746
+ const metadata = packageInfo?.metadata || {};
747
+ const packageInstructions = packageInfo?.instructions;
748
+
749
+ return {
750
+ ...skillDef,
751
+ title: skillDef.title || metadata.title || skillName,
752
+ description: skillDef.description || metadata.description || '',
753
+ instructions: [skillDef.instructions, packageInstructions].filter(Boolean).join('\n\n').trim(),
754
+ language: skillDef.language || metadata.language || 'python',
755
+ codeTemplate: skillDef.codeTemplate || packageInfo?.code || '',
756
+ storageType: 'plugin',
757
+ storageUrl: skillDef.storageUrl || `plugin://${pluginName}/${skillName}`,
758
+ // Keep pluginSource as the skill template name because existing DB records use it for lookup.
759
+ pluginSource: skillName,
760
+ pluginName,
761
+ };
762
+ }
763
+
764
+
765
+
766
+ async install() {
767
+ await this.skillManager.seedDefaults();
768
+ }
769
+ }
770
+
771
+ export default SkillHubSubFeature;