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

115 lines
3.5 KiB
Go

package hooks
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// external_agent.go handles lifecycle hooks for the non-Claude agents
// that share Claude Code's hookSpecificOutput.additionalContext wire
// shape — the Gemini CLI and Google Antigravity. The gortex adapters
// install `gortex hook --agent <name>` SessionStart + AfterTool hooks;
// this handler answers them, injecting the same session orientation and
// stale-index hints Claude gets via its SessionStart hook.
// ExternalAgentInput is the stdin JSON the Gemini CLI / Antigravity send
// on a lifecycle hook. We read only the fields we use; the shapes
// overlap with Claude's hook payloads.
type ExternalAgentInput struct {
HookEventName string `json:"hook_event_name"`
ToolName string `json:"tool_name"`
CWD string `json:"cwd"`
}
// RunExternalAgent reads a lifecycle-hook event from stdin and emits a
// hookSpecificOutput.additionalContext block for a non-Claude agent. It
// is the entry point for `gortex hook --agent <gemini|antigravity>`.
func RunExternalAgent() {
data, err := io.ReadAll(os.Stdin)
if err != nil {
return
}
handleExternalAgent(data)
}
// handleExternalAgent is the testable core: parse the event, build the
// context, emit it. SessionStart gets the full orientation; a post-tool
// event gets a concise stale-index hint, emitted only when something is
// actionable (so the hook stays quiet on the happy path).
func handleExternalAgent(data []byte) {
var input ExternalAgentInput
if err := json.Unmarshal(data, &input); err != nil {
return
}
var ctx string
switch normalizeExternalEvent(input.HookEventName) {
case "sessionstart":
ctx = buildSessionStartBriefing(input.CWD)
case "aftertool":
ctx = buildStaleIndexHint(input.CWD)
default:
return
}
if strings.TrimSpace(ctx) == "" {
return
}
out, err := json.Marshal(HookOutput{HookSpecificOutput: &HookSpecificOutput{
HookEventName: input.HookEventName,
AdditionalContext: ctx,
}})
if err != nil {
return
}
fmt.Print(string(out))
}
// normalizeExternalEvent maps the agent's event name onto the two
// shapes we handle. Gemini/Antigravity use "SessionStart" and
// "AfterTool"; we also accept Claude's "PostToolUse" spelling so a
// shared config works either way.
func normalizeExternalEvent(e string) string {
switch strings.ToLower(strings.TrimSpace(e)) {
case "sessionstart":
return "sessionstart"
case "aftertool", "posttooluse":
return "aftertool"
default:
return ""
}
}
// buildStaleIndexHint returns a one-line actionable hint when the graph
// is unavailable or the cwd is not covered by a tracked repo, and ""
// when everything is fresh — keeping the per-tool hook quiet on the
// happy path rather than spamming context every tool call.
func buildStaleIndexHint(cwd string) string {
status, err := sessionStartStatusFn()
switch {
case errors.Is(err, errDaemonUnreachable):
return "[Gortex] daemon is not running — graph tools are unavailable. Start it: `gortex daemon start --detach`"
case err != nil:
return ""
}
if !status.Ready {
return fmt.Sprintf("[Gortex] daemon is warming up (%s elapsed); graph answers may be partial until it is ready.", formatDuration(status.WarmupSeconds))
}
if cwd != "" {
abs, e := filepath.Abs(cwd)
if e != nil {
abs = cwd
}
exact, contained := classifyCwd(abs, status.TrackedRepos)
if exact == nil && len(contained) == 0 {
return fmt.Sprintf("[Gortex] cwd `%s` is not tracked — run `gortex track %s` to enable graph context here.", abs, abs)
}
}
return ""
}