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
+54 -8
View File
@@ -393,6 +393,8 @@ import type {
AlertEvent,
AlertRule,
Analytics,
CodexHooksStatus,
AntigravityHooksStatus,
CostResult,
DashboardEvent,
Lane,
@@ -676,6 +678,8 @@ export const api = {
status?: string;
q?: string;
cwd?: string;
/** One CLI's sessions only (`claude`, `codex`, `antigravity`). */
tool?: string;
sort_by?: string;
sort_desc?: boolean;
limit?: number;
@@ -686,6 +690,7 @@ export const api = {
if (params?.status) qs.set("status", params.status);
if (params?.q) qs.set("q", params.q);
if (params?.cwd) qs.set("cwd", params.cwd);
if (params?.tool) qs.set("tool", params.tool);
if (params?.sort_by) qs.set("sort_by", params.sort_by);
// `!== undefined` (not truthiness) so an explicit `sort_desc: false` is preserved.
if (params?.sort_desc !== undefined) qs.set("sort_desc", String(params.sort_desc));
@@ -998,17 +1003,56 @@ export const api = {
* GET /api/settings/claude-home - the currently configured Claude home path.
* @returns `{ claude_home }` — the absolute path the server reads config from.
*/
get: () => request<{ claude_home: string }>("/settings/claude-home"),
get: () => request<{ claude_home: string; env_owned?: boolean }>("/settings/claude-home"),
/**
* PUT /api/settings/claude-home - repoint the server at a new Claude home.
* @param path New absolute `~/.claude` root the server should read from.
* @returns `{ ok, claude_home }` — success flag and the accepted path.
* @returns `{ ok, claude_home, routers }` — the accepted path, and the
* lanes whose AI Router profile the new home's store does not have.
*/
set: (path: string) =>
request<{ ok: boolean; claude_home: string }>("/settings/claude-home", {
method: "PUT",
body: JSON.stringify({ path }),
}),
request<{
ok: boolean;
claude_home: string;
env_owned?: boolean;
routers: { missing: string[] };
}>("/settings/claude-home", { method: "PUT", body: JSON.stringify({ path }) }),
},
/** The Codex home: same shape as `claudeHome`, plus the AI Router layers
* a PUT re-wrote into the new home for codex lanes. */
codexHome: {
/** GET /api/settings/codex-home */
get: () => request<{ codex_home: string; env_owned: boolean }>("/settings/codex-home"),
/** PUT /api/settings/codex-home */
set: (path: string) =>
request<{
ok: boolean;
codex_home: string;
env_owned: boolean;
routers: { updated: number; failed: string[] };
}>("/settings/codex-home", { method: "PUT", body: JSON.stringify({ path }) }),
},
/** AgentHub's hooks in Codex's `$CODEX_HOME/hooks.json` — changed only on a
* click; Codex asks the user to trust them before they run. */
codexHooks: {
/** GET /api/settings/codex-hooks */
get: () => request<CodexHooksStatus>("/settings/codex-hooks"),
/** POST /api/settings/codex-hooks — install, or reinstall */
install: () => request<CodexHooksStatus>("/settings/codex-hooks", { method: "POST" }),
/** DELETE /api/settings/codex-hooks — AgentHub's own hooks only */
remove: () => request<CodexHooksStatus>("/settings/codex-hooks", { method: "DELETE" }),
},
/** AgentHub's named hook in `~/.gemini/config/hooks.json` for agy — changed
* only on a click; agy runs it with no trust step. */
antigravityHooks: {
/** GET /api/settings/antigravity-hooks */
get: () => request<AntigravityHooksStatus>("/settings/antigravity-hooks"),
/** POST /api/settings/antigravity-hooks — install, or reinstall */
install: () =>
request<AntigravityHooksStatus>("/settings/antigravity-hooks", { method: "POST" }),
/** DELETE /api/settings/antigravity-hooks — AgentHub's named hook only */
remove: () =>
request<AntigravityHooksStatus>("/settings/antigravity-hooks", { method: "DELETE" }),
},
/**
* POST /api/settings/clear-data - DESTRUCTIVE: wipes sessions/agents/
@@ -1063,11 +1107,13 @@ export const api = {
* Wipes user-customized pricing rules and reseeds the shipped defaults;
* returns the resulting rule set so the Pricing UI can re-render.
*
* @returns `{ ok, pricing }` — the full default rule list now in effect.
* @param scope - `"claude"` resets only `claude-…` rows; omitted, every row.
* @returns `{ ok, pricing }` — the full rule list now in effect.
*/
resetPricing: () =>
resetPricing: (scope?: "claude") =>
request<{ ok: boolean; pricing: ModelPricing[] }>("/settings/reset-pricing", {
method: "POST",
...(scope && { body: JSON.stringify({ scope }) }),
}),
/**
* Direct download URL for GET /api/settings/export (a full DB dump);
+39
View File
@@ -629,6 +629,40 @@ export type EffectiveSessionStatus = SessionStatus | typeof AWAITING_STATUS;
// `Agent` is one process within it (the main loop or a spawned subagent). Both
// are created lazily on their first hook event and mutated as hooks stream in.
/** GET/POST/DELETE /api/settings/codex-hooks: AgentHub's hooks in Codex. */
export interface CodexHooksStatus {
/** False on Windows: the hook command is a POSIX shell script. */
supported: boolean;
/** False when hooks.json exists but is not a JSON object (see `error`). */
readable: boolean;
error?: string;
/** `$CODEX_HOME/hooks.json` and the forwarder script it points at. */
path: string;
script: string;
/** Every event has AgentHub's hook, and the script exists. */
installed: boolean;
/** Codex's config.toml holds a trust entry for every one of them. */
trusted: boolean;
events: Record<string, { installed: boolean; trusted: boolean }>;
}
/** GET /api/settings/antigravity-hooks — AgentHub's named hook for agy. */
export interface AntigravityHooksStatus {
/** False on Windows: the hook command is a POSIX shell script. */
supported: boolean;
/** False when hooks.json exists but is not a JSON object (see `error`). */
readable: boolean;
error?: string;
/** `~/.gemini/config/hooks.json` and the forwarder script it points at. */
path: string;
script: string;
/** Every event runs AgentHub's forwarder, and the script exists. */
installed: boolean;
/** Installed and not switched off (`"enabled": false`) in the file. */
enabled: boolean;
events: Record<string, { installed: boolean }>;
}
/**
* A Claude Code CLI invocation tracked by the dashboard - one row per top-level
* `claude` process, created on its first hook event (or on import) and updated
@@ -695,6 +729,11 @@ export interface Session {
* dashboard's own machine; any other value is a remote source id (see the
* Remote Data Sources feature / `remote_sources`). Maps to `sessions.source`. */
source?: string;
/** The AI coding CLI that produced the session: `"claude"` (the default —
* Claude Code's hooks and imported transcripts), `"codex"` (Codex's rollout
* journals and hooks) or `"antigravity"` (agy's conversation files and
* hooks). Attach/resume offer `claude` rows only. Maps to `sessions.tool`. */
tool?: string;
}
/**