Skip to content
Open
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
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/yamux v0.1.2
github.com/itchyny/gojq v0.12.19
github.com/langchain-ai/langsmith-go v0.2.2
github.com/olekukonko/tablewriter v0.0.5
github.com/spf13/cobra v1.8.1
Expand All @@ -15,12 +16,15 @@ require (
require (
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/itchyny/timefmt-go v0.1.8 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
Expand Down
11 changes: 10 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand All @@ -25,12 +29,17 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/itchyny/gojq v0.12.19 h1:ttXA0XCLEMoaLOz5lSeFOZ6u6Q3QxmG46vfgI4O0DEs=
github.com/itchyny/gojq v0.12.19/go.mod h1:5galtVPDywX8SPSOrqjGxkBeDhSxEW1gSxoy7tn1iZY=
github.com/itchyny/timefmt-go v0.1.8 h1:1YEo1JvfXeAHKdjelbYr/uCuhkybaHCeTkH8Bo791OI=
github.com/itchyny/timefmt-go v0.1.8/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/langchain-ai/langsmith-go v0.2.2 h1:WVNUR9dhnuieIaXrKxmZWva6TX1DV/RHCVgc67wbAbs=
github.com/langchain-ai/langsmith-go v0.2.2/go.mod h1:xdfOA0EBT7KF9ylz+gGq8EM6srRkv2PtpazQ+4oraWk=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down
6 changes: 3 additions & 3 deletions internal/cmd/api/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestInfoCmd_JSON(t *testing.T) {
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"api", "info", "--api-url", ts.URL, "GET", "/api/v1/sessions"})
root.SetArgs([]string{"api", "info", "--api-url", ts.URL, "--format", "json", "GET", "/api/v1/sessions"})

if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down Expand Up @@ -116,7 +116,7 @@ func TestInfoCmd_Shorthand(t *testing.T) {
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"api", "info", "--api-url", ts.URL, "GET", "sessions"})
root.SetArgs([]string{"api", "info", "--api-url", ts.URL, "--format", "json", "GET", "sessions"})

if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand All @@ -139,7 +139,7 @@ func TestInfoCmd_WithRequestBody(t *testing.T) {
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"api", "info", "--api-url", ts.URL, "POST", "runs/query"})
root.SetArgs([]string{"api", "info", "--api-url", ts.URL, "--format", "json", "POST", "runs/query"})

if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down
6 changes: 3 additions & 3 deletions internal/cmd/api/ls_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestLsCmd_JSON(t *testing.T) {
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"api", "ls", "--api-url", ts.URL, "--refresh"})
root.SetArgs([]string{"api", "ls", "--api-url", ts.URL, "--format", "json", "--refresh"})

if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand All @@ -67,7 +67,7 @@ func TestLsCmd_FilterByTag(t *testing.T) {
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"api", "ls", "--api-url", ts.URL, "--tag", "datasets", "--refresh"})
root.SetArgs([]string{"api", "ls", "--api-url", ts.URL, "--tag", "datasets", "--format", "json", "--refresh"})

if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down Expand Up @@ -95,7 +95,7 @@ func TestLsCmd_Search(t *testing.T) {
var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"api", "ls", "--api-url", ts.URL, "--search", "query", "--refresh"})
root.SetArgs([]string{"api", "ls", "--api-url", ts.URL, "--search", "query", "--format", "json", "--refresh"})

if err := root.Execute(); err != nil {
t.Fatalf("unexpected error: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/api/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ func newTestRoot() *cobra.Command {
root := &cobra.Command{Use: "langsmith"}
root.PersistentFlags().String("api-key", "", "")
root.PersistentFlags().String("api-url", "", "")
root.PersistentFlags().String("format", "json", "")
root.PersistentFlags().String("format", "pretty", "")
root.AddCommand(NewCmd())
return root
}
6 changes: 3 additions & 3 deletions internal/cmd/insights.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ with an executive summary of key findings and highlighted traces.
Examples:
langsmith insights list --project my-app
langsmith insights get INSIGHT_ID --project my-app
langsmith insights get INSIGHT_ID --project my-app --format pretty`,
langsmith insights get INSIGHT_ID --project my-app --json`,
}

cmd.AddCommand(newInsightsListCmd())
Expand All @@ -53,7 +53,7 @@ and category distribution. Use 'insights get' with the report ID
for full details including the executive summary and category breakdown.`,
Example: ` langsmith insights list --project my-app
langsmith insights list --project my-app --limit 5
langsmith insights list --project my-app --format pretty`,
langsmith insights list --project my-app --json`,
Run: func(cmd *cobra.Command, args []string) {
c := MustGetClient()
ctx := context.Background()
Expand Down Expand Up @@ -127,7 +127,7 @@ Returns the executive summary (key findings and highlighted traces),
plus a breakdown of all categories and subcategories with their
statistics (error rates, latency, costs, token usage, feedback scores).`,
Example: ` langsmith insights get e4040294-44af-4866-b1dd-3c566a8d42f0 --project my-app
langsmith insights get e4040294-44af-4866-b1dd-3c566a8d42f0 --project my-app --format pretty`,
langsmith insights get e4040294-44af-4866-b1dd-3c566a8d42f0 --project my-app --json`,
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
insightID := args[0]
Expand Down
8 changes: 4 additions & 4 deletions internal/cmd/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,13 @@ func newProjectIssuesListCmd() *cobra.Command {

Fetches issues from the Issues Board for the specified project. Results
can be filtered by status (open/closed) and priority (high/medium/low).
Output is JSON by default; pass --format pretty for a human-readable table.
Output is a human-readable table by default; pass --json for machine-readable JSON.

Examples:
langsmith project issues list --project my-app
langsmith project issues list --project my-app --status open
langsmith project issues list --project my-app --priority high --limit 10
langsmith project issues list --project my-app --format pretty`,
langsmith project issues list --project my-app --json`,
Run: func(cmd *cobra.Command, args []string) {
c := MustGetClient()
ctx := context.Background()
Expand Down Expand Up @@ -177,12 +177,12 @@ Issue events record user and agent actions on issues: status changes, severity
edits, evaluator deployments, and issue creation. The ABM agent reads these on
cron runs to update the User Preferences section of the Agent Overview.

Output is JSON by default; pass --format pretty for a human-readable table.
Output is a human-readable table by default; pass --json for machine-readable JSON.

Examples:
langsmith project issues events --project my-app
langsmith project issues events --project my-app --look-back-minutes 1440
langsmith project issues events --project my-app --limit 50 --format pretty`,
langsmith project issues events --project my-app --limit 50 --json`,
Run: func(cmd *cobra.Command, args []string) {
c := MustGetClient()
ctx := context.Background()
Expand Down
26 changes: 22 additions & 4 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/langchain-ai/langsmith-cli/internal/client"
"github.com/langchain-ai/langsmith-cli/internal/cmd/api"
"github.com/langchain-ai/langsmith-cli/internal/output"
"github.com/spf13/cobra"
)

Expand All @@ -14,6 +15,8 @@ var (
flagAPIKey string
flagAPIURL string
flagOutputFormat string
flagJSON bool
flagJQ string
)

// NewRootCmd creates the top-level `langsmith` command.
Expand All @@ -25,7 +28,6 @@ func NewRootCmd(rawVersion, displayVersion string) *cobra.Command {

Designed for AI coding agents and developers who need fast, scriptable
access to traces, runs, datasets, evaluators, experiments, and threads.
All commands output JSON by default for easy parsing.

Authentication:
Set LANGSMITH_API_KEY as an environment variable, or pass --api-key.
Expand All @@ -41,16 +43,23 @@ Quick start:
langsmith experiment list --dataset my-eval-dataset

Output:
--format json Machine-readable JSON (default). Best for agents and scripts.
--format pretty Human-readable tables, trees, and syntax-highlighted JSON.`,
--format pretty Human-readable tables, trees, and syntax-highlighted JSON (default).
--format json Machine-readable JSON. Best for agents and scripts.
--json Shorthand for --format json.
--jq EXPR Apply a jq expression to JSON output (implies --json).`,
SilenceUsage: true,
SilenceErrors: true,
Version: displayVersion,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
output.JQExpr = flagJQ
},
}

rootCmd.PersistentFlags().StringVar(&flagAPIKey, "api-key", "", "LangSmith API key [env: LANGSMITH_API_KEY]")
rootCmd.PersistentFlags().StringVar(&flagAPIURL, "api-url", "", "LangSmith API URL [env: LANGSMITH_ENDPOINT]")
rootCmd.PersistentFlags().StringVar(&flagOutputFormat, "format", "json", "Output format: json or pretty")
rootCmd.PersistentFlags().StringVar(&flagOutputFormat, "format", "pretty", "Output format: pretty or json")
rootCmd.PersistentFlags().BoolVar(&flagJSON, "json", false, "Shorthand for --format json")
rootCmd.PersistentFlags().StringVar(&flagJQ, "jq", "", "Apply a jq expression to JSON output (implies --json)")

// Register all subcommand groups
rootCmd.AddCommand(newProjectCmd())
Expand Down Expand Up @@ -94,10 +103,19 @@ func GetAPIURL() string {
}

// GetFormat returns the output format.
// --json and --jq flags take precedence as shorthands for --format json.
func GetFormat() string {
if flagJSON || flagJQ != "" {
return "json"
}
return flagOutputFormat
}

// GetJQ returns the --jq expression, or empty string if not set.
func GetJQ() string {
return flagJQ
}

// MustGetClient creates a LangSmith client or exits with an error.
func MustGetClient() *client.Client {
apiKey := GetAPIKey()
Expand Down
66 changes: 64 additions & 2 deletions internal/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,19 @@ func TestRootCmd_PersistentFlags_Format(t *testing.T) {
if f == nil {
t.Fatal("--format flag not found")
}
if f.DefValue != "json" {
t.Errorf("expected default json, got %q", f.DefValue)
if f.DefValue != "pretty" {
t.Errorf("expected default pretty, got %q", f.DefValue)
}
}

func TestRootCmd_PersistentFlags_JSON(t *testing.T) {
root := NewRootCmd("dev", "dev")
f := root.PersistentFlags().Lookup("json")
if f == nil {
t.Fatal("--json flag not found")
}
if f.DefValue != "false" {
t.Errorf("expected default false, got %q", f.DefValue)
}
}

Expand Down Expand Up @@ -175,6 +186,57 @@ func TestGetFormat_ReturnsValue(t *testing.T) {
}
}

func TestGetFormat_JSONFlagOverrides(t *testing.T) {
oldFmt := flagOutputFormat
oldJSON := flagJSON
defer func() { flagOutputFormat = oldFmt; flagJSON = oldJSON }()

flagOutputFormat = "pretty"
flagJSON = true
if got := GetFormat(); got != "json" {
t.Errorf("expected json when --json is set, got %q", got)
}
}

func TestGetFormat_JQImpliesJSON(t *testing.T) {
oldFmt := flagOutputFormat
oldJSON := flagJSON
oldJQ := flagJQ
defer func() { flagOutputFormat = oldFmt; flagJSON = oldJSON; flagJQ = oldJQ }()

flagOutputFormat = "pretty"
flagJSON = false
flagJQ = ".name"
if got := GetFormat(); got != "json" {
t.Errorf("expected json when --jq is set, got %q", got)
}
}

func TestGetFormat_EmptyJQDoesNotImplyJSON(t *testing.T) {
oldFmt := flagOutputFormat
oldJSON := flagJSON
oldJQ := flagJQ
defer func() { flagOutputFormat = oldFmt; flagJSON = oldJSON; flagJQ = oldJQ }()

flagOutputFormat = "pretty"
flagJSON = false
flagJQ = ""
if got := GetFormat(); got != "pretty" {
t.Errorf("expected pretty when --jq is empty, got %q", got)
}
}

func TestRootCmd_PersistentFlags_JQ(t *testing.T) {
root := NewRootCmd("dev", "dev")
f := root.PersistentFlags().Lookup("jq")
if f == nil {
t.Fatal("--jq flag not found")
}
if f.DefValue != "" {
t.Errorf("expected default empty, got %q", f.DefValue)
}
}

// ---------- Unknown subcommand ----------

func TestRootCmd_UnknownSubcommand(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,18 @@ func setupTestEnv(t *testing.T, serverURL string) func() {
oldKey := flagAPIKey
oldURL := flagAPIURL
oldFmt := flagOutputFormat
oldJSON := flagJSON

flagAPIKey = "test-api-key"
flagAPIURL = serverURL
flagOutputFormat = "json"
flagJSON = false

return func() {
flagAPIKey = oldKey
flagAPIURL = oldURL
flagOutputFormat = oldFmt
flagJSON = oldJSON
}
}

Expand Down
Loading
Loading