Skip to content

Commit ee435b9

Browse files
committed
more commands
1 parent e663a61 commit ee435b9

7 files changed

Lines changed: 219 additions & 14 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
## Project Overview
66

7-
Go CLI for the ModelsLab AI platform. 103 commands across 14 groups covering auth, profile, keys, models, generate (image/video/audio/3D/chat), billing, wallet, subscriptions, teams, usage, config, MCP, docs, and shell completions. ~10k lines across 26 Go files.
7+
Go CLI for the ModelsLab AI platform. 105 commands across 14 groups covering auth, profile, keys, models, generate (image/video/audio/3D/chat), billing, wallet, subscriptions, teams, usage, config, MCP, docs, and shell completions. ~10k lines across 26 Go files.
88

99
**Stack**: Go 1.26 · Cobra + Viper · Charmbracelet (bubbletea, lipgloss, huh, glamour) · go-keyring · gojq · mcp-go · GoReleaser v2
1010

@@ -37,9 +37,9 @@ MODELSLAB_TEST_TOKEN="<token>" MODELSLAB_TEST_API_KEY="<key>" go test ./tests/ -
3737
| `internal/cmd/keys.go` | 5 API key commands |
3838
| `internal/cmd/models.go` | 8 model discovery commands |
3939
| `internal/cmd/generate.go` | 20 generation commands + async polling + file download |
40-
| `internal/cmd/billing.go` | 10 billing commands + Stripe card tokenization |
40+
| `internal/cmd/billing.go` | 13 billing commands + Stripe card tokenization + payment links + setup intents |
4141
| `internal/cmd/wallet.go` | 10 wallet commands |
42-
| `internal/cmd/subscriptions.go` | 11 subscription commands |
42+
| `internal/cmd/subscriptions.go` | 10 subscription commands |
4343
| `internal/cmd/teams.go` | 7 team commands |
4444
| `internal/cmd/usage.go` | 3 usage commands |
4545
| `internal/cmd/config.go` | 6 config/profile commands |
@@ -151,4 +151,4 @@ Generation commands use `pollAndDownload()` for async workflows:
151151

152152
## Design Spec
153153

154-
The authoritative spec for all 103 commands, API endpoints, and UX flows is in `cli.md` (1075 lines). Always consult it when adding features or fixing behavior.
154+
The authoritative spec for all 102 commands, API endpoints, and UX flows is in `cli.md`. Always consult it when adding features or fixing behavior.

CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## What is this project?
44

5-
A Go CLI for the [ModelsLab](https://modelslab.com) platform. It covers account management, model discovery, AI generation (image/video/audio/3D/chat), billing, wallet, subscriptions, teams, usage analytics, and MCP server mode. ~10k lines of Go across 26 files, 103 subcommands in 14 groups.
5+
A Go CLI for the [ModelsLab](https://modelslab.com) platform. It covers account management, model discovery, AI generation (image/video/audio/3D/chat), billing, wallet, subscriptions, teams, usage analytics, and MCP server mode. ~10k lines of Go across 26 files, 105 subcommands in 14 groups.
66

77
## Quick Reference
88

@@ -40,9 +40,9 @@ internal/
4040
keys.go # 5 API key commands
4141
models.go # 8 model discovery commands
4242
generate.go # 20 generation commands + polling + download
43-
billing.go # 10 billing commands + Stripe tokenization
43+
billing.go # 13 billing commands + Stripe tokenization + payment links + setup intents
4444
wallet.go # 10 wallet commands
45-
subscriptions.go # 11 subscription commands
45+
subscriptions.go # 10 subscription commands
4646
teams.go # 7 team commands
4747
usage.go # 3 usage analytics commands
4848
config.go # 6 config/profile commands
@@ -110,7 +110,7 @@ CLI flags → env vars (`MODELSLAB_*`) → project config → user config (`~/.c
110110

111111
### MCP Server
112112

113-
`internal/mcp/server.go` registers ~30 tools covering both control plane and generation endpoints. Supports `stdio` (default) and `sse` transports. Used by Claude Desktop/Code:
113+
`internal/mcp/server.go` registers ~37 tools covering both control plane and generation endpoints. Supports `stdio` (default) and `sse` transports. Used by Claude Desktop/Code:
114114
```json
115115
{ "mcpServers": { "modelslab": { "command": "modelslab", "args": ["mcp", "serve"] } } }
116116
```
@@ -172,4 +172,4 @@ Check `internal/cmd/helpers.go` for the shared response parsing helpers. If the
172172

173173
## Design Document
174174

175-
The full design spec is in `cli.md` (1075 lines). It contains the complete API endpoint mapping, all 103 commands, UX flows, and architectural decisions. Consult it for the authoritative specification.
175+
The full design spec is in `cli.md`. It contains the complete API endpoint mapping, all 102 commands, UX flows, and architectural decisions. Consult it for the authoritative specification.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ modelslab models Discover models (search, detail, filters, tags, provider
8686
modelslab generate Generate AI content (image, video, audio, 3D, chat)
8787
modelslab billing Manage billing (overview, payment methods, invoices)
8888
modelslab wallet Manage wallet (balance, fund, transactions, coupons)
89-
modelslab subscriptions Manage subscriptions (plans, create, cancel, pause)
89+
modelslab subscriptions Manage subscriptions (plans, create, pause, resume)
9090
modelslab teams Manage teams (list, invite, update, remove)
9191
modelslab usage View usage analytics (summary, products, history)
9292
modelslab config Manage CLI configuration (set, get, profiles)

internal/cmd/billing.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,119 @@ var billingInvoiceDetailCmd = &cobra.Command{
312312
},
313313
}
314314

315+
var billingStripeConfigCmd = &cobra.Command{
316+
Use: "stripe-config",
317+
Short: "Get Stripe publishable key for client-side card tokenization",
318+
RunE: func(cmd *cobra.Command, args []string) error {
319+
client := getClient()
320+
var result map[string]interface{}
321+
err := client.DoControlPlane("GET", "/billing/stripe-config", nil, &result)
322+
if err != nil {
323+
return err
324+
}
325+
326+
outputResult(result, func() {
327+
data := extractData(result)
328+
pairs := [][2]string{}
329+
if pk, ok := data["publishable_key"].(string); ok {
330+
pairs = append(pairs, [2]string{"publishable_key", pk})
331+
}
332+
if instr, ok := data["instructions"].(string); ok {
333+
pairs = append(pairs, [2]string{"instructions", instr})
334+
}
335+
output.PrintKeyValue(pairs)
336+
})
337+
return nil
338+
},
339+
}
340+
341+
var billingPaymentLinkCmd = &cobra.Command{
342+
Use: "payment-link",
343+
Short: "Create a Stripe-hosted payment URL for human-assisted payments",
344+
Long: `Create a Stripe Checkout payment link that a human can open in a browser.
345+
Used for the human-assisted payment flow where the CLI cannot tokenize cards directly.
346+
347+
The success_url and cancel_url are controlled by ModelsLab and cannot be overridden.`,
348+
RunE: func(cmd *cobra.Command, args []string) error {
349+
purpose, _ := cmd.Flags().GetString("purpose")
350+
amount, _ := cmd.Flags().GetFloat64("amount")
351+
planID, _ := cmd.Flags().GetInt("plan-id")
352+
idempotencyKey, _ := cmd.Flags().GetString("idempotency-key")
353+
354+
body := map[string]interface{}{
355+
"purpose": purpose,
356+
}
357+
if purpose == "fund" {
358+
if amount <= 0 {
359+
return fmt.Errorf("--amount is required when purpose is 'fund'")
360+
}
361+
body["amount"] = amount
362+
} else if purpose == "subscribe" {
363+
if planID <= 0 {
364+
return fmt.Errorf("--plan-id is required when purpose is 'subscribe'")
365+
}
366+
body["plan_id"] = planID
367+
} else {
368+
return fmt.Errorf("--purpose must be 'fund' or 'subscribe'")
369+
}
370+
371+
client := getClient()
372+
var result map[string]interface{}
373+
err := client.DoControlPlaneIdempotent("POST", "/billing/payment-link", body, &result, idempotencyKey)
374+
if err != nil {
375+
return err
376+
}
377+
378+
outputResult(result, func() {
379+
data := extractData(result)
380+
if url, ok := data["payment_url"].(string); ok {
381+
fmt.Println("Payment URL:", url)
382+
fmt.Println("Open this URL in a browser to complete payment.")
383+
}
384+
if sessionID, ok := data["session_id"].(string); ok {
385+
fmt.Printf("Session ID: %s\n", sessionID)
386+
}
387+
if expiresAt, ok := data["expires_at"].(string); ok {
388+
fmt.Printf("Expires at: %s\n", expiresAt)
389+
}
390+
})
391+
return nil
392+
},
393+
}
394+
395+
var billingSetupIntentCmd = &cobra.Command{
396+
Use: "setup-intent",
397+
Short: "Create a Stripe SetupIntent for saving payment methods",
398+
Long: `Create a Stripe SetupIntent to save a payment method for future use.
399+
The returned client_secret can be used with Stripe.js or the Stripe API
400+
to confirm the setup and attach the payment method to the customer.`,
401+
RunE: func(cmd *cobra.Command, args []string) error {
402+
idempotencyKey, _ := cmd.Flags().GetString("idempotency-key")
403+
404+
client := getClient()
405+
var result map[string]interface{}
406+
err := client.DoControlPlaneIdempotent("POST", "/billing/setup-intent", nil, &result, idempotencyKey)
407+
if err != nil {
408+
return err
409+
}
410+
411+
outputResult(result, func() {
412+
data := extractData(result)
413+
pairs := [][2]string{}
414+
for _, key := range []string{"setup_intent_id", "client_secret", "status"} {
415+
if v, ok := data[key]; ok && v != nil {
416+
pairs = append(pairs, [2]string{key, fmt.Sprintf("%v", v)})
417+
}
418+
}
419+
if instr, ok := data["instructions"].(string); ok {
420+
pairs = append(pairs, [2]string{"instructions", instr})
421+
}
422+
output.PrintKeyValue(pairs)
423+
})
424+
return nil
425+
},
426+
}
427+
315428
var billingInvoicePDFCmd = &cobra.Command{
316429
Use: "invoice-pdf",
317430
Short: "Download invoice PDF",
@@ -366,6 +479,14 @@ func init() {
366479
billingInvoiceDetailCmd.Flags().String("id", "", "Invoice ID")
367480
billingInvoicePDFCmd.Flags().String("id", "", "Invoice ID")
368481

482+
billingPaymentLinkCmd.Flags().String("purpose", "", "Payment purpose: 'fund' or 'subscribe'")
483+
billingPaymentLinkCmd.Flags().Float64("amount", 0, "Amount in USD (required when purpose is 'fund')")
484+
billingPaymentLinkCmd.Flags().Int("plan-id", 0, "Plan ID (required when purpose is 'subscribe')")
485+
billingPaymentLinkCmd.Flags().String("idempotency-key", "", "Idempotency key")
486+
billingPaymentLinkCmd.MarkFlagRequired("purpose")
487+
488+
billingSetupIntentCmd.Flags().String("idempotency-key", "", "Idempotency key")
489+
369490
billingCmd.AddCommand(billingOverviewCmd)
370491
billingCmd.AddCommand(billingPaymentMethodsCmd)
371492
billingCmd.AddCommand(billingAddPMCmd)
@@ -376,4 +497,7 @@ func init() {
376497
billingCmd.AddCommand(billingInvoicesCmd)
377498
billingCmd.AddCommand(billingInvoiceDetailCmd)
378499
billingCmd.AddCommand(billingInvoicePDFCmd)
500+
billingCmd.AddCommand(billingStripeConfigCmd)
501+
billingCmd.AddCommand(billingPaymentLinkCmd)
502+
billingCmd.AddCommand(billingSetupIntentCmd)
379503
}

internal/cmd/subscriptions.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,6 @@ func makeSubsActionCmd(action, short, httpMethod string) *cobra.Command {
273273
}
274274
}
275275

276-
var subsCancelCmd = makeSubsActionCmd("cancel", "Cancel subscription", "POST")
277276
var subsPauseCmd = makeSubsActionCmd("pause", "Pause subscription", "POST")
278277
var subsResumeCmd = makeSubsActionCmd("resume", "Resume subscription", "POST")
279278
var subsResetCycleCmd = makeSubsActionCmd("reset-cycle", "Reset billing cycle", "POST")
@@ -326,7 +325,7 @@ func init() {
326325
subsUpdateCmd.MarkFlagRequired("id")
327326
subsUpdateCmd.MarkFlagRequired("new-plan-id")
328327

329-
for _, c := range []*cobra.Command{subsCancelCmd, subsPauseCmd, subsResumeCmd, subsResetCycleCmd} {
328+
for _, c := range []*cobra.Command{subsPauseCmd, subsResumeCmd, subsResetCycleCmd} {
330329
c.Flags().String("id", "", "Subscription ID")
331330
c.Flags().String("idempotency-key", "", "Idempotency key")
332331
c.MarkFlagRequired("id")
@@ -342,7 +341,6 @@ func init() {
342341
subscriptionsCmd.AddCommand(subsConfirmCheckoutCmd)
343342
subscriptionsCmd.AddCommand(subsStatusCmd)
344343
subscriptionsCmd.AddCommand(subsUpdateCmd)
345-
subscriptionsCmd.AddCommand(subsCancelCmd)
346344
subscriptionsCmd.AddCommand(subsPauseCmd)
347345
subscriptionsCmd.AddCommand(subsResumeCmd)
348346
subscriptionsCmd.AddCommand(subsResetCycleCmd)

internal/mcp/server.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,32 @@ func (s *Server) registerControlPlaneTools() {
148148
return result, err
149149
})
150150

151+
s.addTool("billing-stripe-config", "Get Stripe publishable key for client-side card tokenization", nil, func(args map[string]interface{}) (interface{}, error) {
152+
var result map[string]interface{}
153+
err := s.client.DoControlPlane("GET", "/billing/stripe-config", nil, &result)
154+
return result, err
155+
})
156+
157+
s.addTool("billing-payment-link", "Create a Stripe-hosted payment URL for human-assisted payments", map[string]interface{}{
158+
"type": "object",
159+
"properties": map[string]interface{}{
160+
"purpose": map[string]string{"type": "string", "description": "Payment purpose: 'fund' or 'subscribe'"},
161+
"amount": map[string]interface{}{"type": "number", "description": "Amount in USD (required when purpose is 'fund')"},
162+
"plan_id": map[string]interface{}{"type": "integer", "description": "Plan ID (required when purpose is 'subscribe')"},
163+
},
164+
"required": []string{"purpose"},
165+
}, func(args map[string]interface{}) (interface{}, error) {
166+
var result map[string]interface{}
167+
err := s.client.DoControlPlaneIdempotent("POST", "/billing/payment-link", args, &result, "")
168+
return result, err
169+
})
170+
171+
s.addTool("billing-setup-intent", "Create a Stripe SetupIntent for saving payment methods", nil, func(args map[string]interface{}) (interface{}, error) {
172+
var result map[string]interface{}
173+
err := s.client.DoControlPlaneIdempotent("POST", "/billing/setup-intent", nil, &result, "")
174+
return result, err
175+
})
176+
151177
s.addTool("wallet-balance", "Check wallet balance", nil, func(args map[string]interface{}) (interface{}, error) {
152178
var result map[string]interface{}
153179
err := s.client.DoControlPlane("GET", "/wallet/balance", nil, &result)
@@ -167,6 +193,31 @@ func (s *Server) registerControlPlaneTools() {
167193
return result, err
168194
})
169195

196+
s.addTool("wallet-confirm-checkout", "Confirm a Stripe Checkout wallet funding session", map[string]interface{}{
197+
"type": "object",
198+
"properties": map[string]interface{}{
199+
"session_id": map[string]string{"type": "string", "description": "Stripe checkout session ID (cs_...)"},
200+
},
201+
"required": []string{"session_id"},
202+
}, func(args map[string]interface{}) (interface{}, error) {
203+
var result map[string]interface{}
204+
err := s.client.DoControlPlaneIdempotent("POST", "/wallet/confirm-checkout", args, &result, "")
205+
return result, err
206+
})
207+
208+
s.addTool("wallet-payment-status", "Check payment intent status", map[string]interface{}{
209+
"type": "object",
210+
"properties": map[string]interface{}{
211+
"payment_intent_id": map[string]string{"type": "string", "description": "Payment intent ID"},
212+
},
213+
"required": []string{"payment_intent_id"},
214+
}, func(args map[string]interface{}) (interface{}, error) {
215+
id := fmt.Sprintf("%v", args["payment_intent_id"])
216+
var result map[string]interface{}
217+
err := s.client.DoControlPlane("GET", "/payments/"+id+"/status", nil, &result)
218+
return result, err
219+
})
220+
170221
// Usage tools
171222
s.addTool("usage-summary", "Get usage overview", nil, func(args map[string]interface{}) (interface{}, error) {
172223
var result map[string]interface{}
@@ -193,6 +244,31 @@ func (s *Server) registerControlPlaneTools() {
193244
err := s.client.DoControlPlane("GET", "/subscriptions", nil, &result)
194245
return result, err
195246
})
247+
248+
s.addTool("subscriptions-confirm-checkout", "Confirm a Stripe Checkout subscription session", map[string]interface{}{
249+
"type": "object",
250+
"properties": map[string]interface{}{
251+
"session_id": map[string]string{"type": "string", "description": "Stripe checkout session ID (cs_...)"},
252+
},
253+
"required": []string{"session_id"},
254+
}, func(args map[string]interface{}) (interface{}, error) {
255+
var result map[string]interface{}
256+
err := s.client.DoControlPlaneIdempotent("POST", "/subscriptions/confirm-checkout", args, &result, "")
257+
return result, err
258+
})
259+
260+
s.addTool("subscriptions-status", "Check subscription status", map[string]interface{}{
261+
"type": "object",
262+
"properties": map[string]interface{}{
263+
"id": map[string]string{"type": "string", "description": "Subscription ID"},
264+
},
265+
"required": []string{"id"},
266+
}, func(args map[string]interface{}) (interface{}, error) {
267+
id := fmt.Sprintf("%v", args["id"])
268+
var result map[string]interface{}
269+
err := s.client.DoControlPlane("GET", "/subscriptions/"+id+"/status", nil, &result)
270+
return result, err
271+
})
196272
}
197273

198274
func (s *Server) registerGenerationTools() {
@@ -400,12 +476,19 @@ func (s *Server) ListTools() []ToolInfo {
400476
{"models-search", "Search AI models"},
401477
{"models-detail", "Get model details"},
402478
{"billing-overview", "View billing overview"},
479+
{"billing-stripe-config", "Get Stripe publishable key"},
480+
{"billing-payment-link", "Create payment URL for human-assisted payments"},
481+
{"billing-setup-intent", "Create SetupIntent for saving payment methods"},
403482
{"wallet-balance", "Check wallet balance"},
404483
{"wallet-fund", "Add funds to wallet"},
484+
{"wallet-confirm-checkout", "Confirm wallet funding checkout session"},
485+
{"wallet-payment-status", "Check payment intent status"},
405486
{"usage-summary", "Get usage overview"},
406487
{"teams-list", "List team members"},
407488
{"subscriptions-plans", "List subscription plans"},
408489
{"subscriptions-list", "List user subscriptions"},
490+
{"subscriptions-confirm-checkout", "Confirm subscription checkout session"},
491+
{"subscriptions-status", "Check subscription status"},
409492
// Generation
410493
{"text-to-image", "Generate images from text"},
411494
{"image-to-image", "Transform images"},

tests/integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func TestSubscriptions_Help(t *testing.T) {
182182
assert.Equal(t, 0, code)
183183
assert.Contains(t, stdout, "plans")
184184
assert.Contains(t, stdout, "create")
185-
assert.Contains(t, stdout, "cancel")
185+
assert.Contains(t, stdout, "pause")
186186
}
187187

188188
func TestTeams_Help(t *testing.T) {

0 commit comments

Comments
 (0)