Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

116 lines
3.5 KiB
Go

package agents
import (
"slices"
"strings"
)
// hooks.go installs Gortex lifecycle hooks into the Gemini-CLI /
// Antigravity settings.json. These agents share Claude Code's
// hookSpecificOutput.additionalContext wire shape, so a gortex hook can
// inject the same session orientation (SessionStart) and stale-index
// hints (AfterTool) it gives Claude — closing the gap where the
// non-Claude adapters wrote MCP config but no lifecycle hooks.
// GortexHookName is the identifier stamped on every hook entry Gortex
// installs, so a re-run detects and skips (or, with Force, replaces)
// its own entries without disturbing the user's other hooks.
const GortexHookName = "gortex"
// geminiHookTimeoutMS is the per-hook timeout. Gemini CLI measures hook
// timeouts in milliseconds (Claude Code uses seconds), so this is 10s.
const geminiHookTimeoutMS = 10000
// UpsertGeminiHooks installs Gortex SessionStart + AfterTool lifecycle
// hooks into a Gemini-CLI / Antigravity settings.json root, politely
// merging with any existing user hooks (existing entries are preserved;
// gortex's own entry is added once and is idempotent on re-run).
// agentFlag is the value passed to `gortex hook --agent <agentFlag>`.
// Returns true when the root was modified.
func UpsertGeminiHooks(root map[string]any, agentFlag string, opts ApplyOpts) (changed bool) {
hooks, ok := root["hooks"].(map[string]any)
if !ok {
hooks = map[string]any{}
}
cmd := "gortex hook --agent " + agentFlag
// SessionStart: full session orientation (no tool matcher).
if upsertHookEvent(hooks, "SessionStart", "", cmd, "Gortex session orientation", opts) {
changed = true
}
// AfterTool: a concise stale-index hint after the tools where the
// index can drift (shell, search, glob).
if upsertHookEvent(hooks, "AfterTool", "run_shell_command|search_file_content|glob", cmd, "Gortex graph context + stale-index hint", opts) {
changed = true
}
if changed {
root["hooks"] = hooks
}
return changed
}
// upsertHookEvent appends a gortex hook group to one event's array,
// preserving existing user entries. When a gortex entry is already
// present it is a no-op (idempotent) unless opts.Force, which replaces
// it. Returns true when the array was modified.
func upsertHookEvent(hooks map[string]any, event, matcher, cmd, desc string, opts ApplyOpts) bool {
arr, _ := hooks[event].([]any)
hasGortex := slices.ContainsFunc(arr, hookGroupIsGortex)
if hasGortex && !opts.Force {
return false
}
if hasGortex && opts.Force {
// Drop existing gortex groups; keep the user's.
kept := make([]any, 0, len(arr))
for _, e := range arr {
if !hookGroupIsGortex(e) {
kept = append(kept, e)
}
}
arr = kept
}
inner := map[string]any{
"type": "command",
"command": cmd,
"name": GortexHookName,
"timeout": geminiHookTimeoutMS,
"description": desc,
}
group := map[string]any{"hooks": []any{inner}}
if matcher != "" {
group["matcher"] = matcher
}
hooks[event] = append(arr, group)
return true
}
// hookGroupIsGortex reports whether a hook group was installed by
// Gortex — either tagged with GortexHookName or carrying a
// `gortex hook` command.
func hookGroupIsGortex(e any) bool {
g, ok := e.(map[string]any)
if !ok {
return false
}
inner, ok := g["hooks"].([]any)
if !ok {
return false
}
for _, h := range inner {
hm, ok := h.(map[string]any)
if !ok {
continue
}
if name, _ := hm["name"].(string); name == GortexHookName {
return true
}
if cmd, _ := hm["command"].(string); strings.Contains(cmd, "gortex hook") {
return true
}
}
return false
}