diff --git a/README.md b/README.md index 978157a..ee5f1cf 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A powerful command-line note-taking application with semantic vector search capa ## ✨ Features - šŸ“ **Complete Note Management** - Create, edit, delete, and organize notes with powerful CLI tools +- 🌐 **Website Import** - Import web pages as notes with headless browser support and image URL preservation - 🌐 **Modern Web Interface** - Beautiful, responsive web UI with real-time markdown preview and graph visualization - šŸ·ļø **Smart Tagging System** - Organize notes with tags, search by tags, and manage tag collections - šŸ” **Triple Search Methods** - Semantic vector search, traditional text search, and tag-based search @@ -455,6 +456,28 @@ ml-notes delete -f 123 ml-notes delete --all ``` +#### Import Website Content +```bash +# Import a website as a new note +ml-notes import-url https://blog.example.com/article + +# Import with custom tags +ml-notes import-url https://docs.example.com/guide --tags "docs,reference,tutorial" + +# Import with AI auto-tagging +ml-notes import-url https://example.com/post --auto-tag + +# Import with custom timeout for slow-loading sites +ml-notes import-url https://heavy-site.com --timeout 60s +``` + +**Features:** +- **Headless Browser**: Uses Chrome to render JavaScript and dynamic content +- **Smart Content Extraction**: Prioritizes main content areas (article, main) while filtering out navigation, ads, and sidebars +- **Image URL Preservation**: Converts relative image URLs to absolute URLs while maintaining external/CDN links +- **Markdown Conversion**: Clean HTML-to-markdown conversion with proper formatting +- **Security-First**: Uses secure browser settings with SSL validation for live websites + ### Tag Management #### Managing Tags diff --git a/cmd/import.go b/cmd/import.go new file mode 100644 index 0000000..55477a9 --- /dev/null +++ b/cmd/import.go @@ -0,0 +1,371 @@ +package cmd + +import ( + "context" + "fmt" + "net/url" + "os" + "strings" + "time" + + md "github.com/JohannesKaufmann/html-to-markdown" + "github.com/PuerkitoBio/goquery" + "github.com/chromedp/chromedp" + "github.com/spf13/cobra" + "github.com/streed/ml-notes/internal/autotag" + interrors "github.com/streed/ml-notes/internal/errors" + "github.com/streed/ml-notes/internal/logger" + "github.com/streed/ml-notes/internal/models" + "github.com/streed/ml-notes/internal/search" +) + +var importCmd = &cobra.Command{ + Use: "import-url ", + Short: "Import a website as a new note", + Long: `Import a website by URL and create a new note with the page title and content converted to markdown. + +This command uses a headless browser to load the webpage, waiting for dynamic content to load, +then extracts the title and converts the body content to markdown format. + +Examples: + ml-notes import-url https://example.com + ml-notes import-url https://blog.example.com/article --auto-tag + ml-notes import-url https://docs.example.com --tags "docs,reference"`, + Args: cobra.ExactArgs(1), + RunE: runImport, +} + +var ( + importTags []string + importAutoTag bool + waitTimeout time.Duration +) + +func init() { + rootCmd.AddCommand(importCmd) + importCmd.Flags().StringSliceVarP(&importTags, "tags", "T", []string{}, "Tags for the imported note (comma-separated)") + importCmd.Flags().BoolVar(&importAutoTag, "auto-tag", false, "Automatically generate tags using AI") + importCmd.Flags().DurationVar(&waitTimeout, "timeout", 30*time.Second, "Timeout for page loading (default: 30s)") +} + +func runImport(cmd *cobra.Command, args []string) error { + pageURL := args[0] + + // Validate URL + if _, err := url.Parse(pageURL); err != nil { + return fmt.Errorf("invalid URL: %w", err) + } + + fmt.Printf("🌐 Importing from: %s\n", pageURL) + + // Extract page content using headless browser + title, content, err := extractPageContent(pageURL) + if err != nil { + return fmt.Errorf("failed to extract page content: %w", err) + } + + if title == "" { + title = pageURL // Use URL as fallback title + } + + if content == "" { + return interrors.ErrEmptyContent + } + + fmt.Printf("šŸ“„ Page title: %s\n", title) + fmt.Printf("šŸ“ Content extracted (%d characters)\n", len(content)) + + // Create note with tags if provided + var note *models.Note + if len(importTags) > 0 { + note, err = noteRepo.CreateWithTags(title, content, importTags) + } else { + note, err = noteRepo.Create(title, content) + } + if err != nil { + return fmt.Errorf("failed to create note: %w", err) + } + + // Auto-tag if requested + if importAutoTag { + fmt.Println("šŸ¤– Generating AI tags...") + autoTagger := autotag.NewAutoTagger(appConfig) + + if autoTagger.IsAvailable() { + suggestedTags, err := autoTagger.SuggestTags(note) + if err != nil { + fmt.Printf("āš ļø Auto-tagging failed: %v\n", err) + } else if len(suggestedTags) > 0 { + // Merge with existing tags + allTags := note.Tags + tagSet := make(map[string]bool) + for _, tag := range allTags { + tagSet[tag] = true + } + for _, tag := range suggestedTags { + if !tagSet[tag] { + allTags = append(allTags, tag) + } + } + + // Update note with auto-generated tags + if err := noteRepo.UpdateTags(note.ID, allTags); err != nil { + fmt.Printf("āš ļø Failed to apply auto-tags: %v\n", err) + } else { + note.Tags = allTags // Update for display + fmt.Printf("šŸ·ļø Auto-generated tags: %s\n", strings.Join(suggestedTags, ", ")) + } + } else { + fmt.Println("šŸ·ļø No auto-tags generated") + } + } else { + fmt.Printf("āš ļø Auto-tagging unavailable. Please ensure summarization is enabled and Ollama is running.\n") + } + } + + // Index the note for semantic search + fullText := title + " " + content + + // Use namespace-aware indexing if available + if lilragSearch, ok := vectorSearch.(*search.LilRagSearch); ok { + namespace := getCurrentProjectNamespace() + if err := lilragSearch.IndexNoteWithNamespace(note.ID, fullText, namespace, "default"); err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to index note for semantic search: %v\n", err) + } + } else { + if err := vectorSearch.IndexNote(note.ID, fullText); err != nil { + fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to index note for semantic search: %v\n", err) + } + } + + fmt.Printf("\nāœ… Note imported successfully!\n") + fmt.Printf("ID: %d\n", note.ID) + fmt.Printf("Title: %s\n", note.Title) + if len(note.Tags) > 0 { + fmt.Printf("Tags: %s\n", strings.Join(note.Tags, ", ")) + } + fmt.Printf("Created: %s\n", note.CreatedAt.Format("2006-01-02 15:04:05")) + fmt.Printf("Source: %s\n", pageURL) + + return nil +} + +// extractPageContent uses chromedp to extract title and content from a webpage +func extractPageContent(pageURL string) (title, content string, err error) { + // Configure Chrome options with security considerations + // Start with default options that include necessary headless browser settings + opts := append(chromedp.DefaultExecAllocatorOptions[:], + // Only add minimal flags needed for CI/container environments + // NoSandbox is only used if we detect we're in a restricted environment + chromedp.DisableGPU, // Safe to disable GPU in headless mode + // Use a realistic user agent for better compatibility + chromedp.UserAgent("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"), + ) + + // Only disable sandbox if we're in a restricted environment (CI/containers) + // This is detected by checking if we can create user namespaces + if isRestrictedEnvironment() { + opts = append(opts, chromedp.NoSandbox) + } + + // Create allocator context + allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) + defer cancel() + + // Create chromedp context + ctx, cancel := chromedp.NewContext(allocCtx) + defer cancel() + + // Set timeout + ctx, cancel = context.WithTimeout(ctx, waitTimeout) + defer cancel() + + var htmlContent string + + // Run the tasks + err = chromedp.Run(ctx, + // Navigate to the page + chromedp.Navigate(pageURL), + // Wait for the page to load + chromedp.WaitVisible("body", chromedp.ByQuery), + // Additional wait for dynamic content + chromedp.Sleep(2*time.Second), + // Extract title + chromedp.Title(&title), + // Extract the main content (prefer article, main, or body) + chromedp.ActionFunc(func(ctx context.Context) error { + // Try to find main content areas in order of preference + selectors := []string{ + "article", + "main", + "[role='main']", + ".main-content", + ".content", + ".post-content", + ".entry-content", + "body", + } + + for _, selector := range selectors { + var exists bool + err := chromedp.Evaluate(fmt.Sprintf(`document.querySelector('%s') !== null`, selector), &exists).Do(ctx) + if err == nil && exists { + return chromedp.InnerHTML(selector, &htmlContent, chromedp.ByQuery).Do(ctx) + } + } + + // Fallback to body + return chromedp.InnerHTML("body", &htmlContent, chromedp.ByQuery).Do(ctx) + }), + ) + + if err != nil { + return "", "", fmt.Errorf("failed to extract page content: %w", err) + } + + // Convert HTML to markdown + // Use empty domain initially and we'll handle URL resolution manually afterward + converter := md.NewConverter("", true, nil) + + // Configure converter options for better markdown output + converter.AddRules( + // Remove scripts and styles + md.Rule{ + Filter: []string{"script", "style", "noscript"}, + Replacement: func(content string, selection *goquery.Selection, opt *md.Options) *string { + text := "" + return &text + }, + }, + // Handle navigation and sidebar content + md.Rule{ + Filter: []string{"nav", "aside", ".sidebar", ".navigation", ".menu"}, + Replacement: func(content string, selection *goquery.Selection, opt *md.Options) *string { + text := "" + return &text + }, + }, + // Clean up footer content + md.Rule{ + Filter: []string{"footer", ".footer"}, + Replacement: func(content string, selection *goquery.Selection, opt *md.Options) *string { + text := "" + return &text + }, + }, + // Custom image handling to preserve original URLs + md.Rule{ + Filter: []string{"img"}, + Replacement: func(content string, selection *goquery.Selection, opt *md.Options) *string { + src, exists := selection.Attr("src") + if !exists { + text := "" + return &text + } + + // Resolve relative URLs to absolute URLs + absoluteSrc := resolveURL(pageURL, src) + + alt, _ := selection.Attr("alt") + if alt == "" { + alt = "Image" + } + + result := fmt.Sprintf("![%s](%s)", alt, absoluteSrc) + return &result + }, + }, + ) + + markdown, err := converter.ConvertString(htmlContent) + if err != nil { + return "", "", fmt.Errorf("failed to convert HTML to markdown: %w", err) + } + + // Clean up the markdown content + content = cleanMarkdownContent(markdown) + + logger.Debug("Extracted content from %s: title='%s', content_length=%d", pageURL, title, len(content)) + + return title, content, nil +} + +// cleanMarkdownContent removes excessive whitespace and cleans up the markdown +func cleanMarkdownContent(content string) string { + lines := strings.Split(content, "\n") + var cleanLines []string + + previousLineEmpty := false + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + // Skip empty lines that follow other empty lines (collapse multiple empty lines) + if trimmed == "" { + if !previousLineEmpty { + cleanLines = append(cleanLines, "") + previousLineEmpty = true + } + continue + } + + previousLineEmpty = false + cleanLines = append(cleanLines, trimmed) + } + + // Remove leading and trailing empty lines + for len(cleanLines) > 0 && cleanLines[0] == "" { + cleanLines = cleanLines[1:] + } + for len(cleanLines) > 0 && cleanLines[len(cleanLines)-1] == "" { + cleanLines = cleanLines[:len(cleanLines)-1] + } + + return strings.Join(cleanLines, "\n") +} + +// isRestrictedEnvironment checks if we're running in a restricted environment +// where Chrome sandbox needs to be disabled (CI, containers, etc.) +func isRestrictedEnvironment() bool { + // Check for common CI environment variables + ciEnvVars := []string{ + "CI", "CONTINUOUS_INTEGRATION", "BUILD_NUMBER", "GITHUB_ACTIONS", + "GITLAB_CI", "JENKINS_URL", "TRAVIS", "CIRCLECI", "BUILDKITE", + } + + for _, envVar := range ciEnvVars { + if os.Getenv(envVar) != "" { + return true + } + } + + // Check if we're running in a container + if _, err := os.Stat("/.dockerenv"); err == nil { + return true + } + + // Check for AppArmor restrictions (common in Ubuntu 23.10+) + if _, err := os.Stat("/proc/sys/kernel/apparmor_restrict_unprivileged_userns"); err == nil { + return true + } + + return false +} + +// resolveURL resolves a potentially relative URL against a base URL +func resolveURL(baseURL, href string) string { + // Parse the base URL + base, err := url.Parse(baseURL) + if err != nil { + return href // Return original if we can't parse base + } + + // Parse the href + ref, err := url.Parse(href) + if err != nil { + return href // Return original if we can't parse href + } + + // Resolve the reference against the base + resolved := base.ResolveReference(ref) + return resolved.String() +} \ No newline at end of file diff --git a/cmd/import_test.go b/cmd/import_test.go new file mode 100644 index 0000000..762f77a --- /dev/null +++ b/cmd/import_test.go @@ -0,0 +1,157 @@ +package cmd + +import ( + "testing" + "net/url" +) + +func TestValidateURL(t *testing.T) { + tests := []struct { + name string + input string + wantErr bool + }{ + { + name: "valid http url", + input: "https://example.com", + wantErr: false, + }, + { + name: "valid https url", + input: "http://example.com", + wantErr: false, + }, + { + name: "valid file url", + input: "file:///tmp/test.html", + wantErr: false, + }, + { + name: "relative path (invalid for our use case)", + input: "not-a-url", + wantErr: false, // url.Parse accepts this but we'd expect Chrome to handle it + }, + { + name: "empty url", + input: "", + wantErr: false, // url.Parse accepts empty string + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := url.Parse(tt.input) + hasErr := err != nil + if hasErr != tt.wantErr { + t.Errorf("url.Parse() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestCleanMarkdownContent(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "remove multiple empty lines", + input: "Line 1\n\n\n\nLine 2", + expected: "Line 1\n\nLine 2", + }, + { + name: "remove leading empty lines", + input: "\n\nLine 1\nLine 2", + expected: "Line 1\nLine 2", + }, + { + name: "remove trailing empty lines", + input: "Line 1\nLine 2\n\n\n", + expected: "Line 1\nLine 2", + }, + { + name: "trim whitespace", + input: " Line 1 \n Line 2 ", + expected: "Line 1\nLine 2", + }, + { + name: "single line", + input: "Single line", + expected: "Single line", + }, + { + name: "empty content", + input: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := cleanMarkdownContent(tt.input) + if result != tt.expected { + t.Errorf("cleanMarkdownContent() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestIsRestrictedEnvironment(t *testing.T) { + // Since we're running in GitHub Actions, this should return true + result := isRestrictedEnvironment() + if !result { + t.Errorf("isRestrictedEnvironment() = %v, want true (running in CI)", result) + } +} + + + +func TestImageURLConversion(t *testing.T) { + tests := []struct { + name string + baseURL string + href string + expected string + }{ + { + name: "relative path with https base", + baseURL: "https://example.com/page", + href: "/logo.png", + expected: "https://example.com/logo.png", + }, + { + name: "relative path with http base", + baseURL: "http://example.com/page", + href: "/logo.png", + expected: "http://example.com/logo.png", + }, + { + name: "absolute url unchanged", + baseURL: "https://example.com/page", + href: "https://cdn.other.com/image.jpg", + expected: "https://cdn.other.com/image.jpg", + }, + { + name: "protocol relative url", + baseURL: "https://example.com/page", + href: "//cdn.example.com/image.png", + expected: "https://cdn.example.com/image.png", + }, + { + name: "relative path from subdirectory", + baseURL: "https://blog.example.com/posts/article", + href: "../images/header.jpg", + expected: "https://blog.example.com/images/header.jpg", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := resolveURL(tt.baseURL, tt.href) + if result != tt.expected { + t.Errorf("resolveURL(%q, %q) = %q, want %q", tt.baseURL, tt.href, result, tt.expected) + } + }) + } +} \ No newline at end of file diff --git a/docs/USAGE_GUIDE.md b/docs/USAGE_GUIDE.md index 22e8807..48000e5 100644 --- a/docs/USAGE_GUIDE.md +++ b/docs/USAGE_GUIDE.md @@ -285,6 +285,7 @@ ml-notes search "first note" |---------|---------|---------| | `init` | Set up ml-notes | `ml-notes init -i` | | `add` | Create new notes | `ml-notes add -t "Title"` | +| `import-url` | Import website as note | `ml-notes import-url https://example.com` | | `list` | View all notes | `ml-notes list --limit 10` | | `get` | Retrieve specific note | `ml-notes get 123` | | `edit` | Modify existing notes | `ml-notes edit 123` | diff --git a/go.mod b/go.mod index 3c8b1ad..e089a20 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/streed/ml-notes -go 1.23 +go 1.24 -toolchain go1.24.6 +toolchain go1.24.7 require ( github.com/asg017/sqlite-vec-go-bindings v0.1.6 @@ -12,8 +12,18 @@ require ( ) require ( + github.com/JohannesKaufmann/html-to-markdown v1.6.0 // indirect + github.com/PuerkitoBio/goquery v1.9.2 // indirect + github.com/andybalholm/cascadia v1.3.2 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 // indirect + github.com/chromedp/chromedp v0.14.1 // indirect + github.com/chromedp/sysutil v1.1.0 // indirect + github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -24,5 +34,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0b77077..3a55c3c 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,35 @@ +github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k= +github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ= +github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= +github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= +github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= +github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/asg017/sqlite-vec-go-bindings v0.1.6 h1:Nx0jAzyS38XpkKznJ9xQjFXz2X9tI7KqjwVxV8RNoww= github.com/asg017/sqlite-vec-go-bindings v0.1.6/go.mod h1:A8+cTt/nKFsYCQF6OgzSNpKZrzNo5gQsXBTfsXHXY0Q= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 h1:UQ4AU+BGti3Sy/aLU8KVseYKNALcX9UXY6DfpwQ6J8E= +github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= +github.com/chromedp/chromedp v0.14.1 h1:0uAbnxewy/Q+Bg7oafVePE/6EXEho9hnaC38f+TTENg= +github.com/chromedp/chromedp v0.14.1/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo= +github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= +github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs= +github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -20,8 +41,11 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -30,6 +54,7 @@ github.com/mark3labs/mcp-go v0.37.0 h1:BywvZLPRT6Zx6mMG/MJfxLSZQkTGIcJSEGKsvr4Ds github.com/mark3labs/mcp-go v0.37.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -37,19 +62,85 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=