diff --git a/.gitignore b/.gitignore index cf565ba7..3cc5c907 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Binaries /hawk -bin/ +# Ignore build output in bin/, but track committed source scripts like bin/buf. +bin/* +!bin/buf hawk_bin *.exe *.dll diff --git a/bin/buf b/bin/buf new file mode 100755 index 00000000..28ec383f --- /dev/null +++ b/bin/buf @@ -0,0 +1,140 @@ +#!/bin/sh +# Hermetic launcher for the `buf` CLI (bufbuild/buf). +# +# Purpose: hermetic proto codegen — no host `buf` dependency. The binary is +# SHA-pinned per (platform, arch), fetched from the official GitHub release, +# cached locally, and reused. Mirrors grok's `bin/protoc` dotslash launcher, +# which fetches a SHA-pinned protoc per platform. Unlike dotslash (which ships +# an artifact manifest checked into the repo), this resolves a single pinned +# version via env override and verifies a SHA-256 per triple. +# +# Version is pinned via BUF_VERSION (default below). Override the cache root +# with HAWK_BIN_DIR (mirrors grok's GROK_SHELL_RG_DOWNLOAD_BASE overridability +# for offline / mirrored CI): +# +# HAWK_BIN_DIR — base cache dir (default ~/.cache/hawk/bin) +# BUF_VERSION — override the pinned version +# +# Exit codes are from the underlying `buf` invocation; fetch failures exit 127. +# +# Source pattern: grok `bin/protoc`. +set -e + +BUF_VERSION="${BUF_VERSION:-1.50.0}" + +# --------------------------------------------------------------------------- +# SHA-256 checksums of the buf release tarballs, keyed by +# _ (os lowercased, arch as Go sees it: amd64/arm64). +# Verified against https://github.com/bufbuild/buf/releases/tag/v1.50.0 +# sha256sum buf-Linux-x86_64.tar.gz buf-Linux-aarch64.tar.gz +# buf-Darwin-x86_64.tar.gz buf-Darwin-arm64.tar.gz +# --------------------------------------------------------------------------- +SHA256_darwin_arm64="c80f7f8a1d8ffd36c5db31a360c7e0b65c8cf671d60bd3c34e1558e54f84f4cc" +SHA256_darwin_amd64="fc64b4a16964d7ec49fb2d245159d57dbfb3dac947e2a86413f9685cf8de2ac5" +SHA256_linux_amd64="80c1211dfc4844499c6ddad341bb21206579883fd33cea0a2c40c82befd70602" +SHA256_linux_arm64="4c920c5f96eb99ad13eb6f25cf740fdb42963401faa267bee03fbd3e163730b2" + +# Detect platform + arch the same way install.sh does. +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) +case "$ARCH" in + x86_64) arch="amd64" ;; + aarch64) arch="arm64" ;; + arm64) arch="arm64" ;; + *) + echo "bin/buf: unsupported arch $ARCH" >&2 + exit 127 + ;; +esac +case "$OS" in + darwin) triple="darwin_${arch}" ;; + linux) triple="linux_${arch}" ;; + *) + echo "bin/buf: unsupported OS $OS" >&2 + exit 127 + ;; +esac + +# The GitHub release asset name uses a capitalized OS and the arch spelling +# buf publishes (Darwin-arm64, Linux-aarch64), which differs from the +# cache-key triple above. Build it explicitly to avoid 404s. +# uname -m yields arm64 on macOS and aarch64 on Linux, so map each real +# (os, arch) pair to the asset name buf publishes. +case "$OS/$ARCH" in + darwin/x86_64) asset="Darwin-x86_64" ;; + darwin/arm64) asset="Darwin-arm64" ;; + linux/x86_64) asset="Linux-x86_64" ;; + linux/aarch64) asset="Linux-aarch64" ;; + *) asset="" ;; +esac +if [ -z "$asset" ]; then + echo "bin/buf: unsupported platform $OS/$ARCH" >&2 + exit 127 +fi + +# Resolve the pinned checksum for this triple. +sha_var="SHA256_${triple}" +sha="$(eval echo "\${$sha_var}")" +case "$sha" in + SHA256_*) echo "bin/buf: placeholder checksum for $triple — set real $sha_var before use" >&2; exit 127 ;; +esac + +# Cache the extracted binary so repeat invocations skip the network fetch. +XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" +CACHE_BASE="${HAWK_BIN_DIR:-$XDG_CACHE_HOME/hawk/bin}" +CACHE_BIN="$CACHE_BASE/buf-${BUF_VERSION}-${triple}/bin/buf" + +URL="https://github.com/bufbuild/buf/releases/download/v${BUF_VERSION}/buf-${asset}.tar.gz" + +fetch_bin() { + tmp=$(mktemp -d) + trap 'rm -rf "$tmp"' EXIT + archive="$tmp/buf.tar.gz" + if command -v curl >/dev/null 2>&1; then + curl -fsSL --proto '=https' --tlsv1.2 "$URL" -o "$archive" + elif command -v wget >/dev/null 2>&1; then + wget -q "$URL" -O "$archive" + else + echo "bin/buf: need curl or wget" >&2 + exit 127 + fi + + # Verify checksum before extraction. + if command -v sha256sum >/dev/null 2>&1; then + actual=$(sha256sum "$archive" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + actual=$(shasum -a 256 "$archive" | awk '{print $1}') + else + echo "bin/buf: need sha256sum or shasum" >&2 + exit 127 + fi + if [ "$actual" != "$sha" ]; then + echo "bin/buf: checksum mismatch for $triple" >&2 + echo " expected: $sha" >&2 + echo " actual: $actual" >&2 + exit 127 + fi + + # Extract the `buf` binary from the release tarball. + mkdir -p "$tmp/extract" + tar xzf "$archive" -C "$tmp/extract" + extracted="$(find "$tmp/extract" -type f -name buf -path '*bin*' | head -n1)" + if [ -z "$extracted" ]; then + # Fallback: release tgz root may be the binary directly in bin/. + extracted="$tmp/extract/bin/buf" + if [ ! -f "$extracted" ]; then + echo "bin/buf: could not locate buf binary in archive" >&2 + exit 127 + fi + fi + + mkdir -p "$(dirname "$CACHE_BIN")" + chmod +x "$extracted" + mv -f "$extracted" "$CACHE_BIN" +} + +if [ ! -f "$CACHE_BIN" ]; then + fetch_bin +fi + +exec "$CACHE_BIN" "$@" diff --git a/cmd/chat_commands_util.go b/cmd/chat_commands_util.go index 45c7603a..88160ad4 100644 --- a/cmd/chat_commands_util.go +++ b/cmd/chat_commands_util.go @@ -157,3 +157,12 @@ func tasteStoreForSession() (*taste.Store, error) { func stalenessFormatReport(rules []staleness.StaleRule) string { return staleness.FormatReport(rules) } + +// truncatePromptPreview truncates a prompt to a preview string. +func truncatePromptPreview(s string, max int) string { + s = strings.TrimSpace(s) + if len(s) <= max { + return s + } + return s[:max] + "…" +} diff --git a/cmd/chat_subcommand_simple.go b/cmd/chat_subcommand_simple.go index 1bbaca1a..bdd5ad46 100644 --- a/cmd/chat_subcommand_simple.go +++ b/cmd/chat_subcommand_simple.go @@ -15,6 +15,7 @@ import ( analytics "github.com/GrayCodeAI/hawk/internal/observability" "github.com/GrayCodeAI/hawk/internal/plugin" "github.com/GrayCodeAI/hawk/internal/storage" + "github.com/GrayCodeAI/hawk/internal/theme" "github.com/GrayCodeAI/hawk/internal/tool" "github.com/GrayCodeAI/hawk/internal/ui/icons" ) @@ -345,6 +346,111 @@ func init() { }, }) + // /compact-mode — toggle compact mode + subcommandRegistry.Register(&delegatingCommand{ + name: "compact-mode", + aliases: []string{"compact"}, + description: "toggle compact mode (removes outer padding)", + usage: "", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + settings := hawkconfig.LoadGlobalSettings() + current := settings.CompactMode + newVal := !current + valStr := "false" + if newVal { + valStr = "true" + } + if err := hawkconfig.SetGlobalSetting("compact_mode", valStr); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()}) + } else { + state := "disabled" + if newVal { + state = "enabled" + } + m.messages = append(m.messages, displayMsg{role: "system", content: "Compact mode " + state}) + m.viewDirty = true + m.updateViewportContent() + } + return m, nil + }, + }) + + // /scroll-speed — set scroll speed (1-100) + subcommandRegistry.Register(&delegatingCommand{ + name: "scroll-speed", + description: "set scroll speed (1-100)", + usage: "/scroll-speed <1-100>", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + if len(args) < 1 { + m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Usage: /scroll-speed <1-100>\nCurrent: %d", hawkconfig.LoadGlobalSettings().ScrollSpeed)}) + return m, nil + } + speed, err := strconv.Atoi(args[0]) + if err != nil || speed < 1 || speed > 100 { + m.messages = append(m.messages, displayMsg{role: "error", content: "Scroll speed must be 1-100"}) + return m, nil + } + if err := hawkconfig.SetGlobalSetting("scroll_speed", args[0]); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: "Scroll speed → " + args[0]}) + } + return m, nil + }, + }) + + // /scroll-invert — toggle scroll direction invert + subcommandRegistry.Register(&delegatingCommand{ + name: "scroll-invert", + description: "toggle natural scrolling (invert direction)", + usage: "", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + settings := hawkconfig.LoadGlobalSettings() + current := settings.InvertScroll + newVal := !current + valStr := "false" + if newVal { + valStr = "true" + } + enabled := "disabled" + if newVal { + enabled = "enabled" + } + if err := hawkconfig.SetGlobalSetting("invert_scroll", valStr); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: "Natural scrolling " + enabled}) + } + return m, nil + }, + }) + + // /scroll-mode — set scroll mode (auto, wheel, trackpad) + subcommandRegistry.Register(&delegatingCommand{ + name: "scroll-mode", + description: "set scroll mode (auto, wheel, trackpad)", + usage: "/scroll-mode ", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + if len(args) < 1 { + m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Usage: /scroll-mode \nCurrent: %s", hawkconfig.LoadGlobalSettings().ScrollMode)}) + return m, nil + } + mode := strings.ToLower(args[0]) + switch mode { + case "auto", "wheel", "trackpad": + if err := hawkconfig.SetGlobalSetting("scrollmode", mode); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Scroll mode → %s", mode)}) + } + return m, nil + default: + m.messages = append(m.messages, displayMsg{role: "error", content: "Valid modes: auto, wheel, trackpad"}) + return m, nil + } + }, + }) + // /hooks — show configured hooks subcommandRegistry.Register(&delegatingCommand{ name: "hooks", @@ -356,6 +462,82 @@ func init() { }, }) + // /terminal-setup — show terminal configuration recommendations + subcommandRegistry.Register(&delegatingCommand{ + name: "terminal-setup", + description: "show terminal configuration recommendations", + usage: "", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + caps := theme.DetectTerminalCapabilities() + level := "basic (16 colors)" + switch caps.ColorLevel { + case theme.ColorTruecolor: + level = "truecolor (24-bit RGB)" + case theme.Color256: + level = "256-color" + } + + m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Terminal Setup Recommendations:\n\nColor Support: %s\nScroll Mode: %s (use /scroll-mode to change)\nScroll Speed: %d (use /scroll-speed to change)\nCompact Mode: %v (use /compact-mode to toggle)\n\nTips:\n- Set COLORTERM=truecolor for best color experience\n- Use tmux with set -g default-terminal \"tmux-256color\" for 256-color support\n- Enable mouse reporting in your terminal for full TUI interaction", level, hawkconfig.LoadGlobalSettings().ScrollMode, hawkconfig.LoadGlobalSettings().ScrollSpeed)}) + return m, nil + }, + }) + + // /pager-config — configure scrollback pager + subcommandRegistry.Register(&delegatingCommand{ + name: "pager-config", + description: "configure scrollback pager (lines|linenumbers)", + usage: "/pager-config ", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + if len(args) < 2 { + s := hawkconfig.LoadGlobalSettings() + ln := false + messages := fmt.Sprintf("Pager Configuration:\n lines: %d (0 = unlimited)\n linenumbers: %v\n\nUsage: /pager-config ", s.PaginatorLines, ln) + if s.PaginatorShowLineNums != nil { + messages = fmt.Sprintf("Pager Configuration:\n lines: %d (0 = unlimited)\n linenumbers: %v\n\nUsage: /pager-config ", s.PaginatorLines, *s.PaginatorShowLineNums) + } + m.messages = append(m.messages, displayMsg{role: "system", content: messages}) + return m, nil + } + subcmd := strings.ToLower(args[0]) + value := args[1] + switch subcmd { + case "lines": + lines, err := strconv.Atoi(value) + if err != nil || lines < 0 { + m.messages = append(m.messages, displayMsg{role: "error", content: "Lines must be a positive number (0 = unlimited)"}) + return m, nil + } + if err := hawkconfig.SetGlobalSetting("paginatorlines", value); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: fmt.Sprintf("Pager lines → %d", lines)}) + } + return m, nil + case "linenumbers", "linenums", "ln": + switch strings.ToLower(value) { + case "1", "true", "yes", "on": + if err := hawkconfig.SetGlobalSetting("paginatorshowlinenumbers", "true"); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: "Pager line numbers → enabled"}) + } + case "0", "false", "no", "off": + if err := hawkconfig.SetGlobalSetting("paginatorshowlinenumbers", "false"); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: err.Error()}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: "Pager line numbers → disabled"}) + } + default: + m.messages = append(m.messages, displayMsg{role: "error", content: "Valid values: true, false"}) + } + return m, nil + default: + m.messages = append(m.messages, displayMsg{role: "error", content: fmt.Sprintf("Unknown option %q. Use: lines, linenumbers", subcmd)}) + return m, nil + } + }, + }) + // /plugins — list installed plugins subcommandRegistry.Register(&delegatingCommand{ name: "plugins", @@ -388,6 +570,115 @@ func init() { }, }) + // /announcements — show system announcements + subcommandRegistry.Register(&delegatingCommand{ + name: "announcements", + description: "show system announcements (release notes, updates)", + usage: "", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + state, err := tool.ReadAnnouncements() + if err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: fmt.Sprintf("Failed to read announcements: %v", err)}) + return m, nil + } + visible := tool.VisibleAnnouncements(nil, state.HiddenIDs) + if len(visible) == 0 { + m.messages = append(m.messages, displayMsg{role: "system", content: "No announcements."}) + } else { + var b strings.Builder + b.WriteString("Announcements:\n") + for i, a := range visible { + if a.Title != "" { + b.WriteString(fmt.Sprintf(" [%d] %s\n", i+1, a.Title)) + } + b.WriteString(fmt.Sprintf(" %s\n", a.Message)) + if a.CTA != nil && a.CTA.URL != "" { + b.WriteString(fmt.Sprintf(" → %s (%s)\n", a.CTA.Label, a.CTA.URL)) + } + } + m.messages = append(m.messages, displayMsg{role: "system", content: b.String()}) + } + return m, nil + }, + }) + + // /prompt-queue — manage queued prompts + subcommandRegistry.Register(&delegatingCommand{ + name: "prompt-queue", + aliases: []string{"queue"}, + description: "manage queued prompts (add|list|clear|remove)", + usage: "/prompt-queue [args]", + handler: func(m *chatModel, args []string, text string) (tea.Model, tea.Cmd) { + if len(args) < 1 { + m.messages = append(m.messages, displayMsg{role: "system", content: "Usage: /prompt-queue add \n /prompt-queue list\n /prompt-queue clear\n /prompt-queue remove "}) + return m, nil + } + subcmd := strings.ToLower(args[0]) + switch subcmd { + case "add", "queue": + if len(args) < 2 { + m.messages = append(m.messages, displayMsg{role: "error", content: "Usage: /prompt-queue add "}) + return m, nil + } + prompt := strings.TrimSpace(strings.TrimPrefix(text, "/prompt-queue add")) + if prompt == "" { + m.messages = append(m.messages, displayMsg{role: "error", content: "Prompt cannot be empty"}) + return m, nil + } + if err := tool.EnqueuePrompt(prompt, ""); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: fmt.Sprintf("Failed to queue prompt: %v", err)}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: "Prompt queued."}) + } + return m, nil + case "list", "ls": + items := tool.GetPromptQueue() + if len(items) == 0 { + m.messages = append(m.messages, displayMsg{role: "system", content: "Queue is empty."}) + return m, nil + } + var b strings.Builder + b.WriteString(fmt.Sprintf("Prompt queue (%d items):\n", len(items))) + for i, item := range items { + if item.Subject != "" { + b.WriteString(fmt.Sprintf(" [%d] %s\n", i+1, item.Subject)) + } else { + preview := truncatePromptPreview(item.Prompt, 50) + b.WriteString(fmt.Sprintf(" [%d] %s\n", i+1, preview)) + } + } + m.messages = append(m.messages, displayMsg{role: "system", content: b.String()}) + return m, nil + case "clear": + if err := tool.ClearPromptQueue(); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: fmt.Sprintf("Failed to clear queue: %v", err)}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: "Queue cleared."}) + } + return m, nil + case "remove", "rm": + if len(args) < 2 { + m.messages = append(m.messages, displayMsg{role: "error", content: "Usage: /prompt-queue remove "}) + return m, nil + } + idx, err := strconv.Atoi(args[1]) + if err != nil || idx < 1 { + m.messages = append(m.messages, displayMsg{role: "error", content: "Invalid index"}) + return m, nil + } + if err := tool.RemovePromptFromQueue(idx - 1); err != nil { + m.messages = append(m.messages, displayMsg{role: "error", content: fmt.Sprintf("Failed to remove: %v", err)}) + } else { + m.messages = append(m.messages, displayMsg{role: "system", content: "Removed from queue."}) + } + return m, nil + default: + m.messages = append(m.messages, displayMsg{role: "error", content: fmt.Sprintf("Unknown subcommand %q. Use: add, list, clear, remove", subcmd)}) + return m, nil + } + }, + }) + // /keybindings — show keybindings subcommandRegistry.Register(&delegatingCommand{ name: "keybindings", diff --git a/cmd/chat_tools.go b/cmd/chat_tools.go index 357e2ebe..14af11a5 100644 --- a/cmd/chat_tools.go +++ b/cmd/chat_tools.go @@ -57,6 +57,9 @@ func essentialTools() []tool.Tool { tool.TodoWriteTool{}, tool.TaskOutputTool{}, tool.TaskStopTool{}, + tool.WaitTasksTool{}, + tool.KillTaskTool{}, + tool.MonitorTool{}, tool.LSPTool{}, tool.MultiEditTool{}, } diff --git a/cmd/plugin_dynamic.go b/cmd/plugin_dynamic.go index 6b11a3de..6953e3ec 100644 --- a/cmd/plugin_dynamic.go +++ b/cmd/plugin_dynamic.go @@ -276,9 +276,28 @@ See `+"`plugin.json`"+` for the full manifest configuration. return fmt.Errorf("write README.md: %w", err) } - cmd.Printf("Created plugin scaffold at ./%s/\n", name) + // Multi-component package skeleton (PACK-05) + for _, sub := range []string{"skills", "hooks", "tools"} { + if err := os.MkdirAll(filepath.Join(dir, sub), 0o750); err != nil { + return fmt.Errorf("create %s: %w", sub, err) + } + } + skillExample := filepath.Join(dir, "skills", "example") + if err := os.MkdirAll(skillExample, 0o750); err != nil { + return err + } + // #nosec G306 + _ = os.WriteFile(filepath.Join(skillExample, "SKILL.md"), []byte("---\nname: example\ndescription: Example skill from plugin\n---\n\n# Example skill\n"), 0o644) + // #nosec G306 + _ = os.WriteFile(filepath.Join(dir, "mcp.json"), []byte("{\n \"servers\": []\n}\n"), 0o644) + + cmd.Printf("Created multi-component plugin scaffold at ./%s/\n", name) cmd.Printf(" %s/plugin.json - Plugin manifest\n", name) cmd.Printf(" %s/main.go - Plugin entrypoint\n", name) + cmd.Printf(" %s/skills/ - Bundled skills\n", name) + cmd.Printf(" %s/hooks/ - Hook scripts\n", name) + cmd.Printf(" %s/tools/ - Tool binaries\n", name) + cmd.Printf(" %s/mcp.json - MCP server specs\n", name) cmd.Printf(" %s/README.md - Documentation\n", name) cmd.Println() cmd.Printf("Next steps:\n") @@ -354,9 +373,129 @@ var pluginLogsCmd = &cobra.Command{ }, } +var pluginMarketplaceCmd = &cobra.Command{ + Use: "marketplace", + Short: "Browse and install plugins from marketplace sources", +} + +var pluginMarketplaceListCmd = &cobra.Command{ + Use: "list", + Short: "List plugins available from marketplace sources", + RunE: func(cmd *cobra.Command, _ []string) error { + mc := plugin.NewMarketplaceClient() + entries, err := mc.FetchAll() + if err != nil { + return fmt.Errorf("fetch marketplace: %w (indexes may be unpublished; add a source with hawk plugin marketplace add)", err) + } + if len(entries) == 0 { + cmd.Println("No marketplace plugins found.") + cmd.Println("Add a source: hawk plugin marketplace add ") + return nil + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) + _, _ = fmt.Fprintf(w, "NAME\tREPO\tVERSION\tDESCRIPTION\n") + for _, e := range entries { + desc := e.Description + if len(desc) > 48 { + desc = desc[:45] + "..." + } + _, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", e.Name, e.Repo, e.Version, desc) + } + return w.Flush() + }, +} + +var pluginMarketplaceInstallCmd = &cobra.Command{ + Use: "install ", + Short: "Install a plugin by marketplace name", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + mc := plugin.NewMarketplaceClient() + entry, err := mc.Find(args[0]) + if err != nil { + return err + } + dir, err := mc.Install(*entry) + if err != nil { + return err + } + cmd.Printf("Installed %s to %s\n", entry.Name, dir) + // re-discover + _ = getDynamicManager().DiscoverAll() + return nil + }, +} + +var pluginMarketplaceAddCmd = &cobra.Command{ + Use: "add ", + Short: "Add a marketplace source (JSON index URL)", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + if err := plugin.AddSource(args[0], args[1]); err != nil { + return err + } + cmd.Printf("Added marketplace source %q → %s\n", args[0], args[1]) + return nil + }, +} + +var pluginMarketplaceSourcesCmd = &cobra.Command{ + Use: "sources", + Short: "List configured marketplace sources", + RunE: func(cmd *cobra.Command, _ []string) error { + mc := plugin.NewMarketplaceClient() + for _, s := range mc.Sources { + cmd.Printf("%s\t%s\n", s.Name, s.URL) + } + return nil + }, +} + +var pluginInspectCmd = &cobra.Command{ + Use: "inspect ", + Short: "Show multi-component package contents (tools/hooks/skills/mcp)", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + dir := args[0] + if st, err := os.Stat(dir); err != nil || !st.IsDir() { + fromState := filepath.Join(plugin.PluginsStateDir(), dir) + if st, err := os.Stat(fromState); err == nil && st.IsDir() { + dir = fromState + } else { + return fmt.Errorf("plugin directory not found: %s", args[0]) + } + } + comp, err := plugin.DiscoverComponents(dir) + if err != nil { + return err + } + cmd.Printf("Root: %s\n", comp.Root) + cmd.Printf("Components: %s\n", comp.ComponentSummary()) + cmd.Printf("Tools: %v\n", comp.HasTools) + cmd.Printf("Skills (%d):\n", len(comp.Skills)) + for _, s := range comp.Skills { + cmd.Printf(" - %s\n", s) + } + cmd.Printf("Hooks (%d):\n", len(comp.HookFiles)) + for _, h := range comp.HookFiles { + cmd.Printf(" - %s\n", h) + } + cmd.Printf("MCP servers (%d):\n", len(comp.MCPServers)) + for _, m := range comp.MCPServers { + cmd.Printf(" - %s cmd=%s url=%s\n", m.Name, m.Command, m.URL) + } + return nil + }, +} + func init() { pluginStatusCmd.Flags().Bool("json", false, "output as JSON") + pluginMarketplaceCmd.AddCommand(pluginMarketplaceListCmd) + pluginMarketplaceCmd.AddCommand(pluginMarketplaceInstallCmd) + pluginMarketplaceCmd.AddCommand(pluginMarketplaceAddCmd) + pluginMarketplaceCmd.AddCommand(pluginMarketplaceSourcesCmd) + pluginCmd.AddCommand(pluginListCmd) pluginCmd.AddCommand(pluginActivateCmd) pluginCmd.AddCommand(pluginDeactivateCmd) @@ -366,6 +505,8 @@ func init() { pluginCmd.AddCommand(pluginInstallDynamicCmd) pluginCmd.AddCommand(pluginUninstallCmd) pluginCmd.AddCommand(pluginLogsCmd) + pluginCmd.AddCommand(pluginMarketplaceCmd) + pluginCmd.AddCommand(pluginInspectCmd) } // pluginInstallDynamicCmd overrides the default "install" subcommand behavior. diff --git a/cmd/theme.go b/cmd/theme.go index 82406955..c3608bac 100644 --- a/cmd/theme.go +++ b/cmd/theme.go @@ -219,13 +219,16 @@ const quitAgainMsg = "Press Ctrl+C again to quit." // themed styles. Call this at startup (from root.go) and whenever the user // selects a new theme from the picker. // -// "auto" is a no-op: lipgloss already probes the terminal background on -// its own; the caller should still call lipgloss.SetHasDarkBackground for -// the AdaptiveColor vars but the fixed-hex vars remain at their defaults. +// "auto" resolves to the OS preference (dark/light) using internaltheme.ApplyThemePreference. +// If detection fails, defaults to "dark". func ApplyTheme(name string) { - if name == "" || name == "auto" { + if name == "" { return } + // Handle "auto" theme - resolve to actual theme based on OS preference + if name == "auto" || name == "system" { + name = internaltheme.ApplyThemePreference(name) + } entry, ok := internaltheme.LookupTheme(name) if !ok { return diff --git a/cmd/theme_picker.go b/cmd/theme_picker.go index ff90cc5e..6ce210cd 100644 --- a/cmd/theme_picker.go +++ b/cmd/theme_picker.go @@ -7,9 +7,16 @@ import ( tea "charm.land/bubbletea/v2" lipgloss "charm.land/lipgloss/v2" + hawkconfig "github.com/GrayCodeAI/hawk/internal/config" internaltheme "github.com/GrayCodeAI/hawk/internal/theme" ) +// detectAutoIsDark returns whether "auto" theme currently resolves to dark. +func detectAutoIsDark() bool { + settings := hawkconfig.LoadGlobalSettings() + return settings.Theme == "auto" || settings.Theme == "system" || internaltheme.DetectOSTheme() != "light" +} + // ThemeChoice represents a visual theme option. type ThemeChoice struct { Name string @@ -38,8 +45,8 @@ func buildThemeChoices() []ThemeChoice { IsDark: e.IsDark, }) } - // Add "auto" at the end — lets lipgloss detect the terminal background. - out = append(out, ThemeChoice{Name: "auto", Desc: "Auto-detect terminal background", IsDark: true}) + // Add "auto" at the end — follows OS appearance (system dark/light preference) + out = append(out, ThemeChoice{Name: "auto", Desc: "Follow system appearance (auto dark/light)", IsDark: detectAutoIsDark()}) return out } @@ -169,7 +176,51 @@ func (tp *ThemePicker) View() tea.View { } } + // Add live preview for selected theme + b.WriteString("\n") + b.WriteString(hintStyle.Render(" Preview:") + "\n") + b.WriteString(renderThemePreview(tp.entries[tp.sel].Name) + "\n") + v := tea.View{Content: b.String()} v.AltScreen = true return v } + +// renderThemePreview renders a visual preview of the selected theme. +func renderThemePreview(themeName string) string { + var preview strings.Builder + + // Handle auto theme specially + if themeName == "auto" { + preview.WriteString(fmt.Sprintf(" Panel: %s dark\n", lipgloss.NewStyle().Background(lipgloss.Color("#0e0e10")).Render(" "))) + preview.WriteString(fmt.Sprintf(" Accent: %s orange\n", lipgloss.NewStyle().Background(lipgloss.Color("#FF5E0E")).Render(" "))) + return preview.String() + } + + entry, ok := internaltheme.LookupTheme(themeName) + if !ok { + return " Theme not found" + } + p := entry.Palette + + if p.Panel != "" { + preview.WriteString(fmt.Sprintf(" Panel: %s\n", lipgloss.NewStyle().Background(lipgloss.Color(p.Panel)).Render(" "))) + } + if p.PromptBg != "" { + preview.WriteString(fmt.Sprintf(" Prompt: %s\n", lipgloss.NewStyle().Background(lipgloss.Color(p.PromptBg)).Render(" "))) + } + if p.Accent != "" { + preview.WriteString(fmt.Sprintf(" Accent: %s\n", lipgloss.NewStyle().Background(lipgloss.Color(p.Accent)).Render(" "))) + } + if p.Ink != "" { + preview.WriteString(fmt.Sprintf(" Text: %s\n", lipgloss.NewStyle().Background(lipgloss.Color(p.Ink)).Render(" "))) + } + if p.Green != "" { + preview.WriteString(fmt.Sprintf(" Green: %s\n", lipgloss.NewStyle().Background(lipgloss.Color(p.Green)).Render(" "))) + } + if p.Red != "" { + preview.WriteString(fmt.Sprintf(" Red: %s\n", lipgloss.NewStyle().Background(lipgloss.Color(p.Red)).Render(" "))) + } + + return preview.String() +} diff --git a/cmd/trust.go b/cmd/trust.go new file mode 100644 index 00000000..6a22dae9 --- /dev/null +++ b/cmd/trust.go @@ -0,0 +1,142 @@ +package cmd + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/GrayCodeAI/hawk/internal/flags" + "github.com/GrayCodeAI/hawk/internal/trust" + "github.com/spf13/cobra" +) + +var trustCmd = &cobra.Command{ + Use: "trust", + Short: "Manage folder trust for project automation", + Long: `Folder trust controls whether project-scoped hooks, MCP servers, LSP +configs, and plugins may load from a repository. + +When HAWK_Y0_FOLDER_TRUST is enabled (default after Year 0 PACK-03), +untrusted projects cannot run project automation (RCE mitigation). + +User-global plugins under the Hawk state directory always load.`, +} + +var trustAddCmd = &cobra.Command{ + Use: "add [path]", + Short: "Trust a project directory (default: cwd)", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := "" + if len(args) > 0 { + path = args[0] + } else { + var err error + path, err = os.Getwd() + if err != nil { + return err + } + } + s, err := trust.Open("") + if err != nil { + return err + } + reason, _ := cmd.Flags().GetString("reason") + if err := s.Trust(path, reason); err != nil { + return err + } + cmd.Printf("Trusted %s\n", path) + return nil + }, +} + +var trustRemoveCmd = &cobra.Command{ + Use: "remove [path]", + Short: "Remove trust for a project directory (default: cwd)", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := "" + if len(args) > 0 { + path = args[0] + } else { + var err error + path, err = os.Getwd() + if err != nil { + return err + } + } + s, err := trust.Open("") + if err != nil { + return err + } + if err := s.Untrust(path); err != nil { + return err + } + cmd.Printf("Removed trust for %s\n", path) + return nil + }, +} + +var trustListCmd = &cobra.Command{ + Use: "list", + Short: "List trusted directories", + RunE: func(cmd *cobra.Command, _ []string) error { + s, err := trust.Open("") + if err != nil { + return err + } + entries := s.List() + if len(entries) == 0 { + cmd.Println("No trusted directories.") + cmd.Printf("Folder trust enforcement: %v (HAWK_Y0_FOLDER_TRUST)\n", flags.FolderTrust()) + return nil + } + w := tabwriter.NewWriter(cmd.OutOrStdout(), 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "PATH\tTRUSTED_AT\tREASON") + for _, e := range entries { + fmt.Fprintf(w, "%s\t%s\t%s\n", e.Path, e.TrustedAt.Format("2006-01-02 15:04"), e.Reason) + } + _ = w.Flush() + return nil + }, +} + +var trustCheckCmd = &cobra.Command{ + Use: "check [path]", + Short: "Check whether a path is trusted (default: cwd)", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + path := "" + if len(args) > 0 { + path = args[0] + } else { + var err error + path, err = os.Getwd() + if err != nil { + return err + } + } + s, err := trust.Open("") + if err != nil { + return err + } + enforced := flags.FolderTrust() + trusted := s.IsTrusted(path) + cmd.Printf("path: %s\n", path) + cmd.Printf("trusted: %v\n", trusted) + cmd.Printf("enforcement: %v\n", enforced) + if enforced && !trusted { + return fmt.Errorf("not trusted") + } + return nil + }, +} + +func init() { + trustAddCmd.Flags().String("reason", "", "Optional reason recorded in the trust store") + trustCmd.AddCommand(trustAddCmd) + trustCmd.AddCommand(trustRemoveCmd) + trustCmd.AddCommand(trustListCmd) + trustCmd.AddCommand(trustCheckCmd) + rootCmd.AddCommand(trustCmd) +} diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 2318d2e7..ab52ec5a 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -19,6 +19,8 @@ Documents: - `hawk-architecture-v1-definition-of-done.md` - realistic shipping bar for architecture v1 - `tasks.md` - historical implementation checklist from the initial architecture pass (superseded by the definition-of-done doc; kept for record) - `adr/` - accepted architecture decision records, e.g. exceptions to the dependency rules above + - `ADR-0003-grok-behavioral-port-go-multirepo.md` - Year 0 Grok behavioral port keeps Go multi-repo +- Related active execution track: `docs/plans/YEAR-0-ACTIVE.md` Core rule: diff --git a/docs/architecture/adr/ADR-0003-grok-behavioral-port-go-multirepo.md b/docs/architecture/adr/ADR-0003-grok-behavioral-port-go-multirepo.md new file mode 100644 index 00000000..735c42c3 --- /dev/null +++ b/docs/architecture/adr/ADR-0003-grok-behavioral-port-go-multirepo.md @@ -0,0 +1,59 @@ +# ADR-0003: Grok behavioral port keeps Go multi-repo Hawk + +- Status: Accepted +- Date: 2026-07-16 +- Owners: Hawk maintainers +- Related: `docs/plans/FULL-GROK-ECO-TO-HAWK-ECO-PORT-PLAN.md`, + `docs/plans/GROK-CLASS-CAPABILITY-LONG-HORIZON-PLAN.md`, + `docs/plans/YEAR-0-ACTIVE.md` + +## Context + +Grok Build (`grok-eco/grok-build`) is a large Rust monorepo for a terminal AI +coding agent. Hawk is a multi-repo Go platform: product (`hawk`) plus peer +engines (`eyrie`, `yaad`, `tok`, `trace`, `sight`, `inspect`), foundation +contracts (`hawk-core-contracts`, `hawk-mcpkit`), SDKs, cloud, and skills. + +The product goal is Grok-class **agent control-plane quality** (typed +subagents, sandbox profiles, folder trust, hooks, plugins/marketplace, task +runtime, plan UX) without abandoning Hawk’s multi-provider, local-first, and +peer-engine advantages. + +## Decision + +1. **Port means behavioral reimplementation in Go**, not a Rust crate copy, + not a monorepo collapse, and not a runtime dependency on Grok binaries. +2. **Hawk remains multi-repo.** Map Grok capabilities onto existing owners: + - product surface → `hawk` + - shared DTOs → `hawk-core-contracts` + - LLM routing/stream → `eyrie` + - memory → `yaad` + - tokens/secrets/compress → `tok` + - session capture/import → `trace` + - review / live audit → `sight` / `inspect` + - marketplace content → `hawk-community-skills` + - managed policy/tenancy → `hawk-cloud` +3. **Wire-first.** Prefer completing types and paths that already exist + (for example typed subagent modes) over greenfield rewrites. +4. **Privacy-first.** Do not port default Mixpanel/product analytics. Opt-in + OTEL remains the telemetry model. +5. **Typed spawn is the only subagent entrypoint** once Year 0 spawn work + lands. `WireAgentTool` must not hardcode a single mode forever. +6. **Order is non-negotiable:** contracts → spawn/taskruntime → folder trust + + sandbox profiles → hooks-first permissions → plugins/marketplace → + monitor/wait/loop and UX batch. No project marketplace auto-load before + folder trust. +7. **Year 0 active track** (control plane, trust, hooks, plugins, tasks/UX, + user-guide 01–12) is the current execution program. Full TUI parity, + computer hub, and deep enterprise policy are Year 1+. + +## Consequences + +- New shared agent spawn types live in `hawk-core-contracts/agent` (stdlib + only). Engines and SDKs may import them; contracts never import hawk. +- Feature work updates the matrices in the full/long-horizon plans rather + than inventing parallel roadmaps. +- Skip lists stay explicit: Ratatui widgets, Mixpanel defaults, Grok-only + auth as sole path, computer hub until product decision. +- Contributors measure progress against Year 0 exit criteria in + `docs/plans/YEAR-0-ACTIVE.md`. diff --git a/docs/plans/FULL-GROK-ECO-TO-HAWK-ECO-PORT-PLAN.md b/docs/plans/FULL-GROK-ECO-TO-HAWK-ECO-PORT-PLAN.md index 1afdfe96..4f9bf95a 100644 --- a/docs/plans/FULL-GROK-ECO-TO-HAWK-ECO-PORT-PLAN.md +++ b/docs/plans/FULL-GROK-ECO-TO-HAWK-ECO-PORT-PLAN.md @@ -2,6 +2,8 @@ **Status:** Master long-term plan **Date:** 2026-07-16 +**Active execution:** [YEAR-0-ACTIVE.md](./YEAR-0-ACTIVE.md) (Year 0 control-plane track) +**ADR:** [ADR-0003](../architecture/adr/ADR-0003-grok-behavioral-port-go-multirepo.md) **Meaning of “port”:** Reimplement **every Grok Build capability** in idiomatic **Go** across hawk-eco repos. **Not meaning:** Copy Rust crates, depend on Grok binaries, or collapse hawk into a Rust monorepo. @@ -530,7 +532,7 @@ Only if product requires remote tool hosts: ### PACK-16: Docs full port (parallel) -- [ ] `docs/user-guide/01` … `24` +- [x] `docs/user-guide/01` … `24` (completed July 2026) - [ ] Architecture notes stay separate ### PACK-17: Continuous parity (forever) diff --git a/docs/plans/Y0-CALL-SITE-INVENTORY.md b/docs/plans/Y0-CALL-SITE-INVENTORY.md new file mode 100644 index 00000000..7f66737e --- /dev/null +++ b/docs/plans/Y0-CALL-SITE-INVENTORY.md @@ -0,0 +1,77 @@ +# Year 0 Call-Site Inventory + +**Date:** 2026-07-16 +**Purpose:** Freeze entry points before PACK-02 spawn and taskruntime work. +**Rule:** Do not add a fourth background agent system. + +## 1. Agent spawn + +| Location | Role | Today | +|----------|------|--------| +| `internal/tool/tool.go` | `ToolContext.AgentSpawnFn` | **Updated:** `func(ctx, SpawnRequest) (SpawnResult, error)` | +| `internal/engine/agent_session_tool.go` | `WireAgentTool` | **Updated:** typed spawn; maps explore/plan/general | +| `internal/engine/agent_session_tool.go` | `spawnSubAgent` | Uses Normalized + mode; plan tools filter | +| `internal/tool/agent.go` | `Agent` tool | Schema: type, capability, isolation, thoroughness, cwd, model, resume, bg | +| `internal/tool/agent.go` | `MultiAgent` | String tasks + typed object tasks | +| `internal/tool/agentic_fetch.go` | Research spawn | Uses `AgentSpawnFn(prompt)` | +| `internal/tool/agent*_test.go` | Unit tests | Mock prompt-only spawn | + +**Target (PACK-02):** `AgentSpawnFn(ctx, agent.SpawnRequest) (agent.SpawnResult, error)` from +`hawk-core-contracts/agent`, with adapter only if dual-path flag requires it. + +## 2. Background / task systems (unify → one) + +| System | Location | Role | +|--------|----------|------| +| `BackgroundAgentManager` | `internal/tool/background.go` | Sub-agent bg spawn + collect by id | +| `BackgroundRunner` | search under `internal/engine/` | Engine-level bg runs | +| `BackgroundAgentPool` | search under `internal/engine/agent/` | Pool for multi-agent | + +**Target (PACK-02):** single `internal/taskruntime` (or equivalent) registry; +Wait/Kill/Monitor tools (PACK-06) bind only to that registry. + +## 3. Mode / budget libraries (keep, wire) + +| Location | Role | +|----------|------| +| `internal/engine/agent/agent_types.go` | explore / general / plan modes | +| `internal/engine/agent/subagent_budget.go` | tool allowlists + turn budgets | +| `internal/tool/bash_ast.go` | bash AST helpers for explore hard gate | + +## 4. Permission / hooks / plugins (later packs) + +| Location | Role | Y0 pack | +|----------|------|---------| +| `internal/engine/safety/permission_engine.go` (or permissions package) | CheckTool pipeline | PACK-03/04 | +| `internal/hooks/` | Hook registry/events | PACK-04 | +| `internal/plugin/` | Plugin manager V1/V2 | PACK-05 | +| `internal/sandbox/` | OS backends; modes | PACK-03 | + +## 5. Feature flags + +| Flag env | Package | Pack | +|----------|---------|------| +| `HAWK_Y0_SPAWN_V2` | `internal/flags` | PACK-02 | +| `HAWK_Y0_FOLDER_TRUST` | `internal/flags` | PACK-03 | +| `HAWK_Y0_MARKETPLACE` | `internal/flags` | PACK-05 | + +## 6. Spawn test matrix template (PACK-02) + +| subagent_type | capability | isolation | background | Expected | +|---------------|------------|-----------|------------|----------| +| explore | read-only (default) | none | false | No Write/Edit; bash AST gate | +| explore | read-only | worktree | false | Worktree cwd; read-only tools | +| plan | read-only | none | false | Plan tools only; no Write | +| general-purpose | all | none | false | Full tools | +| general-purpose | execute | worktree | true | Task id; killable; worktree | +| explore | — | none | false + resume_from | Continues transcript | + +Cases must run under `go test` (+ race on taskruntime). + +## 7. Dependency freeze + +Until PACK-02 taskruntime cutover: + +- [x] Document three bg systems +- [ ] No new background manager type without replacing an existing one +- [ ] All new spawn call sites take `SpawnRequest` diff --git a/docs/plans/YEAR-0-ACTIVE.md b/docs/plans/YEAR-0-ACTIVE.md new file mode 100644 index 00000000..5ccc0914 --- /dev/null +++ b/docs/plans/YEAR-0-ACTIVE.md @@ -0,0 +1,74 @@ +# Year 0 Active Track (Grok → Hawk) + +**Status:** Active +**Date:** 2026-07-16 +**ADR:** [ADR-0003](../architecture/adr/ADR-0003-grok-behavioral-port-go-multirepo.md) +**Full matrices:** [FULL-GROK-ECO-TO-HAWK-ECO-PORT-PLAN.md](./FULL-GROK-ECO-TO-HAWK-ECO-PORT-PLAN.md), +[GROK-CLASS-CAPABILITY-LONG-HORIZON-PLAN.md](./GROK-CLASS-CAPABILITY-LONG-HORIZON-PLAN.md) +**Call-site inventory:** [Y0-CALL-SITE-INVENTORY.md](./Y0-CALL-SITE-INVENTORY.md) + +This is the **executable Year 0 program**. It does not replace the full +port matrices; it freezes what “Year 0 done” means and tracks pack status. + +## Port meaning + +| Do | Do not | +|----|--------| +| Reimplement Grok **behavior** in Go | Copy Rust crates or depend on Grok | +| Map capabilities to hawk-eco repos | Collapse engines into hawk monorepo | +| Wire existing modes/budgets first | Rebuild eyrie/yaad/tok as Grok clones | +| Privacy-first telemetry (OTEL opt-in) | Port Mixpanel defaults | + +## Pack status + +| Pack | Scope | Status | +|------|--------|--------| +| PACK-00 | ADR, inventory, flags, test matrix template | **Done** (2026-07-16) | +| PACK-01 | `hawk-core-contracts/agent` spawn DTOs | **Done** (v0.1.6) | +| PACK-02 | Typed spawn + Agent tool + taskruntime unify | **Mostly done** — typed Agent tool, explore bash hard gate, taskruntime registry, worktree isolation; true transcript resume still stub | +| PACK-03 | sandbox.toml + folder trust + safe-bash | **Partial** — folder trust + sandbox.toml + project plugin/hook gates; named acceptEdits modes / safe-bash product polish remain | +| PACK-04 | Hooks complete + PreToolUse in PermissionEngine | **Done** — PreToolUse deny-before-autonomy, vendor aliases, HTTP hooks, discovery dirs, plugin env, acceptEdits/dontAsk aliases | +| PACK-05 | Multi-component plugins + marketplace MVP | **Done** — components layout, scopes, marketplace CLI, multi-harness skills trust gate | +| PACK-06 | Monitor / Wait / Kill / `/loop` | **Partial** — TaskOutput, WaitTasks, KillTask, Monitor tools implemented; `/loop` command works; unified taskruntime bridge exists | +| PACK-07 | Structured AskUser + plan/spec align | **Partial** — AskUserQuestion tool enhanced with options; `/btw` (interjection) implemented; plan mode in progress | +| PACK-08 | Crash, announcements, prompt queue, interjection | **Partial** — crash_handler exists in errors.go; `/btw` (interjection) implemented; announcements and prompt queue missing | +| Docs 01–24 | `docs/user-guide/` | **Done** (completed July 2026) | + +## Year 0 exit criteria + +- [x] Model can spawn `explore` \| `plan` \| `general-purpose` via Agent tool schema (isolation=worktree works; resume still stub) +- [x] Explore bash cannot mutate (`ExploreBashAllowed` segment allowlist + `ReadOnlyBash` on subagents) +- [x] Unified agent taskruntime (`internal/taskruntime`; shell TaskOutput merge is PACK-06) +- [x] Folder trust gates project hooks / plugins (`.hawk/plugins`, `.hawk/hooks`; MCP/LSP follow same AllowLoadPath) +- [x] `sandbox.toml` profiles + project additive merge; deny globs fail-closed +- [x] PreToolUse hooks can deny inside `PermissionEngine` before autonomy +- [x] Multi-component plugins + marketplace MVP install path +- [x] Monitor + Wait/Kill + `/loop` tools implemented and working +- [~] Crash handler, announcements, prompt queue, interjection — crash handler exists in cmd/errors.go; `/btw` (interjection) implemented; announcements and prompt queue still missing +- [x] User-guide docs `01`–`24` under `hawk/docs/user-guide/` (completed July 2026) +- [x] ADR-0003 published +- [x] PACK-00 inventory + flags + spawn matrix template complete + +## Explicit deferrals (not Year 0 exit) + +ACP phase-2 depth, full SDK spawn surface, signed enterprise policy E2E, +foreign session import, hunk tracker / CoW worktrees, mermaid/media/video, +computer hub, full slash/TUI pixel parity, Mixpanel. + +## Feature flags (env) + +| Env | Default | Purpose | +|-----|---------|---------| +| `HAWK_Y0_SPAWN_V2` | `1` once PACK-02 ships; `0` during dual path | Typed SpawnRequest path | +| `HAWK_Y0_FOLDER_TRUST` | `1` recommended after PACK-03 | Gate project automation | +| `HAWK_Y0_MARKETPLACE` | `0` until PACK-05 + trust | Marketplace install path | + +Implementation: `internal/flags/y0.go`. + +## Order rule + +```text +contracts → spawn/taskruntime → trust/sandbox → hooks-first → plugins → tasks/UX +``` + +Do not reverse. No marketplace auto-load before folder trust. diff --git a/docs/user-guide/01-getting-started.md b/docs/user-guide/01-getting-started.md new file mode 100644 index 00000000..f261569c --- /dev/null +++ b/docs/user-guide/01-getting-started.md @@ -0,0 +1,228 @@ +# Getting Started + +Hawk is an AI-powered coding agent for your terminal, built for developers by GrayCode AI. It understands your codebase, executes shell commands, edits files, searches the web, and manages tasks — all through natural language. + +You can use Hawk interactively as a full-screen TUI, run it headlessly for scripting and CI/CD, or integrate it into editors via the Agent Client Protocol (ACP). + +--- + +## Installation + +Hawk is currently in active development. Contributor source builds are the primary path while we harden the product in the open. + +### From Source (Recommended for Contributors) + +```bash +git clone https://github.com/GrayCodeAI/hawk && cd hawk +make setup # clones required support repos into external/ and syncs go.work +go build -o hawk ./cmd/hawk +./hawk +``` + +### Verification + +Run the developer path check to verify your setup: + +```bash +./hawk path +``` + +This checks setup, security, and sandbox readiness. + +--- + +## First Launch + +Start Hawk by running: + +```bash +hawk +``` + +On first launch, Hawk opens a TUI where you can configure credentials. Press `/config` (or `/autonomy`) to open the configuration picker. Your API keys are stored in your OS keychain (macOS Keychain or Linux keyring), never in plain text. + +Hawk supports multiple providers: + +- **xAI Grok** — `XAI_API_KEY` +- **Anthropic Claude** — `ANTHROPIC_API_KEY` +- **OpenAI** — `OPENAI_API_KEY` +- **Google Gemini** — `GEMINI_API_KEY` +- **OpenRouter** — `OPENROUTER_API_KEY` + +See [Authentication](02-authentication.md) for the full set of auth options including OIDC and device code flow. + +--- + +## Basic Interaction + +Once authenticated, Hawk presents a full-screen TUI powered by Bubble Tea with two main areas: + +- **Scrollback** — the conversation history showing your prompts, Hawk's responses, tool calls, file edits, and more +- **Prompt** — the input area at the bottom where you type messages + +Type a message and press `Enter` to send it. Hawk reads files, runs commands, and edits code as needed. Each tool run streams into the scrollback in real time. + +Press `Tab` to move focus between the prompt and the scrollback. While a turn is running, `Ctrl+C` cancels it. In Vim mode, use `j`/`k` to navigate and `h`/`l` to collapse/expand entries. + +### File References + +Use `@` in your prompt to attach files: + +``` +@src/main.go # Attach a file +@src/main.go:10-50 # Attach lines 10-50 +@src/ # Browse a directory +``` + +The `@` operator opens a fuzzy file picker. By default it respects `.gitignore` and hides dotfiles. + +### Permissions + +Hawk exposes two independent control surfaces: + +- **`/autonomy`** — Controls trust tier (Always Ask, Scout, Builder, Operator, Autonomous) and sandbox profile (strict, workspace, off) +- **`/spec`** — A workflow gate that blocks Write/Edit/Bash until you approve implementation + +Example: + +``` +/autonomy tier builder +/autonomy sandbox workspace +/autonomy allow Bash(git:*) +/autonomy deny Bash(rm -rf *) +/autonomy save project +``` + +--- + +## Key Concepts + +### Sessions + +Every conversation is a **session**. Sessions are automatically saved and can be resumed later. Each session tracks the full conversation history, tool calls, file edits, and task state. + +- Start a new session: `Ctrl+N` or `/new` +- Resume a previous session: `/resume` in the TUI, or `--resume ` from the CLI +- Continue the most recent session: `hawk -c` + +### Scrollback + +The scrollback shows: + +- **User prompts** — your messages, rendered as sticky headers +- **Agent messages** — Hawk's responses with markdown rendering +- **Thinking blocks** — Hawk's reasoning process (collapsible) +- **Tool calls** — file edits, command executions, search results +- **Task lists** — TODO items tracking progress + +### Tools + +Hawk has built-in tools for: + +| Tool | Description | +|------|-------------| +| `Read` / `Write` / `Edit` | Read and edit files | +| `Grep` | Regex search across your codebase | +| `LS` / `Glob` | List and find files | +| `Bash` | Execute shell commands | +| `WebFetch` / `WebSearch` | Search the web and fetch URLs | +| `TodoWrite` | Create and manage task lists | +| `Agent` | Spawn parallel subagent sessions | + +### Slash Commands + +Type `/` in the prompt to access commands. These provide quick actions without writing a full prompt: + +``` +/autonomy # Open autonomy picker +/spec # Start spec workflow +/compact # Compress conversation history +/model grok-build # Switch model +/new # Start a new session +/resume # Resume a session +``` + +See [Slash Commands](04-slash-commands.md) for the complete reference. + +--- + +## Common Launch Options + +```bash +# Start the interactive TUI +hawk + +# Submit an initial prompt as the first turn +hawk "fix the failing auth test and run it" + +# Start in a specific project directory +hawk --cwd ~/projects/my-app + +# Resume a previous session +hawk -r abc123 + +# Continue the most recent session +hawk -c + +# Use a specific provider/model +hawk --provider openai --model gpt-4o + +# Isolated worktree for changes +hawk --worktree "refactor module X" + +# Non-interactive (headless) mode +hawk -p "Explain this codebase" + +# Full auto mode +hawk exec --auto full "add error handling" + +# Dry-run mode (denies all tools) +hawk exec --autonomy dry-run "What would this do?" +``` + +--- + +## Headless Mode + +Run Hawk non-interactively for scripting, CI/CD, and automation: + +```bash +hawk -p "Your prompt here" +``` + +Output formats: + +| Format | Flag | Description | +|--------|------|-------------| +| `plain` | (default) | Human-readable text | +| `json` | `--output-format json` | Single JSON object with response | +| `streaming-json` | `--output-format streaming-json` | NDJSON event stream | + +--- + +## Project Rules (AGENTS.md) + +Add per-project instructions by creating an `AGENTS.md` file in your repository. Hawk reads these files and injects their contents as a project-instructions message at the start of the conversation: + +``` +~/.hawk/AGENTS.md # Global rules (apply to all projects) +/AGENTS.md # Repository-level rules +/AGENTS.md # Directory-level rules (highest priority) +``` + +Deeper files take precedence. Hawk also reads `CLAUDE.md` files for compatibility. + +--- + +## Where to Go Next + +| Document | What You Will Learn | +|----------|-------------------| +| [Authentication](02-authentication.md) | Browser login, API keys, OIDC, external auth | +| [Keyboard Shortcuts](03-keyboard-shortcuts.md) | Complete reference for all key bindings | +| [Slash Commands](04-slash-commands.md) | All available `/` commands | +| [Configuration](05-configuration.md) | Settings, sandbox profiles, environment variables | + +--- + +© 2026 GrayCode AI. All rights reserved. \ No newline at end of file diff --git a/docs/user-guide/02-authentication.md b/docs/user-guide/02-authentication.md new file mode 100644 index 00000000..2ef5e50d --- /dev/null +++ b/docs/user-guide/02-authentication.md @@ -0,0 +1,231 @@ +# Authentication + +Hawk supports several authentication methods, including API key configuration through the TUI and multi-provider support via Eyrie. + +--- + +## API Key Configuration + +On first launch, Hawk opens the TUI where you can configure credentials. Press `/config` or `/autonomy` to open the configuration picker. Your API keys are stored in your OS keychain (macOS Keychain or Linux keyring), never in plain text or environment variables. + +Hawk supports multiple providers: + +| Provider | ID | Key | +|----------|----|-----| +| xAI Grok | `grok` | `XAI_API_KEY` | +| Anthropic Claude | `anthropic` | `ANTHROPIC_API_KEY` | +| OpenAI | `openai` | `OPENAI_API_KEY` | +| Google Gemini | `gemini` | `GEMINI_API_KEY` | +| OpenRouter | `openrouter` | `OPENROUTER_API_KEY` | +| Ollama (local) | `ollama` | `OLLAMA_BASE_URL` (no API key) | + +### Setting Up Credentials + +```bash +# Verify credential status +hawk credentials status + +# Run the TUI and use /config to set keys interactively +hawk +``` + +### Environment Variables (Fallback) + +For CI/CD or headless environments, you can set API keys as environment variables. These serve as fallback when no keychain entry exists: + +```bash +export XAI_API_KEY="xai-..." +hawk +``` + +--- + +## Provider Configuration + +Hawk uses Eyrie for provider routing, health checks, and retry logic. To configure providers: + +```bash +# In the TUI, press /config to open provider settings +hawk + +# Or validate readiness +hawk path +``` + +### Deployment-Aware Routing + +For deployment-aware routing, set in `.hawk/settings.json`: + +```json +{ + "deployment_routing": true +} +``` + +Or export: + +```bash +export HAWK_DEPLOYMENT_ROUTING=true +``` + +Hawk will route canonical model IDs through Eyrie's deployment catalog. Refresh the catalog with: + +``` +/refresh-model-catalog +``` + +--- + +## OIDC (Customer SSO) + +Authenticate developers through your own Identity Provider (IdP) — such as Okta, Azure AD, or Auth0 — instead of a single vendor. + +### Configure via Settings + +```json +// .hawk/settings.json +{ + "oidc": { + "issuer": "https://acme.okta.com", + "client_id": "0oa1b2c3d4e5f6g7h8i9" + } +} +``` + +Hawk discovers endpoints via `{issuer}/.well-known/openid-configuration`, opens the IdP login page, and stores tokens in the keychain. Tokens auto-refresh silently via the stored `refresh_token`. + +### Required Scopes + +- `openid` +- `profile` +- `email` +- `offline_access` (enables silent token refresh) + +--- + +## External Auth Provider + +When browser-based login isn't possible — for example, on sandboxed VMs, CI runners, or air-gapped networks — delegate authentication to an external binary or script. + +### How It Works + +1. Hawk runs your command via `sh -c ""` +2. Your binary runs whatever auth flow it needs +3. **stdout** is captured and parsed as an access token +4. **stderr** carries human-readable output surfaced to the user +5. Exit 0 = success; exit non-zero = falls back to TUI config + +### Token Format + +Bare string (just the raw token): + +``` +eyJhbGciOiJSUzI1NiIs... +``` + +JSON with optional refresh token: + +```json +{"access_token": "eyJhbGciOi...", "refresh_token": "ref-tok", "expires_in": 3600} +``` + +### Configuration + +```json +// .hawk/settings.json +{ + "auth_provider_command": "/usr/local/bin/my-auth-provider", + "auth_provider_label": "Acme Corp" +} +``` + +Or via environment variables: + +```bash +export HAWK_AUTH_PROVIDER_COMMAND="/usr/local/bin/my-auth-provider" +export HAWK_AUTH_PROVIDER_LABEL="Acme Corp" +``` + +### Token Refresh + +When Hawk needs to refresh an expired token, it re-runs your binary with `HAWK_AUTH_EXPIRED=1` set in the environment: + +```bash +#!/bin/sh +if [ "$HAWK_AUTH_EXPIRED" = "1" ]; then + echo "Refreshing token..." >&2 + TOKEN=$(my-company-auth --refresh --silent) +else + echo "Authenticating via Acme Corp SSO..." >&2 + TOKEN=$(my-company-auth --login --interactive) +fi + +if [ -z "$TOKEN" ]; then + echo "Authentication failed" >&2 + exit 1 +fi + +echo "{\"access_token\": \"$TOKEN\", \"expires_in\": 3600}" +``` + +--- + +## Credential Status + +Check credential status at any time: + +```bash +hawk credentials status +``` + +This verifies keychain entries and validates Eyrie's provider status. + +--- + +## Credential Precedence + +Hawk resolves credentials in this order: + +1. **Per-model configuration** — set via `/config` or settings +2. **Keychain entry** — obtained through TUI configuration +3. **Environment variable** — fallback (e.g., `XAI_API_KEY`) + +During a session, the active method handles all refreshes. + +--- + +## Multi-Provider Support + +Hawk works with any LLM provider through Eyrie's adapter system: + +| Provider | Status | +|----------|--------| +| Anthropic | Full support | +| OpenAI | Full support | +| Google Gemini | Full support | +| DeepSeek | Full support | +| Ollama (local) | Full support | +| OpenRouter | Full support | + +Any provider can be set as default in your settings: + +```json +{ + "default_provider": "openai", + "default_model": "gpt-4o" +} +``` + +--- + +## Where to Go Next + +| Document | What You Will Learn | +|----------|-------------------| +| [Keyboard Shortcuts](03-keyboard-shortcuts.md) | Key bindings for the TUI | +| [Slash Commands](04-slash-commands.md) | Available `/` commands | +| [Configuration](05-configuration.md) | Settings and sandbox profiles | + +--- + +© 2026 GrayCode AI. All rights reserved. \ No newline at end of file diff --git a/docs/user-guide/03-keyboard-shortcuts.md b/docs/user-guide/03-keyboard-shortcuts.md new file mode 100644 index 00000000..9ad63033 --- /dev/null +++ b/docs/user-guide/03-keyboard-shortcuts.md @@ -0,0 +1,156 @@ +# Keyboard Shortcuts + +Reference for key bindings in the Hawk TUI. Bindings are built-in and cannot currently be remapped. + +--- + +## Input Modes + +Hawk has two input modes that control how you navigate the scrollback: + +- **Simple mode** (default): Arrow keys for navigation, `Shift+Arrow` for turn navigation, `Space` to focus the prompt +- **Vim mode** (opt-in): `j`/`k` for navigation, `H`/`L` for turn navigation, `h`/`l` for fold, `Tab` to focus the prompt + +Simple mode is active by default. To switch to Vim mode: + +``` +/vim-mode +``` + +Or set `vim_mode = true` under `[ui]` in `~/.hawk/settings.json`: + +```json +{ + "ui": { + "vim_mode": true + } +} +``` + +--- + +## Navigation (Scrollback Focused) + +Move through conversation entries in the scrollback pane. + +| Key | Alt Key | Action | +|-----|---------|--------| +| `j` | `Down` | Select next entry | +| `k` | `Up` | Select previous entry | +| `⇧L` | `Shift+Right` | Jump to next turn | +| `⇧H` | `Shift+Left` | Jump to previous turn | +| `g` | | Go to top of scrollback | +| `⇧G` | | Go to bottom of scrollback | +| `PageUp` | | Scroll up one page | +| `PageDown` | | Scroll down one page | + +--- + +## View (Scrollback Focused) + +Control how entries are displayed. + +| Key | Alt Key | Action | +|-----|---------|--------| +| `h` | `Left` | Collapse selected entry | +| `l` | `Right` | Expand selected entry | +| `e` | | Toggle fold on selected entry | +| `⇧E` | | Expand/collapse all entries | +| `Enter` | | Open entry in fullscreen viewer | +| `r` | | Toggle raw markdown on selected entry | + +--- + +## Focus + +Switch between the prompt input and scrollback pane. + +| Key | Alt Key | Action | +|-----|---------|--------| +| `Tab` | `Space` | Focus the prompt input | + +--- + +## Escape + +| State | Gesture | Effect | +|-------|---------|--------| +| Idle + non-empty prompt | `Esc` | Clear prompt (saved to history) | +| Idle + empty prompt + messages | `Esc` | Open session picker (same as `/resume`) | +| Turn running | `Ctrl+C` | Cancel the current turn | + +--- + +## Agent-Level Actions + +| Key | Action | +|-----|--------| +| `Ctrl+P` | Open command palette | +| `Ctrl+M` | Open model picker | +| `Ctrl+C` | Cancel current turn | +| `Ctrl+N` | Create new session (double-press to confirm) | +| `Ctrl+Q` | Quit the application | + +--- + +## Mouse Support + +The TUI supports mouse interaction: + +- **Click** on a scrollback entry to select it +- **Scroll wheel** to scroll through scrollback +- **Click** on the prompt area to focus it +- **Middle click** on Linux to paste PRIMARY selection + +--- + +## Quick Reference Card + +### When scrollback is focused (Simple mode) + +``` +Navigation: Up/Down (prev/next entry) +Turn nav: Shift+Left/Right (prev/next turn) +Scrolling: PageUp/PageDown +Focus prompt: Space or Tab +``` + +### When scrollback is focused (Vim mode) + +``` +Navigation: j/k (up/down) +Turn nav: H/L (prev/next turn) +Scrolling: Ctrl+J/K (line) PageUp/PageDown +Folding: h/l (collapse/expand) e (toggle) +Focus prompt: Tab or Space +``` + +### When prompt is focused + +``` +Send: Enter +Newline: Shift+Enter or Alt+Enter +Paste: Ctrl+V +Leave: Tab (back to scrollback) +Cancel (running): Ctrl+C +Clear (idle): Esc (non-empty prompt) +``` + +--- + +## Command Palette + +Press `Ctrl+P` or `?` to open the command palette — a searchable list of actions including all keyboard shortcuts, slash commands, and skills. + +--- + +Where to Go Next + +| Document | What You Will Learn | +|----------|-------------------| +| [Slash Commands](04-slash-commands.md) | All available `/` commands | +| [Configuration](05-configuration.md) | Settings and sandbox profiles | + +--- + +© 2026 GrayCode AI. All rights reserved. \ No newline at end of file diff --git a/docs/user-guide/04-slash-commands.md b/docs/user-guide/04-slash-commands.md new file mode 100644 index 00000000..85a1a7d9 --- /dev/null +++ b/docs/user-guide/04-slash-commands.md @@ -0,0 +1,501 @@ +# Slash Commands + +Type `/` in the prompt to access commands. Each command runs an action immediately and autocompletes as you type. + +--- + +## Session Management + +### `/new` + +Start a new session, clearing the current conversation. + +``` +/new +``` + +### `/resume` + +Open the session picker to load a previous session. + +``` +/resume +``` + +### `/compact [context]` + +Compress conversation history to save context window space. Optionally specify what to preserve. + +``` +/compact +/compact keep the auth implementation details +``` + +### `/context` + +Show context window usage and session stats. + +``` +/context +``` + +### `/session-info` + +Show session details including model, turn count, and context usage. + +``` +/session-info +``` + +### `/fork` + +Branch the current session into a new agent, preserving history up to this point. + +``` +/fork +``` + +### `/home` + +Return to the welcome screen. + +``` +/home +``` + +--- + +## Model and Mode + +### `/model ` + +Switch to a different model. Accepts model IDs or display names. + +``` +/model grok-build +/model gpt-4o +/model claude-3-5-sonnet +``` + +### `/effort ` + +Set reasoning effort on the current model. Levels: `low`, `medium`, `high`, `xhigh`. + +``` +/effort high +``` + +Only works when the active model supports reasoning effort. + +### `/autonomy` + +Control the autonomy tier and sandbox profile. Opens an interactive picker. + +``` +/autonomy +``` + +### `/autonomy tier ` + +Set autonomy tier without opening the picker. + +``` +/autonomy tier scout +/autonomy tier builder +/autonomy tier operator +/autonomy tier autonomous +``` + +### `/autonomy sandbox ` + +Set sandbox profile. + +``` +/autonomy sandbox strict +/autonomy sandbox workspace +/autonomy sandbox off +``` + +### `/autonomy dry-run ` + +Enable or disable dry-run mode (denies all tools unconditionally). + +``` +/autonomy dry-run on +/autonomy dry-run off +``` + +### `/multiline` + +Toggle multiline input mode. When enabled, `Enter` inserts a newline and `Shift+Enter` sends. + +``` +/multiline +``` + +--- + +## Memory (via yaad) + +### `/yaad` + +Browse persistent memory stored in yaad. + +``` +/yaad +``` + +### `/yaad search ` + +Search yaad memories. + +``` +/yaad search auth implementation +``` + +### `/remember ` + +Save a note to memory immediately. + +``` +/remember the staging deploy uses the eu-west cluster +``` + +--- + +## Hooks and Plugins + +### `/hooks` + +Manage hooks — file-triggered and HTTP-based event handlers. + +``` +/hooks +``` + +### `/plugins` + +View and manage installed plugins. + +``` +/plugins +``` + +### `/skills` + +Browse installed skills from the community registry. + +``` +/skills +``` + +### `/skills search ` + +Search the community registry for skills. + +``` +/skills search go +/skills search review +``` + +### `/skills install ` + +Install a skill from a source. + +``` +/skills install go-review +hawk skills install go-review +``` + +### `/skills audit` + +Security scan installed skills. + +``` +/skills audit +hawk skills audit +``` + +--- + +## Planning and Tasks + +### `/spec [description]` + +Enter spec mode. Walks through Specify → Plan → Tasks workflow. + +``` +/spec Add authentication to the API +``` + +### `/spec status` + +Show current spec workflow status. + +``` +/spec status +``` + +### `/spec reset` + +Reset the spec workflow. + +``` +/spec reset +``` + +### `/plan` + +Enter plan mode (alias for `/spec`). + +``` +/plan Refactor the auth module +``` + +### `/todo` + +Create and manage task lists. + +``` +/todo +``` + +--- + +## Asking Questions + +### `/ask ` + +Ask the user a clarifying question. Model uses the `AskUserQuestion` tool internally. + +``` +/ask What framework should I use for the frontend? +``` + +With options (in-tool): + +``` +/ask What database should I use? --options postgresql mysql sqlite +``` + +--- + +## Sandbox and Permissions + +### `/sandbox strict` + +Apply strict sandbox profile (cwd only, no network). + +``` +/sandbox strict +``` + +### `/sandbox workspace` + +Apply workspace sandbox profile (project directory only). + +``` +/sandbox workspace +``` + +### `/sandbox off` + +Disable sandbox (full access). + +``` +/sandbox off +``` + +### `/trust` + +Manage folder trust for project automation. + +``` +/trust +``` + +--- + +## Scheduling + +### `/loop [interval] ` + +Run a prompt on a recurring interval. + +``` +/loop 30m check deploy status +/loop 1 hour review latest changes +``` + +Interval formats: `30s`, `30m`, `1h`, `1d`. Minimum 60 seconds. + +### `/cron` + +Open the cron/scheduler management. + +``` +/cron +``` + +--- + +## Diagnostics + +### `/path` + +Check developer path readiness (setup + security + sandbox). + +``` +/path +``` + +### `/preflight` + +Quick ready-to-chat check. + +``` +/preflight +``` + +### `/ecosystem` + +Show ecosystem component status (Eyrie, yaad, tok). + +``` +/ecosystem +``` + +--- + +## Configuration + +### `/config` + +Open settings/configuration modal. + +``` +/config +``` + +Aliases: `/settings`, `/preferences` + +### `/theme` + +Switch TUI color theme. + +``` +/theme +``` + +### `/scroll-speed ` + +Set scroll speed (1-100). + +``` +/scroll-speed 75 +``` + +### `/scroll-mode ` + +Set scroll mode (auto, wheel, trackpad). + +``` +/scroll-mode auto +/scroll-mode wheel +/scroll-mode trackpad +``` + +### `/scroll-invert` + +Toggle natural scrolling (invert direction). + +``` +/scroll-invert +``` + +### `/compact-mode` + +Toggle compact mode (reduces outer padding). + +``` +/compact-mode +``` + +### `/pager-config