Skip to content
Open
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Realtime: Realtime gateway API service
* [Authentication](#authentication)
* [Browser sign-in (OAuth2)](#browser-sign-in-oauth2)
* [Link a project to a directory](#link-a-project-to-a-directory)
* [Get project context in one call](#get-project-context-in-one-call)
* [Available Commands](#available-commands)
* [Request Body Input](#request-body-input)
* [Server Selection](#server-selection)
Expand Down Expand Up @@ -309,6 +310,33 @@ effect, and `vf unlink` removes it.
instructions file (`CLAUDE.md`, `AGENTS.md`), so the agent knows the project
is linked before it runs its first command.

## Get project context in one call

`vf context` summarizes a project for an AI coding agent: the model, the
global prompt and instructions (line counts and an opening excerpt), the
playbooks with the description the agent routes on, functions, agent tools,
variables, the knowledge base, the most recent changes and conversations, and
the rules for working on it.

```bash
vf context # the linked project; TOON in agent mode
vf context --output-format json
vf context --project-id <id> --environment-alias dev
```

It makes the underlying API calls itself, in parallel, and returns an outline
rather than the raw data. Long text is clipped and long lists are capped, with
the true totals under `counts`. In agent mode the output stays under 20 KB
(about 5,000 tokens) however large the project is; a large real project comes
to about 9 KB. `drillDown` lists the commands that return anything the outline
leaves out.

- **A partial answer beats none.** If part of the project cannot be read, the
outline still prints, that part's count is `null`, and `warnings` says why.
- **When, not who.** `recentChanges` is ordered by `updatedAt`. The API does
not report who made a change, and the agent's own instructions and global
prompt carry no timestamp, so their edits do not appear there.

<!-- Start Available Commands [operations] -->
## Available Commands

Expand Down
1 change: 1 addition & 0 deletions docs/vf.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ vf [flags]
* [vf api-tool](vf_api-tool.md) - Operations for api-tool
* [vf auth](vf_auth.md) - Manage authentication credentials
* [vf configure](vf_configure.md) - Configure authentication credentials and preferences
* [vf context](vf_context.md) - Summarize a project for an AI coding agent in one call
* [vf conversation](vf_conversation.md) - Operations for conversation
* [vf document](vf_document.md) - Operations for document
* [vf environment](vf_environment.md) - Operations for environment
Expand Down
63 changes: 63 additions & 0 deletions docs/vf_context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
## vf context

Summarize a project for an AI coding agent in one call

### Synopsis

Summarize a Voiceflow project in one call: what the agent is (model, global
prompt, instructions, playbooks, workflows, functions, tools, variables and
knowledge base), what changed most recently, the latest conversations, and
the rules for working on it.

It reads the linked project (see 'vf link'), or --project-id and
--environment-alias. The CLI makes the underlying API calls itself, in
parallel, and returns an outline rather than the raw data: long text is
clipped and long lists are capped, with the true totals under counts. In
agent mode the output is TOON and stays under 20 KB (about 5,000 tokens)
however large the project is. drillDown lists the commands that return
anything the outline leaves out.

If part of the project cannot be read, the outline still prints and warnings
names what is missing.

```
vf context [flags]
```

### Examples

```
vf context
vf context --project-id 6a67842584dac97c7626ebaa --environment-alias dev
vf context --output-format json
```

### Options

```
-e, --environment-alias string Environment to summarize (default: the linked environment, else main)
-h, --help help for context
-p, --project-id string Project to summarize (default: the linked project)
```

### Options inherited from parent commands

```
--agent-mode Enable structured errors and default TOON output for AI coding agents. Automatically enabled when a known agent environment is detected (CLAUDE_CODE, CURSOR_AGENT, etc.). Use --agent-mode=false to disable.
--color string Control colored output: auto (color when output is a TTY), always, or never. Respects NO_COLOR and FORCE_COLOR env vars. (default "auto")
-d, --debug Log request and response diagnostics to stderr
--dry-run Preview the request that would be sent without executing it (output to stderr)
-H, --header stringArray Set a custom HTTP request header (format: "Key: Value"). Can be specified multiple times.
--include-headers Include HTTP response headers in the output
-q, --jq string Filter and transform output using a jq expression (e.g., '.name', '.items[] | .id')
--no-interactive Disable all interactive features (auto-prompting, explorer auto-launch, TUI forms)
-o, --output-format string Specify the output format. Options: pretty, json, yaml, table, toon. (default "pretty")
--server-url string Override the default server URL
--timeout string HTTP request timeout (e.g., 30s, 5m, 100ms)
--token string Voiceflow bearer token
--usage Print the CLI Usage schema in KDL format
```

### SEE ALSO

* [vf](vf.md) - Realtime: Realtime gateway API service
259 changes: 259 additions & 0 deletions internal/cli/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
// This file is not generated by Speakeasy. It adds `vf context`: one call that
// returns what an AI coding agent needs to know about a project before it
// edits anything. internal/outline decides what goes into the summary; this
// file fetches the parts.

package cli

import (
"context"
"fmt"
"sort"
"strings"
"sync"
"time"

"github.com/spf13/cobra"
"github.com/voiceflow/cli/internal/client"
"github.com/voiceflow/cli/internal/link"
"github.com/voiceflow/cli/internal/outline"
"github.com/voiceflow/cli/internal/output"
"github.com/voiceflow/cli/internal/sdk/models/components"
"github.com/voiceflow/cli/internal/sdk/models/operations"
)

const (
// contextDeadline bounds the whole fan-out; generated commands have no
// default timeout, and a summary is worthless if it hangs.
contextDeadline = 60 * time.Second
// contextParallelism is how many API calls run at once: fast, and gentle
// on rate limits.
contextParallelism = 4
// contextConversations is how many recent conversations are fetched.
contextConversations = 5
)

// initContextCmd registers `vf context`.
func initContextCmd(parent *cobra.Command) {
cmd := &cobra.Command{
Use: "context",
Short: "Summarize a project for an AI coding agent in one call",
Long: `Summarize a Voiceflow project in one call: what the agent is (model, global
prompt, instructions, playbooks, workflows, functions, tools, variables and
knowledge base), what changed most recently, the latest conversations, and
the rules for working on it.

It reads the linked project (see 'vf link'), or --project-id and
--environment-alias. The CLI makes the underlying API calls itself, in
parallel, and returns an outline rather than the raw data: long text is
clipped and long lists are capped, with the true totals under counts. In
agent mode the output is TOON and stays under 20 KB (about 5,000 tokens)
however large the project is. drillDown lists the commands that return
anything the outline leaves out.

If part of the project cannot be read, the outline still prints and warnings
names what is missing.`,
Example: ` vf context
vf context --project-id 6a67842584dac97c7626ebaa --environment-alias dev
vf context --output-format json`,
Args: cobra.NoArgs,
RunE: runContextCmd,
Comment on lines +59 to +60
}
cmd.Flags().StringP("project-id", "p", "", "Project to summarize (default: the linked project)")
cmd.Flags().StringP("environment-alias", "e", "", "Environment to summarize (default: the linked environment, else main)")
parent.AddCommand(cmd)
}

// contextResult wraps the outline for output.Result, which renders the first
// field of the value it is given.
type contextResult struct {
Context outline.Outline `json:"context"`
}

func runContextCmd(cmd *cobra.Command, args []string) error {
projectID, _ := cmd.Flags().GetString("project-id")
projectID = strings.TrimSpace(projectID)
if projectID == "" {
return linkError(cmd, "no_project",
"no project to summarize: this directory is not linked, and --project-id was not given",
"Link this directory: vf link <project-id>",
"Or pass --project-id (and --environment-alias)")
}
alias, _ := cmd.Flags().GetString("environment-alias")
alias = strings.TrimSpace(alias)
if alias == "" {
alias = link.DefaultEnvironmentAlias
}

s, err := client.NewClient(cmd)
if err != nil {
return err
}
sdkOpts, err := output.PrepareCallOpts(cmd)
if err != nil {
return err
}
dryRun := client.IsDryRun(cmd)
if dryRun {
sdkOpts = append(sdkOpts, operations.WithSkipDeserialization())
}

ctx, cancel := context.WithTimeout(cmd.Context(), contextDeadline)
defer cancel()

var (
mu sync.Mutex
in outline.Inputs
fatal error
wg sync.WaitGroup
slots = make(chan struct{}, contextParallelism)
)
// fetch runs one call. An essential part (the project, the agent) that
// fails fails the command; any other part becomes a warning, because a
// partial outline is more useful than none.
fetch := func(part string, essential bool, call func() error) {
wg.Add(1)
go func() {
defer wg.Done()
slots <- struct{}{}
defer func() { <-slots }()
err := call()
if err == nil {
return
}
mu.Lock()
defer mu.Unlock()
if essential {
if fatal == nil {
fatal = err
}
return
}
in.Warnings = append(in.Warnings, fmt.Sprintf("%s could not be read: %s", part, briefError(err)))
}()
}
// store copies a fetched part into the inputs under the lock.
store := func(apply func()) {
mu.Lock()
defer mu.Unlock()
apply()
}

fetch("project", true, func() error {
res, err := s.Project.Get(ctx, operations.StableProjectControllerGetRequest{ProjectID: projectID}, sdkOpts...)
if err == nil && res.StableProjectResponse != nil {
store(func() { in.Project = res.StableProjectResponse.Project })
}
return err
})
fetch("agent", true, func() error {
res, err := s.Agent.Get(ctx, operations.StableAgentControllerGetV2Request{
ProjectID: projectID,
EnvironmentAlias: alias,
IncludeInstructions: ptrTo(true),
IncludePrompt: ptrTo(true),
}, sdkOpts...)
if err == nil && res.StableAgentResponseV2 != nil {
store(func() { in.Agent = res.StableAgentResponseV2.Agent })
}
return err
})
fetch("environment", false, func() error {
res, err := s.Environment.Get(ctx, operations.StableEnvironmentControllerGetRequest{EnvironmentAlias: alias, ProjectID: projectID}, sdkOpts...)
if err == nil && res.StableEnvironmentResponse != nil {
env := res.StableEnvironmentResponse.Environment
store(func() { in.Environment = &env })
}
return err
})
fetch("playbooks", false, func() error {
res, err := s.Playbook.List(ctx, operations.StablePlaybookControllerListV2Request{
ProjectID: projectID, EnvironmentAlias: alias, IncludeInstructions: ptrTo(false),
}, sdkOpts...)
if err == nil && res.StablePlaybookReadListResponseV2 != nil {
store(func() { in.Playbooks = res.StablePlaybookReadListResponseV2.Playbooks })
}
return err
})
fetch("functions", false, func() error {
res, err := s.Function.List(ctx, operations.StableFunctionControllerListRequest{ProjectID: projectID, EnvironmentAlias: alias}, sdkOpts...)
if err == nil && res.StableFunctionListResponse != nil {
store(func() { in.Functions = res.StableFunctionListResponse.Functions })
}
return err
})
// The tools endpoint needs a target; the outline asks for the agent's own
// tools, and drillDown names the call for a playbook's.
fetch("agent tools", false, func() error {
res, err := s.Tool.List(ctx, operations.StableToolControllerListV2Request{
ProjectID: projectID, EnvironmentAlias: alias, Global: ptrTo(true),
}, sdkOpts...)
if err == nil && res.StableToolListResponseV2 != nil {
store(func() { in.Tools = res.StableToolListResponseV2.Tools })
}
return err
})
fetch("variables", false, func() error {
res, err := s.Variable.List(ctx, operations.StableVariableControllerListV2Request{ProjectID: projectID, EnvironmentAlias: alias}, sdkOpts...)
if err == nil && res.StableVariableListResponseV2 != nil {
store(func() { in.Variables = res.StableVariableListResponseV2.Variables })
}
return err
})
fetch("knowledge base", false, func() error {
res, err := s.Document.List(ctx, operations.StableDocumentControllerListRequest{ProjectID: projectID, EnvironmentAlias: alias}, sdkOpts...)
if err == nil && res.StableDocumentListResponse != nil {
store(func() { in.Documents = res.StableDocumentListResponse.Documents })
}
return err
})
fetch("MCP servers", false, func() error {
res, err := s.McpServer.List(ctx, operations.StableMCPServerControllerListV2Request{ProjectID: projectID, EnvironmentAlias: alias}, sdkOpts...)
if err == nil && res.StableMCPServerListResponseV2 != nil {
store(func() { in.MCPServers = res.StableMCPServerListResponseV2.McpServers })
}
return err
})
fetch("tests", false, func() error {
res, err := s.Test.List(ctx, operations.StableTestControllerListRequest{ProjectID: projectID, EnvironmentAlias: alias}, sdkOpts...)
if err == nil && res.StableTestListResponse != nil {
store(func() { in.Tests = res.StableTestListResponse.Tests })
}
return err
})
fetch("recent conversations", false, func() error {
take := float64(contextConversations)
res, err := s.Transcript.Search(ctx, operations.StableTranscriptControllerSearchRequest{
ProjectID: projectID,
Body: components.StableTranscriptSearchRequest{Take: &take, EnvironmentAlias: &alias},
}, sdkOpts...)
if err == nil && res.StableTranscriptListResponse != nil {
store(func() { in.Transcripts = res.StableTranscriptListResponse.Transcripts })
}
return err
})
wg.Wait()

if fatal != nil {
return output.Error(cmd, fatal)
}
if dryRun {
return nil
}
sort.Strings(in.Warnings)
return output.Result(cmd, contextResult{Context: outline.Build(in)})
}

// briefError is an error's first line, short enough for a warning.
func briefError(err error) string {
if code := statusCode(err); code != 0 {
return fmt.Sprintf("HTTP %d", code)
}
msg, _, _ := strings.Cut(err.Error(), "\n")
if len(msg) > 160 {
msg = msg[:159] + "…"
}
return msg
}

func ptrTo[T any](v T) *T { return &v }
1 change: 1 addition & 0 deletions internal/cli/link.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func runLinkCmd(cmd *cobra.Command, args []string) error {
// by getting them wrong.
func agentInstructions(l link.Link) string {
return fmt.Sprintf(`This directory is linked to the Voiceflow project %q (%s), environment %q. vf commands run here use them by default, so leave out --project-id and --environment-alias.
- Start with 'vf context': one call returns what the agent is, what changed recently, and the latest conversations.
- Changes take effect only after 'vf environment compile'. Test the draft with --version-param draft.
- Publishing ('vf environment publish') ships to real users. Ask before running it.`,
l.ProjectName, l.ProjectID, l.EnvironmentAlias)
Expand Down
3 changes: 2 additions & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ func NewRootCommand() (*cobra.Command, error) {
}
initExploreCmd(rootCmd)
initDocsCmd(rootCmd)
initLinkCmd(rootCmd) // vf link / vf unlink; see link.go
initLinkCmd(rootCmd) // vf link / vf unlink; see link.go
initContextCmd(rootCmd) // vf context; see context.go

// Global output format flag
rootCmd.PersistentFlags().StringP("output-format", "o", "pretty", "Specify the output format. Options: pretty, json, yaml, table, toon.")
Expand Down
Loading
Loading