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
43 changes: 43 additions & 0 deletions cmd/agent_help.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package cmd

import (
"github.com/gleanwork/glean-cli/internal/agenthelp"
"github.com/spf13/cobra"
)

// NewCmdAgentHelp creates the agent-help command: agent-facing usage
// generated from the installed binary, so it is version-accurate by
// construction. It must work unauthenticated — an agent's first command is
// often discovery, before credentials exist.
func NewCmdAgentHelp() *cobra.Command {
var asJSON bool

cmd := &cobra.Command{
Use: "agent-help [command...]",
Short: "Show agent-facing usage that matches this binary",
Long: `Show agent-facing usage generated from the installed glean binary.

With no arguments, prints the environment context (version, server, auth
state, API mode) and a map of all commands with when-to-use guidance. With a
command path, prints that command's flags, payload shapes, subcommands, and
examples.

Because the output comes from the binary itself, it cannot drift from the
installed version — use it as the source of truth when it differs from
static documentation or pre-installed skills.

Examples:
glean agent-help # command map + environment context
glean agent-help search # full usage for search
glean agent-help agents run --json # machine-readable detail`,
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx := agenthelp.BuildContext(cliVersion)
docs := agenthelp.Collect(cmd.Root())
return agenthelp.Render(cmd.OutOrStdout(), ctx, docs, args, asJSON)
},
}

cmd.Flags().BoolVar(&asJSON, "json", false, "Emit machine-readable JSON")
return cmd
}
145 changes: 145 additions & 0 deletions cmd/agent_help_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package cmd

import (
"bytes"
"encoding/json"
"testing"

"github.com/gleanwork/glean-cli/internal/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// driftAllowlist names visible top-level commands that deliberately have no
// schema registry entry: they are interactive or maintenance commands, not
// agent-invocable API operations. Everything else MUST be registered — the
// registry powers agent-help, so an unregistered command is invisible
// guidance-wise.
var driftAllowlist = map[string]bool{
"auth": true, // interactive login flow
"update": true, // self-update maintenance
}

func TestAgentHelp_RegistryDriftBidirectional(t *testing.T) {
root := NewCmdRoot()

visible := map[string]bool{}
for _, cmd := range root.Commands() {
if cmd.Hidden || cmd.Name() == "help" || cmd.Name() == "completion" {
continue
}
visible[cmd.Name()] = true
if driftAllowlist[cmd.Name()] {
continue
}
_, err := schema.Get(cmd.Name())
assert.NoError(t, err,
"visible command %q has no schema registry entry — register it in cmd/schema.go (with WhenToUse and Surface) so agent-help can describe it",
cmd.Name())
}

// Reverse direction: every registry entry must name a live command
// (hidden commands count — `schema` stays registered as a hidden alias
// would, but stale entries for deleted commands must fail).
all := map[string]bool{}
for _, cmd := range root.Commands() {
all[cmd.Name()] = true
}
for _, name := range schema.List() {
assert.True(t, all[name],
"schema registry entry %q does not correspond to any command — remove the stale entry from cmd/schema.go", name)
}
}

func TestAgentHelp_Overview(t *testing.T) {
usePlatformAPIs(t)
root := NewCmdRoot()
buf := &bytes.Buffer{}
root.SetOut(buf)
root.SetArgs([]string{"agent-help"})
require.NoError(t, root.Execute())

out := buf.String()
assert.Contains(t, out, "source of truth")
assert.Contains(t, out, "COMMAND")
assert.Contains(t, out, "search")
assert.Contains(t, out, "agents")
assert.Contains(t, out, "platform")
assert.NotContains(t, out, "generate-skills", "hidden commands stay out of the map")
}

func TestAgentHelp_WorksUnauthenticated(t *testing.T) {
// No credentials anywhere: agent-help must still succeed and say so.
t.Setenv("GLEAN_SERVER_URL", "")
t.Setenv("GLEAN_API_TOKEN", "")
t.Setenv("HOME", t.TempDir())

root := NewCmdRoot()
buf := &bytes.Buffer{}
root.SetOut(buf)
root.SetArgs([]string{"agent-help"})
require.NoError(t, root.Execute())
assert.Contains(t, buf.String(), "authenticated: no")
assert.Contains(t, buf.String(), "glean auth login")
}

func TestAgentHelp_CommandDetail(t *testing.T) {
usePlatformAPIs(t)
root := NewCmdRoot()
buf := &bytes.Buffer{}
root.SetOut(buf)
root.SetArgs([]string{"agent-help", "agents", "run"})
require.NoError(t, root.Execute())

out := buf.String()
assert.Contains(t, out, "agents run")
assert.Contains(t, out, "--json")
}

func TestAgentHelp_JSON(t *testing.T) {
usePlatformAPIs(t)
root := NewCmdRoot()
buf := &bytes.Buffer{}
root.SetOut(buf)
root.SetArgs([]string{"agent-help", "search", "--json"})
require.NoError(t, root.Execute())

var envelope struct {
Context map[string]any `json:"context"`
Command struct {
Path string `json:"path"`
Surface string `json:"surface"`
Flags map[string]map[string]any `json:"flags"`
} `json:"command"`
}
require.NoError(t, json.Unmarshal(buf.Bytes(), &envelope))
assert.Equal(t, "search", envelope.Command.Path)
assert.Equal(t, schema.SurfacePlatform, envelope.Command.Surface)
assert.Contains(t, envelope.Command.Flags, "--page-size")
}

func TestAgentHelp_UnknownCommand(t *testing.T) {
root := NewCmdRoot()
root.SetOut(&bytes.Buffer{})
root.SetErr(&bytes.Buffer{})
root.SetArgs([]string{"agent-help", "frobnicate"})
err := root.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), "unknown command")
}

func TestSchemaCommand_HiddenAliasStillWorks(t *testing.T) {
root := NewCmdRoot()
buf := &bytes.Buffer{}
root.SetOut(buf)
root.SetArgs([]string{"schema", "search"})
require.NoError(t, root.Execute())
assert.Contains(t, buf.String(), `"command": "search"`)

// And it is hidden from help.
for _, cmd := range root.Commands() {
if cmd.Name() == "schema" {
assert.True(t, cmd.Hidden, "schema must be a hidden alias")
}
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func NewCmdRoot() *cobra.Command {
NewCmdSearch(),
NewCmdChat(),
NewCmdAPI(),
NewCmdAgentHelp(),
NewCmdSchema(),
} {
sub.GroupID = "core"
Expand Down
30 changes: 19 additions & 11 deletions cmd/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,6 @@ glean chat --json '{"messages":[{"author":"USER","messageType":"CONTENT","fragme
Example: `glean api search --method POST --raw-field '{"query":"test"}' --no-color | jq .results`,
})

schema.Register(schema.CommandSchema{
Command: "version",
Description: "Print the glean CLI version string.",
WhenToUse: "Check the installed CLI version, e.g. before reporting a bug or verifying an upgrade.",
Surface: schema.SurfaceLocal,
Flags: map[string]schema.FlagSchema{},
Example: `glean version`,
})

schema.Register(schema.CommandSchema{
Command: "shortcuts",
Description: "Manage Glean shortcuts (go-links). Subcommands: list, get, create, update, delete.",
Expand Down Expand Up @@ -235,6 +226,18 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`,
Example: `glean messages get --json '{"messageId":"MSG_ID"}' | jq .`,
})

schema.Register(schema.CommandSchema{
Command: "agent-help",
Description: "Show agent-facing usage generated from the installed glean binary: environment context plus a command map, or full detail for one command.",
WhenToUse: "Start every session here: discover available commands, exact flags, payload shapes, and whether the environment is authenticated — without static docs in context.",
Surface: schema.SurfaceLocal,
Flags: map[string]schema.FlagSchema{
"--json": {Type: "boolean", Default: false, Description: "Emit machine-readable JSON"},
},
Example: `glean agent-help
glean agent-help agents run --json`,
})

schema.Register(schema.CommandSchema{
Command: "announcements",
Description: "Manage Glean announcements. Subcommands: create, update, delete.",
Expand All @@ -250,10 +253,15 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`,
}

// NewCmdSchema creates and returns the schema command.
//
// Deprecated: superseded by `glean agent-help`, which derives structure from
// the live command tree and adds context awareness. Kept as a hidden alias
// for compatibility with existing agent skills that call `glean schema`.
func NewCmdSchema() *cobra.Command {
cmd := &cobra.Command{
Use: "schema [command]",
Short: "Show JSON schema for a command's flags and request format",
Use: "schema [command]",
Hidden: true,
Short: "Show JSON schema for a command's flags and request format (deprecated: use agent-help)",
Long: `Show machine-readable JSON schema for any glean command.

Agents can call this before invoking a command to understand parameter
Expand Down