@phuetz/code-buddy 0.1.12 → 0.1.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 (321) hide show
  1. package/README.md +228 -13
  2. package/dist/agent/architect-mode.d.ts +11 -0
  3. package/dist/agent/architect-mode.js +133 -25
  4. package/dist/agent/architect-mode.js.map +1 -1
  5. package/dist/agent/codebuddy-agent.d.ts +24 -0
  6. package/dist/agent/codebuddy-agent.js +118 -16
  7. package/dist/agent/codebuddy-agent.js.map +1 -1
  8. package/dist/agent/execution/agent-executor.d.ts +9 -0
  9. package/dist/agent/execution/agent-executor.js +62 -1
  10. package/dist/agent/execution/agent-executor.js.map +1 -1
  11. package/dist/agent/message-queue.d.ts +77 -0
  12. package/dist/agent/message-queue.js +116 -0
  13. package/dist/agent/message-queue.js.map +1 -0
  14. package/dist/agent/middleware/auto-observation.d.ts +37 -0
  15. package/dist/agent/middleware/auto-observation.js +231 -0
  16. package/dist/agent/middleware/auto-observation.js.map +1 -0
  17. package/dist/agent/middleware/index.d.ts +2 -0
  18. package/dist/agent/middleware/index.js +1 -0
  19. package/dist/agent/middleware/index.js.map +1 -1
  20. package/dist/agent/tool-handler.js +3 -2
  21. package/dist/agent/tool-handler.js.map +1 -1
  22. package/dist/agent/turn-diff-tracker.js +3 -0
  23. package/dist/agent/turn-diff-tracker.js.map +1 -1
  24. package/dist/agent/types.d.ts +7 -2
  25. package/dist/analytics/budget-alerts.d.ts +81 -0
  26. package/dist/analytics/budget-alerts.js +126 -0
  27. package/dist/analytics/budget-alerts.js.map +1 -0
  28. package/dist/analytics/cost-predictor.d.ts +79 -0
  29. package/dist/analytics/cost-predictor.js +150 -0
  30. package/dist/analytics/cost-predictor.js.map +1 -0
  31. package/dist/analytics/index.d.ts +2 -0
  32. package/dist/analytics/index.js +2 -0
  33. package/dist/analytics/index.js.map +1 -1
  34. package/dist/auth/profile-manager.d.ts +205 -0
  35. package/dist/auth/profile-manager.js +484 -0
  36. package/dist/auth/profile-manager.js.map +1 -0
  37. package/dist/browser-automation/browser-manager.d.ts +79 -1
  38. package/dist/browser-automation/browser-manager.js +265 -2
  39. package/dist/browser-automation/browser-manager.js.map +1 -1
  40. package/dist/browser-automation/profile-manager.d.ts +32 -0
  41. package/dist/browser-automation/profile-manager.js +83 -0
  42. package/dist/browser-automation/profile-manager.js.map +1 -0
  43. package/dist/browser-automation/route-interceptor.d.ts +29 -0
  44. package/dist/browser-automation/route-interceptor.js +103 -0
  45. package/dist/browser-automation/route-interceptor.js.map +1 -0
  46. package/dist/browser-automation/screenshot-annotator.d.ts +23 -0
  47. package/dist/browser-automation/screenshot-annotator.js +86 -0
  48. package/dist/browser-automation/screenshot-annotator.js.map +1 -0
  49. package/dist/browser-automation/types.d.ts +47 -0
  50. package/dist/cache/llm-response-cache.js +3 -0
  51. package/dist/cache/llm-response-cache.js.map +1 -1
  52. package/dist/canvas/canvas-server.js +4 -3
  53. package/dist/canvas/canvas-server.js.map +1 -1
  54. package/dist/channels/discord/client.d.ts +2 -1
  55. package/dist/channels/discord/client.js +28 -16
  56. package/dist/channels/discord/client.js.map +1 -1
  57. package/dist/channels/dm-pairing.js +6 -3
  58. package/dist/channels/dm-pairing.js.map +1 -1
  59. package/dist/channels/google-chat/index.d.ts +210 -0
  60. package/dist/channels/google-chat/index.js +505 -0
  61. package/dist/channels/google-chat/index.js.map +1 -0
  62. package/dist/channels/group-security.d.ts +182 -0
  63. package/dist/channels/group-security.js +407 -0
  64. package/dist/channels/group-security.js.map +1 -0
  65. package/dist/channels/index.d.ts +17 -1
  66. package/dist/channels/index.js +16 -0
  67. package/dist/channels/index.js.map +1 -1
  68. package/dist/channels/matrix/index.d.ts +181 -0
  69. package/dist/channels/matrix/index.js +643 -0
  70. package/dist/channels/matrix/index.js.map +1 -0
  71. package/dist/channels/offline-queue.d.ts +92 -0
  72. package/dist/channels/offline-queue.js +112 -0
  73. package/dist/channels/offline-queue.js.map +1 -0
  74. package/dist/channels/reconnection-manager.d.ts +117 -0
  75. package/dist/channels/reconnection-manager.js +171 -0
  76. package/dist/channels/reconnection-manager.js.map +1 -0
  77. package/dist/channels/signal/index.d.ts +184 -0
  78. package/dist/channels/signal/index.js +488 -0
  79. package/dist/channels/signal/index.js.map +1 -0
  80. package/dist/channels/slack/client.d.ts +2 -1
  81. package/dist/channels/slack/client.js +30 -15
  82. package/dist/channels/slack/client.js.map +1 -1
  83. package/dist/channels/teams/index.d.ts +196 -0
  84. package/dist/channels/teams/index.js +477 -0
  85. package/dist/channels/teams/index.js.map +1 -0
  86. package/dist/channels/telegram/client.d.ts +3 -1
  87. package/dist/channels/telegram/client.js +29 -2
  88. package/dist/channels/telegram/client.js.map +1 -1
  89. package/dist/channels/webchat/index.d.ts +103 -0
  90. package/dist/channels/webchat/index.js +697 -0
  91. package/dist/channels/webchat/index.js.map +1 -0
  92. package/dist/channels/whatsapp/index.d.ts +105 -0
  93. package/dist/channels/whatsapp/index.js +533 -0
  94. package/dist/channels/whatsapp/index.js.map +1 -0
  95. package/dist/codebuddy/client.js +11 -5
  96. package/dist/codebuddy/client.js.map +1 -1
  97. package/dist/codebuddy/tool-definitions/advanced-tools.d.ts +1 -0
  98. package/dist/codebuddy/tool-definitions/advanced-tools.js +103 -3
  99. package/dist/codebuddy/tool-definitions/advanced-tools.js.map +1 -1
  100. package/dist/codebuddy/tool-definitions/index.d.ts +1 -1
  101. package/dist/codebuddy/tool-definitions/index.js +1 -1
  102. package/dist/codebuddy/tool-definitions/index.js.map +1 -1
  103. package/dist/codebuddy/tools.js +3 -1
  104. package/dist/codebuddy/tools.js.map +1 -1
  105. package/dist/commands/cli/config-command.d.ts +8 -0
  106. package/dist/commands/cli/config-command.js +90 -0
  107. package/dist/commands/cli/config-command.js.map +1 -0
  108. package/dist/commands/cli/openclaw-commands.d.ts +12 -0
  109. package/dist/commands/cli/openclaw-commands.js +446 -0
  110. package/dist/commands/cli/openclaw-commands.js.map +1 -0
  111. package/dist/commands/cli/utility-commands.js +30 -0
  112. package/dist/commands/cli/utility-commands.js.map +1 -1
  113. package/dist/commands/client-dispatcher.js +22 -2
  114. package/dist/commands/client-dispatcher.js.map +1 -1
  115. package/dist/commands/enhanced-command-handler.js +21 -2
  116. package/dist/commands/enhanced-command-handler.js.map +1 -1
  117. package/dist/commands/handlers/extra-handlers.d.ts +30 -0
  118. package/dist/commands/handlers/extra-handlers.js +547 -0
  119. package/dist/commands/handlers/extra-handlers.js.map +1 -0
  120. package/dist/commands/handlers/index.d.ts +1 -0
  121. package/dist/commands/handlers/index.js +2 -0
  122. package/dist/commands/handlers/index.js.map +1 -1
  123. package/dist/commands/slash/builtin-commands.js +41 -34
  124. package/dist/commands/slash/builtin-commands.js.map +1 -1
  125. package/dist/config/env-schema.d.ts +58 -0
  126. package/dist/config/env-schema.js +789 -0
  127. package/dist/config/env-schema.js.map +1 -0
  128. package/dist/config/feature-flags.js +2 -1
  129. package/dist/config/feature-flags.js.map +1 -1
  130. package/dist/context/bootstrap-loader.d.ts +48 -0
  131. package/dist/context/bootstrap-loader.js +123 -0
  132. package/dist/context/bootstrap-loader.js.map +1 -0
  133. package/dist/context/codebase-rag/chunker.js +2 -2
  134. package/dist/context/codebase-rag/chunker.js.map +1 -1
  135. package/dist/copilot/copilot-proxy.d.ts +15 -1
  136. package/dist/copilot/copilot-proxy.js +92 -23
  137. package/dist/copilot/copilot-proxy.js.map +1 -1
  138. package/dist/daemon/health-monitor.js +11 -7
  139. package/dist/daemon/health-monitor.js.map +1 -1
  140. package/dist/daemon/heartbeat.d.ts +112 -0
  141. package/dist/daemon/heartbeat.js +339 -0
  142. package/dist/daemon/heartbeat.js.map +1 -0
  143. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  144. package/dist/desktop-automation/smart-snapshot.js +38 -0
  145. package/dist/desktop-automation/smart-snapshot.js.map +1 -1
  146. package/dist/extensions/extension-loader.js +4 -0
  147. package/dist/extensions/extension-loader.js.map +1 -1
  148. package/dist/identity/identity-manager.d.ts +95 -0
  149. package/dist/identity/identity-manager.js +242 -0
  150. package/dist/identity/identity-manager.js.map +1 -0
  151. package/dist/index.js +147 -17
  152. package/dist/index.js.map +1 -1
  153. package/dist/input/text-to-speech.js +4 -2
  154. package/dist/input/text-to-speech.js.map +1 -1
  155. package/dist/input/voice-control.js +5 -3
  156. package/dist/input/voice-control.js.map +1 -1
  157. package/dist/integrations/github-integration.js +1 -1
  158. package/dist/integrations/github-integration.js.map +1 -1
  159. package/dist/orchestration/orchestrator.js +3 -0
  160. package/dist/orchestration/orchestrator.js.map +1 -1
  161. package/dist/persistence/conversation-branches.js +2 -1
  162. package/dist/persistence/conversation-branches.js.map +1 -1
  163. package/dist/persistence/session-store.d.ts +1 -1
  164. package/dist/persistence/session-store.js +1 -1
  165. package/dist/persistence/session-store.js.map +1 -1
  166. package/dist/plugins/plugin-system.js +5 -2
  167. package/dist/plugins/plugin-system.js.map +1 -1
  168. package/dist/providers/gemini-provider.js +6 -4
  169. package/dist/providers/gemini-provider.js.map +1 -1
  170. package/dist/providers/local-llm-provider.js +8 -0
  171. package/dist/providers/local-llm-provider.js.map +1 -1
  172. package/dist/sandbox/auto-sandbox.d.ts +59 -0
  173. package/dist/sandbox/auto-sandbox.js +145 -0
  174. package/dist/sandbox/auto-sandbox.js.map +1 -0
  175. package/dist/scheduler/cron-scheduler.js +2 -0
  176. package/dist/scheduler/cron-scheduler.js.map +1 -1
  177. package/dist/scheduler/scheduler.js +11 -2
  178. package/dist/scheduler/scheduler.js.map +1 -1
  179. package/dist/security/audit-logger.d.ts +127 -0
  180. package/dist/security/audit-logger.js +194 -0
  181. package/dist/security/audit-logger.js.map +1 -0
  182. package/dist/security/bash-allowlist/allowlist-store.js +3 -2
  183. package/dist/security/bash-allowlist/allowlist-store.js.map +1 -1
  184. package/dist/security/bash-parser.js +0 -2
  185. package/dist/security/bash-parser.js.map +1 -1
  186. package/dist/security/code-validator.d.ts +51 -0
  187. package/dist/security/code-validator.js +185 -0
  188. package/dist/security/code-validator.js.map +1 -0
  189. package/dist/security/dangerous-patterns.d.ts +68 -0
  190. package/dist/security/dangerous-patterns.js +218 -0
  191. package/dist/security/dangerous-patterns.js.map +1 -0
  192. package/dist/security/remote-approval.d.ts +65 -0
  193. package/dist/security/remote-approval.js +138 -0
  194. package/dist/security/remote-approval.js.map +1 -0
  195. package/dist/security/security-audit.d.ts +7 -0
  196. package/dist/security/security-audit.js +23 -0
  197. package/dist/security/security-audit.js.map +1 -1
  198. package/dist/security/syntax-validator.d.ts +17 -0
  199. package/dist/security/syntax-validator.js +292 -0
  200. package/dist/security/syntax-validator.js.map +1 -0
  201. package/dist/server/index.js +277 -2
  202. package/dist/server/index.js.map +1 -1
  203. package/dist/server/middleware/logging.js +9 -1
  204. package/dist/server/middleware/logging.js.map +1 -1
  205. package/dist/server/routes/memory.js +4 -1
  206. package/dist/server/routes/memory.js.map +1 -1
  207. package/dist/server/routes/metrics.js +1 -1
  208. package/dist/server/routes/metrics.js.map +1 -1
  209. package/dist/server/routes/sessions.js +5 -4
  210. package/dist/server/routes/sessions.js.map +1 -1
  211. package/dist/server/websocket/handler.js +8 -2
  212. package/dist/server/websocket/handler.js.map +1 -1
  213. package/dist/services/prompt-builder.js +16 -0
  214. package/dist/services/prompt-builder.js.map +1 -1
  215. package/dist/skills/hub.d.ts +231 -0
  216. package/dist/skills/hub.js +694 -0
  217. package/dist/skills/hub.js.map +1 -0
  218. package/dist/skills/skill-loader.js +1 -1
  219. package/dist/skills/skill-loader.js.map +1 -1
  220. package/dist/skills/skill-manager.js +2 -1
  221. package/dist/skills/skill-manager.js.map +1 -1
  222. package/dist/skills/skill-registry.js +4 -0
  223. package/dist/skills/skill-registry.js.map +1 -1
  224. package/dist/talk-mode/providers/audioreader-tts.js +1 -0
  225. package/dist/talk-mode/providers/audioreader-tts.js.map +1 -1
  226. package/dist/tools/apply-patch.d.ts +1 -0
  227. package/dist/tools/apply-patch.js +66 -12
  228. package/dist/tools/apply-patch.js.map +1 -1
  229. package/dist/tools/bash/bash-tool.d.ts +123 -0
  230. package/dist/tools/bash/bash-tool.js +549 -0
  231. package/dist/tools/bash/bash-tool.js.map +1 -0
  232. package/dist/tools/bash/command-validator.d.ts +49 -0
  233. package/dist/tools/bash/command-validator.js +223 -0
  234. package/dist/tools/bash/command-validator.js.map +1 -0
  235. package/dist/tools/bash/index.d.ts +7 -0
  236. package/dist/tools/bash/index.js +8 -0
  237. package/dist/tools/bash/index.js.map +1 -0
  238. package/dist/tools/bash/security-patterns.d.ts +44 -0
  239. package/dist/tools/bash/security-patterns.js +234 -0
  240. package/dist/tools/bash/security-patterns.js.map +1 -0
  241. package/dist/tools/bash/streaming-executor.d.ts +23 -0
  242. package/dist/tools/bash/streaming-executor.js +134 -0
  243. package/dist/tools/bash/streaming-executor.js.map +1 -0
  244. package/dist/tools/bash.js +5 -3
  245. package/dist/tools/bash.js.map +1 -1
  246. package/dist/tools/code-formatter.js +41 -27
  247. package/dist/tools/code-formatter.js.map +1 -1
  248. package/dist/tools/code-review.js +1 -1
  249. package/dist/tools/code-review.js.map +1 -1
  250. package/dist/tools/computer-control-tool.js +21 -0
  251. package/dist/tools/computer-control-tool.js.map +1 -1
  252. package/dist/tools/document-tool.js +3 -2
  253. package/dist/tools/document-tool.js.map +1 -1
  254. package/dist/tools/git-tool.d.ts +45 -0
  255. package/dist/tools/git-tool.js +224 -2
  256. package/dist/tools/git-tool.js.map +1 -1
  257. package/dist/tools/index.d.ts +1 -1
  258. package/dist/tools/index.js +1 -1
  259. package/dist/tools/index.js.map +1 -1
  260. package/dist/tools/morph-editor.js +1 -0
  261. package/dist/tools/morph-editor.js.map +1 -1
  262. package/dist/tools/multi-edit.js +31 -3
  263. package/dist/tools/multi-edit.js.map +1 -1
  264. package/dist/tools/notebook-tool.js +8 -2
  265. package/dist/tools/notebook-tool.js.map +1 -1
  266. package/dist/tools/process-tool.d.ts +69 -0
  267. package/dist/tools/process-tool.js +222 -0
  268. package/dist/tools/process-tool.js.map +1 -0
  269. package/dist/tools/registry/git-tools.d.ts +32 -0
  270. package/dist/tools/registry/git-tools.js +211 -0
  271. package/dist/tools/registry/git-tools.js.map +1 -0
  272. package/dist/tools/registry/index.d.ts +2 -0
  273. package/dist/tools/registry/index.js +8 -0
  274. package/dist/tools/registry/index.js.map +1 -1
  275. package/dist/tools/registry/misc-tools.d.ts +32 -4
  276. package/dist/tools/registry/misc-tools.js +230 -90
  277. package/dist/tools/registry/misc-tools.js.map +1 -1
  278. package/dist/tools/registry/process-tools.d.ts +20 -0
  279. package/dist/tools/registry/process-tools.js +141 -0
  280. package/dist/tools/registry/process-tools.js.map +1 -0
  281. package/dist/tools/registry/types.d.ts +2 -0
  282. package/dist/tools/search.js +4 -2
  283. package/dist/tools/search.js.map +1 -1
  284. package/dist/tools/video-tool.js +30 -14
  285. package/dist/tools/video-tool.js.map +1 -1
  286. package/dist/tools/web-search.js +4 -1
  287. package/dist/tools/web-search.js.map +1 -1
  288. package/dist/ui/components/ChatInterface.js +9 -0
  289. package/dist/ui/components/ChatInterface.js.map +1 -1
  290. package/dist/utils/autonomy-manager.js +3 -2
  291. package/dist/utils/autonomy-manager.js.map +1 -1
  292. package/dist/utils/config-validation/schema.d.ts +15 -15
  293. package/dist/utils/confirmation-service.d.ts +16 -0
  294. package/dist/utils/confirmation-service.js +37 -3
  295. package/dist/utils/confirmation-service.js.map +1 -1
  296. package/dist/utils/custom-instructions.js +2 -1
  297. package/dist/utils/custom-instructions.js.map +1 -1
  298. package/dist/utils/diff-generator.js +3 -1
  299. package/dist/utils/diff-generator.js.map +1 -1
  300. package/dist/utils/graceful-shutdown.js +9 -9
  301. package/dist/utils/graceful-shutdown.js.map +1 -1
  302. package/dist/utils/head-tail-truncation.d.ts +18 -0
  303. package/dist/utils/head-tail-truncation.js +127 -0
  304. package/dist/utils/head-tail-truncation.js.map +1 -1
  305. package/dist/utils/history-manager.js +3 -2
  306. package/dist/utils/history-manager.js.map +1 -1
  307. package/dist/utils/logger.d.ts +2 -0
  308. package/dist/utils/logger.js +18 -3
  309. package/dist/utils/logger.js.map +1 -1
  310. package/dist/utils/performance.js +16 -15
  311. package/dist/utils/performance.js.map +1 -1
  312. package/dist/utils/stream-helpers.js +4 -2
  313. package/dist/utils/stream-helpers.js.map +1 -1
  314. package/dist/utils/update-notifier.js +2 -1
  315. package/dist/utils/update-notifier.js.map +1 -1
  316. package/dist/workflows/pipeline.d.ts +54 -1
  317. package/dist/workflows/pipeline.js +128 -7
  318. package/dist/workflows/pipeline.js.map +1 -1
  319. package/dist/workflows/step-manager.js +2 -1
  320. package/dist/workflows/step-manager.js.map +1 -1
  321. package/package.json +6 -3
@@ -0,0 +1,694 @@
1
+ /**
2
+ * Skills Hub
3
+ *
4
+ * OpenClaw ClawHub-inspired Skills Hub for searching, installing,
5
+ * publishing, and syncing skills from a remote registry.
6
+ *
7
+ * Provides lockfile-based integrity management, SHA-256 checksums,
8
+ * semver version comparison, and event-driven lifecycle hooks.
9
+ */
10
+ import { EventEmitter } from 'events';
11
+ import { createHash } from 'crypto';
12
+ import * as fs from 'fs';
13
+ import * as path from 'path';
14
+ import * as os from 'os';
15
+ import * as yaml from 'yaml';
16
+ import { logger } from '../utils/logger.js';
17
+ import { parseSkillFile, validateSkill } from './parser.js';
18
+ // ============================================================================
19
+ // Constants
20
+ // ============================================================================
21
+ const DEFAULT_HUB_CONFIG = {
22
+ registryUrl: 'https://hub.codebuddy.dev/api/v1',
23
+ cacheDir: path.join(os.homedir(), '.codebuddy', 'hub', 'cache'),
24
+ skillsDir: path.join(os.homedir(), '.codebuddy', 'skills', 'managed'),
25
+ lockfilePath: path.join(os.homedir(), '.codebuddy', 'hub', 'lock.json'),
26
+ autoUpdate: false,
27
+ checkIntervalMs: 24 * 60 * 60 * 1000, // 24 hours
28
+ };
29
+ const LOCKFILE_VERSION = 1;
30
+ // ============================================================================
31
+ // Utility Functions
32
+ // ============================================================================
33
+ /**
34
+ * Compute SHA-256 checksum of content.
35
+ */
36
+ export function computeChecksum(content) {
37
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
38
+ }
39
+ /**
40
+ * Parse a semver string into [major, minor, patch] components.
41
+ * Returns [0, 0, 0] for invalid input.
42
+ */
43
+ export function parseSemver(version) {
44
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)/);
45
+ if (!match) {
46
+ return [0, 0, 0];
47
+ }
48
+ return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
49
+ }
50
+ /**
51
+ * Compare two semver strings.
52
+ * Returns -1 if a < b, 0 if a == b, 1 if a > b.
53
+ */
54
+ export function compareSemver(a, b) {
55
+ const [aMajor, aMinor, aPatch] = parseSemver(a);
56
+ const [bMajor, bMinor, bPatch] = parseSemver(b);
57
+ if (aMajor !== bMajor)
58
+ return aMajor < bMajor ? -1 : 1;
59
+ if (aMinor !== bMinor)
60
+ return aMinor < bMinor ? -1 : 1;
61
+ if (aPatch !== bPatch)
62
+ return aPatch < bPatch ? -1 : 1;
63
+ return 0;
64
+ }
65
+ // ============================================================================
66
+ // SkillsHub Class
67
+ // ============================================================================
68
+ export class SkillsHub extends EventEmitter {
69
+ config;
70
+ lockfile;
71
+ cache = new Map();
72
+ cacheTimestamp = 0;
73
+ cacheTtlMs = 5 * 60 * 1000; // 5 minutes
74
+ constructor(config = {}) {
75
+ super();
76
+ this.config = { ...DEFAULT_HUB_CONFIG, ...config };
77
+ this.lockfile = this.readLockfile();
78
+ this.ensureDirectories();
79
+ }
80
+ // ==========================================================================
81
+ // Directory & Lockfile Management
82
+ // ==========================================================================
83
+ /**
84
+ * Ensure required directories exist.
85
+ */
86
+ ensureDirectories() {
87
+ for (const dir of [this.config.cacheDir, this.config.skillsDir]) {
88
+ if (!fs.existsSync(dir)) {
89
+ fs.mkdirSync(dir, { recursive: true });
90
+ }
91
+ }
92
+ const lockDir = path.dirname(this.config.lockfilePath);
93
+ if (!fs.existsSync(lockDir)) {
94
+ fs.mkdirSync(lockDir, { recursive: true });
95
+ }
96
+ }
97
+ /**
98
+ * Read the lockfile from disk. Returns an empty lockfile if not found.
99
+ */
100
+ readLockfile() {
101
+ try {
102
+ if (fs.existsSync(this.config.lockfilePath)) {
103
+ const raw = fs.readFileSync(this.config.lockfilePath, 'utf-8');
104
+ const parsed = JSON.parse(raw);
105
+ if (parsed.version === LOCKFILE_VERSION && parsed.skills) {
106
+ return parsed;
107
+ }
108
+ }
109
+ }
110
+ catch (err) {
111
+ logger.warn('Failed to read hub lockfile, starting fresh', {
112
+ path: this.config.lockfilePath,
113
+ error: err instanceof Error ? err.message : String(err),
114
+ });
115
+ }
116
+ return {
117
+ version: LOCKFILE_VERSION,
118
+ updatedAt: new Date().toISOString(),
119
+ skills: {},
120
+ };
121
+ }
122
+ /**
123
+ * Write the lockfile to disk.
124
+ */
125
+ writeLockfile() {
126
+ this.lockfile.updatedAt = new Date().toISOString();
127
+ const content = JSON.stringify(this.lockfile, null, 2);
128
+ fs.writeFileSync(this.config.lockfilePath, content, 'utf-8');
129
+ logger.debug('Hub lockfile written', { path: this.config.lockfilePath });
130
+ }
131
+ /**
132
+ * Get the lockfile contents (for testing / external inspection).
133
+ */
134
+ getLockfile() {
135
+ return { ...this.lockfile, skills: { ...this.lockfile.skills } };
136
+ }
137
+ // ==========================================================================
138
+ // Search
139
+ // ==========================================================================
140
+ /**
141
+ * Search for skills by query string matching name, tags, and description.
142
+ * Checks local cache first, then fetches from remote registry.
143
+ */
144
+ async search(query, options = {}) {
145
+ const { tags, page = 1, pageSize: rawPageSize = 20, limit, sortBy = 'downloads', sortOrder = 'desc', } = options;
146
+ const pageSize = limit ?? rawPageSize;
147
+ logger.debug('Hub search', { query, tags, page, pageSize });
148
+ // Try remote fetch, fall back to cache
149
+ let allSkills;
150
+ try {
151
+ allSkills = await this.fetchRemoteSkills(query);
152
+ }
153
+ catch {
154
+ logger.debug('Remote fetch failed, using local cache');
155
+ allSkills = this.getLocalCacheSkills();
156
+ }
157
+ // Filter by query
158
+ const queryLower = query.toLowerCase();
159
+ let filtered = allSkills.filter(skill => {
160
+ const nameMatch = skill.name.toLowerCase().includes(queryLower);
161
+ const descMatch = skill.description.toLowerCase().includes(queryLower);
162
+ const tagMatch = skill.tags.some(t => t.toLowerCase().includes(queryLower));
163
+ return nameMatch || descMatch || tagMatch;
164
+ });
165
+ // Filter by tags
166
+ if (tags && tags.length > 0) {
167
+ const tagsLower = tags.map(t => t.toLowerCase());
168
+ filtered = filtered.filter(skill => skill.tags.some(t => tagsLower.includes(t.toLowerCase())));
169
+ }
170
+ // Sort
171
+ filtered.sort((a, b) => {
172
+ const aVal = a[sortBy];
173
+ const bVal = b[sortBy];
174
+ let cmp;
175
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
176
+ cmp = aVal - bVal;
177
+ }
178
+ else {
179
+ cmp = String(aVal).localeCompare(String(bVal));
180
+ }
181
+ return sortOrder === 'desc' ? -cmp : cmp;
182
+ });
183
+ // Paginate
184
+ const total = filtered.length;
185
+ const start = (page - 1) * pageSize;
186
+ const skills = filtered.slice(start, start + pageSize);
187
+ return { skills, total, page, pageSize };
188
+ }
189
+ /**
190
+ * Fetch skills from the remote registry.
191
+ * In a real implementation this would call the API.
192
+ * For now, returns cached data or an empty array.
193
+ */
194
+ async fetchRemoteSkills(query) {
195
+ const url = `${this.config.registryUrl}/skills/search?q=${encodeURIComponent(query)}`;
196
+ logger.debug('Fetching remote skills', { url });
197
+ // Attempt HTTP fetch
198
+ try {
199
+ const response = await fetch(url, {
200
+ method: 'GET',
201
+ headers: {
202
+ 'Accept': 'application/json',
203
+ 'User-Agent': 'codebuddy-hub/1.0',
204
+ },
205
+ signal: AbortSignal.timeout(10000),
206
+ });
207
+ if (response.ok) {
208
+ const data = await response.json();
209
+ if (data.skills && Array.isArray(data.skills)) {
210
+ // Update local cache
211
+ this.cache.set('remote', data.skills);
212
+ this.cacheTimestamp = Date.now();
213
+ this.writeLocalCache(data.skills);
214
+ return data.skills;
215
+ }
216
+ }
217
+ }
218
+ catch {
219
+ // Network error or timeout - fall through to cache
220
+ }
221
+ // Return cached data if fresh enough
222
+ if (this.cache.has('remote') && Date.now() - this.cacheTimestamp < this.cacheTtlMs) {
223
+ return this.cache.get('remote');
224
+ }
225
+ return this.getLocalCacheSkills();
226
+ }
227
+ /**
228
+ * Read locally cached skills from the cache directory.
229
+ */
230
+ getLocalCacheSkills() {
231
+ const cacheFile = path.join(this.config.cacheDir, 'registry-cache.json');
232
+ try {
233
+ if (fs.existsSync(cacheFile)) {
234
+ const raw = fs.readFileSync(cacheFile, 'utf-8');
235
+ const data = JSON.parse(raw);
236
+ return data.skills || [];
237
+ }
238
+ }
239
+ catch {
240
+ // Corrupted cache, ignore
241
+ }
242
+ return [];
243
+ }
244
+ /**
245
+ * Write skills to local cache file.
246
+ */
247
+ writeLocalCache(skills) {
248
+ const cacheFile = path.join(this.config.cacheDir, 'registry-cache.json');
249
+ try {
250
+ fs.writeFileSync(cacheFile, JSON.stringify({ skills, cachedAt: new Date().toISOString() }), 'utf-8');
251
+ }
252
+ catch {
253
+ logger.debug('Failed to write local cache');
254
+ }
255
+ }
256
+ // ==========================================================================
257
+ // Install
258
+ // ==========================================================================
259
+ /**
260
+ * Install a skill by name and optional version.
261
+ * Downloads the skill content and writes it to the managed skills directory.
262
+ */
263
+ async install(skillName, version) {
264
+ // Validate skill name to prevent path traversal
265
+ if (!/^[a-zA-Z0-9_-]+$/.test(skillName)) {
266
+ throw new Error(`Invalid skill name: ${skillName}. Only alphanumeric, dash, and underscore allowed.`);
267
+ }
268
+ logger.info('Installing skill', { name: skillName, version: version || 'latest' });
269
+ // Check if already installed with same version
270
+ const existing = this.lockfile.skills[skillName];
271
+ if (existing && version && existing.version === version) {
272
+ logger.info('Skill already installed at requested version', { name: skillName, version });
273
+ return existing;
274
+ }
275
+ // Fetch skill content
276
+ const content = await this.fetchSkillContent(skillName, version);
277
+ const checksum = computeChecksum(content);
278
+ // Parse and validate the SKILL.md content
279
+ const resolvedVersion = this.extractVersionFromContent(content) || version || '0.0.0';
280
+ this.validateSkillContent(content, skillName);
281
+ // Write to managed skills directory
282
+ const skillDir = path.join(this.config.skillsDir, skillName);
283
+ if (!fs.existsSync(skillDir)) {
284
+ fs.mkdirSync(skillDir, { recursive: true });
285
+ }
286
+ const skillPath = path.join(skillDir, 'SKILL.md');
287
+ fs.writeFileSync(skillPath, content, 'utf-8');
288
+ // Update lockfile
289
+ const installed = {
290
+ name: skillName,
291
+ version: resolvedVersion,
292
+ installedAt: Date.now(),
293
+ source: 'hub',
294
+ checksum,
295
+ path: skillPath,
296
+ };
297
+ this.lockfile.skills[skillName] = installed;
298
+ this.writeLockfile();
299
+ logger.info('Skill installed', { name: skillName, version: resolvedVersion, checksum });
300
+ this.emit('skill:installed', installed);
301
+ return installed;
302
+ }
303
+ /**
304
+ * Fetch skill content from the hub or local source.
305
+ * In a real implementation, this would download from the registry.
306
+ */
307
+ async fetchSkillContent(skillName, version) {
308
+ const versionParam = version ? `&version=${encodeURIComponent(version)}` : '';
309
+ const url = `${this.config.registryUrl}/skills/${encodeURIComponent(skillName)}/download?format=skillmd${versionParam}`;
310
+ try {
311
+ const response = await fetch(url, {
312
+ method: 'GET',
313
+ headers: {
314
+ 'Accept': 'text/markdown',
315
+ 'User-Agent': 'codebuddy-hub/1.0',
316
+ },
317
+ signal: AbortSignal.timeout(30000),
318
+ });
319
+ if (response.ok) {
320
+ return await response.text();
321
+ }
322
+ throw new Error(`Hub returned status ${response.status}: ${response.statusText}`);
323
+ }
324
+ catch (err) {
325
+ // Check local cache
326
+ const cached = path.join(this.config.cacheDir, `${skillName}.skill.md`);
327
+ if (fs.existsSync(cached)) {
328
+ logger.debug('Using cached skill content', { name: skillName });
329
+ return fs.readFileSync(cached, 'utf-8');
330
+ }
331
+ throw new Error(`Failed to fetch skill '${skillName}': ${err instanceof Error ? err.message : String(err)}`);
332
+ }
333
+ }
334
+ /**
335
+ * Install a skill from local content string (for local/offline installs).
336
+ */
337
+ async installFromContent(skillName, content, source = 'local') {
338
+ if (!/^[a-zA-Z0-9_-]+$/.test(skillName)) {
339
+ throw new Error(`Invalid skill name: ${skillName}. Only alphanumeric, dash, and underscore allowed.`);
340
+ }
341
+ const checksum = computeChecksum(content);
342
+ const version = this.extractVersionFromContent(content) || '0.0.0';
343
+ this.validateSkillContent(content, skillName);
344
+ // Write to managed skills directory
345
+ const skillDir = path.join(this.config.skillsDir, skillName);
346
+ if (!fs.existsSync(skillDir)) {
347
+ fs.mkdirSync(skillDir, { recursive: true });
348
+ }
349
+ const skillPath = path.join(skillDir, 'SKILL.md');
350
+ fs.writeFileSync(skillPath, content, 'utf-8');
351
+ const installed = {
352
+ name: skillName,
353
+ version,
354
+ installedAt: Date.now(),
355
+ source,
356
+ checksum,
357
+ path: skillPath,
358
+ };
359
+ this.lockfile.skills[skillName] = installed;
360
+ this.writeLockfile();
361
+ logger.info('Skill installed from content', { name: skillName, version, source });
362
+ this.emit('skill:installed', installed);
363
+ return installed;
364
+ }
365
+ // ==========================================================================
366
+ // Uninstall
367
+ // ==========================================================================
368
+ /**
369
+ * Remove an installed skill.
370
+ */
371
+ async uninstall(skillName) {
372
+ const installed = this.lockfile.skills[skillName];
373
+ if (!installed) {
374
+ logger.warn('Skill not found in lockfile', { name: skillName });
375
+ return false;
376
+ }
377
+ logger.info('Uninstalling skill', { name: skillName });
378
+ // Remove skill directory
379
+ const skillDir = path.join(this.config.skillsDir, skillName);
380
+ if (fs.existsSync(skillDir)) {
381
+ fs.rmSync(skillDir, { recursive: true, force: true });
382
+ }
383
+ // Remove from lockfile
384
+ delete this.lockfile.skills[skillName];
385
+ this.writeLockfile();
386
+ logger.info('Skill uninstalled', { name: skillName });
387
+ this.emit('skill:removed', skillName);
388
+ return true;
389
+ }
390
+ // ==========================================================================
391
+ // Update
392
+ // ==========================================================================
393
+ /**
394
+ * Update one or all installed skills.
395
+ * If skillName is provided, updates that skill only.
396
+ * Otherwise updates all installed skills.
397
+ */
398
+ async update(skillName) {
399
+ const updated = [];
400
+ const toUpdate = skillName
401
+ ? [this.lockfile.skills[skillName]].filter(Boolean)
402
+ : Object.values(this.lockfile.skills);
403
+ if (toUpdate.length === 0) {
404
+ logger.info('No skills to update');
405
+ return updated;
406
+ }
407
+ for (const skill of toUpdate) {
408
+ try {
409
+ // Check for newer version
410
+ const hubInfo = await this.getHubSkillInfo(skill.name);
411
+ if (!hubInfo) {
412
+ logger.debug('Skill not found on hub, skipping update', { name: skill.name });
413
+ continue;
414
+ }
415
+ if (compareSemver(hubInfo.version, skill.version) <= 0) {
416
+ logger.debug('Skill already at latest version', {
417
+ name: skill.name,
418
+ current: skill.version,
419
+ available: hubInfo.version,
420
+ });
421
+ continue;
422
+ }
423
+ logger.info('Updating skill', {
424
+ name: skill.name,
425
+ from: skill.version,
426
+ to: hubInfo.version,
427
+ });
428
+ const installed = await this.install(skill.name, hubInfo.version);
429
+ updated.push(installed);
430
+ this.emit('skill:updated', installed);
431
+ }
432
+ catch (err) {
433
+ logger.error('Failed to update skill', {
434
+ name: skill.name,
435
+ error: err instanceof Error ? err.message : String(err),
436
+ });
437
+ }
438
+ }
439
+ return updated;
440
+ }
441
+ /**
442
+ * Get skill info from the hub API.
443
+ */
444
+ async getHubSkillInfo(skillName) {
445
+ const url = `${this.config.registryUrl}/skills/${encodeURIComponent(skillName)}`;
446
+ try {
447
+ const response = await fetch(url, {
448
+ method: 'GET',
449
+ headers: {
450
+ 'Accept': 'application/json',
451
+ 'User-Agent': 'codebuddy-hub/1.0',
452
+ },
453
+ signal: AbortSignal.timeout(10000),
454
+ });
455
+ if (response.ok) {
456
+ return await response.json();
457
+ }
458
+ }
459
+ catch {
460
+ // Network error
461
+ }
462
+ return null;
463
+ }
464
+ // ==========================================================================
465
+ // Publish
466
+ // ==========================================================================
467
+ /**
468
+ * Validate and prepare a skill for publishing.
469
+ * Reads the SKILL.md, validates YAML frontmatter, computes checksum,
470
+ * and returns the prepared HubSkill metadata.
471
+ */
472
+ async publish(skillPath) {
473
+ const resolvedPath = path.resolve(skillPath);
474
+ if (!fs.existsSync(resolvedPath)) {
475
+ throw new Error(`Skill file not found: ${resolvedPath}`);
476
+ }
477
+ // Determine the SKILL.md path
478
+ let skillFilePath;
479
+ const stat = fs.statSync(resolvedPath);
480
+ if (stat.isDirectory()) {
481
+ skillFilePath = path.join(resolvedPath, 'SKILL.md');
482
+ if (!fs.existsSync(skillFilePath)) {
483
+ throw new Error(`No SKILL.md found in directory: ${resolvedPath}`);
484
+ }
485
+ }
486
+ else {
487
+ skillFilePath = resolvedPath;
488
+ }
489
+ const content = fs.readFileSync(skillFilePath, 'utf-8');
490
+ // Parse and validate
491
+ const skill = parseSkillFile(content, skillFilePath, 'workspace');
492
+ const validation = validateSkill(skill);
493
+ if (!validation.valid) {
494
+ throw new Error(`Skill validation failed: ${validation.errors.join(', ')}`);
495
+ }
496
+ // Ensure required publish fields
497
+ if (!skill.metadata.version) {
498
+ throw new Error('Skill version is required for publishing (add version to frontmatter)');
499
+ }
500
+ if (!skill.metadata.description) {
501
+ throw new Error('Skill description is required for publishing');
502
+ }
503
+ const checksum = computeChecksum(content);
504
+ const size = Buffer.byteLength(content, 'utf-8');
505
+ const hubSkill = {
506
+ name: skill.metadata.name,
507
+ version: skill.metadata.version,
508
+ description: skill.metadata.description,
509
+ author: skill.metadata.author || 'unknown',
510
+ tags: skill.metadata.tags || [],
511
+ downloads: 0,
512
+ stars: 0,
513
+ updatedAt: new Date().toISOString(),
514
+ checksum,
515
+ size,
516
+ };
517
+ logger.info('Skill prepared for publishing', {
518
+ name: hubSkill.name,
519
+ version: hubSkill.version,
520
+ checksum,
521
+ size,
522
+ });
523
+ this.emit('skill:published', hubSkill);
524
+ return hubSkill;
525
+ }
526
+ // ==========================================================================
527
+ // Sync
528
+ // ==========================================================================
529
+ /**
530
+ * Sync the lockfile with actually installed skills.
531
+ * - Removes lockfile entries for skills that no longer exist on disk.
532
+ * - Detects checksum mismatches (manual edits).
533
+ * - Optionally triggers updates if autoUpdate is enabled.
534
+ */
535
+ async sync() {
536
+ const removed = [];
537
+ const mismatched = [];
538
+ const updated = [];
539
+ logger.info('Syncing hub lockfile');
540
+ // Check each locked skill
541
+ const skillNames = Object.keys(this.lockfile.skills);
542
+ for (const name of skillNames) {
543
+ const entry = this.lockfile.skills[name];
544
+ // Check if skill still exists on disk
545
+ if (!fs.existsSync(entry.path)) {
546
+ logger.info('Skill file missing, removing from lockfile', { name, path: entry.path });
547
+ delete this.lockfile.skills[name];
548
+ removed.push(name);
549
+ continue;
550
+ }
551
+ // Verify checksum
552
+ const content = fs.readFileSync(entry.path, 'utf-8');
553
+ const currentChecksum = computeChecksum(content);
554
+ if (currentChecksum !== entry.checksum) {
555
+ logger.warn('Skill checksum mismatch (file was modified externally)', {
556
+ name,
557
+ expected: entry.checksum,
558
+ actual: currentChecksum,
559
+ });
560
+ mismatched.push(name);
561
+ // Update the lockfile entry to reflect current state
562
+ entry.checksum = currentChecksum;
563
+ const newVersion = this.extractVersionFromContent(content);
564
+ if (newVersion) {
565
+ entry.version = newVersion;
566
+ }
567
+ }
568
+ }
569
+ // Auto-update if configured
570
+ if (this.config.autoUpdate) {
571
+ const updateResults = await this.update();
572
+ for (const result of updateResults) {
573
+ updated.push(result.name);
574
+ }
575
+ }
576
+ this.writeLockfile();
577
+ logger.info('Hub sync complete', {
578
+ removed: removed.length,
579
+ mismatched: mismatched.length,
580
+ updated: updated.length,
581
+ });
582
+ return { removed, mismatched, updated };
583
+ }
584
+ // ==========================================================================
585
+ // List & Info
586
+ // ==========================================================================
587
+ /**
588
+ * List all installed skills from the lockfile.
589
+ */
590
+ list() {
591
+ return Object.values(this.lockfile.skills);
592
+ }
593
+ /**
594
+ * Get detailed information about an installed skill.
595
+ * Returns the lockfile entry plus the current on-disk content metadata.
596
+ */
597
+ info(skillName) {
598
+ const installed = this.lockfile.skills[skillName];
599
+ if (!installed) {
600
+ return null;
601
+ }
602
+ let content;
603
+ let integrityOk = false;
604
+ if (fs.existsSync(installed.path)) {
605
+ content = fs.readFileSync(installed.path, 'utf-8');
606
+ const currentChecksum = computeChecksum(content);
607
+ integrityOk = currentChecksum === installed.checksum;
608
+ }
609
+ return { installed, content, integrityOk };
610
+ }
611
+ // ==========================================================================
612
+ // Helpers
613
+ // ==========================================================================
614
+ /**
615
+ * Extract the version field from SKILL.md YAML frontmatter.
616
+ */
617
+ extractVersionFromContent(content) {
618
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
619
+ if (!match)
620
+ return null;
621
+ try {
622
+ const parsed = yaml.parse(match[1]);
623
+ if (typeof parsed.version === 'string') {
624
+ return parsed.version;
625
+ }
626
+ }
627
+ catch {
628
+ // Invalid YAML
629
+ }
630
+ return null;
631
+ }
632
+ /**
633
+ * Validate skill content by parsing it and checking required fields.
634
+ */
635
+ validateSkillContent(content, skillName) {
636
+ // Check that it has valid frontmatter
637
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
638
+ if (!match) {
639
+ throw new Error(`Invalid SKILL.md format for '${skillName}': missing YAML frontmatter`);
640
+ }
641
+ try {
642
+ const parsed = yaml.parse(match[1]);
643
+ if (!parsed.name || typeof parsed.name !== 'string') {
644
+ throw new Error(`SKILL.md for '${skillName}' is missing required 'name' field`);
645
+ }
646
+ if (!parsed.description || typeof parsed.description !== 'string') {
647
+ throw new Error(`SKILL.md for '${skillName}' is missing required 'description' field`);
648
+ }
649
+ }
650
+ catch (err) {
651
+ if (err instanceof Error && err.message.startsWith('SKILL.md')) {
652
+ throw err;
653
+ }
654
+ throw new Error(`Failed to parse YAML frontmatter for '${skillName}': ${err instanceof Error ? err.message : String(err)}`);
655
+ }
656
+ }
657
+ /**
658
+ * Get hub configuration.
659
+ */
660
+ getConfig() {
661
+ return { ...this.config };
662
+ }
663
+ /**
664
+ * Shutdown and cleanup.
665
+ */
666
+ shutdown() {
667
+ this.cache.clear();
668
+ this.removeAllListeners();
669
+ }
670
+ }
671
+ // ============================================================================
672
+ // Singleton
673
+ // ============================================================================
674
+ let hubInstance = null;
675
+ /**
676
+ * Get the singleton SkillsHub instance.
677
+ */
678
+ export function getSkillsHub(config) {
679
+ if (!hubInstance) {
680
+ hubInstance = new SkillsHub(config);
681
+ }
682
+ return hubInstance;
683
+ }
684
+ /**
685
+ * Reset the singleton SkillsHub instance (for testing).
686
+ */
687
+ export function resetSkillsHub() {
688
+ if (hubInstance) {
689
+ hubInstance.shutdown();
690
+ }
691
+ hubInstance = null;
692
+ }
693
+ export default SkillsHub;
694
+ //# sourceMappingURL=hub.js.map