summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors <[email protected]>2025-11-03 21:17:12 -0500
committers <[email protected]>2025-11-03 21:17:12 -0500
commitf7fcfa623e670dc533bb378912829c73a3593e63 (patch)
tree910119ff7293b407affa9ff34706d627d77a3a04
downloaddborg-f7fcfa623e670dc533bb378912829c73a3593e63.tar.gz
dborg-f7fcfa623e670dc533bb378912829c73a3593e63.zip
hi
-rw-r--r--.gitignore32
-rw-r--r--Makefile16
-rw-r--r--README.md177
-rw-r--r--cmd/admin.go238
-rw-r--r--cmd/npd.go95
-rw-r--r--cmd/osint.go103
-rw-r--r--cmd/root.go25
-rw-r--r--cmd/skiptrace.go199
-rw-r--r--cmd/sl.go73
-rw-r--r--cmd/x.go61
-rw-r--r--go.mod10
-rw-r--r--go.sum10
-rw-r--r--internal/client/admin.go124
-rw-r--r--internal/client/client.go119
-rw-r--r--internal/client/client_test.go46
-rw-r--r--internal/client/npd.go91
-rw-r--r--internal/client/osint.go32
-rw-r--r--internal/client/skiptrace.go179
-rw-r--r--internal/client/sl.go53
-rw-r--r--internal/client/usrsx.go72
-rw-r--r--internal/client/x.go23
-rw-r--r--internal/config/config.go36
-rw-r--r--internal/config/errors.go7
-rw-r--r--internal/models/admin.go43
-rw-r--r--internal/models/npd.go42
-rw-r--r--internal/models/osint.go29
-rw-r--r--internal/models/skiptrace.go92
-rw-r--r--internal/models/sl.go23
-rw-r--r--internal/models/usrsx.go33
-rw-r--r--internal/models/x.go22
-rw-r--r--internal/utils/output.go39
-rw-r--r--main.go7
32 files changed, 2151 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aaadf73
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Code coverage profiles and other test artifacts
+*.out
+coverage.*
+*.coverprofile
+profile.cov
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+go.work.sum
+
+# env file
+.env
+
+# Editor/IDE
+# .idea/
+# .vscode/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..04e7e39
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+.PHONY: build build-admin clean install install-admin
+
+build:
+ go build -o dborg .
+
+build-admin:
+ go build -tags admin -o dborg .
+
+clean:
+ rm -f dborg
+
+install:
+ go install .
+
+install-admin:
+ go install -tags admin .
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a835822
--- /dev/null
+++ b/README.md
@@ -0,0 +1,177 @@
+# dborg - DB.org.ai CLI
+
+CLI tool for querying the DB.org.ai API services.
+
+## Project Structure
+
+```
+dborg/
+├── cmd/ # CLI commands
+│ ├── admin.go # Admin commands (build tag: admin)
+│ ├── npd.go # NPD breach data search
+│ ├── root.go # Root command configuration
+│ ├── sl.go # Stealer logs search
+│ ├── usrsx.go # Username availability check
+│ └── x.go # Twitter/X username history
+├── internal/ # Private application code
+│ ├── client/ # API client implementation
+│ │ ├── admin.go # Admin API methods
+│ │ ├── client.go # Base HTTP client
+│ │ ├── client_test.go # Client tests
+│ │ ├── npd.go # NPD API methods
+│ │ ├── sl.go # Stealer logs API methods
+│ │ ├── usrsx.go # Username check API methods
+│ │ └── x.go # Twitter/X API methods
+│ ├── config/ # Configuration management
+│ │ ├── config.go # Config structure
+│ │ └── errors.go # Custom errors
+│ ├── models/ # Data models
+│ │ ├── admin.go # Admin response types
+│ │ ├── npd.go # NPD response types
+│ │ ├── sl.go # Stealer logs types
+│ │ ├── usrsx.go # Username check types
+│ │ └── x.go # Twitter/X types
+│ └── utils/ # Utility functions
+│ └── output.go # Output formatting helpers
+├── go.mod
+├── go.sum
+├── main.go
+├── Makefile
+└── README.md
+```
+
+## Installation
+
+```bash
+go install
+```
+
+To build with admin commands:
+
+```bash
+make build-admin
+```
+
+Or:
+
+```bash
+go build -tags admin -o dborg .
+```
+
+## Configuration
+
+Set your API key:
+
+```bash
+export DBORG_API_KEY=your_api_key_here
+```
+
+Or pass it with each command:
+
+```bash
+dborg --api-key YOUR_KEY [command]
+```
+
+## Commands
+
+### NPD - Search NPD breach data
+
+```bash
+dborg npd --firstname John --lastname Doe --max_hits 20
+```
+
+Available flags:
+- `--id`, `--firstname`, `--lastname`, `--middlename`
+- `--dob`, `--ssn`, `--phone1`
+- `--address`, `--city`, `--st`, `--zip`, `--county_name`
+- `--name_suff`, `--aka1fullname`, `--aka2fullname`, `--aka3fullname`
+- `--alt1dob`, `--alt2dob`, `--alt3dob`, `--startdat`
+- `--max_hits` (default: 10)
+- `--sort_by`
+
+### SL - Search stealer logs
+
+```bash
+dborg sl "domain.com" --max_hits 20 --format json
+```
+
+Available flags:
+- `--max_hits` (default: 10)
+- `--sort_by` (ingest_timestamp or date_posted)
+- `--ingest_start_date`, `--ingest_end_date`
+- `--posted_start_date`, `--posted_end_date`
+- `--format` (json, ulp, up, pul, etc.)
+
+### USRSX - Check username availability
+
+```bash
+dborg usrsx username123 --sites GitHub,Twitter --max_tasks 100
+```
+
+Available flags:
+- `--sites` (comma-separated list)
+- `--fuzzy` (enable fuzzy validation)
+- `--max_tasks` (default: 50)
+
+### X - Twitter/X username history
+
+```bash
+dborg x elonmusk
+```
+
+### Admin Commands (requires build tag)
+
+Only available when built with `-tags admin`:
+
+```bash
+dborg admin list
+dborg admin create john_doe --credits 1000 --unlimited
+dborg admin delete API_KEY
+dborg admin credits API_KEY 500
+dborg admin disable API_KEY
+dborg admin disable API_KEY --enable
+```
+
+## Architecture Benefits
+
+The refactored structure provides:
+
+1. **Separation of Concerns**: API logic is separated from CLI commands
+2. **Testability**: Components can be easily unit tested
+3. **Maintainability**: Clear organization makes code easier to maintain
+4. **Reusability**: Client can be reused in other Go applications
+5. **Error Handling**: Centralized error handling with custom error types
+6. **Configuration Management**: Single source of truth for configuration
+7. **Type Safety**: Strongly typed models for API requests/responses
+
+## Building
+
+Standard build (no admin commands):
+```bash
+make build
+```
+
+Build with admin commands:
+```bash
+make build-admin
+```
+
+Run tests:
+```bash
+go test ./...
+```
+
+Clean:
+```bash
+make clean
+```
+
+## Development
+
+The codebase follows Go best practices:
+
+- Internal packages for private implementation
+- Interface-based design for testability
+- Structured error handling
+- Clear separation between CLI and business logic
+- Comprehensive type definitions for API interactions
diff --git a/cmd/admin.go b/cmd/admin.go
new file mode 100644
index 0000000..3c43e20
--- /dev/null
+++ b/cmd/admin.go
@@ -0,0 +1,238 @@
+package cmd
+
+import (
+ "dborg/internal/client"
+ "dborg/internal/config"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "strconv"
+
+ "github.com/spf13/cobra"
+)
+
+var adminCmd = &cobra.Command{
+ Use: "admin",
+ Short: "Admin operations",
+ Long: `Administrative operations for managing accounts`,
+}
+
+var adminListCmd = &cobra.Command{
+ Use: "list",
+ Short: "List all accounts",
+ RunE: runAdminList,
+}
+
+var adminCreateCmd = &cobra.Command{
+ Use: "create [name]",
+ Short: "Create new account",
+ Args: cobra.ExactArgs(1),
+ RunE: runAdminCreate,
+}
+
+var adminDeleteCmd = &cobra.Command{
+ Use: "delete [api_key]",
+ Short: "Delete account",
+ Args: cobra.ExactArgs(1),
+ RunE: runAdminDelete,
+}
+
+var adminCreditsCmd = &cobra.Command{
+ Use: "credits [api_key] [amount]",
+ Short: "Add credits to account",
+ Args: cobra.ExactArgs(2),
+ RunE: runAdminCredits,
+}
+
+var adminSetCreditsCmd = &cobra.Command{
+ Use: "set-credits [api_key] [amount]",
+ Short: "Set account credits to specific amount",
+ Args: cobra.ExactArgs(2),
+ RunE: runAdminSetCredits,
+}
+
+var adminDisableCmd = &cobra.Command{
+ Use: "disable [api_key]",
+ Short: "Disable/enable account",
+ Args: cobra.ExactArgs(1),
+ RunE: runAdminDisable,
+}
+
+func init() {
+ rootCmd.AddCommand(adminCmd)
+ adminCmd.AddCommand(adminListCmd)
+ adminCmd.AddCommand(adminCreateCmd)
+ adminCmd.AddCommand(adminDeleteCmd)
+ adminCmd.AddCommand(adminCreditsCmd)
+ adminCmd.AddCommand(adminSetCreditsCmd)
+ adminCmd.AddCommand(adminDisableCmd)
+
+ adminCreateCmd.Flags().IntP("credits", "c", 0, "Initial credits")
+ adminCreateCmd.Flags().BoolP("unlimited", "u", false, "Unlimited credits")
+ adminCreateCmd.Flags().BoolP("premium", "p", false, "Premium account (enables skiptrace access)")
+ adminDisableCmd.Flags().BoolP("enable", "e", false, "Enable account instead of disable")
+}
+
+func getAdminClient(cmd *cobra.Command) (*client.Client, error) {
+ apiKey, _ := cmd.Flags().GetString("api-key")
+ cfg := config.New().WithAPIKey(apiKey)
+ return client.New(cfg)
+}
+
+func runAdminList(cmd *cobra.Command, args []string) error {
+ c, err := getAdminClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ response, err := c.ListAccounts()
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ output, err := json.MarshalIndent(response.Accounts, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+
+ fmt.Println(string(output))
+ return nil
+}
+
+func runAdminCreate(cmd *cobra.Command, args []string) error {
+ c, err := getAdminClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ credits, _ := cmd.Flags().GetInt("credits")
+ unlimited, _ := cmd.Flags().GetBool("unlimited")
+ premium, _ := cmd.Flags().GetBool("premium")
+
+ req := &models.AccountCreateRequest{
+ Name: args[0],
+ Credits: credits,
+ Unlimited: unlimited,
+ IsPremium: premium,
+ }
+
+ response, err := c.CreateAccount(req)
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ if response.Account != nil {
+ fmt.Printf("Account created successfully!\n")
+ fmt.Printf("Name: %s\n", response.Account.Name)
+ fmt.Printf("API Key: %s\n", response.Account.APIKey)
+ fmt.Printf("Credits: %d\n", response.Account.Credits)
+ fmt.Printf("Unlimited: %v\n", response.Account.Unlimited)
+ fmt.Printf("Premium: %v\n", response.Account.IsPremium)
+ fmt.Printf("Disabled: %v\n", response.Account.Disabled)
+ } else {
+ fmt.Println(response.Message)
+ }
+ return nil
+}
+
+func runAdminDelete(cmd *cobra.Command, args []string) error {
+ c, err := getAdminClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ response, err := c.DeleteAccount(args[0])
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ fmt.Println(response.Message)
+ return nil
+}
+
+func runAdminCredits(cmd *cobra.Command, args []string) error {
+ c, err := getAdminClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ credits, err := strconv.Atoi(args[1])
+ if err != nil {
+ return fmt.Errorf("invalid credits amount: %s", args[1])
+ }
+
+ response, err := c.UpdateCredits(args[0], credits)
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ fmt.Println(response.Message)
+ return nil
+}
+
+func runAdminSetCredits(cmd *cobra.Command, args []string) error {
+ c, err := getAdminClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ credits, err := strconv.Atoi(args[1])
+ if err != nil {
+ return fmt.Errorf("invalid credits amount: %s", args[1])
+ }
+
+ response, err := c.SetCredits(args[0], credits)
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ if response.Account != nil {
+ fmt.Printf("Credits set successfully!\n")
+ fmt.Printf("Account: %s\n", response.Account.Name)
+ fmt.Printf("API Key: %s\n", response.Account.APIKey)
+ fmt.Printf("Credits: %d\n", response.Account.Credits)
+ } else if response.Message != "" {
+ fmt.Println(response.Message)
+ }
+ return nil
+}
+
+func runAdminDisable(cmd *cobra.Command, args []string) error {
+ c, err := getAdminClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ enable, _ := cmd.Flags().GetBool("enable")
+ response, err := c.ToggleAccount(args[0], enable)
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ fmt.Println(response.Message)
+ return nil
+}
diff --git a/cmd/npd.go b/cmd/npd.go
new file mode 100644
index 0000000..0df627f
--- /dev/null
+++ b/cmd/npd.go
@@ -0,0 +1,95 @@
+package cmd
+
+import (
+ "dborg/internal/client"
+ "dborg/internal/config"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+var npdCmd = &cobra.Command{
+ Use: "npd",
+ Short: "Search NPD breach data",
+ Long: `Search NPD breach data by various fields`,
+ RunE: runNPDSearch,
+}
+
+func init() {
+ rootCmd.AddCommand(npdCmd)
+ npdCmd.Flags().StringP("id", "i", "", "ID")
+ npdCmd.Flags().StringP("firstname", "f", "", "First name")
+ npdCmd.Flags().StringP("lastname", "l", "", "Last name")
+ npdCmd.Flags().StringP("middlename", "m", "", "Middle name")
+ npdCmd.Flags().StringP("dob", "d", "", "Date of birth")
+ npdCmd.Flags().StringP("ssn", "s", "", "Social security number")
+ npdCmd.Flags().StringP("phone1", "p", "", "Phone number")
+ npdCmd.Flags().StringP("address", "a", "", "Address")
+ npdCmd.Flags().StringP("city", "c", "", "City")
+ npdCmd.Flags().StringP("st", "t", "", "State")
+ npdCmd.Flags().StringP("zip", "z", "", "ZIP code")
+ npdCmd.Flags().StringP("county_name", "y", "", "County name")
+ npdCmd.Flags().StringP("name_suff", "x", "", "Name suffix")
+ npdCmd.Flags().StringP("aka1fullname", "1", "", "AKA 1 full name")
+ npdCmd.Flags().StringP("aka2fullname", "2", "", "AKA 2 full name")
+ npdCmd.Flags().StringP("aka3fullname", "3", "", "AKA 3 full name")
+ npdCmd.Flags().StringP("alt1dob", "4", "", "Alternate DOB 1")
+ npdCmd.Flags().StringP("alt2dob", "5", "", "Alternate DOB 2")
+ npdCmd.Flags().StringP("alt3dob", "6", "", "Alternate DOB 3")
+ npdCmd.Flags().StringP("startdat", "r", "", "Start date")
+ npdCmd.Flags().IntP("max_hits", "n", 10, "Maximum number of hits to return")
+ npdCmd.Flags().StringP("sort_by", "o", "", "Sort by field")
+}
+
+func runNPDSearch(cmd *cobra.Command, args []string) error {
+ apiKey, _ := cmd.Flags().GetString("api-key")
+ cfg := config.New().WithAPIKey(apiKey)
+
+ c, err := client.New(cfg)
+ if err != nil {
+ return err
+ }
+
+ params := &models.NPDParams{}
+ params.ID, _ = cmd.Flags().GetString("id")
+ params.FirstName, _ = cmd.Flags().GetString("firstname")
+ params.LastName, _ = cmd.Flags().GetString("lastname")
+ params.MiddleName, _ = cmd.Flags().GetString("middlename")
+ params.DOB, _ = cmd.Flags().GetString("dob")
+ params.SSN, _ = cmd.Flags().GetString("ssn")
+ params.Phone1, _ = cmd.Flags().GetString("phone1")
+ params.Address, _ = cmd.Flags().GetString("address")
+ params.City, _ = cmd.Flags().GetString("city")
+ params.State, _ = cmd.Flags().GetString("st")
+ params.Zip, _ = cmd.Flags().GetString("zip")
+ params.CountyName, _ = cmd.Flags().GetString("county_name")
+ params.NameSuffix, _ = cmd.Flags().GetString("name_suff")
+ params.AKA1FullName, _ = cmd.Flags().GetString("aka1fullname")
+ params.AKA2FullName, _ = cmd.Flags().GetString("aka2fullname")
+ params.AKA3FullName, _ = cmd.Flags().GetString("aka3fullname")
+ params.Alt1DOB, _ = cmd.Flags().GetString("alt1dob")
+ params.Alt2DOB, _ = cmd.Flags().GetString("alt2dob")
+ params.Alt3DOB, _ = cmd.Flags().GetString("alt3dob")
+ params.StartDate, _ = cmd.Flags().GetString("startdat")
+ params.MaxHits, _ = cmd.Flags().GetInt("max_hits")
+ params.SortBy, _ = cmd.Flags().GetString("sort_by")
+
+ response, err := c.SearchNPD(params)
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ output, err := json.MarshalIndent(response.Results.Hits, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+
+ fmt.Println(string(output))
+ return nil
+}
diff --git a/cmd/osint.go b/cmd/osint.go
new file mode 100644
index 0000000..2f1f427
--- /dev/null
+++ b/cmd/osint.go
@@ -0,0 +1,103 @@
+package cmd
+
+import (
+ "dborg/internal/client"
+ "dborg/internal/config"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+var osintCmd = &cobra.Command{
+ Use: "osint",
+ Short: "OSINT tools and searches",
+ Long: `Open Source Intelligence tools for username, email, and other searches`,
+}
+
+var osintUsernameCmd = &cobra.Command{
+ Use: "username [username]",
+ Short: "Check username availability across websites",
+ Long: `Check username availability across hundreds of websites using WhatsMyName dataset`,
+ Args: cobra.ExactArgs(1),
+ RunE: runOsintUsernameCheck,
+}
+
+var osintBSSIDCmd = &cobra.Command{
+ Use: "bssid [bssid]",
+ Short: "Lookup WiFi access point location by BSSID",
+ Long: `Lookup geographic location of a WiFi access point by its BSSID (MAC address) using Apple's location services`,
+ Args: cobra.ExactArgs(1),
+ RunE: runOsintBSSIDLookup,
+}
+
+func init() {
+ rootCmd.AddCommand(osintCmd)
+ osintCmd.AddCommand(osintUsernameCmd)
+ osintCmd.AddCommand(osintBSSIDCmd)
+
+ osintUsernameCmd.Flags().StringSliceP("sites", "s", []string{}, "Specific sites to check (comma-separated)")
+ osintUsernameCmd.Flags().BoolP("fuzzy", "f", false, "Enable fuzzy validation mode")
+ osintUsernameCmd.Flags().IntP("max_tasks", "m", 50, "Maximum concurrent tasks")
+
+ osintBSSIDCmd.Flags().BoolP("all", "a", false, "Show all related results instead of exact match only")
+ osintBSSIDCmd.Flags().BoolP("map", "m", false, "Include Google Maps URL for the location")
+}
+
+func runOsintUsernameCheck(cmd *cobra.Command, args []string) error {
+ apiKey, _ := cmd.Flags().GetString("api-key")
+ cfg := config.New().WithAPIKey(apiKey)
+
+ c, err := client.New(cfg)
+ if err != nil {
+ return err
+ }
+
+ params := &models.USRSXParams{
+ Username: args[0],
+ }
+ params.Sites, _ = cmd.Flags().GetStringSlice("sites")
+ params.Fuzzy, _ = cmd.Flags().GetBool("fuzzy")
+ params.MaxTasks, _ = cmd.Flags().GetInt("max_tasks")
+
+ err = c.CheckUsernameStream(params, func(result json.RawMessage) error {
+ fmt.Println(string(result))
+ return nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func runOsintBSSIDLookup(cmd *cobra.Command, args []string) error {
+ apiKey, _ := cmd.Flags().GetString("api-key")
+ cfg := config.New().WithAPIKey(apiKey)
+
+ c, err := client.New(cfg)
+ if err != nil {
+ return err
+ }
+
+ params := &models.BSSIDParams{
+ BSSID: args[0],
+ }
+ params.All, _ = cmd.Flags().GetBool("all")
+ params.Map, _ = cmd.Flags().GetBool("map")
+
+ response, err := c.LookupBSSID(params)
+ if err != nil {
+ return err
+ }
+
+ output, err := json.MarshalIndent(response, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+
+ fmt.Println(string(output))
+ return nil
+}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..b8ab9e4
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,25 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "dborg",
+ Short: "DB.org.ai CLI client",
+ Long: `Query db.org.ai API`,
+}
+
+func Execute() {
+ if err := rootCmd.Execute(); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func init() {
+ rootCmd.PersistentFlags().StringP("api-key", "k", os.Getenv("DBORG_API_KEY"), "API key for authentication")
+}
diff --git a/cmd/skiptrace.go b/cmd/skiptrace.go
new file mode 100644
index 0000000..68fb832
--- /dev/null
+++ b/cmd/skiptrace.go
@@ -0,0 +1,199 @@
+package cmd
+
+import (
+ "dborg/internal/client"
+ "dborg/internal/config"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "strconv"
+
+ "github.com/spf13/cobra"
+)
+
+var skiptraceCmd = &cobra.Command{
+ Use: "skiptrace",
+ Short: "Premium skiptrace operations (requires premium API access)",
+ Long: `Search for people, phone numbers, and email addresses using premium skiptrace data.
+
+Note: All skiptrace commands require a premium API key. If you receive a 403 error,
+contact support to upgrade your account for premium access.`,
+}
+
+var skiptracePeopleCmd = &cobra.Command{
+ Use: "people",
+ Short: "Search for people by name",
+ Long: `Search for people by first name, last name, and optional location/age filters`,
+ RunE: runSkiptracePeople,
+}
+
+var skiptraceReportCmd = &cobra.Command{
+ Use: "report [sx_key] [selection]",
+ Short: "Get detailed report for selected person",
+ Long: `Retrieve detailed report for a person from previous search results using sx_key and selection number`,
+ Args: cobra.ExactArgs(2),
+ RunE: runSkiptraceReport,
+}
+
+var skiptracePhoneCmd = &cobra.Command{
+ Use: "phone [phone_number]",
+ Short: "Search for phone number",
+ Long: `Look up information about a phone number (10 digits, no +1 prefix)`,
+ Args: cobra.ExactArgs(1),
+ RunE: runSkiptracePhone,
+}
+
+var skiptraceEmailCmd = &cobra.Command{
+ Use: "email [email_address]",
+ Short: "Search for email address",
+ Long: `Look up information about an email address`,
+ Args: cobra.ExactArgs(1),
+ RunE: runSkiptraceEmail,
+}
+
+func init() {
+ rootCmd.AddCommand(skiptraceCmd)
+ skiptraceCmd.AddCommand(skiptracePeopleCmd)
+ skiptraceCmd.AddCommand(skiptraceReportCmd)
+ skiptraceCmd.AddCommand(skiptracePhoneCmd)
+ skiptraceCmd.AddCommand(skiptraceEmailCmd)
+
+ skiptracePeopleCmd.Flags().StringP("first-name", "f", "", "First name (required)")
+ skiptracePeopleCmd.Flags().StringP("last-name", "l", "", "Last name (required)")
+ skiptracePeopleCmd.Flags().StringP("city", "c", "", "City")
+ skiptracePeopleCmd.Flags().StringP("state", "s", "", "State (2-letter code)")
+ skiptracePeopleCmd.Flags().StringP("age", "a", "", "Age")
+ skiptracePeopleCmd.MarkFlagRequired("first-name")
+ skiptracePeopleCmd.MarkFlagRequired("last-name")
+}
+
+func getSkiptraceClient(cmd *cobra.Command) (*client.Client, error) {
+ apiKey, _ := cmd.Flags().GetString("api-key")
+ cfg := config.New().WithAPIKey(apiKey)
+ return client.New(cfg)
+}
+
+func runSkiptracePeople(cmd *cobra.Command, args []string) error {
+ c, err := getSkiptraceClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ params := &models.SkiptraceParams{}
+ params.FirstName, _ = cmd.Flags().GetString("first-name")
+ params.LastName, _ = cmd.Flags().GetString("last-name")
+ params.City, _ = cmd.Flags().GetString("city")
+ params.State, _ = cmd.Flags().GetString("state")
+ params.Age, _ = cmd.Flags().GetString("age")
+
+ response, err := c.SearchPeople(params)
+ if err != nil {
+ return err
+ }
+
+ if response.Data != nil && len(response.Data) > 0 {
+ output, err := json.MarshalIndent(response.Data, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+ fmt.Println(string(output))
+ }
+
+ return nil
+}
+
+func runSkiptraceReport(cmd *cobra.Command, args []string) error {
+ c, err := getSkiptraceClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ sxKey := args[0]
+ selection, err := strconv.Atoi(args[1])
+ if err != nil {
+ return fmt.Errorf("invalid selection number: %s", args[1])
+ }
+
+ response, err := c.GetPersonReport(sxKey, selection)
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ if response.Data != nil && len(response.Data) > 0 {
+ output, err := json.MarshalIndent(response.Data, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+ fmt.Println(string(output))
+ }
+
+ if response.Message != "" {
+ fmt.Println(response.Message)
+ }
+
+ return nil
+}
+
+func runSkiptracePhone(cmd *cobra.Command, args []string) error {
+ c, err := getSkiptraceClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ response, err := c.SearchPhone(args[0])
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ if response.Data != nil && len(response.Data) > 0 {
+ output, err := json.MarshalIndent(response.Data, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+ fmt.Println(string(output))
+ }
+
+ if response.Message != "" {
+ fmt.Println(response.Message)
+ }
+
+ return nil
+}
+
+func runSkiptraceEmail(cmd *cobra.Command, args []string) error {
+ c, err := getSkiptraceClient(cmd)
+ if err != nil {
+ return err
+ }
+
+ response, err := c.SearchEmail(args[0])
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ if response.Data != nil && len(response.Data) > 0 {
+ output, err := json.MarshalIndent(response.Data, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+ fmt.Println(string(output))
+ }
+
+ if response.Message != "" {
+ fmt.Println(response.Message)
+ }
+
+ return nil
+}
diff --git a/cmd/sl.go b/cmd/sl.go
new file mode 100644
index 0000000..1551681
--- /dev/null
+++ b/cmd/sl.go
@@ -0,0 +1,73 @@
+package cmd
+
+import (
+ "dborg/internal/client"
+ "dborg/internal/config"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+var slCmd = &cobra.Command{
+ Use: "sl [query]",
+ Short: "Search stealer logs",
+ Long: `Search stealer logs with various filters`,
+ Args: cobra.ExactArgs(1),
+ RunE: runSLSearch,
+}
+
+func init() {
+ rootCmd.AddCommand(slCmd)
+ slCmd.Flags().IntP("max_hits", "n", 10, "Maximum number of hits to return")
+ slCmd.Flags().StringP("sort_by", "s", "", "Sort by field (ingest_timestamp or date_posted)")
+ slCmd.Flags().StringP("ingest_start_date", "i", "", "Ingest timestamp start date")
+ slCmd.Flags().StringP("ingest_end_date", "e", "", "Ingest timestamp end date")
+ slCmd.Flags().StringP("posted_start_date", "p", "", "Date posted start date")
+ slCmd.Flags().StringP("posted_end_date", "d", "", "Date posted end date")
+ slCmd.Flags().StringP("format", "f", "json", "Response format")
+}
+
+func runSLSearch(cmd *cobra.Command, args []string) error {
+ apiKey, _ := cmd.Flags().GetString("api-key")
+ cfg := config.New().WithAPIKey(apiKey)
+
+ c, err := client.New(cfg)
+ if err != nil {
+ return err
+ }
+
+ params := &models.SLParams{
+ Query: args[0],
+ }
+ params.MaxHits, _ = cmd.Flags().GetInt("max_hits")
+ params.SortBy, _ = cmd.Flags().GetString("sort_by")
+ params.IngestStartDate, _ = cmd.Flags().GetString("ingest_start_date")
+ params.IngestEndDate, _ = cmd.Flags().GetString("ingest_end_date")
+ params.PostedStartDate, _ = cmd.Flags().GetString("posted_start_date")
+ params.PostedEndDate, _ = cmd.Flags().GetString("posted_end_date")
+ params.Format, _ = cmd.Flags().GetString("format")
+
+ response, err := c.SearchStealerLogs(params)
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ if params.Format != "json" {
+ fmt.Println(response.Message)
+ return nil
+ }
+
+ output, err := json.MarshalIndent(response.Results, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+
+ fmt.Println(string(output))
+ return nil
+}
diff --git a/cmd/x.go b/cmd/x.go
new file mode 100644
index 0000000..b4114b5
--- /dev/null
+++ b/cmd/x.go
@@ -0,0 +1,61 @@
+package cmd
+
+import (
+ "dborg/internal/client"
+ "dborg/internal/config"
+ "encoding/json"
+ "fmt"
+
+ "github.com/spf13/cobra"
+)
+
+var xCmd = &cobra.Command{
+ Use: "x [username]",
+ Short: "Search Twitter/X username history",
+ Long: `Search for Twitter/X username history and previous usernames`,
+ Args: cobra.ExactArgs(1),
+ RunE: runXSearch,
+}
+
+func init() {
+ rootCmd.AddCommand(xCmd)
+}
+
+func runXSearch(cmd *cobra.Command, args []string) error {
+ apiKey, _ := cmd.Flags().GetString("api-key")
+ cfg := config.New().WithAPIKey(apiKey)
+
+ c, err := client.New(cfg)
+ if err != nil {
+ return err
+ }
+
+ response, err := c.SearchTwitterHistory(args[0])
+ if err != nil {
+ return err
+ }
+
+ if response.Error != "" {
+ return fmt.Errorf("API error: %s", response.Error)
+ }
+
+ if len(response.PreviousUsernames) > 0 {
+ output, err := json.MarshalIndent(response.PreviousUsernames, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+ fmt.Println(string(output))
+ } else if response.Response != "" {
+ fmt.Println(response.Response)
+ } else if response.Data != nil {
+ output, err := json.MarshalIndent(response.Data, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format response: %w", err)
+ }
+ fmt.Println(string(output))
+ } else {
+ fmt.Println("No username history found")
+ }
+
+ return nil
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..52d43fa
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module dborg
+
+go 1.24.4
+
+require github.com/spf13/cobra v1.10.1
+
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/spf13/pflag v1.0.9 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e613680
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,10 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
+github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/client/admin.go b/internal/client/admin.go
new file mode 100644
index 0000000..a5a9519
--- /dev/null
+++ b/internal/client/admin.go
@@ -0,0 +1,124 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) ListAccounts() (*models.AdminResponse, error) {
+ data, err := c.Get("/admin/accounts", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) CreateAccount(req *models.AccountCreateRequest) (*models.AdminResponse, error) {
+ data, err := c.Post("/admin/accounts", req)
+ if err != nil {
+ return nil, err
+ }
+
+ var account models.Account
+ if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" {
+ return &models.AdminResponse{
+ Success: true,
+ Account: &account,
+ }, nil
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) DeleteAccount(apiKey string) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s", url.PathEscape(apiKey))
+ data, err := c.Delete(path)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) UpdateCredits(apiKey string, credits int) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey))
+ req := &models.AddCreditsRequest{
+ Credits: credits,
+ }
+
+ data, err := c.Post(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SetCredits(apiKey string, credits int) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/credits", url.PathEscape(apiKey))
+ req := &models.SetCreditsRequest{
+ Credits: credits,
+ }
+
+ data, err := c.Put(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var account models.Account
+ if err := json.Unmarshal(data, &account); err == nil && account.APIKey != "" {
+ return &models.AdminResponse{
+ Success: true,
+ Account: &account,
+ }, nil
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) ToggleAccount(apiKey string, enable bool) (*models.AdminResponse, error) {
+ path := fmt.Sprintf("/admin/accounts/%s/disable", url.PathEscape(apiKey))
+ req := &models.DisableAccountRequest{
+ Disabled: !enable,
+ }
+
+ data, err := c.Patch(path, req)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AdminResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse admin response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/client.go b/internal/client/client.go
new file mode 100644
index 0000000..098479f
--- /dev/null
+++ b/internal/client/client.go
@@ -0,0 +1,119 @@
+package client
+
+import (
+ "bytes"
+ "dborg/internal/config"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+type Client struct {
+ config *config.Config
+ httpClient *http.Client
+}
+
+func New(cfg *config.Config) (*Client, error) {
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+
+ return &Client{
+ config: cfg,
+ httpClient: &http.Client{
+ Timeout: cfg.Timeout,
+ },
+ }, nil
+}
+
+func (c *Client) doRequest(method, path string, params url.Values, body interface{}) ([]byte, error) {
+ fullURL := c.config.BaseURL + path
+ if params != nil && len(params) > 0 {
+ fullURL += "?" + params.Encode()
+ }
+
+ var reqBody io.Reader
+ if body != nil {
+ jsonData, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal request body: %w", err)
+ }
+ reqBody = bytes.NewBuffer(jsonData)
+ }
+
+ req, err := http.NewRequest(method, fullURL, reqBody)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ var resp *http.Response
+ var lastErr error
+
+ for attempt := 0; attempt <= c.config.MaxRetries; attempt++ {
+ if attempt > 0 {
+ time.Sleep(time.Duration(attempt) * time.Second)
+ }
+
+ resp, err = c.httpClient.Do(req)
+ if err != nil {
+ lastErr = err
+ continue
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated {
+ return io.ReadAll(resp.Body)
+ }
+
+ bodyBytes, _ := io.ReadAll(resp.Body)
+
+ switch resp.StatusCode {
+ case http.StatusForbidden:
+ lastErr = fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes))
+ case http.StatusUnauthorized:
+ lastErr = fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes))
+ case http.StatusTooManyRequests:
+ lastErr = fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes))
+ case http.StatusBadRequest:
+ lastErr = fmt.Errorf("bad request (400): %s", string(bodyBytes))
+ default:
+ lastErr = fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+
+ if resp.StatusCode != http.StatusTooManyRequests && resp.StatusCode < 500 {
+ break
+ }
+ }
+
+ return nil, lastErr
+}
+
+func (c *Client) Get(path string, params url.Values) ([]byte, error) {
+ return c.doRequest(http.MethodGet, path, params, nil)
+}
+
+func (c *Client) Post(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPost, path, nil, body)
+}
+
+func (c *Client) Delete(path string) ([]byte, error) {
+ return c.doRequest(http.MethodDelete, path, nil, nil)
+}
+
+func (c *Client) Patch(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPatch, path, nil, body)
+}
+
+func (c *Client) Put(path string, body interface{}) ([]byte, error) {
+ return c.doRequest(http.MethodPut, path, nil, body)
+}
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
new file mode 100644
index 0000000..9bf453d
--- /dev/null
+++ b/internal/client/client_test.go
@@ -0,0 +1,46 @@
+package client
+
+import (
+ "dborg/internal/config"
+ "testing"
+ "time"
+)
+
+func TestNewClient(t *testing.T) {
+ tests := []struct {
+ name string
+ config *config.Config
+ wantErr bool
+ }{
+ {
+ name: "valid config",
+ config: &config.Config{
+ APIKey: "test-key",
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "test-agent",
+ },
+ wantErr: false,
+ },
+ {
+ name: "missing API key",
+ config: &config.Config{
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "test-agent",
+ },
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := New(tt.config)
+ if (err != nil) != tt.wantErr {
+ t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/internal/client/npd.go b/internal/client/npd.go
new file mode 100644
index 0000000..c63327b
--- /dev/null
+++ b/internal/client/npd.go
@@ -0,0 +1,91 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchNPD(params *models.NPDParams) (*models.NPDResponse, error) {
+ queryParams := url.Values{}
+
+ if params.ID != "" {
+ queryParams.Add("id", params.ID)
+ }
+ if params.FirstName != "" {
+ queryParams.Add("firstname", params.FirstName)
+ }
+ if params.LastName != "" {
+ queryParams.Add("lastname", params.LastName)
+ }
+ if params.MiddleName != "" {
+ queryParams.Add("middlename", params.MiddleName)
+ }
+ if params.DOB != "" {
+ queryParams.Add("dob", params.DOB)
+ }
+ if params.SSN != "" {
+ queryParams.Add("ssn", params.SSN)
+ }
+ if params.Phone1 != "" {
+ queryParams.Add("phone1", params.Phone1)
+ }
+ if params.Address != "" {
+ queryParams.Add("address", params.Address)
+ }
+ if params.City != "" {
+ queryParams.Add("city", params.City)
+ }
+ if params.State != "" {
+ queryParams.Add("st", params.State)
+ }
+ if params.Zip != "" {
+ queryParams.Add("zip", params.Zip)
+ }
+ if params.CountyName != "" {
+ queryParams.Add("county_name", params.CountyName)
+ }
+ if params.NameSuffix != "" {
+ queryParams.Add("name_suff", params.NameSuffix)
+ }
+ if params.AKA1FullName != "" {
+ queryParams.Add("aka1fullname", params.AKA1FullName)
+ }
+ if params.AKA2FullName != "" {
+ queryParams.Add("aka2fullname", params.AKA2FullName)
+ }
+ if params.AKA3FullName != "" {
+ queryParams.Add("aka3fullname", params.AKA3FullName)
+ }
+ if params.Alt1DOB != "" {
+ queryParams.Add("alt1dob", params.Alt1DOB)
+ }
+ if params.Alt2DOB != "" {
+ queryParams.Add("alt2dob", params.Alt2DOB)
+ }
+ if params.Alt3DOB != "" {
+ queryParams.Add("alt3dob", params.Alt3DOB)
+ }
+ if params.StartDate != "" {
+ queryParams.Add("startdat", params.StartDate)
+ }
+ if params.MaxHits > 0 && params.MaxHits != 10 {
+ queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
+ }
+ if params.SortBy != "" {
+ queryParams.Add("sort_by", params.SortBy)
+ }
+
+ data, err := c.Get("/npd/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.NPDResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse NPD response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/osint.go b/internal/client/osint.go
new file mode 100644
index 0000000..70dbb72
--- /dev/null
+++ b/internal/client/osint.go
@@ -0,0 +1,32 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) LookupBSSID(params *models.BSSIDParams) (*models.BSSIDLookupResponse, error) {
+ path := fmt.Sprintf("/osint/bssid/%s", url.PathEscape(params.BSSID))
+
+ queryParams := url.Values{}
+ if params.All {
+ queryParams.Add("all", "true")
+ }
+ if params.Map {
+ queryParams.Add("map", "true")
+ }
+
+ data, err := c.Get(path, queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.BSSIDLookupResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse BSSID lookup response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/skiptrace.go b/internal/client/skiptrace.go
new file mode 100644
index 0000000..b1d5008
--- /dev/null
+++ b/internal/client/skiptrace.go
@@ -0,0 +1,179 @@
+package client
+
+import (
+ "bufio"
+ "bytes"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+func parseSSEResponse(data []byte) ([]byte, error) {
+ scanner := bufio.NewScanner(bytes.NewReader(data))
+
+ const maxScanTokenSize = 10 * 1024 * 1024
+ buf := make([]byte, maxScanTokenSize)
+ scanner.Buffer(buf, maxScanTokenSize)
+
+ var resultData []byte
+ var foundResult bool
+
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ if line == "event: result" {
+ foundResult = true
+ continue
+ }
+
+ if foundResult && strings.HasPrefix(line, "data: ") {
+ resultData = []byte(strings.TrimPrefix(line, "data: "))
+ break
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, fmt.Errorf("error reading SSE response: %w", err)
+ }
+
+ if resultData == nil {
+ return nil, fmt.Errorf("no result event found in SSE response")
+ }
+
+ trimmed := strings.TrimSpace(string(resultData))
+ if !strings.HasPrefix(trimmed, "{") && !strings.HasPrefix(trimmed, "[") {
+ return nil, fmt.Errorf("API returned: %s", trimmed)
+ }
+
+ return resultData, nil
+}
+
+func (c *Client) getSSE(path string, params url.Values) ([]byte, error) {
+ fullURL := c.config.BaseURL + path
+ if params != nil && len(params) > 0 {
+ fullURL += "?" + params.Encode()
+ }
+
+ req, err := http.NewRequest("GET", fullURL, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("request failed: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ bodyBytes, _ := io.ReadAll(resp.Body)
+ switch resp.StatusCode {
+ case http.StatusForbidden:
+ return nil, fmt.Errorf("access denied (403): %s - This endpoint requires premium access", string(bodyBytes))
+ case http.StatusUnauthorized:
+ return nil, fmt.Errorf("unauthorized (401): %s - Check your API key", string(bodyBytes))
+ case http.StatusTooManyRequests:
+ return nil, fmt.Errorf("rate limit exceeded (429): %s", string(bodyBytes))
+ case http.StatusBadRequest:
+ return nil, fmt.Errorf("bad request (400): %s", string(bodyBytes))
+ default:
+ return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
+ }
+ }
+
+ data, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read response: %w", err)
+ }
+
+ contentType := resp.Header.Get("Content-Type")
+
+ if strings.HasPrefix(string(data), "event:") || strings.Contains(contentType, "text/event-stream") {
+ return parseSSEResponse(data)
+ }
+
+ return data, nil
+}
+
+func (c *Client) SearchPeople(params *models.SkiptraceParams) (*models.SkiptraceResponse, error) {
+ queryParams := url.Values{}
+ queryParams.Set("first_name", params.FirstName)
+ queryParams.Set("last_name", params.LastName)
+
+ if params.City != "" {
+ queryParams.Set("city", params.City)
+ }
+ if params.State != "" {
+ queryParams.Set("state", params.State)
+ }
+ if params.Age != "" {
+ queryParams.Set("age", params.Age)
+ }
+
+ data, err := c.getSSE("/prem/skiptrace/people/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) GetPersonReport(sxKey string, selection int) (*models.SkiptraceReportResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/people/report/%s/%d", sxKey, selection)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceReportResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SearchPhone(phone string) (*models.SkiptracePhoneResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/phone/%s", phone)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptracePhoneResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
+
+func (c *Client) SearchEmail(email string) (*models.SkiptraceEmailResponse, error) {
+ path := fmt.Sprintf("/prem/skiptrace/email/%s", email)
+
+ data, err := c.getSSE(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.SkiptraceEmailResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/sl.go b/internal/client/sl.go
new file mode 100644
index 0000000..fb3b270
--- /dev/null
+++ b/internal/client/sl.go
@@ -0,0 +1,53 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchStealerLogs(params *models.SLParams) (*models.SLResponse, error) {
+ queryParams := url.Values{}
+ queryParams.Add("query", params.Query)
+
+ if params.MaxHits > 0 && params.MaxHits != 10 {
+ queryParams.Add("max_hits", fmt.Sprintf("%d", params.MaxHits))
+ }
+ if params.SortBy != "" {
+ queryParams.Add("sort_by", params.SortBy)
+ }
+ if params.IngestStartDate != "" {
+ queryParams.Add("ingest_start_date", params.IngestStartDate)
+ }
+ if params.IngestEndDate != "" {
+ queryParams.Add("ingest_end_date", params.IngestEndDate)
+ }
+ if params.PostedStartDate != "" {
+ queryParams.Add("posted_start_date", params.PostedStartDate)
+ }
+ if params.PostedEndDate != "" {
+ queryParams.Add("posted_end_date", params.PostedEndDate)
+ }
+ if params.Format != "" && params.Format != "json" {
+ queryParams.Add("format", params.Format)
+ }
+
+ data, err := c.Get("/sl/search", queryParams)
+ if err != nil {
+ return nil, err
+ }
+
+ if params.Format != "" && params.Format != "json" {
+ return &models.SLResponse{
+ Message: string(data),
+ }, nil
+ }
+
+ var response models.SLResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse stealer logs response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/client/usrsx.go b/internal/client/usrsx.go
new file mode 100644
index 0000000..456acbf
--- /dev/null
+++ b/internal/client/usrsx.go
@@ -0,0 +1,72 @@
+package client
+
+import (
+ "bufio"
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+)
+
+func (c *Client) CheckUsernameStream(params *models.USRSXParams, callback func(result json.RawMessage) error) error {
+ queryParams := url.Values{}
+
+ if len(params.Sites) > 0 {
+ queryParams.Add("sites", strings.Join(params.Sites, ","))
+ }
+ if params.Fuzzy {
+ queryParams.Add("fuzzy", "true")
+ }
+ if params.MaxTasks > 0 && params.MaxTasks != 50 {
+ queryParams.Add("max_tasks", fmt.Sprintf("%d", params.MaxTasks))
+ }
+
+ path := fmt.Sprintf("/osint/username/%s", url.PathEscape(params.Username))
+ fullURL := c.config.BaseURL + path
+ if len(queryParams) > 0 {
+ fullURL += "?" + queryParams.Encode()
+ }
+
+ req, err := http.NewRequest(http.MethodGet, fullURL, nil)
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("X-API-Key", c.config.APIKey)
+ req.Header.Set("User-Agent", c.config.UserAgent)
+ req.Header.Set("Accept", "application/x-ndjson, application/json")
+
+ resp, err := c.httpClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to execute request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
+ }
+
+ scanner := bufio.NewScanner(resp.Body)
+ for scanner.Scan() {
+ line := scanner.Bytes()
+ if len(line) == 0 {
+ continue
+ }
+
+ if err := callback(json.RawMessage(line)); err != nil {
+ return err
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ if !strings.Contains(err.Error(), "context deadline exceeded") && !strings.Contains(err.Error(), "timeout") {
+ return fmt.Errorf("stream reading error: %w", err)
+ }
+ }
+
+ return nil
+}
diff --git a/internal/client/x.go b/internal/client/x.go
new file mode 100644
index 0000000..8bdb21c
--- /dev/null
+++ b/internal/client/x.go
@@ -0,0 +1,23 @@
+package client
+
+import (
+ "dborg/internal/models"
+ "encoding/json"
+ "fmt"
+ "net/url"
+)
+
+func (c *Client) SearchTwitterHistory(username string) (*models.XResponse, error) {
+ path := fmt.Sprintf("/x/search/%s", url.PathEscape(username))
+ data, err := c.Get(path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.XResponse
+ if err := json.Unmarshal(data, &response); err != nil {
+ return nil, fmt.Errorf("failed to parse Twitter/X response: %w", err)
+ }
+
+ return &response, nil
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..44ca7e6
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,36 @@
+package config
+
+import (
+ "os"
+ "time"
+)
+
+type Config struct {
+ APIKey string
+ BaseURL string
+ Timeout time.Duration
+ MaxRetries int
+ UserAgent string
+}
+
+func New() *Config {
+ return &Config{
+ APIKey: os.Getenv("DBORG_API_KEY"),
+ BaseURL: "https://db.org.ai",
+ Timeout: 30 * time.Second,
+ MaxRetries: 3,
+ UserAgent: "dborg-cli/1.0",
+ }
+}
+
+func (c *Config) WithAPIKey(key string) *Config {
+ c.APIKey = key
+ return c
+}
+
+func (c *Config) Validate() error {
+ if c.APIKey == "" {
+ return ErrMissingAPIKey
+ }
+ return nil
+}
diff --git a/internal/config/errors.go b/internal/config/errors.go
new file mode 100644
index 0000000..4fd3636
--- /dev/null
+++ b/internal/config/errors.go
@@ -0,0 +1,7 @@
+package config
+
+import "errors"
+
+var (
+ ErrMissingAPIKey = errors.New("API key required: set DBORG_API_KEY environment variable or use --api-key flag")
+)
diff --git a/internal/models/admin.go b/internal/models/admin.go
new file mode 100644
index 0000000..5cf0f37
--- /dev/null
+++ b/internal/models/admin.go
@@ -0,0 +1,43 @@
+package models
+
+type Account struct {
+ APIKey string `json:"api_key"`
+ Name string `json:"name"`
+ Credits int `json:"credits"`
+ Unlimited bool `json:"unlimited"`
+ Disabled bool `json:"disabled"`
+ IsPremium bool `json:"is_premium"`
+ CreatedAt interface{} `json:"created_at,omitempty"`
+}
+
+type AccountCreateRequest struct {
+ Name string `json:"name"`
+ Credits int `json:"credits,omitempty"`
+ Unlimited bool `json:"unlimited,omitempty"`
+ IsPremium bool `json:"is_premium,omitempty"`
+}
+
+type AccountUpdateRequest struct {
+ Credits int `json:"credits,omitempty"`
+ Disabled bool `json:"disabled"`
+}
+
+type AddCreditsRequest struct {
+ Credits int `json:"credits"`
+}
+
+type SetCreditsRequest struct {
+ Credits int `json:"credits"`
+}
+
+type DisableAccountRequest struct {
+ Disabled bool `json:"disabled"`
+}
+
+type AdminResponse struct {
+ Success bool `json:"success,omitempty"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+ Account *Account `json:"account,omitempty"`
+ Accounts []Account `json:"accounts,omitempty"`
+}
diff --git a/internal/models/npd.go b/internal/models/npd.go
new file mode 100644
index 0000000..a1da05e
--- /dev/null
+++ b/internal/models/npd.go
@@ -0,0 +1,42 @@
+package models
+
+type NPDParams struct {
+ ID string `json:"id,omitempty"`
+ FirstName string `json:"firstname,omitempty"`
+ LastName string `json:"lastname,omitempty"`
+ MiddleName string `json:"middlename,omitempty"`
+ DOB string `json:"dob,omitempty"`
+ SSN string `json:"ssn,omitempty"`
+ Phone1 string `json:"phone1,omitempty"`
+ Address string `json:"address,omitempty"`
+ City string `json:"city,omitempty"`
+ State string `json:"st,omitempty"`
+ Zip string `json:"zip,omitempty"`
+ CountyName string `json:"county_name,omitempty"`
+ NameSuffix string `json:"name_suff,omitempty"`
+ AKA1FullName string `json:"aka1fullname,omitempty"`
+ AKA2FullName string `json:"aka2fullname,omitempty"`
+ AKA3FullName string `json:"aka3fullname,omitempty"`
+ Alt1DOB string `json:"alt1dob,omitempty"`
+ Alt2DOB string `json:"alt2dob,omitempty"`
+ Alt3DOB string `json:"alt3dob,omitempty"`
+ StartDate string `json:"startdat,omitempty"`
+ MaxHits int `json:"max_hits,omitempty"`
+ SortBy string `json:"sort_by,omitempty"`
+}
+
+type NPDResponse struct {
+ MaxHits int `json:"max_hits"`
+ Results struct {
+ ElapsedTimeMicros int `json:"elapsed_time_micros"`
+ Errors []string `json:"errors"`
+ Hits []map[string]any `json:"hits"`
+ NumHits int `json:"num_hits"`
+ } `json:"results"`
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
diff --git a/internal/models/osint.go b/internal/models/osint.go
new file mode 100644
index 0000000..7170c27
--- /dev/null
+++ b/internal/models/osint.go
@@ -0,0 +1,29 @@
+package models
+
+type BSSIDParams struct {
+ BSSID string
+ All bool
+ Map bool
+}
+
+type LocationInfo struct {
+ Latitude float64 `json:"latitude"`
+ Longitude float64 `json:"longitude"`
+ Accuracy int `json:"accuracy"`
+}
+
+type BSSIDResult struct {
+ BSSID string `json:"bssid"`
+ Location *LocationInfo `json:"location"`
+ MapURL string `json:"map_url,omitempty"`
+}
+
+type BSSIDLookupResponse struct {
+ BSSID string `json:"bssid"`
+ Results []BSSIDResult `json:"results"`
+ MapURL string `json:"map_url,omitempty"`
+}
+
+type ErrorResponse struct {
+ Error string `json:"error"`
+}
diff --git a/internal/models/skiptrace.go b/internal/models/skiptrace.go
new file mode 100644
index 0000000..c87fe72
--- /dev/null
+++ b/internal/models/skiptrace.go
@@ -0,0 +1,92 @@
+package models
+
+import "encoding/json"
+
+type SkiptraceParams struct {
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ City string `json:"city,omitempty"`
+ State string `json:"state,omitempty"`
+ Age string `json:"age,omitempty"`
+}
+
+type SkiptraceResponse struct {
+ Data map[string]interface{} `json:"-"`
+ SXKey string `json:"sx_key,omitempty"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptraceResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptraceResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
+
+type SkiptraceReportResponse struct {
+ Data map[string]interface{} `json:"-"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptraceReportResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptraceReportResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
+
+type SkiptracePhoneResponse struct {
+ Data map[string]interface{} `json:"-"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptracePhoneResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptracePhoneResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
+
+type SkiptraceEmailResponse struct {
+ Data map[string]interface{} `json:"-"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+func (s *SkiptraceEmailResponse) UnmarshalJSON(data []byte) error {
+ type Alias SkiptraceEmailResponse
+ aux := &struct {
+ *Alias
+ }{
+ Alias: (*Alias)(s),
+ }
+ if err := json.Unmarshal(data, &aux); err != nil {
+ return err
+ }
+ s.Data = make(map[string]interface{})
+ return json.Unmarshal(data, &s.Data)
+}
diff --git a/internal/models/sl.go b/internal/models/sl.go
new file mode 100644
index 0000000..d55279f
--- /dev/null
+++ b/internal/models/sl.go
@@ -0,0 +1,23 @@
+package models
+
+type SLParams struct {
+ Query string `json:"query"`
+ MaxHits int `json:"max_hits,omitempty"`
+ SortBy string `json:"sort_by,omitempty"`
+ IngestStartDate string `json:"ingest_start_date,omitempty"`
+ IngestEndDate string `json:"ingest_end_date,omitempty"`
+ PostedStartDate string `json:"posted_start_date,omitempty"`
+ PostedEndDate string `json:"posted_end_date,omitempty"`
+ Format string `json:"format,omitempty"`
+}
+
+type SLResponse struct {
+ MaxHits int `json:"max_hits"`
+ Results interface{} `json:"results"`
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
diff --git a/internal/models/usrsx.go b/internal/models/usrsx.go
new file mode 100644
index 0000000..f9264be
--- /dev/null
+++ b/internal/models/usrsx.go
@@ -0,0 +1,33 @@
+package models
+
+type USRSXParams struct {
+ Username string `json:"username"`
+ Sites []string `json:"sites,omitempty"`
+ Fuzzy bool `json:"fuzzy,omitempty"`
+ MaxTasks int `json:"max_tasks,omitempty"`
+}
+
+type USRSXResponse struct {
+ Username string `json:"username"`
+ Results []SiteResult `json:"results"`
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+type SiteResult struct {
+ SiteName string `json:"site_name"`
+ Username string `json:"username"`
+ URL string `json:"url"`
+ Status string `json:"status"`
+ ResponseCode int `json:"response_code"`
+ Category string `json:"category,omitempty"`
+ Timestamp string `json:"timestamp,omitempty"`
+ Type string `json:"type,omitempty"`
+ Elapsed float64 `json:"elapsed,omitempty"`
+ Error string `json:"error,omitempty"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+}
diff --git a/internal/models/x.go b/internal/models/x.go
new file mode 100644
index 0000000..f8c7a70
--- /dev/null
+++ b/internal/models/x.go
@@ -0,0 +1,22 @@
+package models
+
+type XResponse struct {
+ Username string `json:"username,omitempty"`
+ PreviousUsernames []UserHistory `json:"previous_usernames,omitempty"`
+
+ Query string `json:"query,omitempty"`
+ Response string `json:"response,omitempty"`
+ Data interface{} `json:"data,omitempty"`
+
+ Credits struct {
+ Remaining int `json:"remaining"`
+ Unlimited bool `json:"unlimited"`
+ } `json:"credits"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+}
+
+type UserHistory struct {
+ Username string `json:"username"`
+ TimeAgo string `json:"time_ago"`
+}
diff --git a/internal/utils/output.go b/internal/utils/output.go
new file mode 100644
index 0000000..3f2347c
--- /dev/null
+++ b/internal/utils/output.go
@@ -0,0 +1,39 @@
+package utils
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "text/tabwriter"
+)
+
+func PrintJSON(data any) error {
+ output, err := json.MarshalIndent(data, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to format JSON: %w", err)
+ }
+ fmt.Println(string(output))
+ return nil
+}
+
+func PrintTable(headers []string, rows [][]string) {
+ w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
+ defer w.Flush()
+
+ for _, h := range headers {
+ fmt.Fprintf(w, "%s\t", h)
+ }
+ fmt.Fprintln(w)
+
+ for _, row := range rows {
+ for _, col := range row {
+ fmt.Fprintf(w, "%s\t", col)
+ }
+ fmt.Fprintln(w)
+ }
+}
+
+func PrintError(err error) {
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ os.Exit(1)
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..deab8db
--- /dev/null
+++ b/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "dborg/cmd"
+
+func main() {
+ cmd.Execute()
+}