Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- **`internal/api/`** - API client layer. Includes an OpenAPI-generated REST client (`client.go`, `types.go`), shared HTTP client singleton, and request/response types. **Do not edit `client.go` or `types.go` by hand** — they are generated from `openapi.yaml` (see [Code Generation](#code-generation)). The `mock/` subdirectory contains a generated mock of `ClientWithResponsesInterface` for use in tests.
- **`internal/config/`** - Configuration management. Handles config file loading (via Viper), credential storage (keyring with file fallback), and version checking.
- **`internal/common/`** - Shared business logic used across commands and MCP tools. Includes API client initialization, database connection/schema/query utilities, error handling with exit codes, and version update checks.
- **`internal/mcp/`** - Model Context Protocol (MCP) server. Exposes Ghost database operations as MCP tools for AI/LLM integration, plus a documentation search proxy. Each MCP tool lives in its own file, named to match the tool (e.g. `ghost_status` → `status.go`). Helper files like `util.go`, `errors.go`, and `proxy.go` contain shared utilities.
- **`internal/mcp/`** - Model Context Protocol (MCP) server. Exposes Ghost database operations as MCP tools for AI/LLM integration, plus a documentation search proxy. Each MCP tool lives in its own file, named to match the tool (e.g. `ghost_usage` → `usage.go`). Helper files like `util.go`, `errors.go`, and `proxy.go` contain shared utilities.
- **`internal/analytics/`** - Analytics event tracking with sensitive data redaction for flags, positional arguments, and MCP inputs.
- **`internal/util/`** - General utilities: type conversion, duration formatting, path helpers, context-aware stdin reading, JSON/YAML serialization, and terminal detection.
- **`docs/`** - Documentation. `docs/cli/` contains generated Markdown CLI reference docs (produced by `cmd/generate-docs`).
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ ghost list # List all databases
| `schema` | Display database schema information |
| `share` | Share a database |
| `sql` | Execute SQL query on a database |
| `status` | Show space usage |
| `usage` | Show space usage |
| `upgrade` | Upgrade the ghost CLI to the latest version |
| `version` | Show version information |

Expand Down Expand Up @@ -118,7 +118,7 @@ The `ghost mcp` command installs a [Model Context Protocol](https://modelcontext
| `ghost_share_list` | List database shares |
| `ghost_share_revoke` | Revoke a database share |
| `ghost_sql` | Execute a SQL query against a database |
| `ghost_status` | Show space usage |
| `ghost_usage` | Show space usage |
| `search_docs` | Search PostgreSQL, PostGIS, and TimescaleDB documentation |
| `view_skill` | Retrieve skills for PostgreSQL and TimescaleDB best practices |

Expand Down
2 changes: 1 addition & 1 deletion docs/cli/ghost.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,6 @@ Ghost is a command-line interface for managing PostgreSQL databases.
* [ghost schema](ghost_schema.md) - Display database schema information
* [ghost share](ghost_share.md) - Share a database
* [ghost sql](ghost_sql.md) - Execute SQL query on a database
* [ghost status](ghost_status.md) - Show space usage
* [ghost upgrade](ghost_upgrade.md) - Upgrade the ghost CLI to the latest version
* [ghost usage](ghost_usage.md) - Show space usage
* [ghost version](ghost_version.md) - Show version information
18 changes: 9 additions & 9 deletions docs/cli/ghost_status.md → docs/cli/ghost_usage.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
---
title: "ghost status"
slug: "ghost_status"
description: "CLI reference for ghost status"
title: "ghost usage"
slug: "ghost_usage"
description: "CLI reference for ghost usage"
---

## ghost status
## ghost usage

Show space usage

```
ghost status [flags]
ghost usage [flags]
```

### Examples

```
# Show space usage
ghost status
ghost usage

# Output as JSON
ghost status --json
ghost usage --json

# Output as YAML
ghost status --yaml
ghost usage --yaml
```

### Options

```
-h, --help help for status
-h, --help help for usage
--json Output in JSON format
--yaml Output in YAML format
```
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/mcp_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestMCPListCmd(t *testing.T) {
"tool ghost_share_list \n" +
"tool ghost_share_revoke \n" +
"tool ghost_sql \n" +
"tool ghost_status \n" +
"tool ghost_usage \n" +
"tool search_docs \n" +
"tool view_skill \n",
},
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func buildRootCmd() (*cobra.Command, *common.App, error) {
cmd.AddCommand(buildCreateCmd(app))
cmd.AddCommand(buildForkCmd(app))
cmd.AddCommand(buildListCmd(app))
cmd.AddCommand(buildStatusCmd(app))
cmd.AddCommand(buildUsageCmd(app))
cmd.AddCommand(buildDeleteCmd(app))
cmd.AddCommand(buildPauseCmd(app))
cmd.AddCommand(buildResumeCmd(app))
Expand Down
16 changes: 8 additions & 8 deletions internal/cmd/status.go → internal/cmd/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,22 @@ import (
"github.com/timescale/ghost/internal/util"
)

func buildStatusCmd(app *common.App) *cobra.Command {
func buildUsageCmd(app *common.App) *cobra.Command {
var jsonOutput bool
var yamlOutput bool

cmd := &cobra.Command{
Use: "status",
Aliases: []string{"usage"},
Use: "usage",
Aliases: []string{"status"},
Short: "Show space usage",
Example: ` # Show space usage
ghost status
ghost usage

# Output as JSON
ghost status --json
ghost usage --json

# Output as YAML
ghost status --yaml`,
ghost usage --yaml`,
Args: cobra.NoArgs,
ValidArgsFunction: cobra.NoFileCompletions,
SilenceUsage: true,
Expand All @@ -46,7 +46,7 @@ func buildStatusCmd(app *common.App) *cobra.Command {
case yamlOutput:
return util.SerializeToYAML(cmd.OutOrStdout(), status)
default:
outputStatus(cmd, status)
outputUsage(cmd, status)
return nil
}
},
Expand All @@ -59,7 +59,7 @@ func buildStatusCmd(app *common.App) *cobra.Command {
return cmd
}

func outputStatus(cmd *cobra.Command, status common.Status) {
func outputUsage(cmd *cobra.Command, status common.Status) {
computeHours := float64(status.ComputeMinutes) / 60
computeLimitHours := float64(status.ComputeLimitMinutes) / 60
computePercent := float64(status.ComputeMinutes) / float64(status.ComputeLimitMinutes) * 100
Expand Down
26 changes: 13 additions & 13 deletions internal/cmd/status_test.go → internal/cmd/usage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/timescale/ghost/internal/api/mock"
)

func TestStatusCmd(t *testing.T) {
func TestUsageCmd(t *testing.T) {
successSetup := func(m *mock.MockClientWithResponsesInterface) {
m.EXPECT().SpaceStatusWithResponse(validCtx, "test-project").
Return(&api.SpaceStatusResponse{
Expand Down Expand Up @@ -39,13 +39,13 @@ func TestStatusCmd(t *testing.T) {
tests := []cmdTest{
{
name: "not logged in",
args: []string{"status"},
args: []string{"usage"},
opts: []runOption{withClientError(errors.New("authentication required: no credentials found"))},
wantErr: "authentication required: no credentials found",
},
{
name: "network error on space status",
args: []string{"status"},
args: []string{"usage"},
setup: func(m *mock.MockClientWithResponsesInterface) {
m.EXPECT().SpaceStatusWithResponse(validCtx, "test-project").
Return(nil, errors.New("connection refused"))
Expand All @@ -60,7 +60,7 @@ func TestStatusCmd(t *testing.T) {
},
{
name: "API error on space status",
args: []string{"status"},
args: []string{"usage"},
setup: func(m *mock.MockClientWithResponsesInterface) {
m.EXPECT().SpaceStatusWithResponse(validCtx, "test-project").
Return(&api.SpaceStatusResponse{
Expand All @@ -78,7 +78,7 @@ func TestStatusCmd(t *testing.T) {
},
{
name: "nil space status response body",
args: []string{"status"},
args: []string{"usage"},
setup: func(m *mock.MockClientWithResponsesInterface) {
m.EXPECT().SpaceStatusWithResponse(validCtx, "test-project").
Return(&api.SpaceStatusResponse{
Expand All @@ -96,7 +96,7 @@ func TestStatusCmd(t *testing.T) {
},
{
name: "nil list databases response body",
args: []string{"status"},
args: []string{"usage"},
setup: func(m *mock.MockClientWithResponsesInterface) {
m.EXPECT().SpaceStatusWithResponse(validCtx, "test-project").
Return(&api.SpaceStatusResponse{
Expand All @@ -118,7 +118,7 @@ func TestStatusCmd(t *testing.T) {
},
{
name: "text output",
args: []string{"status"},
args: []string{"usage"},
setup: successSetup,
wantStdout: `Space: test-project
Compute: 2/10 hours (20%)
Expand All @@ -128,7 +128,7 @@ Databases: 2 (1 running, 1 paused)
},
{
name: "json output",
args: []string{"status", "--json"},
args: []string{"usage", "--json"},
setup: successSetup,
wantStdout: `{
"compute_minutes": 120,
Expand All @@ -145,7 +145,7 @@ Databases: 2 (1 running, 1 paused)
},
{
name: "yaml output",
args: []string{"status", "--yaml"},
args: []string{"usage", "--yaml"},
setup: successSetup,
wantStdout: `compute_limit_minutes: 600
compute_minutes: 120
Expand All @@ -158,8 +158,8 @@ storage_mib: 512
`,
},
{
name: "usage alias",
args: []string{"usage"},
name: "status alias",
args: []string{"status"},
setup: successSetup,
wantStdout: `Space: test-project
Compute: 2/10 hours (20%)
Expand All @@ -169,7 +169,7 @@ Databases: 2 (1 running, 1 paused)
},
{
name: "text output with cost",
args: []string{"status"},
args: []string{"usage"},
setup: func(m *mock.MockClientWithResponsesInterface) {
m.EXPECT().SpaceStatusWithResponse(validCtx, "test-project").
Return(&api.SpaceStatusResponse{
Expand Down Expand Up @@ -199,7 +199,7 @@ Cost: $12.34 so far this cycle ($27.50 estimated total)
},
{
name: "text output with zero cost is omitted",
args: []string{"status"},
args: []string{"usage"},
setup: func(m *mock.MockClientWithResponsesInterface) {
m.EXPECT().SpaceStatusWithResponse(validCtx, "test-project").
Return(&api.SpaceStatusResponse{
Expand Down
2 changes: 1 addition & 1 deletion internal/mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (s *Server) registerTools(ctx context.Context) {
mcp.AddTool(s.mcpServer, newLoginTool(), s.handleLogin)

// Register Ghost database tools
mcp.AddTool(s.mcpServer, newStatusTool(), s.handleStatus)
mcp.AddTool(s.mcpServer, newUsageTool(), s.handleUsage)
mcp.AddTool(s.mcpServer, newListTool(), s.handleList)
mcp.AddTool(s.mcpServer, newCreateTool(), s.handleCreate)
mcp.AddTool(s.mcpServer, newDeleteTool(), s.handleDelete)
Expand Down
32 changes: 16 additions & 16 deletions internal/mcp/status.go → internal/mcp/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ import (
"github.com/timescale/ghost/internal/util"
)

// StatusInput represents input for ghost_status (empty - no parameters)
type StatusInput struct{}
// UsageInput represents input for ghost_usage (empty - no parameters)
type UsageInput struct{}

func (StatusInput) Schema() *jsonschema.Schema {
return util.Must(jsonschema.For[StatusInput](nil))
func (UsageInput) Schema() *jsonschema.Schema {
return util.Must(jsonschema.For[UsageInput](nil))
}

// StatusOutput represents output for ghost_status
type StatusOutput struct {
// UsageOutput represents output for ghost_usage
type UsageOutput struct {
ComputeMinutes int64 `json:"compute_minutes"`
ComputeLimitMinutes int64 `json:"compute_limit_minutes"`
Storage string `json:"storage"`
Expand All @@ -31,8 +31,8 @@ type StatusOutput struct {
BillingPeriodEnd *time.Time `json:"billing_period_end,omitempty"`
}

func (StatusOutput) Schema() *jsonschema.Schema {
schema := util.Must(jsonschema.For[StatusOutput](nil))
func (UsageOutput) Schema() *jsonschema.Schema {
schema := util.Must(jsonschema.For[UsageOutput](nil))
schema.Properties["compute_minutes"].Description = "Current compute usage in minutes"
schema.Properties["compute_limit_minutes"].Description = "Compute limit in minutes"
schema.Properties["storage"].Description = "Current storage usage"
Expand All @@ -47,13 +47,13 @@ func (StatusOutput) Schema() *jsonschema.Schema {
return schema
}

func newStatusTool() *mcp.Tool {
func newUsageTool() *mcp.Tool {
return &mcp.Tool{
Name: "ghost_status",
Name: "ghost_usage",
Title: "Show Space Usage",
Description: "Display database space usage including compute minutes, storage, and database counts by status.",
InputSchema: StatusInput{}.Schema(),
OutputSchema: StatusOutput{}.Schema(),
InputSchema: UsageInput{}.Schema(),
OutputSchema: UsageOutput{}.Schema(),
Annotations: &mcp.ToolAnnotations{
ReadOnlyHint: true,
OpenWorldHint: new(true),
Expand All @@ -62,18 +62,18 @@ func newStatusTool() *mcp.Tool {
}
}

func (s *Server) handleStatus(ctx context.Context, req *mcp.CallToolRequest, input StatusInput) (*mcp.CallToolResult, StatusOutput, error) {
func (s *Server) handleUsage(ctx context.Context, req *mcp.CallToolRequest, input UsageInput) (*mcp.CallToolResult, UsageOutput, error) {
client, projectID, err := s.app.GetClient()
if err != nil {
return nil, StatusOutput{}, err
return nil, UsageOutput{}, err
}

status, err := common.FetchStatus(ctx, client, projectID)
if err != nil {
return nil, StatusOutput{}, err
return nil, UsageOutput{}, err
}

return nil, StatusOutput{
return nil, UsageOutput{
ComputeMinutes: status.ComputeMinutes,
ComputeLimitMinutes: status.ComputeLimitMinutes,
Storage: common.FormatStorageSize(new(int(status.StorageMib))),
Expand Down