feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)

Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from
Settings → Dashboard to the Claude Code tab's AgentHub sub-tab
(ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub.
A home that is or contains the OS home is refused (EBADHOME).

Settings persistence: dashboard-saved settings go to
~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin
cache an update wipes. Load order: real env > the checkout's .env > that file.
The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put.

C1: codex-usage.js reads Codex rollout journals into sessions (new column
tool) and token_usage, tracked per file in codex_rollouts; forks deduped by
total vector, sub-agent threads folded into the root; pricing-sync also
prices OpenAI models; PricingSettings with a vendor filter.

C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on
a click, through a stable forwarder; Codex trust is never written. The hook
route creates tool='codex' sessions and folds sub-agent hooks into the root.

D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens
from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js
installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never
PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab;
Gemini pricing.

Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing
on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=,
clear-data covering the new tables, Codex child-thread events moving no
status, hooks owning status only while current, EFOREIGN for a forwarder
that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE
honoured from .env.

Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
This commit is contained in:
2026-09-29 16:09:19 +07:00
parent 7ec5a7cd85
commit 969b06253c
95 changed files with 9278 additions and 1696 deletions
+40 -5
View File
@@ -448,7 +448,7 @@ The OpenAPI spec is generated from `server/openapi.js` (`createOpenApiSpec()`),
| Method | Path | Description |
| ------- | ------------------- | ------------------------------------------------ |
| `GET` | `/api/health` | Server health check (`status`, `version`, `timestamp`) |
| `GET` | `/api/sessions` | List sessions (`status`, `limit`, `offset`) |
| `GET` | `/api/sessions` | List sessions (`status`, `limit`, `offset`; `tool=claude\|codex\|antigravity` filters by CLI before the limit) |
| `GET` | `/api/sessions/:id` | Session detail (includes `agents` + `events`) |
| `POST` | `/api/sessions` | Create session (idempotent by `id`) |
| `PATCH` | `/api/sessions/:id` | Update session |
@@ -593,12 +593,37 @@ pipeline's last node, where the pipeline is finished and a later detection is
new work — and, the one rule that matters, never counts as evidence and never
renders a node as `done` (see `docs/LANES.md#stage-detection`).
A `codex` lane has no hooks to read and carries no pipeline, so nothing detects
or advances its stage. `server/lib/codex-ingest.js` only mirrors its `status`
A `codex` lane carries no pipeline, so nothing detects or advances its stage.
Codex's hooks are opt-in (`server/lib/codex-hooks.js`, installed from Settings →
Codex → AgentHub and trusted by the user inside Codex); a hook posted with
`tool: "codex"` reaches only the codex lane at its cwd and creates a
`tool = 'codex'` session (see ARCHITECTURE.md, **Codex hooks**). Otherwise
`server/lib/codex-ingest.js` only mirrors its `status` — skipping a lane whose
hooks are current (an event for its session in the last two minutes) —
between `idle` and `running` from the mtime of Codex's own rollout journals
under `$CODEX_HOME/sessions/**/rollout-*.jsonl`, polled every
`DASHBOARD_CODEX_POLL_MS` ms (default 15000, `0` disables), under the guard hook
ingestion uses (no `run_id`, only from `idle`/`running`); it writes nothing else.
`server/lib/codex-usage.js` reads the same journals for every Codex session
(lane or not) into `sessions` (`tool = 'codex'`) and `token_usage`, tracking
each file's progress in `codex_rollouts` (see ARCHITECTURE.md).
An `antigravity` lane carries no pipeline either.
- **Sessions and tokens:** `server/lib/antigravity-usage.js` reads agy's
per-conversation SQLite files (`~/.gemini/antigravity-cli/conversations/<id>.db`)
every `DASHBOARD_ANTIGRAVITY_POLL_MS` ms (default 15000, `0` disables). They
become `sessions` rows (`tool = 'antigravity'`) and `token_usage` rows, and
`antigravity_conversations` holds the mtime read for each file.
- **Quiet sessions:** agy fires no exit hook, so a session quiet for ten
minutes (with hook events) or two (without) is settled to `completed`, its
agents too, and sets its lane idle. That lane must be one the hooks left
running with no dashboard pane.
- **Hooks:** opt-in. `server/lib/antigravity-hooks.js` installs them from
Settings → Antigravity: the named hook `agenthub` in
`~/.gemini/config/hooks.json`, with PreInvocation, PostToolUse and Stop,
never PreToolUse. A hook posted with `tool: "antigravity"` reaches only the
antigravity lane at its workspace (see ARCHITECTURE.md, **Antigravity
Sessions and Hooks**).
### Remote Data Sources
@@ -647,10 +672,20 @@ Because sync runs non-interactively (`ssh -o BatchMode=yes`), the connection mus
| Method | Path | Description |
| ------ | ------------------------------ | ------------------------------------------------ |
| `GET` | `/api/settings/info` | System info, DB stats, hooks status, cache stats. Also powers the Dashboard Health tab (server uptime, memory, CPU, DB record counts, WAL/journal mode, transcript cache hit/miss rates) |
| `POST` | `/api/settings/clear-data` | Delete all sessions/agents/events/token usage |
| `POST` | `/api/settings/clear-data` | Delete all sessions/agents/events/token usage, and empty `codex_rollouts`/`antigravity_conversations` so those sessions import again |
| `POST` | `/api/settings/reimport` | Re-import legacy sessions from `~/.claude/` |
| `POST` | `/api/settings/reinstall-hooks`| Reinstall Claude Code hooks |
| `POST` | `/api/settings/reset-pricing` | Reset pricing table to defaults |
| `POST` | `/api/settings/reset-pricing` | Reset pricing table to defaults, then run the automatic pricing sync at once (waiting up to 3 s). Body `{scope: "claude"}` (the Claude Code tab's **Reset Defaults**) deletes and reseeds only `claude-…` rows; no body resets every row |
| `GET` | `/api/settings/claude-home` | `{claude_home, env_owned}` — `env_owned` when the real environment or the checkout's `.env` set `CLAUDE_HOME` (a saved value is overridden at the next start) |
| `PUT` | `/api/settings/claude-home` | `{path}` → validate, apply, persist to AgentHub's settings file. 400 `EBADHOME` (is or contains the OS home) / `INVALID_PATH`, 500 `ESAVE`, 403 `EBADORIGIN` (`sameOriginGuard`). Answers `routers: {missing: [...]}` — lanes naming an AI Router profile the new `<CLAUDE_HOME>/agenthub-routers/` store lacks (they start on native) |
| `GET` | `/api/settings/codex-home` | `{codex_home, env_owned}` |
| `PUT` | `/api/settings/codex-home` | Same checks as `claude-home`; answers `routers: {updated, failed}` after re-writing every codex lane's AI Router layer into the new home (`reapplyCodexLayers`) |
| `GET` | `/api/settings/codex-hooks` | Status of AgentHub's hooks in `$CODEX_HOME/hooks.json` (installed, per-event, Codex's trust as read from `config.toml`) |
| `POST` | `/api/settings/codex-hooks` | Install or reinstall them (`sameOriginGuard`). 400 `EBADJSON` / `EUNSUPPORTED` / `EFOREIGN` (the forwarder path holds a file that is not AgentHub's) |
| `DELETE` | `/api/settings/codex-hooks` | Remove AgentHub's hooks only (`sameOriginGuard`); a foreign forwarder is left alone |
| `GET` | `/api/settings/antigravity-hooks` | Status of the named hook `agenthub` in `~/.gemini/config/hooks.json` |
| `POST` | `/api/settings/antigravity-hooks` | Install or reinstall it — PreInvocation, PostToolUse, Stop, never PreToolUse (`sameOriginGuard`; same 400 codes) |
| `DELETE` | `/api/settings/antigravity-hooks` | Remove the `agenthub` hook only (`sameOriginGuard`) |
| `GET` | `/api/settings/export` | Export all data (sessions, agents, events, token_usage, workflows, dashboard_runs, alert_rules, model_pricing) as one versioned JSON attachment |
| `POST` | `/api/settings/import` | Restore a bundle from `/export`. Multipart `file`, or JSON `{ path }` (server reads it). Idempotent + non-destructive: sessions already present are skipped whole |
| `POST` | `/api/settings/cleanup` | Abandon stale sessions and purge old data |