diff --git a/README.md b/README.md index c90d45b..86af886 100644 --- a/README.md +++ b/README.md @@ -68,6 +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 - **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 @@ -241,6 +242,20 @@ exitbox agents config claude # Open agent config in $EDITOR > **Note:** RTK is experimental. If you encounter issues, disable it via `exitbox setup` and rebuild with `exitbox run --update`. +### 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. + +```bash +# Enable via the setup wizard: +exitbox setup +# → Settings → Graphify → Enable +``` + +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`). + ### Agent Skills Skills are reusable SKILL.md files that give agents specialized capabilities (coding conventions, deploy workflows, review checklists, etc.). ExitBox provides a universal skill installer that works across all agents. Skills are installed once per workspace and automatically linked into each agent's skill directory at container start. diff --git a/cmd/run.go b/cmd/run.go index 39634af..24575d7 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -322,6 +322,7 @@ 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 dfdf8fe..99b82ef 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -67,6 +67,7 @@ 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 9d9f5d9..296ed15 100644 --- a/internal/config/types.go +++ b/internal/config/types.go @@ -84,6 +84,7 @@ 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 5b45541..1be2d72 100644 --- a/internal/image/base.go +++ b/internal/image/base.go @@ -288,6 +288,7 @@ 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 72970ef..0249869 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -74,6 +74,7 @@ type Options struct { Keybindings string FullGitSupport bool RTK bool + Graphify bool } // AgentContainer runs an agent container interactively. @@ -413,6 +414,9 @@ 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)...) } @@ -537,6 +541,7 @@ 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/tui.go b/internal/wizard/tui.go index 996b682..14659bf 100644 --- a/internal/wizard/tui.go +++ b/internal/wizard/tui.go @@ -81,6 +81,7 @@ 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. @@ -214,6 +215,7 @@ 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, @@ -315,6 +317,7 @@ 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. @@ -2052,6 +2055,7 @@ 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) { @@ -2078,6 +2082,7 @@ 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 1ae518f..f79bd09 100644 --- a/internal/wizard/wizard.go +++ b/internal/wizard/wizard.go @@ -148,6 +148,7 @@ 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 485820e..1f793ca 100644 --- a/static/build/Dockerfile.base +++ b/static/build/Dockerfile.base @@ -89,6 +89,17 @@ 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 2ff7751..4216886 100755 --- a/static/build/docker-entrypoint +++ b/static/build/docker-entrypoint @@ -998,6 +998,7 @@ run_agent_loop() { inject_sandbox_instructions inject_codex_approval_rules link_skills + setup_graphify fi build_resume_args "$@" @@ -1217,6 +1218,56 @@ setup_rtk() { fi } +# graphify_platform_for_agent maps an ExitBox agent to its graphify "--platform" +# token, or "" when graphify can't target that agent. Only agents whose graphify +# skill destination matches ExitBox's config layout are included: kimi (graphify +# writes ~/.kimi, ExitBox uses ~/.kimi-code), cursor (project-scoped .cursor rules) +# and qwen (no graphify platform) are intentionally excluded. +graphify_platform_for_agent() { + case "$1" in + claude) echo "claude" ;; + codex) echo "codex" ;; + opencode) echo "opencode" ;; + copilot) echo "copilot" ;; + pi) echo "pi" ;; + *) echo "" ;; + esac +} + +# graphify_skill_dir_for_agent returns the directory graphify installs its skill +# into for the agent, so it can be removed when graphify is disabled (the CLI is +# gone after a rebuild, so removal must be file-based). Paths mirror graphify's +# own _PLATFORM_CONFIG destinations. +graphify_skill_dir_for_agent() { + case "$1" in + claude) echo "$HOME/.claude/skills/graphify" ;; + codex) echo "$HOME/.codex/skills/graphify" ;; + opencode) echo "$HOME/.config/opencode/skills/graphify" ;; + copilot) echo "$HOME/.copilot/skills/graphify" ;; + pi) echo "$HOME/.pi/agent/skills/graphify" ;; + *) echo "" ;; + esac +} + +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. + 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 + 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). + [[ -n "$skill_dir" ]] && rm -rf "$skill_dir" 2>/dev/null + fi +} + # ============================================================================ # AGENT DETECTION # ============================================================================ @@ -1797,6 +1848,7 @@ if [[ -n "$AGENT" ]]; then inject_sandbox_instructions inject_codex_approval_rules link_skills + setup_graphify fi # Debug: show what's available diff --git a/static/build/docker-entrypoint_test.sh b/static/build/docker-entrypoint_test.sh index c39398e..cffa89f 100644 --- a/static/build/docker-entrypoint_test.sh +++ b/static/build/docker-entrypoint_test.sh @@ -1696,6 +1696,59 @@ test_link_skills_pi() { test_link_skills_pi +# ============================================================================ +# Test: setup_graphify installs/removes the /graphify skill per agent +# ============================================================================ +test_setup_graphify() { + local gpf sdf sgf + gpf="$(extract_func graphify_platform_for_agent)" + 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. + 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" + 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" + + # Disabled → 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" + eval "$gpf"; eval "$sdf"; eval "$sgf" + 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" + + # Incompatible agent (qwen) → no graphify invocation even when enabled. + local skipped + skipped="$( + export HOME; HOME="$(mktemp -d "$TEST_TMPDIR/gfy3.XXXXXX")" + export AGENT="qwen" EXITBOX_GRAPHIFY="true" + eval "$gpf"; eval "$sdf"; eval "$sgf" + graphify() { echo "CALLED"; } + setup_graphify + echo DONE + )" 2>/dev/null + assert_eq "setup_graphify skips incompatible agent" "DONE" "$skipped" +} + +test_setup_graphify + # ============================================================================ # Results # ============================================================================