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
+2 -1
View File
@@ -24,7 +24,8 @@
"hooks": "Hooks",
"keybindings": "Keybindings",
"settings": "Settings",
"memory": "Memory"
"memory": "Memory",
"agenthub": "AgentHub"
},
"overview": {
"rootsTitle": "Roots",
+2 -1
View File
@@ -18,7 +18,8 @@
"memory": "Memory",
"config": "Config",
"mcp": "MCP servers",
"plugins": "Plugins"
"plugins": "Plugins",
"agenthub": "AgentHub"
},
"overview": {
"rootsTitle": "Roots",
+2 -1
View File
@@ -39,5 +39,6 @@
"timeline": "Timeline ({{shown}}/{{total}})",
"transcriptNotFound": "Conversation transcript not found for this agent. The transcript file may be missing or not yet linked."
},
"remoteSourceBadgeTitle": "Collected from a remote machine"
"remoteSourceBadgeTitle": "Collected from a remote machine",
"toolBadgeTitle": "Recorded by {{tool}}"
}
+77 -9
View File
@@ -1,6 +1,6 @@
{
"title": "Settings",
"subtitle": "Manage pricing, notifications, data, and hooks",
"subtitle": "Manage appearance, AI tools, notifications and data",
"exportData": "Export Data",
"acrossSessions": "Across all tracked sessions",
"basedOnUsage": "Based on per-model token usage",
@@ -23,8 +23,6 @@
"resetResult": "Reset to {{count}} default rules",
"addModel": "Add Model",
"pattern": "Pattern",
"patternPlaceholder": "claude-opus-4%",
"namePlaceholder": "Claude Opus 4",
"validationRequired": "Pattern and display name are required",
"lastUpdated": "Last updated: ",
"tooltip": {
@@ -36,21 +34,41 @@
"manualUpdates": "You must update prices manually",
"manualUpdatesBody": "Anthropic publishes pricing changes on their website - there is no live feed. When a new model launches or rates change, edit the matching rule (or add a new one above the catch-all) so newly recorded sessions are costed correctly. Already-stored sessions keep the price that was applied at ingest time.",
"apiPricing": "Reference only - API rates",
"apiPricingBody": "These are pay-as-you-go API rates. If you use Claude Code on a subscription plan (Pro, Max, Team, or Enterprise), your actual cost is a flat monthly fee, not per-token - so these figures will usually be higher than what you really pay. Treat the costs here as a usage estimate for reference, not a bill."
"apiPricingBody": "These are pay-as-you-go API rates. If you use Claude Code on a subscription plan (Pro, Max, Team, or Enterprise), your actual cost is a flat monthly fee, not per-token - so these figures will usually be higher than what you really pay. Treat the costs here as a usage estimate for reference, not a bill.",
"synced": "Prices are filled in automatically",
"syncedBody": "A model a session uses with no rule of its own is priced from LiteLLM's public price list the first time it is seen. That row is never updated afterwards: edit it here when the vendor's rates change. Already-stored sessions keep the price that was applied at ingest time.",
"apiPricingOtherBody": "These are pay-as-you-go API rates. If you use this CLI on a subscription plan, your actual cost is a flat fee, not per-token - so these figures will usually be higher than what you really pay. Treat them as a usage estimate, not a bill."
},
"cacheWrite5m": "Cache Write (5m)",
"cacheWrite1h": "Cache Write (1h)",
"fastInput": "Fast Input",
"fastOutput": "Fast Output"
"fastOutput": "Fast Output",
"showAll": "Show all models ({{count}} more)",
"empty": {
"claude": "No Claude model prices. Reset Defaults restores them.",
"openai": "No OpenAI model prices yet. They are added automatically from LiteLLM's price list once a Codex session uses a model, or add one yourself.",
"google": "No Gemini model prices yet. They are added automatically from LiteLLM's price list once an Antigravity session uses a model, or add one yourself."
},
"placeholders": {
"claude": {
"pattern": "claude-opus-4%",
"name": "Claude Opus 4"
},
"openai": {
"pattern": "gpt-5%",
"name": "GPT 5"
},
"google": {
"pattern": "gemini-3.8-flash%",
"name": "Gemini 3.8 Flash"
}
}
},
"claudeHome": {
"title": "CLAUDE_HOME",
"description": "Root directory for Claude Code session data. Changing this updates the scan path for transcripts and imports immediately.",
"save": "Save",
"saving": "Saving...",
"current": "Current:",
"placeholder": "/path/to/claude-home",
"saveFailed": "Failed to update CLAUDE_HOME"
"routersMissing": "Saved. These lanes name an AI Router profile the new home does not have, and start on Native until it is added there or they are re-pointed: {{lanes}}"
},
"hooks": {
"title": "Hook Configuration",
@@ -450,6 +468,7 @@
"dashboard": "Dashboard",
"claudeCode": "Claude Code",
"codex": "Codex",
"antigravity": "Antigravity",
"aiRouter": "AI Router",
"gitCredentials": "Git credentials"
},
@@ -556,5 +575,54 @@
"colSaved": "Saved",
"empty": "No git login saved. The git bar asks for one the first time a host needs it.",
"badUrl": "Enter the server as a URL, e.g. https://git.example.com"
},
"homeDir": {
"save": "Save",
"saving": "Saving...",
"current": "Current:",
"saveFailed": "Failed to save the directory",
"envOwned": "This variable is set by the environment the dashboard started in, or by the checkout's .env — either wins at every start: a value saved here lasts until the next restart."
},
"codexHome": {
"title": "CODEX_HOME",
"description": "Root directory for Codex config and session journals. The dashboard reads it, writes AI Router layers into it, and starts every codex lane with it.",
"restartNote": "A codex lane already running keeps the home it started with, and shows as idle, until it is restarted.",
"placeholder": "/path/to/codex-home",
"routersFailed": "Saved, but the AI Router layer could not be re-written for: {{lanes}}"
},
"codexHooks": {
"title": "Codex hooks",
"description": "Let Codex report to AgentHub as it works: each tool call in the session's timeline, \"Codex needs your permission\" on the lane card and as a notification, and running/idle the moment a turn starts or ends. Tokens and cost come from Codex's journals either way.",
"notInstalled": "Not installed",
"untrusted": "Installed — waiting for your trust in Codex",
"trusted": "Installed and trusted",
"trustNote": "Codex runs a hook only after you trust it. The next time Codex starts it asks you to review new hooks: choose Trust all and continue (or run /hooks). Until then Codex skips them.",
"install": "Install",
"reinstall": "Reinstall",
"refresh": "Refresh",
"remove": "Remove",
"eventTrusted": "Trusted in Codex",
"eventUntrusted": "Not trusted in Codex yet",
"eventMissing": "Not installed",
"unsupported": "Codex hooks from AgentHub need a POSIX shell, so they are not available on Windows.",
"unreadable": "AgentHub cannot read {{path}}: {{error}}",
"failed": "Could not change Codex hooks: {{message}}"
},
"antigravityHooks": {
"title": "Antigravity hooks",
"description": "Let Antigravity's CLI (agy) report to AgentHub as it works: each tool call in the session's timeline, and running/idle the moment a run starts or ends. Tokens and cost come from agy's conversation files either way.",
"notInstalled": "Not installed",
"installed": "Installed",
"disabled": "Installed but switched off in hooks.json",
"note": "agy loads hooks at its next start and asks for no trust. The file is global, so the Antigravity IDE runs the hook too; AgentHub ignores what the IDE sends. agy has no hook for a permission prompt, so an Antigravity lane cannot show \"needs your permission\".",
"install": "Install",
"reinstall": "Reinstall",
"refresh": "Refresh",
"remove": "Remove",
"eventInstalled": "Installed",
"eventMissing": "Not installed",
"unsupported": "Antigravity hooks from AgentHub need a POSIX shell, so they are not available on Windows.",
"unreadable": "AgentHub cannot read {{path}}: {{error}}",
"failed": "Could not change Antigravity hooks: {{message}}"
}
}
+2 -1
View File
@@ -24,7 +24,8 @@
"hooks": "Hooks",
"keybindings": "Phím tắt",
"settings": "Cài đặt",
"memory": "Bộ nhớ"
"memory": "Bộ nhớ",
"agenthub": "AgentHub"
},
"overview": {
"rootsTitle": "Thư mục gốc",
+2 -1
View File
@@ -18,7 +18,8 @@
"memory": "Bộ nhớ",
"config": "Cấu hình",
"mcp": "Máy chủ MCP",
"plugins": "Plugin"
"plugins": "Plugin",
"agenthub": "AgentHub"
},
"overview": {
"rootsTitle": "Thư mục gốc",
+2 -1
View File
@@ -38,5 +38,6 @@
"conversation": "Hội thoại",
"timeline": "Dòng thời gian ({{shown}}/{{total}})",
"transcriptNotFound": "Không tìm thấy bản ghi hội thoại cho agent này. Tệp transcript có thể bị thiếu hoặc chưa được liên kết."
}
},
"toolBadgeTitle": "Ghi lại bởi {{tool}}"
}
+77 -9
View File
@@ -1,6 +1,6 @@
{
"title": "Cài đặt",
"subtitle": "Quản lý định giá, thông báo, dữ liệu và hooks",
"subtitle": "Quản lý giao diện, công cụ AI, thông báo và dữ liệu",
"exportData": "Xuất dữ liệu",
"acrossSessions": "Trên toàn bộ phiên đang được theo dõi",
"basedOnUsage": "Dựa trên mức dùng token theo mô hình",
@@ -23,8 +23,6 @@
"resetResult": "Đã đặt lại về {{count}} quy tắc mặc định",
"addModel": "Thêm mô hình",
"pattern": "Mẫu",
"patternPlaceholder": "claude-opus-4%",
"namePlaceholder": "Claude Opus 4",
"validationRequired": "Bắt buộc nhập mẫu và tên hiển thị",
"lastUpdated": "Cập nhật lần cuối: ",
"tooltip": {
@@ -36,21 +34,41 @@
"manualUpdates": "Bạn phải cập nhật giá thủ công",
"manualUpdatesBody": "Anthropic công bố thay đổi giá trên website của họ - không có nguồn dữ liệu trực tiếp. Khi có model mới ra mắt hoặc đơn giá thay đổi, hãy chỉnh sửa quy tắc tương ứng (hoặc thêm quy tắc mới ở phía trên quy tắc bao quát) để những phiên ghi nhận sau này được tính phí chính xác. Phiên đã lưu vẫn giữ nguyên giá đã áp dụng tại thời điểm nạp.",
"apiPricing": "Chỉ để tham khảo - đơn giá API",
"apiPricingBody": "Đây là đơn giá API trả theo lượng dùng. Nếu bạn dùng Claude Code theo gói thuê bao (Pro, Max, Team hoặc Enterprise), chi phí thực tế là phí cố định hàng tháng chứ không tính theo token - nên những con số ở đây thường cao hơn số tiền bạn thực trả. Hãy xem chi phí ở đây là ước tính mức dùng để tham khảo, không phải hóa đơn."
"apiPricingBody": "Đây là đơn giá API trả theo lượng dùng. Nếu bạn dùng Claude Code theo gói thuê bao (Pro, Max, Team hoặc Enterprise), chi phí thực tế là phí cố định hàng tháng chứ không tính theo token - nên những con số ở đây thường cao hơn số tiền bạn thực trả. Hãy xem chi phí ở đây là ước tính mức dùng để tham khảo, không phải hóa đơn.",
"synced": "Giá được điền tự động",
"syncedBody": "Một model mà phiên dùng nhưng chưa có quy tắc riêng sẽ được lấy giá từ bảng giá công khai của LiteLLM ở lần đầu gặp. Dòng đó không bao giờ tự cập nhật lại: hãy sửa ở đây khi nhà cung cấp đổi giá. Các phiên đã lưu vẫn giữ mức giá áp dụng lúc ghi nhận.",
"apiPricingOtherBody": "Đây là giá API trả theo mức dùng. Nếu bạn dùng CLI này theo gói thuê bao, chi phí thực là một khoản cố định chứ không tính theo token, nên các con số ở đây thường cao hơn số tiền bạn thực trả. Hãy xem chúng là ước tính mức dùng, không phải hoá đơn."
},
"cacheWrite5m": "Ghi bộ nhớ đệm (5m)",
"cacheWrite1h": "Ghi bộ nhớ đệm (1h)",
"fastInput": "Fast Input",
"fastOutput": "Fast Output"
"fastOutput": "Fast Output",
"showAll": "Hiện mọi model (thêm {{count}})",
"empty": {
"claude": "Chưa có giá model Claude. Nút Khôi phục mặc định sẽ đưa chúng trở lại.",
"openai": "Chưa có giá model OpenAI. Giá được thêm tự động từ bảng giá LiteLLM khi một phiên Codex dùng model đó, hoặc bạn tự thêm.",
"google": "Chưa có giá model Gemini. Giá được thêm tự động từ bảng giá LiteLLM khi một phiên Antigravity dùng model đó, hoặc bạn tự thêm."
},
"placeholders": {
"claude": {
"pattern": "claude-opus-4%",
"name": "Claude Opus 4"
},
"openai": {
"pattern": "gpt-5%",
"name": "GPT 5"
},
"google": {
"pattern": "gemini-3.8-flash%",
"name": "Gemini 3.8 Flash"
}
}
},
"claudeHome": {
"title": "CLAUDE_HOME",
"description": "Thư mục gốc chứa dữ liệu phiên Claude Code. Thay đổi sẽ cập nhật ngay đường dẫn quét cho transcript và import.",
"save": "Lưu",
"saving": "Đang lưu...",
"current": "Hiện tại:",
"placeholder": "/đường-dẫn/tới/claude-home",
"saveFailed": "Cập nhật CLAUDE_HOME thất bại"
"routersMissing": "Đã lưu. Các lane này dùng một hồ sơ AI Router mà thư mục mới không có, nên sẽ chạy Native cho đến khi bạn thêm hồ sơ đó ở đây hoặc chọn lại router cho lane: {{lanes}}"
},
"hooks": {
"title": "Cấu hình hook",
@@ -450,6 +468,7 @@
"dashboard": "Dashboard",
"claudeCode": "Claude Code",
"codex": "Codex",
"antigravity": "Antigravity",
"aiRouter": "AI Router",
"gitCredentials": "Git credentials"
},
@@ -556,5 +575,54 @@
"colSaved": "Đã lưu",
"empty": "Chưa lưu tài khoản git nào. Thanh git sẽ hỏi khi một host cần đăng nhập.",
"badUrl": "Nhập máy chủ dạng URL, ví dụ https://git.example.com"
},
"homeDir": {
"save": "Lưu",
"saving": "Đang lưu...",
"current": "Hiện tại:",
"saveFailed": "Không lưu được thư mục",
"envOwned": "Biến này được đặt bởi môi trường lúc khởi động dashboard, hoặc bởi file .env của bản checkout, và nguồn đó luôn được ưu tiên mỗi lần khởi động: giá trị lưu ở đây chỉ giữ đến lần khởi động lại tiếp theo."
},
"codexHome": {
"title": "CODEX_HOME",
"description": "Thư mục gốc chứa config và nhật ký phiên của Codex. Dashboard đọc từ đây, ghi layer AI Router vào đây, và mở mọi lane codex với thư mục này.",
"restartNote": "Lane codex đang chạy vẫn dùng thư mục cũ, và hiện là idle, cho tới khi được khởi động lại.",
"placeholder": "/đường-dẫn/tới/codex-home",
"routersFailed": "Đã lưu, nhưng không ghi lại được layer AI Router cho: {{lanes}}"
},
"codexHooks": {
"title": "Hook của Codex",
"description": "Cho Codex báo về AgentHub trong lúc làm việc: từng lần gọi công cụ trong dòng thời gian của phiên, \"Codex cần bạn cho phép\" trên thẻ lane và qua thông báo, và trạng thái chạy/nghỉ ngay khi một lượt bắt đầu hay kết thúc. Token và chi phí vẫn lấy từ nhật ký của Codex dù có hook hay không.",
"notInstalled": "Chưa cài",
"untrusted": "Đã cài — đang chờ bạn tin cậy trong Codex",
"trusted": "Đã cài và được tin cậy",
"trustNote": "Codex chỉ chạy một hook sau khi bạn tin cậy nó. Lần tới Codex khởi động, nó sẽ hỏi bạn xem lại các hook mới: chọn Trust all and continue (hoặc gõ /hooks). Trước đó Codex bỏ qua chúng.",
"install": "Cài đặt",
"reinstall": "Cài lại",
"refresh": "Làm mới",
"remove": "Gỡ bỏ",
"eventTrusted": "Đã được tin cậy trong Codex",
"eventUntrusted": "Chưa được tin cậy trong Codex",
"eventMissing": "Chưa cài",
"unsupported": "Hook của Codex từ AgentHub cần một shell POSIX nên không dùng được trên Windows.",
"unreadable": "AgentHub không đọc được {{path}}: {{error}}",
"failed": "Không đổi được hook của Codex: {{message}}"
},
"antigravityHooks": {
"title": "Hook của Antigravity",
"description": "Cho CLI của Antigravity (agy) báo về AgentHub trong lúc làm việc: từng lần gọi công cụ trong dòng thời gian của phiên, và trạng thái chạy/nghỉ ngay khi một lượt bắt đầu hay kết thúc. Token và chi phí vẫn lấy từ file hội thoại của agy dù có hook hay không.",
"notInstalled": "Chưa cài",
"installed": "Đã cài",
"disabled": "Đã cài nhưng đang tắt trong hooks.json",
"note": "agy nạp hook ở lần khởi động tiếp theo và không hỏi tin cậy. File này dùng chung, nên Antigravity IDE cũng chạy hook; AgentHub bỏ qua những gì IDE gửi. agy không có hook cho lời hỏi quyền, nên lane Antigravity không thể hiện \"cần bạn cấp quyền\".",
"install": "Cài đặt",
"reinstall": "Cài lại",
"refresh": "Làm mới",
"remove": "Gỡ bỏ",
"eventInstalled": "Đã cài",
"eventMissing": "Chưa cài",
"unsupported": "Hook của Antigravity từ AgentHub cần một shell POSIX nên không dùng được trên Windows.",
"unreadable": "AgentHub không đọc được {{path}}: {{error}}",
"failed": "Không đổi được hook của Antigravity: {{message}}"
}
}