diff --git a/cmd/schema.go b/cmd/schema.go index 55836ee..ff5ec5d 100644 --- a/cmd/schema.go +++ b/cmd/schema.go @@ -13,6 +13,8 @@ func init() { schema.Register(schema.CommandSchema{ Command: "search", Description: "Search for content in your Glean instance via the platform API (POST /api/search); responses use its snake_case shape. Falls back to the classic API with a warning when the platform API is not enabled; GLEAN_LEGACY_APIS=1 forces the classic API. Results are JSON.", + WhenToUse: "Find documents, messages, and content across all connected datasources. Start here for any 'find X' task.", + Surface: schema.SurfacePlatform, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "Complete JSON request body in the platform shape: query, page_size, cursor, datasources, datasource_instances, filters, time_range (overrides individual flags). With GLEAN_LEGACY_APIS=1, parsed as the classic shape", Required: false}, "--query": {Type: "string", Description: "Search query (positional arg)", Required: true}, @@ -39,6 +41,8 @@ glean search --json '{"query":"Q1 reports","page_size":5,"datasources":["conflue schema.Register(schema.CommandSchema{ Command: "chat", Description: "Have a conversation with Glean AI. Streams response to stdout.", + WhenToUse: "Ask Glean AI a question that needs a synthesized, cited answer reasoned over company knowledge, rather than a raw list of results.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "Complete JSON chat request body (overrides individual flags)"}, "--message": {Type: "string", Description: "Chat message (positional arg)", Required: true}, @@ -53,6 +57,8 @@ glean chat --json '{"messages":[{"author":"USER","messageType":"CONTENT","fragme schema.Register(schema.CommandSchema{ Command: "api", Description: "Make a raw authenticated HTTP request to any Glean REST API endpoint.", + WhenToUse: "Escape hatch: call any Glean API endpoint directly when no dedicated subcommand exists.", + Surface: schema.SurfaceRaw, Flags: map[string]schema.FlagSchema{ "--method": {Type: "enum", Enum: []string{"GET", "POST", "PUT", "DELETE", "PATCH"}, Default: "GET", Description: "HTTP method"}, "--raw-field": {Type: "string", Description: "JSON request body as a string"}, @@ -68,6 +74,8 @@ glean chat --json '{"messages":[{"author":"USER","messageType":"CONTENT","fragme 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`, }) @@ -75,6 +83,8 @@ glean chat --json '{"messages":[{"author":"USER","messageType":"CONTENT","fragme schema.Register(schema.CommandSchema{ Command: "shortcuts", Description: "Manage Glean shortcuts (go-links). Subcommands: list, get, create, update, delete.", + WhenToUse: "Create or resolve go-links (short memorable URLs like go/roadmap).", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body (see Glean API docs for shape)"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -87,6 +97,8 @@ glean shortcuts create --json '{"data":{"inputAlias":"test/link","destinationUrl schema.Register(schema.CommandSchema{ Command: "agents", Description: "Manage and run Glean agents via the platform API (/api/agents/...). list/get/schemas fall back to the classic API when the platform API is not enabled; run does not (its body shape differs per surface). GLEAN_LEGACY_APIS=1 forces the classic API. Subcommands: list, get, schemas, run.", + WhenToUse: "Discover and execute Glean agents (AI workflows). Use schemas first to learn an agent's expected input, then run.", + Surface: schema.SurfacePlatform, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body. get/schemas: {\"agent_id\":\"\"}. run: {\"agent_id\":\"\"} plus input (map) or messages ([{role, content:[{type:\"text\",text}]}])"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -99,6 +111,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "documents", Description: "Retrieve and summarize Glean documents. Subcommands: get, get-by-facets, get-permissions, summarize.", + WhenToUse: "Fetch full metadata, permissions, or an AI summary for documents you already identified (e.g. from search results).", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -110,6 +124,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "entities", Description: "List and read Glean entities and people. Subcommands: list, read-people.", + WhenToUse: "Look up people (org info, contact details) or other structured entities.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body", Required: true}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -120,6 +136,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "collections", Description: "Manage Glean collections. Subcommands: create, delete, update, add-items, delete-item.", + WhenToUse: "Curate sets of documents into named collections.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -131,6 +149,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "pins", Description: "Manage Glean pins. Subcommands: list, get, create, update, remove.", + WhenToUse: "Pin a document to a search query so it always surfaces for that query.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -142,6 +162,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "answers", Description: "Manage Glean answers. Subcommands: list, get, create, update, delete.", + WhenToUse: "Manage curated Q&A answer cards shown for matching queries.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -153,6 +175,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "tools", Description: "List and run Glean tools. Subcommands: list, run.", + WhenToUse: "Discover and execute Glean tools (actions) such as creating tickets or sending messages.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -164,6 +188,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "verification", Description: "Manage document verification. Subcommands: list, verify, remind.", + WhenToUse: "Track and update document freshness verification (verify docs, send reminders).", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body"}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -175,6 +201,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "activity", Description: "Report user activity and feedback. Subcommands: report, feedback.", + WhenToUse: "Report page-view activity events or submit result feedback to improve ranking.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body (required)", Required: true}, "--dry-run": {Type: "boolean", Default: false}, @@ -185,6 +213,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "insights", Description: "Retrieve Glean usage insights. Subcommands: get.", + WhenToUse: "Retrieve aggregate usage analytics (search/AI adoption metrics) for the deployment.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body (required)", Required: true}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -196,6 +226,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "messages", Description: "Retrieve Glean messages. Subcommands: get.", + WhenToUse: "Fetch a specific chat/communication message by ID.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body (required)", Required: true}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, @@ -206,6 +238,8 @@ glean agents run --json '{"agent_id":"my-agent","input":{"query":"test"}}'`, schema.Register(schema.CommandSchema{ Command: "announcements", Description: "Manage Glean announcements. Subcommands: create, update, delete.", + WhenToUse: "Publish or manage company announcements surfaced in Glean.", + Surface: schema.SurfaceLegacy, Flags: map[string]schema.FlagSchema{ "--json": {Type: "string", Description: "JSON request body (required)", Required: true}, "--output": {Type: "enum", Enum: []string{"json", "ndjson", "text"}, Default: "json"}, diff --git a/internal/agenthelp/agenthelp.go b/internal/agenthelp/agenthelp.go new file mode 100644 index 0000000..d697a9c --- /dev/null +++ b/internal/agenthelp/agenthelp.go @@ -0,0 +1,307 @@ +// Package agenthelp derives agent-facing usage documentation from the live +// cobra command tree, enriched with the schema registry's semantics +// (when-to-use guidance, API surface, examples). Because the output is +// generated from the installed binary at runtime, it is version-accurate by +// construction — agents should treat it as the source of truth when it +// differs from static documentation. +package agenthelp + +import ( + "encoding/json" + "fmt" + "io" + "os" + "sort" + "strings" + + "github.com/gleanwork/glean-cli/internal/client" + "github.com/gleanwork/glean-cli/internal/config" + "github.com/gleanwork/glean-cli/internal/output" + "github.com/gleanwork/glean-cli/internal/platform" + "github.com/gleanwork/glean-cli/internal/schema" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// Context describes the environment the CLI is running in, so agents can +// adapt (e.g. run `glean auth login` before anything else, or expect legacy +// response shapes when the platform opt-out is active). +type Context struct { + Version string `json:"version"` + ServerURL string `json:"server_url,omitempty"` + Authenticated bool `json:"authenticated"` + // AuthType is "api_token" or "oauth" when authenticated. + AuthType string `json:"auth_type,omitempty"` + // LegacyMode reports the GLEAN_LEGACY_APIS opt-out: platform-first + // commands call the classic APIs directly and emit classic shapes. + LegacyMode bool `json:"legacy_mode"` + // ExperimentalDisabledByEnv reports that X_GLEAN_INCLUDE_EXPERIMENTAL is + // explicitly false in the environment, which suppresses the experimental + // header inside the SDK and forces platform-first commands into their + // legacy fallback. + ExperimentalDisabledByEnv bool `json:"experimental_disabled_by_env,omitempty"` +} + +// BuildContext inspects config and environment. It never fails and never +// requires authentication: missing credentials simply yield +// Authenticated=false. +func BuildContext(version string) Context { + c := Context{Version: version, LegacyMode: platform.Legacy()} + if v, ok := os.LookupEnv("X_GLEAN_INCLUDE_EXPERIMENTAL"); ok && strings.EqualFold(v, "false") { + c.ExperimentalDisabledByEnv = true + } + cfg, err := config.LoadConfig() + if err != nil || cfg == nil { + return c + } + c.ServerURL = cfg.GleanServerURL + token, authType := client.ResolveToken(cfg) + if token == "" { + return c + } + c.Authenticated = true + if authType == "" { + c.AuthType = "api_token" + } else { + c.AuthType = "oauth" + } + return c +} + +// FlagDoc describes one flag of one command. Existence, type, and default +// come from cobra (the binary is the source of truth); enum values and +// required-ness are enriched from the schema registry where available. +type FlagDoc struct { + Type string `json:"type"` + Default string `json:"default,omitempty"` + Description string `json:"description,omitempty"` + Enum []string `json:"enum,omitempty"` + Required bool `json:"required,omitempty"` +} + +// CommandDoc describes one command (and, recursively, its subcommands). +type CommandDoc struct { + Path string `json:"path"` + Short string `json:"short,omitempty"` + Long string `json:"long,omitempty"` + WhenToUse string `json:"when_to_use,omitempty"` + Surface string `json:"surface,omitempty"` + Example string `json:"example,omitempty"` + Flags map[string]FlagDoc `json:"flags,omitempty"` + Subcommands []CommandDoc `json:"subcommands,omitempty"` +} + +// Collect walks the visible commands under root and merges cobra's ground +// truth (structure, flags, defaults) with the schema registry's semantics. +func Collect(root *cobra.Command) []CommandDoc { + var docs []CommandDoc + for _, cmd := range root.Commands() { + if skipCommand(cmd) { + continue + } + docs = append(docs, collectCommand(cmd, cmd.Name())) + } + sort.Slice(docs, func(i, j int) bool { return docs[i].Path < docs[j].Path }) + return docs +} + +func skipCommand(cmd *cobra.Command) bool { + return cmd.Hidden || cmd.Name() == "help" || cmd.Name() == "completion" +} + +func collectCommand(cmd *cobra.Command, path string) CommandDoc { + doc := CommandDoc{ + Path: path, + Short: cmd.Short, + Long: cmd.Long, + Flags: map[string]FlagDoc{}, + } + + // Registry semantics are keyed by top-level command name. + topLevel := strings.SplitN(path, " ", 2)[0] + var registered *schema.CommandSchema + if s, err := schema.Get(topLevel); err == nil { + registered = &s + doc.WhenToUse = s.WhenToUse + doc.Surface = s.Surface + if path == topLevel { + doc.Example = s.Example + } + } + + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Hidden { + return + } + fd := FlagDoc{ + Type: f.Value.Type(), + Default: f.DefValue, + Description: f.Usage, + } + if registered != nil { + if rs, ok := registered.Flags["--"+f.Name]; ok { + fd.Enum = rs.Enum + fd.Required = rs.Required + } + } + doc.Flags["--"+f.Name] = fd + }) + + for _, sub := range cmd.Commands() { + if skipCommand(sub) { + continue + } + doc.Subcommands = append(doc.Subcommands, collectCommand(sub, path+" "+sub.Name())) + } + sort.Slice(doc.Subcommands, func(i, j int) bool { return doc.Subcommands[i].Path < doc.Subcommands[j].Path }) + return doc +} + +// Find returns the CommandDoc for a command path like ["agents", "run"]. +func Find(docs []CommandDoc, target []string) (CommandDoc, error) { + if len(target) == 0 { + return CommandDoc{}, fmt.Errorf("empty command path") + } + current := docs + var found *CommandDoc + for depth, name := range target { + found = nil + for i := range current { + parts := strings.Fields(current[i].Path) + if parts[len(parts)-1] == name { + found = ¤t[i] + break + } + } + if found == nil { + return CommandDoc{}, fmt.Errorf("unknown command %q — run 'glean agent-help' for the command map", strings.Join(target[:depth+1], " ")) + } + current = found.Subcommands + } + return *found, nil +} + +// jsonEnvelope is the --json output shape. +type jsonEnvelope struct { + Context Context `json:"context"` + Commands []CommandDoc `json:"commands,omitempty"` + Command *CommandDoc `json:"command,omitempty"` +} + +// Render writes the overview (empty target) or a single command's detail. +func Render(w io.Writer, ctx Context, docs []CommandDoc, target []string, asJSON bool) error { + if len(target) == 0 { + if asJSON { + return writeJSON(w, jsonEnvelope{Context: ctx, Commands: docs}) + } + renderOverview(w, ctx, docs) + return nil + } + doc, err := Find(docs, target) + if err != nil { + return err + } + if asJSON { + return writeJSON(w, jsonEnvelope{Context: ctx, Command: &doc}) + } + renderCommand(w, doc) + return nil +} + +func writeJSON(w io.Writer, v any) error { + data, err := json.MarshalIndent(v, "", " ") + if err != nil { + return err + } + _, err = fmt.Fprintln(w, string(data)) + return err +} + +func renderContext(w io.Writer, ctx Context) { + auth := "no — run 'glean auth login' or set GLEAN_API_TOKEN" + if ctx.Authenticated { + auth = "yes (" + ctx.AuthType + ")" + } + mode := "platform-first (falls back to legacy per endpoint)" + if ctx.LegacyMode { + mode = "legacy (GLEAN_LEGACY_APIS is set)" + } else if ctx.ExperimentalDisabledByEnv { + mode = "legacy (X_GLEAN_INCLUDE_EXPERIMENTAL=false suppresses platform endpoints)" + } + fmt.Fprintf(w, "version: %s\n", ctx.Version) + if ctx.ServerURL != "" { + fmt.Fprintf(w, "server: %s\n", ctx.ServerURL) + } + fmt.Fprintf(w, "authenticated: %s\n", auth) + fmt.Fprintf(w, "api mode: %s\n", mode) +} + +func renderOverview(w io.Writer, ctx Context, docs []CommandDoc) { + fmt.Fprintln(w, "glean — agent-facing usage generated from this binary.") + fmt.Fprintln(w, "Treat this output as the source of truth when it differs from static docs.") + fmt.Fprintln(w) + renderContext(w, ctx) + fmt.Fprintln(w) + + rows := make([][]string, 0, len(docs)) + for _, d := range docs { + use := d.WhenToUse + if use == "" { + use = d.Short + } + rows = append(rows, []string{d.Path, use, d.Surface}) + } + _ = output.WriteTable(w, []string{"COMMAND", "WHEN TO USE", "API"}, rows) + fmt.Fprintln(w) + fmt.Fprintln(w, "Next: `glean agent-help [subcommand]` for flags and payload shapes; add --json for machine-readable output.") +} + +func renderCommand(w io.Writer, doc CommandDoc) { + fmt.Fprintf(w, "glean %s — %s\n", doc.Path, doc.Short) + if doc.Surface != "" { + fmt.Fprintf(w, "api surface: %s\n", doc.Surface) + } + if doc.WhenToUse != "" { + fmt.Fprintf(w, "when to use: %s\n", doc.WhenToUse) + } + if doc.Long != "" && doc.Long != doc.Short { + fmt.Fprintf(w, "\n%s\n", strings.TrimSpace(doc.Long)) + } + if len(doc.Flags) > 0 { + fmt.Fprintln(w, "\nflags:") + names := make([]string, 0, len(doc.Flags)) + for name := range doc.Flags { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + f := doc.Flags[name] + line := fmt.Sprintf(" %s %s", name, f.Type) + if f.Required { + line += " (required)" + } + if len(f.Enum) > 0 { + line += " [" + strings.Join(f.Enum, "|") + "]" + } + if f.Default != "" && f.Default != "false" && f.Default != "0" && f.Default != "[]" { + line += " (default " + f.Default + ")" + } + if f.Description != "" { + line += " — " + f.Description + } + fmt.Fprintln(w, line) + } + } + if len(doc.Subcommands) > 0 { + fmt.Fprintln(w, "\nsubcommands:") + for _, sub := range doc.Subcommands { + fmt.Fprintf(w, " %s — %s\n", sub.Path, sub.Short) + } + } + if doc.Example != "" { + fmt.Fprintln(w, "\nexamples:") + for _, line := range strings.Split(doc.Example, "\n") { + fmt.Fprintf(w, " %s\n", line) + } + } +} diff --git a/internal/agenthelp/agenthelp_test.go b/internal/agenthelp/agenthelp_test.go new file mode 100644 index 0000000..832dea6 --- /dev/null +++ b/internal/agenthelp/agenthelp_test.go @@ -0,0 +1,134 @@ +package agenthelp + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/gleanwork/glean-cli/internal/schema" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newTestTree builds a synthetic cobra tree with a registered schema entry, +// a hidden command, and a nested subcommand. +func newTestTree(t *testing.T) *cobra.Command { + t.Helper() + schema.Register(schema.CommandSchema{ + Command: "widgets", + Description: "Manage widgets.", + WhenToUse: "Use for all widget lifecycle operations.", + Surface: schema.SurfacePlatform, + Flags: map[string]schema.FlagSchema{ + "--output": {Type: "enum", Enum: []string{"json", "text"}}, + "--json": {Type: "string", Required: true}, + }, + Example: "glean widgets list", + }) + + root := &cobra.Command{Use: "glean"} + widgets := &cobra.Command{Use: "widgets", Short: "Manage widgets"} + list := &cobra.Command{Use: "list", Short: "List widgets", Run: func(*cobra.Command, []string) {}} + list.Flags().String("output", "json", "Output format") + list.Flags().String("json", "", "JSON request body") + widgets.AddCommand(list) + hidden := &cobra.Command{Use: "secret", Hidden: true} + root.AddCommand(widgets, hidden) + return root +} + +func TestCollect_SkipsHiddenAndMergesRegistry(t *testing.T) { + docs := Collect(newTestTree(t)) + + require.Len(t, docs, 1, "hidden commands are excluded") + w := docs[0] + assert.Equal(t, "widgets", w.Path) + assert.Equal(t, "Use for all widget lifecycle operations.", w.WhenToUse) + assert.Equal(t, schema.SurfacePlatform, w.Surface) + assert.Equal(t, "glean widgets list", w.Example) + + require.Len(t, w.Subcommands, 1) + list := w.Subcommands[0] + assert.Equal(t, "widgets list", list.Path) + assert.Equal(t, "Use for all widget lifecycle operations.", list.WhenToUse, "subcommands inherit top-level registry semantics") + + // cobra is truth for flag existence/type; registry enriches enum/required. + outFlag, ok := list.Flags["--output"] + require.True(t, ok) + assert.Equal(t, "string", outFlag.Type) + assert.Equal(t, []string{"json", "text"}, outFlag.Enum) + jsonFlag := list.Flags["--json"] + assert.True(t, jsonFlag.Required) +} + +func TestFind(t *testing.T) { + docs := Collect(newTestTree(t)) + + doc, err := Find(docs, []string{"widgets", "list"}) + require.NoError(t, err) + assert.Equal(t, "widgets list", doc.Path) + + _, err = Find(docs, []string{"nonexistent"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "unknown command") +} + +func TestRender_OverviewText(t *testing.T) { + docs := Collect(newTestTree(t)) + buf := &bytes.Buffer{} + ctx := Context{Version: "test", ServerURL: "https://acme-be.glean.com", Authenticated: true, AuthType: "api_token"} + require.NoError(t, Render(buf, ctx, docs, nil, false)) + + out := buf.String() + assert.Contains(t, out, "source of truth") + assert.Contains(t, out, "version: test") + assert.Contains(t, out, "authenticated: yes (api_token)") + assert.Contains(t, out, "widgets") + assert.Contains(t, out, "Use for all widget lifecycle operations.") +} + +func TestRender_UnauthenticatedGuidance(t *testing.T) { + buf := &bytes.Buffer{} + require.NoError(t, Render(buf, Context{Version: "test"}, nil, nil, false)) + assert.Contains(t, buf.String(), "glean auth login") +} + +func TestRender_JSON(t *testing.T) { + docs := Collect(newTestTree(t)) + buf := &bytes.Buffer{} + ctx := Context{Version: "test", LegacyMode: true} + require.NoError(t, Render(buf, ctx, docs, []string{"widgets"}, true)) + + var envelope struct { + Context struct { + Version string `json:"version"` + LegacyMode bool `json:"legacy_mode"` + } `json:"context"` + Command struct { + Path string `json:"path"` + Surface string `json:"surface"` + } `json:"command"` + } + require.NoError(t, json.Unmarshal(buf.Bytes(), &envelope)) + assert.Equal(t, "test", envelope.Context.Version) + assert.True(t, envelope.Context.LegacyMode) + assert.Equal(t, "widgets", envelope.Command.Path) + assert.Equal(t, schema.SurfacePlatform, envelope.Command.Surface) +} + +func TestBuildContext_NeverLeaksTokenMaterial(t *testing.T) { + t.Setenv("GLEAN_SERVER_URL", "https://acme-be.glean.com") + t.Setenv("GLEAN_API_TOKEN", "super-secret-token-value") + t.Setenv("X_GLEAN_INCLUDE_EXPERIMENTAL", "false") + + ctx := BuildContext("test") + assert.True(t, ctx.Authenticated) + assert.Equal(t, "api_token", ctx.AuthType) + assert.True(t, ctx.ExperimentalDisabledByEnv) + + serialized, err := json.Marshal(ctx) + require.NoError(t, err) + assert.False(t, strings.Contains(string(serialized), "super-secret"), "context must never contain token material") +} diff --git a/internal/schema/registry.go b/internal/schema/registry.go index e0e63a1..4a38eb8 100644 --- a/internal/schema/registry.go +++ b/internal/schema/registry.go @@ -20,12 +20,31 @@ type FlagSchema struct { Required bool `json:"required,omitempty"` } +// Surface values for CommandSchema.Surface. +const ( + // SurfacePlatform marks commands served by the platform APIs (/api/*). + SurfacePlatform = "platform" + // SurfaceLegacy marks commands served by the classic client APIs (/rest/api/v1/*). + SurfaceLegacy = "legacy" + // SurfaceRaw marks commands that make raw user-directed HTTP requests. + SurfaceRaw = "raw" + // SurfaceLocal marks commands that never call the Glean API. + SurfaceLocal = "local" +) + // CommandSchema describes one glean command for agent introspection. type CommandSchema struct { - Command string `json:"command"` - Description string `json:"description"` - Flags map[string]FlagSchema `json:"flags"` - Example string `json:"example"` + Command string `json:"command"` + Description string `json:"description"` + // WhenToUse is one sentence of agent guidance: the task this command is + // the right tool for. + WhenToUse string `json:"when_to_use,omitempty"` + // Surface names the API family serving the command: one of the Surface* + // constants. Commands migrated to the platform APIs flip this to + // SurfacePlatform in the same PR. + Surface string `json:"surface,omitempty"` + Flags map[string]FlagSchema `json:"flags"` + Example string `json:"example"` } var (