Full Code of chrysb/alphaclaw for AI

main 27a548e988e6 cached
480 files
3.1 MB
836.6k tokens
110 symbols
1 requests
Download .txt
Showing preview only (3,343K chars total). Download the full file or copy to clipboard to get everything.
Repository: chrysb/alphaclaw
Branch: main
Commit: 27a548e988e6
Files: 480
Total size: 3.1 MB

Directory structure:
gitextract_dikz81a3/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .npmrc
├── AGENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│   └── alphaclaw.js
├── lib/
│   ├── cli/
│   │   ├── git-runtime.js
│   │   ├── git-sync.js
│   │   └── openclaw-config-restore.js
│   ├── plugin/
│   │   └── usage-tracker/
│   │       ├── index.js
│   │       └── openclaw.plugin.json
│   ├── public/
│   │   ├── css/
│   │   │   ├── agents.css
│   │   │   ├── chat.css
│   │   │   ├── cron.css
│   │   │   ├── explorer.css
│   │   │   ├── shell.css
│   │   │   ├── tailwind.input.css
│   │   │   └── theme.css
│   │   ├── js/
│   │   │   ├── app.js
│   │   │   ├── components/
│   │   │   │   ├── action-button.js
│   │   │   │   ├── add-channel-menu.js
│   │   │   │   ├── agent-send-modal.js
│   │   │   │   ├── agents-tab/
│   │   │   │   │   ├── agent-bindings-section/
│   │   │   │   │   │   ├── channel-item-trailing.js
│   │   │   │   │   │   ├── helpers.js
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── use-agent-bindings.js
│   │   │   │   │   │   └── use-channel-items.js
│   │   │   │   │   ├── agent-detail-panel.js
│   │   │   │   │   ├── agent-identity-section.js
│   │   │   │   │   ├── agent-overview/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── manage-card.js
│   │   │   │   │   │   ├── model-card.js
│   │   │   │   │   │   ├── tools-card.js
│   │   │   │   │   │   ├── use-model-card.js
│   │   │   │   │   │   ├── use-workspace-card.js
│   │   │   │   │   │   └── workspace-card.js
│   │   │   │   │   ├── agent-pairing-section.js
│   │   │   │   │   ├── agent-tools/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── tool-catalog.js
│   │   │   │   │   │   └── use-agent-tools.js
│   │   │   │   │   ├── create-agent-modal.js
│   │   │   │   │   ├── create-channel-modal.js
│   │   │   │   │   ├── delete-agent-dialog.js
│   │   │   │   │   ├── edit-agent-modal.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── use-agents.js
│   │   │   │   ├── badge.js
│   │   │   │   ├── channel-account-status-badge.js
│   │   │   │   ├── channel-login-modal.js
│   │   │   │   ├── channel-operations-panel.js
│   │   │   │   ├── channels.js
│   │   │   │   ├── confirm-dialog.js
│   │   │   │   ├── credentials-modal.js
│   │   │   │   ├── cron-tab/
│   │   │   │   │   ├── cron-calendar-helpers.js
│   │   │   │   │   ├── cron-calendar.js
│   │   │   │   │   ├── cron-helpers.js
│   │   │   │   │   ├── cron-insights-panel.js
│   │   │   │   │   ├── cron-job-detail.js
│   │   │   │   │   ├── cron-job-list.js
│   │   │   │   │   ├── cron-job-settings-card.js
│   │   │   │   │   ├── cron-job-trends-panel.js
│   │   │   │   │   ├── cron-job-usage.js
│   │   │   │   │   ├── cron-overview.js
│   │   │   │   │   ├── cron-prompt-editor.js
│   │   │   │   │   ├── cron-run-history-panel.js
│   │   │   │   │   ├── cron-runs-trend-card.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── use-cron-tab.js
│   │   │   │   ├── device-pairings.js
│   │   │   │   ├── doctor/
│   │   │   │   │   ├── findings-list.js
│   │   │   │   │   ├── fix-card-modal.js
│   │   │   │   │   ├── general-warning.js
│   │   │   │   │   ├── helpers.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── summary-cards.js
│   │   │   │   ├── envars.js
│   │   │   │   ├── features.js
│   │   │   │   ├── file-tree.js
│   │   │   │   ├── file-viewer/
│   │   │   │   │   ├── constants.js
│   │   │   │   │   ├── diff-viewer.js
│   │   │   │   │   ├── editor-surface.js
│   │   │   │   │   ├── frontmatter-panel.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── markdown-split-view.js
│   │   │   │   │   ├── media-preview.js
│   │   │   │   │   ├── scroll-sync.js
│   │   │   │   │   ├── sqlite-viewer.js
│   │   │   │   │   ├── status-banners.js
│   │   │   │   │   ├── storage.js
│   │   │   │   │   ├── toolbar.js
│   │   │   │   │   ├── use-editor-line-number-sync.js
│   │   │   │   │   ├── use-editor-selection-restore.js
│   │   │   │   │   ├── use-file-diff.js
│   │   │   │   │   ├── use-file-loader.js
│   │   │   │   │   ├── use-file-viewer-draft-sync.js
│   │   │   │   │   ├── use-file-viewer-hotkeys.js
│   │   │   │   │   ├── use-file-viewer.js
│   │   │   │   │   └── utils.js
│   │   │   │   ├── gateway.js
│   │   │   │   ├── general/
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── use-general-tab.js
│   │   │   │   ├── global-restart-banner.js
│   │   │   │   ├── google/
│   │   │   │   │   ├── account-row.js
│   │   │   │   │   ├── add-account-modal.js
│   │   │   │   │   ├── gmail-setup-wizard.js
│   │   │   │   │   ├── gmail-watch-toggle.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── use-gmail-watch.js
│   │   │   │   │   └── use-google-accounts.js
│   │   │   │   ├── icons.js
│   │   │   │   ├── info-tooltip.js
│   │   │   │   ├── loading-spinner.js
│   │   │   │   ├── modal-shell.js
│   │   │   │   ├── models-tab/
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── model-picker.js
│   │   │   │   │   ├── provider-auth-card.js
│   │   │   │   │   └── use-models.js
│   │   │   │   ├── models.js
│   │   │   │   ├── nodes-tab/
│   │   │   │   │   ├── browser-attach/
│   │   │   │   │   │   └── index.js
│   │   │   │   │   ├── connected-nodes/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── use-connected-nodes-card.js
│   │   │   │   │   │   └── user-connected-nodes.js
│   │   │   │   │   ├── exec-allowlist/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-exec-allowlist.js
│   │   │   │   │   ├── exec-config/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-exec-config.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── setup-wizard/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-setup-wizard.js
│   │   │   │   │   └── use-nodes-tab.js
│   │   │   │   ├── onboarding/
│   │   │   │   │   ├── pairing-utils.js
│   │   │   │   │   ├── use-welcome-codex.js
│   │   │   │   │   ├── use-welcome-pairing.js
│   │   │   │   │   ├── use-welcome-storage.js
│   │   │   │   │   ├── welcome-config.js
│   │   │   │   │   ├── welcome-form-step.js
│   │   │   │   │   ├── welcome-header.js
│   │   │   │   │   ├── welcome-import-step.js
│   │   │   │   │   ├── welcome-pairing-step.js
│   │   │   │   │   ├── welcome-placeholder-review-step.js
│   │   │   │   │   ├── welcome-pre-step.js
│   │   │   │   │   ├── welcome-secret-review-step.js
│   │   │   │   │   ├── welcome-secret-review-utils.js
│   │   │   │   │   └── welcome-setup-step.js
│   │   │   │   ├── overflow-menu.js
│   │   │   │   ├── page-header.js
│   │   │   │   ├── pairings.js
│   │   │   │   ├── pane-shell.js
│   │   │   │   ├── pill-tabs.js
│   │   │   │   ├── pop-actions.js
│   │   │   │   ├── providers.js
│   │   │   │   ├── routes/
│   │   │   │   │   ├── agents-route.js
│   │   │   │   │   ├── browse-route.js
│   │   │   │   │   ├── chat-route.js
│   │   │   │   │   ├── cron-route.js
│   │   │   │   │   ├── doctor-route.js
│   │   │   │   │   ├── envars-route.js
│   │   │   │   │   ├── general-route.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── models-route.js
│   │   │   │   │   ├── nodes-route.js
│   │   │   │   │   ├── providers-route.js
│   │   │   │   │   ├── route-redirect.js
│   │   │   │   │   ├── telegram-route.js
│   │   │   │   │   ├── usage-route.js
│   │   │   │   │   ├── watchdog-route.js
│   │   │   │   │   └── webhooks-route.js
│   │   │   │   ├── scope-picker.js
│   │   │   │   ├── secret-input.js
│   │   │   │   ├── segmented-control.js
│   │   │   │   ├── session-select-field.js
│   │   │   │   ├── sidebar-git-panel.js
│   │   │   │   ├── sidebar.js
│   │   │   │   ├── summary-stat-card.js
│   │   │   │   ├── telegram-workspace/
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── manage.js
│   │   │   │   │   └── onboarding.js
│   │   │   │   ├── theme-toggle.js
│   │   │   │   ├── toast.js
│   │   │   │   ├── toggle-switch.js
│   │   │   │   ├── tooltip.js
│   │   │   │   ├── update-action-button.js
│   │   │   │   ├── update-modal.js
│   │   │   │   ├── usage-tab/
│   │   │   │   │   ├── constants.js
│   │   │   │   │   ├── formatters.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── overview-section.js
│   │   │   │   │   ├── sessions-section.js
│   │   │   │   │   └── use-usage-tab.js
│   │   │   │   ├── watchdog-tab/
│   │   │   │   │   ├── console/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-console.js
│   │   │   │   │   ├── helpers.js
│   │   │   │   │   ├── incidents/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-incidents.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── resource-bar.js
│   │   │   │   │   ├── resources/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-resources.js
│   │   │   │   │   ├── settings/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-settings.js
│   │   │   │   │   ├── terminal/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-terminal.js
│   │   │   │   │   └── use-watchdog-tab.js
│   │   │   │   ├── webhooks/
│   │   │   │   │   ├── create-webhook-modal/
│   │   │   │   │   │   └── index.js
│   │   │   │   │   ├── helpers.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── request-history/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-request-history.js
│   │   │   │   │   ├── webhook-detail/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-webhook-detail.js
│   │   │   │   │   └── webhook-list/
│   │   │   │   │       ├── index.js
│   │   │   │   │       └── use-webhook-list.js
│   │   │   │   └── welcome/
│   │   │   │       ├── index.js
│   │   │   │       └── use-welcome.js
│   │   │   ├── hooks/
│   │   │   │   ├── use-app-shell-controller.js
│   │   │   │   ├── use-app-shell-ui.js
│   │   │   │   ├── use-browse-navigation.js
│   │   │   │   ├── use-cached-fetch.js
│   │   │   │   ├── use-destination-session-selection.js
│   │   │   │   ├── use-hash-location.js
│   │   │   │   ├── useAgentSessions.js
│   │   │   │   └── usePolling.js
│   │   │   ├── lib/
│   │   │   │   ├── agent-identity.js
│   │   │   │   ├── api-cache.js
│   │   │   │   ├── api.js
│   │   │   │   ├── app-navigation.js
│   │   │   │   ├── browse-draft-state.js
│   │   │   │   ├── browse-file-policies.js
│   │   │   │   ├── browse-restart-policy.js
│   │   │   │   ├── browse-route.js
│   │   │   │   ├── channel-accounts.js
│   │   │   │   ├── channel-create-operation.js
│   │   │   │   ├── channel-provider-availability.js
│   │   │   │   ├── clipboard.js
│   │   │   │   ├── codex-oauth-window.js
│   │   │   │   ├── file-highlighting.js
│   │   │   │   ├── file-tree-utils.js
│   │   │   │   ├── format.js
│   │   │   │   ├── model-catalog.js
│   │   │   │   ├── model-config.js
│   │   │   │   ├── session-keys.js
│   │   │   │   ├── sse.js
│   │   │   │   ├── storage-keys.js
│   │   │   │   ├── syntax-highlighters/
│   │   │   │   │   ├── css.js
│   │   │   │   │   ├── frontmatter.js
│   │   │   │   │   ├── html.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── javascript.js
│   │   │   │   │   ├── json.js
│   │   │   │   │   ├── markdown.js
│   │   │   │   │   └── utils.js
│   │   │   │   ├── telegram-api.js
│   │   │   │   └── ui-settings.js
│   │   │   └── tailwind-config.js
│   │   ├── login.html
│   │   ├── setup.html
│   │   └── shared/
│   │       └── browse-file-policies.json
│   ├── scripts/
│   │   ├── git
│   │   ├── git-askpass
│   │   └── systemctl
│   ├── server/
│   │   ├── agents/
│   │   │   ├── agents.js
│   │   │   ├── bindings.js
│   │   │   ├── channels.js
│   │   │   ├── service.js
│   │   │   └── shared.js
│   │   ├── alphaclaw-version.js
│   │   ├── auth-profiles.js
│   │   ├── chat-ws.js
│   │   ├── commands.js
│   │   ├── constants.js
│   │   ├── cost-utils.js
│   │   ├── cron-service.js
│   │   ├── db/
│   │   │   ├── doctor/
│   │   │   │   ├── index.js
│   │   │   │   └── schema.js
│   │   │   ├── usage/
│   │   │   │   ├── index.js
│   │   │   │   ├── pricing.js
│   │   │   │   ├── schema.js
│   │   │   │   ├── sessions.js
│   │   │   │   ├── shared.js
│   │   │   │   ├── summary.js
│   │   │   │   └── timeseries.js
│   │   │   ├── watchdog/
│   │   │   │   ├── index.js
│   │   │   │   └── schema.js
│   │   │   └── webhooks/
│   │   │       ├── index.js
│   │   │       └── schema.js
│   │   ├── discord-api.js
│   │   ├── doctor/
│   │   │   ├── bootstrap-context.js
│   │   │   ├── constants.js
│   │   │   ├── normalize.js
│   │   │   ├── prompt.js
│   │   │   ├── service.js
│   │   │   └── workspace-fingerprint.js
│   │   ├── env.js
│   │   ├── exec-defaults-config.js
│   │   ├── gateway.js
│   │   ├── gmail-push.js
│   │   ├── gmail-serve.js
│   │   ├── gmail-watch.js
│   │   ├── gog-skill.js
│   │   ├── google-state.js
│   │   ├── helpers.js
│   │   ├── init/
│   │   │   ├── register-server-routes.js
│   │   │   ├── runtime-init.js
│   │   │   └── server-lifecycle.js
│   │   ├── internal-files-migration.js
│   │   ├── log-writer.js
│   │   ├── login-throttle.js
│   │   ├── model-catalog-bootstrap.json
│   │   ├── model-catalog-cache.js
│   │   ├── oauth-callback-middleware.js
│   │   ├── onboarding/
│   │   │   ├── cron.js
│   │   │   ├── github.js
│   │   │   ├── import/
│   │   │   │   ├── import-applier.js
│   │   │   │   ├── import-config.js
│   │   │   │   ├── import-scanner.js
│   │   │   │   ├── import-temp.js
│   │   │   │   └── secret-detector.js
│   │   │   ├── index.js
│   │   │   ├── openclaw.js
│   │   │   ├── validation.js
│   │   │   └── workspace.js
│   │   ├── openclaw-config.js
│   │   ├── openclaw-runtime-env.js
│   │   ├── openclaw-version.js
│   │   ├── operation-events.js
│   │   ├── restart-required-state.js
│   │   ├── routes/
│   │   │   ├── agents.js
│   │   │   ├── auth.js
│   │   │   ├── browse/
│   │   │   │   ├── constants.js
│   │   │   │   ├── file-helpers.js
│   │   │   │   ├── git.js
│   │   │   │   ├── index.js
│   │   │   │   ├── path-utils.js
│   │   │   │   └── sqlite.js
│   │   │   ├── codex.js
│   │   │   ├── cron.js
│   │   │   ├── doctor.js
│   │   │   ├── gmail.js
│   │   │   ├── google.js
│   │   │   ├── models.js
│   │   │   ├── nodes.js
│   │   │   ├── onboarding.js
│   │   │   ├── pages.js
│   │   │   ├── pairings.js
│   │   │   ├── proxy.js
│   │   │   ├── system.js
│   │   │   ├── telegram.js
│   │   │   ├── usage.js
│   │   │   ├── watchdog.js
│   │   │   └── webhooks.js
│   │   ├── slack-api.js
│   │   ├── startup.js
│   │   ├── system-resources.js
│   │   ├── telegram-api.js
│   │   ├── telegram-workspace.js
│   │   ├── topic-registry.js
│   │   ├── usage-tracker-config.js
│   │   ├── utils/
│   │   │   ├── boolean.js
│   │   │   ├── channels.js
│   │   │   ├── command-output.js
│   │   │   ├── json.js
│   │   │   ├── network.js
│   │   │   ├── number.js
│   │   │   └── shell.js
│   │   ├── watchdog-notify.js
│   │   ├── watchdog-terminal-ws.js
│   │   ├── watchdog-terminal.js
│   │   ├── watchdog.js
│   │   ├── webhook-middleware.js
│   │   └── webhooks.js
│   ├── server.js
│   └── setup/
│       ├── core-prompts/
│       │   ├── AGENTS.md
│       │   └── TOOLS.md
│       ├── env.template
│       ├── gitignore
│       ├── hourly-git-sync.sh
│       └── skills/
│           └── gog-cli/
│               ├── calendar.md
│               ├── contacts.md
│               ├── docs.md
│               ├── drive.md
│               ├── gmail.md
│               ├── meet.md
│               ├── sheets.md
│               └── tasks.md
├── package.json
├── scripts/
│   ├── build-ui.mjs
│   └── dev/
│       └── crash-watchdog-config.sh
├── tailwind.config.cjs
├── tests/
│   ├── bin/
│   │   ├── alphaclaw.test.js
│   │   └── openclaw-config-restore.test.js
│   ├── frontend/
│   │   ├── agent-identity.test.js
│   │   ├── api.test.js
│   │   ├── browse-draft-state.test.js
│   │   ├── channel-create-operation.test.js
│   │   ├── channel-provider-availability.test.js
│   │   ├── clipboard.test.js
│   │   ├── codex-oauth-window.test.js
│   │   ├── cron-calendar-helpers.test.js
│   │   ├── cron-helpers.test.js
│   │   ├── doctor-helpers.test.js
│   │   ├── file-tree-utils.test.js
│   │   ├── file-viewer-utils.test.js
│   │   ├── model-catalog.test.js
│   │   ├── model-config.test.js
│   │   ├── pairing-utils.test.js
│   │   ├── session-keys.test.js
│   │   ├── syntax-highlighters.test.js
│   │   ├── watchdog-helpers.test.js
│   │   ├── welcome-config.test.js
│   │   └── welcome-secret-review-utils.test.js
│   └── server/
│       ├── agents-service.test.js
│       ├── alphaclaw-version.test.js
│       ├── auth-profiles.test.js
│       ├── chat-ws.test.js
│       ├── commands.test.js
│       ├── cost-utils.test.js
│       ├── cron-service.test.js
│       ├── doctor-db.test.js
│       ├── doctor-normalize.test.js
│       ├── doctor-prompt.test.js
│       ├── doctor-service.test.js
│       ├── exec-defaults-config.test.js
│       ├── express-runtime-guard.test.js
│       ├── gateway.test.js
│       ├── git-runtime.test.js
│       ├── git-shim.test.js
│       ├── git-sync-path.test.js
│       ├── gmail-push.test.js
│       ├── gmail-watch.test.js
│       ├── gog-skill.test.js
│       ├── helpers.test.js
│       ├── import-applier.test.js
│       ├── import-scanner.test.js
│       ├── import-temp.test.js
│       ├── internal-files-migration.test.js
│       ├── login-throttle.test.js
│       ├── model-catalog-cache.test.js
│       ├── oauth-callback-middleware.test.js
│       ├── onboarding-github.test.js
│       ├── onboarding-openclaw.test.js
│       ├── onboarding-validation.test.js
│       ├── onboarding-workspace.test.js
│       ├── openclaw-runtime-env.test.js
│       ├── openclaw-version.test.js
│       ├── operation-events.test.js
│       ├── routes-agents.test.js
│       ├── routes-auth.test.js
│       ├── routes-browse.test.js
│       ├── routes-cron.test.js
│       ├── routes-doctor.test.js
│       ├── routes-models.test.js
│       ├── routes-nodes.test.js
│       ├── routes-onboarding.test.js
│       ├── routes-pairings.test.js
│       ├── routes-system.test.js
│       ├── routes-telegram.test.js
│       ├── routes-usage.test.js
│       ├── routes-watchdog-test-notification.test.js
│       ├── routes-watchdog.test.js
│       ├── routes-webhooks.test.js
│       ├── secret-detector.test.js
│       ├── slack-api.test.js
│       ├── startup.test.js
│       ├── telegram-workspace.test.js
│       ├── topic-registry.test.js
│       ├── usage-db.test.js
│       ├── usage-tracker-config.test.js
│       ├── utils-boolean.test.js
│       ├── utils-json.test.js
│       ├── utils-shell.test.js
│       ├── watchdog-db.test.js
│       ├── watchdog-notify.test.js
│       ├── watchdog.test.js
│       ├── webhook-middleware.test.js
│       ├── webhooks-db.test.js
│       └── webhooks.test.js
└── vitest.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/FUNDING.yml
================================================
github: chrysb


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [22]

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm

      - run: npm ci

      - run: npm run build:ui

      - run: npm test


================================================
FILE: .gitignore
================================================
node_modules/
.DS_Store
.env
.cursor/
coverage/
workspace/.openclaw/

# Build artifacts (generated by npm run build:ui)
lib/public/dist/
lib/public/css/tailwind.generated.css
lib/public/css/vendor/


================================================
FILE: .npmrc
================================================
@chrysb:registry=https://registry.npmjs.org/


================================================
FILE: AGENTS.md
================================================
## Project Overview

### AlphaClaw Project Context

AlphaClaw is the ops and setup layer around OpenClaw. It provides a browser-based setup UI, gateway lifecycle management, watchdog recovery flows, and integrations (for example Telegram, Discord, Google Workspace, and webhooks) so users can operate OpenClaw without manual server intervention.

### Understanding OpenClaw

If you need to understand the internals of OpenClaw, you can inspect the code at `~/Projects/openclaw/src`

### Architecture At A Glance

- `bin/alphaclaw.js`: CLI entrypoint and lifecycle command surface.
- `lib/server`: Express server, authenticated setup APIs, watchdog APIs, channel integrations, and proxying to the OpenClaw gateway.
- `lib/public`: Setup UI frontend (component-driven tabs and flows for providers, envars, watchdog, webhooks, and onboarding).
- `lib/setup`: Prompt hardening templates and setup-related assets injected into agent/system behavior.

Runtime model:

1. AlphaClaw server starts and manages OpenClaw as a child process.
2. Setup UI calls AlphaClaw APIs for configuration and operations.
3. AlphaClaw proxies gateway traffic and handles watchdog monitoring/repair.

### Key Technologies

- Node.js 22.14+ runtime.
- Express-based HTTP API server.
- `http-proxy` for gateway proxy behavior.
- OpenClaw CLI/gateway process orchestration.
- Preact + `htm` frontend patterns for Setup UI components.
- Vitest + Supertest for server and route testing.

## Coding Conventions

### Change Patterns

- Keep edits targeted and production-safe; favor small, reviewable changes.
- Preserve existing behavior unless the task explicitly requires behavior changes.
- Follow existing UI conventions and shared components for consistency.
- Reuse existing server route and state patterns before introducing new abstractions.
- Update tests when behavior changes in routes, watchdog flows, or setup state.
- Before running tests in a fresh checkout, run `npm install` so `vitest` (devDependency) is available for `npm test`.

### Code Structure

- Avoid monolithic implementation files for new features. For new UI areas and new API areas, start with a decomposed structure (focused components/hooks/utilities for UI; focused route modules/services/helpers for server) rather than building one large file first and splitting later.
- When adding a new feature area, follow the existing project patterns from day one (for example feature folders with `index.js` plus `use-*` hooks in UI, and route + service separation on server) so code stays maintainable as the feature grows.
- When continuing to build on a file that is growing large or accumulating unrelated concerns, stop and decompose it before adding more code rather than letting it drift into a monolith.

### Networking and Fetching

- Prefer the shared cache primitives in `lib/public/js/lib/api-cache.js` for backend reads:
  - `cachedFetch(...)` for imperative fetch paths.
  - `getCached(...)` / `setCached(...)` / `invalidateCache(...)` for cache lifecycle.
- For component-level read requests, prefer `useCachedFetch` from `lib/public/js/hooks/use-cached-fetch.js` over ad-hoc `useEffect(() => fetchX())` mount loads.
- Treat the API URL (including query params) as the canonical cache key for GET-style payloads.
- Keep cache in-memory for fast tab switches; do not add persistent storage caching unless explicitly required by product behavior.
- Do not keep route panes mounted via `display:none` just to preserve data. Prefer conditional rendering + cache-backed remounts.
- Use `usePolling` for recurring refreshes and always pass a stable `cacheKey` when poll results should hydrate remounts.
- Keep `pauseWhenHidden` behavior enabled for polling unless a specific flow requires background polling while the browser tab is hidden.
- Tune polling intervals conservatively; avoid 1-2s polling unless there is a clear real-time requirement.
- For app-shell status streams, prefer SSE (`/api/events/status`) where available and keep polling as fallback behavior.
- After write/mutation APIs (POST/PUT/DELETE), refresh or invalidate relevant cached keys so the UI does not show stale data.

### OpenClaw Config Access

- When reading `openclaw.json` in server code, use the shared helper in `lib/server/openclaw-config.js` (`readOpenclawConfig`) instead of ad-hoc `JSON.parse(fs.readFileSync(...))` blocks.

### Where To Put Agent Guidance

- **This file (`AGENTS.md`):** Project-level guidance for coding agents working on the AlphaClaw codebase — architecture, conventions, release flow, UI patterns, etc.
- **`lib/setup/core-prompts/AGENTS.md`:** Runtime prompt injected into the OpenClaw agent's system prompt. Only write there when the guidance is meant for the deployed agent's behavior, not for coding on this project.

## Operations

### Release Flow (Beta -> Production)

Use this release flow when promoting tested beta builds to production:

1. Ensure `main` is clean and synced, and tests pass.
2. Publish beta iterations as needed:
   - `npm version prerelease --preid=beta`
   - `git push && git push --tags`
   - `npm publish --tag beta`
3. Immediately after each beta publish, update `~/Projects/openclaw-railway-template` on the `beta` branch to pin the exact beta version in `package.json` (for example `0.3.2-beta.4`), then commit and push that template change. Do not leave the beta template on `latest`, or Docker layer cache can reuse an older install.
4. When ready for production, publish a stable release version (for example `0.3.2`):
   - `npm version 0.3.2`
   - `git push && git push --tags`
   - `npm publish` (publishes to `latest`)
   - Pin all deployment templates on `main` to that release: set `@chrysb/alphaclaw` in `~/Projects/openclaw-railway-template`, `~/Projects/openclaw-render-template`, and `~/Projects/openclaw-apex-template` to the released version (templates rely on AlphaClaw’s declared `openclaw` dependency — do not add `package.json` `overrides` for `openclaw` unless you have a one-off debug reason). Run `npm install` in each repo, confirm `npm ls openclaw` matches AlphaClaw’s `package.json` pin, commit `package.json` and `package-lock.json`, and push. Skipping a template leaves it stale relative to the others.
5. Return templates to production channel:
   - `@chrysb/alphaclaw: "latest"`
6. Optionally keep beta branch/tag flows active for next release cycle.

### Runtime Dependency Guardrails (Express 4 vs 5)

AlphaClaw currently expects Express 4 semantics in its setup API layer. A broken container dependency tree can accidentally resolve `express@5` at `/app/node_modules/express`, which causes subtle request handling regressions (for example body parsing behavior on certain methods).

Known root cause pattern:

- Mutating `/app/node_modules` in-place (for example copy-over installs used for emergency package swaps) can leave the runtime tree inconsistent with `/app/package.json`.
- This can hoist `express@5` to the app root, so `require("express")` inside AlphaClaw resolves the wrong major version.

Preferred fix/recovery:

1. Ensure template `package.json` pins the intended `@chrysb/alphaclaw` version.
2. Rebuild the `openclaw` container from scratch (no cache) and recreate it:
   - `docker compose down`
   - `docker compose build --no-cache openclaw`
   - `docker compose up -d openclaw`
3. Verify runtime resolution inside the container:
   - `node -p "require('express/package.json').version"` should be `4.x`
   - `npm ls express` should show `@chrysb/alphaclaw` on `express@4.x` (OpenClaw can still carry its own `express@5` subtree).

### Telegram Notice Format (AlphaClaw)

Use this format for any Telegram notices sent from AlphaClaw services (watchdog, system alerts, repair notices):

1. Header line (Markdown): `🐺 *AlphaClaw Watchdog*`
2. Headline line (simple, no `Status:` prefix):
   - `🔴 Crash loop detected`
   - `🔴 Crash loop detected, auto-repairing...`
   - `🟡 Auto-repair started, awaiting health check`
   - `🟢 Auto-repair complete, gateway healthy`
   - `🟢 Gateway healthy again`
   - `🔴 Auto-repair failed`
3. Append a markdown link to the headline when URL is available:
   - `... - [View logs](<full-url>/#/watchdog)`
4. Optional context lines like `Trigger: ...`, `Attempt count: ...`
5. For values with underscores or special characters (for example `crash_loop`), wrap the value in backticks:
   - `Trigger: \`crash_loop\``
6. Do not use HTML tags (`<b>`, `<a href>`) for Telegram watchdog notices.

## UI Conventions

Use these conventions for all UI work under `lib/public/js` and `lib/public/css`.

### Setup UI bundle (esbuild)

- The browser loads the compiled bundle under `lib/public/dist/` (for example `app.bundle.js` and chunk files), produced by `scripts/build-ui.mjs` (esbuild).
- **After any UI source change** that should ship in production (`lib/public/js`, `lib/public/css`, or other inputs to the build), run **`npm run build:ui`** so `lib/public/dist/` stays in sync. Verify the app in the browser against the rebuilt bundle when the change is non-trivial.
- **`npm publish`** runs **`prepack`** → **`npm run build:ui`**, so published packages always include a fresh bundle. Local installs, Docker builds from a git checkout, or commits that include `dist/` still require **`npm run build:ui`** when you change UI sources and expect the built assets to match.

### Component structure

- Use arrow-function components and helpers.
- Prefer shared components over one-off markup when a pattern already exists.
- Keep constants in `kName` format (e.g. `kUiTabs`, `kGroupOrder`, `kNamePattern`).
- Keep component-level helpers near the top of the file, before the main export.
- Treat `index.js` as a presentational shell whenever possible: keep business logic in hooks and pass derived state/actions down as props.
- Add reusable SVG icons to `lib/public/js/components/icons.js` and import them from there; avoid introducing one-off inline SVGs in feature files when a shared icon component can be used.

### Rendering and composition

- Use the `htm` + `preact` pattern:
  - `const html = htm.bind(h);`
  - return `html\`...\``
- In `htm` templates, be explicit with inline spacing around styled inline tags (`<span>`, `<code>`, `<a>`): use ` ${" "}` where needed, and verify rendered copy so words never collapse (`eventsand`) or gain double spaces.
- Prefer early return for hidden states (e.g. `if (!visible) return null;`).
- Use `<PageHeader />` for tab/page headers that need a title and right-side actions.
- Use card shells consistently: `bg-surface border border-border rounded-xl`.
- For nested "surface on surface" blocks (content inside a `bg-surface` card), use `ac-surface-inset` for the inner container treatment so inset sections match shared history/sessions styling.
- For internal section dividers, use `border-t border-border` (avoid opacity variants) with comfortable vertical spacing around the divider.

### Color and theme tokens

- Prefer semantic Tailwind color utilities backed by theme tokens (`text-body`, `text-fg-muted`, `text-fg-dim`, `bg-field`, `bg-status-error-bg`, `border-status-warning-border`) instead of raw palette classes like `text-gray-300` or `bg-red-900/30`.
- When a new reusable UI color role is needed, add the CSS variable in `lib/public/css/theme.css` and expose it through `tailwind.config.cjs` rather than introducing one-off hardcoded color classes in components.
- Keep component refactors token-based so future theme changes stay centralized in the token layer instead of requiring per-component color rewrites.

### Buttons

- Primary actions: `ac-btn-cyan`
- Secondary actions: `ac-btn-secondary`
- Positive/success actions: `ac-btn-green`
- Ghost/text actions: `ac-btn-ghost` (use for low-emphasis actions like "Disconnect" or "Add provider")
- Destructive inline actions: `ac-btn-danger`
- Use consistent disabled treatment: `opacity-50 cursor-not-allowed`.
- Keep action sizing consistent (`text-xs px-3 py-1.5 rounded-lg` for compact controls unless there is a clear reason otherwise).
- For `<PageHeader />` actions, use `ac-btn-cyan` (primary) or `ac-btn-secondary` (secondary) by default; avoid ghost/text-only styling for main header actions.
- Prefer shared action components when available (`ActionButton`, `UpdateActionButton`, `ConfirmDialog`) before custom button logic.
- In setup/onboarding auth flows (e.g. Codex OAuth), prefer `<ActionButton />` over raw `<button>` for consistency in tone, sizing, and loading behavior.
- In setup wizard/multi-step modal footers, use `<ActionButton />` for Back/Next/Finish/Done actions (not raw `<button>`), so loading and tone behavior stays consistent.
- In multi-step auth flows, keep the active "finish" action visually primary and demote the "start/restart" action to secondary once the flow has started.

### Dialogs and modals

- Use `<ConfirmDialog />` for destructive/confirmation flows.
- Use `<ModalShell />` for non-confirm custom modals that need shared overlay and Escape handling.
- Modal overlay convention:
  - `fixed inset-0 bg-black/70 flex items-center justify-center p-4 z-50`
- Modal panel convention:
  - `bg-modal border border-border rounded-xl p-5 ...`
- Support close-on-overlay click and Escape key for dialogs.

### Inputs and forms

- Reuse `<SecretInput />` for sensitive values and token/key inputs.
- Reuse `<ToggleSwitch />` for boolean on/off controls instead of ad-hoc checkbox/switch markup.
- Base input look should remain consistent:
  - `bg-field border border-border rounded-lg ... focus:border-fg-muted`
- Preserve monospace for technical values (`font-mono`) and codes/paths.
- Prefer inline helper text under fields (`text-xs text-fg-muted` / `text-fg-dim`) for setup guidance.
- For tip/help links in helper text, use the shared `ac-tip-link` class (token-backed via `--accent-link`) instead of per-file ad-hoc cyan classes.

### Feedback and state

- Use `showToast(...)` for user-visible operation outcomes.
- Prefer semantic toast levels (`success`, `error`, `warning`, `info`) at callsites. Legacy color aliases are only for backwards compatibility.
- Keep toast positioning relative to the active page container (not the viewport) when layout banners can shift content.
- For hover help and icon labels, use the shared portal-backed tooltip components (`Tooltip`, `InfoTooltip`) instead of inline absolutely positioned popovers, so tooltips are not clipped by cards, rows, or scroll containers.
- Keep loading/saving flags explicit in state (`saving`, `creating`, `restartingGateway`, etc.).
- Reuse `<LoadingSpinner />` for loading indicators instead of inline spinner SVG markup.
- Use `<Badge />` for compact status chips (e.g. connected/not connected) instead of one-off status span styling.
- Use polling via `usePolling` for frequently refreshed backend-backed data.
- For restart-required flows, render the standardized yellow restart banner style used in `providers`, `envars`, and `webhooks`.

### Shared formatting utilities

- Prefer shared formatter helpers in `lib/public/js/lib/format.js` for reusable value formatting (`formatX` style helpers such as date/time, currency, integers, and common duration formats).
- Before adding a new formatter in a component, check `lib/public/js/lib/format.js` and reuse an existing helper when possible.
- Add new formatter helpers to `lib/public/js/lib/format.js` when the behavior is cross-feature and likely to be reused; keep feature-specific transforms local to the feature folder.
- Avoid wrapper pass-through helpers that only rename a global formatter without adding feature-specific behavior.

### Session key utilities

- Keep shared session-key parsing/filtering helpers in `lib/public/js/lib/session-keys.js` (for example extracting `agentId`, destination-session matching checks, and destination payload derivation).
- Before adding session-key logic in a hook/component, check `lib/public/js/lib/session-keys.js` first and reuse existing helpers.
- When session-key behavior is reused across features, add/extend helpers in `lib/public/js/lib/session-keys.js` instead of duplicating regex/string parsing in feature files.

### localStorage keys

- All standalone `localStorage` keys are defined in `lib/public/js/lib/storage-keys.js`. Import keys from this file — never define raw localStorage key strings inline in components.
- Use the naming convention `alphaclaw.<area>.<purpose>` for new keys (e.g. `alphaclaw.doctor.lastSessionKey`).
- Keys that live inside the `alphaclaw.ui.settings` JSON blob (e.g. `browseLastPath`, `doctorWarningDismissedUntilMs`) are sub-keys, not standalone localStorage entries — those stay in their consuming file.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to AlphaClaw

Thanks for your interest in contributing to AlphaClaw. This document covers how we work, what we value, and how to get your changes merged.

## Vision

AlphaClaw makes OpenClaw accessible: easy to deploy, easy to monitor, easy to repair, easy to keep running. Self-managed and open source, always.

One-click deployment templates come and go. The self-managed aspect is what makes this durable.

### Guiding Principles

- **UX over features.** Usability matters more than feature count. Every interaction should feel considered.
- **Smart defaults.** AlphaClaw is opinionated. We bootstrap hooks, prompt hardening, and sensible configs so the out-of-box experience is good without manual tuning.
- **Complement, don't replicate.** OpenClaw's Gateway dashboard is exhaustive. We surface the most common workflows and add net-new value, not duplicate switches.
- **Always ejectable.** AlphaClaw is not a dependency. Remove it and your OpenClaw instance keeps running. Nothing proprietary, nothing to migrate.
- **Reliability is a feature.** The watchdog, auto-repair, crash-loop recovery - these matter as much as any UI improvement.

## What We're Looking For

### Always welcome

- Bug fixes
- Reliability improvements (watchdog, crash recovery, gateway management)
- Test coverage
- Documentation fixes and clarifications

### Welcome, but reviewed carefully

- UX changes and small features
- New integrations or wizard steps
- Bootstrap prompt improvements

### Proposal first

- Large features or architectural changes
- New paradigms (e.g., plugin system changes, new deployment targets)
- Anything that changes the default experience significantly

For big changes, open an issue describing what you want to build, why, and your proposed approach. This saves everyone time.

## Getting Started

### Prerequisites

- Node.js >= 22.14.0
- Git

### Setup

```bash
git clone https://github.com/chrysb/alphaclaw.git
cd alphaclaw
npm install
```

### Running Tests

```bash
npm test              # Run all tests
npm run test:watch    # Watch mode
npm run test:coverage # With coverage report
```

AlphaClaw uses [Vitest](https://vitest.dev/) for testing.

### Project Structure

- `bin/` - CLI entrypoint (`alphaclaw.js`)
- `lib/` - Core library (gateway manager, watchdog, setup UI, webhooks, etc.)
- `tests/` - Test suites

## Submitting Changes

### Pull Request Process

1. Fork the repo and create a branch from `main`.
2. Make your changes. Write tests if applicable.
3. Run `npm test` and make sure everything passes.
4. Write a clear PR description: what changed, why, and how to test it.
5. Sign off your commits (see DCO below).

### Commit Messages

Keep them clear and concise. Prefix with the area when it helps:

```text
watchdog: recover from port conflict on restart
setup-ui: fix credential validation for Gemini provider
docs: clarify Railway deployment steps
```

### Code Style

- Match the existing style. If something looks inconsistent, follow what the majority of the codebase does.
- No unnecessary dependencies. AlphaClaw ships lean on purpose.

## Developer Certificate of Origin (DCO)

We use the [DCO](https://developercertificate.org/) to certify that contributors have the right to submit their code under this project's MIT license.

Add a sign-off line to each commit:

```text
Signed-off-by: Your Name <your.email@example.com>
```

Git makes this easy:

```bash
git commit -s -m "your commit message"
```

The `-s` flag adds the sign-off automatically using your configured `user.name` and `user.email`.

## Code of Conduct

We follow the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) (v2.1).

The short version: be respectful, be constructive, assume good intent. We're building something useful together.

## Questions?

Open an issue or start a discussion on the repo. We're happy to help you find the right place to contribute.

## License

By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 chrysb

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<p align="center">
  <img width="771" height="339" alt="image" src="https://github.com/user-attachments/assets/b96b45ab-52f2-4010-bfbe-c640e66b0f36" />
</p>
<h1 align="center">AlphaClaw</h1>
<p align="center">
  <strong>The ultimate OpenClaw harness. Deploy in minutes. Stay running for months.</strong><br>
  <strong>Observability. Reliability. Agent discipline. Zero SSH rescue missions.</strong>
</p>

<p align="center">
  <a href="https://github.com/chrysb/alphaclaw/actions/workflows/ci.yml"><img src="https://github.com/chrysb/alphaclaw/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
  <a href="https://www.npmjs.com/package/@chrysb/alphaclaw"><img src="https://img.shields.io/npm/v/@chrysb/alphaclaw" alt="npm version" /></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT" /></a>
</p>

<p align="center">AlphaClaw wraps <a href="https://github.com/openclaw/openclaw">OpenClaw</a> with a convenient setup wizard, self-healing watchdog, Git-backed rollback, and full browser-based observability. Ships with anti-drift prompt hardening to keep your agent disciplined, and simplifies integrations (e.g. Google Workspace, Google Pub/Sub, Telegram Topics, Slack, Discord) so you can manage multiple agents from one UI instead of config files.</p>

<p align="center"><em>First deploy to first message in under five minutes.</em></p>

<p align="center">
  <a href="https://railway.com/deploy/openclaw-fast-start?referralCode=jcFhp_&utm_medium=integration&utm_source=template&utm_campaign=generic"><img height="40" src="https://railway.com/button.svg" alt="Deploy on Railway" /></a>
  <a href="https://render.com/deploy?repo=https://github.com/chrysb/openclaw-render-template"><img height="40" src="https://render.com/images/deploy-to-render-button.svg" alt="Deploy to Render" /></a>
  <a href="https://updates.alphaclaw.md/desktop/prod/alphaclaw-mac-latest.dmg"><img height="40" src="https://img.shields.io/badge/Download%20for%20macOS-000000?style=for-the-badge&logo=apple&logoColor=white" alt="Download for macOS" /></a>
</p>

> **Platform:** AlphaClaw currently targets Docker/Linux deployments. macOS local development is not yet supported.

## Features

- **Setup UI:** Password-protected web dashboard for onboarding, configuration, and day-to-day management.
- **Guided Onboarding:** Step-by-step setup wizard — model selection, provider credentials, GitHub repo, channel pairing.
- **Multi-Agent Management:** Sidebar-driven agent navigation with create, rename, and delete flows. Per-agent overview cards, channel bindings, and URL-driven agent selection.
- **Gateway Manager:** Spawns, monitors, restarts, and proxies the OpenClaw gateway as a managed child process.
- **Watchdog:** Crash detection, crash-loop recovery, auto-repair (`openclaw doctor --fix`), Telegram/Discord/Slack notifications, and a live interactive terminal for monitoring gateway output directly from the browser.
- **Channel Orchestration:** Telegram, Discord, and Slack bot pairing with per-agent channel bindings, credential sync, and a guided wizard for splitting Telegram into multi-threaded topic groups as your usage grows.
- **Google Workspace:** OAuth integration for Gmail, Calendar, Drive, Docs, Sheets, Tasks, Contacts, and Meet, plus guided Gmail watch setup with Google Pub/Sub topic, subscription, and push endpoint handling.
- **Cron Jobs:** Dedicated cron tab with job management, an interactive rolling calendar, run-history drilldowns, trend analytics, and per-run usage breakdowns.
- **Nodes:** Guided local-node setup for VPS deployments with per-node browser attach checks, reconnect commands, and routing/pairing controls.
- **Webhooks:** Named webhook endpoints with per-hook transform modules, request logging, payload inspection, editable delivery destinations, and OAuth callback support for third-party auth flows.
- **File Explorer:** Browser-based workspace explorer with file visibility, inline edits, diff view, and Git-aware sync for quick fixes without SSH.
- **Prompt Hardening:** Ships anti-drift bootstrap prompts (`AGENTS.md`, `TOOLS.md`) injected into your agent's system prompt on every message — enforcing safe practices, commit discipline, and change summaries out of the box.
- **Git Sync:** Automatic hourly commits of your OpenClaw workspace to GitHub with configurable cron schedule. Combined with prompt hardening, every agent action is version-controlled and auditable.
- **Version Management:** In-place updates for both AlphaClaw and OpenClaw with in-app release notes, changelog review, and one-click apply.
- **Codex OAuth:** Built-in PKCE flow for OpenAI Codex CLI model access.

## Why AlphaClaw

- **Zero to production in one deploy:** Railway/Render templates ship a complete stack — no manual gateway setup.
- **Self-healing:** Watchdog detects crashes, enters repair mode, relaunches the gateway, and notifies you.
- **Everything in the browser:** No SSH, no config files to hand-edit, no CLI required after first deploy.
- **Stays out of the way:** AlphaClaw manages infrastructure; OpenClaw handles the AI.

## No Lock-in. Eject Anytime.

AlphaClaw simply wraps OpenClaw, it's not a dependency. Remove AlphaClaw and your agent keeps running. Nothing proprietary, nothing to migrate.

## Quick Start

### Deploy (recommended)

[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/openclaw-fast-start?referralCode=jcFhp_&utm_medium=integration&utm_source=template&utm_campaign=generic)
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/chrysb/openclaw-render-template)

Set `SETUP_PASSWORD` at deploy time and visit your deployment URL. The welcome wizard handles the rest.

> **Railway users:** after deploying, upgrade to the **Hobby plan** and redeploy to ensure your service has at least **8 GB of RAM**. The Trial plan's memory limit can cause out-of-memory crashes during normal operation.

### Local / Docker

```bash
npm install @chrysb/alphaclaw
npx alphaclaw start
```

Or with Docker:

```dockerfile
FROM node:22-slim
RUN apt-get update && apt-get install -y git curl procps cron tini && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
ENV PATH="/app/node_modules/.bin:$PATH"
ENV ALPHACLAW_ROOT_DIR=/data
EXPOSE 3000
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["alphaclaw", "start"]
```

## Setup UI

| Tab           | What it manages                                                                                                          |
| ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **General**   | Gateway status, channel health, pending pairings, Google Workspace, repo sync schedule, OpenClaw dashboard               |
| **Browse**    | File explorer for workspace visibility, inline edits, diff review, and Git-backed sync                                   |
| **Usage**     | Token summaries, per-session and per-agent cost and token breakdown with source/agent dimension comparisons              |
| **Cron**      | Cron job management, interactive rolling calendar, run-history drilldowns, trend analytics, and per-run usage breakdowns |
| **Nodes**     | Guided local-node setup for VPS deployments, per-node browser attach, reconnect commands, and routing/pairing controls   |
| **Watchdog**  | Health monitoring, crash-loop status, auto-repair toggle, notifications, event log, live log tail, interactive terminal  |
| **Providers** | AI provider credentials (Anthropic, OpenAI, Gemini, Mistral, Voyage, Groq, Deepgram) and model selection                 |
| **Envars**    | Environment variables — view, edit, add — with gateway restart prompts                                                   |
| **Webhooks**  | Webhook endpoints, transform modules, request history, payload inspection, OAuth callbacks, Gmail watch delivery flows   |

## CLI

| Command                                                    | Description                                   |
| ---------------------------------------------------------- | --------------------------------------------- |
| `alphaclaw start`                                          | Start the server (Setup UI + gateway manager) |
| `alphaclaw git-sync -m "message"`                          | Commit and push the OpenClaw workspace        |
| `alphaclaw telegram topic add --thread <id> --name <text>` | Register a Telegram topic mapping             |
| `alphaclaw version`                                        | Print version                                 |
| `alphaclaw help`                                           | Show help                                     |

## Architecture

```mermaid
graph TD
    subgraph AlphaClaw
        UI["Setup UI<br/><small>Preact + htm + Wouter</small>"]
        WD["Watchdog<br/><small>Crash recovery · Notifications</small>"]
        WH["Webhooks<br/><small>Transforms · Request logging</small>"]
        UI --> API
        WD --> API
        WH --> API
        API["Express Server<br/><small>JSON APIs · Auth · Proxy</small>"]
    end

    API -- "proxy" --> GW["OpenClaw Gateway<br/><small>Child process · 127.0.0.1:18789</small>"]
    GW --> DATA["ALPHACLAW_ROOT_DIR<br/><small>.openclaw/ · .env · logs · SQLite</small>"]
```

## Watchdog

The built-in watchdog monitors gateway health and recovers from failures automatically.

| Capability               | Details                                                                |
| ------------------------ | ---------------------------------------------------------------------- |
| **Health checks**        | Periodic `openclaw health` with configurable interval                  |
| **Crash detection**      | Listens for gateway exit events                                        |
| **Crash-loop detection** | Threshold-based (default: 3 crashes in 300s)                           |
| **Auto-repair**          | Runs `openclaw doctor --fix --yes`, relaunches gateway                 |
| **Notifications**        | Telegram, Discord, and Slack alerts for crashes, repairs, and recovery |
| **Event log**            | SQLite-backed incident history with API and UI access                  |

## Environment Variables

| Variable                          | Required | Description                                        |
| --------------------------------- | -------- | -------------------------------------------------- |
| `SETUP_PASSWORD`                  | Yes      | Password for the Setup UI                          |
| `OPENCLAW_GATEWAY_TOKEN`          | Auto     | Gateway auth token (auto-generated if unset)       |
| `GITHUB_TOKEN`                    | Yes      | GitHub PAT for workspace repo                      |
| `GITHUB_WORKSPACE_REPO`           | Yes      | GitHub repo for workspace sync (e.g. `owner/repo`) |
| `TELEGRAM_BOT_TOKEN`              | Optional | Telegram bot token                                 |
| `DISCORD_BOT_TOKEN`               | Optional | Discord bot token                                  |
| `SLACK_BOT_TOKEN`                 | Optional | Slack bot token (Socket Mode)                      |
| `WATCHDOG_AUTO_REPAIR`            | Optional | Enable auto-repair on crash (`true`/`false`)       |
| `WATCHDOG_NOTIFICATIONS_DISABLED` | Optional | Disable watchdog notifications (`true`/`false`)    |
| `PORT`                            | Optional | Server port (default `3000`)                       |
| `ALPHACLAW_ROOT_DIR`              | Optional | Data directory (default `/data`)                   |
| `TRUST_PROXY_HOPS`                | Optional | Trust proxy hop count for correct client IP        |

## Security Notes

AlphaClaw is a convenience wrapper — it intentionally trades some of OpenClaw's default hardening for ease of setup. You should understand what's different:

| Area                    | What AlphaClaw does                                                                                                                   | Trade-off                                                                                              |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Setup password**      | All gateway access is gated behind a single `SETUP_PASSWORD`. Brute-force protection is built in (exponential backoff lockout).       | Simpler than OpenClaw's pairing code flow, but the password must be strong.                            |
| **One-click pairing**   | Channel pairings (Telegram/Discord/Slack) can be approved from the Setup UI instead of the CLI.                                       | No terminal access required, but anyone with the setup password can approve pairings.                  |
| **Auto CLI approval**   | The first CLI device pairing is auto-approved so you can connect without a second screen. Subsequent requests appear in the UI.       | Removes the manual pairing step for the initial CLI connection.                                        |
| **Query-string tokens** | Webhook URLs support `?token=<WEBHOOK_TOKEN>` for providers that don't support `Authorization` headers. Warnings are shown in the UI. | Tokens may appear in server logs and referrer headers. Use header auth when your provider supports it. |
| **Gateway token**       | `OPENCLAW_GATEWAY_TOKEN` is auto-generated and injected into the environment so the proxy can authenticate with the gateway.          | The token lives in the `.env` file on the server — standard for managed deployments but worth noting.  |

If you need OpenClaw's full security posture (manual pairing codes, no query-string tokens, no auto-approval), use OpenClaw directly without AlphaClaw.

## Development

```bash
npm install
npm run build:ui        # Generate Setup UI bundle, Tailwind CSS, and vendor CSS (required for local runs from a git checkout)
npm test                # Full suite (440 tests)
npm run test:watchdog   # Watchdog-focused suite (14 tests)
npm run test:watch      # Watch mode
npm run test:coverage   # Coverage report
```

**Requirements:** Node.js ≥ 22.14.0

## License

MIT


================================================
FILE: bin/alphaclaw.js
================================================
#!/usr/bin/env node
"use strict";

const fs = require("fs");
const os = require("os");
const path = require("path");
const { execSync } = require("child_process");
const {
  normalizeGitSyncFilePath,
  validateGitSyncFilePath,
  resolveRealGitPath,
  shouldRefreshHourlyGitSyncScript,
} = require("../lib/cli/git-runtime");
const {
  ensureMainUpstream,
  restoreMissingOpenclawConfigFromRemote,
} = require("../lib/cli/openclaw-config-restore");
const { buildSecretReplacements } = require("../lib/server/helpers");
const {
  migrateManagedInternalFiles,
} = require("../lib/server/internal-files-migration");

const kUsageTrackerPluginPath = path.resolve(
  __dirname,
  "..",
  "lib",
  "plugin",
  "usage-tracker",
);

// ---------------------------------------------------------------------------
// Parse CLI flags
// ---------------------------------------------------------------------------

const args = process.argv.slice(2);

const flagValue = (argv, ...flags) => {
  for (const flag of flags) {
    const idx = argv.indexOf(flag);
    if (idx !== -1 && idx + 1 < argv.length) {
      return argv[idx + 1];
    }
  }
  return undefined;
};

const kGlobalValueFlags = new Set(["--root-dir", "--port"]);
const splitGlobalAndCommandArgs = (argv) => {
  const globalArgs = [];
  let index = 0;
  while (index < argv.length) {
    const token = argv[index];
    if (!token.startsWith("-")) break;
    globalArgs.push(token);
    if (kGlobalValueFlags.has(token) && index + 1 < argv.length) {
      globalArgs.push(argv[index + 1]);
      index += 2;
      continue;
    }
    index += 1;
  }
  return {
    globalArgs,
    commandArgs: argv.slice(index),
  };
};

const { globalArgs, commandArgs } = splitGlobalAndCommandArgs(args);
const command = commandArgs[0];
const commandScope = commandArgs[1];
const commandAction = commandArgs[2];

const pkg = JSON.parse(
  fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"),
);

if (
  args.includes("--version") ||
  args.includes("-v") ||
  command === "version"
) {
  console.log(pkg.version);
  process.exit(0);
}

if (!command || command === "help" || args.includes("--help")) {
  console.log(`
alphaclaw v${pkg.version}

Usage: alphaclaw <command> [options]

Commands:
  start     Start the AlphaClaw server (Setup UI + gateway manager)
  git-sync  Commit and push /data/.openclaw safely using GITHUB_TOKEN
  telegram topic add  Add/update Telegram topic mapping by thread ID
  version   Print version

Global options:
--version, -v       Print version
--help              Show this help message

start options:
--root-dir <path>   Persistent data directory (default: ~/.alphaclaw)
--port <number>     Server port (default: 3000)

git-sync options:
  --message, -m <text> Commit message
  --file, -f <path>    Optional file path in .openclaw to sync only one file

telegram topic add options:
  --thread <id>       Telegram thread ID
  --name <text>       Topic name
  --system <text>     Optional system instructions
  --agent <id>        Optional agent ID for per-topic routing
  --group <id>        Optional group ID override (auto-resolves when one group exists)

Examples:
  alphaclaw git-sync --message "sync workspace"
  alphaclaw git-sync --message "update config" --file "workspace/app/config.json"
  alphaclaw telegram topic add --thread 12 --name "Testing"
  alphaclaw telegram topic add --thread 12 --name "Testing" --system "Handle QA requests"
  alphaclaw telegram topic add --thread 12 --name "Ops" --agent ops
`);
  process.exit(0);
}

const quoteArg = (value) => `'${String(value || "").replace(/'/g, "'\"'\"'")}'`;
const resolveGithubRepoPath = (value) =>
  String(value || "")
    .trim()
    .replace(/^git@github\.com:/, "")
    .replace(/^https:\/\/github\.com\//, "")
    .replace(/\.git$/, "");

// ---------------------------------------------------------------------------
// 1. Resolve root directory (before requiring any lib/ modules)
// ---------------------------------------------------------------------------

const rootDir =
  flagValue(args, "--root-dir") ||
  process.env.ALPHACLAW_ROOT_DIR ||
  path.join(os.homedir(), ".alphaclaw");

process.env.ALPHACLAW_ROOT_DIR = rootDir;

const portFlag = flagValue(args, "--port");
if (portFlag) {
  process.env.PORT = portFlag;
}

// ---------------------------------------------------------------------------
// 2. Create directory structure
// ---------------------------------------------------------------------------

const openclawDir = path.join(rootDir, ".openclaw");
fs.mkdirSync(openclawDir, { recursive: true });
const { hourlyGitSyncPath } = migrateManagedInternalFiles({
  fs,
  openclawDir,
});
console.log(`[alphaclaw] Root directory: ${rootDir}`);

// Check for pending update marker (written by the update endpoint before restart).
// In environments where the container filesystem is ephemeral (Railway, etc.),
// the npm install from the update endpoint is lost on restart. This re-runs it
// from the fresh container using the persistent volume marker.
const pendingUpdateMarker = path.join(rootDir, ".alphaclaw-update-pending");
if (fs.existsSync(pendingUpdateMarker)) {
  console.log(
    "[alphaclaw] Pending update detected, installing @chrysb/alphaclaw@latest...",
  );
  const alphaPkgRoot = path.resolve(__dirname, "..");
  const nmIndex = alphaPkgRoot.lastIndexOf(
    `${path.sep}node_modules${path.sep}`,
  );
  const installDir =
    nmIndex >= 0 ? alphaPkgRoot.slice(0, nmIndex) : alphaPkgRoot;
  try {
    execSync(
      "npm install @chrysb/alphaclaw@latest --omit=dev --prefer-online",
      {
        cwd: installDir,
        stdio: "inherit",
        timeout: 180000,
      },
    );
    fs.unlinkSync(pendingUpdateMarker);
    console.log("[alphaclaw] Update applied successfully");
  } catch (e) {
    console.log(`[alphaclaw] Update install failed: ${e.message}`);
    fs.unlinkSync(pendingUpdateMarker);
  }
}

// ---------------------------------------------------------------------------
// 3. Symlink ~/.openclaw -> <root>/.openclaw
// ---------------------------------------------------------------------------

const homeOpenclawLink = path.join(os.homedir(), ".openclaw");
try {
  if (!fs.existsSync(homeOpenclawLink)) {
    fs.symlinkSync(openclawDir, homeOpenclawLink);
    console.log(`[alphaclaw] Symlinked ${homeOpenclawLink} -> ${openclawDir}`);
  }
} catch (e) {
  console.log(`[alphaclaw] Symlink skipped: ${e.message}`);
}

// ---------------------------------------------------------------------------
// 4. Ensure <rootDir>/.env exists (seed from template if missing)
// ---------------------------------------------------------------------------

const envFilePath = path.join(rootDir, ".env");
const setupDir = path.join(__dirname, "..", "lib", "setup");
const templatePath = path.join(setupDir, "env.template");

try {
  if (!fs.existsSync(envFilePath) && fs.existsSync(templatePath)) {
    fs.copyFileSync(templatePath, envFilePath);
    console.log(`[alphaclaw] Created env at ${envFilePath}`);
  }
} catch (e) {
  console.log(`[alphaclaw] .env setup skipped: ${e.message}`);
}

// ---------------------------------------------------------------------------
// 5. Load .env into process.env
// ---------------------------------------------------------------------------

if (fs.existsSync(envFilePath)) {
  const content = fs.readFileSync(envFilePath, "utf8");
  for (const line of content.split("\n")) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith("#")) continue;
    const eqIdx = trimmed.indexOf("=");
    if (eqIdx === -1) continue;
    const key = trimmed.slice(0, eqIdx);
    const value = trimmed.slice(eqIdx + 1);
    if (value) process.env[key] = value;
  }
  console.log("[alphaclaw] Loaded .env");
}

const runGitSync = () => {
  const githubToken = String(process.env.GITHUB_TOKEN || "").trim();
  const githubRepo = resolveGithubRepoPath(
    process.env.GITHUB_WORKSPACE_REPO || "",
  );
  const commitMessage = String(
    flagValue(commandArgs, "--message", "-m") || "",
  ).trim();
  const requestedFilePath = String(
    flagValue(commandArgs, "--file", "-f") || "",
  ).trim();
  const normalizedFilePath = normalizeGitSyncFilePath(requestedFilePath);
  if (!commitMessage) {
    console.error("[alphaclaw] Missing --message for git-sync");
    return 1;
  }
  if (normalizedFilePath) {
    const pathValidation = validateGitSyncFilePath(normalizedFilePath);
    if (!pathValidation.ok) {
      console.error(pathValidation.error);
      return 1;
    }
  }
  if (!githubToken) {
    console.error("[alphaclaw] Missing GITHUB_TOKEN for git-sync");
    return 1;
  }
  if (!githubRepo) {
    console.error("[alphaclaw] Missing GITHUB_WORKSPACE_REPO for git-sync");
    return 1;
  }
  if (!fs.existsSync(path.join(openclawDir, ".git"))) {
    console.error("[alphaclaw] No git repository at /data/.openclaw");
    return 1;
  }

  const realGitPath = resolveRealGitPath({
    shimPath: "/usr/local/bin/git",
  });
  if (!realGitPath) {
    console.error(
      "[alphaclaw] Missing git binary for git-sync; install git in the runtime image",
    );
    return 1;
  }

  const originUrl = `https://github.com/${githubRepo}.git`;
  let branch = "main";
  try {
    branch =
      String(
        execSync("git symbolic-ref --short HEAD", {
          cwd: openclawDir,
          encoding: "utf8",
          stdio: ["ignore", "pipe", "ignore"],
        }),
      ).trim() || "main";
  } catch {}
  const askPassPath = path.join(
    os.tmpdir(),
    `alphaclaw-git-askpass-${process.pid}.sh`,
  );
  const runGit = (gitCommand, { withAuth = false } = {}) => {
    const cmd = withAuth
      ? `GIT_TERMINAL_PROMPT=0 GIT_ASKPASS=${quoteArg(askPassPath)} ${quoteArg(realGitPath)} ${gitCommand}`
      : `${quoteArg(realGitPath)} ${gitCommand}`;
    return execSync(cmd, {
      cwd: openclawDir,
      stdio: "pipe",
      encoding: "utf8",
      env: {
        ...process.env,
        GITHUB_TOKEN: githubToken,
      },
    });
  };

  try {
    fs.writeFileSync(
      askPassPath,
      [
        "#!/usr/bin/env sh",
        'case "$1" in',
        '  *Username*) echo "x-access-token" ;;',
        '  *Password*) echo "${GITHUB_TOKEN:-}" ;;',
        '  *) echo "" ;;',
        "esac",
        "",
      ].join("\n"),
      { mode: 0o700 },
    );

    runGit(`remote set-url origin ${quoteArg(originUrl)}`);
    runGit(`config user.name ${quoteArg("AlphaClaw Agent")}`);
    runGit(`config user.email ${quoteArg("agent@alphaclaw.md")}`);
    try {
      runGit(`ls-remote --exit-code --heads origin ${quoteArg(branch)}`, {
        withAuth: true,
      });
      runGit(`pull --rebase --autostash origin ${quoteArg(branch)}`, {
        withAuth: true,
      });
    } catch {
      console.log(
        `[alphaclaw] Remote branch "${branch}" not found, skipping pull`,
      );
    }
    if (normalizedFilePath) {
      runGit(`add -A -- ${quoteArg(normalizedFilePath)}`);
    } else {
      runGit("add -A");
    }
    try {
      runGit("diff --cached --quiet");
      console.log("[alphaclaw] No changes to commit");
      return 0;
    } catch {}
    if (normalizedFilePath) {
      runGit(
        `commit -m ${quoteArg(commitMessage)} -- ${quoteArg(normalizedFilePath)}`,
      );
    } else {
      runGit(`commit -m ${quoteArg(commitMessage)}`);
    }
    runGit(`push origin ${quoteArg(branch)}`, { withAuth: true });
    const hash = String(runGit("rev-parse --short HEAD")).trim();
    console.log(`[alphaclaw] Git sync complete (${hash})`);
    console.log(
      `[alphaclaw] Commit URL: https://github.com/${githubRepo}/commit/${hash}`,
    );
    return 0;
  } catch (e) {
    const details = String(e.stderr || e.stdout || e.message || "").trim();
    console.error(`[alphaclaw] git-sync failed: ${details.slice(0, 400)}`);
    return 1;
  } finally {
    try {
      fs.rmSync(askPassPath, { force: true });
    } catch {}
  }
};

if (command === "git-sync") {
  process.exit(runGitSync());
}

const runTelegramTopicAdd = () => {
  const topicName = String(flagValue(commandArgs, "--name") || "").trim();
  const threadId = String(flagValue(commandArgs, "--thread") || "").trim();
  const systemInstructions = String(
    flagValue(commandArgs, "--system") || "",
  ).trim();
  const agentId = String(flagValue(commandArgs, "--agent") || "").trim();
  const requestedGroupId = String(
    flagValue(commandArgs, "--group") || "",
  ).trim();
  if (!threadId) {
    console.error("[alphaclaw] Missing --thread for telegram topic add");
    return 1;
  }
  if (!topicName) {
    console.error("[alphaclaw] Missing --name for telegram topic add");
    return 1;
  }

  const configPath = path.join(openclawDir, "openclaw.json");
  if (!fs.existsSync(configPath)) {
    console.error("[alphaclaw] Missing openclaw.json. Run setup first.");
    return 1;
  }

  try {
    const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
    const configuredGroups = Object.keys(cfg.channels?.telegram?.groups || {});
    let groupId = requestedGroupId;
    if (!groupId) {
      if (configuredGroups.length === 1) {
        [groupId] = configuredGroups;
      } else if (configuredGroups.length === 0) {
        console.error(
          "[alphaclaw] No Telegram group configured. Configure Telegram workspace first.",
        );
        return 1;
      } else {
        console.error(
          "[alphaclaw] Multiple Telegram groups detected. Provide --group <groupId>.",
        );
        return 1;
      }
    }

    const topicRegistry = require("../lib/server/topic-registry");
    const {
      syncConfigForTelegram,
    } = require("../lib/server/telegram-workspace");
    const {
      syncBootstrapPromptFiles,
    } = require("../lib/server/onboarding/workspace");
    topicRegistry.updateTopic(groupId, threadId, {
      name: topicName,
      ...(systemInstructions ? { systemInstructions } : {}),
      ...(agentId ? { agentId } : {}),
    });

    const requireMention =
      !!cfg.channels?.telegram?.groups?.[groupId]?.requireMention;
    const syncResult = syncConfigForTelegram({
      fs,
      openclawDir,
      topicRegistry,
      groupId,
      requireMention,
      resolvedUserId: "",
    });
    syncBootstrapPromptFiles({
      fs,
      workspaceDir: path.join(openclawDir, "workspace"),
    });

    const agentSuffix = agentId ? ` agent=${agentId}` : "";
    console.log(
      `[alphaclaw] Topic mapped: group=${groupId} thread=${threadId} name=${topicName}${agentSuffix}`,
    );
    console.log(
      `[alphaclaw] Concurrency updated: agent=${syncResult.maxConcurrent} subagents=${syncResult.subagentMaxConcurrent} topics=${syncResult.totalTopics}`,
    );
    return 0;
  } catch (e) {
    console.error(`[alphaclaw] telegram topic add failed: ${e.message}`);
    return 1;
  }
};

if (
  command === "telegram" &&
  commandScope === "topic" &&
  commandAction === "add"
) {
  process.exit(runTelegramTopicAdd());
}

const kPort = String(process.env.PORT || "3000").trim();
if (kPort === "18789") {
  console.error(
    [
      "[alphaclaw] Fatal config error: AlphaClaw cannot be started on port 18789.",
      "[alphaclaw] Port 18789 is reserved for the OpenClaw gateway.",
    ].join("\n"),
  );
  process.exit(1);
}

const kSetupPassword = String(process.env.SETUP_PASSWORD || "").trim();
if (!kSetupPassword) {
  console.error(
    [
      "[alphaclaw] Fatal config error: SETUP_PASSWORD is missing or empty.",
      "[alphaclaw] Set SETUP_PASSWORD in your deployment environment variables and restart.",
      "[alphaclaw] Examples:",
      "[alphaclaw] - Render: Dashboard -> Environment -> Add SETUP_PASSWORD",
      "[alphaclaw] - Railway: Project -> Variables -> Add SETUP_PASSWORD",
    ].join("\n"),
  );
  process.exit(1);
}

// ---------------------------------------------------------------------------
// 7. Set OPENCLAW_HOME globally so all child processes inherit it
// ---------------------------------------------------------------------------

process.env.OPENCLAW_HOME = rootDir;
process.env.HOME = rootDir;
process.env.OPENCLAW_CONFIG_PATH = path.join(openclawDir, "openclaw.json");
process.env.OPENCLAW_STATE_DIR = openclawDir;
process.env.GOG_KEYRING_PASSWORD =
  process.env.GOG_KEYRING_PASSWORD || "alphaclaw";

// ---------------------------------------------------------------------------
// 8. Install gog (Google Workspace CLI) if not present
// ---------------------------------------------------------------------------

process.env.XDG_CONFIG_HOME = openclawDir;

const ensureGogCliCompatConfigPath = () => {
  const configDir = path.join(rootDir, ".config");
  const compatPath = path.join(configDir, "gogcli");
  const managedPath = path.join(openclawDir, "gogcli");

  try {
    fs.mkdirSync(configDir, { recursive: true });
    if (!fs.existsSync(compatPath)) {
      fs.symlinkSync(managedPath, compatPath, "dir");
      console.log(
        `[alphaclaw] Linked gogcli config path ${compatPath} -> ${managedPath}`,
      );
      return;
    }

    const stat = fs.lstatSync(compatPath);
    if (!stat.isSymbolicLink()) return;
    const linkTarget = fs.readlinkSync(compatPath);
    const resolvedTarget = path.resolve(configDir, linkTarget);
    if (resolvedTarget !== managedPath) {
      console.log(
        `[alphaclaw] gogcli config path already exists at ${compatPath}; leaving existing symlink in place`,
      );
    }
  } catch (error) {
    console.log(
      `[alphaclaw] gogcli config path compatibility setup skipped: ${error.message}`,
    );
  }
};

ensureGogCliCompatConfigPath();

const gogInstalled = (() => {
  try {
    execSync("command -v gog", { stdio: "ignore" });
    return true;
  } catch {
    return false;
  }
})();

if (!gogInstalled) {
  console.log("[alphaclaw] Installing gog CLI...");
  try {
    const gogVersion = process.env.GOG_VERSION || "0.11.0";
    const platform = os.platform() === "darwin" ? "darwin" : "linux";
    const arch = os.arch() === "arm64" ? "arm64" : "amd64";
    const tarball = `gogcli_${gogVersion}_${platform}_${arch}.tar.gz`;
    const url = `https://github.com/steipete/gogcli/releases/download/v${gogVersion}/${tarball}`;
    execSync(
      `curl -fsSL "${url}" -o /tmp/gog.tar.gz && tar -xzf /tmp/gog.tar.gz -C /tmp/ && mv /tmp/gog /usr/local/bin/gog && chmod +x /usr/local/bin/gog && rm -f /tmp/gog.tar.gz`,
      { stdio: "inherit" },
    );
    console.log("[alphaclaw] gog CLI installed");
  } catch (e) {
    console.log(`[alphaclaw] gog install skipped: ${e.message}`);
  }
}

// ---------------------------------------------------------------------------
// 9. Install/reconcile system cron entry
// ---------------------------------------------------------------------------

const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");

try {
  if (fs.existsSync(packagedHourlyGitSyncPath)) {
    const packagedSyncScript = fs.readFileSync(
      packagedHourlyGitSyncPath,
      "utf8",
    );
    const installedSyncScript = fs.existsSync(hourlyGitSyncPath)
      ? fs.readFileSync(hourlyGitSyncPath, "utf8")
      : "";
    if (
      shouldRefreshHourlyGitSyncScript({
        packagedSyncScript,
        installedSyncScript,
      })
    ) {
      fs.writeFileSync(hourlyGitSyncPath, packagedSyncScript, { mode: 0o755 });
      console.log("[alphaclaw] Refreshed hourly git sync script");
    }
  }
} catch (e) {
  console.log(
    `[alphaclaw] Hourly git sync script refresh skipped: ${e.message}`,
  );
}

if (fs.existsSync(hourlyGitSyncPath)) {
  try {
    const syncCronConfig = path.join(openclawDir, "cron", "system-sync.json");
    let cronEnabled = true;
    let cronSchedule = "0 * * * *";

    if (fs.existsSync(syncCronConfig)) {
      try {
        const cfg = JSON.parse(fs.readFileSync(syncCronConfig, "utf8"));
        cronEnabled = cfg.enabled !== false;
        const schedule = String(cfg.schedule || "").trim();
        if (/^(\S+\s+){4}\S+$/.test(schedule)) cronSchedule = schedule;
      } catch {}
    }

    const cronFilePath = "/etc/cron.d/openclaw-hourly-sync";
    if (cronEnabled) {
      const cronContent = [
        "SHELL=/bin/bash",
        "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
        `${cronSchedule} root bash "${hourlyGitSyncPath}" >> /var/log/openclaw-hourly-sync.log 2>&1`,
        "",
      ].join("\n");
      fs.writeFileSync(cronFilePath, cronContent, { mode: 0o644 });
      console.log("[alphaclaw] System cron entry installed");
    } else {
      try {
        fs.unlinkSync(cronFilePath);
      } catch {}
      console.log("[alphaclaw] System cron entry disabled");
    }
  } catch (e) {
    console.log(`[alphaclaw] Cron setup skipped: ${e.message}`);
  }
}

// ---------------------------------------------------------------------------
// 9. Start cron daemon if available
// ---------------------------------------------------------------------------

try {
  execSync("command -v cron", { stdio: "ignore" });
  try {
    execSync("pgrep -x cron", { stdio: "ignore" });
  } catch {
    execSync("cron", { stdio: "ignore" });
  }
  console.log("[alphaclaw] Cron daemon running");
} catch {}

// ---------------------------------------------------------------------------
// 10. Reconcile channels if already onboarded
// ---------------------------------------------------------------------------

const configPath = path.join(openclawDir, "openclaw.json");
const githubRepo = process.env.GITHUB_WORKSPACE_REPO;

if (fs.existsSync(path.join(openclawDir, ".git"))) {
  if (githubRepo) {
    const repoUrl = githubRepo
      .replace(/^git@github\.com:/, "")
      .replace(/^https:\/\/github\.com\//, "")
      .replace(/\.git$/, "");
    const remoteUrl = `https://github.com/${repoUrl}.git`;
    try {
      execSync(`git remote set-url origin "${remoteUrl}"`, {
        cwd: openclawDir,
        stdio: "ignore",
      });
      console.log("[alphaclaw] Repo ready");
    } catch {}
  }

  // Migration path: scrub persisted PATs from existing GitHub origin URLs.
  try {
    const existingOrigin = execSync("git remote get-url origin", {
      cwd: openclawDir,
      stdio: ["ignore", "pipe", "ignore"],
      encoding: "utf8",
    }).trim();
    const match = existingOrigin.match(/^https:\/\/[^/@]+@github\.com\/(.+)$/i);
    if (match?.[1]) {
      const cleanedPath = String(match[1]).replace(/\.git$/i, "");
      const cleanedOrigin = `https://github.com/${cleanedPath}.git`;
      execSync(`git remote set-url origin "${cleanedOrigin}"`, {
        cwd: openclawDir,
        stdio: "ignore",
      });
      console.log("[alphaclaw] Scrubbed tokenized GitHub remote URL");
    }
  } catch {}

  restoreMissingOpenclawConfigFromRemote({
    openclawDir,
    configPath,
    env: process.env,
  });
  if (
    ensureMainUpstream({
      openclawDir,
      gitEnv: process.env,
    })
  ) {
    console.log("[alphaclaw] Set main upstream to origin/main");
  }
}

if (fs.existsSync(configPath)) {
  console.log("[alphaclaw] Config exists, reconciling channels...");

  try {
    const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
    if (!cfg.channels) cfg.channels = {};
    if (!cfg.plugins) cfg.plugins = {};
    if (!cfg.plugins.load) cfg.plugins.load = {};
    if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
    if (!cfg.plugins.entries) cfg.plugins.entries = {};
    let changed = false;

    if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
      cfg.channels.telegram = {
        enabled: true,
        botToken: process.env.TELEGRAM_BOT_TOKEN,
        dmPolicy: "pairing",
        groupPolicy: "allowlist",
      };
      cfg.plugins.entries.telegram = { enabled: true };
      console.log("[alphaclaw] Telegram added");
      changed = true;
    }

    if (process.env.DISCORD_BOT_TOKEN && !cfg.channels.discord) {
      cfg.channels.discord = {
        enabled: true,
        token: process.env.DISCORD_BOT_TOKEN,
        dmPolicy: "pairing",
        groupPolicy: "allowlist",
      };
      cfg.plugins.entries.discord = { enabled: true };
      console.log("[alphaclaw] Discord added");
      changed = true;
    }
    if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
      cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
      changed = true;
    }
    if (cfg.plugins.entries["usage-tracker"]?.enabled !== true) {
      cfg.plugins.entries["usage-tracker"] = { enabled: true };
      changed = true;
    }

    if (changed) {
      let content = JSON.stringify(cfg, null, 2);
      const replacements = buildSecretReplacements(process.env);
      for (const [secret, envRef] of replacements) {
        if (secret) {
          // Only replace the secret if it is an exact match for a JSON string value
          // This ensures we do not replace substrings inside other strings
          const secretJson = JSON.stringify(secret);
          content = content.replace(
            new RegExp(
              secretJson.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"),
              "g",
            ),
            JSON.stringify(envRef),
          );
        }
      }
      fs.writeFileSync(configPath, content);
      console.log("[alphaclaw] Config updated and sanitized");
    }
  } catch (e) {
    console.error(`[alphaclaw] Channel reconciliation error: ${e.message}`);
  }
} else {
  console.log(
    "[alphaclaw] No config yet -- onboarding will run from the Setup UI",
  );
}

// ---------------------------------------------------------------------------
// 12. Install systemctl shim if in Docker (no real systemd)
// ---------------------------------------------------------------------------

try {
  execSync("command -v systemctl", { stdio: "ignore" });
} catch {
  const shimSrc = path.join(__dirname, "..", "lib", "scripts", "systemctl");
  const shimDest = "/usr/local/bin/systemctl";
  try {
    fs.copyFileSync(shimSrc, shimDest);
    fs.chmodSync(shimDest, 0o755);
    console.log("[alphaclaw] systemctl shim installed");
  } catch (e) {
    console.log(`[alphaclaw] systemctl shim skipped: ${e.message}`);
  }
}

// ---------------------------------------------------------------------------
// 13. Install git auth shim
// ---------------------------------------------------------------------------

try {
  const gitAskPassSrc = path.join(__dirname, "..", "lib", "scripts", "git-askpass");
  const gitAskPassDest = "/tmp/alphaclaw-git-askpass.sh";
  const gitShimTemplatePath = path.join(__dirname, "..", "lib", "scripts", "git");
  const gitShimDest = "/usr/local/bin/git";

  if (fs.existsSync(gitAskPassSrc)) {
    fs.copyFileSync(gitAskPassSrc, gitAskPassDest);
    fs.chmodSync(gitAskPassDest, 0o755);
  }

  if (fs.existsSync(gitShimTemplatePath)) {
    const realGitPath =
      resolveRealGitPath({
        shimPath: gitShimDest,
      }) || "/usr/bin/git";

    const gitShimTemplate = fs.readFileSync(gitShimTemplatePath, "utf8");
    const gitShimContent = gitShimTemplate
      .replace("@@REAL_GIT@@", realGitPath)
      .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir);
    fs.writeFileSync(gitShimDest, gitShimContent, { mode: 0o755 });
    console.log("[alphaclaw] git auth shim installed");
  }
} catch (e) {
  console.log(`[alphaclaw] git auth shim skipped: ${e.message}`);
}

// ---------------------------------------------------------------------------
// 14. Start Express server
// ---------------------------------------------------------------------------

console.log("[alphaclaw] Setup complete -- starting server");
require("../lib/server.js");


================================================
FILE: lib/cli/git-runtime.js
================================================
const fs = require("fs");
const path = require("path");
const { execSync } = require("child_process");

const normalizeGitSyncFilePath = (requestedFilePath) => {
  const rawPath = String(requestedFilePath || "").trim();
  if (!rawPath) return "";
  return rawPath.replace(/\\/g, "/").replace(/^\.\/+/, "");
};

const validateGitSyncFilePath = (normalizedFilePath) => {
  if (!normalizedFilePath) return { ok: true };
  if (
    normalizedFilePath.startsWith("/") ||
    normalizedFilePath.startsWith("../") ||
    normalizedFilePath.includes("/../")
  ) {
    return {
      ok: false,
      error: "[alphaclaw] --file must stay within /data/.openclaw",
    };
  }
  return { ok: true };
};

const listGitCandidates = ({ execSyncImpl = execSync } = {}) => {
  try {
    return String(
      execSyncImpl("which -a git", {
        stdio: ["ignore", "pipe", "ignore"],
        encoding: "utf8",
      }),
    )
      .split("\n")
      .map((candidate) => candidate.trim())
      .filter(Boolean);
  } catch {
    return [];
  }
};

const canExecute = ({ fsModule = fs, candidatePath = "" } = {}) => {
  const normalizedCandidatePath = String(candidatePath || "").trim();
  if (!normalizedCandidatePath) return false;
  try {
    fsModule.accessSync(normalizedCandidatePath, fsModule.constants.X_OK);
    return true;
  } catch {
    return false;
  }
};

const resolveRealGitPath = ({
  execSyncImpl = execSync,
  fsModule = fs,
  shimPath = "",
  hintedPath = "",
} = {}) => {
  const normalizedShimPath = String(shimPath || "").trim()
    ? path.resolve(String(shimPath || "").trim())
    : "";
  const candidates = [
    String(process.env.ALPHACLAW_REAL_GIT || "").trim(),
    String(hintedPath || "").trim(),
    "/usr/bin/git",
    "/bin/git",
    "/usr/libexec/git-core/git",
    "/usr/local/bin/git.real",
  ];

  for (const candidatePath of [...candidates, ...listGitCandidates({ execSyncImpl })]) {
    const normalizedCandidatePath = String(candidatePath || "").trim();
    if (!normalizedCandidatePath) continue;
    const resolvedCandidatePath = path.resolve(normalizedCandidatePath);
    if (normalizedShimPath && resolvedCandidatePath === normalizedShimPath) continue;
    if (!canExecute({ fsModule, candidatePath: resolvedCandidatePath })) continue;
    return resolvedCandidatePath;
  }

  return "";
};

const shouldRefreshHourlyGitSyncScript = ({
  packagedSyncScript = "",
  installedSyncScript = "",
} = {}) => {
  const nextPackagedSyncScript = String(packagedSyncScript || "");
  if (!nextPackagedSyncScript.trim()) return false;
  return nextPackagedSyncScript !== String(installedSyncScript || "");
};

module.exports = {
  normalizeGitSyncFilePath,
  validateGitSyncFilePath,
  resolveRealGitPath,
  shouldRefreshHourlyGitSyncScript,
};


================================================
FILE: lib/cli/git-sync.js
================================================
const normalizeGitSyncFilePath = (requestedFilePath) => {
  const rawPath = String(requestedFilePath || "").trim();
  if (!rawPath) return "";
  return rawPath.replace(/\\/g, "/").replace(/^\.\/+/, "");
};

const validateGitSyncFilePath = (normalizedFilePath) => {
  if (!normalizedFilePath) return { ok: true };
  if (
    normalizedFilePath.startsWith("/") ||
    normalizedFilePath.startsWith("../") ||
    normalizedFilePath.includes("/../")
  ) {
    return {
      ok: false,
      error: "[alphaclaw] --file must stay within /data/.openclaw",
    };
  }
  return { ok: true };
};

module.exports = {
  normalizeGitSyncFilePath,
  validateGitSyncFilePath,
};


================================================
FILE: lib/cli/openclaw-config-restore.js
================================================
"use strict";

const fs = require("fs");
const os = require("os");
const path = require("path");
const { execSync } = require("child_process");

const kOpenclawConfigFile = "openclaw.json";

const quoteArg = (value) => `'${String(value || "").replace(/'/g, "'\"'\"'")}'`;

const resolveCurrentBranch = ({ execSyncImpl, openclawDir }) => {
  try {
    return (
      String(
        execSyncImpl("git symbolic-ref --short HEAD", {
          cwd: openclawDir,
          stdio: ["ignore", "pipe", "ignore"],
          encoding: "utf8",
        }),
      ).trim() || "main"
    );
  } catch {
    return "main";
  }
};

const createGitEnv = ({ fsModule, osModule, env, processId }) => {
  const githubToken = String(env.GITHUB_TOKEN || "").trim();
  const gitEnv = { ...env };
  if (!githubToken) {
    return { gitEnv, askPassPath: "" };
  }

  const askPassPath = path.join(
    osModule.tmpdir(),
    `alphaclaw-boot-git-askpass-${processId}.sh`,
  );
  fsModule.writeFileSync(
    askPassPath,
    [
      "#!/usr/bin/env sh",
      'case "$1" in',
      '  *Username*) echo "x-access-token" ;;',
      '  *Password*) echo "${GITHUB_TOKEN:-}" ;;',
      '  *) echo "" ;;',
      "esac",
      "",
    ].join("\n"),
    { mode: 0o700 },
  );
  gitEnv.GITHUB_TOKEN = githubToken;
  gitEnv.GIT_TERMINAL_PROMPT = "0";
  gitEnv.GIT_ASKPASS = askPassPath;
  return { gitEnv, askPassPath };
};

const restoreMissingOpenclawConfigFromRemote = ({
  fsModule = fs,
  osModule = os,
  execSyncImpl = execSync,
  env = process.env,
  logger = console,
  processId = process.pid,
  openclawDir,
  configPath = path.join(openclawDir || "", kOpenclawConfigFile),
} = {}) => {
  if (!openclawDir) {
    throw new Error("openclawDir is required");
  }

  if (fsModule.existsSync(configPath)) {
    logger.log(
      "[alphaclaw] Remote config restore skipped: local openclaw.json already exists",
    );
    return { restored: false, skipped: true, reason: "exists" };
  }

  const branch = resolveCurrentBranch({ execSyncImpl, openclawDir });
  const { gitEnv, askPassPath } = createGitEnv({
    fsModule,
    osModule,
    env,
    processId,
  });

  try {
    execSyncImpl(
      `git ls-remote --exit-code --heads origin ${quoteArg(branch)}`,
      {
        cwd: openclawDir,
        stdio: "ignore",
        env: gitEnv,
      },
    );
    execSyncImpl(`git fetch --quiet origin ${quoteArg(branch)}`, {
      cwd: openclawDir,
      stdio: "ignore",
      env: gitEnv,
    });
    const remoteConfig = String(
      execSyncImpl(`git show ${quoteArg(`origin/${branch}:openclaw.json`)}`, {
        cwd: openclawDir,
        stdio: ["ignore", "pipe", "ignore"],
        encoding: "utf8",
        env: gitEnv,
      }),
    );
    if (!remoteConfig.trim()) {
      logger.log("[alphaclaw] Remote config restore skipped: remote config empty");
      return { restored: false, skipped: true, reason: "empty_remote", branch };
    }
    fsModule.writeFileSync(configPath, remoteConfig);
    logger.log(`[alphaclaw] Restored missing openclaw.json from origin/${branch}`);
    return { restored: true, skipped: false, reason: "missing", branch };
  } catch (e) {
    logger.log(
      `[alphaclaw] Remote config restore skipped: ${String(e.message || "").slice(0, 200)}`,
    );
    return {
      restored: false,
      skipped: true,
      reason: "error",
      branch,
      error: e,
    };
  } finally {
    if (askPassPath) {
      try {
        fsModule.rmSync(askPassPath, { force: true });
      } catch {}
    }
  }
};

const ensureMainUpstream = ({ execSyncImpl = execSync, openclawDir, gitEnv }) => {
  try {
    execSyncImpl("git show-ref --verify --quiet refs/heads/main", {
      cwd: openclawDir,
      stdio: "ignore",
    });
    try {
      execSyncImpl("git rev-parse --abbrev-ref --symbolic-full-name main@{upstream}", {
        cwd: openclawDir,
        stdio: "ignore",
      });
    } catch {
      execSyncImpl("git branch --set-upstream-to=origin/main main", {
        cwd: openclawDir,
        stdio: "ignore",
        env: gitEnv,
      });
      return true;
    }
  } catch {}
  return false;
};

module.exports = {
  ensureMainUpstream,
  restoreMissingOpenclawConfigFromRemote,
};


================================================
FILE: lib/plugin/usage-tracker/index.js
================================================
const fs = require("fs");
const os = require("os");
const path = require("path");
const { DatabaseSync } = require("node:sqlite");

const kPluginId = "usage-tracker";
const kFallbackRootDir = path.join(os.homedir(), ".alphaclaw");

const coerceCount = (value) => {
  const parsed = Number.parseInt(String(value ?? 0), 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
};

const resolveRootDir = () =>
  process.env.ALPHACLAW_ROOT_DIR ||
  process.env.OPENCLAW_HOME ||
  process.env.OPENCLAW_ROOT_DIR ||
  kFallbackRootDir;

const safeAlterTable = (database, sql) => {
  try {
    database.exec(sql);
  } catch (err) {
    const message = String(err?.message || "").toLowerCase();
    if (!message.includes("duplicate column name")) throw err;
  }
};

const ensureSchema = (database) => {
  database.exec("PRAGMA journal_mode=WAL;");
  database.exec("PRAGMA synchronous=NORMAL;");
  database.exec("PRAGMA busy_timeout=5000;");
  database.exec(`
    CREATE TABLE IF NOT EXISTS usage_events (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      timestamp INTEGER NOT NULL,
      session_id TEXT,
      session_key TEXT,
      run_id TEXT,
      provider TEXT NOT NULL,
      model TEXT NOT NULL,
      input_tokens INTEGER NOT NULL DEFAULT 0,
      output_tokens INTEGER NOT NULL DEFAULT 0,
      cache_read_tokens INTEGER NOT NULL DEFAULT 0,
      cache_write_tokens INTEGER NOT NULL DEFAULT 0,
      total_tokens INTEGER NOT NULL DEFAULT 0
    );
  `);
  database.exec(`
    CREATE INDEX IF NOT EXISTS idx_usage_events_ts
    ON usage_events(timestamp DESC);
  `);
  database.exec(`
    CREATE INDEX IF NOT EXISTS idx_usage_events_session
    ON usage_events(session_id);
  `);
  safeAlterTable(
    database,
    "ALTER TABLE usage_events ADD COLUMN session_key TEXT;",
  );
  database.exec(`
    CREATE INDEX IF NOT EXISTS idx_usage_events_session_key
    ON usage_events(session_key);
  `);
  database.exec(`
    CREATE TABLE IF NOT EXISTS usage_daily (
      date TEXT NOT NULL,
      model TEXT NOT NULL,
      provider TEXT,
      input_tokens INTEGER NOT NULL DEFAULT 0,
      output_tokens INTEGER NOT NULL DEFAULT 0,
      cache_read_tokens INTEGER NOT NULL DEFAULT 0,
      cache_write_tokens INTEGER NOT NULL DEFAULT 0,
      total_tokens INTEGER NOT NULL DEFAULT 0,
      turn_count INTEGER NOT NULL DEFAULT 0,
      PRIMARY KEY (date, model)
    );
  `);
  database.exec(`
    CREATE TABLE IF NOT EXISTS tool_events (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      timestamp INTEGER NOT NULL,
      session_id TEXT,
      session_key TEXT,
      tool_name TEXT NOT NULL,
      success INTEGER NOT NULL DEFAULT 1,
      duration_ms INTEGER
    );
  `);
  database.exec(`
    CREATE INDEX IF NOT EXISTS idx_tool_events_session
    ON tool_events(session_id);
  `);
  safeAlterTable(
    database,
    "ALTER TABLE tool_events ADD COLUMN session_key TEXT;",
  );
  database.exec(`
    CREATE INDEX IF NOT EXISTS idx_tool_events_session_key
    ON tool_events(session_key);
  `);
};

const createPlugin = () => {
  let database = null;
  let dbPath = "";
  let insertUsageEventStmt = null;
  let upsertUsageDailyStmt = null;
  let insertToolEventStmt = null;

  const getDatabase = () => {
    if (database) return database;
    const rootDir = resolveRootDir();
    const dbDir = path.join(rootDir, "db");
    fs.mkdirSync(dbDir, { recursive: true });
    dbPath = path.join(dbDir, "usage.db");
    database = new DatabaseSync(dbPath);
    ensureSchema(database);
    insertUsageEventStmt = database.prepare(`
      INSERT INTO usage_events (
        timestamp,
        session_id,
        session_key,
        run_id,
        provider,
        model,
        input_tokens,
        output_tokens,
        cache_read_tokens,
        cache_write_tokens,
        total_tokens
      ) VALUES (
        $timestamp,
        $session_id,
        $session_key,
        $run_id,
        $provider,
        $model,
        $input_tokens,
        $output_tokens,
        $cache_read_tokens,
        $cache_write_tokens,
        $total_tokens
      )
    `);
    upsertUsageDailyStmt = database.prepare(`
      INSERT INTO usage_daily (
        date,
        model,
        provider,
        input_tokens,
        output_tokens,
        cache_read_tokens,
        cache_write_tokens,
        total_tokens,
        turn_count
      ) VALUES (
        $date,
        $model,
        $provider,
        $input_tokens,
        $output_tokens,
        $cache_read_tokens,
        $cache_write_tokens,
        $total_tokens,
        1
      )
      ON CONFLICT(date, model) DO UPDATE SET
        provider = COALESCE(excluded.provider, usage_daily.provider),
        input_tokens = usage_daily.input_tokens + excluded.input_tokens,
        output_tokens = usage_daily.output_tokens + excluded.output_tokens,
        cache_read_tokens = usage_daily.cache_read_tokens + excluded.cache_read_tokens,
        cache_write_tokens = usage_daily.cache_write_tokens + excluded.cache_write_tokens,
        total_tokens = usage_daily.total_tokens + excluded.total_tokens,
        turn_count = usage_daily.turn_count + 1
    `);
    insertToolEventStmt = database.prepare(`
      INSERT INTO tool_events (
        timestamp,
        session_id,
        session_key,
        tool_name,
        success,
        duration_ms
      ) VALUES (
        $timestamp,
        $session_id,
        $session_key,
        $tool_name,
        $success,
        $duration_ms
      )
    `);
    return database;
  };

  const writeUsageEvent = (event, ctx, logger) => {
    const usage = event?.usage ?? {};
    const timestamp = Date.now();
    const date = new Date(timestamp).toISOString().slice(0, 10);
    const inputTokens = coerceCount(usage.input);
    const outputTokens = coerceCount(usage.output);
    const cacheReadTokens = coerceCount(usage.cacheRead);
    const cacheWriteTokens = coerceCount(usage.cacheWrite);
    const fallbackTotal = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
    const totalTokens = coerceCount(usage.total) || fallbackTotal;
    if (totalTokens <= 0) return;
    getDatabase();
    insertUsageEventStmt.run({
      $timestamp: timestamp,
      $session_id: String(event?.sessionId || ctx?.sessionId || ""),
      $session_key: String(ctx?.sessionKey || ""),
      $run_id: String(event?.runId || ""),
      $provider: String(event?.provider || "unknown"),
      $model: String(event?.model || "unknown"),
      $input_tokens: inputTokens,
      $output_tokens: outputTokens,
      $cache_read_tokens: cacheReadTokens,
      $cache_write_tokens: cacheWriteTokens,
      $total_tokens: totalTokens,
    });
    upsertUsageDailyStmt.run({
      $date: date,
      $model: String(event?.model || "unknown"),
      $provider: String(event?.provider || "unknown"),
      $input_tokens: inputTokens,
      $output_tokens: outputTokens,
      $cache_read_tokens: cacheReadTokens,
      $cache_write_tokens: cacheWriteTokens,
      $total_tokens: totalTokens,
    });
    if (logger?.debug) {
      logger.debug(
        `[${kPluginId}] usage event recorded model=${String(event?.model || "unknown")} total=${totalTokens}`,
      );
    }
  };

  const deriveToolSuccess = (event) => {
    const message = event?.message;
    if (!message || typeof message !== "object") {
      return event?.error ? 0 : 1;
    }
    if (message?.isError === true) return 0;
    if (message?.ok === false) return 0;
    if (typeof message?.error === "string" && message.error.trim()) return 0;
    return 1;
  };

  const writeToolEvent = (event, ctx) => {
    const toolName = String(event?.toolName || "").trim();
    if (!toolName) return;
    const sessionKey = String(ctx?.sessionKey || "").trim();
    const sessionId = String(ctx?.sessionId || "").trim();
    if (!sessionKey && !sessionId) return;
    getDatabase();
    insertToolEventStmt.run({
      $timestamp: Date.now(),
      $session_id: sessionId,
      $session_key: sessionKey,
      $tool_name: toolName,
      $success: deriveToolSuccess(event),
      $duration_ms: coerceCount(event?.durationMs) || null,
    });
  };

  return {
    id: kPluginId,
    name: "AlphaClaw Usage Tracker",
    description: "Captures LLM and tool usage into SQLite for Usage UI",
    register: (api) => {
      const logger = api?.logger;
      try {
        getDatabase();
        logger?.info?.(`[${kPluginId}] initialized db=${dbPath}`);
      } catch (err) {
        logger?.error?.(`[${kPluginId}] failed to initialize database: ${err?.message || err}`);
        return;
      }
      api.on("llm_output", (event, ctx) => {
        try {
          writeUsageEvent(event, ctx, logger);
        } catch (err) {
          logger?.error?.(`[${kPluginId}] llm_output write error: ${err?.message || err}`);
        }
      });
      api.on("tool_result_persist", (event, ctx) => {
        try {
          writeToolEvent(
            {
              ...event,
              toolName: String(event?.toolName || ctx?.toolName || ""),
              durationMs: event?.durationMs,
            },
            ctx,
          );
        } catch (err) {
          logger?.error?.(`[${kPluginId}] tool_result_persist write error: ${err?.message || err}`);
        }
        return {};
      });
    },
  };
};

const plugin = createPlugin();
module.exports = plugin;
module.exports.default = plugin;


================================================
FILE: lib/plugin/usage-tracker/openclaw.plugin.json
================================================
{
  "id": "usage-tracker",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {}
  }
}


================================================
FILE: lib/public/css/agents.css
================================================
/* ── Agents detail layout ────────────────────── */

.app-content-pane.agents-pane {
  overflow: hidden;
  padding-left: 0;
  padding-right: 0;
  padding-bottom: 0;
}

/* ── Detail panel ────────────────────────────── */

.agents-detail-panel {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.agents-detail-header-area {
  padding: 8px 32px 16px;
}

.agents-detail-header-area-inner {
  max-width: 42rem;
  width: 100%;
  margin: 0 auto;
}

.agents-detail-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.agents-detail-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0 32px;
}

.agents-detail-content {
  max-width: 42rem;
  width: 100%;
  margin: 0 auto;
  padding: 0 0 24px;
}

@media (max-width: 768px) {
  .agents-detail-header-area {
    padding: 16px 14px 0;
  }

  .agents-detail-body {
    padding: 0 14px;
  }
}

.agents-detail-header-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Sub-tabs ────────────────────────────────── */

.agents-sub-tabs {
  display: flex;
  align-items: center;
  gap: 2px;
  padding-top: 12px;
  border-bottom: 1px solid var(--border);
}

.agents-sub-tab {
  padding: 8px 14px;
  font-size: 13px;
  font-weight: 500;
  color: var(--text-muted);
  background: transparent;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color 0.12s, border-color 0.12s;
  font-family: inherit;
  margin-bottom: -1px;
}

.agents-sub-tab:hover {
  color: var(--text);
}

.agents-sub-tab.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

/* ── Empty state ─────────────────────────────── */

.agents-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: var(--text-dim);
  gap: 12px;
  padding: 32px;
  text-align: center;
}


================================================
FILE: lib/public/css/chat.css
================================================
/* ── Chat route ──────────────────────────────── */

.app-content-pane.chat-pane {
  padding: 0;
}

.chat-route-shell {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  height: 100%;
}

.chat-route-header {
  padding: 16px 24px 10px;
  border-bottom: 1px solid var(--border);
}

.chat-route-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text);
}

.chat-route-subtitle {
  margin-top: 4px;
  font-size: 12px;
  color: var(--text-muted);
}

.chat-route-warning {
  margin-top: 8px;
  font-size: 12px;
  color: #fca5a5;
}

.chat-thread {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 16px 24px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.chat-empty-state {
  color: var(--text-dim);
  font-size: 12px;
  text-align: center;
  margin-top: 24px;
}

.chat-bubble {
  max-width: 86%;
  border-radius: 10px;
  border: 1px solid var(--panel-border-contrast);
  background: rgba(255, 255, 255, 0.02);
  padding: 12px 14px;
}

.chat-bubble.is-user {
  align-self: flex-end;
  background: rgba(99, 235, 255, 0.06);
  border-color: rgba(99, 235, 255, 0.24);
}

.chat-bubble.is-assistant {
  align-self: flex-start;
}

.chat-bubble-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 8px;
  font-size: 11px;
  color: var(--text-muted);
}

.chat-bubble-content {
  margin: 0;
  font-family: inherit;
  font-size: 12px;
  color: var(--text);
  line-height: 1.6;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.chat-bubble-markdown > :first-child {
  margin-top: 0;
}

.chat-bubble-markdown > :last-child {
  margin-bottom: 0;
}

.chat-bubble-markdown {
  white-space: normal;
}

.chat-bubble-markdown p,
.chat-bubble-markdown ul,
.chat-bubble-markdown ol,
.chat-bubble-markdown pre,
.chat-bubble-markdown blockquote {
  margin: 8px 0;
}

.chat-bubble-markdown ul,
.chat-bubble-markdown ol {
  padding-left: 18px;
  padding-top: 0;
  padding-bottom: 0;
}

.chat-bubble-markdown ul {
  list-style: disc;
}

.chat-bubble-markdown ol {
  list-style: decimal;
}

.chat-bubble-markdown li > p {
  margin: 0;
}

.chat-bubble-markdown li + li {
  margin-top: 4px;
}

.chat-bubble-markdown > * + * {
  margin-top: 8px;
}

.chat-bubble-markdown h1,
.chat-bubble-markdown h2,
.chat-bubble-markdown h3,
.chat-bubble-markdown h4 {
  margin: 0;
  line-height: 1.25;
}

.chat-bubble-markdown h1 {
  font-size: 16px;
}

.chat-bubble-markdown h2 {
  font-size: 14px;
}

.chat-bubble-markdown h3,
.chat-bubble-markdown h4 {
  font-size: 13px;
}

.chat-bubble-markdown h1 + *,
.chat-bubble-markdown h2 + *,
.chat-bubble-markdown h3 + *,
.chat-bubble-markdown h4 + * {
  margin-top: 10px;
}

.chat-bubble-markdown code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
    "Courier New", monospace;
  font-size: 11px;
  background: rgba(255, 255, 255, 0.06);
  padding: 1px 4px;
  border-radius: 4px;
}

.chat-bubble-markdown pre code {
  display: block;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  padding: 8px;
}

.chat-bubble-json {
  white-space: pre-wrap;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
    "Courier New", monospace;
  font-size: 11px;
  line-height: 1.6;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--panel-border-contrast);
  border-radius: 8px;
  padding: 10px;
}

.chat-message-json {
  margin-top: 10px;
  border-top: 1px dashed var(--border);
  padding-top: 8px;
}

.chat-message-json summary {
  cursor: pointer;
  color: var(--text-muted);
  font-size: 11px;
}

.chat-message-json pre {
  margin: 8px 0 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-muted);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.chat-tool-call-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 6px;
}

.chat-tool-call-row {
  border: 1px dashed var(--border);
  border-radius: 8px;
  padding: 6px 8px;
}

.chat-tool-call-row summary {
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.chat-tool-call-name {
  color: var(--text);
  font-size: 12px;
}

.chat-tool-call-status {
  color: var(--text-muted);
  font-size: 10px;
  text-transform: lowercase;
}

.chat-tool-inline {
  font-weight: 600;
}

.chat-tool-inline-message {
  margin: 0;
}

.chat-tool-inline-message summary {
  cursor: pointer;
  list-style: none;
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 10px;
  font-size: 12px;
  line-height: 1.6;
}

.chat-tool-inline-message summary::-webkit-details-marker {
  display: none;
}

.chat-tool-inline-message summary::before {
  content: "▸";
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1;
  margin-right: 2px;
}

.chat-tool-inline-message[open] summary::before {
  content: "▾";
}

.chat-tool-inline-icon {
  font-size: 12px;
  line-height: 1;
}

.chat-tool-inline-title {
  font-weight: 600;
}

.chat-tool-inline-time {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-muted);
}

.chat-tool-inline-body {
  margin-top: 8px;
}

.chat-tool-inline-label {
  margin-top: 8px;
  margin-bottom: 4px;
  font-size: 11px;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.chat-tool-inline-body pre {
  margin: 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--text-muted);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.chat-typing-indicator {
  min-width: 0;
  align-self: flex-start;
}

.chat-typing-dots {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 0 1px;
}

.chat-typing-dots span {
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: var(--text-muted);
  opacity: 0.45;
  animation: chatTypingPulse 1.2s infinite ease-in-out;
}

.chat-typing-dots span:nth-child(2) {
  animation-delay: 0.15s;
}

.chat-typing-dots span:nth-child(3) {
  animation-delay: 0.3s;
}

@keyframes chatTypingPulse {
  0%,
  80%,
  100% {
    transform: translateY(0);
    opacity: 0.35;
  }
  40% {
    transform: translateY(-2px);
    opacity: 0.9;
  }
}

.chat-composer {
  border-top: 1px solid var(--border);
  padding: 12px 24px 16px;
  display: flex;
  align-items: flex-end;
  gap: 10px;
}

.chat-composer-input {
  flex: 1;
  min-height: calc(12px * 1.4 + 20px);
  max-height: calc(12px * 1.4 * 5 + 20px);
  border-radius: 10px;
  resize: none;
  overflow-x: hidden;
  overflow-y: auto;
  font-family: inherit;
  font-size: 12px;
  line-height: 1.4;
  padding: 10px;
  box-sizing: border-box;
}

.chat-composer-send {
  white-space: nowrap;
  padding: 8px 14px;
  border-radius: 9px;
  font-family: inherit;
  font-size: 12px;
}

.chat-composer-actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.chat-composer-stop {
  white-space: nowrap;
  padding: 8px 12px;
  border-radius: 9px;
  font-family: inherit;
  font-size: 12px;
}

.chat-raw-debug {
  margin-top: 8px;
  border-top: 1px dashed var(--border);
  padding-top: 8px;
}

.chat-raw-debug summary {
  cursor: pointer;
  color: var(--text-muted);
  font-size: 11px;
}

.chat-raw-debug pre {
  margin: 8px 0 0;
  font-size: 11px;
  color: var(--text-muted);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}


================================================
FILE: lib/public/css/cron.css
================================================
.app-content-pane.cron-pane {
  padding: 24px 32px 12px;
  overflow: hidden;
}

.cron-tab-shell {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.cron-tab-header {
  padding: 0 0 16px;
}

.cron-tab-header-content {
  width: 100%;
  max-width: 672px;
  margin-left: auto;
  margin-right: auto;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.cron-tab-main {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
}

.cron-tab-main-content {
  width: 100%;
  max-width: 672px;
  margin-left: auto;
  margin-right: auto;
  padding: 0 0 24px;
  min-height: 100%;
}

.cron-tab-main-content .cron-detail-content {
  padding-top: 8px;
}

@media (max-width: 768px) {
  .app-content-pane.cron-pane {
    padding: 0 14px 12px;
  }

  .cron-tab-header {
    padding: 0 0 12px;
  }
}

.cron-tab-selector-shell {
  position: relative;
  width: min(100%, 320px);
}

.cron-tab-selector-toggle {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.015);
  color: var(--text);
  text-align: left;
  font: inherit;
  padding: 8px 10px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.cron-tab-selector-toggle:hover {
  border-color: rgba(148, 163, 184, 0.45);
}

.cron-tab-selector-toggle.is-open {
  border-color: rgba(99, 235, 255, 0.55);
  background: rgba(99, 235, 255, 0.08);
}

.cron-tab-selector-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cron-tab-selector-caret {
  color: var(--text-dim);
  transition: transform 0.12s ease;
}

.cron-tab-selector-toggle.is-open .cron-tab-selector-caret {
  transform: rotate(180deg);
}

.cron-tab-selector-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  width: min(100vw - 40px, 420px);
  max-height: min(70vh, 620px);
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-sidebar);
  box-shadow: 0 14px 32px rgba(0, 0, 0, 0.45);
  overflow: hidden;
  z-index: 8;
}

.cron-tab-selector-dropdown .cron-list-panel-inner {
  padding: 0 10px 10px;
  max-height: min(70vh, 620px);
  overflow-y: auto;
}

.cron-list-sticky-search {
  position: sticky;
  top: 0;
  z-index: 2;
  padding: 10px 0 8px;
  background: var(--bg-sidebar);
}

.cron-list-search-input {
  width: 100%;
  height: 30px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.02);
  color: var(--text);
  font-size: 12px;
  padding: 0 9px;
  outline: none;
  font-family: inherit;
}

.cron-list-search-input::placeholder {
  color: var(--text-dim);
}

.cron-list-search-input:focus {
  border-color: rgba(99, 235, 255, 0.45);
  box-shadow: 0 0 0 1px rgba(99, 235, 255, 0.18);
}

.cron-list-separator {
  border-top: 1px solid var(--border);
  margin: 8px 2px;
}

.cron-list-items {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cron-list-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cron-list-group + .cron-list-group {
  margin-top: 2px;
}

.cron-list-group-header {
  position: sticky;
  top: 48px;
  z-index: 1;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
  background: var(--bg-sidebar);
  padding: 4px 2px;
}

.cron-list-group-items {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cron-list-item {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.015);
  color: var(--text-muted);
  text-align: left;
  font: inherit;
  padding: 8px 10px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.cron-list-item:hover {
  border-color: rgba(148, 163, 184, 0.45);
  color: var(--text);
}

.cron-list-item.is-selected {
  border-color: rgba(99, 235, 255, 0.55);
  background: rgba(99, 235, 255, 0.08);
  color: var(--text);
}

.cron-list-all {
  margin-bottom: 8px;
}

.cron-list-item-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.cron-list-status-inline {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}

.cron-list-last-run {
  font-size: 11px;
  color: var(--text-muted);
  line-height: 1;
}

.cron-list-item-title {
  font-size: 12px;
  font-weight: 700;
  color: var(--text);
}

.cron-list-item-subtitle {
  font-size: 11px;
  color: var(--text-muted);
}

.cron-list-health-dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
  flex: 0 0 auto;
}

.cron-detail-panel {
  height: auto;
  min-width: 0;
  min-height: 100%;
  overflow: visible;
}

.cron-detail-scroll {
  height: auto;
  overflow: visible;
}

.cron-detail-content {
  padding: 16px 0 0;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 100%;
  width: 100%;
  max-width: 672px;
  margin-left: auto;
  margin-right: auto;
}

.cron-prompt-editor-shell {
  height: 280px;
  min-height: 180px;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  resize: vertical;
  background: rgba(255, 255, 255, 0.01);
}

.cron-prompt-editor-shell .file-viewer-editor-line-num-col {
  width: 44px;
  padding: 16px 10px 112px 0;
}

.cron-calendar-repeating-strip {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 8px;
  background: rgba(255, 255, 255, 0.015);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cron-calendar-repeating-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.cron-calendar-repeating-pill {
  min-width: 220px;
  max-width: 100%;
  border: 1px solid transparent;
  border-radius: 8px;
  padding: 6px 8px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.cron-calendar-legend {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
}

.cron-calendar-title {
  color: var(--card-label-bright);
}

.cron-calendar-legend-label {
  font-size: 11px;
  color: var(--text-dim);
  margin-right: 2px;
}

.cron-calendar-legend-pill {
  font-size: 10px;
  line-height: 1;
  border-radius: 999px;
  padding: 4px 7px;
}

.cron-calendar-grid-wrap {
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: auto;
  background: var(--bg-surface);
}

.cron-calendar-grid-header,
.cron-calendar-grid-row {
  display: grid;
  grid-template-columns: 72px repeat(7, minmax(80px, 1fr));
}

.cron-calendar-day-header {
  font-size: 11px;
  color: var(--text-muted);
  padding: 8px;
  border-left: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.12);
  position: sticky;
  top: 0;
  z-index: 1;
}

.cron-calendar-day-header.is-today {
  background: rgba(99, 235, 255, 0.08);
}

.cron-calendar-hour-cell {
  font-size: 11px;
  color: var(--text-dim);
  padding: 8px;
  border-bottom: 1px solid var(--border);
  border-right: 1px solid var(--border);
  background: rgba(0, 0, 0, 0.15);
  position: sticky;
  left: 0;
  z-index: 2;
}

.cron-calendar-grid-header .cron-calendar-hour-cell {
  top: 0;
  z-index: 3;
}

.cron-calendar-grid-corner {
  display: flex;
  align-items: center;
  justify-content: center;
}

.cron-calendar-grid-wrap {
  position: relative;
}

.cron-calendar-lightbox-panel {
  width: min(96vw, 1200px);
  max-height: 88vh;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--bg-sidebar);
}

.cron-calendar-lightbox-close {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.03);
  color: var(--text-dim);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.cron-calendar-lightbox-close:hover {
  color: var(--text);
  border-color: rgba(148, 163, 184, 0.5);
}

.cron-calendar-lightbox-body {
  min-height: 0;
  overflow: auto;
}

.cron-calendar-grid-row .cron-calendar-hour-cell {
  box-shadow: 1px 0 0 var(--border);
}

.cron-calendar-grid-cell {
  min-height: 44px;
  border-left: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  padding: 5px;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.cron-calendar-grid-cell.is-today {
  background: rgba(99, 235, 255, 0.04);
}

.cron-calendar-now-indicator {
  position: absolute;
  left: 5px;
  right: 5px;
  height: 2px;
  border-radius: 999px;
  background: rgba(248, 113, 113, 0.95);
  box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.18), 0 0 8px rgba(239, 68, 68, 0.55);
  pointer-events: none;
  z-index: 1;
}

.cron-calendar-now-indicator-dot {
  position: absolute;
  left: -3px;
  top: 50%;
  width: 6px;
  height: 6px;
  border-radius: 999px;
  background: rgba(248, 113, 113, 0.98);
  transform: translateY(-50%);
}

.cron-calendar-slot-chip {
  font-size: 11px;
  line-height: 1.2;
  border: 1px solid transparent;
  border-radius: 7px;
  padding: 4px 6px;
  display: inline-flex;
  align-items: center;
  min-height: 20px;
  width: 100%;
  max-width: 100%;
  overflow: hidden;
  position: relative;
  z-index: 2;
}

.cron-calendar-slot-overflow {
  font-size: 10px;
  color: var(--text-dim);
  padding-left: 2px;
}

.cron-calendar-slot-tier-low {
  background: rgba(34, 211, 238, 0.14);
  border-color: rgba(34, 211, 238, 0.38);
  color: #c8f6ff;
}

.cron-calendar-slot-tier-unknown {
  background: rgba(148, 163, 184, 0.08);
  border-color: rgba(148, 163, 184, 0.24);
  color: #c9d2df;
}

.cron-calendar-slot-tier-medium {
  background: rgba(59, 130, 246, 0.14);
  border-color: rgba(59, 130, 246, 0.38);
  color: #d6e6ff;
}

.cron-calendar-slot-tier-high {
  background: rgba(251, 191, 36, 0.14);
  border-color: rgba(251, 191, 36, 0.38);
  color: #ffecc1;
}

.cron-calendar-slot-tier-very-high {
  background: rgba(239, 68, 68, 0.14);
  border-color: rgba(239, 68, 68, 0.38);
  color: #ffd6d6;
}

.cron-calendar-slot-tier-disabled {
  background: rgba(148, 163, 184, 0.08);
  border-color: rgba(148, 163, 184, 0.28);
  color: #98a5b8;
}

.cron-calendar-slot-upcoming {
  opacity: 1;
}

.cron-calendar-slot-past {
  opacity: 0.75;
}

.cron-calendar-slot-ok {
  box-shadow: inset 0 0 0 1px rgba(34, 197, 94, 0.38);
}

.cron-calendar-slot-error {
  box-shadow: inset 0 0 0 1px rgba(239, 68, 68, 0.45);
}

.cron-calendar-slot-skipped {
  box-shadow: inset 0 0 0 1px rgba(250, 204, 21, 0.45);
}

.cron-calendar-compact-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cron-calendar-compact-row {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  border: 1px solid rgba(148, 163, 184, 0.32);
  border-radius: 8px;
  padding: 7px 9px;
  text-align: left;
  font-size: 11px;
  line-height: 1.2;
}

.cron-calendar-compact-main {
  min-width: 0;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.cron-calendar-compact-time {
  font-size: 10px;
  font-family: var(--font-mono, monospace);
  opacity: 0.72;
  white-space: nowrap;
}

.cron-calendar-compact-name {
  color: var(--text);
}

.cron-calendar-compact-estimate {
  font-size: 10px;
  color: var(--text);
  font-weight: 500;
  white-space: nowrap;
  text-align: right;
}

.cron-runs-trend-bars {
  display: grid;
  grid-template-columns: repeat(var(--cron-runs-trend-columns, 7), minmax(0, 1fr));
  gap: 4px;
  align-items: end;
}

.cron-runs-trend-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  width: 100%;
  cursor: pointer;
  transition: opacity 140ms ease, transform 140ms ease;
}

.cron-runs-trend-col.is-dimmed {
  opacity: 0.35;
}

.cron-runs-trend-col.is-selected .cron-runs-trend-track {
  border-color: rgba(148, 163, 184, 0.55);
  box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.2);
}

.cron-runs-trend-col:focus-visible {
  outline: none;
}

.cron-runs-trend-col:focus-visible .cron-runs-trend-track {
  border-color: rgba(148, 163, 184, 0.65);
  box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.24);
}

.cron-runs-trend-track {
  width: 100%;
  height: 120px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: rgba(0, 0, 0, 0.16);
  overflow: hidden;
  display: flex;
  align-items: flex-end;
}

.cron-runs-trend-bar {
  width: 100%;
  min-height: 2px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.cron-runs-trend-segment-ok {
  background: rgba(34, 255, 170, 0.82);
  box-shadow: inset 0 0 0 1px rgba(34, 255, 170, 0.28), 0 0 8px rgba(34, 255, 170, 0.24);
}

.cron-runs-trend-segment-error {
  background: rgba(255, 74, 138, 0.84);
  box-shadow: inset 0 0 0 1px rgba(255, 74, 138, 0.32), 0 0 8px rgba(255, 74, 138, 0.24);
}

.cron-runs-trend-segment-skipped {
  background: rgba(255, 214, 64, 0.82);
  box-shadow: inset 0 0 0 1px rgba(255, 214, 64, 0.28), 0 0 8px rgba(255, 214, 64, 0.22);
}

.cron-runs-trend-label {
  font-size: 10px;
  color: var(--text-dim);
  min-height: 12px;
}

.cron-runs-trend-legend {
  margin-top: 2px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.cron-runs-trend-legend-item {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10px;
  color: var(--text-dim);
}

.cron-runs-trend-legend-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
}

.cron-runs-trend-legend-dot.is-ok {
  background: rgba(34, 255, 170, 0.98);
  box-shadow: 0 0 8px rgba(34, 255, 170, 0.6);
}

.cron-runs-trend-legend-dot.is-error {
  background: rgba(255, 74, 138, 0.98);
  box-shadow: 0 0 8px rgba(255, 74, 138, 0.58);
}

.cron-runs-trend-legend-dot.is-skipped {
  background: rgba(255, 214, 64, 0.98);
  box-shadow: 0 0 8px rgba(255, 214, 64, 0.5);
}


================================================
FILE: lib/public/css/explorer.css
================================================
/* ── Browse/Explorer mode ─────────────────────── */

.sidebar-tabs {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0px 12px 6px;
  background: transparent;
}

.sidebar-tab {
  width: 30px;
  height: 30px;
  padding: 0;
  color: var(--text-muted);
  border: 0;
  border-radius: 6px;
  background: transparent;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: color 0.12s, background 0.12s, box-shadow 0.12s, transform 0.12s;
}

.sidebar-tab:hover {
  color: #a9eefb;
}

.sidebar-tab.active {
  color: #b9f5ff;
  background: rgba(99, 235, 255, 0.05);
  /* box-shadow: 0 0 8px rgba(99, 235, 255, 0.16); */
}

.sidebar-tab-icon {
  width: 17px;
  height: 17px;
  display: block;
  opacity: 0.92;
}

.sidebar-tab:hover .sidebar-tab-icon,
.sidebar-tab.active .sidebar-tab-icon {
  opacity: 1;
}

.sidebar-browse-layout {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.sidebar-browse-panel {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

.sidebar-browse-resizer {
  height: 6px;
  cursor: row-resize;
  position: relative;
}

.sidebar-browse-resizer::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 2px;
  height: 2px;
  background: transparent;
  transition: background 0.12s;
}

.sidebar-browse-resizer:hover::before,
.sidebar-browse-resizer.is-resizing::before {
  background: rgba(99, 235, 255, 0.55);
}

.sidebar-browse-bottom {
  flex: 0 0 auto;
  min-height: 0;
  overflow: hidden;
  padding-top: 0;
}

.sidebar-browse-bottom-inner {
  min-height: 120px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

/* ── Sidebar agents list ─────────────────────── */

.sidebar-agents-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 12px 16px 4px;
}

.sidebar-agents-label {
  padding: 0;
}

.sidebar-agents-add-button {
  border: none;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  padding: 6px;
  margin: -6px;
  cursor: pointer;
  opacity: 0.9;
  transition: color 0.1s, opacity 0.1s;
}

.sidebar-agents-add-icon {
  width: 16px;
  height: 16px;
  display: block;
}

.sidebar-agents-add-button:hover {
  color: var(--text);
  opacity: 1;
}

.sidebar-agents-list {
  flex: 0 0 auto;
  overflow: visible;
  padding: 2px 0 0;
}

.sidebar-agent-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 16px 6px 16px;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.4;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
  position: relative;
  user-select: none;
  border: none;
  background: transparent;
  width: 100%;
  text-align: left;
  font: inherit;
}

.sidebar-agent-item:hover {
  background: var(--bg-hover);
  color: var(--text);
}

.sidebar-agent-item.active {
  background: var(--bg-active);
  color: var(--accent);
}

.sidebar-agent-item.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--accent);
}

.sidebar-agent-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
}

.sidebar-agent-emoji {
  flex: 0 0 auto;
  min-width: 14px;
  max-width: 1.5em;
  line-height: 1;
  font-size: 14px;
  text-align: center;
  overflow: hidden;
}

.sidebar-agent-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar-agents-add {
  margin: 0;
  padding: 16px 14px 8px 20px;
  border-top: 1px solid var(--border);
  margin-top: 8px;
}

/* ── Sidebar chat sessions ───────────────────── */

.sidebar-chat-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px 4px;
}

.sidebar-chat-label {
  padding: 0;
}

.sidebar-chat-sessions-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 2px 0 8px;
}

.sidebar-chat-agent-group {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.sidebar-chat-agent-toggle {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  margin: 0;
  padding: 6px 12px 4px 10px;
  border: none;
  background: transparent;
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-align: left;
  cursor: pointer;
  font-family: inherit;
  user-select: none;
}

.sidebar-chat-agent-toggle:hover {
  color: var(--text);
}

.sidebar-chat-agent-chevron {
  display: inline-flex;
  flex-shrink: 0;
  transition: transform 0.15s ease;
  color: var(--text-dim);
}

.sidebar-chat-agent-chevron.is-collapsed {
  transform: rotate(-90deg);
}

.sidebar-chat-agent-chevron-icon {
  width: 12px;
  height: 12px;
  display: block;
}

.sidebar-chat-agent-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar-chat-agent-sessions {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 0 0 4px;
}

.sidebar-chat-session-channel-icon {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  border-radius: 2px;
  object-fit: contain;
  opacity: 0.92;
}

.sidebar-chat-session-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 16px 6px 24px;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.4;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
  position: relative;
  user-select: none;
  border: none;
  background: transparent;
  width: 100%;
  text-align: left;
  font-family: inherit;
}

.sidebar-chat-session-item:hover {
  background: var(--bg-hover);
  color: var(--text);
}

.sidebar-chat-session-item.active {
  background: var(--bg-active);
  color: var(--accent);
}

.sidebar-chat-session-item.active::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--accent);
}

.sidebar-chat-session-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar-chat-empty {
  padding: 10px 16px 10px 24px;
  color: var(--text-dim);
  font-size: 12px;
}

.file-tree-wrap {
  width: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
  flex: 1;
  padding: 6px 0 0;
}

.file-tree-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding-bottom: 8px;
}

.file-tree-wrap-loading {
  min-height: 100%;
  display: flex;
}

.file-tree-search {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 0 8px 6px;
}

.file-tree-search-actions {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex-shrink: 0;
}

.file-tree-search-input {
  width: 100%;
  height: 28px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.02);
  color: var(--text);
  font-size: 12px;
  padding: 0 9px;
  outline: none;
  font-family: inherit;
}

.file-tree-search-input::placeholder {
  color: var(--text-dim);
}

.file-tree-search-input:focus {
  border-color: rgba(99, 235, 255, 0.45);
  box-shadow: 0 0 0 1px rgba(99, 235, 255, 0.18);
}

.file-tree-scroll::-webkit-scrollbar {
  width: 6px;
}

.file-tree-scroll::-webkit-scrollbar-track {
  background: transparent;
}

.file-tree-scroll::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 3px;
}

.file-tree {
  list-style: none;
}

.tree-item {
  position: relative;
}

.tree-item > a {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 10px 2px 18px;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 13px;
  font-weight: 400;
  transition: background 0.1s, color 0.1s;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  user-select: none;
}

.tree-item > a:hover {
  background: var(--bg-hover);
  color: var(--text);
}

.tree-item > a.active {
  background: var(--bg-active);
  color: var(--accent);
}

.tree-item > a.soft-active:not(.active) {
  background: rgba(99, 235, 255, 0.06);
  color: var(--text);
}

.tree-item > a.active::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--accent);
}

.tree-folder {
  padding: 2px 10px 2px 12px;
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--text);
  font-weight: 400;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  overflow: hidden;
}

.tree-folder:hover {
  background: var(--bg-hover);
}

.tree-folder.active {
  background: var(--bg-active);
  color: var(--accent);
}

.tree-folder.active .arrow {
  color: var(--accent);
}

.tree-folder-toggle {
  border: 0;
  margin: 0;
  padding: 0;
  background: transparent;
  color: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex: 0 0 auto;
}

.arrow {
  font-size: 10px;
  transition: transform 0.15s;
  color: var(--text-dim);
  flex-shrink: 0;
}

.tree-folder.collapsed .arrow {
  transform: rotate(-90deg);
}

.file-icon {
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  display: block;
  color: var(--text-dim);
}

.file-icon-md {
  color: var(--accent);
}

.file-icon-js {
  color: #f4d03f;
}

.file-icon-json {
  color: #9b7bff;
}

.file-icon-css {
  color: #7ec8ff;
}

.file-icon-html {
  color: #ff9d57;
}

.file-icon-image {
  color: #ff7ac6;
}

.file-icon-audio {
  color: #f5a6ff;
}

.file-icon-shell {
  color: #71f8a7;
}

.file-icon-db {
  color: #67b3ff;
}

.file-icon-generic {
  color: var(--text-muted);
}

.tree-label {
  overflow: hidden;
  text-overflow: ellipsis;
}

.tree-draft-dot {
  flex: 0 0 auto;
  margin-left: auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #2de2ff;
  box-shadow: 0 0 6px rgba(45, 226, 255, 0.75);
}

.tree-lock-icon {
  flex: 0 0 auto;
  margin-left: auto;
  width: 11px;
  height: 11px;
  color: var(--text-dim);
}

.tree-folder-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: color 0.1s, background 0.1s;
}

.tree-folder-action:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.08);
}

.tree-folder-action-icon {
  width: 16px;
  height: 16px;
  display: block;
}

.tree-create-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 10px 2px 18px;
}

.tree-create-input {
  flex: 1;
  min-width: 0;
  height: 22px;
  border-radius: 4px;
  border: 1px solid rgba(99, 235, 255, 0.45);
  background: rgba(0, 0, 0, 0.3);
  color: var(--text);
  font-size: 12px;
  font-family: inherit;
  padding: 0 6px;
  outline: none;
}

.tree-create-input::placeholder {
  color: var(--text-dim);
}

.tree-create-input:focus {
  box-shadow: 0 0 0 1px rgba(99, 235, 255, 0.18);
}

.tree-create-icon {
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  display: block;
  color: var(--text-dim);
}

.tree-context-menu {
  position: fixed;
  z-index: 100;
  min-width: 160px;
  padding: 4px 0;
  background: var(--bg-sidebar);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}

.tree-context-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 12px;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
  text-align: left;
  transition: background 0.1s, color 0.1s;
}

.tree-context-menu-item:hover {
  background: var(--bg-hover);
  color: var(--text);
}

.tree-context-menu-item.is-disabled {
  color: var(--text-dim);
  cursor: default;
  pointer-events: none;
}

.tree-context-menu-item.is-danger {
  color: #f87171;
}

.tree-context-menu-item.is-danger:hover {
  background: rgba(239, 68, 68, 0.1);
  color: #fca5a5;
}

.tree-context-menu-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

.tree-context-menu-sep {
  height: 1px;
  margin: 4px 8px;
  background: var(--border);
}

.tree-item.is-dragging {
  opacity: 0.4;
}

.tree-folder.is-drop-target {
  outline: 1px dashed var(--accent);
  outline-offset: -1px;
  background: rgba(99, 235, 255, 0.06);
}

.tree-children {
  list-style: none;
}

.tree-children.hidden {
  display: none;
}

.file-tree-state {
  padding: 10px 14px;
  font-size: 12px;
  color: var(--text-muted);
}

.file-tree-state-loading {
  width: 100%;
  flex: 1 1 auto;
  min-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.file-tree-state-error {
  color: #f87171;
}

.file-viewer {
  width: 100%;
  min-height: 100%;
  height: calc(100vh - 24px);
  display: flex;
  flex-direction: column;
  background: transparent;
}

.file-viewer-tabbar {
  position: sticky;
  top: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  background: var(--bg-sidebar);
  border-bottom: 1px solid var(--border);
  height: 40px;
}

.file-viewer-protected-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  min-height: 36px;
  padding: 4px 0;
  height: 42px;
  background: rgba(234, 179, 8, 0.08);
}

.file-viewer-protected-banner.is-locked {
  background: rgba(220, 38, 38, 0.16);
}

.file-viewer-protected-banner-icon {
  width: 14px;
  height: 14px;
  color: #fca5a5;
  flex-shrink: 0;
}

.file-viewer-protected-banner.is-locked .file-viewer-protected-banner-text {
  color: #fecaca;
}

.file-viewer-protected-banner-text {
  font-size: 12px;
  color: #f7cc5e;
  text-align: center;
}

.file-viewer-protected-banner-unlocked {
  font-size: 11px;
  color: #fde68a;
  opacity: 0.95;
  letter-spacing: 0.01em;
}

.file-viewer-diff-banner {
  background: rgba(59, 130, 246, 0.12);
}

.file-viewer-diff-banner .file-viewer-protected-banner-text,
.file-viewer-diff-banner {
  color: #bfdbfe;
}

.file-viewer-tabbar-spacer {
  flex: 1;
}

.file-viewer-preview-pill {
  margin-right: 8px;
  font-size: 11px;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 3px 8px;
  line-height: 1;
}

.file-viewer-tab {
  display: flex;
  align-items: center;
  gap: 6px;
  height: 40px;
  padding: 0 16px;
  font-size: 12px;
  line-height: 1;
  color: var(--text-muted);
  border-right: 1px solid var(--border);
  white-space: nowrap;
}

.file-viewer-dirty-dot {
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: 0 0 10px rgba(99, 235, 255, 0.55);
  margin-left: 2px;
  flex-shrink: 0;
}

.file-viewer-tab.active {
  color: var(--text);
  background: var(--bg);
  border-bottom: 1px solid var(--accent);
  margin-bottom: -1px;
}

.file-viewer-breadcrumb {
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.file-viewer-breadcrumb-item {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--text-muted);
}

.file-viewer-breadcrumb-item .is-current {
  color: var(--text);
}

.file-viewer-sep {
  color: var(--text-dim);
}

.frontmatter-box {
  margin-top: 16px;
  margin-bottom: 4px;
  margin-right: 20px;
  margin-left: 36px;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: rgba(99, 235, 255, 0.025);
}

.frontmatter-title {
  width: 100%;
  border: 0;
  text-align: left;
  display: flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  background: rgba(13, 17, 23, 0.55);
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  padding: 4px 10px;
}

.frontmatter-title:hover {
  background: rgba(13, 17, 23, 0.75);
}

.frontmatter-chevron {
  width: 12px;
  height: 12px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  transition: transform 0.15s ease;
}

.frontmatter-chevron.open {
  transform: rotate(90deg);
}

.frontmatter-chevron svg {
  width: 12px;
  height: 12px;
  display: block;
}

.frontmatter-chevron path {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.frontmatter-grid {
  display: grid;
}

.frontmatter-row {
  display: grid;
  grid-template-columns: 160px 1fr;
  border-top: 1px solid rgba(255, 255, 255, 0.05);
}

.frontmatter-row:first-child {
  border-top: 0;
}

.frontmatter-key {
  padding: 6px 10px;
  color: var(--keyword);
  border-right: 1px solid rgba(255, 255, 255, 0.05);
  word-break: break-word;
  opacity: 0.85;
}

.frontmatter-value {
  padding: 6px 10px;
  color: var(--string);
  white-space: pre-wrap;
  word-break: break-word;
  opacity: 0.88;
}

.frontmatter-value-pre {
  margin: 0;
  font-family: inherit;
  font-size: 12px;
}

.file-viewer-save-action {
  margin-right: 10px;
}

.file-viewer-save-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}

.file-viewer-icon-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin-right: 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.02);
  color: var(--text-muted);
  transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
}

.file-viewer-icon-action:hover {
  border-color: rgba(148, 163, 184, 0.45);
  color: var(--text);
  background: rgba(148, 163, 184, 0.08);
}

.file-viewer-icon-action.is-disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.file-viewer-icon-action-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}

.file-viewer-view-toggle {
  display: flex;
  align-items: center;
  margin-right: 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.02);
  height: 28px;
}

.file-viewer-view-toggle-button {
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 12px;
  text-transform: lowercase;
  letter-spacing: 0.03em;
  height: 100%;
  line-height: 1;
  padding: 0 10px;
  cursor: pointer;
}

.file-viewer-view-toggle-button:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.03);
}

.file-viewer-view-toggle-button.active {
  color: var(--accent);
  background: var(--bg-active);
}

.file-viewer-editor-shell {
  width: 100%;
  min-height: 0;
  height: 100%;
  flex: 1;
  display: flex;
  align-items: stretch;
}

.file-viewer-diff-shell {
  width: 100%;
  min-height: 0;
  height: 100%;
  overflow: auto;
  padding: 8px 0;
}

.file-viewer-diff-pre {
  margin: 0;
  padding: 0 12px 12px;
  font-family: inherit;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text-muted);
}

.file-viewer-diff-line {
  white-space: pre-wrap;
  word-break: break-word;
  padding: 1px 8px;
  border-radius: 4px;
}

.file-viewer-diff-line.is-added {
  color: #86efac;
  background: rgba(34, 197, 94, 0.1);
}

.file-viewer-diff-line.is-removed {
  color: #fca5a5;
  background: rgba(239, 68, 68, 0.1);
}

.file-viewer-diff-line.is-hunk {
  color: #93c5fd;
}

.file-viewer-diff-line.is-header {
  color: var(--text-dim);
}

.file-viewer-editor-line-num-col {
  width: 56px;
  flex-shrink: 0;
  overflow: hidden;
  padding: 16px 16px 112px 0;
  text-align: right;
}

.file-viewer-editor-line-num {
  min-height: 22px;
  line-height: 22px;
  color: var(--text-dim);
  font-size: 12px;
  user-select: none;
  display: flex;
  justify-content: flex-end;
  align-items: flex-start;
}

.line-highlight-flash {
  color: #4ade80 !important;
}

.file-viewer-editor {
  width: 100%;
  min-height: 0;
  height: 100%;
  flex: 1;
  border: 0;
  outline: none;
  resize: none;
  overflow-y: auto;
  background: transparent;
  color: var(--text);
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  line-height: 22px;
  padding: 16px 20px 112px 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.file-viewer-editor-stack {
  position: relative;
  flex: 1;
  min-height: 0;
}

.file-viewer-editor-highlight {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  background: transparent;
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  line-height: 22px;
  color: var(--text);
  padding: 16px 20px 112px 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.file-viewer-editor-highlight-line {
  min-height: 22px;
}

.file-viewer-editor-highlight-line-content {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.file-viewer-editor-overlay {
  position: absolute;
  inset: 0;
  color: transparent;
  caret-color: var(--text);
  -webkit-text-fill-color: transparent;
}

.file-viewer-editor-overlay::selection {
  background: rgba(99, 235, 255, 0.25);
}

.file-viewer-preview {
  flex: 1;
  overflow-y: auto;
  padding: 0 20px 112px 36px;
  line-height: 1.75;
  background: transparent;
}

.file-viewer-pane-hidden {
  display: none;
}

.file-viewer-preview h1,
.file-viewer-preview h2,
.file-viewer-preview h3,
.file-viewer-preview h4,
.file-viewer-preview h5,
.file-viewer-preview h6 {
  color: var(--accent);
  margin: 18px 0 10px;
  font-weight: 600;
}

.file-viewer-preview h1 {
  font-size: 2em;
}

.file-viewer-preview h2 {
  font-size: 1.5em;
}

.file-viewer-preview h3 {
  font-size: 1.25em;
}

.file-viewer-preview h4 {
  font-size: 1.1em;
}

.file-viewer-preview h5 {
  font-size: 1em;
}

.file-viewer-preview h6 {
  font-size: 0.9em;
}

.file-viewer-preview p,
.file-viewer-preview ul,
.file-viewer-preview ol,
.file-viewer-preview blockquote,
.file-viewer-preview pre,
.file-viewer-preview table {
  margin: 10px 0;
}

.file-viewer-preview ul,
.file-viewer-preview ol {
  padding-left: 20px;
}

.file-viewer-preview ul {
  list-style: disc;
}

.file-viewer-preview ol {
  list-style: decimal;
}

.file-viewer-preview a {
  color: var(--accent);
}

.file-viewer-preview code {
  color: var(--string);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 1px 6px;
}

.file-viewer-preview pre {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 12px;
  overflow-x: auto;
}

.file-viewer-preview pre code {
  background: transparent;
  border: 0;
  padding: 0;
}

.file-viewer-preview blockquote {
  border-left: 2px solid var(--accent-dim);
  padding-left: 12px;
  color: var(--comment);
}

.file-viewer-preview table {
  width: 100%;
  border-collapse: collapse;
}

.file-viewer-preview th,
.file-viewer-preview td {
  border: 1px solid var(--border);
  padding: 6px 8px;
  text-align: left;
}

.file-viewer-preview th {
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
}

.release-notes-preview {
  padding: 8px 12px 24px 14px;
}

.release-notes-preview > :first-child {
  margin-top: 0;
}

.release-notes-preview > :last-child {
  margin-bottom: 0;
}

.file-viewer-editor-highlight-line-content .hl-comment {
  color: var(--comment);
  font-style: italic;
}

.file-viewer-editor-highlight-line-content .hl-heading {
  color: var(--accent);
  font-weight: 700;
}

.file-viewer-editor-highlight-line-content .hl-string {
  color: var(--string);
}

.file-viewer-editor-highlight-line-content .hl-bullet {
  color: var(--orange);
}

.file-viewer-editor-highlight-line-content .hl-bold {
  color: var(--text);
  font-weight: 700;
}

.file-viewer-editor-highlight-line-content .hl-link {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-style: dotted;
}

.file-viewer-editor-highlight-line-content .hl-meta {
  color: var(--text-dim);
}

.file-viewer-editor-highlight-line-content .hl-key {
  color: var(--keyword);
}

.file-viewer-editor-highlight-line-content .hl-keyword {
  color: var(--keyword);
}

.file-viewer-editor-highlight-line-content .hl-tag {
  color: var(--accent);
}

.file-viewer-editor-highlight-line-content .hl-attr {
  color: var(--keyword);
}

.file-viewer-editor-highlight-line-content .hl-entity {
  color: var(--number);
}

.file-viewer-editor-highlight-line-content .hl-number {
  color: var(--number);
}

.file-viewer-editor-highlight-line-content .hl-boolean,
.file-viewer-editor-highlight-line-content .hl-null {
  color: var(--orange);
}

.file-viewer-editor-highlight-line-content .hl-punc {
  color: #5f6674;
}

.file-viewer-state {
  padding: 20px;
  color: var(--text-muted);
  font-size: 12px;
}

.file-viewer-loading-shell {
  flex: 1 1 auto;
  min-height: 140px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-muted);
}

.file-viewer-image-shell {
  flex: 1 1 auto;
  min-height: 0;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  overflow: auto;
}

.file-viewer-image {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.02);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
}

.file-viewer-audio-shell {
  flex: 1 1 auto;
  min-height: 0;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  overflow: auto;
}

.file-viewer-audio-player {
  width: min(640px, 100%);
}

.file-viewer-sqlite-shell {
  flex: 1 1 auto;
  min-height: 0;
  height: 100%;
  overflow: auto;
  padding: 14px 16px 22px;
}

.file-viewer-sqlite-header {
  font-size: 12px;
  color: var(--text-dim);
  margin-bottom: 10px;
}

.file-viewer-sqlite-footer {
  margin-top: 10px;
  text-align: center;
  font-size: 12px;
  color: var(--text-dim);
}

.file-viewer-sqlite-list {
  flex: 0 0 240px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.file-viewer-sqlite-layout {
  display: flex;
  gap: 12px;
  align-items: stretch;
  min-height: 0;
}

.file-viewer-sqlite-card {
  width: 100%;
  text-align: left;
  cursor: pointer;
  display: flex;
  align-items: center;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 7px 10px;
  background: rgba(255, 255, 255, 0.02);
  font: inherit;
}

.file-viewer-sqlite-card:hover {
  background: rgba(255, 255, 255, 0.04);
}

.file-viewer-sqlite-card.is-active {
  border-color: rgba(99, 235, 255, 0.45);
  background: rgba(99, 235, 255, 0.08);
}

.file-viewer-sqlite-title {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 12px;
  color: var(--text);
  margin-bottom: 0;
}

.file-viewer-sqlite-type {
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.file-viewer-sqlite-table-shell {
  flex: 1;
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px;
  background: rgba(255, 255, 255, 0.015);
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.file-viewer-sqlite-table-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

.file-viewer-sqlite-table-name {
  font-size: 12px;
  color: var(--text);
  font-weight: 600;
}

.file-viewer-sqlite-table-nav {
  display: inline-flex;
  gap: 6px;
}

.file-viewer-sqlite-table-meta {
  margin-top: 4px;
  margin-bottom: 8px;
  font-size: 11px;
  color: var(--text-dim);
}

.file-viewer-sqlite-table-wrap {
  min-height: 0;
  overflow: auto;
}

.file-viewer-sqlite-table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}

.file-viewer-sqlite-table th,
.file-viewer-sqlite-table td {
  border: 1px solid var(--border);
  padding: 6px 8px;
  font-size: 11px;
  color: var(--text-muted);
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.file-viewer-sqlite-table th {
  color: var(--text);
  background: rgba(255, 255, 255, 0.04);
}

.file-viewer-sqlite-table-empty {
  color: var(--text-dim);
}

.file-viewer-state-error {
  color: #f87171;
}

.file-viewer-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: calc(100vh - 120px);
  color: var(--text-dim);
  text-align: center;
  padding: 48px;
  gap: 10px;
}

.file-viewer-empty-mark {
  font-size: 26px;
  font-weight: 500;
  letter-spacing: 0.08em;
  color: var(--accent);
  text-shadow:
    0 0 12px rgba(99, 235, 255, 0.38),
    0 0 22px rgba(99, 235, 255, 0.2);
}

.file-viewer-empty-title {
  font-size: 13px;
  font-weight: 400;
  color: var(--text);
  letter-spacing: 0.01em;
}

.sidebar-git-panel {
  padding: 0;
  margin: 0;
  font-size: 11px;
  display: flex;
  flex-direction: column;
  gap: 0;
  flex: 1 1 auto;
  min-height: 0;
}

.sidebar-git-loading {
  min-height: 58px;
  display: flex;
  align-items: center;
  justify-content: center;
}

.sidebar-git-panel-error {
  color: #f87171;
}

.sidebar-git-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-shrink: 0;
  min-height: 28px;
  padding: 0 12px;
  border-top: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.018);
}

.sidebar-git-bar:first-child {
  border-bottom: 1px double var(--border);
}

.sidebar-git-bar-secondary {
  margin-top: 0;
  border-top: 0;
  border-bottom: 0;
  background: rgba(255, 255, 255, 0.012);
}

.sidebar-git-bar-main {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.sidebar-git-link {
  color: inherit;
  text-decoration: none;
}

.sidebar-git-link:hover .sidebar-git-repo-name {
  color: var(--accent);
}

.sidebar-git-bar-icon {
  width: 14px;
  height: 14px;
  color: var(--text-muted);
  flex-shrink: 0;
}

.sidebar-git-repo-name {
  color: var(--text);
  font-size: 11px;
  letter-spacing: 0.03em;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sidebar-git-branch {
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sidebar-git-sync-status {
  font-size: 12px;
  font-weight: 600;
  line-height: 1;
}

.sidebar-git-sync-status.is-up-to-date {
  color: #71f8a7;
}

.sidebar-git-sync-status.is-ahead,
.sidebar-git-sync-status.is-diverged {
  color: #f3a86a;
}

.sidebar-git-sync-status.is-behind {
  color: #93c5fd;
}

.sidebar-git-sync-status.is-no-upstream,
.sidebar-git-sync-status.is-upstream-gone {
  color: var(--text-dim);
}

.sidebar-git-meta {
  color: var(--text-muted);
}

.sidebar-git-changes-label {
  padding: 7px 10px 4px;
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.sidebar-git-changes-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 0 6px;
}

.sidebar-git-change-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 22px;
  padding: 2px 6px;
  border-radius: 6px;
  line-height: 1.25;
}

.sidebar-git-change-row.is-clickable {
  cursor: pointer;
}

.sidebar-git-change-row.is-clickable:hover {
  background: rgba(255, 255, 255, 0.04);
}

.sidebar-git-change-path {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--text-muted);
  font-weight: 400;
}

.sidebar-git-change-meta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  font-size: 10px;
}

.sidebar-git-change-plus {
  color: #71f8a7;
}

.sidebar-git-change-minus {
  color: #f3a86a;
}

.sidebar-git-change-status {
  font-size: 10px;
  letter-spacing: 0.06em;
}

.sidebar-git-change-row.is-untracked .sidebar-git-change-status {
  color: #71f8a7;
}

.sidebar-git-change-row.is-modified .sidebar-git-change-status {
  color: #63ebff;
}

.sidebar-git-change-row.is-deleted .sidebar-git-change-status {
  color: #f87171;
}

.sidebar-git-change-row.is-deleted .sidebar-git-change-path {
  text-decoration: line-through;
  text-decoration-thickness: 1px;
}

.sidebar-git-scroll {
  overflow-y: auto;
  min-height: 0;
  flex: 1 1 auto;
}

.sidebar-git-actions {
  padding: 8px 10px 6px;
}

.sidebar-git-sync-button {
  width: 100%;
}

.sidebar-git-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 6px 10px 0;
}

.sidebar-git-list li {
  display: flex;
  align-items: baseline;
  flex: 0 0 auto;
  gap: 6px;
  color: var(--text-muted);
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sidebar-git-commit-link {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  color: inherit;
  text-decoration: none;
  min-width: 0;
  line-height: 1.4;
}

.sidebar-git-commit-link:hover {
  color: var(--text);
}

.sidebar-git-commit-link:hover .sidebar-git-hash {
  color: var(--accent);
}

.sidebar-git-hash {
  color: var(--accent);
  flex-shrink: 0;
}

@media (max-width: 768px) {
  .sidebar-browse-resizer {
    display: none;
  }

}

/* ── Light theme overrides ─────────────────────── */

[data-theme="light"] .sidebar-tab {
  color: var(--text-muted);
}

[data-theme="light"] .sidebar-tab:hover {
  color: var(--text-bright);
}

[data-theme="light"] .sidebar-tab.active {
  color: #0e7490;
  background: rgba(8, 145, 178, 0.1);
}

[data-theme="light"] .file-viewer-protected-banner {
  background: rgba(234, 179, 8, 0.12);
}

[data-theme="light"] .file-viewer-protected-banner-text {
  color: #92400e;
}

[data-theme="light"] .file-viewer-protected-banner-unlocked {
  color: #a16207;
}

[data-theme="light"] .file-viewer-protected-banner.is-locked {
  background: rgba(220, 38, 38, 0.1);
}

[data-theme="light"] .file-viewer-protected-banner.is-locked .file-viewer-protected-banner-text {
  color: #b91c1c;
}

[data-theme="light"] .file-viewer-protected-banner.is-locked .file-viewer-protected-banner-icon {
  color: #dc2626;
}

[data-theme="light"] .file-viewer-diff-banner {
  background: rgba(59, 130, 246, 0.08);
}

[data-theme="light"] .file-viewer-diff-banner .file-viewer-protected-banner-text,
[data-theme="light"] .file-viewer-diff-banner {
  color: #1d4ed8;
}


================================================
FILE: lib/public/css/shell.css
================================================
/* ── App shell grid ─────────────────────────────── */

.app-shell {
  --sidebar-width: 220px;
  display: grid;
  grid-template-columns: var(--sidebar-width) 0px minmax(0, 1fr);
  grid-template-rows: auto 1fr 24px;
  height: 100vh;
  position: relative;
  z-index: 1;
}

.global-restart-banner {
  position: fixed;
  left: 50%;
  bottom: 52px;
  transform: translateX(-50%);
  width: auto;
  max-width: calc(100vw - 32px);
  z-index: 40;
  pointer-events: none;
}

.global-restart-banner__content {
  border: 1px solid rgba(234, 179, 8, 0.35);
  background: rgba(43, 32, 6, 0.95);
  box-shadow: 0 18px 46px rgba(0, 0, 0, 0.42);
  border-radius: 14px;
  padding: 10px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  pointer-events: auto;
}

.global-restart-banner__text {
  font-size: 12px;
  color: #fde68a;
  line-height: 1.4;
}

.global-restart-banner__button {
  flex-shrink: 0;
}

.global-restart-banner__actions {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.global-restart-banner__dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px;
  color: #fde68a;
  opacity: 0.85;
}

.global-restart-banner__dismiss:hover {
  opacity: 1;
}

.app-content {
  grid-column: 3;
  grid-row: 2;
  overflow: hidden;
  position: relative;
  z-index: 1;
}

.app-content-pane {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  padding: 24px 32px;
}

.app-content-pane.browse-pane {
  padding: 0;
}

/* ── Fixed-header pane shell ────────────────────── */

.app-content-pane.ac-fixed-header-pane {
  overflow: hidden;
  padding-left: 0;
  padding-right: 0;
  padding-bottom: 0;
}

.ac-pane-shell {
  height: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.ac-pane-header {
  padding: 16px 32px 16px;
}

.ac-pane-header-content {
  width: 100%;
  max-width: 672px;
  margin-left: auto;
  margin-right: auto;
}

.ac-pane-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 0 32px;
}

.ac-pane-body-content {
  width: 100%;
  max-width: 672px;
  margin-left: auto;
  margin-right: auto;
  padding-bottom: 24px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

@media (max-width: 768px) {
  .ac-pane-header {
    padding: 16px 14px 12px;
  }

  .ac-pane-body {
    padding: 0 14px;
  }
}

/* ── Sidebar ───────────────────────────────────── */

.app-sidebar {
  grid-column: 1;
  grid-row: 2;
  background:
    linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%),
    linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.16) 100%),
    var(--bg-sidebar);
  border-right: 1px solid var(--border);
  overflow-y: auto;
  display: flex;
  flex-direction: column;
}

.sidebar-resizer {
  grid-column: 2;
  grid-row: 2;
  cursor: col-resize;
  position: relative;
  width: 6px;
  margin-left: -3px;
  z-index: 4;
}

.sidebar-resizer::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 6px;
  background: transparent;
}

.sidebar-resizer::after {
  content: "";
  position: absolute;
  left: 2px;
  top: 0;
  bottom: 0;
  width: 2px;
  background: transparent;
  transition: background 0.12s;
}

.sidebar-resizer:hover::after,
.sidebar-resizer.is-resizing::after {
  background: rgba(99, 235, 255, 0.55);
}

.sidebar-brand {
  padding: 16px;
  font-size: 14px;
  letter-spacing: 0.03em;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  gap: 8px;
}

.sidebar-label {
  padding: 12px 16px 6px;
  font-size: 11px;
  font-weight: 700;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  user-select: none;
}

.sidebar-nav { list-style: none; }

.sidebar-nav a {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 16px 6px 16px;
  color: var(--text-muted);
  text-decoration: none;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
  position: relative;
  user-select: none;
}

.sidebar-nav a:hover { background: var(--bg-hover); color: var(--text); }
.sidebar-nav a.active { background: var(--bg-active); color: var(--accent); }

.sidebar-nav a.active::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--accent);
}

.sidebar-nav-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
}

/* ── Sidebar footer (update banner) ────────────── */

.sidebar-footer {
  margin-top: auto;
  font-size: 11px;
  color: var(--text-dim);
}

.sidebar-footer:empty { display: none; }

.sidebar-footer:not(:empty) {
  padding: 12px 16px;
  border-top: 1px solid var(--border);
}

.sidebar-update-btn {
  width: 100%;
  font-size: 11px;
  padding: 5px 10px;
  border-radius: 6px;
  border: 1px solid rgba(227, 179, 65, 0.2);
  color: #e3b341;
  background: rgba(227, 179, 65, 0.08);
  cursor: pointer;
  font-family: inherit;
  white-space: nowrap;
  text-align: center;
  transition: background 0.15s, border-color 0.15s;
}

.sidebar-update-btn:hover { background: rgba(227, 179, 65, 0.14); border-color: rgba(227, 179, 65, 0.35); }
.sidebar-update-btn:disabled { opacity: 0.5; cursor: not-allowed; }

/* ── Brand dropdown menu ───────────────────────── */

.brand-menu {
  position: relative;
  margin-left: auto;
}

.brand-menu-trigger {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: none;
  border-radius: 6px;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  transition: background 0.1s, color 0.1s;
}
.brand-menu-trigger:hover { background: var(--bg-hover); color: var(--text-muted); }

.brand-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  min-width: max-content;
  width: max-content;
  max-width: min(280px, calc(100vw - 24px));
  background: var(--bg-sidebar);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  padding: 4px;
  z-index: 50;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

.brand-dropdown a,
.brand-dropdown-item {
  display: block;
  width: 100%;
  padding: 6px 10px;
  font: inherit;
  font-size: 12px;
  color: var(--text-muted);
  text-decoration: none;
  border-radius: 5px;
  transition: background 0.1s, color 0.1s;
  background: transparent;
  border: none;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}
.brand-dropdown a:hover,
.brand-dropdown-item:hover { background: var(--bg-hover); color: var(--text); }

/* ── Statusbar ─────────────────────────────────── */

.app-statusbar {
  grid-row: 3;
  grid-column: 1 / -1;
  background: var(--bg-sidebar);
  border-top: 1px solid var(--border-strong);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 12px;
  font-size: 11px;
  color: var(--text-dim);
}

.app-statusbar a { color: var(--text-muted); text-decoration: none; }
.app-statusbar a:hover { color: var(--accent); }

.statusbar-left, .statusbar-right { display: flex; align-items: center; gap: 16px; margin-left: 2px; }

.mobile-topbar {
  display: none;
}

.mobile-topbar-menu {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  background: var(--bg-hover);
  color: var(--text);
  cursor: pointer;
}
.mobile-topbar-menu:hover { background: var(--bg-active); }

.mobile-topbar-title {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  text-align: center;
  font-size: 14px;
  letter-spacing: 0.03em;
  color: var(--text-muted);
}

.mobile-sidebar-overlay {
  display: none;
}

/* ── Responsive ────────────────────────────────── */

@media (max-width: 768px) {
  .app-shell {
    --sidebar-width: 0px !important;
    grid-template-columns: 1fr;
    grid-template-rows: auto 1fr 24px;
  }
  .global-restart-banner {
    max-width: calc(100vw - 20px);
    bottom: 44px;
  }
  .global-restart-banner__content {
    align-items: stretch;
    flex-direction: column;
    gap: 8px;
  }
  .global-restart-banner__text {
    text-align: left;
  }
  .global-restart-banner__button {
    position: static;
    transform: none;
  }
  .global-restart-banner__actions {
    width: 100%;
    justify-content: flex-end;
  }
  .app-content {
    grid-column: 1;
    grid-row: 2;
  }
  .app-content-pane {
    padding: 0 14px 12px;
    top: 52px;
  }

  .sidebar-resizer {
    display: none;
  }

  .mobile-topbar {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    z-index: 15;
    background: var(--panel-bg-contrast);
    border: 0;
    border-bottom: 1px solid var(--panel-border-contrast);
    border-radius: 0;
    min-height: 52px;
    padding: 8px 14px;
    margin: 0;
  }

  .mobile-topbar.is-scrolled {
    background: var(--bg-content);
  }

  .mobile-topbar-menu {
    position: absolute;
    left: 14px;
    top: 50%;
    transform: translateY(-50%);
  }

  .app-sidebar {
    display: flex;
    position: fixed;
    top: 0;
    left: 0;
    bottom: 24px;
    width: min(260px, 82vw);
    z-index: 30;
    transform: translateX(-100%);
    transition: transform 0.18s ease;
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
  }

  .app-sidebar.mobile-open {
    transform: translateX(0);
  }

  .mobile-sidebar-overlay {
    display: block;
    position: fixed;
    inset: 0 0 24px 0;
    background: rgba(0, 0, 0, 0.45);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
    z-index: 20;
  }

  .mobile-sidebar-overlay.active {
    opacity: 1;
    pointer-events: auto;
  }
}

/* ── Theme toggle dropdown ────────────────────── */

.theme-toggle-menu {
  position: relative;
  display: inline-flex;
}

.theme-toggle-trigger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 6px;
  border: none;
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}

.theme-toggle-trigger:hover {
  color: var(--text-muted);
  background: var(--bg-hover);
}

.theme-toggle-dropdown {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  min-width: 120px;
  padding: 4px;
  background: var(--bg-content);
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
  z-index: 50;
  display: flex;
  flex-direction: column;
}

[data-theme="light"] .theme-toggle-dropdown {
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}

.theme-toggle-option {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 10px;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: var(--text-muted);
  font-size: 12px;
  font-family: inherit;
  cursor: pointer;
  transition: color 0.15s, background 0.15s;
}

.theme-toggle-option:hover {
  background: var(--bg-hover);
  color: var(--text);
}

.theme-toggle-option.active {
  color: var(--accent);
}

/* ── Light theme overrides ─────────────────────── */

[data-theme="light"] .app-sidebar {
  background:
    linear-gradient(180deg, rgba(0, 0, 0, 0.02) 0%, rgba(0, 0, 0, 0.04) 100%),
    var(--bg-sidebar);
  border-right-color: rgba(0, 0, 0, 0.1);
}

[data-theme="light"] .sidebar-brand {
  color: var(--text);
}

[data-theme="light"] .sidebar-label {
  color: var(--text-muted);
}

[data-theme="light"] .sidebar-nav a {
  color: var(--text);
}

[data-theme="light"] .sidebar-nav a:hover {
  background: rgba(0, 0, 0, 0.06);
  color: var(--text-bright);
}

[data-theme="light"] .sidebar-nav a.active {
  background: rgba(8, 145, 178, 0.1);
  color: #0e7490;
}

[data-theme="light"] .sidebar-nav a.active::before {
  background: #0e7490;
}

[data-theme="light"] .brand-dropdown {
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}

[data-theme="light"] .global-restart-banner__content {
  background: rgba(254, 243, 199, 0.97);
  border: 1px solid rgba(202, 138, 4, 0.5);
  box-shadow: 0 18px 46px rgba(0, 0, 0, 0.12);
}

[data-theme="light"] .global-restart-banner__text {
  color: #78350f;
}

[data-theme="light"] .global-restart-banner__dismiss {
  color: #78350f;
}

[data-theme="light"] .global-restart-banner__dismiss:hover {
  color: #451a03;
}

[data-theme="light"] .sidebar-update-btn {
  border-color: rgba(202, 138, 4, 0.3);
  color: #a16207;
  background: rgba(202, 138, 4, 0.06);
}

[data-theme="light"] .sidebar-update-btn:hover {
  background: rgba(202, 138, 4, 0.1);
  border-color: rgba(202, 138, 4, 0.4);
}

[data-theme="light"] .sidebar-resizer:hover::after,
[data-theme="light"] .sidebar-resizer.is-resizing::after {
  background: rgba(8, 145, 178, 0.55);
}

@media (max-width: 768px) {
  [data-theme="light"] .app-sidebar {
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.15);
  }
}


================================================
FILE: lib/public/css/tailwind.input.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;


================================================
FILE: lib/public/css/theme.css
================================================
:root {
  --bg: #0d121b;
  --bg-sidebar: #0f141f;
  --bg-content: #0f1521;
  --bg-hover: rgba(99, 235, 255, 0.05);
  --bg-active: rgba(99, 235, 255, 0.08);
  --border: rgba(255, 255, 255, 0.06);
  --border-strong: rgba(255, 255, 255, 0.11);
  --text: #c9d1d9;
  --text-muted: #6e7681;
  --text-dim: #424854;
  --card-label-bright: #dbe7f6;
  --accent: #63ebff;
  --accent-dim: rgba(99, 235, 255, 0.4);
  --accent-link: rgba(99, 235, 255, 0.6);
  --orange: #d98a58;
  --comment: #6a737d;
  --keyword: #ff7b72;
  --string: #a5d6ff;
  --number: #79c0ff;
  --panel-bg-contrast: rgba(255, 255, 255, 0.028);
  --panel-border-contrast: rgba(255, 255, 255, 0.11);
  --field-bg-contrast: rgba(0, 0, 0, 0.3);
  --field-border-contrast: rgba(255, 255, 255, 0.13);

  /* ── Semantic theme tokens ────────────────────── */
  --text-bright: #f3f4f6;
  --overlay: rgba(0, 0, 0, 0.7);

  /* Status: error */
  --status-error: #fca5a5;
  --status-error-muted: #f87171;
  --status-error-bg: rgba(127, 29, 29, 0.95);
  --status-error-border: rgba(185, 28, 28, 0.8);

  /* Status: warning */
  --status-warning: #fde047;
  --status-warning-muted: #facc15;
  --status-warning-bg: rgba(66, 32, 6, 0.95);
  --status-warning-border: rgba(161, 98, 7, 0.8);

  /* Status: success */
  --status-success: #86efac;
  --status-success-muted: #22c55e;
  --status-success-bg: rgba(5, 46, 22, 0.5);
  --status-success-border: rgba(21, 128, 61, 0.8);

  /* Status: info */
  --status-info: #a5f3fc;
  --status-info-muted: #06b6d4;
  --status-info-bg: rgba(8, 51, 68, 0.95);
  --status-info-border: rgba(14, 116, 144, 0.8);
}

/* ── Light theme ─────────────────────────────────── */
[data-theme="light"] {
  --bg: #f8f9fb;
  --bg-sidebar: #f0f2f5;
  --bg-content: #ffffff;
  --bg-hover: rgba(0, 0, 0, 0.04);
  --bg-active: rgba(8, 145, 178, 0.08);
  --border: rgba(0, 0, 0, 0.08);
  --border-strong: rgba(0, 0, 0, 0.15);
  --text: #1f2937;
  --text-muted: #6b7280;
  --text-dim: #9ca3af;
  --text-bright: #111827;
  --card-label-bright: #1f2937;
  --accent: #0891b2;
  --accent-dim: rgba(8, 145, 178, 0.3);
  --accent-link: #0e7490;
  --orange: #c2410c;
  --comment: #9ca3af;
  --keyword: #dc2626;
  --string: #2563eb;
  --number: #0284c7;
  --panel-bg-contrast: rgba(0, 0, 0, 0.02);
  --panel-border-contrast: rgba(0, 0, 0, 0.1);
  --field-bg-contrast: rgba(0, 0, 0, 0.04);
  --field-border-contrast: rgba(0, 0, 0, 0.15);
  --overlay: rgba(0, 0, 0, 0.5);

  --status-error: #dc2626;
  --status-error-muted: #ef4444;
  --status-error-bg: rgba(254, 226, 226, 0.95);
  --status-error-border: rgba(252, 165, 165, 0.8);
  --status-warning: #a16207;
  --status-warning-muted: #854d0e;
  --status-warning-bg: rgba(254, 249, 195, 0.95);
  --status-warning-border: rgba(202, 138, 4, 0.5);
  --status-success: #16a34a;
  --status-success-muted: #22c55e;
  --status-success-bg: rgba(220, 252, 231, 0.95);
  --status-success-border: rgba(134, 239, 172, 0.8);
  --status-info: #0891b2;
  --status-info-muted: #06b6d4;
  --status-info-bg: rgba(207, 250, 254, 0.95);
  --status-info-border: rgba(103, 232, 249, 0.8);
}

html, body { height: 100%; }

body {
  background: var(--bg);
  color: var(--text);
  font-family: 'JetBrains Mono', monospace;
  font-size: 13px;
  line-height: 1.6;
}

.ac-logo-mark {
  display: inline-block;
  flex: 0 0 auto;
  width: var(--ac-logo-width, 20px);
  height: var(--ac-logo-height, 20px);
  background: #00efff;
  -webkit-mask: url("../img/logo.svg") center / contain no-repeat;
  mask: url("../img/logo.svg") center / contain no-repeat;
}

[data-theme="light"] .ac-logo-mark {
  background: var(--accent);
}

/* Subtle grid texture overlay */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.024) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.024) 1px, transparent 1px);
  background-size: 48px 48px;
  pointer-events: none;
  z-index: 0;
}

/* Standardised card / section label. */
.card-label {
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.card-label-bright {
  color: var(--card-label-bright);
}

.ac-small-heading {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Shared collapsible history rows (incidents, webhook requests). */
.ac-history-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.ac-history-list.ac-history-list-tight {
  gap: 0;
}

.ac-history-item {
  border: 1px solid var(--panel-border-contrast);
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.12);
}

.ac-history-item.ac-history-item-flat {
  border: 0;
  border-bottom: 1px solid var(--panel-border-contrast);
  border-radius: 0;
  background: transparent;
}

.snippet-collapse-fade {
  background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.75) 70%);
}

/* Shared inset panel for "surface on surface" layouts. */
.ac-surface-inset {
  border: 1px solid var(--panel-border-contrast);
  border-radius: 10px;
  background: rgba(0, 0, 0, 0.12);
}

.ac-history-summary {
  cursor: pointer;
  list-style: none;
  padding: 8px 10px;
  color: #d1d5db;
}

.ac-history-summary::-webkit-details-marker {
  display: none;
}

.ac-history-summary-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.ac-history-toggle {
  color: var(--text-muted);
  transition: transform 0.15s ease, color 0.15s ease;
}

.ac-history-item[open] > .ac-history-summary .ac-history-toggle {
  transform: rotate(90deg);
  color: #d1d5db;
}

.ac-history-body {
  margin: 4px 10px 10px;
  padding-top: 8px;
  border-top: 1px solid var(--panel-border-contrast);
}

/* Unified panel treatment across tabs/pages. */
.bg-surface {
  background: var(--panel-bg-contrast) !important;
  border-color: var(--panel-border-contrast) !important;
}

/* Solid background for modals so page content doesn't bleed through. */
.bg-modal {
  background: var(--bg) !important;
  border-color: var(--panel-border-contrast) !important;
}

.border-border {
  border-color: var(--panel-border-contrast) !important;
}

.ac-tip-link {
  color: var(--accent-link);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.ac-tip-link:hover {
  color: var(--accent);
}

/* Universal field contrast treatment (all tabs/pages). */
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
select,
textarea {
  background: var(--field-bg-contrast);
  border-color: var(--field-border-contrast);
}

input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus,
select:focus,
textarea:focus {
  border-color: rgba(255, 255, 255, 0.28);
}

::placeholder { color: var(--text-dim) !important; opacity: 1 !important; }
::-webkit-input-placeholder { color: var(--text-dim) !important; }
::-moz-placeholder { color: var(--text-dim) !important; }

::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 3px; }

.scrollbar-hidden {
  -ms-overflow-style: none;
  scrollbar-width: none;
}

.scrollbar-hidden::-webkit-scrollbar {
  display: none;
}

/* Google scope picker toggle buttons */
.scope-btn { background: rgba(255,255,255,0.03); color: var(--text-muted); border: 1px solid var(--border); transition: all 0.15s; }
.scope-btn:hover { border-color: var(--text-dim); color: var(--text); }
.scope-btn-read.active {
  background: rgba(255, 255, 255, 0.03);
  color: #f3f4f6;
  border-color: rgba(255, 255, 255, 0.35);
}
.scope-btn-write.active {
  background: rgba(255, 255, 255, 0.03);
  color: #f3f4f6;
  border-color: rgba(255, 255, 255, 0.35);
}

/* Reusable cyan action buttons */
.ac-btn-cyan {
  border: 1px solid var(--accent-dim);
  background: linear-gradient(
    180deg,
    rgba(99, 235, 255, 0.14) 0%,
    rgba(99, 235, 255, 0.08) 100%
  );
  color: var(--accent);
  box-shadow: inset 0 0 0 1px rgba(99, 235, 255, 0.12);
  transition:
    border-color 0.15s ease,
    background 0.15s ease,
    color 0.15s ease,
    box-shadow 0.15s ease,
    transform 0.15s ease;
}

.ac-btn-cyan:hover:not(:disabled) {
  border-color: rgba(99, 235, 255, 0.7);
  background: linear-gradient(
    180deg,
    rgba(99, 235, 255, 0.24) 0%,
    rgba(99, 235, 255, 0.12) 100%
  );
  color: #b9f8ff;
  box-shadow:
    inset 0 0 0 1px rgba(99, 235, 255, 0.2),
    0 0 12px rgba(99, 235, 255, 0.14);
}

.ac-btn-cyan:active:not(:disabled) {
  transform: translateY(1px);
}

.ac-btn-cyan:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ac-btn-cyan-ghost {
  border: 1px solid var(--accent-dim);
  color: rgba(99, 235, 255, 0.75);
  background: rgba(99, 235, 255, 0.04);
  transition:
    border-color 0.15s ease,
    color 0.15s ease,
    background 0.15s ease;
}

.ac-btn-cyan-ghost:hover {
  border-color: rgba(99, 235, 255, 0.5);
  color: #a4f3ff;
  background: rgba(99, 235, 255, 0.08);
}

.ac-path-card {
  border: 1px solid var(--panel-border-contrast);
  background: rgba(0, 0, 0, 0.2);
  transition:
    border-color 0.15s ease,
    background 0.15s ease,
    box-shadow 0.15s ease,
    transform 0.15s ease;
}

.ac-path-card:hover {
  border-color: rgba(99, 235, 255, 0.5);
  background: rgba(99, 235, 255, 0.04);
  box-shadow:
    inset 0 0 0 1px rgba(99, 235, 255, 0.1),
    0 0 12px rgba(99, 235, 255, 0.08);
}

.ac-path-card:hover .ac-path-icon {
  color: var(--accent);
  border-color: rgba(99, 235, 255, 0.3);
  background: rgba(99, 235, 255, 0.1);
}

.ac-path-card:hover .ac-path-title {
  color: #b9f8ff;
}

.ac-path-card:hover .ac-path-desc {
  color: #94a3b8;
}

.ac-path-icon {
  transition:
    color 0.15s ease,
    border-color 0.15s ease,
    background 0.15s ease;
}

.ac-btn-secondary {
  border: 1px solid var(--panel-border-contrast);
  color: #d1d5db;
  background: rgba(255, 255, 255, 0.03);
  transition:
    border-color 0.15s ease,
    color 0.15s ease,
    background 0.15s ease,
    transform 0.15s ease;
}

.ac-btn-secondary:hover:not(:disabled) {
  border-color: rgba(255, 255, 255, 0.35);
  color: #f3f4f6;
  background: rgba(255, 255, 255, 0.06);
}

.ac-btn-secondary:active:not(:disabled) {
  transform: translateY(1px);
}

.ac-btn-secondary:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ac-btn-ghost {
  border: none;
  color: var(--text-muted);
  background: transparent;
  transition: color 0.15s ease;
}

.ac-btn-ghost:hover:not(:disabled) {
  color: #f3f4f6;
}

.ac-btn-ghost:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ac-btn-danger {
  border: 1px solid rgba(239, 68, 68, 0.45);
  background: rgba(239, 68, 68, 0.08);
  color: #fca5a5;
  transition:
    border-color 0.15s ease,
    color 0.15s ease,
    background 0.15s ease,
    transform 0.15s ease;
}

.ac-btn-danger:hover:not(:disabled) {
  border-color: rgba(239, 68, 68, 0.7);
  background: rgba(239, 68, 68, 0.14);
  color: #fecaca;
}

.ac-btn-danger:active:not(:disabled) {
  transform: translateY(1px);
}

.ac-btn-danger:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.ac-toggle {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  user-select: none;
}

.ac-toggle-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.ac-toggle-track {
  position: relative;
  width: 34px;
  height: 20px;
  border-radius: 999px;
  border: 1px solid var(--panel-border-contrast);
  background: rgba(0, 0, 0, 0.35);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.02);
  transition:
    border-color 0.18s ease,
    background-color 0.18s ease,
    box-shadow 0.18s ease;
}

.ac-toggle-thumb {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 999px;
  background: #94a3b8;
  box-shadow: 0 0 0 1px rgba(15, 21, 33, 0.3);
  transition:
    transform 0.18s ease,
    background-color 0.18s ease,
    box-shadow 0.18s ease;
}

.ac-toggle-label {
  font-size: 12px;
  color: #d1d5db;
}

.ac-toggle-input:checked + .ac-toggle-track {
  border-color: rgba(99, 235, 255, 0.8);
  background: rgba(99, 235, 255, 0.16);
  box-shadow:
    inset 0 0 0 1px rgba(99, 235, 255, 0.2),
    0 0 10px rgba(99, 235, 255, 0.2);
}

.ac-toggle-input:checked + .ac-toggle-track .ac-toggle-thumb {
  transform: translateX(14px);
  background: #b6f6ff;
  box-shadow:
    0 0 0 1px rgba(15, 21, 33, 0.25),
    0 0 10px rgba(99, 235, 255, 0.45);
}

.ac-toggle-input:focus-visible + .ac-toggle-track {
  outline: 2px solid rgba(99, 235, 255, 0.55);
  outline-offset: 2px;
}

.ac-toggle-input:disabled + .ac-toggle-track {
  opacity: 0.55;
  box-shadow: none;
}

.ac-toggle-input:disabled + .ac-toggle-track + .ac-toggle-label {
  opacity: 0.6;
}

@keyframes acStatusDotPulse {
  0%,
  100% {
    transform: scale(1);
    box-shadow:
      0 0 0 0 rgba(34, 197, 94, 0.14),
      0 0 5px rgba(34, 197, 94, 0.18);
  }
  50% {
    transform: scale(1.08);
    box-shadow:
      0 0 0 3px rgba(34, 197, 94, 0.08),
      0 0 9px rgba(34, 197, 94, 0.32);
  }
}

@keyframes acStatusDotPulseInfo {
  0%,
  100% {
    transform: scale(1);
    box-shadow:
      0 0 0 0 rgba(34, 211, 238, 0.16),
      0 0 5px rgba(34, 211, 238, 0.22);
  }
  50% {
    transform: scale(1.08);
    box-shadow:
      0 0 0 3px rgba(34, 211, 238, 0.1),
      0 0 9px rgba(34, 211, 238, 0.34);
  }
}

@keyframes acStepPillPulse {
  0%,
  100% {
    box-shadow:
      0 0 0 0 rgba(99, 235, 255, 0.14),
      0 0 5px rgba(99, 235, 255, 0.18);
  }
  50% {
    box-shadow:
      0 0 0 3px rgba(99, 235, 255, 0.08),
      0 0 9px rgba(99, 235, 255, 0.32);
  }
}

.ac-step-pill-pulse {
  animation: acStepPillPulse 2.6s ease-in-out infinite;
}

.ac-status-dot {
  width: 8px;
  height: 8px;
  border-radius: 999px;
}

.ac-status-dot--healthy {
  background: #22c55e;
  animation: acStatusDotPulse 2.6s ease-in-out infinite;
}

.ac-status-dot--info {
  background: #22d3ee;
  animation: acStatusDotPulseInfo 2.6s ease-in-out infinite;
}

.ac-status-dot--healthy-offset {
  animation-delay: 0.95s;
}

.ac-btn-green {
  border: 1px solid rgba(34, 197, 94, 0.45);
  background: linear-gradient(
    180deg,
    rgba(34, 197, 94, 0.2) 0%,
    rgba(34, 197, 94, 0.12) 100%
  );
  color: #86efac;
  box-shadow: inset 0 0 0 1px rgba(34, 197, 94, 0.12);
  transition:
    border-color 0.15s ease,
    background 0.15s ease,
    color 0.15s ease,
    box-shadow 0.15s ease,
    transform 0.15s ease;
}

.ac-btn-green:hover:not(:disabled) {
  border-color: rgba(34, 197, 94, 0.7);
  background: linear-gradient(
    180deg,
    rgba(34, 197, 94, 0.28) 0%,
    rgba(34, 197, 94, 0.16) 100%
  );
  color: #bbf7d0;
  box-shadow:
    inset 0 0 0 1px rgba(34, 197, 94, 0.2),
    0 0 12px rgba(34, 197, 94, 0.12);
}

.ac-btn-green:active:not(:disabled) {
  transform: translateY(1px);
}

@keyframes acSpinnerOrbit {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.ac-spinner {
  animation: acSpinnerOrbit 1s cubic-bezier(0.3, 0.2, 0.7, 0.8) infinite;
  transform-origin: center;
  will-change: transform;
}

@media (prefers-reduced-motion: reduce) {
  .ac-spinner {
    animation-duration: 1.8s;
    animation-timing-function: linear;
  }
}

/* Reusable segmented control (pill toggle). */
.ac-segmented-control {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--panel-border-contrast);
  border-radius: 8px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.02);
  height: 28px;
}

.ac-segmented-control-full {
  display: flex;
  width: 100%;
}

.ac-segmented-control-button {
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-family: inherit;
  font-size: 12px;
  letter-spacing: 0.03em;
  height: 100%;
  line-height: 1;
  padding: 0 10px;
  cursor: pointer;
  transition: color 0.12s, background 0.12s;
}

.ac-segmented-control-full .ac-segmented-control-button {
  flex: 1 1 0%;
}

.ac-segmented-control > .inline-flex {
  align-self: stretch;
  display: flex;
}

.ac-segmented-control-full > .inline-flex {
  flex: 1 1 0%;
}

.ac-segmented-control-lg {
  height: 36px;
  border-radius: 12px;
}

.ac-segmented-control-lg .ac-segmented-control-button {
  font-size: 14px;
  padding: 0 16px;
}

.ac-segmented-control-button:hover {
  color: var(--text);
  background: rgba(255, 255, 255, 0.03);
}

.ac-segmented-control-button.active {
  color: var(--accent);
  background: var(--bg-active);
}

.ac-segmented-control-dark {
  background: rgba(0, 0, 0, 0.25);
}

/* ── PopActions: animated action group ───────── */

.ac-pop-actions {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  flex-shrink: 0;
}

.ac-pop-actions-hidden {
  visibility: hidden;
  pointer-events: none;
}

.ac-pop-actions-in > * {
  animation: acPopIn 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

.ac-pop-actions-in > *:nth-child(2) {
  animation-delay: 0.06s;
}

.ac-pop-actions-in .ac-btn-cyan {
  animation: acPopIn 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) 0.06s both;
}

.ac-pop-actions-out > * {
  animation: acPopOut 0.18s ease-in both;
}

.ac-pop-actions-out > *:first-child {
  animation-delay: 0.03s;
}

.ac-pop-actions > button:active:not(:disabled) {
  transform: translateY(1px) scale(0.98);
}

@keyframes acPopIn {
  from { opacity: 0; transform: scale(0.85); }
  to   { opacity: 1; transform: scale(1); }
}

@keyframes acPopOut {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(0.85); }
}

.watchdog-logs-panel {
  min-height: 160px;
  max-height: 80vh;
  resize: vertical;
}

.watchdog-terminal-host {
  position: relative;
}

.watchdog-terminal-host .xterm {
  height: 100%;
  letter-spacing: 0;
  font-kerning: none;
}

.watchdog-terminal-host .xterm-viewport {
  overflow-y: auto !important;
}

/* ── Light theme overrides for hardcoded dark patterns ── */

[data-theme="light"] body::before {
  background-image:
    linear-gradient(rgba(0, 0, 0, 0.03) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0, 0, 0, 0.03) 1px, transparent 1px);
}

[data-theme="light"] .ac-history-item {
  background: rgba(0, 0, 0, 0.02);
}

[data-theme="light"] .ac-history-summary {
  color: var(--text);
}

[data-theme="light"] .ac-history-item[open] > .ac-history-summary .ac-history-toggle {
  color: var(--text);
}

[data-theme="light"] .ac-surface-inset {
  background: rgba(0, 0, 0, 0.02);
}

[data-theme="light"] .snippet-collapse-fade {
  background: linear-gradient(to bottom, transparent 0%, rgba(255, 255, 255, 0.85) 70%);
}

[data-theme="light"] input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus,
[data-theme="light"] select:focus,
[data-theme="light"] textarea:focus {
  border-color: rgba(0, 0, 0, 0.35);
}

[data-theme="light"] ::-webkit-scrollbar-thumb {
  background: rgba(0, 0, 0, 0.12);
}

[data-theme="light"] .scope-btn { background: rgba(0, 0, 0, 0.03); }
[data-theme="light"] .scope-btn-read.active,
[data-theme="light"] .scope-btn-write.active {
  background: rgba(0, 0, 0, 0.03);
  color: var(--text-bright);
  border-color: rgba(0, 0, 0, 0.35);
}

[data-theme="light"] .ac-btn-cyan {
  border: 1px solid var(--accent-dim);
  background: linear-gradient(180deg, rgba(8, 145, 178, 0.1) 0%, rgba(8, 145, 178, 0.05) 100%);
  color: var(--accent);
  box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.08);
}

[data-theme="light"] .ac-btn-cyan:hover:not(:disabled) {
  border-color: rgba(8, 145, 178, 0.6);
  background: linear-gradient(180deg, rgba(8, 145, 178, 0.16) 0%, rgba(8, 145, 178, 0.08) 100%);
  color: #065666;
  box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.15), 0 0 12px rgba(8, 145, 178, 0.1);
}

[data-theme="light"] .ac-btn-cyan-ghost {
  border: 1px solid var(--accent-dim);
  color: var(--accent);
  background: rgba(8, 145, 178, 0.04);
}

[data-theme="light"] .ac-btn-cyan-ghost:hover {
  border-color: rgba(8, 145, 178, 0.5);
  color: #065666;
  background: rgba(8, 145, 178, 0.08);
}

[data-theme="light"] .ac-btn-secondary {
  color: var(--text);
  background: rgba(0, 0, 0, 0.02);
}

[data-theme="light"] .ac-btn-secondary:hover:not(:disabled) {
  border-color: rgba(0, 0, 0, 0.25);
  color: var(--text-bright);
  background: rgba(0, 0, 0, 0.04);
}

[data-theme="light"] .ac-btn-ghost:hover:not(:disabled) {
  color: var(--text-bright);
}

[data-theme="light"] .ac-btn-danger {
  border: 1px solid rgba(220, 38, 38, 0.3);
  background: rgba(220, 38, 38, 0.06);
  color: #dc2626;
}

[data-theme="light"] .ac-btn-danger:hover:not(:disabled) {
  border-color: rgba(220, 38, 38, 0.5);
  background: rgba(220, 38, 38, 0.1);
  color: #b91c1c;
}

[data-theme="light"] .ac-btn-green {
  border: 1px solid rgba(22, 163, 74, 0.3);
  background: linear-gradient(180deg, rgba(22, 163, 74, 0.1) 0%, rgba(22, 163, 74, 0.05) 100%);
  color: #16a34a;
  box-shadow: inset 0 0 0 1px rgba(22, 163, 74, 0.08);
}

[data-theme="light"] .ac-btn-green:hover:not(:disabled) {
  border-color: rgba(22, 163, 74, 0.5);
  background: linear-gradient(180deg, rgba(22, 163, 74, 0.16) 0%, rgba(22, 163, 74, 0.08) 100%);
  color: #15803d;
  box-shadow: inset 0 0 0 1px rgba(22, 163, 74, 0.15), 0 0 12px rgba(22, 163, 74, 0.08);
}

[data-theme="light"] .ac-toggle-track {
  background: rgba(0, 0, 0, 0.1);
}

[data-theme="light"] .ac-toggle-thumb {
  background: #9ca3af;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
}

[data-theme="light"] .ac-toggle-input:checked + .ac-toggle-track {
  border-color: rgba(8, 145, 178, 0.6);
  background: rgba(8, 145, 178, 0.12);
  box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.15);
}

[data-theme="light"] .ac-toggle-input:checked + .ac-toggle-track .ac-toggle-thumb {
  background: #0891b2;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
}

[data-theme="light"] .ac-toggle-label {
  color: var(--text);
}

[data-theme="light"] .ac-path-card {
  background: rgba(0, 0, 0, 0.02);
}

[data-theme="light"] .ac-path-card:hover {
  border-color: rgba(8, 145, 178, 0.4);
  background: rgba(8, 145, 178, 0.04);
  box-shadow: inset 0 0 0 1px rgba(8, 145, 178, 0.08), 0 0 12px rgba(8, 145, 178, 0.06);
}

[data-theme="light"] .ac-path-card:hover .ac-path-title {
  color: #065666;
}

[data-theme="light"] .ac-path-card:hover .ac-path-desc {
  color: var(--text-muted);
}

[data-theme="light"] .ac-segmented-control {
  background: rgba(0, 0, 0, 0.02);
  border-color: rgba(0, 0, 0, 0.12);
}

[data-theme="light"] .ac-segmented-control-button:hover {
  background: rgba(0, 0, 0, 0.04);
}

[data-theme="light"] .ac-segmented-control-button.active {
  background: rgba(8, 145, 178, 0.12);
  color: #0e7490;
}

[data-theme="light"] .ac-segmented-control-dark {
  background: rgba(0, 0, 0, 0.04);
}

/* Modal and link overrides for light mode */
[data-theme="light"] .bg-modal {
  background: #ffffff;
}

[data-theme="light"] a[style*="color: rgba(99, 235, 255"] {
  color: #0e7490 !important;
}

[data-theme="light"] a[style*="color: rgba(99, 235, 255"]:hover {
  color: var(--text-bright) !important;
}

[data-theme="light"] .text-cyan-400 {
  color: #0e7490 !important;
}

[data-theme="light"] .text-cyan-300 {
  color: #0e7490 !important;
}

[data-theme="light"] .text-blue-400 {
  color: #1d4ed8 !important;
}

[data-theme="light"] .text-indigo-300 {
  color: #4338ca !important;
}

[data-theme="light"] .text-purple-400 {
  color: #7e22ce !important;
}


================================================
FILE: lib/public/js/app.js
================================================
import { h, render } from "preact";
import { useState, useEffect } from "preact/hooks";
import htm from "htm";
import {
  Router,
  Route,
  Switch,
  useLocation,
} from "wouter-preact";
import { logout } from "./lib/api.js";
import { Welcome } from "./components/welcome/index.js";
import { ThemeToggle } from "./components/theme-toggle.js";
import { ToastContainer } from "./components/toast.js";
import { GlobalRestartBanner } from "./components/global-restart-banner.js";
import { LoadingSpinner } from "./components/loading-spinner.js";
import { AppSidebar } from "./components/sidebar.js";
import {
  AgentsRoute,
  BrowseRoute,
  ChatRoute,
  CronRoute,
  DoctorRoute,
  EnvarsRoute,
  GeneralRoute,
  ModelsRoute,
  NodesRoute,
  RouteRedirect,
  TelegramRoute,
  UsageRoute,
  WatchdogRoute,
  WebhooksRoute,
} from "./components/routes/index.js";
import { useAgents } from "./components/agents-tab/use-agents.js";
import { useAppShellController } from "./hooks/use-app-shell-controller.js";
import { useAppShellUi } from "./hooks/use-app-shell-ui.js";
import { useBrowseNavigation } from "./hooks/use-browse-navigation.js";
import { useAgentSessions } from "./hooks/useAgentSessions.js";
import {
  getHashRouterPath,
  useHashLocation,
} from "./hooks/use-hash-location.js";
import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";

const html = htm.bind(h);
const kDoctorWarningDismissedUntilUiSettingKey =
  "doctorWarningDismissedUntilMs";
const kOneWeekMs = 7 * 24 * 60 * 60 * 1000;
const kPendingCreateAgentWindowFlag = "__alphaclawPendingCreateAgent";

const App = () => {
  const [location, setLocation] = useLocation();
  const [doctorWarningDismissedUntilMs, setDoctorWarningDismissedUntilMs] =
    useState(() => {
      const settings = readUiSettings();
      return Number(settings[kDoctorWarningDismissedUntilUiSettingKey] || 0);
    });

  const { state: controllerState, actions: controllerActions } =
    useAppShellController({
      location,
    });
  const {
    refs: shellRefs,
    state: shellState,
    actions: shellActions,
  } = useAppShellUi();
  const {
    state: browseState,
    actions: browseActions,
    constants: browseConstants,
  } = useBrowseNavigation({
    location,
    setLocation,
    onCloseMobileSidebar: shellActions.closeMobileSidebar,
  });

  const {
    state: agentsState,
    actions: agentsActions,
  } = useAgents();

  const isAgentsRoute = location.startsWith("/agents");
  const isChatRoute = location.startsWith("/chat");
  const isCronRoute = location.startsWith("/cron");
  const isEnvarsRoute = location.startsWith("/envars");
  const isModelsRoute = location.startsWith("/models");
  const isNodesRoute = location.startsWith("/nodes");
  const selectedAgentId = (() => {
    const match = location.match(/^\/agents\/([^/]+)/);
    return match ? decodeURIComponent(match[1]) : "";
  })();
  const agentDetailTab = (() => {
    const match = location.match(/^\/agents\/[^/]+\/([^/]+)/);
    const tab = match ? match[1] : "";
    return tab === "tools" ? "tools" : "overview";
  })();
  const selectedCronJobId = (() => {
    const match = location.match(/^\/cron\/([^/]+)/);
    return match ? decodeURIComponent(match[1]) : "";
  })();
  const {
    sessions: chatSessions,
    selectedSessionKey: selectedChatSessionKey,
    setSelectedSessionKey: setSelectedChatSessionKey,
  } = useAgentSessions({
    enabled: controllerState.onboarded === true,
  });
  const footerVersion = (() => {
    const openclawVersion = String(
      controllerState.acCurrentOpenclawVersion || "",
    ).trim();
    const alphaclawVersion = String(controllerState.acVersion || "").trim();
    if (openclawVersion && alphaclawVersion) {
      return `OpenClaw ${openclawVersion} / AlphaClaw ${alphaclawVersion}`;
    }
    if (openclawVersion) {
      return `OpenClaw ${openclawVersion}`;
    }
    if (alphaclawVersion) {
      return `AlphaClaw ${alphaclawVersion}`;
    }
    return null;
  })();

  useEffect(() => {
    if (!isAgentsRoute) return;
    if (window[kPendingCreateAgentWindowFlag]) return;
    if (selectedAgentId) return;
    if (agentsState.loading || agentsState.agents.length === 0) return;
    setLocation(`/agents/${encodeURIComponent(agentsState.agents[0].id)}`);
  }, [isAgentsRoute, selectedAgentId, agentsState.loading, agentsState.agents, setLocation]);

  useEffect(() => {
    if (!isAgentsRoute) return;
    if (!window[kPendingCreateAgentWindowFlag]) return;
    window[kPendingCreateAgentWindowFlag] = false;
    window.setTimeout(() => {
      window.dispatchEvent(new Event("alphaclaw:create-agent"));
    }, 0);
  }, [isAgentsRoute]);

  useEffect(() => {
    const settings = readUiSettings();
    settings[kDoctorWarningDismissedUntilUiSettingKey] =
      doctorWarningDismissedUntilMs;
    writeUiSettings(settings);
  }, [doctorWarningDismissedUntilMs]);

  const handleSidebarLogout = async () => {
    shellActions.setMenuOpen(false);
    await logout();
    try {
      window.localStorage.clear();
      window.sessionStorage.clear();
    } catch {}
    window.location.href = "/login.html";
  };

  if (controllerState.onboarded === null) {
    return html`
      <div
        class="min-h-screen flex items-center justify-center"
        style="position: relative; z-index: 1"
      >
        <${LoadingSpinner}
          className="h-6 w-6"
          style="color: var(--text-muted)"
        />
      </div>
      <${ToastContainer} />
    `;
  }

  if (!controllerState.onboarded) {
    return html`
      <div
        class="min-h-screen flex flex-col items-center pt-12 pb-8 px-4"
        style="position: relative; z-index: 1"
      >
        <div style="position: fixed; top: 16px; right: 16px; z-index: 50;">
          <${ThemeToggle} />
        </div>
        <${Welcome}
          onComplete=${controllerActions.handleOnboardingComplete}
          acVersion=${controllerState.acVersion}
        />
      </div>
      <${ToastContainer} />
    `;
  }

  return html`
    <div
      class="app-shell"
      ref=${shellRefs.appShellRef}
      style=${{ "--sidebar-width": `${shellState.sidebarWidthPx}px` }}
    >
      <${GlobalRestartBanner}
        visible=${controllerState.isAnyRestartRequired}
        restarting=${controllerState.restartingGateway}
        onRestart=${controllerActions.handleGatewayRestart}
        onDismiss=${controllerActions.dismissRestartBanner}
      />
      <${AppSidebar}
        mobileSidebarOpen=${shellState.mobileSidebarOpen}
        authEnabled=${controllerState.authEnabled}
        menuRef=${shellRefs.menuRef}
        menuOpen=${shellState.menuOpen}
        onToggleMenu=${shellActions.onToggleMenu}
        onLogout=${handleSidebarLogout}
        sidebarTab=${browseState.sidebarTab}
        onSelectSidebarTab=${browseActions.handleSelectSidebarTab}
        navSections=${browseConstants.kNavSections}
        selectedNavId=${browseState.selectedNavId}
        onSelectNavItem=${browseActions.handleSelectNavItem}
        selectedBrowsePath=${browseState.selectedBrowsePath}
        onSelectBrowseFile=${browseActions.navigateToBrowseFile}
        onPreviewBrowseFile=${browseActions.handleBrowsePreviewFile}
        acHasUpdate=${controllerState.acHasUpdate}
        acVersion=${controllerState.acVersion}
        acCurrentOpenclawVersion=${controllerState.acCurrentOpenclawVersion}
        acLatest=${controllerState.acLatest}
        acLatestOpenclawVersion=${controllerState.acLatestOpenclawVersion}
        acUpdateStrategy=${controllerState.acUpdateStrategy}
        acUpdating=${controllerState.acUpdating}
        onAcUpdate=${controllerActions.handleAcUpdate}
        agents=${agentsState.agents}
        selectedAgentId=${selectedAgentId}
        onSelectAgent=${(agentId) => setLocation(`/agents/${encodeURIComponent(agentId)}`)}
        onAddAgent=${() => {
          if (isAgentsRoute) {
            window.dispatchEvent(new Event("alphaclaw:create-agent"));
            return;
          }
          window[kPendingCreateAgentWindowFlag] = true;
          setLocation("/agents");
        }}
        chatSessions=${chatSessions}
        selectedChatSessionKey=${selectedChatSessionKey}
        onSelectChatSession=${(sessionKey) => {
          setSelectedChatSessionKey(sessionKey);
          if (!isChatRoute) setLocation("/chat");
        }}
      />
      <div
        class=${`sidebar-resizer ${shellState.isResizingSidebar ? "is-resizing" : ""}`}
        onpointerdown=${shellActions.onSidebarResizerPointerDown}
        role="separator"
        aria-orientation="vertical"
        aria-label="Resize sidebar"
      ></div>

      <div
        class=${`mobile-sidebar-overlay ${shellState.mobileSidebarOpen ? "active" : ""}`}
        onclick=${shellActions.closeMobileSidebar}
      />

      <div class="app-content">
        <div
          class=${`mobile-topbar ${shellState.mobileTopbarScrolled ? "is-scrolled" : ""}`}
        >
          <button
            class="mobile-topbar-menu"
            onclick=${() =>
              shellActions.setMobileSidebarOpen((open) => !open)}
            aria-label="Open menu"
            aria-expanded=${shellState.mobileSidebarOpen ? "true" : "false"}
          >
            <svg
              width="18"
              height="18"
              viewBox="0 0 16 16"
              fill="currentColor"
            >
              <path
                d="M2 3.75a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8zm0 4.25a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75a.75.75 0 01-.75-.75z"
              />
            </svg>
          </button>
          <span class="mobile-topbar-title">
            <span style="color: var(--accent)">alpha</span>claw
          </span>
        </div>
        ${browseState.isBrowseRoute
          ? html`
              <div class="app-content-pane browse-pane">
                <${BrowseRoute}
                  activeBrowsePath=${browseState.activeBrowsePath}
                  browseView=${browseState.browseViewerMode}
                  lineTarget=${browseState.browseLineTarget}
                  lineEndTarget=${browseState.browseLineEndTarget}
                  selectedBrowsePath=${browseState.selectedBrowsePath}
                  onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
                  onEditSelectedBrowseFile=${() =>
                    setLocation(
                      browseActions.buildBrowseRoute(browseState.selectedBrowsePath, {
                        view: "edit",
                      }),
                    )}
                  onClearSelection=${() => {
                    browseActions.clearBrowsePreview();
                    setLocation("/browse");
                  }}
                />
              </div>
            `
          : null}
        ${isAgentsRoute
          ? html`
              <div class="app-content-pane agents-pane">
                <${AgentsRoute}
                  agents=${agentsState.agents}
                  loading=${agentsState.loading}
                  saving=${agentsState.saving}
                  agentsActions=${agentsActions}
                  selectedAgentId=${selectedAgentId}
                  activeTab=${agentDetailTab}
                  onSelectAgent=${(agentId) =>
                    setLocation(`/agents/${encodeURIComponent(agentId)}`)}
                  onSelectTab=${(tab) => {
                    const safePath = tab && tab !== "overview"
                      ? `/agents/${encodeURIComponent(selectedAgentId)}/${tab}`
                      : `/agents/${encodeURIComponent(selectedAgentId)}`;
                    setLocation(safePath);
                  }}
                  onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
                  onSetLocation=${setLocation}
                />
              </div>
            `
          : null}
        ${isChatRoute
          ? html`
              <div class="app-content-pane chat-pane">
                <${ChatRoute}
                  sessions=${chatSessions}
                  selectedSessionKey=${selectedChatSessionKey}
                />
              </div>
            `
          : null}
        ${isCronRoute
          ? html`
              <div class="app-content-pane cron-pane">
                <${CronRoute}
                  jobId=${selectedCronJobId}
                  onSetLocation=${setLocation}
                />
              </div>
            `
          : null}
        ${isEnvarsRoute
          ? html`
              <div class="app-content-pane ac-fixed-header-pane">
                <${EnvarsRoute}
                  onRestartRequired=${controllerActions.setRestartRequired}
                />
              </div>
            `
          : null}
        ${isModelsRoute
          ? html`
              <div class="app-content-pane ac-fixed-header-pane">
                <${ModelsRoute}
                  onRestartRequired=${controllerActions.setRestartRequired}
                />
              </div>
            `
          : null}
        ${isNodesRoute
          ? html`
              <div class="app-content-pane">
                <${NodesRoute}
                  onRestartRequired=${controllerActions.setRestartRequired}
                />
              </div>
            `
          : null}
        ${browseState.isBrowseRoute ||
        isAgentsRoute ||
        isChatRoute ||
        isCronRoute ||
        isEnvarsRoute ||
        isModelsRoute ||
        isNodesRoute
          ? null
          : html`
              <div
                class="app-content-pane"
                onscroll=${shellActions.handlePaneScroll}
              >
          <div class="max-w-2xl w-full mx-auto">
            <${Switch}>
                    <${Route} path="/general">
                      <${GeneralRoute}
                        statusData=${controllerState.sharedStatus}
                        watchdogData=${controllerState.sharedWatchdogStatus}
                        doctorStatusData=${controllerState.sharedDoctorStatus}
                        agents=${agentsState.agents}
                        doctorWarningDismissedUntilMs=${doctorWarningDismissedUntilMs}
                        onRefreshStatuses=${controllerActions.refreshSharedStatuses}
                        onSetLocation=${setLocation}
                        onNavigate=${browseActions.navigateToSubScreen}
                        restartingGateway=${controllerState.restartingGateway}
                        onRestartGateway=${controllerActions.handleGatewayRestart}
                        restartSignal=${controllerState.gatewayRestartSignal}
                        onRestartRequired=${controllerActions.setRestartRequired}
                        onDismissDoctorWarning=${() =>
                          setDoctorWarningDismissedUntilMs(
                            Date.now() + kOneWeekMs,
                          )}
                      />
                    </${Route}>
                    <${Route} path="/doctor">
                      <${DoctorRoute} onNavigateToBrowseFile=${browseActions.navigateToBrowseFile} />
                    </${Route}>
                    <${Route} path="/telegram/:accountId">
                      ${(params) => html`
                        <${TelegramRoute}
                          accountId=${decodeURIComponent(params.accountId || "default")}
                          onBack=${browseActions.exitSubScreen}
                        />
                      `}
                    </${Route}>
                    <${Route} path="/telegram">
                      <${RouteRedirect} to="/telegram/default" />
                    </${Route}>
                    <${Route} path="/providers">
                      <${RouteRedirect} to="/models" />
                    </${Route}>
                    <${Route} path="/watchdog">
                      <${WatchdogRoute}
                        statusData=${controllerState.sharedStatus}
                        watchdogStatus=${controllerState.sharedWatchdogStatus}
                        onRefreshStatuses=${controllerActions.refreshSharedStatuses}
                        restartingGateway=${controllerState.restartingGateway}
                        onRestartGateway=${controllerActions.handleGatewayRestart}
                        restartSignal=${controllerState.gatewayRestartSignal}
                      />
                    </${Route}>
                    <${Route} path="/usage/:sessionId">
                      ${(params) => html`
                        <${UsageRoute}
                          sessionId=${decodeURIComponent(
                            params.sessionId || "",
                          )}
                          onSetLocation=${setLocation}
                        />
                      `}
                    </${Route}>
                    <${Route} path="/usage">
                      <${UsageRoute} onSetLocation=${setLocation} />
                    </${Route}>
                    <${Route} path="/webhooks/:hookName">
                      ${(params) => html`
                        <${WebhooksRoute}
                          hookName=${decodeURIComponent(params.hookName || "")}
                          routeHistoryRef=${browseState.routeHistoryRef}
                          getCurrentPath=${getHashRouterPath}
                          onSetLocation=${setLocation}
                          onRestartRequired=${controllerActions.setRestartRequired}
                          onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
                        />
                      `}
                    </${Route}>
                    <${Route} path="/webhooks">
                      <${WebhooksRoute}
                        routeHistoryRef=${browseState.routeHistoryRef}
                        getCurrentPath=${getHashRouterPath}
                        onSetLocation=${setLocation}
                        onRestartRequired=${controllerActions.setRestartRequired}
                        onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
                      />
                    </${Route}>
                    <${Route}>
                      <${RouteRedirect} to="/general" />
                    </${Route}>
                  </${Switch}>
          </div>
              </div>
            `}
        <${ToastContainer}
          className="fixed top-4 right-4 z-[60] space-y-2 pointer-events-none"
        />
      </div>

      <div class="app-statusbar">
        <div class="statusbar-left">
          ${footerVersion
            ? html`<span style="color: var(--text-muted)">${footerVersion}</span>`
            : null}
        </div>
        <div class="statusbar-right">
          <a href="https://docs.openclaw.ai" target="_blank" rel="noreferrer"
            >docs</a
          >
          <a
            href="https://discord.com/invite/clawd"
            target="_blank"
            rel="noreferrer"
            >discord</a
          >
          <a
            href="https://github.com/openclaw/openclaw"
            target="_blank"
            rel="noreferrer"
            >github</a
          >
        </div>
      </div>
    </div>
  `;
};

const rootElement = document.getElementById("app");
if (rootElement) {
  const appBootCounter = "__alphaclawSetupAppBootCount";
  window[appBootCounter] = Number(window[appBootCounter] || 0) + 1;
  // Defensive: clear root so duplicate bootstraps cannot stack full app shells.
  render(null, rootElement);
  rootElement.replaceChildren();
  render(
    html`
      <${Router} hook=${useHashLocation}>
        <${App} />
      </${Router}>
    `,
    rootElement,
  );
}


================================================
FILE: lib/public/js/components/action-button.js
================================================
import { h } from "preact";
import htm from "htm";
import { LoadingSpinner } from "./loading-spinner.js";

const html = htm.bind(h);

const kStaticToneClassByTone = {
  primary: "ac-btn-cyan",
  secondary: "ac-btn-secondary",
  success: "ac-btn-green",
  danger: "ac-btn-danger",
  ghost: "ac-btn-ghost",
};

const getToneClass = (tone, isInteractive) => {
  if (tone === "subtle") {
    return isInteractive
      ? "border border-border text-fg-muted hover:text-body hover:border-fg-muted"
      : "border border-border text-fg-muted";
  }
  if (tone === "neutral") {
    return isInteractive
      ? "border border-border text-fg-muted hover:text-body hover:border-fg-muted"
      : "border border-border text-fg-muted";
  }
  if (tone === "warning") {
    return isInteractive
      ? "border border-yellow-500/35 text-status-warning-muted bg-yellow-500/10 hover:border-yellow-400/60 hover:text-status-warning hover:bg-yellow-500/15"
      : "border border-yellow-500/35 text-status-warning-muted bg-yellow-500/10";
  }
  return kStaticToneClassByTone[tone] || kStaticToneClassByTone.primary;
};

const kSizeClassBySize = {
  sm: "h-7 text-xs leading-none px-2.5 py-1 rounded-lg",
  md: "h-9 text-sm font-medium leading-none px-4 rounded-xl",
  lg: "h-10 text-sm font-medium leading-none px-5 rounded-lg",
};
const kIconOnlySizeClassBySize = {
  sm: "h-7 w-7 p-0 rounded-lg",
  md: "h-9 w-9 p-0 rounded-xl",
  lg: "h-10 w-10 p-0 rounded-lg",
};

export const ActionButton = ({
  onClick,
  type = "button",
  disabled = false,
  loading = false,
  tone = "primary",
  size = "sm",
  idleLabel = "Action",
  loadingLabel = "Working...",
  loadingMode = "replace",
  className = "",
  idleIcon = null,
  idleIconClassName = "h-3 w-3",
  iconOnly = false,
  title = "",
  ariaLabel = "",
}) => {
  const isDisabled = disabled || loading;
  const isInteractive = !isDisabled;
  const toneClass = getToneClass(tone, isInteractive);
  const sizeClass = iconOnly
    ? kIconOnlySizeClassBySize[size] || kIconOnlySizeClassBySize.sm
    : kSizeClassBySize[size] || kSizeClassBySize.sm;
  const loadingClass = loading
    ? `cursor-not-allowed ${
        tone === "warning"
          ? "opacity-90 animate-pulse shadow-[0_0_0_1px_rgba(234,179,8,0.22),0_0_18px_rgba(234,179,8,0.12)]"
          : "opacity-80"
      }`
    : "";
  const spinnerSizeClass =
    size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
  const isInlineLoading = loadingMode === "inline";
  const IdleIcon = idleIcon;
  const idleContent =
    iconOnly && IdleIcon
      ? html`<${IdleIcon} className=${idleIconClassName} />`
      : IdleIcon
        ? html`
            <span class="inline-flex items-center gap-1.5 leading-none">
              <${IdleIcon} className=${idleIconClassName} />
              ${idleLabel}
            </span>
          `
        : idleLabel;
  const currentLabel = loading && !isInlin
Download .txt
gitextract_dikz81a3/

├── .github/
│   ├── FUNDING.yml
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .npmrc
├── AGENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── bin/
│   └── alphaclaw.js
├── lib/
│   ├── cli/
│   │   ├── git-runtime.js
│   │   ├── git-sync.js
│   │   └── openclaw-config-restore.js
│   ├── plugin/
│   │   └── usage-tracker/
│   │       ├── index.js
│   │       └── openclaw.plugin.json
│   ├── public/
│   │   ├── css/
│   │   │   ├── agents.css
│   │   │   ├── chat.css
│   │   │   ├── cron.css
│   │   │   ├── explorer.css
│   │   │   ├── shell.css
│   │   │   ├── tailwind.input.css
│   │   │   └── theme.css
│   │   ├── js/
│   │   │   ├── app.js
│   │   │   ├── components/
│   │   │   │   ├── action-button.js
│   │   │   │   ├── add-channel-menu.js
│   │   │   │   ├── agent-send-modal.js
│   │   │   │   ├── agents-tab/
│   │   │   │   │   ├── agent-bindings-section/
│   │   │   │   │   │   ├── channel-item-trailing.js
│   │   │   │   │   │   ├── helpers.js
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── use-agent-bindings.js
│   │   │   │   │   │   └── use-channel-items.js
│   │   │   │   │   ├── agent-detail-panel.js
│   │   │   │   │   ├── agent-identity-section.js
│   │   │   │   │   ├── agent-overview/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── manage-card.js
│   │   │   │   │   │   ├── model-card.js
│   │   │   │   │   │   ├── tools-card.js
│   │   │   │   │   │   ├── use-model-card.js
│   │   │   │   │   │   ├── use-workspace-card.js
│   │   │   │   │   │   └── workspace-card.js
│   │   │   │   │   ├── agent-pairing-section.js
│   │   │   │   │   ├── agent-tools/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── tool-catalog.js
│   │   │   │   │   │   └── use-agent-tools.js
│   │   │   │   │   ├── create-agent-modal.js
│   │   │   │   │   ├── create-channel-modal.js
│   │   │   │   │   ├── delete-agent-dialog.js
│   │   │   │   │   ├── edit-agent-modal.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── use-agents.js
│   │   │   │   ├── badge.js
│   │   │   │   ├── channel-account-status-badge.js
│   │   │   │   ├── channel-login-modal.js
│   │   │   │   ├── channel-operations-panel.js
│   │   │   │   ├── channels.js
│   │   │   │   ├── confirm-dialog.js
│   │   │   │   ├── credentials-modal.js
│   │   │   │   ├── cron-tab/
│   │   │   │   │   ├── cron-calendar-helpers.js
│   │   │   │   │   ├── cron-calendar.js
│   │   │   │   │   ├── cron-helpers.js
│   │   │   │   │   ├── cron-insights-panel.js
│   │   │   │   │   ├── cron-job-detail.js
│   │   │   │   │   ├── cron-job-list.js
│   │   │   │   │   ├── cron-job-settings-card.js
│   │   │   │   │   ├── cron-job-trends-panel.js
│   │   │   │   │   ├── cron-job-usage.js
│   │   │   │   │   ├── cron-overview.js
│   │   │   │   │   ├── cron-prompt-editor.js
│   │   │   │   │   ├── cron-run-history-panel.js
│   │   │   │   │   ├── cron-runs-trend-card.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── use-cron-tab.js
│   │   │   │   ├── device-pairings.js
│   │   │   │   ├── doctor/
│   │   │   │   │   ├── findings-list.js
│   │   │   │   │   ├── fix-card-modal.js
│   │   │   │   │   ├── general-warning.js
│   │   │   │   │   ├── helpers.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── summary-cards.js
│   │   │   │   ├── envars.js
│   │   │   │   ├── features.js
│   │   │   │   ├── file-tree.js
│   │   │   │   ├── file-viewer/
│   │   │   │   │   ├── constants.js
│   │   │   │   │   ├── diff-viewer.js
│   │   │   │   │   ├── editor-surface.js
│   │   │   │   │   ├── frontmatter-panel.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── markdown-split-view.js
│   │   │   │   │   ├── media-preview.js
│   │   │   │   │   ├── scroll-sync.js
│   │   │   │   │   ├── sqlite-viewer.js
│   │   │   │   │   ├── status-banners.js
│   │   │   │   │   ├── storage.js
│   │   │   │   │   ├── toolbar.js
│   │   │   │   │   ├── use-editor-line-number-sync.js
│   │   │   │   │   ├── use-editor-selection-restore.js
│   │   │   │   │   ├── use-file-diff.js
│   │   │   │   │   ├── use-file-loader.js
│   │   │   │   │   ├── use-file-viewer-draft-sync.js
│   │   │   │   │   ├── use-file-viewer-hotkeys.js
│   │   │   │   │   ├── use-file-viewer.js
│   │   │   │   │   └── utils.js
│   │   │   │   ├── gateway.js
│   │   │   │   ├── general/
│   │   │   │   │   ├── index.js
│   │   │   │   │   └── use-general-tab.js
│   │   │   │   ├── global-restart-banner.js
│   │   │   │   ├── google/
│   │   │   │   │   ├── account-row.js
│   │   │   │   │   ├── add-account-modal.js
│   │   │   │   │   ├── gmail-setup-wizard.js
│   │   │   │   │   ├── gmail-watch-toggle.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── use-gmail-watch.js
│   │   │   │   │   └── use-google-accounts.js
│   │   │   │   ├── icons.js
│   │   │   │   ├── info-tooltip.js
│   │   │   │   ├── loading-spinner.js
│   │   │   │   ├── modal-shell.js
│   │   │   │   ├── models-tab/
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── model-picker.js
│   │   │   │   │   ├── provider-auth-card.js
│   │   │   │   │   └── use-models.js
│   │   │   │   ├── models.js
│   │   │   │   ├── nodes-tab/
│   │   │   │   │   ├── browser-attach/
│   │   │   │   │   │   └── index.js
│   │   │   │   │   ├── connected-nodes/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   ├── use-connected-nodes-card.js
│   │   │   │   │   │   └── user-connected-nodes.js
│   │   │   │   │   ├── exec-allowlist/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-exec-allowlist.js
│   │   │   │   │   ├── exec-config/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-exec-config.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── setup-wizard/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-setup-wizard.js
│   │   │   │   │   └── use-nodes-tab.js
│   │   │   │   ├── onboarding/
│   │   │   │   │   ├── pairing-utils.js
│   │   │   │   │   ├── use-welcome-codex.js
│   │   │   │   │   ├── use-welcome-pairing.js
│   │   │   │   │   ├── use-welcome-storage.js
│   │   │   │   │   ├── welcome-config.js
│   │   │   │   │   ├── welcome-form-step.js
│   │   │   │   │   ├── welcome-header.js
│   │   │   │   │   ├── welcome-import-step.js
│   │   │   │   │   ├── welcome-pairing-step.js
│   │   │   │   │   ├── welcome-placeholder-review-step.js
│   │   │   │   │   ├── welcome-pre-step.js
│   │   │   │   │   ├── welcome-secret-review-step.js
│   │   │   │   │   ├── welcome-secret-review-utils.js
│   │   │   │   │   └── welcome-setup-step.js
│   │   │   │   ├── overflow-menu.js
│   │   │   │   ├── page-header.js
│   │   │   │   ├── pairings.js
│   │   │   │   ├── pane-shell.js
│   │   │   │   ├── pill-tabs.js
│   │   │   │   ├── pop-actions.js
│   │   │   │   ├── providers.js
│   │   │   │   ├── routes/
│   │   │   │   │   ├── agents-route.js
│   │   │   │   │   ├── browse-route.js
│   │   │   │   │   ├── chat-route.js
│   │   │   │   │   ├── cron-route.js
│   │   │   │   │   ├── doctor-route.js
│   │   │   │   │   ├── envars-route.js
│   │   │   │   │   ├── general-route.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── models-route.js
│   │   │   │   │   ├── nodes-route.js
│   │   │   │   │   ├── providers-route.js
│   │   │   │   │   ├── route-redirect.js
│   │   │   │   │   ├── telegram-route.js
│   │   │   │   │   ├── usage-route.js
│   │   │   │   │   ├── watchdog-route.js
│   │   │   │   │   └── webhooks-route.js
│   │   │   │   ├── scope-picker.js
│   │   │   │   ├── secret-input.js
│   │   │   │   ├── segmented-control.js
│   │   │   │   ├── session-select-field.js
│   │   │   │   ├── sidebar-git-panel.js
│   │   │   │   ├── sidebar.js
│   │   │   │   ├── summary-stat-card.js
│   │   │   │   ├── telegram-workspace/
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── manage.js
│   │   │   │   │   └── onboarding.js
│   │   │   │   ├── theme-toggle.js
│   │   │   │   ├── toast.js
│   │   │   │   ├── toggle-switch.js
│   │   │   │   ├── tooltip.js
│   │   │   │   ├── update-action-button.js
│   │   │   │   ├── update-modal.js
│   │   │   │   ├── usage-tab/
│   │   │   │   │   ├── constants.js
│   │   │   │   │   ├── formatters.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── overview-section.js
│   │   │   │   │   ├── sessions-section.js
│   │   │   │   │   └── use-usage-tab.js
│   │   │   │   ├── watchdog-tab/
│   │   │   │   │   ├── console/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-console.js
│   │   │   │   │   ├── helpers.js
│   │   │   │   │   ├── incidents/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-incidents.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── resource-bar.js
│   │   │   │   │   ├── resources/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-resources.js
│   │   │   │   │   ├── settings/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-settings.js
│   │   │   │   │   ├── terminal/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-terminal.js
│   │   │   │   │   └── use-watchdog-tab.js
│   │   │   │   ├── webhooks/
│   │   │   │   │   ├── create-webhook-modal/
│   │   │   │   │   │   └── index.js
│   │   │   │   │   ├── helpers.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── request-history/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-request-history.js
│   │   │   │   │   ├── webhook-detail/
│   │   │   │   │   │   ├── index.js
│   │   │   │   │   │   └── use-webhook-detail.js
│   │   │   │   │   └── webhook-list/
│   │   │   │   │       ├── index.js
│   │   │   │   │       └── use-webhook-list.js
│   │   │   │   └── welcome/
│   │   │   │       ├── index.js
│   │   │   │       └── use-welcome.js
│   │   │   ├── hooks/
│   │   │   │   ├── use-app-shell-controller.js
│   │   │   │   ├── use-app-shell-ui.js
│   │   │   │   ├── use-browse-navigation.js
│   │   │   │   ├── use-cached-fetch.js
│   │   │   │   ├── use-destination-session-selection.js
│   │   │   │   ├── use-hash-location.js
│   │   │   │   ├── useAgentSessions.js
│   │   │   │   └── usePolling.js
│   │   │   ├── lib/
│   │   │   │   ├── agent-identity.js
│   │   │   │   ├── api-cache.js
│   │   │   │   ├── api.js
│   │   │   │   ├── app-navigation.js
│   │   │   │   ├── browse-draft-state.js
│   │   │   │   ├── browse-file-policies.js
│   │   │   │   ├── browse-restart-policy.js
│   │   │   │   ├── browse-route.js
│   │   │   │   ├── channel-accounts.js
│   │   │   │   ├── channel-create-operation.js
│   │   │   │   ├── channel-provider-availability.js
│   │   │   │   ├── clipboard.js
│   │   │   │   ├── codex-oauth-window.js
│   │   │   │   ├── file-highlighting.js
│   │   │   │   ├── file-tree-utils.js
│   │   │   │   ├── format.js
│   │   │   │   ├── model-catalog.js
│   │   │   │   ├── model-config.js
│   │   │   │   ├── session-keys.js
│   │   │   │   ├── sse.js
│   │   │   │   ├── storage-keys.js
│   │   │   │   ├── syntax-highlighters/
│   │   │   │   │   ├── css.js
│   │   │   │   │   ├── frontmatter.js
│   │   │   │   │   ├── html.js
│   │   │   │   │   ├── index.js
│   │   │   │   │   ├── javascript.js
│   │   │   │   │   ├── json.js
│   │   │   │   │   ├── markdown.js
│   │   │   │   │   └── utils.js
│   │   │   │   ├── telegram-api.js
│   │   │   │   └── ui-settings.js
│   │   │   └── tailwind-config.js
│   │   ├── login.html
│   │   ├── setup.html
│   │   └── shared/
│   │       └── browse-file-policies.json
│   ├── scripts/
│   │   ├── git
│   │   ├── git-askpass
│   │   └── systemctl
│   ├── server/
│   │   ├── agents/
│   │   │   ├── agents.js
│   │   │   ├── bindings.js
│   │   │   ├── channels.js
│   │   │   ├── service.js
│   │   │   └── shared.js
│   │   ├── alphaclaw-version.js
│   │   ├── auth-profiles.js
│   │   ├── chat-ws.js
│   │   ├── commands.js
│   │   ├── constants.js
│   │   ├── cost-utils.js
│   │   ├── cron-service.js
│   │   ├── db/
│   │   │   ├── doctor/
│   │   │   │   ├── index.js
│   │   │   │   └── schema.js
│   │   │   ├── usage/
│   │   │   │   ├── index.js
│   │   │   │   ├── pricing.js
│   │   │   │   ├── schema.js
│   │   │   │   ├── sessions.js
│   │   │   │   ├── shared.js
│   │   │   │   ├── summary.js
│   │   │   │   └── timeseries.js
│   │   │   ├── watchdog/
│   │   │   │   ├── index.js
│   │   │   │   └── schema.js
│   │   │   └── webhooks/
│   │   │       ├── index.js
│   │   │       └── schema.js
│   │   ├── discord-api.js
│   │   ├── doctor/
│   │   │   ├── bootstrap-context.js
│   │   │   ├── constants.js
│   │   │   ├── normalize.js
│   │   │   ├── prompt.js
│   │   │   ├── service.js
│   │   │   └── workspace-fingerprint.js
│   │   ├── env.js
│   │   ├── exec-defaults-config.js
│   │   ├── gateway.js
│   │   ├── gmail-push.js
│   │   ├── gmail-serve.js
│   │   ├── gmail-watch.js
│   │   ├── gog-skill.js
│   │   ├── google-state.js
│   │   ├── helpers.js
│   │   ├── init/
│   │   │   ├── register-server-routes.js
│   │   │   ├── runtime-init.js
│   │   │   └── server-lifecycle.js
│   │   ├── internal-files-migration.js
│   │   ├── log-writer.js
│   │   ├── login-throttle.js
│   │   ├── model-catalog-bootstrap.json
│   │   ├── model-catalog-cache.js
│   │   ├── oauth-callback-middleware.js
│   │   ├── onboarding/
│   │   │   ├── cron.js
│   │   │   ├── github.js
│   │   │   ├── import/
│   │   │   │   ├── import-applier.js
│   │   │   │   ├── import-config.js
│   │   │   │   ├── import-scanner.js
│   │   │   │   ├── import-temp.js
│   │   │   │   └── secret-detector.js
│   │   │   ├── index.js
│   │   │   ├── openclaw.js
│   │   │   ├── validation.js
│   │   │   └── workspace.js
│   │   ├── openclaw-config.js
│   │   ├── openclaw-runtime-env.js
│   │   ├── openclaw-version.js
│   │   ├── operation-events.js
│   │   ├── restart-required-state.js
│   │   ├── routes/
│   │   │   ├── agents.js
│   │   │   ├── auth.js
│   │   │   ├── browse/
│   │   │   │   ├── constants.js
│   │   │   │   ├── file-helpers.js
│   │   │   │   ├── git.js
│   │   │   │   ├── index.js
│   │   │   │   ├── path-utils.js
│   │   │   │   └── sqlite.js
│   │   │   ├── codex.js
│   │   │   ├── cron.js
│   │   │   ├── doctor.js
│   │   │   ├── gmail.js
│   │   │   ├── google.js
│   │   │   ├── models.js
│   │   │   ├── nodes.js
│   │   │   ├── onboarding.js
│   │   │   ├── pages.js
│   │   │   ├── pairings.js
│   │   │   ├── proxy.js
│   │   │   ├── system.js
│   │   │   ├── telegram.js
│   │   │   ├── usage.js
│   │   │   ├── watchdog.js
│   │   │   └── webhooks.js
│   │   ├── slack-api.js
│   │   ├── startup.js
│   │   ├── system-resources.js
│   │   ├── telegram-api.js
│   │   ├── telegram-workspace.js
│   │   ├── topic-registry.js
│   │   ├── usage-tracker-config.js
│   │   ├── utils/
│   │   │   ├── boolean.js
│   │   │   ├── channels.js
│   │   │   ├── command-output.js
│   │   │   ├── json.js
│   │   │   ├── network.js
│   │   │   ├── number.js
│   │   │   └── shell.js
│   │   ├── watchdog-notify.js
│   │   ├── watchdog-terminal-ws.js
│   │   ├── watchdog-terminal.js
│   │   ├── watchdog.js
│   │   ├── webhook-middleware.js
│   │   └── webhooks.js
│   ├── server.js
│   └── setup/
│       ├── core-prompts/
│       │   ├── AGENTS.md
│       │   └── TOOLS.md
│       ├── env.template
│       ├── gitignore
│       ├── hourly-git-sync.sh
│       └── skills/
│           └── gog-cli/
│               ├── calendar.md
│               ├── contacts.md
│               ├── docs.md
│               ├── drive.md
│               ├── gmail.md
│               ├── meet.md
│               ├── sheets.md
│               └── tasks.md
├── package.json
├── scripts/
│   ├── build-ui.mjs
│   └── dev/
│       └── crash-watchdog-config.sh
├── tailwind.config.cjs
├── tests/
│   ├── bin/
│   │   ├── alphaclaw.test.js
│   │   └── openclaw-config-restore.test.js
│   ├── frontend/
│   │   ├── agent-identity.test.js
│   │   ├── api.test.js
│   │   ├── browse-draft-state.test.js
│   │   ├── channel-create-operation.test.js
│   │   ├── channel-provider-availability.test.js
│   │   ├── clipboard.test.js
│   │   ├── codex-oauth-window.test.js
│   │   ├── cron-calendar-helpers.test.js
│   │   ├── cron-helpers.test.js
│   │   ├── doctor-helpers.test.js
│   │   ├── file-tree-utils.test.js
│   │   ├── file-viewer-utils.test.js
│   │   ├── model-catalog.test.js
│   │   ├── model-config.test.js
│   │   ├── pairing-utils.test.js
│   │   ├── session-keys.test.js
│   │   ├── syntax-highlighters.test.js
│   │   ├── watchdog-helpers.test.js
│   │   ├── welcome-config.test.js
│   │   └── welcome-secret-review-utils.test.js
│   └── server/
│       ├── agents-service.test.js
│       ├── alphaclaw-version.test.js
│       ├── auth-profiles.test.js
│       ├── chat-ws.test.js
│       ├── commands.test.js
│       ├── cost-utils.test.js
│       ├── cron-service.test.js
│       ├── doctor-db.test.js
│       ├── doctor-normalize.test.js
│       ├── doctor-prompt.test.js
│       ├── doctor-service.test.js
│       ├── exec-defaults-config.test.js
│       ├── express-runtime-guard.test.js
│       ├── gateway.test.js
│       ├── git-runtime.test.js
│       ├── git-shim.test.js
│       ├── git-sync-path.test.js
│       ├── gmail-push.test.js
│       ├── gmail-watch.test.js
│       ├── gog-skill.test.js
│       ├── helpers.test.js
│       ├── import-applier.test.js
│       ├── import-scanner.test.js
│       ├── import-temp.test.js
│       ├── internal-files-migration.test.js
│       ├── login-throttle.test.js
│       ├── model-catalog-cache.test.js
│       ├── oauth-callback-middleware.test.js
│       ├── onboarding-github.test.js
│       ├── onboarding-openclaw.test.js
│       ├── onboarding-validation.test.js
│       ├── onboarding-workspace.test.js
│       ├── openclaw-runtime-env.test.js
│       ├── openclaw-version.test.js
│       ├── operation-events.test.js
│       ├── routes-agents.test.js
│       ├── routes-auth.test.js
│       ├── routes-browse.test.js
│       ├── routes-cron.test.js
│       ├── routes-doctor.test.js
│       ├── routes-models.test.js
│       ├── routes-nodes.test.js
│       ├── routes-onboarding.test.js
│       ├── routes-pairings.test.js
│       ├── routes-system.test.js
│       ├── routes-telegram.test.js
│       ├── routes-usage.test.js
│       ├── routes-watchdog-test-notification.test.js
│       ├── routes-watchdog.test.js
│       ├── routes-webhooks.test.js
│       ├── secret-detector.test.js
│       ├── slack-api.test.js
│       ├── startup.test.js
│       ├── telegram-workspace.test.js
│       ├── topic-registry.test.js
│       ├── usage-db.test.js
│       ├── usage-tracker-config.test.js
│       ├── utils-boolean.test.js
│       ├── utils-json.test.js
│       ├── utils-shell.test.js
│       ├── watchdog-db.test.js
│       ├── watchdog-notify.test.js
│       ├── watchdog.test.js
│       ├── webhook-middleware.test.js
│       ├── webhooks-db.test.js
│       └── webhooks.test.js
└── vitest.config.js
Download .txt
SYMBOL INDEX (110 symbols across 11 files)

FILE: lib/public/js/components/channels.js
  constant ALL_CHANNELS (line 35) | const ALL_CHANNELS = ["telegram", "discord", "slack", "whatsapp"];

FILE: lib/public/js/components/pairings.js
  constant ALL_CHANNELS (line 66) | const ALL_CHANNELS = ['telegram', 'discord', 'slack', 'whatsapp'];
  function Pairings (line 77) | function Pairings({

FILE: lib/public/js/components/scope-picker.js
  constant SERVICES (line 6) | const SERVICES = [
  constant API_ENABLE_URLS (line 17) | const API_ENABLE_URLS = {
  function getApiEnableUrl (line 28) | function getApiEnableUrl(svc) {
  function ScopePicker (line 32) | function ScopePicker({ scopes, onToggle, apiStatus, loading }) {
  function toggleScopeLogic (line 80) | function toggleScopeLogic(scopes, scope) {
  function getDefaultScopes (line 99) | function getDefaultScopes() {

FILE: lib/public/js/components/toast.js
  function showToast (line 35) | function showToast(text, type = "info") {
  function ToastContainer (line 39) | function ToastContainer({

FILE: lib/public/js/lib/api.js
  function fetchStatus (line 63) | async function fetchStatus() {
  function fetchPairings (line 68) | async function fetchPairings() {
  function approvePairing (line 73) | async function approvePairing(id, channel, accountId = "") {
  function rejectPairing (line 82) | async function rejectPairing(id, channel, accountId = "") {
  function fetchGoogleAccounts (line 91) | async function fetchGoogleAccounts() {
  function fetchGoogleStatus (line 96) | async function fetchGoogleStatus(accountId = "") {
  function fetchGoogleCredentials (line 104) | async function fetchGoogleCredentials({
  function checkGoogleApis (line 116) | async function checkGoogleApis(accountId = "") {
  function saveGoogleCredentials (line 124) | async function saveGoogleCredentials({
  function saveGoogleAccount (line 149) | async function saveGoogleAccount({
  function disconnectGoogle (line 164) | async function disconnectGoogle(accountId = "") {
  function restartGateway (line 339) | async function restartGateway() {
  function fetchRestartStatus (line 344) | async function fetchRestartStatus() {
  function dismissRestartStatus (line 349) | async function dismissRestartStatus() {
  function fetchWatchdogStatus (line 356) | async function fetchWatchdogStatus() {
  function fetchUsageSummary (line 361) | async function fetchUsageSummary(days = 30) {
  function fetchUsageSessions (line 367) | async function fetchUsageSessions(limit = 50) {
  function fetchUsageSessionDetail (line 373) | async function fetchUsageSessionDetail(sessionId) {
  function fetchUsageSessionTimeSeries (line 380) | async function fetchUsageSessionTimeSeries(sessionId, maxPoints = 100) {
  function fetchWatchdogEvents (line 389) | async function fetchWatchdogEvents(limit = 20) {
  function fetchWatchdogLogs (line 396) | async function fetchWatchdogLogs(tail = 65536) {
  function createWatchdogTerminalSession (line 404) | async function createWatchdogTerminalSession() {
  function fetchWatchdogTerminalOutput (line 411) | async function fetchWatchdogTerminalOutput(sessionId, cursor = 0) {
  function sendWatchdogTerminalInput (line 420) | async function sendWatchdogTerminalInput(sessionId, input = "") {
  function closeWatchdogTerminalSession (line 432) | async function closeWatchdogTerminalSession(sessionId) {
  function triggerWatchdogRepair (line 443) | async function triggerWatchdogRepair() {
  function fetchWatchdogResources (line 448) | async function fetchWatchdogResources() {
  function fetchWatchdogSettings (line 453) | async function fetchWatchdogSettings() {
  function updateWatchdogSettings (line 458) | async function updateWatchdogSettings(settings) {
  function fetchDashboardUrl (line 467) | async function fetchDashboardUrl() {
  function fetchAlphaclawVersion (line 472) | async function fetchAlphaclawVersion(refresh = false) {
  function fetchAlphaclawReleaseNotes (line 478) | async function fetchAlphaclawReleaseNotes(tag = "") {
  function updateAlphaclaw (line 514) | async function updateAlphaclaw() {
  function fetchSyncCron (line 519) | async function fetchSyncCron() {
  function updateSyncCron (line 534) | async function updateSyncCron(payload) {
  function fetchCronJobs (line 553) | async function fetchCronJobs({ sortBy = "nextRunAtMs", sortDir = "asc" }...
  function fetchCronStatus (line 562) | async function fetchCronStatus() {
  function fetchCronJobRuns (line 567) | async function fetchCronJobRuns(
  function fetchCronJobUsage (line 591) | async function fetchCronJobUsage(id, { days = 30 } = {}) {
  function fetchCronJobTrends (line 598) | async function fetchCronJobTrends(id, { range = "7d" } = {}) {
  function fetchCronBulkUsage (line 605) | async function fetchCronBulkUsage({ days = 30 } = {}) {
  function fetchCronBulkRuns (line 611) | async function fetchCronBulkRuns({
  function triggerCronJobRun (line 629) | async function triggerCronJobRun(id) {
  function setCronJobEnabled (line 635) | async function setCronJobEnabled(id, enabled) {
  function updateCronJobPrompt (line 644) | async function updateCronJobPrompt(id, message) {
  function updateCronJobRouting (line 654) | async function updateCronJobRouting(
  function fetchDevicePairings (line 679) | async function fetchDevicePairings() {
  function approveDevice (line 684) | async function approveDevice(id) {
  function rejectDevice (line 690) | async function rejectDevice(id) {
  function fetchOnboardStatus (line 797) | async function fetchOnboardStatus() {
  function runOnboard (line 802) | async function runOnboard(vars, modelKey, { importMode = false } = {}) {
  function verifyGithubOnboardingRepo (line 811) | async function verifyGithubOnboardingRepo(repo, token, mode = "new") {
  function scanImportRepo (line 820) | async function scanImportRepo(tempDir) {
  function applyImport (line 829) | async function applyImport({
  function fetchEnvVars (line 1109) | async function fetchEnvVars() {
  function saveEnvVars (line 1114) | async function saveEnvVars(vars) {
  function fetchWebhooks (line 1147) | async function fetchWebhooks() {
  function fetchWebhookDetail (line 1152) | async function fetchWebhookDetail(name) {
  function createWebhook (line 1157) | async function createWebhook(
  function deleteWebhook (line 1173) | async function deleteWebhook(name, { deleteTransformDir = false } = {}) {
  function updateWebhookDestination (line 1182) | async function updateWebhookDestination(name, { destination = null } = {...
  function createWebhookOauthCallback (line 1196) | async function createWebhookOauthCallback(name) {
  function rotateWebhookOauthCallback (line 1206) | async function rotateWebhookOauthCallback(name) {
  function deleteWebhookOauthCallback (line 1216) | async function deleteWebhookOauthCallback(name) {
  function fetchWebhookRequests (line 1226) | async function fetchWebhookRequests(
  function fetchWebhookRequest (line 1241) | async function fetchWebhookRequest(name, id) {

FILE: lib/server/constants.js
  constant ALPHACLAW_DIR (line 10) | const ALPHACLAW_DIR = kRootDir;
  constant PORT (line 15) | const PORT = parseInt(process.env.PORT || "3000", 10);
  constant GATEWAY_HOST (line 17) | const GATEWAY_HOST = "127.0.0.1";
  constant OPENCLAW_DIR (line 19) | const OPENCLAW_DIR = path.join(kRootDir, ".openclaw");
  constant GATEWAY_TOKEN (line 20) | const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
  constant ENV_FILE_PATH (line 21) | const ENV_FILE_PATH = path.join(kRootDir, ".env");
  constant WORKSPACE_DIR (line 22) | const WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
  constant AUTH_PROFILES_PATH (line 24) | const AUTH_PROFILES_PATH = path.join(
  constant CODEX_PROFILE_ID (line 31) | const CODEX_PROFILE_ID = "openai-codex:codex-cli";
  constant CODEX_OAUTH_CLIENT_ID (line 32) | const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
  constant CODEX_OAUTH_AUTHORIZE_URL (line 33) | const CODEX_OAUTH_AUTHORIZE_URL = "https://auth.openai.com/oauth/authori...
  constant CODEX_OAUTH_TOKEN_URL (line 34) | const CODEX_OAUTH_TOKEN_URL = "https://auth.openai.com/oauth/token";
  constant CODEX_OAUTH_REDIRECT_URI (line 35) | const CODEX_OAUTH_REDIRECT_URI = "http://localhost:1455/auth/callback";
  constant CODEX_OAUTH_SCOPE (line 36) | const CODEX_OAUTH_SCOPE = "openid profile email offline_access";
  constant CODEX_JWT_CLAIM_PATH (line 37) | const CODEX_JWT_CLAIM_PATH = "https://api.openai.com/auth";
  constant SCOPE_MAP (line 313) | const SCOPE_MAP = {
  constant REVERSE_SCOPE_MAP (line 331) | const REVERSE_SCOPE_MAP = Object.fromEntries(
  constant BASE_SCOPES (line 334) | const BASE_SCOPES = [
  constant GOG_CONFIG_DIR (line 339) | const GOG_CONFIG_DIR = path.join(OPENCLAW_DIR, "gogcli");
  constant GOG_CREDENTIALS_PATH (line 340) | const GOG_CREDENTIALS_PATH = path.join(GOG_CONFIG_DIR, "credentials.json");
  constant GOG_STATE_PATH (line 341) | const GOG_STATE_PATH = path.join(GOG_CONFIG_DIR, "state.json");
  constant GOG_KEYRING_PASSWORD (line 342) | const GOG_KEYRING_PASSWORD = process.env.GOG_KEYRING_PASSWORD || "alphac...
  constant API_TEST_COMMANDS (line 363) | const API_TEST_COMMANDS = {
  constant SETUP_API_PREFIXES (line 391) | const SETUP_API_PREFIXES = [

FILE: lib/server/model-catalog-cache.js
  method getCatalogResponse (line 275) | async getCatalogResponse() {
  method markStale (line 323) | markStale() {

FILE: tests/frontend/browse-draft-state.test.js
  method length (line 6) | get length() {

FILE: tests/server/gateway.test.js
  method on (line 30) | on(event, handler) {

FILE: tests/server/git-runtime.test.js
  method accessSync (line 13) | accessSync(targetPath) {
  method accessSync (line 31) | accessSync(targetPath) {

FILE: tests/server/gmail-push.test.js
  method status (line 27) | status(code) {
  method json (line 31) | json(payload) {
  method send (line 35) | send(payload) {
Condensed preview — 480 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (3,443K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 15,
    "preview": "github: chrysb\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 422,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    runs-on: ubuntu-lates"
  },
  {
    "path": ".gitignore",
    "chars": 198,
    "preview": "node_modules/\n.DS_Store\n.env\n.cursor/\ncoverage/\nworkspace/.openclaw/\n\n# Build artifacts (generated by npm run build:ui)\n"
  },
  {
    "path": ".npmrc",
    "chars": 45,
    "preview": "@chrysb:registry=https://registry.npmjs.org/\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 16657,
    "preview": "## Project Overview\n\n### AlphaClaw Project Context\n\nAlphaClaw is the ops and setup layer around OpenClaw. It provides a "
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 4054,
    "preview": "# Contributing to AlphaClaw\n\nThanks for your interest in contributing to AlphaClaw. This document covers how we work, wh"
  },
  {
    "path": "LICENSE",
    "chars": 1063,
    "preview": "MIT License\n\nCopyright (c) 2025 chrysb\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 14389,
    "preview": "<p align=\"center\">\n  <img width=\"771\" height=\"339\" alt=\"image\" src=\"https://github.com/user-attachments/assets/b96b45ab-"
  },
  {
    "path": "bin/alphaclaw.js",
    "chars": 27513,
    "preview": "#!/usr/bin/env node\n\"use strict\";\n\nconst fs = require(\"fs\");\nconst os = require(\"os\");\nconst path = require(\"path\");\ncon"
  },
  {
    "path": "lib/cli/git-runtime.js",
    "chars": 2763,
    "preview": "const fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execSync } = require(\"child_process\");\n\nconst normalizeG"
  },
  {
    "path": "lib/cli/git-sync.js",
    "chars": 665,
    "preview": "const normalizeGitSyncFilePath = (requestedFilePath) => {\n  const rawPath = String(requestedFilePath || \"\").trim();\n  if"
  },
  {
    "path": "lib/cli/openclaw-config-restore.js",
    "chars": 4197,
    "preview": "\"use strict\";\n\nconst fs = require(\"fs\");\nconst os = require(\"os\");\nconst path = require(\"path\");\nconst { execSync } = re"
  },
  {
    "path": "lib/plugin/usage-tracker/index.js",
    "chars": 9355,
    "preview": "const fs = require(\"fs\");\nconst os = require(\"os\");\nconst path = require(\"path\");\nconst { DatabaseSync } = require(\"node"
  },
  {
    "path": "lib/plugin/usage-tracker/openclaw.plugin.json",
    "chars": 131,
    "preview": "{\n  \"id\": \"usage-tracker\",\n  \"configSchema\": {\n    \"type\": \"object\",\n    \"additionalProperties\": false,\n    \"properties\""
  },
  {
    "path": "lib/public/css/agents.css",
    "chars": 2030,
    "preview": "/* ── Agents detail layout ────────────────────── */\n\n.app-content-pane.agents-pane {\n  overflow: hidden;\n  padding-left"
  },
  {
    "path": "lib/public/css/chat.css",
    "chars": 7394,
    "preview": "/* ── Chat route ──────────────────────────────── */\n\n.app-content-pane.chat-pane {\n  padding: 0;\n}\n\n.chat-route-shell {"
  },
  {
    "path": "lib/public/css/cron.css",
    "chars": 13594,
    "preview": ".app-content-pane.cron-pane {\n  padding: 24px 32px 12px;\n  overflow: hidden;\n}\n\n.cron-tab-shell {\n  height: 100%;\n  disp"
  },
  {
    "path": "lib/public/css/explorer.css",
    "chars": 33814,
    "preview": "/* ── Browse/Explorer mode ─────────────────────── */\n\n.sidebar-tabs {\n  display: flex;\n  align-items: center;\n  gap: 8p"
  },
  {
    "path": "lib/public/css/shell.css",
    "chars": 12849,
    "preview": "/* ── App shell grid ─────────────────────────────── */\n\n.app-shell {\n  --sidebar-width: 220px;\n  display: grid;\n  grid-"
  },
  {
    "path": "lib/public/css/tailwind.input.css",
    "chars": 59,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"
  },
  {
    "path": "lib/public/css/theme.css",
    "chars": 23218,
    "preview": ":root {\n  --bg: #0d121b;\n  --bg-sidebar: #0f141f;\n  --bg-content: #0f1521;\n  --bg-hover: rgba(99, 235, 255, 0.05);\n  --b"
  },
  {
    "path": "lib/public/js/app.js",
    "chars": 19722,
    "preview": "import { h, render } from \"preact\";\nimport { useState, useEffect } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {\n"
  },
  {
    "path": "lib/public/js/components/action-button.js",
    "chars": 4127,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { LoadingSpinner } from \"./loading-spinner.js\";\n\nconst html = "
  },
  {
    "path": "lib/public/js/components/add-channel-menu.js",
    "chars": 1615,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"./action-button.js\";\nimport { AddLineIc"
  },
  {
    "path": "lib/public/js/components/agent-send-modal.js",
    "chars": 3854,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ModalShe"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-bindings-section/channel-item-trailing.js",
    "chars": 6292,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Badge } from \"../../badge.js\";\nimport { ChannelAccountStatus"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-bindings-section/helpers.js",
    "chars": 1945,
    "preview": "import {\n  isImplicitDefaultAccount,\n  resolveChannelAccountLabel,\n} from \"../../../lib/channel-accounts.js\";\n\nexport co"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-bindings-section/index.js",
    "chars": 6667,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { isChannelProviderDisabledForAdd } from \"../../../lib/channel"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js",
    "chars": 7232,
    "preview": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"preact/hooks\";\nimport {\n  deleteChannelAccount,\n  fe"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-bindings-section/use-channel-items.js",
    "chars": 6942,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-detail-panel.js",
    "chars": 5529,
    "preview": "import { h } from \"preact\";\nimport { useState, useCallback, useMemo } from \"preact/hooks\";\nimport htm from \"htm\";\nimport"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-identity-section.js",
    "chars": 6380,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionBu"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-overview/index.js",
    "chars": 1713,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ChannelOperationsPanel } from \"../../channel-operations-pane"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-overview/manage-card.js",
    "chars": 1131,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../../action-button.js\";\n\nconst html = "
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-overview/model-card.js",
    "chars": 5467,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Badge } from \"../../badge.js\";\nimport { LoadingSpinner } fro"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-overview/tools-card.js",
    "chars": 1534,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { kProfileLabels } from \"../agent-tools/tool-catalog.js\";\n\ncon"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-overview/use-model-card.js",
    "chars": 4788,
    "preview": "import { useEffect, useMemo, useState } from \"preact/hooks\";\nimport { useModels } from \"../../models-tab/use-models.js\";"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-overview/use-workspace-card.js",
    "chars": 1411,
    "preview": "import { useEffect, useState } from \"preact/hooks\";\nimport { fetchAgentWorkspaceSize } from \"../../../lib/api.js\";\n\nexpo"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-overview/workspace-card.js",
    "chars": 1543,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { formatBytes } from \"../../../lib/format.js\";\nimport { useWor"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-pairing-section.js",
    "chars": 10328,
    "preview": "import { h } from \"preact\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"preact/hooks\";"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-tools/index.js",
    "chars": 3328,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ToggleSwitch } from \"../../toggle-switch.js\";\nimport { InfoT"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-tools/tool-catalog.js",
    "chars": 7537,
    "preview": "/**\n * Static tool catalog mirroring OpenClaw's tool-catalog.ts.\n * Grouped and labeled for the AlphaClaw Setup UI.\n */\n"
  },
  {
    "path": "lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js",
    "chars": 3797,
    "preview": "import { useState, useCallback, useMemo, useEffect, useRef } from \"preact/hooks\";\nimport {\n  resolveToolStates,\n  derive"
  },
  {
    "path": "lib/public/js/components/agents-tab/create-agent-modal.js",
    "chars": 6005,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {"
  },
  {
    "path": "lib/public/js/components/agents-tab/create-channel-modal.js",
    "chars": 25018,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {"
  },
  {
    "path": "lib/public/js/components/agents-tab/delete-agent-dialog.js",
    "chars": 1295,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ConfirmD"
  },
  {
    "path": "lib/public/js/components/agents-tab/edit-agent-modal.js",
    "chars": 3007,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionBu"
  },
  {
    "path": "lib/public/js/components/agents-tab/index.js",
    "chars": 4603,
    "preview": "import { h } from \"preact\";\nimport { useState, useEffect, useCallback } from \"preact/hooks\";\nimport htm from \"htm\";\nimpo"
  },
  {
    "path": "lib/public/js/components/agents-tab/use-agents.js",
    "chars": 2044,
    "preview": "import { useCallback, useEffect, useState } from \"preact/hooks\";\nimport {\n  createAgent,\n  deleteAgent,\n  fetchAgents,\n "
  },
  {
    "path": "lib/public/js/components/badge.js",
    "chars": 688,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nconst kToneClasses = {\n  success: \"bg-gre"
  },
  {
    "path": "lib/public/js/components/channel-account-status-badge.js",
    "chars": 932,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Badge } from \"./badge.js\";\n\nconst html = htm.bind(h);\n\nexpor"
  },
  {
    "path": "lib/public/js/components/channel-login-modal.js",
    "chars": 2454,
    "preview": "import { h } from \"https://esm.sh/preact\";\nimport htm from \"https://esm.sh/htm\";\nimport { ActionButton } from \"./action-"
  },
  {
    "path": "lib/public/js/components/channel-operations-panel.js",
    "chars": 786,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { AgentBindingsSection } from \"./agents-tab/agent-bindings-sec"
  },
  {
    "path": "lib/public/js/components/channels.js",
    "chars": 30422,
    "preview": "import { h } from \"preact\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"preact/hooks\";\nimport ht"
  },
  {
    "path": "lib/public/js/components/confirm-dialog.js",
    "chars": 2082,
    "preview": "import { h } from \"preact\";\nimport { useEffect } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionButton } fro"
  },
  {
    "path": "lib/public/js/components/credentials-modal.js",
    "chars": 14362,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { "
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-calendar-helpers.js",
    "chars": 13239,
    "preview": "const kMinuteMs = 60 * 1000;\nconst kHourMs = 60 * kMinuteMs;\nconst kDayMs = 24 * kHourMs;\nconst kRollingPastDays = 3;\nco"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-calendar.js",
    "chars": 28600,
    "preview": "import { h } from \"preact\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"preact/hooks\";\nimport ht"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-helpers.js",
    "chars": 14574,
    "preview": "import {\n  formatDurationCompactMs,\n  formatInteger,\n  formatLocaleDateTimeWithTodayTime,\n  formatUsd,\n} from \"../../lib"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-insights-panel.js",
    "chars": 9537,
    "preview": "import { h } from \"preact\";\nimport { useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { SegmentedC"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-job-detail.js",
    "chars": 5245,
    "preview": "import { h } from \"preact\";\nimport { useMemo } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionButton } from "
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-job-list.js",
    "chars": 12426,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\n"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-job-settings-card.js",
    "chars": 8668,
    "preview": "import { h } from \"preact\";\nimport { useMemo } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionButton } from "
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-job-trends-panel.js",
    "chars": 10841,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\n"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-job-usage.js",
    "chars": 3244,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { formatCost, formatTokenCount } from \"./cron-helpers.js\";\nimp"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-overview.js",
    "chars": 12949,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-prompt-editor.js",
    "chars": 6291,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\n"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-run-history-panel.js",
    "chars": 12364,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { SegmentedControl } from \"../segmented-control.js\";\nimport {\n"
  },
  {
    "path": "lib/public/js/components/cron-tab/cron-runs-trend-card.js",
    "chars": 13562,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\n"
  },
  {
    "path": "lib/public/js/components/cron-tab/index.js",
    "chars": 9225,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\n"
  },
  {
    "path": "lib/public/js/components/cron-tab/use-cron-tab.js",
    "chars": 16303,
    "preview": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"preact/hooks\";\nimport { usePolling } from "
  },
  {
    "path": "lib/public/js/components/device-pairings.js",
    "chars": 2486,
    "preview": "import { h } from 'preact';\nimport { useState } from 'preact/hooks';\nimport htm from 'htm';\nimport { ActionButton } from"
  },
  {
    "path": "lib/public/js/components/doctor/findings-list.js",
    "chars": 14735,
    "preview": "import { h } from \"preact\";\nimport { useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { Badge } from \"../ba"
  },
  {
    "path": "lib/public/js/components/doctor/fix-card-modal.js",
    "chars": 1680,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { sendDoctorCardFix, updateDoctorCardStatus } from \"../../lib/"
  },
  {
    "path": "lib/public/js/components/doctor/general-warning.js",
    "chars": 1173,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../action-button.js\";\nimport { getDocto"
  },
  {
    "path": "lib/public/js/components/doctor/helpers.js",
    "chars": 6521,
    "preview": "export const getDoctorPriorityTone = (priority = \"\") => {\n  const normalized = String(priority || \"\")\n    .trim()\n    .t"
  },
  {
    "path": "lib/public/js/components/doctor/index.js",
    "chars": 23072,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {"
  },
  {
    "path": "lib/public/js/components/doctor/summary-cards.js",
    "chars": 751,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { SummaryStatCard } from \"../summary-stat-card.js\";\nimport { b"
  },
  {
    "path": "lib/public/js/components/envars.js",
    "chars": 21693,
    "preview": "import { h } from \"preact\";\nimport {\n  useState,\n  useEffect,\n  useCallback,\n  useRef,\n} from \"preact/hooks\";\nimport htm"
  },
  {
    "path": "lib/public/js/components/features.js",
    "chars": 2463,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { fetchEnvVars } from \"../lib/api.js\";\nimport { useCachedFetch"
  },
  {
    "path": "lib/public/js/components/file-tree.js",
    "chars": 41586,
    "preview": "import { h } from \"preact\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"preact/hooks\";"
  },
  {
    "path": "lib/public/js/components/file-viewer/constants.js",
    "chars": 345,
    "preview": "export {\n  kFileViewerModeStorageKey,\n  kEditorSelectionStorageKey,\n} from \"../../lib/storage-keys.js\";\nexport const kLo"
  },
  {
    "path": "lib/public/js/components/file-viewer/diff-viewer.js",
    "chars": 1379,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { LoadingSpinner } from \"../loading-spinner.js\";\n\nconst html ="
  },
  {
    "path": "lib/public/js/components/file-viewer/editor-surface.js",
    "chars": 4093,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nconst EditorTextarea = ({\n  overlay = fal"
  },
  {
    "path": "lib/public/js/components/file-viewer/frontmatter-panel.js",
    "chars": 1781,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { formatFrontmatterValue } from \"../../lib/syntax-highlighters"
  },
  {
    "path": "lib/public/js/components/file-viewer/index.js",
    "chars": 9117,
    "preview": "import { h } from \"preact\";\nimport { useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { LoadingSpinner } fr"
  },
  {
    "path": "lib/public/js/components/file-viewer/markdown-split-view.js",
    "chars": 1792,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { EditorSurface } from \"./editor-surface.js\";\n\nconst html = ht"
  },
  {
    "path": "lib/public/js/components/file-viewer/media-preview.js",
    "chars": 1097,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nexport const MediaPreview = ({\n  isImageF"
  },
  {
    "path": "lib/public/js/components/file-viewer/scroll-sync.js",
    "chars": 3054,
    "preview": "import { useRef } from \"preact/hooks\";\n\nexport const getScrollRatio = (element) => {\n  if (!element) return 0;\n  const m"
  },
  {
    "path": "lib/public/js/components/file-viewer/sqlite-viewer.js",
    "chars": 7717,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { LoadingSpinner } from \"../loading-spinner.js\";\n\nconst html ="
  },
  {
    "path": "lib/public/js/components/file-viewer/status-banners.js",
    "chars": 1844,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../action-button.js\";\nimport { LockLine"
  },
  {
    "path": "lib/public/js/components/file-viewer/storage.js",
    "chars": 1609,
    "preview": "import {\n  kEditorSelectionStorageKey,\n  kFileViewerModeStorageKey,\n} from \"./constants.js\";\n\nexport const readStoredFil"
  },
  {
    "path": "lib/public/js/components/file-viewer/toolbar.js",
    "chars": 4304,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../action-button.js\";\nimport { Segmente"
  },
  {
    "path": "lib/public/js/components/file-viewer/use-editor-line-number-sync.js",
    "chars": 1244,
    "preview": "import { useCallback, useEffect } from \"preact/hooks\";\n\nexport const useEditorLineNumberSync = ({\n  enabled = false,\n  s"
  },
  {
    "path": "lib/public/js/components/file-viewer/use-editor-selection-restore.js",
    "chars": 7049,
    "preview": "import { useEffect, useRef } from \"preact/hooks\";\nimport { readStoredEditorSelection } from \"./storage.js\";\nimport { cla"
  },
  {
    "path": "lib/public/js/components/file-viewer/use-file-diff.js",
    "chars": 1627,
    "preview": "import { useEffect, useState } from \"preact/hooks\";\nimport { fetchBrowseFileDiff } from \"../../lib/api.js\";\n\nexport cons"
  },
  {
    "path": "lib/public/js/components/file-viewer/use-file-loader.js",
    "chars": 9799,
    "preview": "import { useEffect, useRef } from \"preact/hooks\";\nimport { fetchBrowseSqliteTable, fetchFileContent } from \"../../lib/ap"
  },
  {
    "path": "lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js",
    "chars": 946,
    "preview": "import { useEffect } from \"preact/hooks\";\nimport {\n  clearStoredFileDraft,\n  updateDraftIndex,\n  writeStoredFileDraft,\n}"
  },
  {
    "path": "lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js",
    "chars": 783,
    "preview": "import { useEffect } from \"preact/hooks\";\n\nexport const useFileViewerHotkeys = ({\n  canEditFile,\n  isPreviewOnly,\n  isDi"
  },
  {
    "path": "lib/public/js/components/file-viewer/use-file-viewer.js",
    "chars": 16483,
    "preview": "import { useCallback, useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport { marked } from \"marked\";\nimpor"
  },
  {
    "path": "lib/public/js/components/file-viewer/utils.js",
    "chars": 762,
    "preview": "export const parsePathSegments = (inputPath) =>\n  String(inputPath || \"\")\n    .split(\"/\")\n    .map((part) => part.trim()"
  },
  {
    "path": "lib/public/js/components/gateway.js",
    "chars": 6093,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {"
  },
  {
    "path": "lib/public/js/components/general/index.js",
    "chars": 8232,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Gateway } from \"../gateway.js\";\nimport { Channels } from \".."
  },
  {
    "path": "lib/public/js/components/general/use-general-tab.js",
    "chars": 9129,
    "preview": "import { useEffect, useRef, useState } from \"preact/hooks\";\nimport {\n  approveDevice,\n  approvePairing,\n  fetchDashboard"
  },
  {
    "path": "lib/public/js/components/global-restart-banner.js",
    "chars": 1291,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { UpdateActionButton } from \"./update-action-button.js\";\nimpor"
  },
  {
    "path": "lib/public/js/components/google/account-row.js",
    "chars": 5334,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Badge } from \"../badge.js\";\nimport { ScopePicker } from \"../"
  },
  {
    "path": "lib/public/js/components/google/add-account-modal.js",
    "chars": 2619,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionBu"
  },
  {
    "path": "lib/public/js/components/google/gmail-setup-wizard.js",
    "chars": 18015,
    "preview": "import { h } from \"preact\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"preact/hooks\";\nimport ht"
  },
  {
    "path": "lib/public/js/components/google/gmail-watch-toggle.js",
    "chars": 2666,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Badge } from \"../badge.js\";\nimport { ToggleSwitch } from \".."
  },
  {
    "path": "lib/public/js/components/google/index.js",
    "chars": 19064,
    "preview": "import { h } from \"preact\";\nimport {\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"preact/hooks\";\nimport ht"
  },
  {
    "path": "lib/public/js/components/google/use-gmail-watch.js",
    "chars": 3806,
    "preview": "import { useCallback, useEffect, useMemo, useState } from \"preact/hooks\";\nimport {\n  fetchGmailConfig,\n  renewGmailWatch"
  },
  {
    "path": "lib/public/js/components/google/use-google-accounts.js",
    "chars": 1273,
    "preview": "import { useCallback, useEffect, useMemo, useRef } from \"preact/hooks\";\nimport { fetchGoogleAccounts } from \"../../lib/a"
  },
  {
    "path": "lib/public/js/components/icons.js",
    "chars": 26600,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nexport const ChevronDownIcon = ({ classNa"
  },
  {
    "path": "lib/public/js/components/info-tooltip.js",
    "chars": 471,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Tooltip } from \"./tooltip.js\";\n\nconst html = htm.bind(h);\n\ne"
  },
  {
    "path": "lib/public/js/components/loading-spinner.js",
    "chars": 619,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nexport const LoadingSpinner = ({\n  classN"
  },
  {
    "path": "lib/public/js/components/modal-shell.js",
    "chars": 1609,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useRef } from \"preact/hooks\";\nimport { createPortal } from \"preact/compa"
  },
  {
    "path": "lib/public/js/components/models-tab/index.js",
    "chars": 8481,
    "preview": "import { h } from \"preact\";\nimport { useMemo } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { PageHeader } from \"."
  },
  {
    "path": "lib/public/js/components/models-tab/model-picker.js",
    "chars": 8995,
    "preview": "import { h } from \"preact\";\nimport { useState, useMemo, useRef, useEffect } from \"preact/hooks\";\nimport htm from \"htm\";\n"
  },
  {
    "path": "lib/public/js/components/models-tab/provider-auth-card.js",
    "chars": 13963,
    "preview": "import { h } from \"preact\";\nimport { useState, useRef, useEffect } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { "
  },
  {
    "path": "lib/public/js/components/models-tab/use-models.js",
    "chars": 11689,
    "preview": "import { useState, useEffect, useRef, useCallback } from \"preact/hooks\";\nimport {\n  fetchModels,\n  fetchModelsConfig,\n  "
  },
  {
    "path": "lib/public/js/components/models.js",
    "chars": 18023,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {\n"
  },
  {
    "path": "lib/public/js/components/nodes-tab/browser-attach/index.js",
    "chars": 2230,
    "preview": "import { h } from \"preact\";\nimport { useMemo } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { marked } from \"marke"
  },
  {
    "path": "lib/public/js/components/nodes-tab/connected-nodes/index.js",
    "chars": 20985,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../../action-button.js\";\nimport { Badge"
  },
  {
    "path": "lib/public/js/components/nodes-tab/connected-nodes/use-connected-nodes-card.js",
    "chars": 9958,
    "preview": "import {\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"preact/hooks\";\nimport { copyTextToClipboard } from \"."
  },
  {
    "path": "lib/public/js/components/nodes-tab/connected-nodes/user-connected-nodes.js",
    "chars": 907,
    "preview": "import { usePolling } from \"../../../hooks/usePolling.js\";\nimport { fetchNodesStatus } from \"../../../lib/api.js\";\n\ncons"
  },
  {
    "path": "lib/public/js/components/nodes-tab/exec-allowlist/index.js",
    "chars": 3294,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../../action-button.js\";\nimport { useEx"
  },
  {
    "path": "lib/public/js/components/nodes-tab/exec-allowlist/use-exec-allowlist.js",
    "chars": 2199,
    "preview": "import { useCallback, useEffect, useState } from \"preact/hooks\";\nimport {\n  addNodeExecAllowlistPattern,\n  fetchNodeExec"
  },
  {
    "path": "lib/public/js/components/nodes-tab/exec-config/index.js",
    "chars": 4751,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../../action-button.js\";\nimport { useEx"
  },
  {
    "path": "lib/public/js/components/nodes-tab/exec-config/use-exec-config.js",
    "chars": 1977,
    "preview": "import { useCallback, useEffect, useState } from \"preact/hooks\";\nimport { fetchNodeExecConfig, saveNodeExecConfig } from"
  },
  {
    "path": "lib/public/js/components/nodes-tab/index.js",
    "chars": 1617,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { PageHeader } from \"../page-header.js\";\nimport { ActionButton"
  },
  {
    "path": "lib/public/js/components/nodes-tab/setup-wizard/index.js",
    "chars": 7570,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ModalShell } from \"../../modal-shell.js\";\nimport { ActionBut"
  },
  {
    "path": "lib/public/js/components/nodes-tab/setup-wizard/use-setup-wizard.js",
    "chars": 6458,
    "preview": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"preact/hooks\";\nimport {\n  approveDevice,\n "
  },
  {
    "path": "lib/public/js/components/nodes-tab/use-nodes-tab.js",
    "chars": 1721,
    "preview": "import { useCallback, useEffect, useState } from \"preact/hooks\";\nimport { fetchNodeConnectInfo } from \"../../lib/api.js\""
  },
  {
    "path": "lib/public/js/components/onboarding/pairing-utils.js",
    "chars": 471,
    "preview": "export const getPreferredPairingChannel = (vals = {}) => {\n  if (vals.TELEGRAM_BOT_TOKEN) return \"telegram\";\n  if (vals."
  },
  {
    "path": "lib/public/js/components/onboarding/use-welcome-codex.js",
    "chars": 4055,
    "preview": "import { useEffect, useRef, useState } from \"preact/hooks\";\nimport {\n  disconnectCodex,\n  exchangeCodexOAuth,\n  fetchCod"
  },
  {
    "path": "lib/public/js/components/onboarding/use-welcome-pairing.js",
    "chars": 2410,
    "preview": "import { useEffect, useState } from \"preact/hooks\";\nimport { approvePairing, fetchPairings, fetchStatus, rejectPairing }"
  },
  {
    "path": "lib/public/js/components/onboarding/use-welcome-storage.js",
    "chars": 1783,
    "preview": "import { useEffect, useState } from \"preact/hooks\";\n\nimport { kOnboardingStorageKey } from \"../../lib/storage-keys.js\";\n"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-config.js",
    "chars": 7828,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { kAllAiAuthFields } from \"../../lib/model-config.js\";\n\nconst "
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-form-step.js",
    "chars": 17407,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { SecretIn"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-header.js",
    "chars": 2122,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nexport const WelcomeHeader = ({\n  groups,"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-import-step.js",
    "chars": 8945,
    "preview": "import { h } from \"preact\";\nimport { useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionButton } from"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-pairing-step.js",
    "chars": 6145,
    "preview": "import { h } from \"preact\";\nimport { useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { Badge } from \"../ba"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-placeholder-review-step.js",
    "chars": 2932,
    "preview": "import { h } from \"preact\";\nimport { useMemo } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { ActionButton } from "
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-pre-step.js",
    "chars": 3491,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { kGithubFlowFresh, kGithubFlowImport } from \"./welcome-config"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-secret-review-step.js",
    "chars": 5746,
    "preview": "import { h } from \"preact\";\nimport { useState, useCallback } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { Action"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-secret-review-utils.js",
    "chars": 658,
    "preview": "export const buildApprovedImportSecrets = (secrets = []) =>\n  (Array.isArray(secrets) ? secrets : [])\n    .filter((secre"
  },
  {
    "path": "lib/public/js/components/onboarding/welcome-setup-step.js",
    "chars": 3149,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { LoadingS"
  },
  {
    "path": "lib/public/js/components/overflow-menu.js",
    "chars": 3101,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useRef } from \"preact/hooks\";\nimport htm from \"htm\";\n\nconst html = htm.b"
  },
  {
    "path": "lib/public/js/components/page-header.js",
    "chars": 386,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nexport const PageHeader = ({ title = \"\", "
  },
  {
    "path": "lib/public/js/components/pairings.js",
    "chars": 5997,
    "preview": "import { h } from 'preact';\nimport { useEffect, useState } from 'preact/hooks';\nimport htm from 'htm';\nimport { ActionBu"
  },
  {
    "path": "lib/public/js/components/pane-shell.js",
    "chars": 784,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\n/**\n * Shared layout shell for pages that"
  },
  {
    "path": "lib/public/js/components/pill-tabs.js",
    "chars": 1010,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nconst kPillBaseClassName =\n  \"inline-flex"
  },
  {
    "path": "lib/public/js/components/pop-actions.js",
    "chars": 1726,
    "preview": "import { h } from \"preact\";\nimport { useState, useEffect, useRef } from \"preact/hooks\";\nimport htm from \"htm\";\n\nconst ht"
  },
  {
    "path": "lib/public/js/components/providers.js",
    "chars": 21599,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {\n"
  },
  {
    "path": "lib/public/js/components/routes/agents-route.js",
    "chars": 760,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { AgentsTab } from \"../agents-tab/index.js\";\n\nconst html = htm"
  },
  {
    "path": "lib/public/js/components/routes/browse-route.js",
    "chars": 1003,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { FileViewer } from \"../file-viewer/index.js\";\n\nconst html = h"
  },
  {
    "path": "lib/public/js/components/routes/chat-route.js",
    "chars": 39952,
    "preview": "import { h } from \"preact\";\nimport {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} f"
  },
  {
    "path": "lib/public/js/components/routes/cron-route.js",
    "chars": 270,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { CronTab } from \"../cron-tab/index.js\";\n\nconst html = htm.bin"
  },
  {
    "path": "lib/public/js/components/routes/doctor-route.js",
    "chars": 659,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { DoctorTab } from \"../doctor/index.js\";\n\nconst html = htm.bin"
  },
  {
    "path": "lib/public/js/components/routes/envars-route.js",
    "chars": 247,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Envars } from \"../envars.js\";\n\nconst html = htm.bind(h);\n\nex"
  },
  {
    "path": "lib/public/js/components/routes/general-route.js",
    "chars": 1244,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { GeneralTab } from \"../general/index.js\";\n\nconst html = htm.b"
  },
  {
    "path": "lib/public/js/components/routes/index.js",
    "chars": 747,
    "preview": "export { AgentsRoute } from \"./agents-route.js\";\nexport { BrowseRoute } from \"./browse-route.js\";\nexport { ChatRoute } f"
  },
  {
    "path": "lib/public/js/components/routes/models-route.js",
    "chars": 257,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Models } from \"../models-tab/index.js\";\n\nconst html = htm.bi"
  },
  {
    "path": "lib/public/js/components/routes/nodes-route.js",
    "chars": 316,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { NodesTab } from \"../nodes-tab/index.js\";\n\nconst html = htm.b"
  },
  {
    "path": "lib/public/js/components/routes/providers-route.js",
    "chars": 291,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Providers } from \"../providers.js\";\n\nconst html = htm.bind(h"
  },
  {
    "path": "lib/public/js/components/routes/route-redirect.js",
    "chars": 256,
    "preview": "import { useEffect } from \"preact/hooks\";\nimport { useLocation } from \"wouter-preact\";\n\nexport const RouteRedirect = ({ "
  },
  {
    "path": "lib/public/js/components/routes/telegram-route.js",
    "chars": 351,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { TelegramWorkspace } from \"../telegram-workspace/index.js\";\n\n"
  },
  {
    "path": "lib/public/js/components/routes/usage-route.js",
    "chars": 450,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { UsageTab } from \"../usage-tab/index.js\";\n\nconst html = htm.b"
  },
  {
    "path": "lib/public/js/components/routes/watchdog-route.js",
    "chars": 719,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { WatchdogTab } from \"../watchdog-tab/index.js\";\n\nconst html ="
  },
  {
    "path": "lib/public/js/components/routes/webhooks-route.js",
    "chars": 1199,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Webhooks } from \"../webhooks/index.js\";\n\nconst html = htm.bi"
  },
  {
    "path": "lib/public/js/components/scope-picker.js",
    "chars": 4728,
    "preview": "import { h } from 'preact';\nimport { useState } from 'preact/hooks';\nimport htm from 'htm';\nconst html = htm.bind(h);\n\ne"
  },
  {
    "path": "lib/public/js/components/secret-input.js",
    "chars": 1455,
    "preview": "import { h } from \"preact\";\nimport { useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { LoadingSpinner } fr"
  },
  {
    "path": "lib/public/js/components/segmented-control.js",
    "chars": 1443,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Tooltip } from \"./tooltip.js\";\n\nconst html = htm.bind(h);\n\n/"
  },
  {
    "path": "lib/public/js/components/session-select-field.js",
    "chars": 2695,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport {\n  getSessionDisplayLabel,\n  getSessionRowKey,\n} from \"../lib"
  },
  {
    "path": "lib/public/js/components/sidebar-git-panel.js",
    "chars": 12675,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { fetchBro"
  },
  {
    "path": "lib/public/js/components/sidebar.js",
    "chars": 17357,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\n"
  },
  {
    "path": "lib/public/js/components/summary-stat-card.js",
    "chars": 487,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nexport const SummaryStatCard = ({\n  title"
  },
  {
    "path": "lib/public/js/components/telegram-workspace/index.js",
    "chars": 15071,
    "preview": "import { h } from \"preact\";\nimport { useState, useEffect } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { showToas"
  },
  {
    "path": "lib/public/js/components/telegram-workspace/manage.js",
    "chars": 20157,
    "preview": "import { h } from \"preact\";\nimport { useState, useEffect } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { showToas"
  },
  {
    "path": "lib/public/js/components/telegram-workspace/onboarding.js",
    "chars": 20839,
    "preview": "import { h } from \"preact\";\nimport { useState, useEffect } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { Badge } "
  },
  {
    "path": "lib/public/js/components/theme-toggle.js",
    "chars": 3387,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport { "
  },
  {
    "path": "lib/public/js/components/toast.js",
    "chars": 1792,
    "preview": "import { h } from 'preact';\nimport { useState, useEffect } from 'preact/hooks';\nimport { createPortal } from 'preact/com"
  },
  {
    "path": "lib/public/js/components/toggle-switch.js",
    "chars": 611,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nexport const ToggleSwitch = ({\n  checked "
  },
  {
    "path": "lib/public/js/components/tooltip.js",
    "chars": 4767,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useRef, useState } from \"preact/hooks\";\nimport { createPortal } from \"pr"
  },
  {
    "path": "lib/public/js/components/update-action-button.js",
    "chars": 572,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"./action-button.js\";\n\nconst html = htm."
  },
  {
    "path": "lib/public/js/components/update-modal.js",
    "chars": 10140,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useMemo, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {"
  },
  {
    "path": "lib/public/js/components/usage-tab/constants.js",
    "chars": 1119,
    "preview": "export const kColorPalette = [\n  \"#7dd3fc\",\n  \"#22d3ee\",\n  \"#fbbf24\",\n  \"#34d399\",\n  \"#fb7185\",\n  \"#a78bfa\",\n  \"#f472b6\""
  },
  {
    "path": "lib/public/js/components/usage-tab/formatters.js",
    "chars": 1164,
    "preview": "import { kColorPalette } from \"./constants.js\";\n\nexport const toLocalDayKey = (value) => {\n  const d = value instanceof "
  },
  {
    "path": "lib/public/js/components/usage-tab/index.js",
    "chars": 2545,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { ActionButton } from \"../action-button.js\";\nimport { PageHead"
  },
  {
    "path": "lib/public/js/components/usage-tab/overview-section.js",
    "chars": 11196,
    "preview": "import { h } from \"preact\";\nimport { useEffect, useState } from \"preact/hooks\";\nimport htm from \"htm\";\nimport {\n  format"
  },
  {
    "path": "lib/public/js/components/usage-tab/sessions-section.js",
    "chars": 6658,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport {\n  formatDurationCompactMs,\n  formatInteger,\n  formatLocaleDa"
  },
  {
    "path": "lib/public/js/components/usage-tab/use-usage-tab.js",
    "chars": 9774,
    "preview": "import {\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"preact/hooks\";\nimport Chart from \"chart.js"
  },
  {
    "path": "lib/public/js/components/watchdog-tab/console/index.js",
    "chars": 4615,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { FileCopyLineIcon } from \"../../icons.js\";\nimport {\n  kWatchd"
  },
  {
    "path": "lib/public/js/components/watchdog-tab/console/use-console.js",
    "chars": 5026,
    "preview": "import { useEffect, useRef, useState } from \"preact/hooks\";\nimport { fetchWatchdogLogs } from \"../../../lib/api.js\";\nimp"
  },
  {
    "path": "lib/public/js/components/watchdog-tab/helpers.js",
    "chars": 4159,
    "preview": "export const kWatchdogConsoleTabLogs = \"logs\";\nexport const kWatchdogConsoleTabTerminal = \"terminal\";\nexport const kWatc"
  },
  {
    "path": "lib/public/js/components/watchdog-tab/incidents/index.js",
    "chars": 2035,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { getIncidentStatusTone } from \"../helpers.js\";\n\nconst html = "
  },
  {
    "path": "lib/public/js/components/watchdog-tab/incidents/use-incidents.js",
    "chars": 885,
    "preview": "import { useEffect } from \"preact/hooks\";\nimport { usePolling } from \"../../../hooks/usePolling.js\";\nimport { fetchWatch"
  },
  {
    "path": "lib/public/js/components/watchdog-tab/index.js",
    "chars": 2611,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { Gateway } from \"../gateway.js\";\nimport { useWatchdogTab } fr"
  },
  {
    "path": "lib/public/js/components/watchdog-tab/resource-bar.js",
    "chars": 2184,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\n\nconst html = htm.bind(h);\n\nconst barColor = (percent) => {\n  if (per"
  },
  {
    "path": "lib/public/js/components/watchdog-tab/resources/index.js",
    "chars": 3385,
    "preview": "import { h } from \"preact\";\nimport htm from \"htm\";\nimport { formatBytes } from \"../helpers.js\";\nimport { ResourceBar } f"
  }
]

// ... and 280 more files (download for full content)

About this extraction

This page contains the full source code of the chrysb/alphaclaw GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 480 files (3.1 MB), approximately 836.6k tokens, and a symbol index with 110 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!