diff --git a/cmd/categories.go b/cmd/categories.go new file mode 100644 index 0000000..ff40e5e --- /dev/null +++ b/cmd/categories.go @@ -0,0 +1,53 @@ +package cmd + +import ( + "fmt" + + "github.com/example/splitwise-cli/internal/api" + "github.com/example/splitwise-cli/internal/output" + "github.com/spf13/cobra" +) + +var categoriesCmd = &cobra.Command{ + Use: "categories", + Short: "List available expense categories", + Run: func(cmd *cobra.Command, args []string) { + client, err := api.New() + if err != nil { + output.Die("%v", err) + } + + categories, err := client.GetCategories() + if err != nil { + output.Die("%v", err) + } + + if jsonOut { + output.JSON(categories) + return + } + + if quiet { + for _, cat := range categories { + fmt.Printf("%d\t%s\n", cat.ID, cat.Name) + for _, sub := range cat.Subcategories { + fmt.Printf("%d\t%s\n", sub.ID, sub.Name) + } + } + return + } + + var rows [][]string + for _, cat := range categories { + rows = append(rows, []string{fmt.Sprintf("%d", cat.ID), cat.Name, ""}) + for _, sub := range cat.Subcategories { + rows = append(rows, []string{fmt.Sprintf("%d", sub.ID), " " + sub.Name, cat.Name}) + } + } + output.Table([]string{"ID", "Name", "Parent Category"}, rows) + }, +} + +func init() { + rootCmd.AddCommand(categoriesCmd) +} diff --git a/cmd/expenses.go b/cmd/expenses.go index 5d76aee..c5b6cfb 100644 --- a/cmd/expenses.go +++ b/cmd/expenses.go @@ -113,6 +113,7 @@ var expensesCreateCmd = &cobra.Command{ split, _ := cmd.Flags().GetString("split") currency, _ := cmd.Flags().GetString("currency") paidBy, _ := cmd.Flags().GetString("paid-by") + category, _ := cmd.Flags().GetInt("category") // Resolve defaults. cfg, _ := config.Load() @@ -137,6 +138,7 @@ var expensesCreateCmd = &cobra.Command{ Cost: cost, CurrencyCode: currency, GroupID: group.ID, + CategoryID: category, } if split == "" || split == "even" { @@ -277,6 +279,7 @@ func init() { expensesCreateCmd.Flags().String("split", "even", `Split type: even, or exact:Name:Amount,Name:Amount (e.g. "exact:MemberA:60,MemberB:40")`) expensesCreateCmd.Flags().String("paid-by", "", "Who paid (name, defaults to you)") expensesCreateCmd.Flags().StringP("currency", "c", "", "Currency code (e.g. USD)") + expensesCreateCmd.Flags().Int("category", 0, "Category ID for the expense") expensesCmd.AddCommand(expensesListCmd) expensesCmd.AddCommand(expensesCreateCmd) diff --git a/internal/api/client.go b/internal/api/client.go index 130ac8e..00afe74 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -198,8 +198,9 @@ type ExpenseShare struct { } type Category struct { - ID int `json:"id"` - Name string `json:"name"` + ID int `json:"id"` + Name string `json:"name"` + Subcategories []Category `json:"subcategories,omitempty"` } // ---------- API Methods ---------- @@ -264,6 +265,21 @@ func (c *Client) GetFriends() ([]Friend, error) { return resp.Friends, nil } +// GetCategories returns all available expense categories. +func (c *Client) GetCategories() ([]Category, error) { + data, err := c.get("/get_categories", nil) + if err != nil { + return nil, err + } + var resp struct { + Categories []Category `json:"categories"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + return resp.Categories, nil +} + // GetExpensesParams holds query parameters for listing expenses. type GetExpensesParams struct { GroupID int64 @@ -316,6 +332,7 @@ type CreateExpenseParams struct { GroupID int64 SplitEqually bool Date string + CategoryID int // For by-shares split: user_id -> {paid_share, owed_share} Shares []ShareParam } @@ -337,6 +354,9 @@ func (c *Client) CreateExpense(p CreateExpenseParams) (*Expense, error) { if p.Date != "" { params.Set("date", p.Date) } + if p.CategoryID > 0 { + params.Set("category_id", fmt.Sprintf("%d", p.CategoryID)) + } if p.SplitEqually { params.Set("group_id", fmt.Sprintf("%d", p.GroupID)) diff --git a/skills/splitwise/SKILL.md b/skills/splitwise/SKILL.md index eaf56e5..45b3824 100644 --- a/skills/splitwise/SKILL.md +++ b/skills/splitwise/SKILL.md @@ -66,6 +66,9 @@ splitwise expenses create "Dinner" 120.00 --group "Trip" # Different currency splitwise expenses create "Dinner on Trip" 45.00 --group "Trip" --currency EUR + +# With a category +splitwise expenses create "Groceries" 87.50 --category 18 ``` ### Other commands @@ -74,6 +77,7 @@ splitwise me # Current user info splitwise groups # List all groups splitwise group "Household" # Group details + member balances splitwise friends # List friends +splitwise categories # List all categories with their id, name, and subcategories splitwise settle "MemberB" # Record a settlement splitwise expenses delete 12345 # Delete an expense by ID ``` @@ -114,6 +118,7 @@ Run multiple `splitwise expenses create` commands in sequence. No special syntax - Group/friend names use case-insensitive partial matching - A configured default group means `--group` is optional - Amounts are USD by default (configurable via `splitwise config set default_currency`) +- You can categorize an expense using the `--category ` flag. If the user provides enough context, make an effort to find and use an appropriate category by checking `splitwise categories`. - `--split even` is the default — expense split equally among all group members - `--split "exact:Name:Amount,Name:Amount"` — custom per-person split (amounts must sum to total) - The `--paid-by` flag defaults to the authenticated user