plugin-agent-orchestrator 1.0.13 → 1.0.14

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 (251) 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.js +6 -0
  153. package/dist/server/collections/skill-definitions.d.ts +3 -0
  154. package/dist/server/collections/skill-definitions.js +158 -0
  155. package/dist/server/collections/skill-executions.d.ts +3 -0
  156. package/dist/server/collections/skill-executions.js +123 -0
  157. package/dist/server/collections/skill-worker-configs.d.ts +3 -0
  158. package/dist/server/collections/skill-worker-configs.js +115 -0
  159. package/dist/server/migrations/20260423000000-add-progress-fields.d.ts +4 -0
  160. package/dist/server/migrations/20260423000000-add-progress-fields.js +69 -0
  161. package/dist/server/migrations/20260425000000-add-interaction-schema.d.ts +4 -0
  162. package/dist/server/migrations/20260425000000-add-interaction-schema.js +61 -0
  163. package/dist/server/migrations/20260427000000-change-packages-to-text.d.ts +4 -0
  164. package/dist/server/migrations/20260427000000-change-packages-to-text.js +70 -0
  165. package/dist/server/migrations/20260427000001-change-other-json-to-text.d.ts +4 -0
  166. package/dist/server/migrations/20260427000001-change-other-json-to-text.js +80 -0
  167. package/dist/server/migrations/20260429000000-add-llm-fields.js +8 -0
  168. package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.d.ts +16 -0
  169. package/dist/server/migrations/20260429000000-fix-inputargs-json-to-text.js +51 -0
  170. package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.d.ts +7 -0
  171. package/dist/server/migrations/20260503000000-add-orchestrator-trace-fields.js +57 -0
  172. package/dist/server/plugin.d.ts +3 -0
  173. package/dist/server/plugin.js +37 -1
  174. package/dist/server/resources/tracing.js +154 -11
  175. package/dist/server/services/CodeValidator.d.ts +32 -0
  176. package/dist/server/services/CodeValidator.js +205 -0
  177. package/dist/server/services/ExecutionSpanService.d.ts +44 -0
  178. package/dist/server/services/ExecutionSpanService.js +104 -0
  179. package/dist/server/services/FileManager.d.ts +28 -0
  180. package/dist/server/services/FileManager.js +151 -0
  181. package/dist/server/services/SandboxRunner.d.ts +41 -0
  182. package/dist/server/services/SandboxRunner.js +167 -0
  183. package/dist/server/services/SkillManager.d.ts +6 -0
  184. package/dist/server/services/SkillManager.js +640 -0
  185. package/dist/server/services/SkillRepositoryService.d.ts +22 -0
  186. package/dist/server/services/SkillRepositoryService.js +157 -0
  187. package/dist/server/services/WorkerEnvManager.d.ts +26 -0
  188. package/dist/server/services/WorkerEnvManager.js +120 -0
  189. package/dist/server/skill-hub/actions/git-import.d.ts +21 -0
  190. package/dist/server/skill-hub/actions/git-import.js +413 -0
  191. package/dist/server/skill-hub/mcp/McpController.d.ts +15 -0
  192. package/dist/server/skill-hub/mcp/McpController.js +111 -0
  193. package/dist/server/skill-hub/plugin.d.ts +58 -0
  194. package/dist/server/skill-hub/plugin.js +694 -0
  195. package/dist/server/skill-hub/sandbox-config.json +6 -0
  196. package/dist/server/skill-hub/tasks/SkillExecutionTask.d.ts +14 -0
  197. package/dist/server/skill-hub/tasks/SkillExecutionTask.js +267 -0
  198. package/dist/server/skill-hub/utils/json-fields.d.ts +7 -0
  199. package/dist/server/skill-hub/utils/json-fields.js +88 -0
  200. package/dist/server/tools/delegate-task.d.ts +4 -0
  201. package/dist/server/tools/delegate-task.js +606 -104
  202. package/dist/server/tools/skill-execute.d.ts +36 -0
  203. package/dist/server/tools/skill-execute.js +167 -0
  204. package/package.json +3 -1
  205. package/src/client/AIEmployeeSelect.tsx +1 -3
  206. package/src/client/AIEmployeesContext.tsx +28 -13
  207. package/src/client/OrchestratorSettings.tsx +43 -5
  208. package/src/client/RulesTab.tsx +253 -32
  209. package/src/client/TracingTab.tsx +277 -213
  210. package/src/client/plugin.tsx +39 -0
  211. package/src/client/skill-hub/components/ExecutionHistory.tsx +201 -0
  212. package/src/client/skill-hub/components/ExecutionProgress.tsx +55 -0
  213. package/src/client/skill-hub/components/GitSkillImport.tsx +555 -0
  214. package/src/client/skill-hub/components/SkillEditor.tsx +456 -0
  215. package/src/client/skill-hub/components/SkillManager.tsx +181 -0
  216. package/src/client/skill-hub/components/SkillMetrics.tsx +124 -0
  217. package/src/client/skill-hub/components/SkillTestPanel.tsx +144 -0
  218. package/src/client/skill-hub/index.tsx +75 -0
  219. package/src/client/skill-hub/locale.ts +16 -0
  220. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +59 -0
  221. package/src/client/skill-hub/tools/SkillHubCard.tsx +78 -0
  222. package/src/client/skill-hub/utils/jsonFields.ts +37 -0
  223. package/src/server/collections/agent-execution-spans.ts +129 -0
  224. package/src/server/collections/orchestrator-config.ts +7 -0
  225. package/src/server/collections/skill-definitions.ts +128 -0
  226. package/src/server/collections/skill-executions.ts +94 -0
  227. package/src/server/collections/skill-worker-configs.ts +86 -0
  228. package/src/server/migrations/20260423000000-add-progress-fields.ts +50 -0
  229. package/src/server/migrations/20260425000000-add-interaction-schema.ts +35 -0
  230. package/src/server/migrations/20260427000000-change-packages-to-text.ts +47 -0
  231. package/src/server/migrations/20260427000001-change-other-json-to-text.ts +57 -0
  232. package/src/server/migrations/20260429000000-add-llm-fields.ts +9 -0
  233. package/src/server/migrations/20260429000000-fix-inputargs-json-to-text.ts +38 -0
  234. package/src/server/migrations/20260503000000-add-orchestrator-trace-fields.ts +32 -0
  235. package/src/server/plugin.ts +51 -3
  236. package/src/server/resources/tracing.ts +182 -15
  237. package/src/server/services/CodeValidator.ts +159 -0
  238. package/src/server/services/ExecutionSpanService.ts +106 -0
  239. package/src/server/services/FileManager.ts +144 -0
  240. package/src/server/services/SandboxRunner.ts +205 -0
  241. package/src/server/services/SkillManager.ts +623 -0
  242. package/src/server/services/SkillRepositoryService.ts +142 -0
  243. package/src/server/services/WorkerEnvManager.ts +113 -0
  244. package/src/server/skill-hub/actions/git-import.ts +486 -0
  245. package/src/server/skill-hub/mcp/McpController.ts +86 -0
  246. package/src/server/skill-hub/plugin.ts +771 -0
  247. package/src/server/skill-hub/sandbox-config.json +6 -0
  248. package/src/server/skill-hub/tasks/SkillExecutionTask.ts +297 -0
  249. package/src/server/skill-hub/utils/json-fields.ts +57 -0
  250. package/src/server/tools/delegate-task.ts +803 -127
  251. 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.app.logger, storagePath);
99
+ this.skillManager = new SkillManager(this.db);
100
+ this.workerEnvManager = new WorkerEnvManager(this.app, this.db, storagePath);
101
+ this.skillRepoService = new SkillRepositoryService(storagePath);
102
+ this.mcpController = new McpController(this);
103
+
104
+ // 3. Register REST actions
105
+ this.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.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.app.logger.error(`[skill-hub] Failed to cleanup physical storage for execId ${execId}`, { error: err });
131
+ }
132
+ });
133
+
134
+ this.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.db.getRepository('attachments').findOne({
139
+ filter: { id: model.get('fileId') },
140
+ transaction: options.transaction,
141
+ });
142
+
143
+ if (attachment) {
144
+ const fileManager = this.app.pm.get('@nocobase/plugin-file-manager') as any;
145
+ if (!fileManager) {
146
+ this.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.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.db.getRepository('skillDefinitions').update({
183
+ filter: { id: model.get('id') },
184
+ values: updateValues,
185
+ transaction: options.transaction,
186
+ });
187
+
188
+ unlinkSync(tempZipPath);
189
+ this.app.logger.info(`[skill-hub] Successfully extracted zip and updated skill: ${skillName}`);
190
+ }
191
+ }
192
+ } catch (err) {
193
+ this.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.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.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.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.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.app.logger.info(`[skill-hub] Worker received queue task: ${message.id}`);
224
+ const execution = await this.db.getRepository('skillExecutions').findOne({
225
+ filter: { id: message.id },
226
+ appends: ['skill'],
227
+ });
228
+ if (!execution) {
229
+ this.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.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.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.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.app.pubSubManager.subscribe(progressChannel, progressCallback);
321
+ cleanups.push({ channel: progressChannel, callback: progressCallback });
322
+
323
+ await this.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.app.pubSubManager.publish(abortChannel, { reason: 'user_cancel' }).catch(() => {});
333
+ // Also update the execution status
334
+ this.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.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.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.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.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.db.getRepository('skillWorkerConfigs').update({
469
+ filter: {},
470
+ values,
471
+ forceUpdate: true,
472
+ });
473
+ this.app.logger.info(`[skill-hub] Init env ${data.status}`);
474
+ } catch (err) {
475
+ this.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.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.app.pubSubManager.subscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
495
+ await this.app.pubSubManager.subscribe('skill-hub.init-env.progress', this.initEnvProgressCallback);
496
+ }
497
+
498
+ private registerAITools() {
499
+ try {
500
+ const aiPlugin = this.app.pm.get('@nocobase/plugin-ai') as any;
501
+ if (!aiPlugin?.ai?.toolsManager) {
502
+ this.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.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.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.app.logger.warn('[skill-hub] Failed to provide dynamic tools', err);
560
+ }
561
+ });
562
+
563
+ this.app.logger.info('[skill-hub] AI tools registered (dynamic provider + general tool).');
564
+ } catch (error) {
565
+ this.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.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.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.app.logger.info(`[skill-hub] Auto-cleaned up ${outdated.length} expired execution records`);
593
+ }
594
+ }
595
+ } catch (err) {
596
+ this.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.app.pubSubManager.unsubscribe('skill-hub.init-env.done', this.initEnvDoneCallback);
609
+ } catch { /* ignore */ }
610
+ }
611
+ if (this.initEnvProgressCallback) {
612
+ try {
613
+ await this.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.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.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.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.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.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.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.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;