| Earl Lee | 2e463fb | 2025-04-17 11:22:22 -0700 | [diff] [blame] | 1 | // Package claudetool provides tools for Claude AI models. |
| 2 | // |
| 3 | // When adding, removing, or modifying tools in this package, |
| 4 | // remember to update the tool display template in termui/termui.go |
| 5 | // to ensure proper tool output formatting. |
| 6 | package claudetool |
| 7 | |
| 8 | import ( |
| 9 | "bytes" |
| 10 | "context" |
| 11 | "encoding/json" |
| 12 | "fmt" |
| 13 | "log/slog" |
| 14 | "net/http" |
| 15 | "os" |
| 16 | "time" |
| 17 | ) |
| 18 | |
| 19 | type workingDirCtxKeyType string |
| 20 | |
| 21 | const workingDirCtxKey workingDirCtxKeyType = "workingDir" |
| 22 | |
| 23 | func WithWorkingDir(ctx context.Context, wd string) context.Context { |
| 24 | return context.WithValue(ctx, workingDirCtxKey, wd) |
| 25 | } |
| 26 | |
| 27 | func WorkingDir(ctx context.Context) string { |
| 28 | // If cmd.Dir is empty, it uses the current working directory, |
| 29 | // so we can use that as a fallback. |
| 30 | wd, _ := ctx.Value(workingDirCtxKey).(string) |
| 31 | return wd |
| 32 | } |
| 33 | |
| 34 | // sendTelemetry posts debug data to an internal logging server. |
| 35 | // It is meant for use by people developing sketch and is disabled by default. |
| 36 | // This is a best-effort operation; errors are logged but not returned. |
| 37 | func sendTelemetry(ctx context.Context, typ string, data any) { |
| 38 | telemetryEndpoint := os.Getenv("SKETCH_TELEMETRY_ENDPOINT") |
| 39 | if telemetryEndpoint == "" { |
| 40 | return |
| 41 | } |
| 42 | err := doPostTelemetry(ctx, telemetryEndpoint, typ, data) |
| 43 | if err != nil { |
| 44 | slog.DebugContext(ctx, "failed to send JSON to server", "type", typ, "error", err) |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | func doPostTelemetry(ctx context.Context, telemetryEndpoint, typ string, data any) error { |
| 49 | jsonData, err := json.Marshal(data) |
| 50 | if err != nil { |
| 51 | return fmt.Errorf("failed to marshal %#v as JSON: %w", data, err) |
| 52 | } |
| 53 | timestamp := time.Now().Unix() |
| 54 | url := fmt.Sprintf(telemetryEndpoint+"/%s_%d.json", typ, timestamp) |
| 55 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) |
| 56 | if err != nil { |
| 57 | return fmt.Errorf("failed to create HTTP request for %s: %w", typ, err) |
| 58 | } |
| 59 | req.Header.Set("Content-Type", "application/json") |
| 60 | |
| 61 | resp, err := http.DefaultClient.Do(req) |
| 62 | if err != nil { |
| 63 | return fmt.Errorf("failed to send %s JSON to server: %w", typ, err) |
| 64 | } |
| 65 | defer resp.Body.Close() |
| 66 | |
| 67 | if resp.StatusCode/100 != 2 { |
| 68 | return fmt.Errorf("server returned non-success status for %s: %d", typ, resp.StatusCode) |
| 69 | } |
| 70 | slog.DebugContext(ctx, "successfully sent JSON to server", "file_type", typ, "url", url) |
| 71 | return nil |
| 72 | } |