diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28124fc..b4a43b7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,11 +13,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 with: - go-version: "1.24" + go-version: "1.25" cache: true + # praxis-harness is a private repo in the same org. The default + # GITHUB_TOKEN can't read cross-repo, so CI_GITHUB_TOKEN (a PAT with + # read access to Facets-cloud repos) authenticates module fetches. + - name: Configure private module access + env: + CI_GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + run: | + git config --global url."https://x-access-token:${CI_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE="github.com/Facets-cloud/*" + go env -w GONOSUMDB="github.com/Facets-cloud/*" + - name: gofmt run: | unformatted=$(gofmt -l .) @@ -42,8 +54,17 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 with: - go-version: "1.24" + go-version: "1.25" cache: true + + - name: Configure private module access + env: + CI_GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + run: | + git config --global url."https://x-access-token:${CI_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE="github.com/Facets-cloud/*" + go env -w GONOSUMDB="github.com/Facets-cloud/*" + - uses: goreleaser/goreleaser-action@v7 with: version: "~> v2" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e0165d1..6a8882e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,9 +18,17 @@ jobs: - uses: actions/setup-go@v6 with: - go-version: "1.24" + go-version: "1.25" cache: true + - name: Configure private module access + env: + CI_GITHUB_TOKEN: ${{ secrets.CI_GITHUB_TOKEN }} + run: | + git config --global url."https://x-access-token:${CI_GITHUB_TOKEN}@github.com/".insteadOf "https://github.com/" + go env -w GOPRIVATE="github.com/Facets-cloud/*" + go env -w GONOSUMDB="github.com/Facets-cloud/*" + - name: Verify HOMEBREW_TAP_TOKEN propagates + can write to homebrew-tap env: HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} diff --git a/cmd/chat.go b/cmd/chat.go new file mode 100644 index 0000000..4af5142 --- /dev/null +++ b/cmd/chat.go @@ -0,0 +1,120 @@ +package cmd + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/Facets-cloud/praxis-cli/internal/agent" + "github.com/Facets-cloud/praxis-cli/internal/render" + "github.com/spf13/cobra" +) + +var ( + chatExperimental bool + chatAgents bool + chatModel string + chatThinking string + chatPermission string + chatCwd string + chatSessionID string + chatSessionDir string + chatResume string + chatTeamName string + chatSafe bool + chatEphemeral bool + chatMcpConfig string + chatSettings string + chatProfile string + chatFallback string + chatPrompt string + chatMaxTurns int +) + +var chatCmd = &cobra.Command{ + Use: "chat", + Short: "Start the interactive Praxis coding agent (experimental)", + Long: `Launch a full-featured terminal coding agent — multi-turn conversations, +tool execution, MCP integration, session persistence, and streaming model output. + +This is an EXPERIMENTAL feature. Enable it with --experimental or +PRAXIS_EXPERIMENTAL=1. The agent runs in-process (no separate binary needed) +and shares the Praxis profile directory (~/.praxis/agent/). + +Authentication: the agent uses LLM provider credentials stored in +~/.praxis/agent/auth.json, separate from your Praxis control-plane credentials +in ~/.praxis/credentials. Run 'praxis chat' once and use the /login command +inside the TUI to authenticate with an AI provider (Anthropic, OpenAI, etc.). + +Start views: 'praxis chat' opens a single session; 'praxis chat --agents' opens +the session dashboard, which lists persisted sessions grouped by state and +creates or resumes them from its composer. (Note 'praxis agents' is unrelated: +it lists the agent files praxis installed into your AI hosts.)`, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + if chatExperimental { + agent.Enable() + } + if err := agent.CheckEnabled(); err != nil { + render.PrintError(os.Stderr, false, err.Error(), "", 1) + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + opts := agent.ChatOptions{ + AgentsView: chatAgents, + Model: chatModel, + Thinking: chatThinking, + PermissionMode: chatPermission, + Cwd: chatCwd, + SessionID: chatSessionID, + SessionDir: chatSessionDir, + Resume: chatResume, + TeamName: chatTeamName, + SafeMode: chatSafe, + Ephemeral: chatEphemeral, + McpConfig: chatMcpConfig, + SettingsPath: chatSettings, + Profile: chatProfile, + FallbackModels: chatFallback, + Prompt: chatPrompt, + MaxTurns: chatMaxTurns, + } + + return agent.RunChat(ctx, opts) + }, +} + +func init() { + chatCmd.Flags().BoolVar(&chatExperimental, "experimental", false, "enable experimental agent features") + chatCmd.Flags().BoolVar(&chatAgents, "agents", false, "start on the session dashboard instead of a single session") + chatCmd.Flags().StringVarP(&chatModel, "model", "m", "", "model fuzzy-match (opus, gpt-5.2, glm-5.2)") + chatCmd.Flags().StringVar(&chatThinking, "thinking", "high", "reasoning effort: off|minimal|low|medium|high|xhigh|max|ultra") + chatCmd.Flags().StringVar(&chatPermission, "permission-mode", "auto", "permission mode: ask|auto|yolo") + chatCmd.Flags().StringVar(&chatCwd, "cwd", ".", "working directory") + chatCmd.Flags().StringVar(&chatSessionID, "session-id", "", "fixed session ID to create or resume") + chatCmd.Flags().StringVar(&chatSessionDir, "session-dir", "", "directory for persisted sessions") + chatCmd.Flags().StringVar(&chatResume, "resume", "", "resume a session by its file path") + chatCmd.Flags().StringVar(&chatTeamName, "team-name", "", "resume an existing durable agent team") + chatCmd.Flags().BoolVar(&chatSafe, "safe", false, "safe mode: ask permissions, block destructive ops") + chatCmd.Flags().BoolVar(&chatEphemeral, "ephemeral", false, "do not persist the session (no resume)") + chatCmd.Flags().StringVar(&chatMcpConfig, "mcp-config", "", "explicit MCP config path") + chatCmd.Flags().StringVar(&chatSettings, "settings", "", "explicit settings.json for hooks") + chatCmd.Flags().StringVar(&chatProfile, "profile", "", "isolated Praxis profile") + chatCmd.Flags().StringVar(&chatFallback, "fallback-models", "", "comma-separated fallback model IDs") + chatCmd.Flags().StringVar(&chatPrompt, "prompt", "", "initial prompt to send after startup") + chatCmd.Flags().IntVar(&chatMaxTurns, "max-turns", 0, "max turns per prompt (0 = default)") + + // The dashboard owns session identity: it picks the row to open and clears the + // startup prompt and resume path (harness tui.loadDashboardApplication). Refuse + // the combinations it would silently drop rather than ignoring the user's input. + chatCmd.MarkFlagsMutuallyExclusive("agents", "prompt") + chatCmd.MarkFlagsMutuallyExclusive("agents", "resume") + chatCmd.MarkFlagsMutuallyExclusive("agents", "session-id") + + rootCmd.AddCommand(chatCmd) +} diff --git a/cmd/chat_test.go b/cmd/chat_test.go new file mode 100644 index 0000000..e20d349 --- /dev/null +++ b/cmd/chat_test.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "bytes" + "strings" + "testing" +) + +// execChat drives the command the way a user does: through the ROOT command. +// chatCmd.Execute() would not run chat at all — cobra's ExecuteC starts with +// `if c.HasParent() { return c.Root().ExecuteC() }`, so invoking Execute on a +// registered subcommand re-runs root with root's args (in a test binary, the +// `go test` flags) and silently ignores SetArgs on the child. +func execChat(t *testing.T, args ...string) (string, error) { + t.Helper() + // Reset BEFORE the run, not in a t.Cleanup: cleanups registered by a helper run + // when the test ends, so a table-driven caller would otherwise inherit the flag + // marks of the previous case and assert against the wrong conflict. + resetChatFlagState() + var buf bytes.Buffer + rootCmd.SetOut(&buf) + rootCmd.SetErr(&buf) + rootCmd.SetArgs(append([]string{"chat"}, args...)) + t.Cleanup(func() { + rootCmd.SetArgs(nil) + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + resetChatFlagState() + }) + err := rootCmd.Execute() + return buf.String(), err +} + +// resetChatFlagState restores each flag's default VALUE and clears the "was this +// flag set" mark cobra records on the shared command. Both matter: flag-group +// enforcement reads the marks, so a stale one manufactures a conflict nobody asked +// for — and a leftover help=true makes cobra's execute() return flag.ErrHelp before +// it ever validates, which ExecuteC turns into a nil error that looks like success. +func resetChatFlagState() { + chatAgents, chatPrompt, chatResume, chatSessionID = false, "", "", "" + for _, name := range []string{"agents", "prompt", "resume", "session-id", "help"} { + if flag := chatCmd.Flags().Lookup(name); flag != nil { + _ = flag.Value.Set(flag.DefValue) + flag.Changed = false + } + } +} + +// `praxis agents` is this CLI's installed-agent-file lister, and the skills praxis +// installs into AI hosts call it with --json. The harness session dashboard is +// therefore a START VIEW of the agent command, `praxis chat --agents`, and the help +// has to say so — otherwise the two meanings of "agents" are indistinguishable to +// a user who just wants the dashboard. +func TestChatCmd_AgentsFlagDocumentsDashboardStartView(t *testing.T) { + out, err := execChat(t, "--help") + if err != nil { + t.Fatalf("chat --help err = %v", err) + } + for _, want := range []string{ + "--agents", + "start on the session dashboard", + "'praxis agents' is unrelated", + } { + if !strings.Contains(out, want) { + t.Errorf("chat --help missing %q\nfull output:\n%s", want, out) + } + } +} + +// The dashboard chooses which session to open, and the harness clears the startup +// prompt, resume path, and session id for that row (tui.loadDashboardApplication). +// Silently dropping those flags would look like the agent ignoring the user, so the +// combination is refused before anything launches. +func TestChatCmd_AgentsRejectsSingleSessionIdentityFlags(t *testing.T) { + for _, conflicting := range [][]string{ + {"--agents", "--prompt", "fix the bug"}, + {"--agents", "--resume", "/tmp/session.jsonl"}, + {"--agents", "--session-id", "abc123"}, + } { + _, err := execChat(t, conflicting...) + if err == nil { + t.Fatalf("chat %v was accepted; want a mutually-exclusive-flag error", conflicting) + } + if !strings.Contains(err.Error(), "none of the others can be") { + t.Fatalf("chat %v err = %v, want a mutual exclusion error", conflicting, err) + } + } +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..0b2bc71 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/Facets-cloud/praxis-cli/internal/agent" + "github.com/Facets-cloud/praxis-cli/internal/render" + "github.com/spf13/cobra" +) + +var ( + runExperimental bool + runPrompt string + runPromptFile string + runModel string + runProvider string + runThinking string + runSession string + runForkFrom string + runCwd string + runResultJSON bool + runUsageJSON bool + runNoMCP bool + runMcpConfig string + runSettings string + runMaxTurns int + runMaxTokenBudget int +) + +var runCmd = &cobra.Command{ + Use: "run", + Short: "Run a one-shot prompt through the Praxis agent (experimental)", + Long: `Execute a single prompt headlessly: build the agent, run the prompt to +completion, print the result, and exit. Designed for scripting, CI, and +automation pipelines. + +This is an EXPERIMENTAL feature. Enable it with --experimental or +PRAXIS_EXPERIMENTAL=1. + +The prompt can be provided via --prompt, --prompt-file, or piped through stdin +with --prompt -. + +Examples: + praxis run --experimental --prompt "fix the failing test" + praxis run --experimental --prompt - < input.txt + praxis run --experimental --prompt-file task.txt --model opus + praxis run --experimental --prompt "refactor" --result-json`, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + if runExperimental { + agent.Enable() + } + if err := agent.CheckEnabled(); err != nil { + render.PrintError(os.Stderr, false, err.Error(), "", 1) + return err + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + ha := agent.HeadlessArgs{ + Prompt: runPrompt, + PromptFile: runPromptFile, + Model: runModel, + Provider: runProvider, + Thinking: runThinking, + Session: runSession, + ForkFrom: runForkFrom, + Cwd: runCwd, + ResultJSON: runResultJSON, + UsageJSON: runUsageJSON, + NoMCP: runNoMCP, + McpConfig: runMcpConfig, + SettingsPath: runSettings, + MaxTurns: runMaxTurns, + MaxTokenBudget: runMaxTokenBudget, + } + + exitCode := agent.RunHeadless(ctx, ha.ToNativeArgs()) + if exitCode != 0 { + os.Exit(exitCode) + } + return nil + }, +} + +func init() { + runCmd.Flags().BoolVar(&runExperimental, "experimental", false, "enable experimental agent features") + runCmd.Flags().StringVar(&runPrompt, "prompt", "", "prompt text (or use - to read from stdin)") + runCmd.Flags().StringVar(&runPromptFile, "prompt-file", "", "read the prompt from this file") + runCmd.Flags().StringVarP(&runModel, "model", "m", "", "model name") + runCmd.Flags().StringVar(&runProvider, "provider", "", "provider override (anthropic|openai|google|zai)") + runCmd.Flags().StringVar(&runThinking, "thinking", "high", "reasoning effort") + runCmd.Flags().StringVar(&runSession, "session", "", "session ID to create/load/resume") + runCmd.Flags().StringVar(&runForkFrom, "fork-from", "", "fork a source session into -session") + runCmd.Flags().StringVar(&runCwd, "cwd", ".", "working directory") + runCmd.Flags().BoolVar(&runResultJSON, "result-json", false, "emit final result as JSON") + runCmd.Flags().BoolVar(&runUsageJSON, "usage-json", false, "emit cumulative usage as JSON") + runCmd.Flags().BoolVar(&runNoMCP, "no-mcp", false, "disable MCP discovery") + runCmd.Flags().StringVar(&runMcpConfig, "mcp-config", "", "explicit MCP config path") + runCmd.Flags().StringVar(&runSettings, "settings", "", "explicit settings.json for hooks") + runCmd.Flags().IntVar(&runMaxTurns, "max-turns", 25, "maximum model/tool turns") + runCmd.Flags().IntVar(&runMaxTokenBudget, "max-token-budget", 0, "maximum total billed tokens") + + rootCmd.AddCommand(runCmd) +} diff --git a/go.mod b/go.mod index 273a51d..e7a4852 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,88 @@ module github.com/Facets-cloud/praxis-cli -go 1.24.2 +go 1.25.0 -require github.com/spf13/cobra v1.10.2 +require ( + github.com/Facets-cloud/praxis-harness v0.1.1 + github.com/spf13/cobra v1.10.2 +) require ( + charm.land/bubbletea/v2 v2.0.8 // indirect + charm.land/lipgloss/v2 v2.0.5 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/aymanbagabas/go-nativeclipboard v0.1.3 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/creack/pty v1.1.24 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/dlclark/regexp2 v1.11.4 // indirect + github.com/dlclark/regexp2/v2 v2.2.1 // indirect + github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 // indirect + github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9 // indirect + github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.10.0-alpha.4 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanw/esbuild v0.28.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect + github.com/go-git/go-git/v5 v5.19.1 // indirect + github.com/go-python/gpython v0.2.0 // indirect + github.com/go-rod/rod v0.116.2 // indirect + github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect + github.com/pkoukk/tiktoken-go v0.1.8 // indirect + github.com/pkoukk/tiktoken-go-loader v0.0.2 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/image v0.41.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.39.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.73.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.53.0 // indirect + mvdan.cc/sh/v3 v3.13.1 // indirect + rsc.io/pdf v0.1.1 // indirect ) diff --git a/go.sum b/go.sum index a6ee3e0..e1a08b1 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,258 @@ +charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY= +charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss= +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Facets-cloud/praxis-harness v0.1.1 h1:DG4GSSgSSstqrTkJEIKpi/irC8f1EKntSHh2y9U9HZY= +github.com/Facets-cloud/praxis-harness v0.1.1/go.mod h1:KgNWo9U3CrzPJE+mzAY0WeG7Sbw7YO0FljOXezXiYww= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aymanbagabas/go-nativeclipboard v0.1.3 h1:FmAWHPTwneAixu7uGDn3cL42xPlUCdNp2J8egMn3P1k= +github.com/aymanbagabas/go-nativeclipboard v0.1.3/go.mod h1:2o7MyZwwi4pmXXpOpvOS5FwaHyoCIUks0ktjUvB0EoE= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +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= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= +github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= +github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217 h1:16iT9CBDOniJwFGPI41MbUDfEk74hFaKTqudrX8kenY= +github.com/dop251/base64dec v0.0.0-20231022112746-c6c9f9a96217/go.mod h1:eIb+f24U+eWQCIsj9D/ah+MD9UP+wdxuqzsdLD+mhGM= +github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9 h1:q33zakIx+wEp1Ko5NpDyDBICuXL4JeHUaHbhPowcMEk= +github.com/dop251/goja v0.0.0-20260701091749-b07b74453ea9/go.mod h1:Sc+QOu1WruvaaeT/cxFez/pXHpI9ZDjg/E8QNfSVveI= +github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14 h1:3U8dTgyNBhEQ/GVw0jZW5q+93Zw2gAZPRWhJ9TwV3rM= +github.com/dop251/goja_nodejs v0.0.0-20260212111938-1f56ff5bcf14/go.mod h1:Tb7Xxye4LX7cT3i8YLvmPMGCV92IOi4CDZvm/V8ylc0= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0-alpha.4 h1:JzPbdf+cqbyT9sZtP4xnqelwUXwf7LvD8xKS6+ofTds= +github.com/ebitengine/purego v0.10.0-alpha.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/evanw/esbuild v0.28.1 h1:ds+yuRyUaZGx++GR56CrCeuXh8PVhVM4xq8v7PNELFc= +github.com/evanw/esbuild v0.28.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= +github.com/go-python/gpython v0.2.0 h1:MW7m7pFnbpzHL88vhAdIhT1pgG1QUZ0Q5jcF94z5MBI= +github.com/go-python/gpython v0.2.0/go.mod h1:fUN4z1X+GFaOwPOoHOAM8MOPnh1NJatWo/cDqGlZDEI= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= +github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= +github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= +github.com/pkoukk/tiktoken-go-loader v0.0.2 h1:LUKws63GV3pVHwH1srkBplBv+7URgmOmhSkRxsIvsK4= +github.com/pkoukk/tiktoken-go-loader v0.0.2/go.mod h1:4mIkYyZooFlnenDlormIo6cd5wrlUKNr97wp9nGgEKo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= +mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/agent/agent.go b/internal/agent/agent.go new file mode 100644 index 0000000..da814a1 --- /dev/null +++ b/internal/agent/agent.go @@ -0,0 +1,240 @@ +// Package agent bridges the praxis-harness SDK (TUI + headless runner) into the +// praxis-cli cobra command tree. It is the single import boundary between the +// two repos: cobra commands in cmd/ call these helpers, which delegate to +// praxis-harness's tui.Run / native.Run. +// +// All agent functionality is gated behind an experimental flag (PRAXIS_EXPERIMENTAL +// env var or --experimental CLI flag). When disabled, the chat/run commands +// print a message telling the user how to opt in. +package agent + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/Facets-cloud/praxis-harness/ai" + native "github.com/Facets-cloud/praxis-harness/cli/native" + "github.com/Facets-cloud/praxis-harness/tui" +) + +// experimentalEnvVar gates the agent commands behind an opt-in. The flag is +// also exposed as --experimental on the cobra subcommands. +const experimentalEnvVar = "PRAXIS_EXPERIMENTAL" + +// ErrExperimentalDisabled is returned when the agent commands are invoked +// without the experimental flag. +var ErrExperimentalDisabled = errors.New("agent commands are experimental; set PRAXIS_EXPERIMENTAL=1 or pass --experimental to enable") + +// Enabled returns true if the experimental agent features are turned on. +// The env var is the single source of truth: the --experimental cobra flag +// sets it before any agent command runs. +func Enabled() bool { + return os.Getenv(experimentalEnvVar) == "1" || os.Getenv(experimentalEnvVar) == "true" +} + +// Enable sets the env var so downstream harness code sees the flag. +func Enable() { + _ = os.Setenv(experimentalEnvVar, "1") +} + +// ChatOptions are the flags for the interactive TUI agent (praxis chat). +type ChatOptions struct { + // AgentsView starts the TUI on its session dashboard instead of a single chat + // session — the same view the harness's own `prx agents` opens. It is a start + // view, not a separate mode: the dashboard opens rows in the same in-process + // client. Note `praxis agents` is a DIFFERENT, non-agent command in this CLI + // (it lists installed agent files), so the dashboard is exposed as a flag. + AgentsView bool + Model string + Thinking string + PermissionMode string + Cwd string + SessionID string + SessionDir string + Resume string + TeamName string + SafeMode bool + Ephemeral bool + McpConfig string + SettingsPath string + Profile string + FallbackModels string + Prompt string + MaxTurns int +} + +// RunChat launches the interactive Bubble Tea TUI, on a single chat session or on +// the session dashboard (ChatOptions.AgentsView). It maps ChatOptions to the +// harness's tui.Config (via tui.ParseFlags on a synthesized flag slice) and +// calls tui.Run. Blocks until the user exits the TUI. +func RunChat(ctx context.Context, opts ChatOptions) error { + args := chatOptsToArgs(opts) + cfg := tui.ParseFlags(args) + return tui.Run(ctx, cfg) +} + +// RunHeadless executes a one-shot prompt through the headless native runner. +// It delegates to native.Run with a synthesized argv, exactly like the prx run +// subcommand. Returns the exit code. +func RunHeadless(ctx context.Context, args []string) int { + argv := append([]string{"praxis"}, args...) + return native.Run(ctx, argv) +} + +// chatOptsToArgs converts ChatOptions to a []string that tui.ParseFlags can +// consume. tui.Config fields are unexported, so we go through ParseFlags rather +// than constructing a Config directly. This keeps the bridge resilient to +// Config field additions in the harness — new fields just need new flag +// entries here. +func chatOptsToArgs(opts ChatOptions) []string { + var args []string + // tui.ParseFlags selects the dashboard start view from a LEADING positional + // "agents", which it consumes before parsing flags. It must therefore be + // args[0]; anywhere else it is an unparsed trailing positional and the TUI + // silently opens a normal chat session instead. + if opts.AgentsView { + args = append(args, "agents") + } + if opts.Model != "" { + args = append(args, "-model", opts.Model) + } + if opts.Thinking != "" { + args = append(args, "-thinking", opts.Thinking) + } + if opts.PermissionMode != "" { + args = append(args, "-permission-mode", opts.PermissionMode) + } + if opts.Cwd != "" { + args = append(args, "-cwd", opts.Cwd) + } + if opts.SessionID != "" { + args = append(args, "-session-id", opts.SessionID) + } + if opts.SessionDir != "" { + args = append(args, "-session-dir", opts.SessionDir) + } + if opts.Resume != "" { + args = append(args, "-resume", opts.Resume) + } + if opts.TeamName != "" { + args = append(args, "-team-name", opts.TeamName) + } + if opts.SafeMode { + args = append(args, "-safe") + } + if opts.Ephemeral { + args = append(args, "-ephemeral") + } + if opts.McpConfig != "" { + args = append(args, "-mcp-config", opts.McpConfig) + } + if opts.SettingsPath != "" { + args = append(args, "-settings", opts.SettingsPath) + } + if opts.Profile != "" { + args = append(args, "-profile", opts.Profile) + } + if opts.FallbackModels != "" { + args = append(args, "-fallback-models", opts.FallbackModels) + } + if opts.Prompt != "" { + args = append(args, "-prompt", opts.Prompt) + } + if opts.MaxTurns > 0 { + args = append(args, "-max-turns", fmt.Sprintf("%d", opts.MaxTurns)) + } + return args +} + +// HeadlessArgs converts a set of headless options to the argv slice that +// native.Run expects (without the leading program name). +type HeadlessArgs struct { + Prompt string + PromptFile string + Model string + Provider string + Thinking string + Session string + ForkFrom string + Cwd string + ResultJSON bool + UsageJSON bool + NoMCP bool + McpConfig string + SettingsPath string + MaxTurns int + MaxTokenBudget int +} + +// ToNativeArgs converts HeadlessArgs to the flag slice that native.Run parses. +func (h HeadlessArgs) ToNativeArgs() []string { + var args []string + add := func(flag, val string) { + if val != "" { + args = append(args, flag, val) + } + } + add("-prompt", h.Prompt) + add("-prompt-file", h.PromptFile) + add("-model", h.Model) + add("-provider", h.Provider) + add("-thinking", h.Thinking) + add("-session", h.Session) + add("-fork-from", h.ForkFrom) + add("-cwd", h.Cwd) + add("-mcp-config", h.McpConfig) + add("-settings", h.SettingsPath) + if h.ResultJSON { + args = append(args, "-result-json") + } + if h.UsageJSON { + args = append(args, "-usage-json") + } + if h.NoMCP { + args = append(args, "-no-mcp") + } + if h.MaxTurns > 0 { + args = append(args, "-max-turns", fmt.Sprintf("%d", h.MaxTurns)) + } + if h.MaxTokenBudget > 0 { + args = append(args, "-max-token-budget", fmt.Sprintf("%d", h.MaxTokenBudget)) + } + return args +} + +// CheckEnabled is a convenience for cobra RunE functions: returns +// ErrExperimentalDisabled when the experimental flag is off, with a hint. +func CheckEnabled() error { + if Enabled() { + return nil + } + return fmt.Errorf("%w\n\n %s", ErrExperimentalDisabled, + strings.Join([]string{ + "To try the experimental Praxis coding agent:", + " PRAXIS_EXPERIMENTAL=1 praxis chat", + " praxis chat --experimental", + }, "\n ")) +} + +// Logo returns the Praxis pixel-art logo from the harness TUI package. +// Used by the help/branding output in praxis-cli. +func Logo() string { + return tui.PraxisLogo() +} + +// ResolveModel resolves a model name to its provider using the harness's +// model registry. Returns the resolved model ID and provider name. +func ResolveModel(model string) (provider, resolved string, err error) { + resolvedModel, resolvedProvider, ok := ai.ResolveModelName(model) + if !ok { + return "", "", fmt.Errorf("unknown model %q", model) + } + return resolvedProvider, resolvedModel, nil +} + +// Ensure context is referenced for the linter even if future callers +// don't use it yet — RunChat and RunHeadless take ctx. +var _ = context.Background diff --git a/internal/agent/agent_test.go b/internal/agent/agent_test.go new file mode 100644 index 0000000..e2be72c --- /dev/null +++ b/internal/agent/agent_test.go @@ -0,0 +1,154 @@ +package agent + +import ( + "errors" + "testing" +) + +func TestEnabledDefaultOff(t *testing.T) { + t.Setenv(experimentalEnvVar, "") + if Enabled() { + t.Fatal("Enabled() = true, want false when PRAXIS_EXPERIMENTAL is unset") + } +} + +func TestEnabledEnvVar(t *testing.T) { + t.Setenv(experimentalEnvVar, "1") + if !Enabled() { + t.Fatal("Enabled() = false, want true when PRAXIS_EXPERIMENTAL=1") + } +} + +func TestEnabledEnvVarTrue(t *testing.T) { + t.Setenv(experimentalEnvVar, "true") + if !Enabled() { + t.Fatal("Enabled() = false, want true when PRAXIS_EXPERIMENTAL=true") + } +} + +func TestCheckEnabledReturnsErrorWhenOff(t *testing.T) { + t.Setenv(experimentalEnvVar, "") + err := CheckEnabled() + if err == nil { + t.Fatal("CheckEnabled() = nil, want error when experimental is off") + } + if !errors.Is(err, ErrExperimentalDisabled) { + t.Fatalf("CheckEnabled() err = %v, want ErrExperimentalDisabled", err) + } +} + +func TestCheckEnabledReturnsNilWhenOn(t *testing.T) { + t.Setenv(experimentalEnvVar, "1") + err := CheckEnabled() + if err != nil { + t.Fatalf("CheckEnabled() = %v, want nil when experimental is on", err) + } +} + +func TestChatOptsToArgs(t *testing.T) { + opts := ChatOptions{ + Model: "opus", + Thinking: "high", + PermissionMode: "auto", + Cwd: ".", + SafeMode: true, + MaxTurns: 10, + } + args := chatOptsToArgs(opts) + + want := []string{"-model", "opus", "-thinking", "high", "-permission-mode", "auto", "-cwd", ".", "-safe", "-max-turns", "10"} + if len(args) != len(want) { + t.Fatalf("chatOptsToArgs returned %v (len %d), want %v (len %d)", args, len(args), want, len(want)) + } + for i := range want { + if args[i] != want[i] { + t.Fatalf("args[%d] = %q, want %q (full args: %v)", i, args[i], want[i], args) + } + } +} + +// The dashboard start view rides on a LEADING positional that tui.ParseFlags +// consumes before parsing flags. If "agents" is not args[0] the harness treats it +// as a trailing positional and silently opens a normal chat session, so the +// position — not merely the presence — is the contract under test. +func TestChatOptsToArgsPutsAgentsViewFirst(t *testing.T) { + args := chatOptsToArgs(ChatOptions{AgentsView: true, Model: "opus", Cwd: "/repo"}) + + want := []string{"agents", "-model", "opus", "-cwd", "/repo"} + if len(args) != len(want) { + t.Fatalf("chatOptsToArgs = %v, want %v", args, want) + } + for i := range want { + if args[i] != want[i] { + t.Fatalf("args[%d] = %q, want %q (full args: %v)", i, args[i], want[i], args) + } + } + + if plain := chatOptsToArgs(ChatOptions{Model: "opus"}); len(plain) == 0 || plain[0] == "agents" { + t.Fatalf("chat without --agents must not request the dashboard: %v", plain) + } +} + +func TestChatOptsToArgsEmpty(t *testing.T) { + args := chatOptsToArgs(ChatOptions{}) + if len(args) != 0 { + t.Fatalf("chatOptsToArgs(empty) = %v, want empty slice", args) + } +} + +func TestHeadlessArgsToNativeArgs(t *testing.T) { + ha := HeadlessArgs{ + Prompt: "hello", + Model: "opus", + Cwd: ".", + NoMCP: true, + MaxTurns: 5, + } + args := ha.ToNativeArgs() + + // Verify each flag AND its value (not just presence). + type kv struct{ flag, val string } + want := []kv{ + {"-prompt", "hello"}, + {"-model", "opus"}, + {"-cwd", "."}, + {"-max-turns", "5"}, + } + for _, w := range want { + found := false + for i, a := range args { + if a == w.flag && i+1 < len(args) && args[i+1] == w.val { + found = true + break + } + } + if !found { + t.Errorf("ToNativeArgs missing %s %s, got %v", w.flag, w.val, args) + } + } + // Boolean flags: verify -no-mcp is present (no value). + hasNoMCP := false + for _, a := range args { + if a == "-no-mcp" { + hasNoMCP = true + } + } + if !hasNoMCP { + t.Errorf("ToNativeArgs missing -no-mcp, got %v", args) + } +} + +func TestEnable(t *testing.T) { + t.Setenv(experimentalEnvVar, "") + Enable() + if !Enabled() { + t.Fatal("Enable() did not set PRAXIS_EXPERIMENTAL") + } +} + +func TestLogoNotEmpty(t *testing.T) { + logo := Logo() + if logo == "" { + t.Fatal("Logo() returned empty string") + } +}