@ev-ai/agent-hook 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COLLECTED-DATA.md +67 -0
- package/README.md +197 -0
- package/dist/cli.mjs +19 -0
- package/dist/hook.mjs +12 -0
- package/dist/plugin-hooks.mjs +122 -0
- package/package.json +43 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Collected data
|
|
2
|
+
|
|
3
|
+
What `@evalon-ai/agent-hook` POSTs to the Evalon collector (`POST /{orgToken}/hook`, default `https://hooks.evalon.ai/{orgToken}/hook`). Auth is the org path id in the URL — no Bearer header and not in the JSON body. See [README.md](./README.md) for install and privacy defaults.
|
|
4
|
+
|
|
5
|
+
## Tracked events
|
|
6
|
+
|
|
7
|
+
| Event | When |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `session_start` | Coding-agent session starts |
|
|
10
|
+
| `session_end` | Session stops |
|
|
11
|
+
| `tool_invoked` | Tool, skill, MCP, shell, or file-edit hook fires |
|
|
12
|
+
|
|
13
|
+
Every event also carries: UTC timestamp, `installation_id`, runner `platform` (`claude` / `cursor` / `codex`), `platform_agent_id` (Layer-1 inventory join key: `{owner}/{repo}:{scopePath/}{markerDir}`, e.g. `acme/my-app:.claude` or `acme/my-app:apps/web/.cursor`), `session_id`, `turn_id`, raw `runner_event` name, `agent_hook_version`, `host_arch`, and `node_version`.
|
|
14
|
+
|
|
15
|
+
## Tracked data points
|
|
16
|
+
|
|
17
|
+
### On `session_start`
|
|
18
|
+
|
|
19
|
+
- **Host** — hostname, OS / version, username, shell, TTY, runtime context (interactive / CI / container / cron), optional virt and cloud provider / region / instance id. Cloud metadata is probed only for non-interactive contexts (CI, container, cron, GitHub Actions); interactive laptop sessions skip IMDS entirely. Non-interactive probes are cached locally for 24h per context.
|
|
20
|
+
- **Git** — origin remote (credentials stripped), branch, HEAD SHA, dirty flag
|
|
21
|
+
- **Env (allowlist only)** — CI markers (`CI`, `GITHUB_*`, `GITLAB_CI`, …), `NODE_ENV`, `TERM_PROGRAM`, vendor base-URL overrides (`ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, …), Claude Bedrock/Vertex flags. API keys and proxy vars are never read. Gateway URLs have embedded credentials and credential-shaped query params (`key`, `api-key`, `token`, `sig`, …) redacted.
|
|
22
|
+
- **MCP inventory** — declared server **names** (and discovery source), not commands/URLs/env
|
|
23
|
+
- **User-scope config** — `cli_version` (claude / agent|cursor-agent / codex `--version`); model (Claude; SessionStart stdin `model` preferred over settings-file), Claude-only `claude_version` mirror; presence/size of global settings / memory / MCP files; name+size listings of global skills, commands, subagents, rules (full listing when the hash changed or every 10th session start as a repair re-send; capped at 500 entries per list with `truncated: true` when capped). File **contents** are not sent.
|
|
24
|
+
- **Permission keys (Claude)** — each rule as `{ key, mode }` where `mode` is the list it came from (`allow` / `deny` / `ask`), so a blocked rule is never mistaken for a permitted one. Absolute POSIX / Windows / `~`-anchored paths inside a rule are replaced with `<path>` (`Bash(find /Users/me/repo -name x)` → `Bash(find <path> -name x)`) — the rule shape is the posture signal, the operator's home layout is not. Capped at 200 rules, 200 characters each.
|
|
25
|
+
- **Allowlisted hook metadata** — see below (includes SessionStart `model`, `permission_mode`, …)
|
|
26
|
+
|
|
27
|
+
### On `session_end`
|
|
28
|
+
|
|
29
|
+
- Stop `status` / `stop_reason` / SessionEnd `reason`, and Claude `StopFailure` `error` only when it is identifier-shaped (`rate_limit`); error prose is dropped, leaving `status` null
|
|
30
|
+
- **`model`** (Claude only, when readable) — last assistant `message.model` from `transcript_path` on Stop/SessionEnd; reflects mid-session `/model` or consent fallback, not just SessionStart settings snapshot
|
|
31
|
+
- Allowlisted hook metadata (see below)
|
|
32
|
+
|
|
33
|
+
### On `tool_invoked`
|
|
34
|
+
|
|
35
|
+
- `tool_name`, `skill_name`
|
|
36
|
+
- **Cursor `skill_name` (derived, not from a native Skill tool):** slash commands via `beforeSubmitPrompt` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt text is never POSTed); Read of `**/skills/<id>/SKILL.md` on `preToolUse` / `postToolUse` (skill id only — file path is never POSTed). Silent auto-inject with no slash and no SKILL.md Read is not observable.
|
|
37
|
+
- **Claude `skill_name` (derived):** slash commands via `UserPromptExpansion` (`command_name` → `tool_name: Skill`, `skill_name: <id>`; `prompt` / `command_args` are never POSTed). Typing `/skillname` bypasses `PreToolUse(Skill)`; Read of `**/skills/<id>/SKILL.md` on Pre/Post tool hooks is also derived when present.
|
|
38
|
+
- **Codex `skill_name` (derived):** slash / custom-prompt ids via `UserPromptSubmit` (`/my-skill` → `tool_name: Skill`, `skill_name: my-skill`; prompt text is never POSTed); Bash/Shell whose `tool_input.command` references `.claude/.cursor/.agents/skills/<id>/SKILL.md` or `.codex/commands/<id>.md` (id only — command string never POSTed on allowlist); Read-like tools of those paths when present. Silent auto-inject with no slash and no skill-file touch is not observable.
|
|
39
|
+
- `tool_use_id` — opaque Claude Pre/Post join key when present in stdin
|
|
40
|
+
- `success` — `true` on Claude/Cursor `PostToolUse` / `postToolUse`, `false` on `PostToolUseFailure` / `postToolUseFailure` / Claude `PermissionDenied`, otherwise `null`
|
|
41
|
+
- `failure_reason` — when `success === false`, a **failure class, never a runner message**: `interrupted` (stdin `is_interrupt`), `permission_denied` (Claude `PermissionDenied`), an identifier-shaped `failure_type` / `error_type` (`timeout`, `ShellError`), else the fixed value `error`. Runner `error` / `error_message` strings are dropped — they carry raw tool stderr including absolute paths and whole result bodies.
|
|
42
|
+
- `duration_ms` — from Claude `duration_ms` or Cursor `duration` on post-tool stdin
|
|
43
|
+
- `mcp_server` / `mcp_tool` — derived from opaque `tool_name` when it matches Claude `mcp__<server>__<tool>` or Cursor `MCP:<server>:<tool>` / `MCP:<tool>` (no `tool_input` parsing)
|
|
44
|
+
- `subagent_type` — on subagent start/stop hooks, which carry an agent type instead of a `tool_name`: Claude `agent_type` (`Explore`, plugin-scoped `my-plugin:reviewer`) or Cursor `subagent_type` (`generalPurpose`, `explore`). Only identifier-shaped values are sent; the neighbouring `task` / `description` / `summary` fields are free prose and are never captured
|
|
45
|
+
- Allowlisted hook metadata (see below)
|
|
46
|
+
|
|
47
|
+
### Hook metadata (default)
|
|
48
|
+
|
|
49
|
+
From runner stdin, only: session/conversation/turn ids (`prompt_id` / `prompt.id` / `generation_id` / `turn_id`), event name/source, status / `stop_reason` / SessionEnd `reason`, tool/skill name, `tool_use_id`, `permission_mode`, active `model` / Cursor `model_id`, `agent_id` / `agent_type`, Cursor `subagent_id` / `subagent_type` / `subagent_model` / `is_parallel_worker`, `effort` / `effort.level`, `duration_ms` / `duration`, failure category fields (`failure_type`, `is_interrupt` — the message fields `error`, `error_details`, `error_message` are **not** forwarded), Claude `UserPromptExpansion` metadata (`command_name`, `expansion_type`, `command_source` — never `prompt` / `command_args`), and git-root-relative `cwd_relative` (never absolute `cwd`). Session titles, tool args, prompts, shell commands, file contents, and tool result bodies are dropped unless the operator opts into full capture (`capture_hook_input: "full"` or `EVALON_HOOK_CAPTURE_FULL_INPUT=1`).
|
|
50
|
+
|
|
51
|
+
Install wires Claude `SessionStart` / `UserPromptExpansion` / `PreToolUse` (all tools) / `PostToolUse` / `PostToolUseFailure` / `PermissionDenied` / `Stop` / `StopFailure` / `SessionEnd` / `SubagentStart` / `SubagentStop`; Cursor `sessionStart` / `beforeSubmitPrompt` / `preToolUse` / `postToolUse` / `postToolUseFailure` / `subagentStart` / `subagentStop` / `stop`; Codex `SessionStart` / `UserPromptSubmit` / `PreToolUse` (all tools) / `PostToolUse` / `Stop`. Re-run `install` to upgrade existing repos (Cursor upgrades strip retired `beforeShellExecution` / `afterFileEdit` / `beforeMCPExecution` Evalon hooks).
|
|
52
|
+
|
|
53
|
+
## PII we do not send
|
|
54
|
+
|
|
55
|
+
Username and hostname *are* sent (per-member usage for the installing org). The following are not:
|
|
56
|
+
|
|
57
|
+
- Prompts, chat transcripts, session titles, and keystrokes
|
|
58
|
+
- Tool arguments, shell commands, and file / code contents
|
|
59
|
+
- Tool error messages, stderr, and result bodies (a failure **class** is sent, never the message)
|
|
60
|
+
- Contents of skills, rules, commands, memory, or settings files (names and sizes only)
|
|
61
|
+
- API keys, tokens, and other secret env vars
|
|
62
|
+
- HTTP(S) proxy credentials and proxy env vars
|
|
63
|
+
- Embedded credentials in git remotes and gateway URLs (`user:pass@` stripped; credential-shaped query params redacted)
|
|
64
|
+
- MCP server commands, args, env blocks, and URLs (name/type only)
|
|
65
|
+
- Email addresses, full names, or employee IDs beyond what appears in OS username / hostname
|
|
66
|
+
- Home-directory paths (including inside Claude permission rules), local IP addresses, and listening ports
|
|
67
|
+
- Data to any endpoint other than the org-configured collector
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @evalon-ai/agent-hook
|
|
2
|
+
|
|
3
|
+
Cross-runner runtime telemetry hook for **Claude Code**, **Cursor**, and **OpenAI Codex**. Each session (and selected tool/MCP events) POSTs a runtime envelope to your Evalon collector at `POST /{orgToken}/hook`.
|
|
4
|
+
|
|
5
|
+
## What this is (and isn't)
|
|
6
|
+
|
|
7
|
+
This is an **opt-in, first-party observability agent** that an Evalon AI customer installs on their own developers' machines / CI to get an inventory and runtime view of **their own org's AI coding-agent usage**. It is the AI-coding-agent equivalent of an EDR/telemetry sensor or an APM agent: the org deploys it, on its own hosts, to send data **to its own collector**.
|
|
8
|
+
|
|
9
|
+
- **Data goes to the customer's own collector.** The destination is the org's Evalon endpoint (default `https://hooks.evalon.ai/{orgToken}/hook`, or a self-hosted collector via `--url`). It is not a third party from the org's perspective — the customer is the data controller.
|
|
10
|
+
- **It is consent-based and removable.** Nothing installs silently: the operator enables the Evalon plugins (or runs file-wiring `install`), and uninstall / plugin disable removes Evalon wiring. Non-managed hooks can be removed locally by the developer at any time.
|
|
11
|
+
- **Collection is deliberately minimized.** `hook_input` defaults to an allowlist of safe metadata (session/tool ids, status) — tool arguments, prompts, file contents, and shell commands are dropped unless the operator explicitly opts into full capture. `env_vars` is a fixed allowlist of non-secret CI/runtime markers; API keys and proxy vars are never read. See [Privacy defaults](#privacy-defaults).
|
|
12
|
+
- **What it is not:** it is not spyware, not a keylogger, and not a data broker. It does not collect keystrokes, file contents, or credentials, and it does not exfiltrate to any endpoint the operator did not configure.
|
|
13
|
+
|
|
14
|
+
## Registry access
|
|
15
|
+
|
|
16
|
+
The package is distributed on **GitHub Packages**, so every machine that installs it needs these two lines in `~/.npmrc` with a token that can read packages from the `evalon-ai` org:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
@evalon-ai:registry=https://npm.pkg.github.com
|
|
20
|
+
//npm.pkg.github.com/:_authToken=<TOKEN>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Put the token only on the `_authToken` line — never inside the registry URL.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
**Primary path:** enable the Evalon **runner plugins** (Claude / Cursor / Codex) from the hosted marketplace [`evalon-ai/marketplace`](https://github.com/evalon-ai/marketplace). Their hooks pin `npx -y @evalon-ai/agent-hook@<ver> --runner …`. Your repo commits only a shared `collector_url` manifest plus plugin enablement — not per-event blocks in `.claude/settings.json` / `.cursor/hooks.json` / `.codex/hooks.json`.
|
|
28
|
+
|
|
29
|
+
**Fallback:** the `install` CLI merges hooks into those three config files directly.
|
|
30
|
+
|
|
31
|
+
### Primary — plugin + npm
|
|
32
|
+
|
|
33
|
+
1. Write the org destination (manifest only):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @evalon-ai/agent-hook@X.Y.Z configure \
|
|
37
|
+
--url https://hooks.evalon.ai/<your-org-path-id>/hook
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`--url` must be the full ingest URL ending in `/{orgToken}/hook`. Prefer a long opaque hex org path id (64 lowercase hex chars). Safe to commit in private repos — auth is in the URL path, not a Bearer header.
|
|
41
|
+
|
|
42
|
+
2. Enable the Claude / Cursor / Codex plugins per [`evalon-ai/marketplace`](https://github.com/evalon-ai/marketplace).
|
|
43
|
+
3. Commit the shared manifest (`.claude/evalon-runtime-report.json`) plus plugin enablement **or** the file-wired hook configs (not both for the same events). Restart runners.
|
|
44
|
+
|
|
45
|
+
Plugins pin an exact package version, never `@latest`.
|
|
46
|
+
|
|
47
|
+
**Migrating from file-wiring:** strip Evalon per-event blocks while keeping the manifest:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @evalon-ai/agent-hook@X.Y.Z migrate-to-plugin
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then enable plugins. Do not leave both plugin and file hooks active — that double-POSTs.
|
|
54
|
+
|
|
55
|
+
**Codex:** plugin hooks stay inactive until trusted (`/hooks` in the CLI) or deployed as managed hooks.
|
|
56
|
+
|
|
57
|
+
**Windows:** not supported in v1. Use macOS/Linux.
|
|
58
|
+
|
|
59
|
+
### Fallback — file wiring (`install`)
|
|
60
|
+
|
|
61
|
+
One-time wiring by the repo owner. Teammates pull committed hook configs and need **Node only**.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @evalon-ai/agent-hook@latest install \
|
|
65
|
+
--url https://hooks.evalon.ai/<your-org-path-id>/hook
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When Claude Code already has native OTEL enabled (managed/user/project settings), install **skips** the Claude hook to avoid duplicate events. Cursor and Codex are still wired. Override with `--force`:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npx @evalon-ai/agent-hook@latest install \
|
|
72
|
+
--url https://hooks.evalon.ai/<token>/hook \
|
|
73
|
+
--force
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Skip a vendor entirely with `--no-claude`, `--no-cursor`, or `--no-codex`:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npx @evalon-ai/agent-hook@latest install \
|
|
80
|
+
--url https://hooks.evalon.ai/<token>/hook \
|
|
81
|
+
--no-cursor --no-codex
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Wired command shape (auth is in the committed `collector_url`). The per-event command is **pinned to the installer's version** (not `@latest`):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npx -y @evalon-ai/agent-hook@1.0.6 --runner cursor
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
You can optionally put the collector URL on the hook command itself — `--url` (or `--token` + optional `--base-url`) overrides any shared manifest, so `.claude/evalon-runtime-report.json` is not required:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"command": "npx -y @evalon-ai/agent-hook@1.0.6 --runner claude --url 'https://hooks.evalon.ai/<orgToken>/hook'"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Commit** `.claude/evalon-runtime-report.json` (contains `collector_url`) plus `.claude/settings.json`, `.cursor/hooks.json`, and `.codex/hooks.json` when using this path (or omit the manifest if every wired command already has `--url`). Do **not** also enable the Evalon plugins for the same events — that double-posts.
|
|
99
|
+
|
|
100
|
+
Install is **additive**: existing non-Evalon hooks are kept; re-install replaces only Evalon-owned entries. Uninstall removes only Evalon entries (and the Evalon manifest).
|
|
101
|
+
|
|
102
|
+
### Alternative — CDN binary (no Node at hook time)
|
|
103
|
+
|
|
104
|
+
For environments where Node is not on PATH when runners fire hooks (typically with file-wiring or a future plugin that bundles a binary):
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
curl -fsSL https://cdn.evalon.ai/agent-hook/install.sh | sh -s -- -- \
|
|
108
|
+
--url https://hooks.evalon.ai/<token>/hook
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Pin a specific release:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
AGENT_HOOK_VERSION=1.0.6 curl -fsSL https://cdn.evalon.ai/agent-hook/install.sh | sh -s -- -- \
|
|
115
|
+
--url https://hooks.evalon.ai/<token>/hook
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The installer downloads a **standalone binary** (~50–100MB) for your OS/arch, verifies SHA256, caches it under `~/.evalon/agent-hook/<version>/`, copies it into the git repo at `.evalon/agent-hook/evalon-agent-hook`, and wires all three runners with `--delivery binary` (default for the compiled installer).
|
|
119
|
+
|
|
120
|
+
Wired command shape (git-root-relative path + `--runner`):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
"$(git rev-parse --show-toplevel)/.evalon/agent-hook/evalon-agent-hook" --runner cursor
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Install per machine** for the binary path: each collaborator runs install once to download their platform binary into `.evalon/agent-hook/` (gitignored).
|
|
127
|
+
|
|
128
|
+
Force npx delivery from Node even when a global binary pin exists:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx @evalon-ai/agent-hook@latest install \
|
|
132
|
+
--url https://hooks.evalon.ai/<token>/hook \
|
|
133
|
+
--delivery npx
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Identity and runtime notes
|
|
137
|
+
|
|
138
|
+
Each developer's `installation_id` is created on first hook fire and stored under `~/.evalon/agent-hook/installations/<repo-key>.json`.
|
|
139
|
+
|
|
140
|
+
Per-event execution uses `npx` (primary) or the pinned binary (CDN path). The runner-facing process **acks immediately** (`{"continue":true}` + exit 0) after draining stdin and spawning a detached `__work` child that collects + POSTs — **except teardown events**, which stay in-process and await the HTTP POST so a runner that tears down the process tree on exit cannot kill the worker mid-flight. Teardown = Claude `SessionEnd` / `StopFailure`, Codex `Stop` (its only end signal), and `SubagentStop`. Per-turn Claude `Stop` / Cursor `stop` fire at the end of **every turn** (not teardown), so they detach like tool events and never block the runner between turns. Set `EVALON_HOOK_SYNC=1` to keep all events in-process for debugging.
|
|
141
|
+
|
|
142
|
+
### Gitignore (recommended)
|
|
143
|
+
|
|
144
|
+
```gitignore
|
|
145
|
+
/.evalon/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Do **not** gitignore `.claude/evalon-runtime-report.json` when using a committed org path id — teammates need it to post without env vars. Keep the repo private; rotate the path id via your org settings if it leaks.
|
|
149
|
+
|
|
150
|
+
For the **plugin** path, do not add Evalon per-event hooks to `.claude/settings.json` / `.cursor/hooks.json` / `.codex/hooks.json`. For the **file-wiring** fallback, add those files only if your repo does not already track other hook settings you need to share.
|
|
151
|
+
|
|
152
|
+
## Fail-open contract
|
|
153
|
+
|
|
154
|
+
- Always prints `{"continue":true}` to stdout and exits `0`
|
|
155
|
+
- High-frequency and per-turn events (incl. Claude `Stop` / Cursor `stop`): collect + POST in a detached `__work` child so the runner is not blocked
|
|
156
|
+
- Teardown events stay in-process (sync POST) — Claude `SessionEnd` / `StopFailure`, Codex `Stop`, and `SubagentStop`, because the runner may kill detached children when the session/worker tree is torn down
|
|
157
|
+
- POST / spawn failures log to stderr only
|
|
158
|
+
|
|
159
|
+
Debug envelope logging: `EVALON_HOOK_DEBUG=1` (redacted summary). Full envelope: `EVALON_HOOK_DEBUG=full`. Force all events in-process: `EVALON_HOOK_SYNC=1`.
|
|
160
|
+
|
|
161
|
+
## Privacy defaults
|
|
162
|
+
|
|
163
|
+
Full field-by-field inventory of what is POSTed: [COLLECTED-DATA.md](./COLLECTED-DATA.md).
|
|
164
|
+
|
|
165
|
+
- **`hook_input`**: allowlisted metadata only (`session_id`, `tool_name`, `status`, …). Tool args / prompts / session titles are dropped. Opt-in full stdin: untracked operator manifest `capture_hook_input: "full"` or `EVALON_HOOK_CAPTURE_FULL_INPUT=1` (still size-capped). A **git-tracked** (committed) manifest cannot set `full` — use the env var or an untracked operator manifest instead.
|
|
166
|
+
- **`env_vars`**: fixed allowlist only (CI markers, `GITHUB_*` context, vendor base-URL overrides). Gateway URL values (`*_BASE_URL`, etc.) are sent with host/path and non-sensitive query keys for proxy-substitution detection; embedded URL credentials (`user:pass@`) and credential-shaped query params (`key`, `api-key`, `token`, `sig`, …) are redacted. API keys and HTTP proxy vars are never read.
|
|
167
|
+
- **Transport**: collector URL must be `https` (or `http://localhost` / `127.0.0.1`, which is allowed only so a collector can be exercised on the same machine). Cleartext remote URLs are blocked (fail-open, no POST).
|
|
168
|
+
|
|
169
|
+
## Uninstall
|
|
170
|
+
|
|
171
|
+
**Plugin path:** disable / uninstall the Evalon marketplace plugins, then remove or rotate the shared manifest.
|
|
172
|
+
|
|
173
|
+
**File-wiring → plugin:** keep the manifest, strip only Evalon file hooks:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npx @evalon-ai/agent-hook migrate-to-plugin
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**File-wiring full uninstall** (also deletes the shared manifest):
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npx @evalon-ai/agent-hook uninstall
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Or via the CDN-installed binary:
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
"$HOME/.evalon/agent-hook/current/evalon-agent-hook" uninstall
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Or:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
curl -fsSL https://cdn.evalon.ai/agent-hook/install.sh | sh -s -- uninstall
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`uninstall` removes Evalon-owned hook entries from Claude/Cursor/Codex config files (leaves other hooks intact) and deletes `evalon-runtime-report.json` manifest(s) in the current git repo. Does not remove `~/.evalon/agent-hook/` (pinned binary and per-machine identity).
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import{realpathSync as $s}from"node:fs";import{resolve as Bs}from"node:path";import{fileURLToPath as Ks,pathToFileURL as Gs}from"node:url";var Ln="https://hooks.evalon.ai";function Z(e,t=Ln){let n=t.replace(/\/+$/,""),o=e.trim();return`${n}/${o}/hook`}var D=".claude/evalon-runtime-report.json",Ye="node_modules/@evalon/agent-hook/dist/hook.mjs",P="evalon-agent-hook",Xe="https://cdn.evalon.ai/agent-hook",ye=`current/${P}`,Se=`.evalon/agent-hook/${P}`,qe=5e3,Ze=5e3,Ee=3e3;var ve=3e3,Qe=1500,et=4*1024,tt=32*1024,Un=4*1024,b="1.0.6",Q=`@evalon-ai/agent-hook@${b}`,nt="Evalon runtime report",ot=["claude","cursor","codex"];import{chmodSync as xt,cpSync as bt,existsSync as C,mkdirSync as Pe,readdirSync as go,readFileSync as ho,rmSync as ae,symlinkSync as _o,writeFileSync as ko}from"node:fs";import{homedir as A}from"node:os";import{basename as yo,dirname as Te,isAbsolute as So,join as f,relative as Rt,resolve as It,sep as Eo}from"node:path";import{fileURLToPath as vo}from"node:url";var jn=new Set(["localhost","127.0.0.1","[::1]","::1"]),rt=/^[A-Za-z0-9._~-]+$/;function st(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n<=32||n===127||n===39)return!0}return!1}function ee(e){try{let t=new URL(e);return t.protocol==="https:"?!0:t.protocol==="http:"?jn.has(t.hostname):!1}catch{return!1}}function $(e){let t;try{t=new URL(e)}catch{return`invalid URL (got: ${e})`}if(!ee(e))return`must be https (or http on localhost / 127.0.0.1 / ::1) (got: ${e})`;let n=Fn(e);if(!n)return`must end with /{orgToken}/hook (got: ${e})`;if(!rt.test(n))return`org token must match ${rt.source} (got: ${n})`;if(st(e))return`must not contain whitespace, control characters, or single quotes (got: ${e})`;if(st(t.pathname))return`pathname must not contain whitespace, control characters, or single quotes (got: ${e})`}function B(e){return new URL(e).toString()}function Fn(e){try{let n=new URL(e).pathname.split("/").filter(o=>o.length>0);if(n.length>=2&&n[n.length-1]==="hook"){let o=n[n.length-2];return o.length>0?o:void 0}return}catch{return}}function we(e){try{let t=new URL(e),n=t.pathname.split("/").filter(o=>o.length>0);return n.length>=2&&n[n.length-1]==="hook"&&(n[n.length-2]="***",t.pathname=`/${n.join("/")}`),t.toString()}catch{return"<invalid-url>"}}function it(e){if(e.collector_url)return e.collector_url;let t=e.repo_ingest_token?.trim();return t?Z(t):""}import{createHash as Wn,randomBytes as Vn}from"node:crypto";import{existsSync as zn,mkdirSync as Yn,readFileSync as Xn,writeFileSync as qn}from"node:fs";import{homedir as oe}from"node:os";import{dirname as Zn,join as xe,resolve as Qn}from"node:path";import{randomBytes as Dn}from"node:crypto";import{existsSync as ut,mkdirSync as $n,readdirSync as Bn,renameSync as at,statSync as dt,unlinkSync as ct,writeFileSync as Kn}from"node:fs";import{basename as Gn,dirname as lt,join as ft,relative as Jn}from"node:path";function l(e){process.stderr.write(`[evalon-agent-hook] ${e}
|
|
4
|
+
`)}function L(e,t){$n(lt(e),{recursive:!0});let n=ft(lt(e),`.${Gn(e)}.${process.pid}.${Dn(4).toString("hex")}.tmp`);try{Kn(n,t,"utf8");try{at(n,e)}catch(o){let r=o instanceof Error&&"code"in o?o.code:void 0;if(r==="EEXIST"||r==="EPERM"||r==="EACCES"){ct(e),at(n,e);return}throw o}}catch(o){try{ct(n)}catch{}throw o}}function pt(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function mt(){process.stdout.write('{"continue":true}')}function g(e){let t=process.env[e];return t===void 0||t===""?void 0:t}async function te(){return new Promise(e=>{let t="";if(process.stdin.isTTY){e("");return}process.stdin.setEncoding("utf8"),process.stdin.on("data",n=>{t+=n}),process.stdin.on("end",()=>{e(t)}),process.stdin.on("error",()=>{e(t)}),process.stdin.resume()})}async function Ie(e){return new Promise(t=>{let n="",o=!1,r=a=>{o||(o=!0,clearTimeout(s),t(a||"{}"))},s=setTimeout(()=>{r(n)},e);if(process.stdin.isTTY){r("{}");return}process.stdin.setEncoding("utf8"),process.stdin.on("data",a=>{n+=a}),process.stdin.on("end",()=>{r(n)}),process.stdin.on("error",()=>{r(n)}),process.stdin.resume()})}async function R(e,t,n=3e3,o={}){let{execFile:r}=await import("node:child_process"),{promisify:s}=await import("node:util"),a=s(r);try{let{stdout:i}=await a(e,t,{timeout:n,windowsHide:!0,maxBuffer:1048576,cwd:o.cwd});return{ok:!0,stdout:i.trim()}}catch(i){let c=i,u=typeof c.code=="number"?c.code:typeof c.status=="number"?c.status:void 0;return{ok:!1,code:u,spawnFailed:u===void 0}}}async function h(e,t,n=3e3,o={}){let r=await R(e,t,n,o);return r.ok?r.stdout:""}async function w(e=process.cwd()){return await h("git",["rev-parse","--show-toplevel"],2e3,{cwd:e})||e}function _(e,t){try{return JSON.parse(e)}catch{return t}}function K(e){return typeof e=="string"?e:""}function N(e,t){let n=e[t];if(typeof n=="string"||typeof n=="number"||typeof n=="boolean")return n;if(!t.includes("."))return;let o=t.split("."),r=e;for(let s of o){if(r===null||typeof r!="object"||Array.isArray(r))return;r=r[s]}if(typeof r=="string"||typeof r=="number"||typeof r=="boolean")return r}function d(e,t){let n=N(e,t);return typeof n=="string"?n:""}var ne=500;function G(e,t=2){if(!ut(e))return{entries:[],truncated:!1};let n=[],o=!1,r=(s,a)=>{if(a>t||o)return;let i;try{i=Bn(s,{withFileTypes:!0})}catch{return}for(let c of i){if(o)return;let u=ft(s,c.name);if(c.isDirectory()){r(u,a+1);continue}if(c.isFile()){try{n.push({name:Jn(e,u),size:dt(u).size})}catch{continue}if(n.length>=ne){o=!0;return}}}};return r(e,1),{entries:n,truncated:o}}function J(e){return ut(e)?{present:!0,size:dt(e).size}:{present:!1,size:0}}function _t(e,t){return`${e}:${t??""}`}function be(e=oe()){return xe(e,".evalon","agent-hook")}function eo(e=oe()){return xe(be(e),"installations")}function to(e,t=oe()){return xe(eo(t),`${e}.json`)}function gt(){return Vn(8).toString("hex")}async function no(e){let n=await h("git",["config","--get","remote.origin.url"],2e3,{cwd:e})||Qn(e);return Wn("sha256").update(n).digest("hex").slice(0,16)}function oo(e){if(!e||typeof e!="object"||Array.isArray(e))return;let t={};for(let[n,o]of Object.entries(e))typeof o=="string"&&o.length>0&&(t[n]=o);return Object.keys(t).length>0?t:void 0}function re(e){if(zn(e))try{let t=JSON.parse(Xn(e,"utf8"));if(typeof t.installation_id=="string"&&t.installation_id.length>0){let n=oo(t.platform_agent_ids);return{installation_id:t.installation_id,...n?{platform_agent_ids:n}:{}}}}catch{}}function kt(e,t){let o=re(e)?.platform_agent_ids?.[t];return typeof o=="string"&&o.length>0?o:void 0}function yt(e,t,n){if(!n)return;let o=re(e);if(!o||o.platform_agent_ids?.[t]===n)return;let r={installation_id:o.installation_id,platform_agent_ids:{...o.platform_agent_ids,[t]:n}};try{L(e,`${JSON.stringify(r,null,2)}
|
|
5
|
+
`)}catch(s){let a=s instanceof Error?s.message:String(s);l(`WARN: platform_agent_id cache write failed open: ${a}`)}}function ro(e){return e instanceof Error&&"code"in e&&e.code==="EEXIST"}function so(e,t){Yn(Zn(e),{recursive:!0}),qn(e,`${JSON.stringify({installation_id:t},null,2)}
|
|
6
|
+
`,{encoding:"utf8",flag:"wx"})}function ht(e,t){try{return so(e,t),{installation_id:t}}catch(n){if(!ro(n))throw n;return re(e)}}async function se(e,t={}){let n=t.home??oe(),o=await no(e),r=to(o,n),s=re(r);if(s)return{installationId:s.installation_id,storePath:r,repoKey:o};let a=t.overrideId??t.migrateFromManifestId??gt(),i=ht(r,a);if(i)return{installationId:i.installation_id,storePath:r,repoKey:o};let c=ht(r,gt());if(c)return{installationId:c.installation_id,storePath:r,repoKey:o};throw new Error(`Failed to create installation record at ${r}`)}import{existsSync as Re,readFileSync as io}from"node:fs";import{dirname as ao,join as St,relative as co}from"node:path";var ie=[D,".cursor/evalon-runtime-report.json",".codex/evalon-runtime-report.json"];async function lo(e=process.cwd(),t){let n=g("EVALON_HOOK_MANIFEST");if(n&&Re(n))return n;let o=t??await w(e);for(let r of ie){let s=St(o,r);if(Re(s))return s}for(let r of ie){let s=St(e,r);if(Re(s))return s}}async function uo(e,t){let n=t??await w(ao(e)),o=co(n,e),r=await R("git",["ls-files","--error-unmatch",o],2e3,{cwd:n});return r.ok?!0:r.code===1?!1:(l(`WARN: git ls-files failed for manifest ${e} (code=${String(r.code)}); treating as tracked`),!0)}function fo(e){let t=io(e,"utf8");return _(t,{})}function po(){return{schema_version:2,mcp_servers:[]}}async function Et(e=process.cwd()){let t=await w(e),n=await lo(e,t),o=n?fo(n):po(),r,s=n?await uo(n,t):!1;s&&o.installation_id?(l(`WARN: ignoring installation_id in git-tracked manifest ${n}; identity is per-machine under ~/.evalon/agent-hook/installations/`),delete o.installation_id):!s&&o.installation_id&&(r=o.installation_id,delete o.installation_id),o.capture_hook_input==="full"&&n&&s&&(l(`WARN: ignoring capture_hook_input=full in git-tracked manifest ${n}; use EVALON_HOOK_CAPTURE_FULL_INPUT=1 or an untracked operator manifest`),delete o.capture_hook_input);let{installationId:a,storePath:i}=await se(t,{migrateFromManifestId:r});return o.installation_id=a,{manifestPath:n??i,manifest:o,installationId:a,installationStorePath:i,collectorUrl:it(o),platformAgentId:o.platform_agent_id??"",gitRoot:t}}function vt(e){return{schema_version:2,...e?{collector_url:e}:{},mcp_servers:[]}}var mo=["darwin-arm64","darwin-x64","linux-arm64","linux-x64"];function Ce(e=process.platform,t=process.arch){if(e==="darwin")return t==="arm64"?"darwin-arm64":t==="x64"?"darwin-x64":void 0;if(e==="linux")return t==="arm64"?"linux-arm64":t==="x64"?"linux-x64":void 0}function wt(e=process.platform,t=process.arch){let n=Ce(e,t);if(!n)throw new Error(`Unsupported platform ${e}-${t}. Supported: ${mo.join(", ")}.`);return n}function Ae(e){return`compile-out/bin/${e}/evalon-agent-hook`}function wo(e,t){let n=e.collectorUrl?.trim();if(!n)throw new Error(`${t} requires --url <https://hooks.evalon.ai/{orgToken}/hook>`);let o=$(n);if(o)throw new Error(`collector URL rejected: ${o}`);return B(n)}function Io(e){let t=e.collectorUrl?.trim();if(!t)return;let n=$(t);if(n)throw new Error(`collector URL rejected: ${n}`);return B(t)}function xo(e){return wo(e,"install")}function Ct(e){let t={};for(let n=0;n<e.length;n++){let o=e[n];if(o==="--url"){let r=e[n+1];r&&!r.startsWith("--")&&(t.collectorUrl=r,n+=1);continue}if(o==="--collector-url")throw new Error("--collector-url was renamed to --url")}return t}function At(e){let t={};for(let n=0;n<e.length;n++){let o=e[n];if(o==="--url"){let r=e[n+1];r&&!r.startsWith("--")&&(t.collectorUrl=r,n+=1);continue}if(o==="--collector-url")throw new Error("--collector-url was renamed to --url");if(o==="--update")throw new Error("--update was removed; use the `update` command instead of `install --update`");if(o==="--delivery"){let r=e[n+1];(r==="npx"||r==="binary")&&(t.delivery=r,n+=1);continue}if(o==="--force"){t.force=!0;continue}if(o==="--no-claude"){t.skipClaude=!0;continue}if(o==="--no-cursor"){t.skipCursor=!0;continue}if(o==="--no-codex"){t.skipCodex=!0;continue}}return t}function Ot(e=process.execPath){return Pt(e)?"binary":"npx"}function bo(){return f(A(),".evalon","agent-hook")}function Ro(e=A()){return f(e,".evalon","agent-hook",ye)}function Co(){let e=Te(vo(import.meta.url)),t=f(e,"hook.mjs");return C(t)?t:f(e,"..","dist","hook.mjs")}function Pt(e=process.execPath){return yo(e)===P}function Tt(e,t){let n=Rt(It(e),It(t));return!n.startsWith("..")&&!So(n)}function ce(e){return e.split(Eo).join("/")}function Ao(e){return`node "$(git rev-parse --show-toplevel)/${ce(e)}"`}function Oo(e){return`"$(git rev-parse --show-toplevel)/${ce(e)}"`}function Po(e){return f(e,Se)}function Nt(e,t){let n=Po(e);Pe(Te(n),{recursive:!0}),bt(t,n,{force:!0});try{xt(n,493)}catch{}return n}function To(e){let t=f(bo(),"current");try{ae(t,{recursive:!0,force:!0})}catch{}_o(e,t,process.platform==="win32"?"junction":"dir")}function Ht(e,t=b,n=A()){let o=f(n,".evalon","agent-hook",t),r=f(o,P);Pe(o,{recursive:!0}),bt(e,r,{force:!0});try{xt(r,493)}catch{}return To(o),f(n,".evalon","agent-hook",ye)}function Mt(e){let t=Ce();if(!t)return;let n=[f(e,Ae(t)),f(e,"packages","agent-hook",Ae(t))];for(let o of n)if(C(o)&&Tt(e,o))return o}function No(e=A()){let t=Ro(e);return C(t)?t:void 0}function Ho(e,t=A()){let n=No(t);return n||(Pt()?Ht(process.execPath,b,t):Mt(e))}function Mo(e,t=A()){let n=Mt(e);if(!n)throw new Error(`update requires a locally compiled binary \u2014 run this package\u2019s build:hook-binary script first.
|
|
7
|
+
To install a released binary instead: curl -fsSL https://cdn.evalon.ai/agent-hook/install.sh | sh`);let o=Ht(n,b,t),r=Nt(e,n);return{sourcePath:n,projectPath:r,globalPath:o}}function Lo(e){let t=[f(e,"packages","agent-hook","dist","hook.mjs"),f(e,Ye)],n=Co();C(n)&&Tt(e,n)&&t.unshift(n);for(let o of t){if(!C(o))continue;let r=ce(Rt(e,o));return{command:Ao(r),hookPath:r,strategy:"dev_mjs"}}}function Uo(e=Q){return`npx -y ${e}`}function jo(e,t=A(),n=Ot()){if(n==="npx")return{command:Uo(),hookPath:Q,strategy:"npx"};let o=Ho(e,t);if(o){Nt(e,o);let s=ce(Se);return{command:Oo(s),hookPath:s,strategy:"project_binary"}}let r=Lo(e);if(r)return r;throw new Error(`evalon-agent-hook binary not found. Install from CDN:
|
|
8
|
+
curl -fsSL ${Xe}/install.sh | sh
|
|
9
|
+
Or use npx delivery:
|
|
10
|
+
npx ${Q} install`)}function Lt(e){return typeof e!="string"?!1:e.includes("@evalon-ai/agent-hook")||e.includes("@evalon/agent-hook")||e.includes("agent-hook/dist/hook")||e.includes(".evalon/agent-hook")||e.includes(P)||/(?:^|[\s/"'])evalon-agent-hook(?:$|[\s"'])/.test(e)}function Ne(e,t){return`${e.replace(/\s+--runner(?:=|\s+)\S+/g,"").trimEnd()} --runner ${t}`}function T(e,t){Pe(Te(e),{recursive:!0}),ko(e,`${JSON.stringify(t,null,2)}
|
|
11
|
+
`,"utf8")}function I(e){if(C(e))try{return JSON.parse(ho(e,"utf8"))}catch(t){throw t instanceof SyntaxError?new Error(`Invalid JSON in ${e} \u2014 fix or remove the file`,{cause:t}):t}}function Fo(e,t){let n=vt(t.collectorUrl),o={...e,schema_version:e?.schema_version??n.schema_version};return t.collectorUrl!==void 0?o.collector_url=t.collectorUrl:o.collector_url=e?.collector_url??n.collector_url,delete o.repo_ingest_token,delete o.installation_id,o.mcp_servers??=n.mcp_servers,o}function Do(e){return(e.schema_version??2)===2&&e.collector_url===void 0&&(e.mcp_servers?.length??0)===0&&e.platform_agent_id===void 0&&e.capture_hook_input===void 0&&e.repo_ingest_token===void 0}function Ut(e,t){let n=f(e,D),o=I(n),r=Fo(o,t);return t.collectorUrl!==void 0||C(n)||!Do(r)?(T(n,r),{manifestPath:n,wrote:!0}):{manifestPath:void 0,wrote:!1}}function Oe(e){if(e===null||typeof e!="object")return!1;let t=e,n=String(t.CLAUDE_CODE_ENABLE_TELEMETRY??"").toLowerCase();return n==="1"||n==="true"?!0:Object.keys(t).some(o=>o.startsWith("OTEL_"))}function $o(e=process.platform){switch(e){case"darwin":return"/Library/Application Support/ClaudeCode";case"win32":return f(process.env.ProgramFiles??"C:\\Program Files","ClaudeCode");default:return"/etc/claude-code"}}function Bo(e=process.platform){let t=$o(e),n=[f(t,"managed-settings.json")],o=f(t,"managed-settings.d");try{if(!C(o))return n;let r=go(o).filter(s=>s.endsWith(".json")&&!s.startsWith(".")).sort().map(s=>f(o,s));n.push(...r)}catch{}return n}function Ko(e){if(Oe(process.env))return!0;let t=A();return[...Bo(),f(t,".claude","settings.json"),f(t,".claude","remote-settings.json"),f(e,".claude","settings.json"),f(e,".claude","settings.local.json")].some(o=>{try{let r=I(o);return r?Oe(r.env):!1}catch{return!1}})}function Go(e){try{let t=I(f(e,".claude","settings.json"));return t?Oe(t.env):!1}catch{return!1}}function jt(e){return Array.isArray(e)?e:[]}function le(e){let t=[],n=o=>!!o&&typeof o=="object"&&Lt(o.command);for(let o of jt(e)){if(!o||typeof o!="object"){t.push(o);continue}let r=Array.isArray(o.hooks)?o.hooks:[],s=r.filter(a=>!n(a));if(!(s.length===0&&r.some(n))){if(s.length!==r.length){t.push({...o,hooks:s});continue}t.push(o)}}return t}function Ft(e,t){return[...le(e),...t]}function He(e){return Array.isArray(e)?e.filter(t=>!t||typeof t!="object"?!0:!Lt(t.command)):[]}function Jo(e,t){return[...He(e),{command:t}]}function Wo(e){let t=()=>({hooks:[{type:"command",command:e}]});return{SessionStart:[t()],UserPromptExpansion:[t()],PreToolUse:[t()],PostToolUse:[t()],PostToolUseFailure:[t()],PermissionDenied:[t()],Stop:[t()],StopFailure:[t()],SessionEnd:[t()],SubagentStart:[t()],SubagentStop:[t()]}}function Vo(e){let t={type:"command",command:e,statusMessage:nt},n=()=>({hooks:[t]});return{SessionStart:[n()],UserPromptSubmit:[n()],PreToolUse:[n()],PostToolUse:[n()],Stop:[n()]}}var zo=["sessionStart","beforeSubmitPrompt","preToolUse","postToolUse","postToolUseFailure","subagentStart","subagentStop","stop"],Yo=["beforeMCPExecution","beforeShellExecution","afterFileEdit"];function Xo(e){let t={...e};for(let n of Yo){if(!(n in t))continue;let o=He(t[n]);o.length===0?delete t[n]:t[n]=o}return t}function qo(e,t,n={}){let o=f(e,".claude/settings.json"),r=Ne(t,"claude"),s=I(o)??{};if(Ko(e)&&!n.force){if(s.hooks&&typeof s.hooks=="object"){let p={...s.hooks};for(let m of Object.keys(p))p[m]=le(p[m]),jt(p[m]).length===0&&delete p[m];Object.keys(p).length===0?delete s.hooks:s.hooks=p,T(o,s)}return!1}let i=s.hooks&&typeof s.hooks=="object"?s.hooks:{},c=Wo(r),u={...i};for(let[p,m]of Object.entries(c))u[p]=Ft(i[p],m);return s.hooks=u,T(o,s),!0}function Zo(e,t){let n=f(e,".cursor/hooks.json"),o=Ne(t,"cursor"),r=I(n)??{},s=r.hooks&&typeof r.hooks=="object"?r.hooks:{},a=Xo({...s});for(let i of zo)a[i]=Jo(a[i],o);T(n,{...r,version:typeof r.version=="number"?r.version:1,hooks:a})}function Qo(e,t){let n=f(e,".codex/hooks.json"),o=Ne(t,"codex"),r=I(n)??{},s=r.hooks&&typeof r.hooks=="object"?r.hooks:{},a=Vo(o),i={...s};for(let[c,u]of Object.entries(a))i[c]=Ft(s[c],u);T(n,{...r,hooks:i})}function er(e){let t=f(e,".claude/settings.json"),n=I(t);if(!n?.hooks||typeof n.hooks!="object")return;let o={...n.hooks},r=!1;for(let s of Object.keys(o)){let a=o[s],i=le(a);JSON.stringify(a)!==JSON.stringify(i)&&(r=!0),i.length===0?delete o[s]:o[s]=i}r&&(Object.keys(o).length===0?delete n.hooks:n.hooks=o,T(t,n),l(`Removed Evalon hooks from ${t}`))}function tr(e){let t=f(e,".cursor/hooks.json"),n=I(t);if(!n?.hooks||typeof n.hooks!="object")return;let o={...n.hooks},r=!1,s=0;for(let a of Object.keys(o)){let i=o[a],c=He(i);JSON.stringify(i)!==JSON.stringify(c)&&(r=!0),c.length===0?delete o[a]:(o[a]=c,s+=c.length)}if(r){if(s===0&&Object.keys(o).length===0&&Object.keys(n).filter(i=>i!=="hooks"&&i!=="version").length===0){ae(t,{force:!0}),l(`Removed ${t}`);return}T(t,{...n,hooks:o}),l(`Removed Evalon hooks from ${t}`)}}function nr(e){let t=f(e,".codex/hooks.json"),n=I(t);if(!n?.hooks||typeof n.hooks!="object")return;let o={...n.hooks},r=!1,s=0;for(let a of Object.keys(o)){let i=o[a],c=le(i);JSON.stringify(i)!==JSON.stringify(c)&&(r=!0),c.length===0?delete o[a]:(o[a]=c,s+=c.length)}if(r){if(s===0&&Object.keys(o).length===0&&Object.keys(n).filter(i=>i!=="hooks").length===0){ae(t,{force:!0}),l(`Removed ${t}`);return}T(t,{...n,hooks:o}),l(`Removed Evalon hooks from ${t}`)}}function Me(e=process.platform,t=process.arch,n={}){if(e==="win32")throw new Error("Windows install is not supported yet. Use macOS/Linux, or wait for phase-2 native commands.");(n.requireBinaryArch??!0)&&wt(e,t)}async function Dt(e={}){let t=xo(e);e.collectorUrl=t;let n=e.skipClaude===!0,o=e.skipCursor===!0,r=e.skipCodex===!0;if(n&&o&&r)throw new Error("at least one runner must be wired; refuse --no-claude --no-cursor --no-codex together");let s=e.delivery??Ot();Me(process.platform,process.arch,{requireBinaryArch:s==="binary"});let a=e.cwd??process.cwd(),i=await w(a),{command:c,hookPath:u,strategy:p}=jo(i,A(),s),m=I(f(i,D)),{installationId:E,storePath:k}=await se(i,{overrideId:e.installationId,migrateFromManifestId:m?.installation_id}),{manifestPath:O,wrote:v}=Ut(i,e),y=[];return n?l("Skipped Claude hook \u2014 --no-claude"):qo(i,c,{force:e.force})?(y.push("Claude"),e.force&&Go(i)&&l("Forced Claude hook despite project native OTEL \u2014 expect duplicate session/tool events")):l("Skipped Claude hook \u2014 native OTEL telemetry already enabled (avoids duplicate events); pass --force to wire anyway"),o?l("Skipped Cursor hook \u2014 --no-cursor"):(Zo(i,c),y.push("Cursor")),r?l("Skipped Codex hook \u2014 --no-codex"):(Qo(i,c),y.push("Codex")),l(`Installed hook (${p}) at ${u}`),l(`Installation identity ${E} at ${k}`),v&&O?l(`Shared manifest written to ${O}`):l("Using package default collector config (no shared manifest file needed)"),l(y.length>0?`Wired ${y.join(", ")} hooks in ${i} (commands include --runner)`:`No runner hooks wired in ${i} (Claude skipped by OTEL; Cursor/Codex excluded via flags)`),p==="npx"?l("Hook commands use npx and may be committed; teammates need Node on PATH only (no install step, no env vars when collector_url is in the shared manifest)."):p==="project_binary"&&l("Hook commands are git-root-relative and may be committed; each collaborator runs install once to populate .evalon/agent-hook/ (gitignored)."),v&&O&&e.collectorUrl&&l("Shared manifest includes collector_url \u2014 commit it with hook configs so teammates post without env vars."),{gitRoot:i,hookPath:u,manifestPath:O,installationStorePath:k,strategy:p}}async function $t(e=process.cwd()){Me(process.platform,process.arch,{requireBinaryArch:!0});let t=await w(e),{projectPath:n,globalPath:o}=Mo(t);l(`Refreshed ${o}`),l(`Refreshed ${n}`),l("Hook configs and shared manifest left unchanged")}async function Bt(e={}){let t=Io(e);e.collectorUrl=t,Me(process.platform,process.arch,{requireBinaryArch:!1});let n=e.cwd??process.cwd(),o=await w(n),{manifestPath:r,wrote:s}=Ut(o,e);if(t!==void 0&&(!r||!s))throw new Error("configure failed to write shared collector manifest");return s&&r?l(`Wrote ${r}`):t===void 0&&l("No --url given and no shared manifest yet \u2014 pass --url <\u2026/{orgToken}/hook> to write collector destination."),l("Manifest only \u2014 enable Evalon marketplace plugins (https://github.com/evalon-ai/marketplace). Do not also run file-wiring install for the same events."),{gitRoot:o,manifestPath:r}}function Kt(e){for(let t of[()=>er(e),()=>tr(e),()=>nr(e)])try{t()}catch(n){let o=n instanceof Error?n.message:String(n);l(`WARN: strip skipped a hook file: ${o}`)}}async function Gt(e=process.cwd()){let t=await w(e);return Kt(t),l("Stripped Evalon file-wired hooks; shared collector manifest left in place."),l("Next: enable Evalon marketplace plugins (https://github.com/evalon-ai/marketplace), then restart runners."),{gitRoot:t}}async function Jt(e=process.cwd()){let t=await w(e);Kt(t);for(let n of ie){let o=f(t,n);if(C(o))try{ae(o,{force:!0}),l(`Removed ${o}`)}catch(r){let s=r instanceof Error?r.message:String(r);l(`WARN: uninstall could not remove ${o}: ${s}`)}}}var or={claude:".claude",cursor:".cursor",codex:".codex"};function ue(e){let n=e.replace(/^\/+|\/+$/g,"").toLowerCase().split("/").filter(Boolean);return n.length<2?"":n.join("/")}function rr(e){if(!e)return"";let t=e.replace(/^([a-z]+:\/\/)[^/@]+@/i,"$1");t=t.replace(/\.git$/i,"");let n=t.match(/^git@[^:]+:(.+)$/);if(n?.[1])return ue(n[1]);let o=t.match(/^ssh:\/\/[^/]+\/(.+)$/i);if(o?.[1])return ue(o[1]);t=t.replace(/^https:\/\//i,""),t=t.replace(/^http:\/\//i,""),t=t.replace(/^ssh:\/\//i,"");let r=t.match(/^[^/]+\/(.+)$/);return r?.[1]?ue(r[1]):ue(t)}async function Le(e=process.cwd()){let t=await R("git",["rev-parse","--is-inside-work-tree"],2e3,{cwd:e});if(!t.ok||t.stdout!=="true")return null;let[n,o,r,s]=await Promise.all([R("git",["remote","get-url","origin"],2e3,{cwd:e}),R("git",["branch","--show-current"],2e3,{cwd:e}),R("git",["rev-parse","HEAD"],2e3,{cwd:e}),R("git",["status","--porcelain"],2e3,{cwd:e})]),a=n.ok?n.stdout:null;return{remote:a===null?null:a.replace(/^([a-z]+:\/\/)[^/@]+@/i,"$1"),branch:o.ok?o.stdout:null,head:r.ok?r.stdout:null,dirty:s.ok?s.stdout.length>0:null}}async function Wt(e,t,n,o=process.cwd(),r,s){if(t)return t;let a=or[n];if(!a)return"";let i=_t(n,e.scope_path);if(s){let k=kt(s,i);if(k)return k}let c=r??await Le(o),u=c?.remote?rr(c.remote):"";if(u||(u=process.env.GITHUB_REPOSITORY??""),!u)return"";let p=e.scope_path??"",m=p?`${p}/`:"",E=`${u}:${m}${a}`;return s&&yt(s,i,E),E}import{existsSync as sr,readFileSync as ir}from"node:fs";import{hostname as ar,userInfo as cr}from"node:os";function Vt(){if(g("GITHUB_ACTIONS"))return"github_actions";let e=g("CI");if(e&&e!=="false"&&e!=="0")return"managed";try{if(sr("/.dockerenv")||ir("/proc/1/environ","utf8").includes("container="))return"container"}catch{}return process.stdin.isTTY||process.stdout.isTTY||g("TERM")?"interactive":"cron"}async function zt(){return await h("systemd-detect-virt",[],1e3)}async function Yt(){let e=process.platform==="darwin"?"Darwin":process.platform==="win32"?"Windows_NT":"Linux",t="";if(process.platform==="darwin")t=await h("sw_vers",["-productVersion"],1e3);else if(process.platform==="linux"){try{let{readFileSync:s}=await import("node:fs"),a=s("/etc/os-release","utf8"),i=a.match(/^PRETTY_NAME="?([^"\n]+)"?/m)?.[1],c=a.match(/^VERSION_ID="?([^"\n]+)"?/m)?.[1];t=i??c??""}catch{t=await h("lsb_release",["-sd"],1e3).then(s=>s.replace(/^"|"$/g,""))}t||(t=await h("uname",["-r"],1e3))}else t=await h("ver",[],1e3);let n=process.env.USER??cr().username??"",o=(process.env.SHELL??"").split(/[/\\]/).pop()??"",r="";try{r=await h("tty",[],500),r.includes("not a tty")&&(r="")}catch{r=""}return{os:e,os_version:t||null,user:n||null,shell:o||null,tty:r||null}}function Xt(){return ar()}import{existsSync as lr,readFileSync as ur}from"node:fs";import{homedir as qt}from"node:os";import{join as dr}from"node:path";var fr="cloud-cache.json",pr=1440*60*1e3,mr=900*1e3;function gr(e=qt()){return dr(be(e),fr)}function hr(e){if(!lr(e))return;let t=_(ur(e,"utf8"),{});if(!(typeof t.context!="string"||typeof t.probed_at!="number"||t.result!==null&&t.result!==void 0&&typeof t.result!="object"))return{context:t.context,probed_at:t.probed_at,result:t.result??null}}function _r(e,t){try{L(e,`${JSON.stringify(t)}
|
|
12
|
+
`)}catch(n){let o=n instanceof Error?n.message:String(n);l(`WARN: cloud cache write failed open: ${o}`)}}function kr(e,t,n){if(!e||e.context!==t)return!1;let o=e.result===null?mr:pr;return n-e.probed_at<o}async function yr(e){let t=await fetch("http://169.254.169.254/latest/api/token",{method:"PUT",headers:{"X-aws-ec2-metadata-token-ttl-seconds":"60"},signal:AbortSignal.timeout(e)}).catch(()=>{});if(!t?.ok)return;let n=(await t.text()).trim();if(!n)return;let o={"X-aws-ec2-metadata-token":n},[r,s]=await Promise.all([fetch("http://169.254.169.254/latest/meta-data/placement/region",{headers:o,signal:AbortSignal.timeout(e)}).catch(()=>{}),fetch("http://169.254.169.254/latest/meta-data/instance-id",{headers:o,signal:AbortSignal.timeout(e)}).catch(()=>{})]);if(!r?.ok&&!s?.ok)return;let a=r?.ok?(await r.text()).trim():"",i=s?.ok?(await s.text()).trim():"";if(!(!a&&!i))return{provider:"aws",region:a,instance_id:i}}async function Sr(e){let t=await fetch("http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true",{headers:{"Metadata-Flavor":"Google"},signal:AbortSignal.timeout(e)}).catch(()=>{});if(!t?.ok)return;let n=await t.json(),o=typeof n.zone=="string"?n.zone.split("/").pop()??"":"",r=n.id===void 0?"":String(n.id);if(!(!o&&!r))return{provider:"gcp",region:o,instance_id:r}}async function Er(e){let t=await fetch("http://169.254.169.254/metadata/instance?api-version=2021-02-01",{headers:{Metadata:"true"},signal:AbortSignal.timeout(e)}).catch(()=>{});if(!t?.ok)return;let n=await t.json(),o=n.compute?.location??"",r=n.compute?.vmId??"";if(!(!o&&!r))return{provider:"azure",region:o,instance_id:r}}async function vr(e){let t=await Promise.allSettled([yr(e),Sr(e),Er(e)]);for(let n of t)if(n.status==="fulfilled"&&n.value)return n.value;return null}async function Zt(e,t){if(e==="interactive")return null;let n=t?.home??qt(),o=t?.timeoutMs??1e3,r=t?.nowMs??Date.now(),s=gr(n),a=hr(s);if(kr(a,e,r))return a.result;let i=await vr(o);return _r(s,{context:e,probed_at:r,result:i}),i}import{existsSync as wr,readFileSync as Ir}from"node:fs";import{homedir as xr}from"node:os";import{join as Ue}from"node:path";function je(e,t,n){if(!wr(e))return;let o=_(Ir(e,"utf8"),{});for(let r of Object.keys(o.mcpServers??{}))n.set(r,{name:r,source:t})}async function Qt(e,t=process.cwd(),n="claude",o=xr()){let r=new Map;for(let s of e.mcp_servers??[])s.name&&r.set(s.name,{name:s.name,source:"manifest"});return n==="claude"&&je(Ue(o,".claude.json"),"claude_json_user",r),je(Ue(t,".mcp.json"),"mcp_json",r),je(Ue(t,".cursor/mcp.json"),"cursor_mcp",r),[...r.values()]}var br=["CI","GITHUB_ACTIONS","GITLAB_CI","CIRCLECI","BUILDKITE","TF_BUILD","GITHUB_REPOSITORY","GITHUB_REF","GITHUB_SHA","GITHUB_RUN_ID","GITHUB_WORKFLOW","RUNNER_OS","RUNNER_ARCH","NODE_ENV","TERM_PROGRAM","ANTHROPIC_BASE_URL","OPENAI_BASE_URL","OPENAI_API_BASE","AZURE_OPENAI_ENDPOINT","GOOGLE_GEMINI_BASE_URL","GEMINI_API_BASE","CLAUDE_CODE_USE_BEDROCK","CLAUDE_CODE_USE_VERTEX"],Rr=new Set(["key","api-key","api_key","apikey","token","sig","signature","access_token","password","pwd","secret","client_secret"]);function Cr(e){try{let t=new URL(e);(t.username||t.password)&&(t.username="",t.password="");for(let[n]of t.searchParams)Rr.has(n.toLowerCase())&&t.searchParams.set(n,"[redacted]");return t.toString()}catch{return e}}function en(e=process.env){return br.map(t=>Object.prototype.hasOwnProperty.call(e,t)?{name:t,present:!0,value:Cr(e[t]??"")}:{name:t,present:!1})}import{createHash as Ar}from"node:crypto";import{existsSync as on,readFileSync as rn}from"node:fs";import{homedir as sn}from"node:os";import{join as S}from"node:path";var Or={claude:{skills:".claude/skills",subagents:".claude/agents",commands:".claude/commands",memory:".claude/CLAUDE.md",settings:".claude/settings.json"},cursor:{skills:[".cursor/skills",".cursor/skills-cursor"],commands:".cursor/commands",rules:".cursor/rules",mcp:".cursor/mcp.json"},codex:{skills:".agents/skills",commands:".codex/prompts",memory:".codex/AGENTS.md",settings:".codex/config.toml"}},Pr=200,tn=200,Tr=["allow","deny","ask"],Nr=/(?<=^|[\s(,=:])(?:~|[A-Za-z]:)?[\\/][^\s,)]+/g,Fe="<path>",Hr=new RegExp(`${Fe}(?:\\s+[A-Za-z.][^\\s,)]*[\\\\/][^\\s,)]*)+`,"g");function Mr(e){let t=e.replace(Nr,Fe).replace(Hr,Fe);return t.length>tn?`${t.slice(0,tn)}...`:t}function nn(e){if(!on(e))return{model:"",permissionKeys:[]};let t=_(rn(e,"utf8"),{}),n=[];for(let o of Tr)for(let r of t.permissions?.[o]??[])typeof r=="string"&&r.length>0&&n.push({key:Mr(r),mode:o});return{model:t.model??"",permissionKeys:n}}var Lr=10,Ur="self-config-cache.json",jr={claude:["claude"],cursor:["agent","cursor-agent"],codex:["codex"]};async function Fr(e,t=h){let n=jr[e];if(!n||n.length===0)return null;let o=async r=>{let s=n[r];if(s===void 0)return null;let i=((await t(s,["--version"],2e3)).split(`
|
|
13
|
+
`)[0]??"").trim();return i||o(r+1)};return o(0)}function Dr(e=sn()){return S(e,".evalon","agent-hook",Ur)}function $r(e){let n=[...e].sort((o,r)=>o.name.localeCompare(r.name)).map(o=>`${o.name}:${o.size}`).join(`
|
|
14
|
+
`);return Ar("sha256").update(n).digest("hex").slice(0,16)}function de(e,t,n=!1){let o=$r(e),r=e.length;return t===o?n?{hash:o,count:r,truncated:n}:{hash:o,count:r}:n?{hash:o,count:r,entries:e,truncated:n}:{hash:o,count:r,entries:e}}function Br(e){return on(e)?_(rn(e,"utf8"),{}):{}}function Kr(e,t){try{L(e,`${JSON.stringify(t)}
|
|
15
|
+
`)}catch(n){let o=n instanceof Error?n.message:String(n);l(`WARN: self-config cache write failed open: ${o}`)}}function Gr(e,t,n){let o={...e,send_count:n};for(let r of Object.keys(t)){let s=t[r];s.entries!==void 0&&(o[r]=s.hash)}return o}async function an(e,t=process.cwd(),n=sn()){let o=e==="claude",r=await Fr(e),s="",a=[];if(o){let x=nn(S(n,".claude/settings.json")),X=nn(S(t,".claude/settings.local.json"));s=X.model||x.model,a=[...x.permissionKeys,...X.permissionKeys].slice(0,Pr)}let i=Or[e]??{},c={present:!1,size:0},u=x=>{let X=Array.isArray(x)?x:x?[x]:[],M=[],q=!1;for(let Mn of X){let ze=G(S(n,Mn));if(M=M.concat(ze.entries),q=q||ze.truncated,M.length>=ne){M=M.slice(0,ne),q=!0;break}}return{entries:M,truncated:q}},p=Dr(n),m=Br(p),E=m[e]??{},k=(E.send_count??0)+1,O=k>0&&k%Lr===0,v=x=>O?void 0:E[x],y=u(i.skills),j=i.subagents?G(S(n,i.subagents)):{entries:[],truncated:!1},We=i.commands?G(S(n,i.commands)):{entries:[],truncated:!1},Ve=i.rules?G(S(n,i.rules)):{entries:[],truncated:!1},F={skills:de(y.entries,v("skills"),y.truncated),subagents:de(j.entries,v("subagents"),j.truncated),commands:de(We.entries,v("commands"),We.truncated),rules:de(Ve.entries,v("rules"),Ve.truncated)};return m[e]=Gr(E,F,k),Kr(p,m),{runner_platform:e,config_scope:"user",cli_version:r,claude_version:o?r:null,model:o&&s||null,permission_keys:a,user_scope:{skills:F.skills,subagents:F.subagents,commands:F.commands,rules:F.rules,memory:i.memory?J(S(n,i.memory)):c,settings:i.settings?J(S(n,i.settings)):c,mcp:i.mcp?J(S(n,i.mcp)):c},local_settings:o?J(S(t,".claude/settings.local.json")):c}}function W(e,t,n){let o={installation_id:n.installationId};return n.platformAgentId&&(o.platform_agent_id=n.platformAgentId),n.sessionId&&(o.session_id=n.sessionId),n.turnId&&(o.turn_id=n.turnId),{schema_version:2,event:e,timestamp:pt(),session_id:n.sessionId||null,turn_id:n.turnId||null,installation_id:n.installationId,platform:n.runnerPlatform,platform_agent_id:n.platformAgentId||null,runner_event:n.runnerEvent||null,agent_hook_version:b,host_arch:process.arch,node_version:process.version,correlation:o,payload:t}}import{existsSync as En}from"node:fs";import{spawn as vn}from"node:child_process";import{basename as ms}from"node:path";import{fileURLToPath as gs}from"node:url";var cn=/(?:^|\/|[\s"'=(])(?:\.cursor\/|\.claude\/|\.agents\/)skills(?:-cursor)?\/([^/]+)\/SKILL\.md/i,ln=/(?:^|\/|[\s"'=(])\.codex\/commands\/([^/]+)\.md\b/i,Jr=/(?:^|\s)\/([a-z][a-z0-9-]{1,63})\b/gi,Wr=new Set(["applications","app","bin","boot","data","dev","etc","home","lib","media","mnt","node_modules","opt","pkg","private","proc","projects","repos","run","sbin","src","srv","sys","tmp","usr","users","var","volumes","workspace","workspaces"]),Vr=new Set(["read","read_file","view"]),zr=new Set(["bash","shell","zsh"]);function Yr(e){let t=e.replace(/\\/g,"/");return cn.exec(t)?.[1]??null}function Xr(e){let t=e.replace(/\\/g,"/"),o=ln.exec(t)?.[1];return o?o.toLowerCase():null}function qr(e){return Yr(e)??Xr(e)}function Zr(e){for(let t of["path","file_path","target_file"]){let n=e[t];if(typeof n=="string"&&n.length>0)return n}return null}function un(e){if(typeof e=="string")try{let t=JSON.parse(e);return un(t)}catch{return null}return!e||typeof e!="object"?null:Zr(e)}function Qr(e){if(!e||typeof e!="object")return null;let t=e.command;return typeof t=="string"&&t.length>0?t:null}function es(e){let t=e.replace(/\\/g,"/"),n=cn.exec(t);if(n?.[1])return n[1];let r=ln.exec(t)?.[1];return r?r.toLowerCase():null}function ts(e){return typeof e.tool_name=="string"?e.tool_name:typeof e.tool=="string"?e.tool:""}function dn(e){let t=ts(e).toLowerCase();if(Vr.has(t)){let n=un(e.tool_input);return n?qr(n):null}if(zr.has(t)){let n=Qr(e.tool_input);return n?es(n):null}return null}function ns(e){let t=new Set,n=[];for(let o of e.matchAll(Jr)){let r=o[1]?.toLowerCase();!r||t.has(r)||Wr.has(r)||(t.add(r),n.push(r))}return n}function os(e){return K(e.prompt)}function fn(e){let t=d(e,"command_name");if(t){let o=d(e,"expansion_type");if(o&&o!=="slash_command")return[];let r=t.replace(/^\//,"").toLowerCase();return r?[r]:[]}let n=os(e);return n?ns(n):[]}function De(e){return e!==void 0&&ot.includes(e)}function fe(e,t){let n=e[t+1];return n&&!n.startsWith("--")?{value:n,next:t+1}:{next:t}}function pe(e){let t,n,o,r,s;for(let a=0;a<e.length;a++){let i=e[a];if(i!==void 0){if(i==="--runner"){let c=fe(e,a);t=c.value,a=c.next;continue}if(i.startsWith("--runner=")){t=i.slice(9);continue}if(i==="--url"){let c=fe(e,a);c.value&&(o=c.value),a=c.next;continue}if(i.startsWith("--url=")){o=i.slice(6);continue}if(i==="--token"){let c=fe(e,a);c.value&&(r=c.value),a=c.next;continue}if(i.startsWith("--token=")){r=i.slice(8);continue}if(i==="--base-url"){let c=fe(e,a);c.value&&(s=c.value),a=c.next;continue}if(i.startsWith("--base-url=")){s=i.slice(11);continue}!i.startsWith("-")&&n===void 0&&(n=i)}}return{runner:De(t)?t:void 0,event:n,collectorUrl:o,orgToken:r,baseUrl:s}}function $e(e){let t=e.collectorUrl?.trim(),n;if(t)n=t;else{let r=e.orgToken?.trim();r&&(n=Z(r,e.baseUrl?.trim()??void 0))}if(!n)return;let o=$(n);if(o){l(`WARN: ignoring --url/--token collector override: ${o}`);return}return B(n)}function rs(e,t){return d(e,"conversation_id")?!0:t!==""&&t===t.toLowerCase()}function Be(e,t,n,o){if(rs(e,t))return"cursor";if(De(o))return o;let r=d(e,"conversation_id"),s=d(e,"turn_id"),a=d(e,"session_id"),i=t.toLowerCase(),c="unknown";return r||t===i&&t!==""?c="cursor":s?c="codex":(a||i==="sessionstart"||i==="sessionend"||i==="stop"||i==="stopfailure"||i==="pretooluse"||i==="posttooluse"||i==="posttoolusefailure"||i==="permissiondenied"||i==="subagentstart"||i==="subagentstop"||i==="userpromptexpansion")&&(c="claude"),De(n)&&(c=n),c}function me(e,t){let n=e.toLowerCase();return n==="sessionend"||n==="stopfailure"||n==="subagentstop"?!0:n==="stop"&&t==="codex"}function ss(e){switch(e){case"sessionstart":return"session_start";case"stop":case"sessionend":case"stopfailure":return"session_end";case"pretooluse":case"posttooluse":case"posttoolusefailure":case"permissiondenied":case"beforemcpexecution":case"beforeshellexecution":case"afterfileedit":case"subagentstart":case"subagentstop":return"tool_invoked";case"beforesubmitprompt":case"userpromptexpansion":case"userpromptsubmit":return"skill_invoked";default:return""}}function pn(e){let t=e.toLowerCase();return t==="posttooluse"?!0:t==="posttoolusefailure"||t==="permissiondenied"?!1:null}function mn(e,t,n){let o=d(e,"session_id")||d(e,"conversation_id"),r=d(e,"prompt.id")||d(e,"prompt_id")||d(e,"generation_id")||d(e,"turn_id"),s=d(e,"hook_event_name")||d(e,"event"),a=d(e,"source"),i=Be(e,s,t,n);return{sessionId:o,turnId:r,runnerEvent:s,hookSource:a,runnerPlatform:i}}var is=["session_start","session_end","tool_invoked","skill_invoked"];function as(e){return is.includes(e)}function gn(e,t){return e&&as(e)?e:ss(t.toLowerCase())}function hn(e){let t=d(e,"tool_name")||d(e,"tool")||null,n=e.tool_input,r=(n&&typeof n=="object"?K(n.skill)||K(n.skill_name):"")||d(e,"skill_name")||null;return r??=dn(e),{toolName:t,skillName:r}}var cs=/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;function _n(e){for(let t of["subagent_type","agent_type"]){let n=d(e,t);if(n&&cs.test(n))return n}return null}function kn(e){let t=N(e,"duration_ms");if(typeof t=="number")return t;let n=N(e,"duration");return typeof n=="number"?n:null}var ls="error",us="interrupted",ds="permission_denied",fs=/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;function Ke(e){return fs.test(e)?e:null}function ps(e){for(let t of["failure_type","error_type"]){let n=Ke(d(e,t));if(n)return n}return null}function yn(e,t){return N(e,"is_interrupt")===!0?us:t.toLowerCase()==="permissiondenied"?ds:ps(e)??ls}function Sn(e){if(!e)return{mcpServer:null,mcpTool:null};let t=/^mcp__(.+?)__(.+)$/i.exec(e);if(t?.[1]&&t[2])return{mcpServer:t[1],mcpTool:t[2]};if(/^MCP:/i.test(e)){let n=e.slice(4);if(!n)return{mcpServer:null,mcpTool:null};let o=n.indexOf(":");return o>0?{mcpServer:n.slice(0,o),mcpTool:n.slice(o+1)||null}:{mcpServer:null,mcpTool:n}}return{mcpServer:null,mcpTool:null}}var V="__post",Ge="__work";function hs(e,t){let n=e.payload,o=n&&typeof n=="object"&&!Array.isArray(n)?Object.keys(n):[],r=e.correlation&&typeof e.correlation=="object"?Object.keys(e.correlation):[];return JSON.stringify({event:e.event,schema_version:e.schema_version,agent_hook_version:e.agent_hook_version,host_arch:e.host_arch,node_version:e.node_version,body_bytes:t,correlation_keys:r,payload_keys:o})}function wn(e){switch(e){case"session_start":return qe;case"session_end":return Ze;case"tool_invoked":case"skill_invoked":return Ee;default:return Ee}}function _s(e,t,n){return`${JSON.stringify({url:e,timeoutMs:t})}
|
|
16
|
+
${n}`}function In(e,t=[],n=process.execPath,o=import.meta.url,r=En){if(ms(n)===P)return{command:n,args:[e,...t]};let s=gs(o);return r(s)?{command:n,args:[s,e,...t]}:{command:n,args:[e,...t]}}function ks(e=process.execPath,t=import.meta.url,n=En){return In(V,[],e,t,n)}function ys(e){let t=e.indexOf(`
|
|
17
|
+
`);if(t<0)return null;let n;try{n=JSON.parse(e.slice(0,t))}catch{return null}return{meta:n,body:e.slice(t+1)}}async function ge(e,t=fetch){let n=await e(),o=ys(n);if(!o)return;let{meta:r,body:s}=o,a={"Content-Type":"application/json"};try{await t(r.url,{method:"POST",headers:a,body:s,signal:AbortSignal.timeout(Number(r.timeoutMs))})}catch{}}async function Ss(e,t,n){let o=wn(e);l(`POST event=${e} url=${we(n)} size=${t.length} platform=${process.platform}`);let r={"Content-Type":"application/json"},s=Date.now();try{let a=await fetch(n,{method:"POST",headers:r,body:t,signal:AbortSignal.timeout(o)}),i=Date.now()-s;if(a.ok)return l(`POST outcome=ok elapsed_ms=${i} http=${a.status}`),!0;let c=(await a.text()).slice(0,200).replace(/[\n\r]/g,"");return l(`POST outcome=fail elapsed_ms=${i} http=${a.status} body=${c}`),!1}catch(a){let i=Date.now()-s,c=a instanceof Error?a.message:String(a);return l(`POST outcome=fail elapsed_ms=${i} http=000 body=${c}`),!1}}async function xn(e,t,n,o,r){try{let s=o(e,t,{detached:!0,stdio:["pipe","ignore","ignore"],windowsHide:!0});s.on("error",()=>{}),s.stdin.on("error",()=>{});let a=!1;return await new Promise(i=>{let c=setTimeout(()=>{if(!a){l(`WARN: ${r} stdin handoff timed out \u2014 continuing fail-open`);try{s.stdin.destroy()}catch{}}i()},Qe),u=()=>{a=!0,clearTimeout(c),i()};s.stdin.end(n,()=>u()),s.stdin.once("error",()=>u())}),s.unref(),s}catch(s){let a=s instanceof Error?s.message:String(s);return l(`WARN: ${r} spawn failed open: ${a}`),null}}async function Es(e,t,n,o=vn,r=ks){let s=wn(e),a=_s(n,s,t),{command:i,args:c}=r();return xn(i,c,a,o,"detached post")}async function bn(e,t,n=vn,o=In){let{command:r,args:s}=o(Ge,t);return xn(r,s,e,n,"detached work")}async function z(e,t,n,o,r){if(!r){l("WARN: collector URL missing \u2014 pass --url on the hook command or set collector_url in manifest");return}if(!ee(r)){l(`WARN: collector URL blocked (https required): ${we(r)}`);return}let s=o(e,t,n),a=`${JSON.stringify(s)}
|
|
18
|
+
`,i=g("EVALON_HOOK_DEBUG");if(i==="full"?l(`DEBUG envelope=${JSON.stringify(s)}`):i==="1"&&l(`DEBUG summary=${hs(s,Buffer.byteLength(a,"utf8"))}`),!me(n.runnerEvent,n.runnerPlatform)){await Es(e,a,r);return}await Ss(e,a,r)}import{relative as vs,resolve as Je,sep as Rn}from"node:path";var ws=["session_id","conversation_id","turn_id","generation_id","prompt.id","prompt_id","hook_event_name","event","source","status","stop_reason","reason","tool_name","tool","tool_use_id","skill_name","command_name","expansion_type","command_source","permission_mode","model","model_id","agent_id","agent_type","subagent_id","subagent_type","subagent_model","is_parallel_worker","effort","effort.level","duration_ms","duration","failure_type","is_interrupt"];function Is(e){return e.capture_hook_input==="full"?!0:e.capture_hook_input==="allowlist"?!1:g("EVALON_HOOK_CAPTURE_FULL_INPUT")==="1"}function xs(e){let t={};for(let n of ws){let o=N(e,n);o!==void 0&&(t[n]=o)}return t}function bs(e,t){if(typeof e!="string"||e.length===0||!t)return null;let n=Je(e),o=Je(t),r=vs(o,n);return!r||r==="."?".":r.startsWith(`..${Rn}`)||r===".."||Je(o,r)!==n?null:r.split(Rn).join("/")}function Cn(e,t){let n=JSON.stringify(e),o=Buffer.byteLength(n,"utf8");if(o<=t)return e;let r=Object.entries(e).filter(([a,i])=>!a.startsWith("_")&&(typeof i=="string"||typeof i=="number"||typeof i=="boolean")).sort((a,i)=>Buffer.byteLength(JSON.stringify(a[1]),"utf8")-Buffer.byteLength(JSON.stringify(i[1]),"utf8")),s={_truncated:!0,_original_bytes:o};for(let[a,i]of r){let c={...s,[a]:i};if(Buffer.byteLength(JSON.stringify(c),"utf8")>t)break;s[a]=i}return s}function Y(e,t,n={}){let o=n.gitRoot!==void 0?bs(e.cwd,n.gitRoot):null;if(Is(t)){let s={...e};return delete s.cwd,delete s.transcript_path,o!==null&&(s.cwd_relative=o),Cn(s,tt)}let r=xs(e);return o!==null&&(r.cwd_relative=o),Cn(r,et)}import{createReadStream as Rs}from"node:fs";import{stat as Cs}from"node:fs/promises";import{basename as As,resolve as Os}from"node:path";import{createInterface as Ps}from"node:readline";var Ts=1024*1024;function Ns(e){let t=e.replace(/\\/g,"/");return t.includes("/.claude/projects/")&&As(t).endsWith(".jsonl")}async function An(e){if(!e||!Ns(e))return null;try{let t=Os(e),n=await Cs(t);if(!n.isFile())return null;let o=Math.max(0,n.size-Ts),r=[],s=Ps({input:Rs(t,{encoding:"utf8",start:o}),crlfDelay:1/0});for await(let a of s)a.length!==0&&r.push(a);o>0&&r.length>0&&r.shift();for(let a=r.length-1;a>=0;a--){let i=r[a];if(i)try{let c=JSON.parse(i);if(c.type!=="assistant")continue;let u=c.message;if(!u||typeof u!="object")continue;let p=u.model;if(typeof p=="string"&&p.length>0)return p}catch{continue}}return null}catch{return null}}function he(e){return{sessionId:e.sessionId,turnId:e.turnId,installationId:e.manifest.installationId,runnerPlatform:e.runnerPlatform,platformAgentId:e.platformAgentId,runnerEvent:e.runnerEvent}}function Hs(...e){for(let t of e)if(t)return t;return null}function U(e,t,n){if(e.status==="fulfilled")return e.value;let o=e.reason instanceof Error?e.reason.message:String(e.reason);return l(`WARN: ${n} failed open: ${o}`),t}function Ms(e){let t={present:!1,size:0},n={hash:"",count:0};return{runner_platform:e,config_scope:"user",cli_version:null,claude_version:null,model:null,permission_keys:[],user_scope:{skills:n,subagents:n,commands:n,rules:n,memory:t,settings:t,mcp:t},local_settings:t}}async function _e(e,t,n){try{e.platformAgentId=await Wt(e.manifest.manifest,e.platformAgentId,String(e.runnerPlatform),t,n,e.manifest.installationStorePath)}catch(o){let r=o instanceof Error?o.message:String(o);l(`WARN: derivePlatformAgentId failed open: ${r}`)}}async function Ls(e){let{gitRoot:t}=e.manifest,n=Vt(),o=String(e.runnerPlatform),[r,s,a,i,c,u]=await Promise.allSettled([Le(t),zt(),Zt(n),Yt(),Qt(e.manifest.manifest,t,o),an(o,t)]),p=U(r,null,"gitSnapshot"),m=U(s,"","detectVirt"),E=U(a,null,"probeCloud"),k=U(i,{},"hostEnv"),O=U(c,[],"mcpDeclared"),v=U(u,Ms(o),"selfConfig");await _e(e,t,p);let y=d(e.hookInput,"model");y&&(v.model=y);let j={identity:{host:Xt(),context:n,virt:m||null,cloud:E,git:p},host_env:k,env_vars:en(),mcp:{declared:O},self_config:v,run_meta:{trigger:e.hookSource||"startup"},hook_input:Y(e.hookInput,e.manifest.manifest,{gitRoot:t})};await z("session_start",j,he(e),W,e.manifest.collectorUrl)}async function Us(e){let{gitRoot:t}=e.manifest;await _e(e,t);let n=Hs(d(e.hookInput,"status"),d(e.hookInput,"stop_reason"),d(e.hookInput,"reason"),Ke(d(e.hookInput,"error"))),o=e.runnerPlatform==="claude"?await An(d(e.hookInput,"transcript_path")):null,r=Y(e.hookInput,e.manifest.manifest,{gitRoot:t});o&&(r.model=o),await z("session_end",{status:n,...o?{model:o}:{},hook_input:r},he(e),W,e.manifest.collectorUrl)}async function js(e){let t=fn(e.hookInput);if(t.length===0)return;let{gitRoot:n}=e.manifest;await _e(e,n);let o=Y(e.hookInput,e.manifest.manifest,{gitRoot:n}),r=he(e);await Promise.all(t.map(s=>z("tool_invoked",{tool_name:"Skill",skill_name:s,success:null,duration_ms:null,hook_input:o},r,W,e.manifest.collectorUrl)))}async function Fs(e){let{gitRoot:t}=e.manifest;await _e(e,t);let{toolName:n,skillName:o}=hn(e.hookInput),r=kn(e.hookInput),s=pn(e.runnerEvent),a=s===!1?yn(e.hookInput,e.runnerEvent):null,{mcpServer:i,mcpTool:c}=Sn(n),u=_n(e.hookInput);await z("tool_invoked",{tool_name:n,skill_name:o,success:s,duration_ms:r,...a?{failure_reason:a}:{},...i?{mcp_server:i}:{},...c?{mcp_tool:c}:{},...u?{subagent_type:u}:{},hook_input:Y(e.hookInput,e.manifest.manifest,{gitRoot:t})},he(e),W,e.manifest.collectorUrl)}async function On(e){switch(e.internalEvent){case"session_start":await Ls(e);return;case"session_end":await Us(e);return;case"skill_invoked":await js(e);return;case"tool_invoked":await Fs(e);return;default:return}}function Pn(e){return e!=="session_start"&&e!=="session_end"&&e!=="tool_invoked"&&e!=="skill_invoked"&&e!==""}async function Tn(e,t,n,o){let r=t??await Ie(ve),s=_(r,{}),a=await Et();if(!a)return;o&&(a.collectorUrl=o);let i=mn(s,a.manifest.platform,n),c=gn(e,i.runnerEvent);if(!c){l(`WARN: unknown hook event '${i.runnerEvent}'; skipping telemetry`);return}let u={hookInput:s,sessionId:i.sessionId,turnId:i.turnId,runnerEvent:i.runnerEvent,runnerPlatform:i.runnerPlatform,hookSource:i.hookSource,internalEvent:c,manifest:a,platformAgentId:a.platformAgentId};if(Pn(c)){l(`WARN: unsupported internal event '${c}'`);return}await On(u)}function Ds(e,t){if(g("EVALON_HOOK_SYNC")==="1")return!0;let n=pe(e),o=_(t,{}),r=n.event??(d(o,"hook_event_name")||d(o,"event")),s=Be(o,r,void 0,n.runner);return me(r,s)}async function Nn(e=process.argv.slice(2)){if(e[0]===V){try{await ge(te)}catch{}return 0}if(e[0]===Ge){try{let t=pe(e.slice(1));await Tn(t.event,void 0,t.runner,$e(t))}catch(t){let n=t instanceof Error?t.message:String(t);l(`WARN: detached work failed open: ${n}`)}return 0}try{let t=await Ie(ve),n=pe(e),o=$e(n);Ds(e,t)?await Tn(n.event,t,n.runner,o):await bn(t,e)}catch(t){let n=t instanceof Error?t.message:String(t);l(`WARN: hook failed open: ${n}`)}finally{mt()}return 0}function ke(){l("Usage: evalon-agent-hook <configure|install|migrate-to-plugin|update|uninstall|--version>"),l(" configure [--url <\u2026/{orgToken}/hook>]"),l(" (manifest only \u2014 pair with marketplace plugins)"),l(" install --url <\u2026/{orgToken}/hook>"),l(" [--delivery npx|binary] [--force]"),l(" [--no-claude] [--no-cursor] [--no-codex]"),l(" (fallback: merge hooks into .claude / .cursor / .codex configs)"),l(" (writes collector_url to shared manifest; hook commands use --runner only)"),l(" migrate-to-plugin"),l(" (strip Evalon file hooks; keep collector manifest)"),l(" update (refresh binary pins from a local build; leaves hook configs alone)"),l(" uninstall"),l("Hook mode: --runner <platform> [--url <\u2026/hook>|--token <orgToken>] [event]"),l(" (--url/--token optional overrides; else collector_url from manifest)")}function Hn(e){return e.includes("--help")||e.includes("-h")}async function Js(e=process.argv.slice(2)){let[t,...n]=e;if(t===V){try{await ge(te)}catch{}return 0}return t==="--version"||t==="-V"?(process.stdout.write(`${b}
|
|
19
|
+
`),0):t==="configure"?Hn(n)?(ke(),0):(await Bt(Ct(n)),0):t==="install"?Hn(n)?(ke(),0):(await Dt(At(n)),0):t==="migrate-to-plugin"?(await Gt(),0):t==="update"?(await $t(),0):t==="uninstall"?(await Jt(),0):t==="help"||t==="--help"||t==="-h"?(ke(),0):t===void 0?(ke(),1):Nn(e)}function Ws(e,t,n=$s){if(!t)return!1;try{return e===Gs(Bs(t)).href?!0:n(Ks(e))===n(t)}catch{return!1}}Ws(import.meta.url,process.argv[1])&&process.env.VITEST===void 0&&Js().then(e=>{process.exit(e)}).catch(e=>{let t=e instanceof Error?e.message:String(e);l(t),process.exit(1)});export{Ws as isCliEntrypoint,Js as main};
|
package/dist/hook.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
var Ke="https://hooks.evalon.ai";function U(t,e=Ke){let n=e.replace(/\/+$/,""),r=t.trim();return`${n}/${r}/hook`}var It=".claude/evalon-runtime-report.json";var D="evalon-agent-hook";var ro=`current/${D}`,oo=`.evalon/agent-hook/${D}`,Rt=5e3,vt=5e3,Q=3e3;var tt=3e3,wt=1500,At=4*1024,Ct=32*1024,je=4*1024,et="1.0.6",so=`@evalon-ai/agent-hook@${et}`;var Tt=["claude","cursor","codex"];import{createHash as Ye,randomBytes as Xe}from"node:crypto";import{existsSync as Ze,mkdirSync as qe,readFileSync as Qe,writeFileSync as tn}from"node:fs";import{homedir as j}from"node:os";import{dirname as en,join as rt,resolve as nn}from"node:path";import{randomBytes as Be}from"node:crypto";import{existsSync as bt,mkdirSync as ze,readdirSync as We,renameSync as xt,statSync as Ot,unlinkSync as Nt,writeFileSync as Ve}from"node:fs";import{basename as Ge,dirname as Pt,join as Lt,relative as Je}from"node:path";function u(t){process.stderr.write(`[evalon-agent-hook] ${t}
|
|
2
|
+
`)}function w(t,e){ze(Pt(t),{recursive:!0});let n=Lt(Pt(t),`.${Ge(t)}.${process.pid}.${Be(4).toString("hex")}.tmp`);try{Ve(n,e,"utf8");try{xt(n,t)}catch(r){let o=r instanceof Error&&"code"in r?r.code:void 0;if(o==="EEXIST"||o==="EPERM"||o==="EACCES"){Nt(t),xt(n,t);return}throw r}}catch(r){try{Nt(n)}catch{}throw r}}function Mt(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function Ht(){process.stdout.write('{"continue":true}')}function p(t){let e=process.env[t];return e===void 0||e===""?void 0:e}async function Ft(){return new Promise(t=>{let e="";if(process.stdin.isTTY){t("");return}process.stdin.setEncoding("utf8"),process.stdin.on("data",n=>{e+=n}),process.stdin.on("end",()=>{t(e)}),process.stdin.on("error",()=>{t(e)}),process.stdin.resume()})}async function nt(t){return new Promise(e=>{let n="",r=!1,o=a=>{r||(r=!0,clearTimeout(s),e(a||"{}"))},s=setTimeout(()=>{o(n)},t);if(process.stdin.isTTY){o("{}");return}process.stdin.setEncoding("utf8"),process.stdin.on("data",a=>{n+=a}),process.stdin.on("end",()=>{o(n)}),process.stdin.on("error",()=>{o(n)}),process.stdin.resume()})}async function S(t,e,n=3e3,r={}){let{execFile:o}=await import("node:child_process"),{promisify:s}=await import("node:util"),a=s(o);try{let{stdout:i}=await a(t,e,{timeout:n,windowsHide:!0,maxBuffer:1048576,cwd:r.cwd});return{ok:!0,stdout:i.trim()}}catch(i){let c=i,d=typeof c.code=="number"?c.code:typeof c.status=="number"?c.status:void 0;return{ok:!1,code:d,spawnFailed:d===void 0}}}async function m(t,e,n=3e3,r={}){let o=await S(t,e,n,r);return o.ok?o.stdout:""}async function $(t=process.cwd()){return await m("git",["rev-parse","--show-toplevel"],2e3,{cwd:t})||t}function g(t,e){try{return JSON.parse(t)}catch{return e}}function x(t){return typeof t=="string"?t:""}function R(t,e){let n=t[e];if(typeof n=="string"||typeof n=="number"||typeof n=="boolean")return n;if(!e.includes("."))return;let r=e.split("."),o=t;for(let s of r){if(o===null||typeof o!="object"||Array.isArray(o))return;o=o[s]}if(typeof o=="string"||typeof o=="number"||typeof o=="boolean")return o}function l(t,e){let n=R(t,e);return typeof n=="string"?n:""}var K=500;function N(t,e=2){if(!bt(t))return{entries:[],truncated:!1};let n=[],r=!1,o=(s,a)=>{if(a>e||r)return;let i;try{i=We(s,{withFileTypes:!0})}catch{return}for(let c of i){if(r)return;let d=Lt(s,c.name);if(c.isDirectory()){o(d,a+1);continue}if(c.isFile()){try{n.push({name:Je(t,d),size:Ot(d).size})}catch{continue}if(n.length>=K){r=!0;return}}}};return o(t,1),{entries:n,truncated:r}}function P(t){return bt(t)?{present:!0,size:Ot(t).size}:{present:!1,size:0}}function $t(t,e){return`${t}:${e??""}`}function ot(t=j()){return rt(t,".evalon","agent-hook")}function rn(t=j()){return rt(ot(t),"installations")}function on(t,e=j()){return rt(rn(e),`${t}.json`)}function Ut(){return Xe(8).toString("hex")}async function sn(t){let n=await m("git",["config","--get","remote.origin.url"],2e3,{cwd:t})||nn(t);return Ye("sha256").update(n).digest("hex").slice(0,16)}function an(t){if(!t||typeof t!="object"||Array.isArray(t))return;let e={};for(let[n,r]of Object.entries(t))typeof r=="string"&&r.length>0&&(e[n]=r);return Object.keys(e).length>0?e:void 0}function B(t){if(Ze(t))try{let e=JSON.parse(Qe(t,"utf8"));if(typeof e.installation_id=="string"&&e.installation_id.length>0){let n=an(e.platform_agent_ids);return{installation_id:e.installation_id,...n?{platform_agent_ids:n}:{}}}}catch{}}function Kt(t,e){let r=B(t)?.platform_agent_ids?.[e];return typeof r=="string"&&r.length>0?r:void 0}function jt(t,e,n){if(!n)return;let r=B(t);if(!r||r.platform_agent_ids?.[e]===n)return;let o={installation_id:r.installation_id,platform_agent_ids:{...r.platform_agent_ids,[e]:n}};try{w(t,`${JSON.stringify(o,null,2)}
|
|
3
|
+
`)}catch(s){let a=s instanceof Error?s.message:String(s);u(`WARN: platform_agent_id cache write failed open: ${a}`)}}function cn(t){return t instanceof Error&&"code"in t&&t.code==="EEXIST"}function ln(t,e){qe(en(t),{recursive:!0}),tn(t,`${JSON.stringify({installation_id:e},null,2)}
|
|
4
|
+
`,{encoding:"utf8",flag:"wx"})}function Dt(t,e){try{return ln(t,e),{installation_id:e}}catch(n){if(!cn(n))throw n;return B(t)}}async function Bt(t,e={}){let n=e.home??j(),r=await sn(t),o=on(r,n),s=B(o);if(s)return{installationId:s.installation_id,storePath:o,repoKey:r};let a=e.overrideId??e.migrateFromManifestId??Ut(),i=Dt(o,a);if(i)return{installationId:i.installation_id,storePath:o,repoKey:r};let c=Dt(o,Ut());if(c)return{installationId:c.installation_id,storePath:o,repoKey:r};throw new Error(`Failed to create installation record at ${o}`)}var un={claude:".claude",cursor:".cursor",codex:".codex"};function z(t){let n=t.replace(/^\/+|\/+$/g,"").toLowerCase().split("/").filter(Boolean);return n.length<2?"":n.join("/")}function dn(t){if(!t)return"";let e=t.replace(/^([a-z]+:\/\/)[^/@]+@/i,"$1");e=e.replace(/\.git$/i,"");let n=e.match(/^git@[^:]+:(.+)$/);if(n?.[1])return z(n[1]);let r=e.match(/^ssh:\/\/[^/]+\/(.+)$/i);if(r?.[1])return z(r[1]);e=e.replace(/^https:\/\//i,""),e=e.replace(/^http:\/\//i,""),e=e.replace(/^ssh:\/\//i,"");let o=e.match(/^[^/]+\/(.+)$/);return o?.[1]?z(o[1]):z(e)}async function st(t=process.cwd()){let e=await S("git",["rev-parse","--is-inside-work-tree"],2e3,{cwd:t});if(!e.ok||e.stdout!=="true")return null;let[n,r,o,s]=await Promise.all([S("git",["remote","get-url","origin"],2e3,{cwd:t}),S("git",["branch","--show-current"],2e3,{cwd:t}),S("git",["rev-parse","HEAD"],2e3,{cwd:t}),S("git",["status","--porcelain"],2e3,{cwd:t})]),a=n.ok?n.stdout:null;return{remote:a===null?null:a.replace(/^([a-z]+:\/\/)[^/@]+@/i,"$1"),branch:r.ok?r.stdout:null,head:o.ok?o.stdout:null,dirty:s.ok?s.stdout.length>0:null}}async function zt(t,e,n,r=process.cwd(),o,s){if(e)return e;let a=un[n];if(!a)return"";let i=$t(n,t.scope_path);if(s){let E=Kt(s,i);if(E)return E}let c=o??await st(r),d=c?.remote?dn(c.remote):"";if(d||(d=process.env.GITHUB_REPOSITORY??""),!d)return"";let f=t.scope_path??"",k=f?`${f}/`:"",y=`${d}:${k}${a}`;return s&&jt(s,i,y),y}import{existsSync as fn,readFileSync as pn}from"node:fs";import{hostname as mn,userInfo as gn}from"node:os";function Wt(){if(p("GITHUB_ACTIONS"))return"github_actions";let t=p("CI");if(t&&t!=="false"&&t!=="0")return"managed";try{if(fn("/.dockerenv")||pn("/proc/1/environ","utf8").includes("container="))return"container"}catch{}return process.stdin.isTTY||process.stdout.isTTY||p("TERM")?"interactive":"cron"}async function Vt(){return await m("systemd-detect-virt",[],1e3)}async function Gt(){let t=process.platform==="darwin"?"Darwin":process.platform==="win32"?"Windows_NT":"Linux",e="";if(process.platform==="darwin")e=await m("sw_vers",["-productVersion"],1e3);else if(process.platform==="linux"){try{let{readFileSync:s}=await import("node:fs"),a=s("/etc/os-release","utf8"),i=a.match(/^PRETTY_NAME="?([^"\n]+)"?/m)?.[1],c=a.match(/^VERSION_ID="?([^"\n]+)"?/m)?.[1];e=i??c??""}catch{e=await m("lsb_release",["-sd"],1e3).then(s=>s.replace(/^"|"$/g,""))}e||(e=await m("uname",["-r"],1e3))}else e=await m("ver",[],1e3);let n=process.env.USER??gn().username??"",r=(process.env.SHELL??"").split(/[/\\]/).pop()??"",o="";try{o=await m("tty",[],500),o.includes("not a tty")&&(o="")}catch{o=""}return{os:t,os_version:e||null,user:n||null,shell:r||null,tty:o||null}}function Jt(){return mn()}import{existsSync as _n,readFileSync as hn}from"node:fs";import{homedir as Yt}from"node:os";import{join as Sn}from"node:path";var yn="cloud-cache.json",En=1440*60*1e3,kn=900*1e3;function In(t=Yt()){return Sn(ot(t),yn)}function Rn(t){if(!_n(t))return;let e=g(hn(t,"utf8"),{});if(!(typeof e.context!="string"||typeof e.probed_at!="number"||e.result!==null&&e.result!==void 0&&typeof e.result!="object"))return{context:e.context,probed_at:e.probed_at,result:e.result??null}}function vn(t,e){try{w(t,`${JSON.stringify(e)}
|
|
5
|
+
`)}catch(n){let r=n instanceof Error?n.message:String(n);u(`WARN: cloud cache write failed open: ${r}`)}}function wn(t,e,n){if(!t||t.context!==e)return!1;let r=t.result===null?kn:En;return n-t.probed_at<r}async function An(t){let e=await fetch("http://169.254.169.254/latest/api/token",{method:"PUT",headers:{"X-aws-ec2-metadata-token-ttl-seconds":"60"},signal:AbortSignal.timeout(t)}).catch(()=>{});if(!e?.ok)return;let n=(await e.text()).trim();if(!n)return;let r={"X-aws-ec2-metadata-token":n},[o,s]=await Promise.all([fetch("http://169.254.169.254/latest/meta-data/placement/region",{headers:r,signal:AbortSignal.timeout(t)}).catch(()=>{}),fetch("http://169.254.169.254/latest/meta-data/instance-id",{headers:r,signal:AbortSignal.timeout(t)}).catch(()=>{})]);if(!o?.ok&&!s?.ok)return;let a=o?.ok?(await o.text()).trim():"",i=s?.ok?(await s.text()).trim():"";if(!(!a&&!i))return{provider:"aws",region:a,instance_id:i}}async function Cn(t){let e=await fetch("http://metadata.google.internal/computeMetadata/v1/instance/?recursive=true",{headers:{"Metadata-Flavor":"Google"},signal:AbortSignal.timeout(t)}).catch(()=>{});if(!e?.ok)return;let n=await e.json(),r=typeof n.zone=="string"?n.zone.split("/").pop()??"":"",o=n.id===void 0?"":String(n.id);if(!(!r&&!o))return{provider:"gcp",region:r,instance_id:o}}async function Tn(t){let e=await fetch("http://169.254.169.254/metadata/instance?api-version=2021-02-01",{headers:{Metadata:"true"},signal:AbortSignal.timeout(t)}).catch(()=>{});if(!e?.ok)return;let n=await e.json(),r=n.compute?.location??"",o=n.compute?.vmId??"";if(!(!r&&!o))return{provider:"azure",region:r,instance_id:o}}async function xn(t){let e=await Promise.allSettled([An(t),Cn(t),Tn(t)]);for(let n of e)if(n.status==="fulfilled"&&n.value)return n.value;return null}async function Xt(t,e){if(t==="interactive")return null;let n=e?.home??Yt(),r=e?.timeoutMs??1e3,o=e?.nowMs??Date.now(),s=In(n),a=Rn(s);if(wn(a,t,o))return a.result;let i=await xn(r);return vn(s,{context:t,probed_at:o,result:i}),i}import{existsSync as Nn,readFileSync as Pn}from"node:fs";import{homedir as bn}from"node:os";import{join as it}from"node:path";function at(t,e,n){if(!Nn(t))return;let r=g(Pn(t,"utf8"),{});for(let o of Object.keys(r.mcpServers??{}))n.set(o,{name:o,source:e})}async function Zt(t,e=process.cwd(),n="claude",r=bn()){let o=new Map;for(let s of t.mcp_servers??[])s.name&&o.set(s.name,{name:s.name,source:"manifest"});return n==="claude"&&at(it(r,".claude.json"),"claude_json_user",o),at(it(e,".mcp.json"),"mcp_json",o),at(it(e,".cursor/mcp.json"),"cursor_mcp",o),[...o.values()]}var On=["CI","GITHUB_ACTIONS","GITLAB_CI","CIRCLECI","BUILDKITE","TF_BUILD","GITHUB_REPOSITORY","GITHUB_REF","GITHUB_SHA","GITHUB_RUN_ID","GITHUB_WORKFLOW","RUNNER_OS","RUNNER_ARCH","NODE_ENV","TERM_PROGRAM","ANTHROPIC_BASE_URL","OPENAI_BASE_URL","OPENAI_API_BASE","AZURE_OPENAI_ENDPOINT","GOOGLE_GEMINI_BASE_URL","GEMINI_API_BASE","CLAUDE_CODE_USE_BEDROCK","CLAUDE_CODE_USE_VERTEX"],Ln=new Set(["key","api-key","api_key","apikey","token","sig","signature","access_token","password","pwd","secret","client_secret"]);function Mn(t){try{let e=new URL(t);(e.username||e.password)&&(e.username="",e.password="");for(let[n]of e.searchParams)Ln.has(n.toLowerCase())&&e.searchParams.set(n,"[redacted]");return e.toString()}catch{return t}}function qt(t=process.env){return On.map(e=>Object.prototype.hasOwnProperty.call(t,e)?{name:e,present:!0,value:Mn(t[e]??"")}:{name:e,present:!1})}import{createHash as Hn}from"node:crypto";import{existsSync as ee,readFileSync as ne}from"node:fs";import{homedir as re}from"node:os";import{join as _}from"node:path";var Fn={claude:{skills:".claude/skills",subagents:".claude/agents",commands:".claude/commands",memory:".claude/CLAUDE.md",settings:".claude/settings.json"},cursor:{skills:[".cursor/skills",".cursor/skills-cursor"],commands:".cursor/commands",rules:".cursor/rules",mcp:".cursor/mcp.json"},codex:{skills:".agents/skills",commands:".codex/prompts",memory:".codex/AGENTS.md",settings:".codex/config.toml"}},Un=200,Qt=200,Dn=["allow","deny","ask"],$n=/(?<=^|[\s(,=:])(?:~|[A-Za-z]:)?[\\/][^\s,)]+/g,ct="<path>",Kn=new RegExp(`${ct}(?:\\s+[A-Za-z.][^\\s,)]*[\\\\/][^\\s,)]*)+`,"g");function jn(t){let e=t.replace($n,ct).replace(Kn,ct);return e.length>Qt?`${e.slice(0,Qt)}...`:e}function te(t){if(!ee(t))return{model:"",permissionKeys:[]};let e=g(ne(t,"utf8"),{}),n=[];for(let r of Dn)for(let o of e.permissions?.[r]??[])typeof o=="string"&&o.length>0&&n.push({key:jn(o),mode:r});return{model:e.model??"",permissionKeys:n}}var Bn=10,zn="self-config-cache.json",Wn={claude:["claude"],cursor:["agent","cursor-agent"],codex:["codex"]};async function Vn(t,e=m){let n=Wn[t];if(!n||n.length===0)return null;let r=async o=>{let s=n[o];if(s===void 0)return null;let i=((await e(s,["--version"],2e3)).split(`
|
|
6
|
+
`)[0]??"").trim();return i||r(o+1)};return r(0)}function Gn(t=re()){return _(t,".evalon","agent-hook",zn)}function Jn(t){let n=[...t].sort((r,o)=>r.name.localeCompare(o.name)).map(r=>`${r.name}:${r.size}`).join(`
|
|
7
|
+
`);return Hn("sha256").update(n).digest("hex").slice(0,16)}function W(t,e,n=!1){let r=Jn(t),o=t.length;return e===r?n?{hash:r,count:o,truncated:n}:{hash:r,count:o}:n?{hash:r,count:o,entries:t,truncated:n}:{hash:r,count:o,entries:t}}function Yn(t){return ee(t)?g(ne(t,"utf8"),{}):{}}function Xn(t,e){try{w(t,`${JSON.stringify(e)}
|
|
8
|
+
`)}catch(n){let r=n instanceof Error?n.message:String(n);u(`WARN: self-config cache write failed open: ${r}`)}}function Zn(t,e,n){let r={...t,send_count:n};for(let o of Object.keys(e)){let s=e[o];s.entries!==void 0&&(r[o]=s.hash)}return r}async function oe(t,e=process.cwd(),n=re()){let r=t==="claude",o=await Vn(t),s="",a=[];if(r){let h=te(_(n,".claude/settings.json")),H=te(_(e,".claude/settings.local.json"));s=H.model||h.model,a=[...h.permissionKeys,...H.permissionKeys].slice(0,Un)}let i=Fn[t]??{},c={present:!1,size:0},d=h=>{let H=Array.isArray(h)?h:h?[h]:[],v=[],F=!1;for(let $e of H){let kt=N(_(n,$e));if(v=v.concat(kt.entries),F=F||kt.truncated,v.length>=K){v=v.slice(0,K),F=!0;break}}return{entries:v,truncated:F}},f=Gn(n),k=Yn(f),y=k[t]??{},E=(y.send_count??0)+1,q=E>0&&E%Bn===0,I=h=>q?void 0:y[h],C=d(i.skills),M=i.subagents?N(_(n,i.subagents)):{entries:[],truncated:!1},yt=i.commands?N(_(n,i.commands)):{entries:[],truncated:!1},Et=i.rules?N(_(n,i.rules)):{entries:[],truncated:!1},T={skills:W(C.entries,I("skills"),C.truncated),subagents:W(M.entries,I("subagents"),M.truncated),commands:W(yt.entries,I("commands"),yt.truncated),rules:W(Et.entries,I("rules"),Et.truncated)};return k[t]=Zn(y,T,E),Xn(f,k),{runner_platform:t,config_scope:"user",cli_version:o,claude_version:r?o:null,model:r&&s||null,permission_keys:a,user_scope:{skills:T.skills,subagents:T.subagents,commands:T.commands,rules:T.rules,memory:i.memory?P(_(n,i.memory)):c,settings:i.settings?P(_(n,i.settings)):c,mcp:i.mcp?P(_(n,i.mcp)):c},local_settings:r?P(_(e,".claude/settings.local.json")):c}}function b(t,e,n){let r={installation_id:n.installationId};return n.platformAgentId&&(r.platform_agent_id=n.platformAgentId),n.sessionId&&(r.session_id=n.sessionId),n.turnId&&(r.turn_id=n.turnId),{schema_version:2,event:t,timestamp:Mt(),session_id:n.sessionId||null,turn_id:n.turnId||null,installation_id:n.installationId,platform:n.runnerPlatform,platform_agent_id:n.platformAgentId||null,runner_event:n.runnerEvent||null,agent_hook_version:et,host_arch:process.arch,node_version:process.version,correlation:r,payload:e}}import{existsSync as Re}from"node:fs";import{spawn as ve}from"node:child_process";import{basename as Rr}from"node:path";import{fileURLToPath as vr}from"node:url";var qn=new Set(["localhost","127.0.0.1","[::1]","::1"]),se=/^[A-Za-z0-9._~-]+$/;function ie(t){for(let e=0;e<t.length;e++){let n=t.charCodeAt(e);if(n<=32||n===127||n===39)return!0}return!1}function V(t){try{let e=new URL(t);return e.protocol==="https:"?!0:e.protocol==="http:"?qn.has(e.hostname):!1}catch{return!1}}function ae(t){let e;try{e=new URL(t)}catch{return`invalid URL (got: ${t})`}if(!V(t))return`must be https (or http on localhost / 127.0.0.1 / ::1) (got: ${t})`;let n=Qn(t);if(!n)return`must end with /{orgToken}/hook (got: ${t})`;if(!se.test(n))return`org token must match ${se.source} (got: ${n})`;if(ie(t))return`must not contain whitespace, control characters, or single quotes (got: ${t})`;if(ie(e.pathname))return`pathname must not contain whitespace, control characters, or single quotes (got: ${t})`}function ce(t){return new URL(t).toString()}function Qn(t){try{let n=new URL(t).pathname.split("/").filter(r=>r.length>0);if(n.length>=2&&n[n.length-1]==="hook"){let r=n[n.length-2];return r.length>0?r:void 0}return}catch{return}}function ut(t){try{let e=new URL(t),n=e.pathname.split("/").filter(r=>r.length>0);return n.length>=2&&n[n.length-1]==="hook"&&(n[n.length-2]="***",e.pathname=`/${n.join("/")}`),e.toString()}catch{return"<invalid-url>"}}function le(t){if(t.collector_url)return t.collector_url;let e=t.repo_ingest_token?.trim();return e?U(e):""}var ue=/(?:^|\/|[\s"'=(])(?:\.cursor\/|\.claude\/|\.agents\/)skills(?:-cursor)?\/([^/]+)\/SKILL\.md/i,de=/(?:^|\/|[\s"'=(])\.codex\/commands\/([^/]+)\.md\b/i,tr=/(?:^|\s)\/([a-z][a-z0-9-]{1,63})\b/gi,er=new Set(["applications","app","bin","boot","data","dev","etc","home","lib","media","mnt","node_modules","opt","pkg","private","proc","projects","repos","run","sbin","src","srv","sys","tmp","usr","users","var","volumes","workspace","workspaces"]),nr=new Set(["read","read_file","view"]),rr=new Set(["bash","shell","zsh"]);function or(t){let e=t.replace(/\\/g,"/");return ue.exec(e)?.[1]??null}function sr(t){let e=t.replace(/\\/g,"/"),r=de.exec(e)?.[1];return r?r.toLowerCase():null}function ir(t){return or(t)??sr(t)}function ar(t){for(let e of["path","file_path","target_file"]){let n=t[e];if(typeof n=="string"&&n.length>0)return n}return null}function fe(t){if(typeof t=="string")try{let e=JSON.parse(t);return fe(e)}catch{return null}return!t||typeof t!="object"?null:ar(t)}function cr(t){if(!t||typeof t!="object")return null;let e=t.command;return typeof e=="string"&&e.length>0?e:null}function lr(t){let e=t.replace(/\\/g,"/"),n=ue.exec(e);if(n?.[1])return n[1];let o=de.exec(e)?.[1];return o?o.toLowerCase():null}function ur(t){return typeof t.tool_name=="string"?t.tool_name:typeof t.tool=="string"?t.tool:""}function pe(t){let e=ur(t).toLowerCase();if(nr.has(e)){let n=fe(t.tool_input);return n?ir(n):null}if(rr.has(e)){let n=cr(t.tool_input);return n?lr(n):null}return null}function dr(t){let e=new Set,n=[];for(let r of t.matchAll(tr)){let o=r[1]?.toLowerCase();!o||e.has(o)||er.has(o)||(e.add(o),n.push(o))}return n}function fr(t){return x(t.prompt)}function me(t){let e=l(t,"command_name");if(e){let r=l(t,"expansion_type");if(r&&r!=="slash_command")return[];let o=e.replace(/^\//,"").toLowerCase();return o?[o]:[]}let n=fr(t);return n?dr(n):[]}function dt(t){return t!==void 0&&Tt.includes(t)}function G(t,e){let n=t[e+1];return n&&!n.startsWith("--")?{value:n,next:e+1}:{next:e}}function J(t){let e,n,r,o,s;for(let a=0;a<t.length;a++){let i=t[a];if(i!==void 0){if(i==="--runner"){let c=G(t,a);e=c.value,a=c.next;continue}if(i.startsWith("--runner=")){e=i.slice(9);continue}if(i==="--url"){let c=G(t,a);c.value&&(r=c.value),a=c.next;continue}if(i.startsWith("--url=")){r=i.slice(6);continue}if(i==="--token"){let c=G(t,a);c.value&&(o=c.value),a=c.next;continue}if(i.startsWith("--token=")){o=i.slice(8);continue}if(i==="--base-url"){let c=G(t,a);c.value&&(s=c.value),a=c.next;continue}if(i.startsWith("--base-url=")){s=i.slice(11);continue}!i.startsWith("-")&&n===void 0&&(n=i)}}return{runner:dt(e)?e:void 0,event:n,collectorUrl:r,orgToken:o,baseUrl:s}}function ft(t){let e=t.collectorUrl?.trim(),n;if(e)n=e;else{let o=t.orgToken?.trim();o&&(n=U(o,t.baseUrl?.trim()??void 0))}if(!n)return;let r=ae(n);if(r){u(`WARN: ignoring --url/--token collector override: ${r}`);return}return ce(n)}function pr(t,e){return l(t,"conversation_id")?!0:e!==""&&e===e.toLowerCase()}function pt(t,e,n,r){if(pr(t,e))return"cursor";if(dt(r))return r;let o=l(t,"conversation_id"),s=l(t,"turn_id"),a=l(t,"session_id"),i=e.toLowerCase(),c="unknown";return o||e===i&&e!==""?c="cursor":s?c="codex":(a||i==="sessionstart"||i==="sessionend"||i==="stop"||i==="stopfailure"||i==="pretooluse"||i==="posttooluse"||i==="posttoolusefailure"||i==="permissiondenied"||i==="subagentstart"||i==="subagentstop"||i==="userpromptexpansion")&&(c="claude"),dt(n)&&(c=n),c}function Y(t,e){let n=t.toLowerCase();return n==="sessionend"||n==="stopfailure"||n==="subagentstop"?!0:n==="stop"&&e==="codex"}function mr(t){switch(t){case"sessionstart":return"session_start";case"stop":case"sessionend":case"stopfailure":return"session_end";case"pretooluse":case"posttooluse":case"posttoolusefailure":case"permissiondenied":case"beforemcpexecution":case"beforeshellexecution":case"afterfileedit":case"subagentstart":case"subagentstop":return"tool_invoked";case"beforesubmitprompt":case"userpromptexpansion":case"userpromptsubmit":return"skill_invoked";default:return""}}function ge(t){let e=t.toLowerCase();return e==="posttooluse"?!0:e==="posttoolusefailure"||e==="permissiondenied"?!1:null}function _e(t,e,n){let r=l(t,"session_id")||l(t,"conversation_id"),o=l(t,"prompt.id")||l(t,"prompt_id")||l(t,"generation_id")||l(t,"turn_id"),s=l(t,"hook_event_name")||l(t,"event"),a=l(t,"source"),i=pt(t,s,e,n);return{sessionId:r,turnId:o,runnerEvent:s,hookSource:a,runnerPlatform:i}}var gr=["session_start","session_end","tool_invoked","skill_invoked"];function _r(t){return gr.includes(t)}function he(t,e){return t&&_r(t)?t:mr(e.toLowerCase())}function Se(t){let e=l(t,"tool_name")||l(t,"tool")||null,n=t.tool_input,o=(n&&typeof n=="object"?x(n.skill)||x(n.skill_name):"")||l(t,"skill_name")||null;return o??=pe(t),{toolName:e,skillName:o}}var hr=/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/;function ye(t){for(let e of["subagent_type","agent_type"]){let n=l(t,e);if(n&&hr.test(n))return n}return null}function Ee(t){let e=R(t,"duration_ms");if(typeof e=="number")return e;let n=R(t,"duration");return typeof n=="number"?n:null}var Sr="error",yr="interrupted",Er="permission_denied",kr=/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;function mt(t){return kr.test(t)?t:null}function Ir(t){for(let e of["failure_type","error_type"]){let n=mt(l(t,e));if(n)return n}return null}function ke(t,e){return R(t,"is_interrupt")===!0?yr:e.toLowerCase()==="permissiondenied"?Er:Ir(t)??Sr}function Ie(t){if(!t)return{mcpServer:null,mcpTool:null};let e=/^mcp__(.+?)__(.+)$/i.exec(t);if(e?.[1]&&e[2])return{mcpServer:e[1],mcpTool:e[2]};if(/^MCP:/i.test(t)){let n=t.slice(4);if(!n)return{mcpServer:null,mcpTool:null};let r=n.indexOf(":");return r>0?{mcpServer:n.slice(0,r),mcpTool:n.slice(r+1)||null}:{mcpServer:null,mcpTool:n}}return{mcpServer:null,mcpTool:null}}var gt="__post",_t="__work";function wr(t,e){let n=t.payload,r=n&&typeof n=="object"&&!Array.isArray(n)?Object.keys(n):[],o=t.correlation&&typeof t.correlation=="object"?Object.keys(t.correlation):[];return JSON.stringify({event:t.event,schema_version:t.schema_version,agent_hook_version:t.agent_hook_version,host_arch:t.host_arch,node_version:t.node_version,body_bytes:e,correlation_keys:o,payload_keys:r})}function we(t){switch(t){case"session_start":return Rt;case"session_end":return vt;case"tool_invoked":case"skill_invoked":return Q;default:return Q}}function Ar(t,e,n){return`${JSON.stringify({url:t,timeoutMs:e})}
|
|
9
|
+
${n}`}function Ae(t,e=[],n=process.execPath,r=import.meta.url,o=Re){if(Rr(n)===D)return{command:n,args:[t,...e]};let s=vr(r);return o(s)?{command:n,args:[s,t,...e]}:{command:n,args:[t,...e]}}function Cr(t=process.execPath,e=import.meta.url,n=Re){return Ae(gt,[],t,e,n)}function Tr(t){let e=t.indexOf(`
|
|
10
|
+
`);if(e<0)return null;let n;try{n=JSON.parse(t.slice(0,e))}catch{return null}return{meta:n,body:t.slice(e+1)}}async function Ce(t,e=fetch){let n=await t(),r=Tr(n);if(!r)return;let{meta:o,body:s}=r,a={"Content-Type":"application/json"};try{await e(o.url,{method:"POST",headers:a,body:s,signal:AbortSignal.timeout(Number(o.timeoutMs))})}catch{}}async function xr(t,e,n){let r=we(t);u(`POST event=${t} url=${ut(n)} size=${e.length} platform=${process.platform}`);let o={"Content-Type":"application/json"},s=Date.now();try{let a=await fetch(n,{method:"POST",headers:o,body:e,signal:AbortSignal.timeout(r)}),i=Date.now()-s;if(a.ok)return u(`POST outcome=ok elapsed_ms=${i} http=${a.status}`),!0;let c=(await a.text()).slice(0,200).replace(/[\n\r]/g,"");return u(`POST outcome=fail elapsed_ms=${i} http=${a.status} body=${c}`),!1}catch(a){let i=Date.now()-s,c=a instanceof Error?a.message:String(a);return u(`POST outcome=fail elapsed_ms=${i} http=000 body=${c}`),!1}}async function Te(t,e,n,r,o){try{let s=r(t,e,{detached:!0,stdio:["pipe","ignore","ignore"],windowsHide:!0});s.on("error",()=>{}),s.stdin.on("error",()=>{});let a=!1;return await new Promise(i=>{let c=setTimeout(()=>{if(!a){u(`WARN: ${o} stdin handoff timed out \u2014 continuing fail-open`);try{s.stdin.destroy()}catch{}}i()},wt),d=()=>{a=!0,clearTimeout(c),i()};s.stdin.end(n,()=>d()),s.stdin.once("error",()=>d())}),s.unref(),s}catch(s){let a=s instanceof Error?s.message:String(s);return u(`WARN: ${o} spawn failed open: ${a}`),null}}async function Nr(t,e,n,r=ve,o=Cr){let s=we(t),a=Ar(n,s,e),{command:i,args:c}=o();return Te(i,c,a,r,"detached post")}async function xe(t,e,n=ve,r=Ae){let{command:o,args:s}=r(_t,e);return Te(o,s,t,n,"detached work")}async function O(t,e,n,r,o){if(!o){u("WARN: collector URL missing \u2014 pass --url on the hook command or set collector_url in manifest");return}if(!V(o)){u(`WARN: collector URL blocked (https required): ${ut(o)}`);return}let s=r(t,e,n),a=`${JSON.stringify(s)}
|
|
11
|
+
`,i=p("EVALON_HOOK_DEBUG");if(i==="full"?u(`DEBUG envelope=${JSON.stringify(s)}`):i==="1"&&u(`DEBUG summary=${wr(s,Buffer.byteLength(a,"utf8"))}`),!Y(n.runnerEvent,n.runnerPlatform)){await Nr(t,a,o);return}await xr(t,a,o)}import{relative as Pr,resolve as ht,sep as Ne}from"node:path";var br=["session_id","conversation_id","turn_id","generation_id","prompt.id","prompt_id","hook_event_name","event","source","status","stop_reason","reason","tool_name","tool","tool_use_id","skill_name","command_name","expansion_type","command_source","permission_mode","model","model_id","agent_id","agent_type","subagent_id","subagent_type","subagent_model","is_parallel_worker","effort","effort.level","duration_ms","duration","failure_type","is_interrupt"];function Or(t){return t.capture_hook_input==="full"?!0:t.capture_hook_input==="allowlist"?!1:p("EVALON_HOOK_CAPTURE_FULL_INPUT")==="1"}function Lr(t){let e={};for(let n of br){let r=R(t,n);r!==void 0&&(e[n]=r)}return e}function Mr(t,e){if(typeof t!="string"||t.length===0||!e)return null;let n=ht(t),r=ht(e),o=Pr(r,n);return!o||o==="."?".":o.startsWith(`..${Ne}`)||o===".."||ht(r,o)!==n?null:o.split(Ne).join("/")}function Pe(t,e){let n=JSON.stringify(t),r=Buffer.byteLength(n,"utf8");if(r<=e)return t;let o=Object.entries(t).filter(([a,i])=>!a.startsWith("_")&&(typeof i=="string"||typeof i=="number"||typeof i=="boolean")).sort((a,i)=>Buffer.byteLength(JSON.stringify(a[1]),"utf8")-Buffer.byteLength(JSON.stringify(i[1]),"utf8")),s={_truncated:!0,_original_bytes:r};for(let[a,i]of o){let c={...s,[a]:i};if(Buffer.byteLength(JSON.stringify(c),"utf8")>e)break;s[a]=i}return s}function L(t,e,n={}){let r=n.gitRoot!==void 0?Mr(t.cwd,n.gitRoot):null;if(Or(e)){let s={...t};return delete s.cwd,delete s.transcript_path,r!==null&&(s.cwd_relative=r),Pe(s,Ct)}let o=Lr(t);return r!==null&&(o.cwd_relative=r),Pe(o,At)}import{createReadStream as Hr}from"node:fs";import{stat as Fr}from"node:fs/promises";import{basename as Ur,resolve as Dr}from"node:path";import{createInterface as $r}from"node:readline";var Kr=1024*1024;function jr(t){let e=t.replace(/\\/g,"/");return e.includes("/.claude/projects/")&&Ur(e).endsWith(".jsonl")}async function be(t){if(!t||!jr(t))return null;try{let e=Dr(t),n=await Fr(e);if(!n.isFile())return null;let r=Math.max(0,n.size-Kr),o=[],s=$r({input:Hr(e,{encoding:"utf8",start:r}),crlfDelay:1/0});for await(let a of s)a.length!==0&&o.push(a);r>0&&o.length>0&&o.shift();for(let a=o.length-1;a>=0;a--){let i=o[a];if(i)try{let c=JSON.parse(i);if(c.type!=="assistant")continue;let d=c.message;if(!d||typeof d!="object")continue;let f=d.model;if(typeof f=="string"&&f.length>0)return f}catch{continue}}return null}catch{return null}}function X(t){return{sessionId:t.sessionId,turnId:t.turnId,installationId:t.manifest.installationId,runnerPlatform:t.runnerPlatform,platformAgentId:t.platformAgentId,runnerEvent:t.runnerEvent}}function Br(...t){for(let e of t)if(e)return e;return null}function A(t,e,n){if(t.status==="fulfilled")return t.value;let r=t.reason instanceof Error?t.reason.message:String(t.reason);return u(`WARN: ${n} failed open: ${r}`),e}function zr(t){let e={present:!1,size:0},n={hash:"",count:0};return{runner_platform:t,config_scope:"user",cli_version:null,claude_version:null,model:null,permission_keys:[],user_scope:{skills:n,subagents:n,commands:n,rules:n,memory:e,settings:e,mcp:e},local_settings:e}}async function Z(t,e,n){try{t.platformAgentId=await zt(t.manifest.manifest,t.platformAgentId,String(t.runnerPlatform),e,n,t.manifest.installationStorePath)}catch(r){let o=r instanceof Error?r.message:String(r);u(`WARN: derivePlatformAgentId failed open: ${o}`)}}async function Wr(t){let{gitRoot:e}=t.manifest,n=Wt(),r=String(t.runnerPlatform),[o,s,a,i,c,d]=await Promise.allSettled([st(e),Vt(),Xt(n),Gt(),Zt(t.manifest.manifest,e,r),oe(r,e)]),f=A(o,null,"gitSnapshot"),k=A(s,"","detectVirt"),y=A(a,null,"probeCloud"),E=A(i,{},"hostEnv"),q=A(c,[],"mcpDeclared"),I=A(d,zr(r),"selfConfig");await Z(t,e,f);let C=l(t.hookInput,"model");C&&(I.model=C);let M={identity:{host:Jt(),context:n,virt:k||null,cloud:y,git:f},host_env:E,env_vars:qt(),mcp:{declared:q},self_config:I,run_meta:{trigger:t.hookSource||"startup"},hook_input:L(t.hookInput,t.manifest.manifest,{gitRoot:e})};await O("session_start",M,X(t),b,t.manifest.collectorUrl)}async function Vr(t){let{gitRoot:e}=t.manifest;await Z(t,e);let n=Br(l(t.hookInput,"status"),l(t.hookInput,"stop_reason"),l(t.hookInput,"reason"),mt(l(t.hookInput,"error"))),r=t.runnerPlatform==="claude"?await be(l(t.hookInput,"transcript_path")):null,o=L(t.hookInput,t.manifest.manifest,{gitRoot:e});r&&(o.model=r),await O("session_end",{status:n,...r?{model:r}:{},hook_input:o},X(t),b,t.manifest.collectorUrl)}async function Gr(t){let e=me(t.hookInput);if(e.length===0)return;let{gitRoot:n}=t.manifest;await Z(t,n);let r=L(t.hookInput,t.manifest.manifest,{gitRoot:n}),o=X(t);await Promise.all(e.map(s=>O("tool_invoked",{tool_name:"Skill",skill_name:s,success:null,duration_ms:null,hook_input:r},o,b,t.manifest.collectorUrl)))}async function Jr(t){let{gitRoot:e}=t.manifest;await Z(t,e);let{toolName:n,skillName:r}=Se(t.hookInput),o=Ee(t.hookInput),s=ge(t.runnerEvent),a=s===!1?ke(t.hookInput,t.runnerEvent):null,{mcpServer:i,mcpTool:c}=Ie(n),d=ye(t.hookInput);await O("tool_invoked",{tool_name:n,skill_name:r,success:s,duration_ms:o,...a?{failure_reason:a}:{},...i?{mcp_server:i}:{},...c?{mcp_tool:c}:{},...d?{subagent_type:d}:{},hook_input:L(t.hookInput,t.manifest.manifest,{gitRoot:e})},X(t),b,t.manifest.collectorUrl)}async function Oe(t){switch(t.internalEvent){case"session_start":await Wr(t);return;case"session_end":await Vr(t);return;case"skill_invoked":await Gr(t);return;case"tool_invoked":await Jr(t);return;default:return}}function Le(t){return t!=="session_start"&&t!=="session_end"&&t!=="tool_invoked"&&t!=="skill_invoked"&&t!==""}import{existsSync as St,readFileSync as Yr}from"node:fs";import{dirname as Xr,join as Me,relative as Zr}from"node:path";var He=[It,".cursor/evalon-runtime-report.json",".codex/evalon-runtime-report.json"];async function qr(t=process.cwd(),e){let n=p("EVALON_HOOK_MANIFEST");if(n&&St(n))return n;let r=e??await $(t);for(let o of He){let s=Me(r,o);if(St(s))return s}for(let o of He){let s=Me(t,o);if(St(s))return s}}async function Qr(t,e){let n=e??await $(Xr(t)),r=Zr(n,t),o=await S("git",["ls-files","--error-unmatch",r],2e3,{cwd:n});return o.ok?!0:o.code===1?!1:(u(`WARN: git ls-files failed for manifest ${t} (code=${String(o.code)}); treating as tracked`),!0)}function to(t){let e=Yr(t,"utf8");return g(e,{})}function eo(){return{schema_version:2,mcp_servers:[]}}async function Fe(t=process.cwd()){let e=await $(t),n=await qr(t,e),r=n?to(n):eo(),o,s=n?await Qr(n,e):!1;s&&r.installation_id?(u(`WARN: ignoring installation_id in git-tracked manifest ${n}; identity is per-machine under ~/.evalon/agent-hook/installations/`),delete r.installation_id):!s&&r.installation_id&&(o=r.installation_id,delete r.installation_id),r.capture_hook_input==="full"&&n&&s&&(u(`WARN: ignoring capture_hook_input=full in git-tracked manifest ${n}; use EVALON_HOOK_CAPTURE_FULL_INPUT=1 or an untracked operator manifest`),delete r.capture_hook_input);let{installationId:a,storePath:i}=await Bt(e,{migrateFromManifestId:o});return r.installation_id=a,{manifestPath:n??i,manifest:r,installationId:a,installationStorePath:i,collectorUrl:le(r),platformAgentId:r.platform_agent_id??"",gitRoot:e}}async function Ue(t,e,n,r){let o=e??await nt(tt),s=g(o,{}),a=await Fe();if(!a)return;r&&(a.collectorUrl=r);let i=_e(s,a.manifest.platform,n),c=he(t,i.runnerEvent);if(!c){u(`WARN: unknown hook event '${i.runnerEvent}'; skipping telemetry`);return}let d={hookInput:s,sessionId:i.sessionId,turnId:i.turnId,runnerEvent:i.runnerEvent,runnerPlatform:i.runnerPlatform,hookSource:i.hookSource,internalEvent:c,manifest:a,platformAgentId:a.platformAgentId};if(Le(c)){u(`WARN: unsupported internal event '${c}'`);return}await Oe(d)}function no(t,e){if(p("EVALON_HOOK_SYNC")==="1")return!0;let n=J(t),r=g(e,{}),o=n.event??(l(r,"hook_event_name")||l(r,"event")),s=pt(r,o,void 0,n.runner);return Y(o,s)}async function De(t=process.argv.slice(2)){if(t[0]===gt){try{await Ce(Ft)}catch{}return 0}if(t[0]===_t){try{let e=J(t.slice(1));await Ue(e.event,void 0,e.runner,ft(e))}catch(e){let n=e instanceof Error?e.message:String(e);u(`WARN: detached work failed open: ${n}`)}return 0}try{let e=await nt(tt),n=J(t),r=ft(n);no(t,e)?await Ue(n.event,e,n.runner,r):await xe(e,t)}catch(e){let n=e instanceof Error?e.message:String(e);u(`WARN: hook failed open: ${n}`)}finally{Ht()}return 0}De().then(t=>{process.exit(t)}).catch(t=>{let e=t instanceof Error?t.message:String(t);process.stderr.write(`[evalon-agent-hook] ${e}
|
|
12
|
+
`),process.exit(1)});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/plugin-hooks.ts
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var HOOK_BINARY_NAME = "evalon-agent-hook";
|
|
7
|
+
var GLOBAL_HOOK_BINARY_RELATIVE = `current/${HOOK_BINARY_NAME}`;
|
|
8
|
+
var PROJECT_HOOK_BINARY_RELATIVE = `.evalon/agent-hook/${HOOK_BINARY_NAME}`;
|
|
9
|
+
var HOOK_INPUT_ALLOWLIST_MAX_BYTES = 4 * 1024;
|
|
10
|
+
var HOOK_INPUT_FULL_MAX_BYTES = 32 * 1024;
|
|
11
|
+
var HOOK_INPUT_MCP_MAX_BYTES = 4 * 1024;
|
|
12
|
+
var PACKAGE_VERSION = "1.0.6";
|
|
13
|
+
var NPX_PACKAGE_SPEC = `@evalon-ai/agent-hook@${PACKAGE_VERSION}`;
|
|
14
|
+
var EVALON_HOOK_STATUS_MESSAGE = "Evalon runtime report";
|
|
15
|
+
|
|
16
|
+
// src/install.ts
|
|
17
|
+
function npxHookCommand(packageSpec = NPX_PACKAGE_SPEC) {
|
|
18
|
+
return `npx -y ${packageSpec}`;
|
|
19
|
+
}
|
|
20
|
+
function withRunnerFlag(command, runner) {
|
|
21
|
+
const stripped = command.replace(/\s+--runner(?:=|\s+)\S+/g, "").trimEnd();
|
|
22
|
+
return `${stripped} --runner ${runner}`;
|
|
23
|
+
}
|
|
24
|
+
function desiredClaudeHookGroups(command) {
|
|
25
|
+
const commandHook = () => ({
|
|
26
|
+
hooks: [{ type: "command", command }]
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
SessionStart: [commandHook()],
|
|
30
|
+
// Slash skills bypass PreToolUse(Skill); UserPromptExpansion is the observable path.
|
|
31
|
+
UserPromptExpansion: [commandHook()],
|
|
32
|
+
// All tools (replaces prior Skill-only matcher on reinstall).
|
|
33
|
+
PreToolUse: [commandHook()],
|
|
34
|
+
PostToolUse: [commandHook()],
|
|
35
|
+
PostToolUseFailure: [commandHook()],
|
|
36
|
+
// Denied-before-run tools never fire PostToolUseFailure.
|
|
37
|
+
PermissionDenied: [commandHook()],
|
|
38
|
+
Stop: [commandHook()],
|
|
39
|
+
// Turn ended due to API error (rate_limit / overloaded / …).
|
|
40
|
+
StopFailure: [commandHook()],
|
|
41
|
+
SessionEnd: [commandHook()],
|
|
42
|
+
SubagentStart: [commandHook()],
|
|
43
|
+
SubagentStop: [commandHook()]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function desiredCodexHookGroups(command) {
|
|
47
|
+
const entry = {
|
|
48
|
+
type: "command",
|
|
49
|
+
command,
|
|
50
|
+
statusMessage: EVALON_HOOK_STATUS_MESSAGE
|
|
51
|
+
};
|
|
52
|
+
const commandHook = () => ({ hooks: [entry] });
|
|
53
|
+
return {
|
|
54
|
+
SessionStart: [commandHook()],
|
|
55
|
+
// Slash / custom-prompt skill ids from prompt text (body never POSTed on allowlist).
|
|
56
|
+
UserPromptSubmit: [commandHook()],
|
|
57
|
+
PreToolUse: [commandHook()],
|
|
58
|
+
PostToolUse: [commandHook()],
|
|
59
|
+
Stop: [commandHook()]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
var CURSOR_WIRED_HOOK_EVENTS = [
|
|
63
|
+
"sessionStart",
|
|
64
|
+
"beforeSubmitPrompt",
|
|
65
|
+
"preToolUse",
|
|
66
|
+
"postToolUse",
|
|
67
|
+
"postToolUseFailure",
|
|
68
|
+
"subagentStart",
|
|
69
|
+
"subagentStop",
|
|
70
|
+
"stop"
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
// src/plugin-hooks.ts
|
|
74
|
+
function buildClaudePluginHooks(command) {
|
|
75
|
+
return { hooks: desiredClaudeHookGroups(command) };
|
|
76
|
+
}
|
|
77
|
+
function buildCursorPluginHooks(command) {
|
|
78
|
+
const hooks = {};
|
|
79
|
+
for (const eventName of CURSOR_WIRED_HOOK_EVENTS) {
|
|
80
|
+
hooks[eventName] = [{ command }];
|
|
81
|
+
}
|
|
82
|
+
return { hooks };
|
|
83
|
+
}
|
|
84
|
+
function buildCodexPluginHooks(command) {
|
|
85
|
+
return { hooks: desiredCodexHookGroups(command) };
|
|
86
|
+
}
|
|
87
|
+
function marketplacePluginHookPaths(marketplaceRoot) {
|
|
88
|
+
return {
|
|
89
|
+
claude: join(marketplaceRoot, "plugins/claude-runtime-hooks/hooks/hooks.json"),
|
|
90
|
+
cursor: join(marketplaceRoot, "plugins/cursor-runtime-hooks/hooks/hooks.json"),
|
|
91
|
+
codex: join(marketplaceRoot, "plugins/codex-runtime-hooks/hooks/hooks.json")
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function writeJson(path, value) {
|
|
95
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
96
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
97
|
+
`, "utf8");
|
|
98
|
+
}
|
|
99
|
+
function writeMarketplacePluginHooks(marketplaceRoot, packageSpec = NPX_PACKAGE_SPEC) {
|
|
100
|
+
const base = npxHookCommand(packageSpec);
|
|
101
|
+
const paths = marketplacePluginHookPaths(marketplaceRoot);
|
|
102
|
+
writeJson(paths.claude, buildClaudePluginHooks(withRunnerFlag(base, "claude")));
|
|
103
|
+
writeJson(paths.cursor, buildCursorPluginHooks(withRunnerFlag(base, "cursor")));
|
|
104
|
+
writeJson(paths.codex, buildCodexPluginHooks(withRunnerFlag(base, "codex")));
|
|
105
|
+
return paths;
|
|
106
|
+
}
|
|
107
|
+
function expectedMarketplacePluginHooks(packageSpec = NPX_PACKAGE_SPEC) {
|
|
108
|
+
const base = npxHookCommand(packageSpec);
|
|
109
|
+
return {
|
|
110
|
+
claude: buildClaudePluginHooks(withRunnerFlag(base, "claude")),
|
|
111
|
+
cursor: buildCursorPluginHooks(withRunnerFlag(base, "cursor")),
|
|
112
|
+
codex: buildCodexPluginHooks(withRunnerFlag(base, "codex"))
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
buildClaudePluginHooks,
|
|
117
|
+
buildCodexPluginHooks,
|
|
118
|
+
buildCursorPluginHooks,
|
|
119
|
+
expectedMarketplacePluginHooks,
|
|
120
|
+
marketplacePluginHookPaths,
|
|
121
|
+
writeMarketplacePluginHooks
|
|
122
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ev-ai/agent-hook",
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Cross-runner runtime telemetry hook for Claude Code, Cursor, and Codex",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"evalon-agent-hook": "dist/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"COLLECTED-DATA.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"registry": "https://npm.pkg.github.com"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "node scripts/build.mjs",
|
|
19
|
+
"build:hook-binary": "node scripts/compile-hook.mjs",
|
|
20
|
+
"build:hook-binary:host": "node scripts/compile-hook.mjs --host",
|
|
21
|
+
"ensure:project-binary": "sh -c 'test -x \"$(git rev-parse --show-toplevel)/.evalon/agent-hook/evalon-agent-hook\" || (yarn build && yarn build:hook-binary:host && node dist/cli.mjs update)'",
|
|
22
|
+
"update:local": "yarn build && yarn build:hook-binary:host && node dist/cli.mjs update",
|
|
23
|
+
"clean": "rm -rf dist compile-out",
|
|
24
|
+
"lint": "eslint src",
|
|
25
|
+
"lint:fix": "eslint --fix src",
|
|
26
|
+
"test": "vitest run --config vitest.config.unit.ts",
|
|
27
|
+
"sync:hooks": "node scripts/sync-hook-events.mjs",
|
|
28
|
+
"pin:hooks": "node scripts/pin-version.mjs",
|
|
29
|
+
"pin:local": "node scripts/pin-version.mjs --local",
|
|
30
|
+
"publish:gh": "node scripts/publish-gh.mjs"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "~24.10.13",
|
|
37
|
+
"esbuild": "~0.27.3",
|
|
38
|
+
"eslint": "~9.39.1",
|
|
39
|
+
"eslint-config-prettier": "~10.1.0",
|
|
40
|
+
"typescript": "~5.9.3",
|
|
41
|
+
"vitest": "~4.1.0"
|
|
42
|
+
}
|
|
43
|
+
}
|