@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.
- package/README.md +228 -13
- package/dist/agent/architect-mode.d.ts +11 -0
- package/dist/agent/architect-mode.js +133 -25
- package/dist/agent/architect-mode.js.map +1 -1
- package/dist/agent/codebuddy-agent.d.ts +24 -0
- package/dist/agent/codebuddy-agent.js +118 -16
- package/dist/agent/codebuddy-agent.js.map +1 -1
- package/dist/agent/execution/agent-executor.d.ts +9 -0
- package/dist/agent/execution/agent-executor.js +62 -1
- package/dist/agent/execution/agent-executor.js.map +1 -1
- package/dist/agent/message-queue.d.ts +77 -0
- package/dist/agent/message-queue.js +116 -0
- package/dist/agent/message-queue.js.map +1 -0
- package/dist/agent/middleware/auto-observation.d.ts +37 -0
- package/dist/agent/middleware/auto-observation.js +231 -0
- package/dist/agent/middleware/auto-observation.js.map +1 -0
- package/dist/agent/middleware/index.d.ts +2 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/index.js.map +1 -1
- package/dist/agent/tool-handler.js +3 -2
- package/dist/agent/tool-handler.js.map +1 -1
- package/dist/agent/turn-diff-tracker.js +3 -0
- package/dist/agent/turn-diff-tracker.js.map +1 -1
- package/dist/agent/types.d.ts +7 -2
- package/dist/analytics/budget-alerts.d.ts +81 -0
- package/dist/analytics/budget-alerts.js +126 -0
- package/dist/analytics/budget-alerts.js.map +1 -0
- package/dist/analytics/cost-predictor.d.ts +79 -0
- package/dist/analytics/cost-predictor.js +150 -0
- package/dist/analytics/cost-predictor.js.map +1 -0
- package/dist/analytics/index.d.ts +2 -0
- package/dist/analytics/index.js +2 -0
- package/dist/analytics/index.js.map +1 -1
- package/dist/auth/profile-manager.d.ts +205 -0
- package/dist/auth/profile-manager.js +484 -0
- package/dist/auth/profile-manager.js.map +1 -0
- package/dist/browser-automation/browser-manager.d.ts +79 -1
- package/dist/browser-automation/browser-manager.js +265 -2
- package/dist/browser-automation/browser-manager.js.map +1 -1
- package/dist/browser-automation/profile-manager.d.ts +32 -0
- package/dist/browser-automation/profile-manager.js +83 -0
- package/dist/browser-automation/profile-manager.js.map +1 -0
- package/dist/browser-automation/route-interceptor.d.ts +29 -0
- package/dist/browser-automation/route-interceptor.js +103 -0
- package/dist/browser-automation/route-interceptor.js.map +1 -0
- package/dist/browser-automation/screenshot-annotator.d.ts +23 -0
- package/dist/browser-automation/screenshot-annotator.js +86 -0
- package/dist/browser-automation/screenshot-annotator.js.map +1 -0
- package/dist/browser-automation/types.d.ts +47 -0
- package/dist/cache/llm-response-cache.js +3 -0
- package/dist/cache/llm-response-cache.js.map +1 -1
- package/dist/canvas/canvas-server.js +4 -3
- package/dist/canvas/canvas-server.js.map +1 -1
- package/dist/channels/discord/client.d.ts +2 -1
- package/dist/channels/discord/client.js +28 -16
- package/dist/channels/discord/client.js.map +1 -1
- package/dist/channels/dm-pairing.js +6 -3
- package/dist/channels/dm-pairing.js.map +1 -1
- package/dist/channels/google-chat/index.d.ts +210 -0
- package/dist/channels/google-chat/index.js +505 -0
- package/dist/channels/google-chat/index.js.map +1 -0
- package/dist/channels/group-security.d.ts +182 -0
- package/dist/channels/group-security.js +407 -0
- package/dist/channels/group-security.js.map +1 -0
- package/dist/channels/index.d.ts +17 -1
- package/dist/channels/index.js +16 -0
- package/dist/channels/index.js.map +1 -1
- package/dist/channels/matrix/index.d.ts +181 -0
- package/dist/channels/matrix/index.js +643 -0
- package/dist/channels/matrix/index.js.map +1 -0
- package/dist/channels/offline-queue.d.ts +92 -0
- package/dist/channels/offline-queue.js +112 -0
- package/dist/channels/offline-queue.js.map +1 -0
- package/dist/channels/reconnection-manager.d.ts +117 -0
- package/dist/channels/reconnection-manager.js +171 -0
- package/dist/channels/reconnection-manager.js.map +1 -0
- package/dist/channels/signal/index.d.ts +184 -0
- package/dist/channels/signal/index.js +488 -0
- package/dist/channels/signal/index.js.map +1 -0
- package/dist/channels/slack/client.d.ts +2 -1
- package/dist/channels/slack/client.js +30 -15
- package/dist/channels/slack/client.js.map +1 -1
- package/dist/channels/teams/index.d.ts +196 -0
- package/dist/channels/teams/index.js +477 -0
- package/dist/channels/teams/index.js.map +1 -0
- package/dist/channels/telegram/client.d.ts +3 -1
- package/dist/channels/telegram/client.js +29 -2
- package/dist/channels/telegram/client.js.map +1 -1
- package/dist/channels/webchat/index.d.ts +103 -0
- package/dist/channels/webchat/index.js +697 -0
- package/dist/channels/webchat/index.js.map +1 -0
- package/dist/channels/whatsapp/index.d.ts +105 -0
- package/dist/channels/whatsapp/index.js +533 -0
- package/dist/channels/whatsapp/index.js.map +1 -0
- package/dist/codebuddy/client.js +11 -5
- package/dist/codebuddy/client.js.map +1 -1
- package/dist/codebuddy/tool-definitions/advanced-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/advanced-tools.js +103 -3
- package/dist/codebuddy/tool-definitions/advanced-tools.js.map +1 -1
- package/dist/codebuddy/tool-definitions/index.d.ts +1 -1
- package/dist/codebuddy/tool-definitions/index.js +1 -1
- package/dist/codebuddy/tool-definitions/index.js.map +1 -1
- package/dist/codebuddy/tools.js +3 -1
- package/dist/codebuddy/tools.js.map +1 -1
- package/dist/commands/cli/config-command.d.ts +8 -0
- package/dist/commands/cli/config-command.js +90 -0
- package/dist/commands/cli/config-command.js.map +1 -0
- package/dist/commands/cli/openclaw-commands.d.ts +12 -0
- package/dist/commands/cli/openclaw-commands.js +446 -0
- package/dist/commands/cli/openclaw-commands.js.map +1 -0
- package/dist/commands/cli/utility-commands.js +30 -0
- package/dist/commands/cli/utility-commands.js.map +1 -1
- package/dist/commands/client-dispatcher.js +22 -2
- package/dist/commands/client-dispatcher.js.map +1 -1
- package/dist/commands/enhanced-command-handler.js +21 -2
- package/dist/commands/enhanced-command-handler.js.map +1 -1
- package/dist/commands/handlers/extra-handlers.d.ts +30 -0
- package/dist/commands/handlers/extra-handlers.js +547 -0
- package/dist/commands/handlers/extra-handlers.js.map +1 -0
- package/dist/commands/handlers/index.d.ts +1 -0
- package/dist/commands/handlers/index.js +2 -0
- package/dist/commands/handlers/index.js.map +1 -1
- package/dist/commands/slash/builtin-commands.js +41 -34
- package/dist/commands/slash/builtin-commands.js.map +1 -1
- package/dist/config/env-schema.d.ts +58 -0
- package/dist/config/env-schema.js +789 -0
- package/dist/config/env-schema.js.map +1 -0
- package/dist/config/feature-flags.js +2 -1
- package/dist/config/feature-flags.js.map +1 -1
- package/dist/context/bootstrap-loader.d.ts +48 -0
- package/dist/context/bootstrap-loader.js +123 -0
- package/dist/context/bootstrap-loader.js.map +1 -0
- package/dist/context/codebase-rag/chunker.js +2 -2
- package/dist/context/codebase-rag/chunker.js.map +1 -1
- package/dist/copilot/copilot-proxy.d.ts +15 -1
- package/dist/copilot/copilot-proxy.js +92 -23
- package/dist/copilot/copilot-proxy.js.map +1 -1
- package/dist/daemon/health-monitor.js +11 -7
- package/dist/daemon/health-monitor.js.map +1 -1
- package/dist/daemon/heartbeat.d.ts +112 -0
- package/dist/daemon/heartbeat.js +339 -0
- package/dist/daemon/heartbeat.js.map +1 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +38 -0
- package/dist/desktop-automation/smart-snapshot.js.map +1 -1
- package/dist/extensions/extension-loader.js +4 -0
- package/dist/extensions/extension-loader.js.map +1 -1
- package/dist/identity/identity-manager.d.ts +95 -0
- package/dist/identity/identity-manager.js +242 -0
- package/dist/identity/identity-manager.js.map +1 -0
- package/dist/index.js +147 -17
- package/dist/index.js.map +1 -1
- package/dist/input/text-to-speech.js +4 -2
- package/dist/input/text-to-speech.js.map +1 -1
- package/dist/input/voice-control.js +5 -3
- package/dist/input/voice-control.js.map +1 -1
- package/dist/integrations/github-integration.js +1 -1
- package/dist/integrations/github-integration.js.map +1 -1
- package/dist/orchestration/orchestrator.js +3 -0
- package/dist/orchestration/orchestrator.js.map +1 -1
- package/dist/persistence/conversation-branches.js +2 -1
- package/dist/persistence/conversation-branches.js.map +1 -1
- package/dist/persistence/session-store.d.ts +1 -1
- package/dist/persistence/session-store.js +1 -1
- package/dist/persistence/session-store.js.map +1 -1
- package/dist/plugins/plugin-system.js +5 -2
- package/dist/plugins/plugin-system.js.map +1 -1
- package/dist/providers/gemini-provider.js +6 -4
- package/dist/providers/gemini-provider.js.map +1 -1
- package/dist/providers/local-llm-provider.js +8 -0
- package/dist/providers/local-llm-provider.js.map +1 -1
- package/dist/sandbox/auto-sandbox.d.ts +59 -0
- package/dist/sandbox/auto-sandbox.js +145 -0
- package/dist/sandbox/auto-sandbox.js.map +1 -0
- package/dist/scheduler/cron-scheduler.js +2 -0
- package/dist/scheduler/cron-scheduler.js.map +1 -1
- package/dist/scheduler/scheduler.js +11 -2
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/security/audit-logger.d.ts +127 -0
- package/dist/security/audit-logger.js +194 -0
- package/dist/security/audit-logger.js.map +1 -0
- package/dist/security/bash-allowlist/allowlist-store.js +3 -2
- package/dist/security/bash-allowlist/allowlist-store.js.map +1 -1
- package/dist/security/bash-parser.js +0 -2
- package/dist/security/bash-parser.js.map +1 -1
- package/dist/security/code-validator.d.ts +51 -0
- package/dist/security/code-validator.js +185 -0
- package/dist/security/code-validator.js.map +1 -0
- package/dist/security/dangerous-patterns.d.ts +68 -0
- package/dist/security/dangerous-patterns.js +218 -0
- package/dist/security/dangerous-patterns.js.map +1 -0
- package/dist/security/remote-approval.d.ts +65 -0
- package/dist/security/remote-approval.js +138 -0
- package/dist/security/remote-approval.js.map +1 -0
- package/dist/security/security-audit.d.ts +7 -0
- package/dist/security/security-audit.js +23 -0
- package/dist/security/security-audit.js.map +1 -1
- package/dist/security/syntax-validator.d.ts +17 -0
- package/dist/security/syntax-validator.js +292 -0
- package/dist/security/syntax-validator.js.map +1 -0
- package/dist/server/index.js +277 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/middleware/logging.js +9 -1
- package/dist/server/middleware/logging.js.map +1 -1
- package/dist/server/routes/memory.js +4 -1
- package/dist/server/routes/memory.js.map +1 -1
- package/dist/server/routes/metrics.js +1 -1
- package/dist/server/routes/metrics.js.map +1 -1
- package/dist/server/routes/sessions.js +5 -4
- package/dist/server/routes/sessions.js.map +1 -1
- package/dist/server/websocket/handler.js +8 -2
- package/dist/server/websocket/handler.js.map +1 -1
- package/dist/services/prompt-builder.js +16 -0
- package/dist/services/prompt-builder.js.map +1 -1
- package/dist/skills/hub.d.ts +231 -0
- package/dist/skills/hub.js +694 -0
- package/dist/skills/hub.js.map +1 -0
- package/dist/skills/skill-loader.js +1 -1
- package/dist/skills/skill-loader.js.map +1 -1
- package/dist/skills/skill-manager.js +2 -1
- package/dist/skills/skill-manager.js.map +1 -1
- package/dist/skills/skill-registry.js +4 -0
- package/dist/skills/skill-registry.js.map +1 -1
- package/dist/talk-mode/providers/audioreader-tts.js +1 -0
- package/dist/talk-mode/providers/audioreader-tts.js.map +1 -1
- package/dist/tools/apply-patch.d.ts +1 -0
- package/dist/tools/apply-patch.js +66 -12
- package/dist/tools/apply-patch.js.map +1 -1
- package/dist/tools/bash/bash-tool.d.ts +123 -0
- package/dist/tools/bash/bash-tool.js +549 -0
- package/dist/tools/bash/bash-tool.js.map +1 -0
- package/dist/tools/bash/command-validator.d.ts +49 -0
- package/dist/tools/bash/command-validator.js +223 -0
- package/dist/tools/bash/command-validator.js.map +1 -0
- package/dist/tools/bash/index.d.ts +7 -0
- package/dist/tools/bash/index.js +8 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/security-patterns.d.ts +44 -0
- package/dist/tools/bash/security-patterns.js +234 -0
- package/dist/tools/bash/security-patterns.js.map +1 -0
- package/dist/tools/bash/streaming-executor.d.ts +23 -0
- package/dist/tools/bash/streaming-executor.js +134 -0
- package/dist/tools/bash/streaming-executor.js.map +1 -0
- package/dist/tools/bash.js +5 -3
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/code-formatter.js +41 -27
- package/dist/tools/code-formatter.js.map +1 -1
- package/dist/tools/code-review.js +1 -1
- package/dist/tools/code-review.js.map +1 -1
- package/dist/tools/computer-control-tool.js +21 -0
- package/dist/tools/computer-control-tool.js.map +1 -1
- package/dist/tools/document-tool.js +3 -2
- package/dist/tools/document-tool.js.map +1 -1
- package/dist/tools/git-tool.d.ts +45 -0
- package/dist/tools/git-tool.js +224 -2
- package/dist/tools/git-tool.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/morph-editor.js +1 -0
- package/dist/tools/morph-editor.js.map +1 -1
- package/dist/tools/multi-edit.js +31 -3
- package/dist/tools/multi-edit.js.map +1 -1
- package/dist/tools/notebook-tool.js +8 -2
- package/dist/tools/notebook-tool.js.map +1 -1
- package/dist/tools/process-tool.d.ts +69 -0
- package/dist/tools/process-tool.js +222 -0
- package/dist/tools/process-tool.js.map +1 -0
- package/dist/tools/registry/git-tools.d.ts +32 -0
- package/dist/tools/registry/git-tools.js +211 -0
- package/dist/tools/registry/git-tools.js.map +1 -0
- package/dist/tools/registry/index.d.ts +2 -0
- package/dist/tools/registry/index.js +8 -0
- package/dist/tools/registry/index.js.map +1 -1
- package/dist/tools/registry/misc-tools.d.ts +32 -4
- package/dist/tools/registry/misc-tools.js +230 -90
- package/dist/tools/registry/misc-tools.js.map +1 -1
- package/dist/tools/registry/process-tools.d.ts +20 -0
- package/dist/tools/registry/process-tools.js +141 -0
- package/dist/tools/registry/process-tools.js.map +1 -0
- package/dist/tools/registry/types.d.ts +2 -0
- package/dist/tools/search.js +4 -2
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/video-tool.js +30 -14
- package/dist/tools/video-tool.js.map +1 -1
- package/dist/tools/web-search.js +4 -1
- package/dist/tools/web-search.js.map +1 -1
- package/dist/ui/components/ChatInterface.js +9 -0
- package/dist/ui/components/ChatInterface.js.map +1 -1
- package/dist/utils/autonomy-manager.js +3 -2
- package/dist/utils/autonomy-manager.js.map +1 -1
- package/dist/utils/config-validation/schema.d.ts +15 -15
- package/dist/utils/confirmation-service.d.ts +16 -0
- package/dist/utils/confirmation-service.js +37 -3
- package/dist/utils/confirmation-service.js.map +1 -1
- package/dist/utils/custom-instructions.js +2 -1
- package/dist/utils/custom-instructions.js.map +1 -1
- package/dist/utils/diff-generator.js +3 -1
- package/dist/utils/diff-generator.js.map +1 -1
- package/dist/utils/graceful-shutdown.js +9 -9
- package/dist/utils/graceful-shutdown.js.map +1 -1
- package/dist/utils/head-tail-truncation.d.ts +18 -0
- package/dist/utils/head-tail-truncation.js +127 -0
- package/dist/utils/head-tail-truncation.js.map +1 -1
- package/dist/utils/history-manager.js +3 -2
- package/dist/utils/history-manager.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +18 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/performance.js +16 -15
- package/dist/utils/performance.js.map +1 -1
- package/dist/utils/stream-helpers.js +4 -2
- package/dist/utils/stream-helpers.js.map +1 -1
- package/dist/utils/update-notifier.js +2 -1
- package/dist/utils/update-notifier.js.map +1 -1
- package/dist/workflows/pipeline.d.ts +54 -1
- package/dist/workflows/pipeline.js +128 -7
- package/dist/workflows/pipeline.js.map +1 -1
- package/dist/workflows/step-manager.js +2 -1
- package/dist/workflows/step-manager.js.map +1 -1
- 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
|