Skip to content
This repository was archived by the owner on Dec 17, 2025. It is now read-only.

Commit 51b97f9

Browse files
committed
Add logging for install failures
- Moved some claude code stuff out into the client correctly
1 parent cdd9c6c commit 51b97f9

File tree

6 files changed

+248
-9
lines changed

6 files changed

+248
-9
lines changed

internal/clients/claude_code/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,8 @@ func (c *Client) EnsureSkillsSupport(ctx context.Context, scope *clients.Install
235235
// Claude Code loads global rules, so no special setup needed
236236
return nil
237237
}
238+
239+
// InstallHooks installs Claude Code-specific hooks (auto-update and usage tracking)
240+
func (c *Client) InstallHooks(ctx context.Context) error {
241+
return installHooks()
242+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
package claude_code
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/sleuth-io/skills/internal/logger"
10+
)
11+
12+
// installHooks installs system hooks for Claude Code (auto-update and usage tracking).
13+
// This is different from installing hook artifacts - these are the skills CLI's own hooks.
14+
func installHooks() error {
15+
home, err := os.UserHomeDir()
16+
if err != nil {
17+
return fmt.Errorf("failed to get home directory: %w", err)
18+
}
19+
claudeDir := filepath.Join(home, ".claude")
20+
21+
log := logger.Get()
22+
23+
// Install usage reporting hook
24+
if err := installUsageReportingHook(claudeDir); err != nil {
25+
log.Error("failed to install usage reporting hook", "error", err)
26+
return fmt.Errorf("failed to install usage reporting hook: %w", err)
27+
}
28+
29+
// Install session start hook for auto-update
30+
if err := installSessionStartHook(claudeDir); err != nil {
31+
log.Error("failed to install session start hook", "error", err)
32+
return fmt.Errorf("failed to install session start hook: %w", err)
33+
}
34+
35+
return nil
36+
}
37+
38+
// installSessionStartHook installs the SessionStart hook for auto-updating artifacts
39+
func installSessionStartHook(claudeDir string) error {
40+
settingsPath := filepath.Join(claudeDir, "settings.json")
41+
log := logger.Get()
42+
43+
// Read existing settings or create new
44+
var settings map[string]interface{}
45+
if data, err := os.ReadFile(settingsPath); err == nil {
46+
if err := json.Unmarshal(data, &settings); err != nil {
47+
log.Error("failed to parse settings.json for SessionStart hook", "error", err)
48+
return fmt.Errorf("failed to parse settings.json: %w", err)
49+
}
50+
} else {
51+
settings = make(map[string]interface{})
52+
}
53+
54+
// Get or create hooks section
55+
hooks, ok := settings["hooks"].(map[string]interface{})
56+
if !ok {
57+
hooks = make(map[string]interface{})
58+
settings["hooks"] = hooks
59+
}
60+
61+
// Get or create SessionStart array
62+
sessionStart, ok := hooks["SessionStart"].([]interface{})
63+
if !ok {
64+
sessionStart = []interface{}{}
65+
}
66+
67+
// Check if our hook already exists
68+
hookExists := false
69+
for _, item := range sessionStart {
70+
if hookMap, ok := item.(map[string]interface{}); ok {
71+
if hooksArray, ok := hookMap["hooks"].([]interface{}); ok {
72+
for _, h := range hooksArray {
73+
if hMap, ok := h.(map[string]interface{}); ok {
74+
if cmd, ok := hMap["command"].(string); ok && (cmd == "skills install --hook-mode" || cmd == "skills install" || cmd == "skills install --error-on-change") {
75+
hookExists = true
76+
break
77+
}
78+
}
79+
}
80+
}
81+
}
82+
if hookExists {
83+
break
84+
}
85+
}
86+
87+
// Add hook if it doesn't exist
88+
if !hookExists {
89+
newHook := map[string]interface{}{
90+
"hooks": []interface{}{
91+
map[string]interface{}{
92+
"type": "command",
93+
"command": "skills install --hook-mode",
94+
},
95+
},
96+
}
97+
sessionStart = append(sessionStart, newHook)
98+
hooks["SessionStart"] = sessionStart
99+
100+
// Write back to file
101+
data, err := json.MarshalIndent(settings, "", " ")
102+
if err != nil {
103+
log.Error("failed to marshal settings for SessionStart hook", "error", err)
104+
return fmt.Errorf("failed to marshal settings: %w", err)
105+
}
106+
107+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
108+
log.Error("failed to write settings.json for SessionStart hook", "error", err, "path", settingsPath)
109+
return fmt.Errorf("failed to write settings.json: %w", err)
110+
}
111+
112+
log.Info("hook installed", "hook", "SessionStart", "command", "skills install --hook-mode")
113+
}
114+
115+
return nil
116+
}
117+
118+
// installUsageReportingHook installs the PostToolUse hook for usage tracking
119+
func installUsageReportingHook(claudeDir string) error {
120+
settingsPath := filepath.Join(claudeDir, "settings.json")
121+
log := logger.Get()
122+
123+
// Read existing settings or create new
124+
var settings map[string]interface{}
125+
if data, err := os.ReadFile(settingsPath); err == nil {
126+
if err := json.Unmarshal(data, &settings); err != nil {
127+
log.Error("failed to parse settings.json for PostToolUse hook", "error", err)
128+
return fmt.Errorf("failed to parse settings.json: %w", err)
129+
}
130+
} else {
131+
settings = make(map[string]interface{})
132+
}
133+
134+
// Get or create hooks section
135+
hooks, ok := settings["hooks"].(map[string]interface{})
136+
if !ok {
137+
hooks = make(map[string]interface{})
138+
settings["hooks"] = hooks
139+
}
140+
141+
// Get or create PostToolUse array
142+
postToolUse, ok := hooks["PostToolUse"].([]interface{})
143+
if !ok {
144+
postToolUse = []interface{}{}
145+
}
146+
147+
// Check if our hook already exists
148+
hookExists := false
149+
for _, item := range postToolUse {
150+
if hookMap, ok := item.(map[string]interface{}); ok {
151+
if hooksArray, ok := hookMap["hooks"].([]interface{}); ok {
152+
for _, h := range hooksArray {
153+
if hMap, ok := h.(map[string]interface{}); ok {
154+
if cmd, ok := hMap["command"].(string); ok && cmd == "skills report-usage" {
155+
hookExists = true
156+
break
157+
}
158+
}
159+
}
160+
}
161+
}
162+
if hookExists {
163+
break
164+
}
165+
}
166+
167+
// Add hook if it doesn't exist
168+
if !hookExists {
169+
newHook := map[string]interface{}{
170+
"matcher": "Skill|Task|SlashCommand|mcp__.*",
171+
"hooks": []interface{}{
172+
map[string]interface{}{
173+
"type": "command",
174+
"command": "skills report-usage",
175+
},
176+
},
177+
}
178+
postToolUse = append(postToolUse, newHook)
179+
hooks["PostToolUse"] = postToolUse
180+
181+
// Write back to file
182+
data, err := json.MarshalIndent(settings, "", " ")
183+
if err != nil {
184+
log.Error("failed to marshal settings for PostToolUse hook", "error", err)
185+
return fmt.Errorf("failed to marshal settings: %w", err)
186+
}
187+
188+
if err := os.WriteFile(settingsPath, data, 0644); err != nil {
189+
log.Error("failed to write settings.json for PostToolUse hook", "error", err, "path", settingsPath)
190+
return fmt.Errorf("failed to write settings.json: %w", err)
191+
}
192+
193+
log.Info("hook installed", "hook", "PostToolUse", "command", "skills report-usage")
194+
}
195+
196+
return nil
197+
}

internal/clients/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ type Client interface {
3939
// For Cursor, this creates local .cursor/rules/skills.md with skills from all applicable scopes.
4040
// Clients that don't need post-install setup can return nil.
4141
EnsureSkillsSupport(ctx context.Context, scope *InstallScope) error
42+
43+
// InstallHooks installs client-specific hooks (e.g., auto-update, usage tracking).
44+
// This is called during installation to set up hooks in the client's configuration.
45+
// Clients that don't need hooks can return nil.
46+
InstallHooks(ctx context.Context) error
4247
}
4348

4449
// InstalledSkill represents a skill that has been installed

internal/clients/cursor/client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,13 @@ func (c *Client) ReadSkill(ctx context.Context, name string, scope *clients.Inst
421421
}, nil
422422
}
423423

424+
// InstallHooks is a no-op for Cursor since it doesn't need system hooks.
425+
// This method exists to satisfy the Client interface.
426+
func (c *Client) InstallHooks(ctx context.Context) error {
427+
// Cursor doesn't need system hooks
428+
return nil
429+
}
430+
424431
func init() {
425432
// Auto-register on package import
426433
clients.Register(NewClient())

internal/commands/install.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,14 @@ func runInstall(cmd *cobra.Command, args []string, hookMode bool) error {
9292
}
9393
} else if repoURL != "" && newETag != "" {
9494
// Save new ETag and lock file content
95+
log := logger.Get()
9596
if err := cache.SaveETag(repoURL, newETag); err != nil {
9697
out.printfErr("Warning: failed to save ETag: %v\n", err)
98+
log.Error("failed to save ETag", "repo_url", repoURL, "error", err)
9799
}
98100
if err := cache.SaveLockFile(repoURL, lockFileData); err != nil {
99101
out.printfErr("Warning: failed to cache lock file: %v\n", err)
102+
log.Error("failed to cache lock file", "repo_url", repoURL, "error", err)
100103
}
101104
}
102105

@@ -232,10 +235,8 @@ func runInstall(cmd *cobra.Command, args []string, hookMode bool) error {
232235
// Save state even if nothing changed (updates timestamp)
233236
saveInstallationState(trackingBase, lockFile, sortedArtifacts, targetClientIDs, out)
234237

235-
// Install Claude Code hooks even if no artifacts changed
236-
if err := installClaudeCodeHooks(claudeDir, out); err != nil {
237-
out.printfErr("\nWarning: failed to install hooks: %v\n", err)
238-
}
238+
// Install client-specific hooks (e.g., auto-update, usage tracking)
239+
installClientHooks(ctx, targetClients, out)
239240

240241
// Ensure skills support is configured for all clients (creates local rules files, etc.)
241242
// This is important even when no new artifacts are installed, as the local rules file
@@ -286,8 +287,10 @@ func runInstall(cmd *cobra.Command, args []string, hookMode bool) error {
286287

287288
if len(downloadErrors) > 0 {
288289
out.printErr("\nDownload errors:")
290+
log := logger.Get()
289291
for _, err := range downloadErrors {
290292
out.printfErr(" - %v\n", err)
293+
log.Error("artifact download failed", "error", err)
291294
}
292295
out.println()
293296
}
@@ -330,15 +333,13 @@ func runInstall(cmd *cobra.Command, args []string, hookMode bool) error {
330333
out.printfErr("✗ Failed to install %d artifacts:\n", len(installResult.Failed))
331334
for i, name := range installResult.Failed {
332335
out.printfErr(" - %s: %v\n", name, installResult.Errors[i])
336+
log.Error("artifact installation failed", "name", name, "error", installResult.Errors[i])
333337
}
334338
return fmt.Errorf("some artifacts failed to install")
335339
}
336340

337-
// Install Claude Code hooks
338-
if err := installClaudeCodeHooks(claudeDir, out); err != nil {
339-
out.printfErr("\nWarning: failed to install hooks: %v\n", err)
340-
// Don't fail the install command if hook installation fails
341-
}
341+
// Install client-specific hooks (e.g., auto-update, usage tracking)
342+
installClientHooks(ctx, targetClients, out)
342343

343344
// If in hook mode and artifacts were installed, output JSON message
344345
if hookMode && len(installResult.Installed) > 0 {
@@ -411,6 +412,8 @@ func loadPreviousInstallState(trackingBase string, out *outputHelper) *artifacts
411412
previousInstall, err := artifacts.LoadInstalledArtifacts(trackingBase)
412413
if err != nil {
413414
out.printfErr("Warning: failed to load previous installation state: %v\n", err)
415+
log := logger.Get()
416+
log.Error("failed to load previous installation state", "tracking_base", trackingBase, "error", err)
414417
return &artifacts.InstalledArtifacts{
415418
Version: artifacts.TrackerFormatVersion,
416419
Artifacts: []artifacts.InstalledArtifact{},
@@ -481,6 +484,7 @@ func cleanupRemovedArtifacts(ctx context.Context, previousInstall *artifacts.Ins
481484
resp, err := client.UninstallArtifacts(ctx, uninstallReq)
482485
if err != nil {
483486
out.printfErr("Warning: cleanup failed for %s: %v\n", client.DisplayName(), err)
487+
log.Error("cleanup failed", "client", client.ID(), "error", err)
484488
continue
485489
}
486490

@@ -490,6 +494,7 @@ func cleanupRemovedArtifacts(ctx context.Context, previousInstall *artifacts.Ins
490494
log.Info("artifact removed", "name", result.ArtifactName, "client", client.ID())
491495
} else if result.Status == clients.StatusFailed {
492496
out.printfErr("Warning: failed to remove %s from %s: %v\n", result.ArtifactName, client.DisplayName(), result.Error)
497+
log.Error("artifact removal failed", "name", result.ArtifactName, "client", client.ID(), "error", result.Error)
493498
}
494499
}
495500
}
@@ -587,11 +592,25 @@ func processInstallationResults(allResults map[string]clients.InstallResponse, o
587592
return installResult
588593
}
589594

595+
// installClientHooks calls InstallHooks on all clients to install client-specific hooks
596+
func installClientHooks(ctx context.Context, targetClients []clients.Client, out *outputHelper) {
597+
log := logger.Get()
598+
for _, client := range targetClients {
599+
if err := client.InstallHooks(ctx); err != nil {
600+
out.printfErr("Warning: failed to install hooks for %s: %v\n", client.DisplayName(), err)
601+
log.Error("failed to install client hooks", "client", client.ID(), "error", err)
602+
// Don't fail the install command if hook installation fails
603+
}
604+
}
605+
}
606+
590607
// ensureSkillsSupport calls EnsureSkillsSupport on all clients to set up local rules files, etc.
591608
func ensureSkillsSupport(ctx context.Context, targetClients []clients.Client, scope *clients.InstallScope, out *outputHelper) {
609+
log := logger.Get()
592610
for _, client := range targetClients {
593611
if err := client.EnsureSkillsSupport(ctx, scope); err != nil {
594612
out.printfErr("Warning: failed to ensure skills support for %s: %v\n", client.DisplayName(), err)
613+
log.Error("failed to ensure skills support", "client", client.ID(), "error", err)
595614
}
596615
}
597616
}
@@ -617,5 +636,7 @@ func saveInstallationState(trackingBase string, lockFile *lockfile.LockFile, sor
617636

618637
if err := artifacts.SaveInstalledArtifacts(trackingBase, newInstall); err != nil {
619638
out.printfErr("Warning: failed to save installation state: %v\n", err)
639+
log := logger.Get()
640+
log.Error("failed to save installation state", "tracking_base", trackingBase, "error", err)
620641
}
621642
}

internal/mcp/server_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ func (m *mockClient) EnsureSkillsSupport(ctx context.Context, scope *clients.Ins
5757
return nil
5858
}
5959

60+
func (m *mockClient) InstallHooks(ctx context.Context) error {
61+
return nil
62+
}
63+
6064
func (m *mockClient) addSkill(name, description, content, baseDir string) {
6165
m.skills[name] = &clients.SkillContent{
6266
Name: name,

0 commit comments

Comments
 (0)