diff --git a/README.md b/README.md index 86af886..c7b3c27 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ ExitBox automatically: - **Full Git Support** — optional mode that mounts host `.gitconfig` and SSH agent for seamless git operations inside the container - **GitHub CLI Authentication** — pre-flight vault import for `GITHUB_TOKEN` with automatic in-container export so `gh` and HTTPS git work transparently - **RTK Token Optimizer (Experimental)** — optional [rtk](https://github.com/rtk-ai/rtk) integration reduces CLI output token consumption by 60-90% -- **Graphify Knowledge Graph (Experimental)** — optional [graphify](https://github.com/safishamsi/graphify) integration; when enabled, the `/graphify` skill is auto-installed into compatible agents and removed when disabled +- **Graphify Knowledge Graph (Experimental)** — optional [graphify](https://github.com/safishamsi/graphify) external tool; when selected, the `/graphify` skill is auto-installed into compatible agents and removed when deselected - **External Tools** — configure third-party tools (GitHub CLI, etc.) via the setup wizard; packages are auto-installed at image build time - **Supply-Chain Hardened Installs** — Claude Code and OpenCode installed via direct download with SHA-256 checksum verification - **Alpine Base Image** — minimal ~5 MB base with 3-layer image hierarchy and incremental rebuilds @@ -244,17 +244,17 @@ exitbox agents config claude # Open agent config in $EDITOR ### Graphify Knowledge Graph (Experimental) -[graphify](https://github.com/safishamsi/graphify) maps a project (code, docs, PDFs, images) into a queryable knowledge graph you invoke with `/graphify` inside an agent. When enabled, the `graphify` CLI is installed at image build time and the `/graphify` skill is registered into each compatible agent at container start; when disabled, the skill is removed. +[graphify](https://github.com/safishamsi/graphify) maps a project (code, docs, PDFs, images) into a queryable knowledge graph you invoke with `/graphify` inside an agent. It's an ExitBox **External Tool**: when selected, the `graphify` CLI is installed into the tools image layer and the `/graphify` skill is registered into each compatible agent at container start; when deselected, the skill is removed. ```bash -# Enable via the setup wizard: +# Select via the setup wizard: exitbox setup -# → Settings → Graphify → Enable +# → Tools → External Tools → Graphify ``` Compatible agents: **Claude, Codex, OpenCode, Copilot, Pi**. (Cursor's graphify install is project-scoped and Kimi Code's skill path differs from graphify's `kimi` target, so they are not auto-wired; Qwen has no graphify platform.) -> **Note:** Graphify is experimental and gated at image build time like RTK — toggling it requires a rebuild (`exitbox run --update`). +> **Note:** Graphify is experimental. Selecting/deselecting it rebuilds the tools image layer (`exitbox run --update`). ### Agent Skills diff --git a/cmd/run.go b/cmd/run.go index 24575d7..39634af 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -322,7 +322,6 @@ func runAgent(agentName string, passthrough []string) { Keybindings: cfg.Settings.Keybindings.EnvValue(), FullGitSupport: cfg.Settings.DefaultFlags.FullGitSupport, RTK: cfg.Settings.RTK, - Graphify: cfg.Settings.Graphify, } exitCode, err := run.AgentContainer(rt, opts) diff --git a/internal/config/defaults.go b/internal/config/defaults.go index 99b82ef..dfdf8fe 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -67,7 +67,6 @@ func DefaultConfig() *Config { AutoUpdate: false, StatusBar: true, RTK: false, - Graphify: false, DefaultWorkspace: "default", DefaultFlags: DefaultFlags{ AutoResume: false, diff --git a/internal/config/types.go b/internal/config/types.go index 296ed15..9d9f5d9 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -84,7 +84,6 @@ type SettingsConfig struct { AutoUpdate bool `yaml:"auto_update"` StatusBar bool `yaml:"status_bar"` RTK bool `yaml:"rtk"` - Graphify bool `yaml:"graphify"` DefaultWorkspace string `yaml:"default_workspace,omitempty"` DefaultFlags DefaultFlags `yaml:"default_flags"` Keybindings KeybindingsConfig `yaml:"keybindings,omitempty"` diff --git a/internal/image/base.go b/internal/image/base.go index 1be2d72..5b45541 100644 --- a/internal/image/base.go +++ b/internal/image/base.go @@ -288,7 +288,6 @@ func buildBaseFull(ctx context.Context, rt container.Runtime, cmd, imageName str args = append(args, "--build-arg", fmt.Sprintf("EXITBOX_VERSION=%s", Version), "--build-arg", fmt.Sprintf("INSTALL_RTK=%v", cfg.Settings.RTK), - "--build-arg", fmt.Sprintf("INSTALL_GRAPHIFY=%v", cfg.Settings.Graphify), "-t", publishedName, "-f", dockerfilePath, buildCtx, diff --git a/internal/run/run.go b/internal/run/run.go index 0249869..72970ef 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -74,7 +74,6 @@ type Options struct { Keybindings string FullGitSupport bool RTK bool - Graphify bool } // AgentContainer runs an agent container interactively. @@ -414,9 +413,6 @@ func AgentContainer(rt container.Runtime, opts Options) (int, error) { if opts.RTK { args = append(args, "-e", "EXITBOX_RTK=true") } - if opts.Graphify { - args = append(args, "-e", "EXITBOX_GRAPHIFY=true") - } if opts.Ollama { args = append(args, ollamaEnvVars(opts.Agent)...) } @@ -541,7 +537,6 @@ func isReservedEnvVar(key string) bool { "EXITBOX_VAULT_ENABLED": true, "EXITBOX_VAULT_READONLY": true, "EXITBOX_RTK": true, - "EXITBOX_GRAPHIFY": true, "EXITBOX_IDE_PORT": true, "CLAUDE_CODE_SSE_PORT": true, "ENABLE_IDE_INTEGRATION": true, diff --git a/internal/wizard/roles.go b/internal/wizard/roles.go index 1c23549..7e1e4bb 100644 --- a/internal/wizard/roles.go +++ b/internal/wizard/roles.go @@ -194,6 +194,12 @@ var AllExternalTools = []ExternalTool{ {HostPath: ".config/gh", Description: "GitHub CLI auth"}, }, }, + { + Name: "Graphify", + Description: "graphify — /graphify skill maps your project into a queryable knowledge graph (auto-installed into compatible agents)", + Packages: []string{"py3-pip"}, + InstallStep: "RUN pip install --no-cache-dir --break-system-packages graphifyy && graphify --version\n", + }, } // AllAgents defines the selectable agents, built from the agent registry. diff --git a/internal/wizard/tui.go b/internal/wizard/tui.go index 14659bf..996b682 100644 --- a/internal/wizard/tui.go +++ b/internal/wizard/tui.go @@ -81,7 +81,6 @@ type State struct { ExternalTools []string // selected external tools (e.g. "GitHub CLI") FullGitSupport bool // full git support (SSH agent + .gitconfig) RTK bool // experimental: token-optimized CLI wrappers - Graphify bool // graphify knowledge-graph /graphify skill } // Model is the root bubbletea model for the wizard. @@ -215,7 +214,6 @@ func NewModel() Model { checked["setting:pass_env"] = true checked["setting:read_only"] = false checked["setting:rtk"] = false - checked["setting:graphify"] = false kb := config.DefaultKeybindings() return Model{ step: stepWelcome, @@ -317,7 +315,6 @@ func NewModelFromConfig(cfg *config.Config) Model { checked["setting:read_only"] = cfg.Settings.DefaultFlags.ReadOnly checked["setting:full_git"] = cfg.Settings.DefaultFlags.FullGitSupport checked["setting:rtk"] = cfg.Settings.RTK - checked["setting:graphify"] = cfg.Settings.Graphify // On re-run, always start at the top menu so the user can choose // between workspace management and general settings. @@ -2055,7 +2052,6 @@ var settingsOptions = []struct { {"setting:read_only", "Read-only workspace", "Mount workspace as read-only (agents cannot modify files)"}, {"setting:full_git", "Full Git support", "Mount SSH agent + .gitconfig into container (exposes git identity)"}, {"setting:rtk", "RTK token optimizer", "Experimental: use rtk to reduce CLI output tokens by 60-90%"}, - {"setting:graphify", "Graphify knowledge graph", "Install the /graphify skill (maps your project into a queryable graph) in compatible agents"}, } func (m Model) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -2082,7 +2078,6 @@ func (m Model) updateSettings(msg tea.Msg) (tea.Model, tea.Cmd) { m.state.MakeDefault = m.checked["setting:make_default"] m.state.FullGitSupport = m.checked["setting:full_git"] m.state.RTK = m.checked["setting:rtk"] - m.state.Graphify = m.checked["setting:graphify"] m.visitedSteps[stepSettings] = true if !m.isFirstRun && m.topMenuChoice == 0 { // Workspace management: Settings → Domains or Vault diff --git a/internal/wizard/wizard.go b/internal/wizard/wizard.go index f79bd09..1ae518f 100644 --- a/internal/wizard/wizard.go +++ b/internal/wizard/wizard.go @@ -148,7 +148,6 @@ func applyResult(state State, existingCfg *config.Config) error { cfg.Settings.AutoUpdate = state.AutoUpdate cfg.Settings.StatusBar = state.StatusBar cfg.Settings.RTK = state.RTK - cfg.Settings.Graphify = state.Graphify cfg.Settings.DefaultFlags = config.DefaultFlags{ NoFirewall: !state.EnableFirewall, AutoResume: state.AutoResume, diff --git a/static/build/Dockerfile.base b/static/build/Dockerfile.base index 1f793ca..485820e 100644 --- a/static/build/Dockerfile.base +++ b/static/build/Dockerfile.base @@ -89,17 +89,6 @@ RUN if [ "$INSTALL_RTK" = "true" ]; then \ apk del musl-dev gcc; \ fi -# Install graphify — knowledge-graph "/graphify" skill for coding agents -# (conditional). The CLI (pip package "graphifyy", command "graphify") is installed -# globally so the non-root runtime user can run it; per-agent skill registration -# happens at container start (see setup_graphify in docker-entrypoint). -ARG INSTALL_GRAPHIFY=false -RUN if [ "$INSTALL_GRAPHIFY" = "true" ]; then \ - apk add --no-cache py3-pip && \ - pip install --no-cache-dir --break-system-packages graphifyy && \ - graphify --version; \ - fi - # Stub xdg-open: no browser in container, just print the URL so OAuth # flows (OpenCode, Codex) show the link instead of failing with ENOENT. RUN printf '#!/bin/sh\necho "Open this URL in your browser:"\necho "$1"\n' \ diff --git a/static/build/docker-entrypoint b/static/build/docker-entrypoint index 4216886..1d0d283 100755 --- a/static/build/docker-entrypoint +++ b/static/build/docker-entrypoint @@ -1250,20 +1250,21 @@ graphify_skill_dir_for_agent() { } setup_graphify() { - # Register (enabled) or remove (disabled) the graphify "/graphify" skill for - # the current agent. Runs after apply_active_workspace_links so the agent's - # config dir is symlinked to the persistent workspace location. + # Register or remove the graphify "/graphify" skill for the current agent. + # graphify is an ExitBox "External Tool": when selected, its CLI is baked into + # the tools image layer, so the presence of the `graphify` command is the + # enabled signal. Runs after apply_active_workspace_links so the agent's config + # dir is symlinked to the persistent workspace location. local platform skill_dir platform="$(graphify_platform_for_agent "$AGENT")" [[ -n "$platform" ]] || return 0 skill_dir="$(graphify_skill_dir_for_agent "$AGENT")" - if [[ "${EXITBOX_GRAPHIFY:-}" == "true" ]]; then - command -v graphify >/dev/null 2>&1 || return 0 + if command -v graphify >/dev/null 2>&1; then graphify install --platform "$platform" >/dev/null 2>&1 || true else - # Disabled: remove a previously installed graphify skill (file-based so it - # works even when the CLI is no longer present after a rebuild). + # Not selected (CLI absent, e.g. after a tools-layer rebuild): remove any + # previously installed graphify skill. File-based so it works without the CLI. [[ -n "$skill_dir" ]] && rm -rf "$skill_dir" 2>/dev/null fi } diff --git a/static/build/docker-entrypoint_test.sh b/static/build/docker-entrypoint_test.sh index cffa89f..cb570c6 100644 --- a/static/build/docker-entrypoint_test.sh +++ b/static/build/docker-entrypoint_test.sh @@ -1705,40 +1705,42 @@ test_setup_graphify() { sdf="$(extract_func graphify_skill_dir_for_agent)" sgf="$(extract_func setup_graphify)" - # Enabled + compatible agent → runs `graphify install --platform `. - # setup_graphify redirects the command's stdout to /dev/null, so the stub - # records its args to a file instead. + # graphify CLI present (External Tool selected) + compatible agent → runs + # `graphify install --platform `. setup_graphify redirects the command's + # stdout to /dev/null, so the stub records its args to a file instead. local marker="$TEST_TMPDIR/gfy_args" rm -f "$marker" ( export HOME; HOME="$(mktemp -d "$TEST_TMPDIR/gfy1.XXXXXX")" - export AGENT="pi" EXITBOX_GRAPHIFY="true" GFY_MARKER="$marker" + export AGENT="pi" GFY_MARKER="$marker" eval "$gpf"; eval "$sdf"; eval "$sgf" graphify() { echo "$*" > "$GFY_MARKER"; } setup_graphify ) 2>/dev/null local enabled="" [[ -f "$marker" ]] && enabled="$(cat "$marker")" - assert_contains "setup_graphify installs for compatible agent" "$enabled" "install --platform pi" + assert_contains "setup_graphify installs when graphify CLI present" "$enabled" "install --platform pi" - # Disabled → removes a previously installed skill dir. + # graphify CLI absent (not selected) → removes a previously installed skill dir. local home2 removed home2="$(mktemp -d "$TEST_TMPDIR/gfy2.XXXXXX")" mkdir -p "$home2/.pi/agent/skills/graphify" removed="$( export HOME="$home2" - export AGENT="pi" EXITBOX_GRAPHIFY="false" + export AGENT="pi" eval "$gpf"; eval "$sdf"; eval "$sgf" + # No graphify stub and graphify is not installed in the test env, so + # `command -v graphify` is false → the removal branch runs. setup_graphify [[ -d "$home2/.pi/agent/skills/graphify" ]] && echo PRESENT || echo GONE )" 2>/dev/null - assert_eq "setup_graphify removes skill when disabled" "GONE" "$removed" + assert_eq "setup_graphify removes skill when CLI absent" "GONE" "$removed" - # Incompatible agent (qwen) → no graphify invocation even when enabled. + # Incompatible agent (qwen) → no graphify invocation even when CLI present. local skipped skipped="$( export HOME; HOME="$(mktemp -d "$TEST_TMPDIR/gfy3.XXXXXX")" - export AGENT="qwen" EXITBOX_GRAPHIFY="true" + export AGENT="qwen" eval "$gpf"; eval "$sdf"; eval "$sgf" graphify() { echo "CALLED"; } setup_graphify