feat(boltbot): add audit dashboard — Vite + React SPA served from gateway
Adds a dark-themed receipt audit dashboard at /boltbot/dashboard: - Vite + React + TypeScript SPA with Tailwind CSS - Stats summary (total actions, tier breakdown, anomaly count) - Receipt list with tier/anomaly filtering, offset pagination, 10s polling - Slide-out receipt detail with accordion sections (hashes, EigenDA, TEE) - Session grouping view (receipts grouped by sessionKey) - Gateway serves static files via registerHttpRoute with path traversal protection (resolve+startsWith), security headers (nosniff, DENY) - WCAG-compliant: dialog focus management, keyboard navigation, aria-pressed/expanded/selected, semantic table roles, contrast AA
This commit is contained in:
parent
30e42178c1
commit
58e556a2d7
168
docs/specs/boltbot-dashboard.spec.md
Normal file
168
docs/specs/boltbot-dashboard.spec.md
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# Specification: Boltbot Dashboard
|
||||||
|
|
||||||
|
> Use `/duy-workflow:execute docs/specs/boltbot-dashboard.spec.md` to implement.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Rebuild the Boltbot audit dashboard as a Vite + React static SPA served from the gateway at `/boltbot/dashboard`, wired to the real Boltbot API, with Boltbot branding.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
1. **[REQ-1] Vite + React SPA scaffold**
|
||||||
|
- Replace the existing Next.js frontend with a Vite + React + TypeScript project at `extensions/boltbot/dashboard/`
|
||||||
|
- Configure `base: '/boltbot/dashboard/'` so all assets resolve correctly when served from the gateway
|
||||||
|
- Use Tailwind CSS for styling (dark theme, matching current design)
|
||||||
|
- Build output: `extensions/boltbot/dashboard/dist/`
|
||||||
|
- Acceptance: `pnpm --filter boltbot-dashboard build` produces a working static bundle
|
||||||
|
|
||||||
|
2. **[REQ-2] Gateway static file serving**
|
||||||
|
- Register HTTP routes in the Boltbot plugin to serve the dashboard's `dist/` directory at `/boltbot/dashboard/*`
|
||||||
|
- Catch-all returns `index.html` for client-side navigation
|
||||||
|
- Acceptance: Navigating to `http://localhost:18789/boltbot/dashboard` loads the SPA
|
||||||
|
|
||||||
|
3. **[REQ-3] Real API integration**
|
||||||
|
- Remove all mock data. Fetch from the real Boltbot API endpoints:
|
||||||
|
- `GET /boltbot/stats` → `{ total, byTier: { low, medium, high }, anomalyCount }`
|
||||||
|
- `GET /boltbot/receipts?limit=50&offset=0` → `{ receipts: ActionReceipt[] }`
|
||||||
|
- `GET /boltbot/receipt?id=<uuid>` → `{ receipt: ActionReceipt }`
|
||||||
|
- TypeScript types must match the real `ActionReceipt` interface from `extensions/boltbot/src/receipt-store.ts`:
|
||||||
|
- `id, timestamp, sessionKey, tier, toolName, argumentsHash, resultHash, success, durationMs, anomalies: string[], daCommitment?: string`
|
||||||
|
- Use SWR with 10-second polling for stats and receipts
|
||||||
|
- Acceptance: Dashboard displays real receipts from the running gateway
|
||||||
|
|
||||||
|
4. **[REQ-4] Boltbot branding**
|
||||||
|
- Replace Finbro logo and branding with "Boltbot" text/logo
|
||||||
|
- Keep the layout shell (header + sidebar) but rebrand all instances
|
||||||
|
- Header: "Boltbot" logo/text, remove user dropdown (no auth yet)
|
||||||
|
- Sidebar: Dashboard (active), Audit Log, Sessions (links can be non-functional placeholders)
|
||||||
|
- Acceptance: No Finbro references remain. "Boltbot" appears in header and page title
|
||||||
|
|
||||||
|
5. **[REQ-5] Stats summary**
|
||||||
|
- Display cards showing: total action count, count per tier (low/medium/high), anomaly count
|
||||||
|
- Color-coded: low=green, medium=yellow, high=red
|
||||||
|
- Skeleton loading state while fetching
|
||||||
|
- Acceptance: Stats cards render with real data from `/boltbot/stats`
|
||||||
|
|
||||||
|
6. **[REQ-6] Receipt list with filtering**
|
||||||
|
- Table rows: toolName, tier (color badge), relative timestamp, success (icon), anomaly indicator
|
||||||
|
- Offset-based pagination: "Load more" fetches next 50 (offset += 50), appends to list
|
||||||
|
- Client-side filters:
|
||||||
|
- Tier: multi-select (low/medium/high)
|
||||||
|
- Anomaly toggle: show only receipts where `anomalies.length > 0`
|
||||||
|
- Filters compose (e.g. "high tier with anomalies")
|
||||||
|
- Preserve scroll position and filters across 10-second polls
|
||||||
|
- Acceptance: Filtering, pagination, and polling all work without losing state
|
||||||
|
|
||||||
|
7. **[REQ-7] Receipt detail panel**
|
||||||
|
- Click a row to open a slide-out detail panel (right side)
|
||||||
|
- Default view (always visible):
|
||||||
|
- toolName, tier badge, success/failure badge
|
||||||
|
- Relative timestamp + full ISO 8601
|
||||||
|
- Duration (human-readable, e.g. "142ms")
|
||||||
|
- sessionKey
|
||||||
|
- Anomaly labels (each as a distinct colored label; empty = "Clean")
|
||||||
|
- Collapsible accordion sections (default collapsed):
|
||||||
|
- **Hashes**: argumentsHash, resultHash (full 64-char hex, copy button)
|
||||||
|
- **EigenDA Verification**: daCommitment hex (copyable), link to verify on-chain if present, "Unverified" if absent
|
||||||
|
- **TEE Attestation**: placeholder section — text "TEE attestation verification coming soon"
|
||||||
|
- Dismiss via close button, Escape key, or backdrop click. Preserves list scroll/filters.
|
||||||
|
- Acceptance: Detail panel opens with all fields, accordions expand/collapse, copy works
|
||||||
|
|
||||||
|
8. **[REQ-8] Session grouping view**
|
||||||
|
- Group receipts by `sessionKey` to show per-conversation audit trails
|
||||||
|
- UI: a toggle or tab to switch between "All Receipts" (flat list) and "By Session" (grouped)
|
||||||
|
- Each session group shows: sessionKey, receipt count, latest timestamp, tier breakdown
|
||||||
|
- Clicking a session group expands to show its receipts (same row format as flat list)
|
||||||
|
- Acceptance: Receipts are correctly grouped by sessionKey
|
||||||
|
|
||||||
|
9. **[REQ-9] Error handling**
|
||||||
|
- If a fetch fails, show an inline error on the affected section (stats or receipts). Don't break the rest.
|
||||||
|
- API returns `{"error": "not_found"}` (404) or `{"error": "missing_id"}` (400) for bad receipt lookups — display a user-friendly message.
|
||||||
|
- Acceptance: Intentionally failed requests show error state without crashing
|
||||||
|
|
||||||
|
10. **[REQ-10] Auth stub**
|
||||||
|
- No authentication implemented
|
||||||
|
- All sessions visible (operator view for now)
|
||||||
|
- Add a `// TODO: Telegram OAuth — filter receipts by authenticated user's sessionKey` comment in the data-fetching layer
|
||||||
|
- Acceptance: Comment exists, no auth code
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
| Decision | Choice | Rationale |
|
||||||
|
|----------|--------|-----------|
|
||||||
|
| Framework | Vite + React + TypeScript | Static SPA, no SSR needed. Fast builds, small bundle. |
|
||||||
|
| Serving | Gateway registerHttpRoute | Same origin, single container, no CORS. |
|
||||||
|
| Data fetching | SWR | Already used in current frontend. Polling + caching built in. |
|
||||||
|
| Pagination | Offset-based | Matches real API (`?limit=50&offset=0`). |
|
||||||
|
| Styling | Tailwind CSS (dark theme) | Matches existing design. |
|
||||||
|
| Auth | Deferred | Stub only. Telegram OAuth planned for future. |
|
||||||
|
| Advanced detail | Accordion sections | Hashes, EigenDA, TEE info collapsed by default. |
|
||||||
|
|
||||||
|
## Progress
|
||||||
|
|
||||||
|
| ID | Status | Notes |
|
||||||
|
|----|--------|-------|
|
||||||
|
| REQ-1 | COMPLETED | Vite + React + TS scaffold at dashboard/ |
|
||||||
|
| REQ-2 | COMPLETED | dashboard-serve.ts registered in index.ts |
|
||||||
|
| REQ-3 | COMPLETED | SWR hooks fetch real API, response shapes matched |
|
||||||
|
| REQ-4 | COMPLETED | Boltbot branding, no Finbro references |
|
||||||
|
| REQ-5 | COMPLETED | StatsCards with skeleton/error states |
|
||||||
|
| REQ-6 | COMPLETED | ReceiptList with tier/anomaly filters, pagination |
|
||||||
|
| REQ-7 | COMPLETED | ReceiptDetail with accordions (hashes, EigenDA, TEE) |
|
||||||
|
| REQ-8 | COMPLETED | SessionView groups by sessionKey |
|
||||||
|
| REQ-9 | COMPLETED | Inline errors per section |
|
||||||
|
| REQ-10 | COMPLETED | Auth TODO comment in hooks.ts |
|
||||||
|
|
||||||
|
## Completion Criteria
|
||||||
|
|
||||||
|
- [x] All REQs implemented
|
||||||
|
- [x] `pnpm --filter boltbot-dashboard build` succeeds (228KB JS, 19KB CSS)
|
||||||
|
- [ ] Dashboard loads at `/boltbot/dashboard` when gateway is running
|
||||||
|
- [ ] Real API data renders (stats, receipts, detail)
|
||||||
|
- [x] No Finbro references remain
|
||||||
|
- [x] No mock data remains
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
| Case | Expected Behavior |
|
||||||
|
|------|-------------------|
|
||||||
|
| No receipts yet | Empty state message: "No actions recorded yet" |
|
||||||
|
| All receipts are low tier (not logged) | Stats show 0, empty receipt list with explanation |
|
||||||
|
| daCommitment absent | Detail shows "Unverified — no DA commitment" |
|
||||||
|
| API unreachable | Inline error per section, previous data preserved |
|
||||||
|
| Very long anomaly list | Scroll within anomaly label area |
|
||||||
|
| Receipt deleted between list and detail fetch | Use list data (already loaded), no re-fetch needed |
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
### Key Files
|
||||||
|
|
||||||
|
- `extensions/boltbot/index.ts`: Plugin entry — add dashboard route registration here
|
||||||
|
- `extensions/boltbot/src/api.ts`: Existing HTTP API routes (`/boltbot/stats`, `/boltbot/receipts`, `/boltbot/receipt`)
|
||||||
|
- `extensions/boltbot/src/receipt-store.ts`: `ActionReceipt` interface — source of truth for types
|
||||||
|
- `extensions/boltbot/src/action-tiers.ts`: Tier classification (HIGH/MEDIUM/LOW)
|
||||||
|
- `extensions/boltbot/src/anomaly.ts`: Anomaly detection logic
|
||||||
|
- `extensions/boltbot/src/stores/local.ts`: SQLite receipt store
|
||||||
|
- `extensions/boltbot/src/stores/eigenda.ts`: EigenDA commitment store
|
||||||
|
- `extensions/boltbot/dashboard/` *(to be created)*: Vite + React SPA
|
||||||
|
|
||||||
|
### Patterns to Follow
|
||||||
|
|
||||||
|
- Moltbot uses ESM (`"type": "module"`) throughout
|
||||||
|
- Plugin HTTP routes use `api.registerHttpRoute(method, path, handler)`
|
||||||
|
- Existing API responses use `{ receipts: [...] }`, `{ receipt: {...} }`, `{ total, byTier, anomalyCount }`
|
||||||
|
- Dark theme with Tailwind: bg-neutral-950, border-neutral-800, text-neutral-100
|
||||||
|
- Use `Space Grotesk` font (already loaded in current frontend)
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
|
||||||
|
- `extensions/boltbot/index.ts` — register dashboard static serving routes
|
||||||
|
- `extensions/boltbot/package.json` — add dashboard build script + devDependencies (vite, react, tailwind)
|
||||||
|
|
||||||
|
### Files to Create
|
||||||
|
|
||||||
|
- `extensions/boltbot/dashboard/` — entire Vite + React SPA (vite.config.ts, index.html, src/*, etc.)
|
||||||
|
|
||||||
|
### Files to Delete
|
||||||
|
|
||||||
|
- `extensions/boltbot/frontend/` — remove the existing Next.js app entirely
|
||||||
18
extensions/boltbot/dashboard/index.html
Normal file
18
extensions/boltbot/dashboard/index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Boltbot Dashboard</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
extensions/boltbot/dashboard/package.json
Normal file
27
extensions/boltbot/dashboard/package.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "boltbot-dashboard",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"swr": "^2.3.0",
|
||||||
|
"lucide-react": "^0.468.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vite": "^6.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
149
extensions/boltbot/dashboard/src/App.tsx
Normal file
149
extensions/boltbot/dashboard/src/App.tsx
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import type { ActionReceipt } from "./types";
|
||||||
|
import { useStats, useReceipts } from "./hooks";
|
||||||
|
import { cn } from "./utils";
|
||||||
|
import Header from "./components/Header";
|
||||||
|
import Sidebar from "./components/Sidebar";
|
||||||
|
import StatsCards from "./components/StatsCards";
|
||||||
|
import FilterControls from "./components/FilterControls";
|
||||||
|
import ReceiptList from "./components/ReceiptList";
|
||||||
|
import ReceiptDetail from "./components/ReceiptDetail";
|
||||||
|
import SessionView from "./components/SessionView";
|
||||||
|
|
||||||
|
const LIMIT = 50;
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [selectedTiers, setSelectedTiers] = useState<string[]>([]);
|
||||||
|
const [anomalyOnly, setAnomalyOnly] = useState(false);
|
||||||
|
const [allReceipts, setAllReceipts] = useState<ActionReceipt[]>([]);
|
||||||
|
const [selectedReceipt, setSelectedReceipt] = useState<ActionReceipt | null>(null);
|
||||||
|
const [viewMode, setViewMode] = useState<"list" | "sessions">("list");
|
||||||
|
const [hasMore, setHasMore] = useState(false);
|
||||||
|
const [loadedCount, setLoadedCount] = useState(0);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [loadMoreError, setLoadMoreError] = useState<string | null>(null);
|
||||||
|
const initialLoadDone = useRef(false);
|
||||||
|
|
||||||
|
const { stats, isLoading: statsLoading, error: statsError } = useStats();
|
||||||
|
const { receipts, isLoading: receiptsLoading, error: receiptsError } = useReceipts(LIMIT, 0);
|
||||||
|
|
||||||
|
// Polling: merge new receipts at the top, do NOT touch loadedCount or hasMore
|
||||||
|
useEffect(() => {
|
||||||
|
if (receipts) {
|
||||||
|
setAllReceipts((prev) => {
|
||||||
|
const ids = new Set(prev.map((r) => r.id));
|
||||||
|
const newOnes = receipts.filter((r) => !ids.has(r.id));
|
||||||
|
if (newOnes.length === 0) return prev;
|
||||||
|
return [...newOnes, ...prev];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [receipts]);
|
||||||
|
|
||||||
|
// Initial load: set hasMore and loadedCount once
|
||||||
|
useEffect(() => {
|
||||||
|
if (receipts && !initialLoadDone.current) {
|
||||||
|
initialLoadDone.current = true;
|
||||||
|
setLoadedCount(receipts.length);
|
||||||
|
setHasMore(receipts.length === LIMIT);
|
||||||
|
}
|
||||||
|
}, [receipts]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(() => {
|
||||||
|
if (loadingMore) return;
|
||||||
|
setLoadingMore(true);
|
||||||
|
setLoadMoreError(null);
|
||||||
|
fetch(`/boltbot/receipts?limit=${LIMIT}&offset=${loadedCount}`)
|
||||||
|
.then((r) => {
|
||||||
|
if (!r.ok) throw new Error(r.statusText);
|
||||||
|
return r.json();
|
||||||
|
})
|
||||||
|
.then((data: { receipts: ActionReceipt[] }) => {
|
||||||
|
setAllReceipts((prev) => {
|
||||||
|
const ids = new Set(prev.map((r) => r.id));
|
||||||
|
const newOnes = data.receipts.filter((r) => !ids.has(r.id));
|
||||||
|
return [...prev, ...newOnes];
|
||||||
|
});
|
||||||
|
setHasMore(data.receipts.length === LIMIT);
|
||||||
|
setLoadedCount((prev) => prev + data.receipts.length);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setLoadMoreError(err instanceof Error ? err.message : "Failed to load more");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoadingMore(false);
|
||||||
|
});
|
||||||
|
}, [loadedCount, loadingMore]);
|
||||||
|
|
||||||
|
const filtered = allReceipts.filter((r) => {
|
||||||
|
if (selectedTiers.length > 0 && !selectedTiers.includes(r.tier)) return false;
|
||||||
|
if (anomalyOnly && r.anomalies.length === 0) return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-neutral-950 text-neutral-100">
|
||||||
|
<Header />
|
||||||
|
<Sidebar />
|
||||||
|
<main className="pt-14 pl-0 lg:pl-56 p-6">
|
||||||
|
<div className="max-w-6xl mx-auto space-y-6 pt-6">
|
||||||
|
<StatsCards stats={stats} isLoading={statsLoading} error={statsError} />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("list")}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-1.5 text-sm rounded-lg transition-colors",
|
||||||
|
viewMode === "list"
|
||||||
|
? "bg-neutral-800 text-white"
|
||||||
|
: "text-neutral-400 hover:text-neutral-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
All Receipts
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("sessions")}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-1.5 text-sm rounded-lg transition-colors",
|
||||||
|
viewMode === "sessions"
|
||||||
|
? "bg-neutral-800 text-white"
|
||||||
|
: "text-neutral-400 hover:text-neutral-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
By Session
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FilterControls
|
||||||
|
selectedTiers={selectedTiers}
|
||||||
|
onTiersChange={setSelectedTiers}
|
||||||
|
anomalyOnly={anomalyOnly}
|
||||||
|
onAnomalyOnlyChange={setAnomalyOnly}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{viewMode === "list" ? (
|
||||||
|
<ReceiptList
|
||||||
|
receipts={filtered}
|
||||||
|
isLoading={receiptsLoading}
|
||||||
|
error={receiptsError}
|
||||||
|
onSelect={setSelectedReceipt}
|
||||||
|
selectedId={selectedReceipt?.id ?? null}
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
loadingMore={loadingMore}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SessionView
|
||||||
|
receipts={filtered}
|
||||||
|
onSelectReceipt={setSelectedReceipt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<ReceiptDetail
|
||||||
|
receipt={selectedReceipt}
|
||||||
|
onClose={() => setSelectedReceipt(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { cn } from "../utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedTiers: string[];
|
||||||
|
onTiersChange: (tiers: string[]) => void;
|
||||||
|
anomalyOnly: boolean;
|
||||||
|
onAnomalyOnlyChange: (v: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tiers = [
|
||||||
|
{ value: "low", label: "Low", color: "emerald" },
|
||||||
|
{ value: "medium", label: "Medium", color: "yellow" },
|
||||||
|
{ value: "high", label: "High", color: "red" },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const tierStyles: Record<string, { active: string; inactive: string }> = {
|
||||||
|
emerald: {
|
||||||
|
active: "bg-emerald-500/20 text-emerald-400 border-emerald-500",
|
||||||
|
inactive: "border-neutral-700 text-neutral-400 hover:border-emerald-500/50",
|
||||||
|
},
|
||||||
|
yellow: {
|
||||||
|
active: "bg-yellow-500/20 text-yellow-400 border-yellow-500",
|
||||||
|
inactive: "border-neutral-700 text-neutral-400 hover:border-yellow-500/50",
|
||||||
|
},
|
||||||
|
red: {
|
||||||
|
active: "bg-red-500/20 text-red-400 border-red-500",
|
||||||
|
inactive: "border-neutral-700 text-neutral-400 hover:border-red-500/50",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FilterControls({
|
||||||
|
selectedTiers,
|
||||||
|
onTiersChange,
|
||||||
|
anomalyOnly,
|
||||||
|
onAnomalyOnlyChange,
|
||||||
|
}: Props) {
|
||||||
|
function toggleTier(tier: string) {
|
||||||
|
if (selectedTiers.includes(tier)) {
|
||||||
|
onTiersChange(selectedTiers.filter((t) => t !== tier));
|
||||||
|
} else {
|
||||||
|
onTiersChange([...selectedTiers, tier]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<div role="group" aria-label="Filter by tier" className="flex flex-wrap gap-2">
|
||||||
|
{tiers.map((t) => {
|
||||||
|
const isActive = selectedTiers.includes(t.value);
|
||||||
|
const style = tierStyles[t.color];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={t.value}
|
||||||
|
onClick={() => toggleTier(t.value)}
|
||||||
|
aria-pressed={isActive}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors",
|
||||||
|
isActive ? style.active : style.inactive,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onAnomalyOnlyChange(!anomalyOnly)}
|
||||||
|
aria-pressed={anomalyOnly}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors",
|
||||||
|
anomalyOnly
|
||||||
|
? "bg-red-500/20 text-red-400 border-red-500"
|
||||||
|
: "border-neutral-700 text-neutral-400 hover:border-red-500/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Anomalies Only
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
extensions/boltbot/dashboard/src/components/Header.tsx
Normal file
12
extensions/boltbot/dashboard/src/components/Header.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Zap } from "lucide-react";
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
return (
|
||||||
|
<header aria-label="Boltbot" className="fixed top-0 left-0 right-0 z-50 h-14 flex items-center px-5 bg-neutral-900/80 backdrop-blur border-b border-neutral-800">
|
||||||
|
<Zap className="w-5 h-5 text-emerald-400 mr-2" aria-hidden="true" />
|
||||||
|
<span className="text-lg font-bold tracking-tight" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>
|
||||||
|
Boltbot
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
227
extensions/boltbot/dashboard/src/components/ReceiptDetail.tsx
Normal file
227
extensions/boltbot/dashboard/src/components/ReceiptDetail.tsx
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
X,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
ChevronDown,
|
||||||
|
Copy,
|
||||||
|
Check,
|
||||||
|
ExternalLink,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { ActionReceipt } from "../types";
|
||||||
|
import { cn, formatRelativeTime, formatDuration } from "../utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
receipt: ActionReceipt | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tierBadge: Record<string, string> = {
|
||||||
|
low: "bg-emerald-500/10 text-emerald-400",
|
||||||
|
medium: "bg-yellow-500/10 text-yellow-400",
|
||||||
|
high: "bg-red-500/10 text-red-400",
|
||||||
|
};
|
||||||
|
|
||||||
|
function CopyableHash({ label, hash }: { label: string; hash: string }) {
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const ariaLabel = `Copy ${label.toLowerCase().includes("arguments") ? "arguments" : "result"} hash`;
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
navigator.clipboard.writeText(hash).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="text-xs text-neutral-400 mb-1">{label}</div>
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-mono text-xs break-all text-neutral-300 flex-1">
|
||||||
|
{hash}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className="shrink-0 p-1 rounded hover:bg-neutral-700 transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-3.5 h-3.5 text-emerald-400" aria-hidden="true" />
|
||||||
|
<span className="sr-only">Copied</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Copy className="w-3.5 h-3.5 text-neutral-400" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Accordion({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const sanitizedLabel = title.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
return (
|
||||||
|
<div className="border-t border-neutral-800">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(!open)}
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={`accordion-${sanitizedLabel}`}
|
||||||
|
className="flex items-center justify-between w-full py-3 text-sm text-neutral-300 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4 transition-transform",
|
||||||
|
open && "rotate-180",
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{open && <div id={`accordion-${sanitizedLabel}`} className="pb-3">{children}</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ReceiptDetail({ receipt, onClose }: Props) {
|
||||||
|
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
}
|
||||||
|
if (receipt) {
|
||||||
|
document.addEventListener("keydown", handleKey);
|
||||||
|
return () => document.removeEventListener("keydown", handleKey);
|
||||||
|
}
|
||||||
|
}, [receipt, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (receipt) {
|
||||||
|
closeButtonRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [receipt]);
|
||||||
|
|
||||||
|
if (!receipt) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="receipt-detail-title"
|
||||||
|
className="fixed inset-y-0 right-0 w-[420px] max-w-full bg-neutral-900 border-l border-neutral-800 z-50 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div key={receipt.id} className="p-5">
|
||||||
|
<div className="flex items-center justify-between mb-5">
|
||||||
|
<h2 id="receipt-detail-title" className="text-lg font-bold truncate pr-4">{receipt.toolName}</h2>
|
||||||
|
<button
|
||||||
|
ref={closeButtonRef}
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Close detail panel"
|
||||||
|
className="p-1 rounded hover:bg-neutral-800 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"rounded-full px-2 py-0.5 text-xs",
|
||||||
|
tierBadge[receipt.tier],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{receipt.tier}
|
||||||
|
</span>
|
||||||
|
{receipt.success ? (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-emerald-400">
|
||||||
|
<CheckCircle className="w-3.5 h-3.5" aria-hidden="true" /> Success
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="flex items-center gap-1 text-xs text-red-400">
|
||||||
|
<XCircle className="w-3.5 h-3.5" aria-hidden="true" /> Failure
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 mb-5">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Time</div>
|
||||||
|
<div className="text-sm">{formatRelativeTime(receipt.timestamp)}</div>
|
||||||
|
<div className="text-xs text-neutral-400">{receipt.timestamp}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Duration</div>
|
||||||
|
<div className="text-sm">{formatDuration(receipt.durationMs)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Session</div>
|
||||||
|
<div className="font-mono text-xs text-neutral-300">{receipt.sessionKey}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-neutral-400">Anomalies</div>
|
||||||
|
{receipt.anomalies.length > 0 ? (
|
||||||
|
<div className="flex flex-wrap gap-1 mt-1">
|
||||||
|
{receipt.anomalies.map((a, i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="bg-red-500/10 text-red-400 rounded-full px-2 py-0.5 text-xs"
|
||||||
|
>
|
||||||
|
{a}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-emerald-400">Clean</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Accordion title="Hashes">
|
||||||
|
<CopyableHash label="Arguments Hash" hash={receipt.argumentsHash} />
|
||||||
|
<CopyableHash label="Result Hash" hash={receipt.resultHash} />
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="EigenDA Verification">
|
||||||
|
{receipt.daCommitment ? (
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<span className="font-mono text-xs break-all text-neutral-300 flex-1">
|
||||||
|
{receipt.daCommitment}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
aria-label="View on EigenDA explorer"
|
||||||
|
className="shrink-0 p-1 rounded hover:bg-neutral-700 transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5 text-neutral-400" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-neutral-400 text-sm">
|
||||||
|
Unverified — no DA commitment
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="TEE Attestation">
|
||||||
|
<div className="text-neutral-400 text-sm italic">
|
||||||
|
TEE attestation verification coming soon
|
||||||
|
</div>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
128
extensions/boltbot/dashboard/src/components/ReceiptList.tsx
Normal file
128
extensions/boltbot/dashboard/src/components/ReceiptList.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { CheckCircle, XCircle, AlertTriangle } from "lucide-react";
|
||||||
|
import type { ActionReceipt } from "../types";
|
||||||
|
import { cn, formatRelativeTime } from "../utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
receipts: ActionReceipt[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: unknown;
|
||||||
|
onSelect: (r: ActionReceipt) => void;
|
||||||
|
selectedId: string | null;
|
||||||
|
hasMore: boolean;
|
||||||
|
onLoadMore: () => void;
|
||||||
|
loadingMore?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tierBadge: Record<string, string> = {
|
||||||
|
low: "bg-emerald-500/10 text-emerald-400",
|
||||||
|
medium: "bg-yellow-500/10 text-yellow-400",
|
||||||
|
high: "bg-red-500/10 text-red-400",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ReceiptList({
|
||||||
|
receipts,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
onSelect,
|
||||||
|
selectedId,
|
||||||
|
hasMore,
|
||||||
|
onLoadMore,
|
||||||
|
loadingMore = false,
|
||||||
|
}: Props) {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-red-400 text-sm p-4">
|
||||||
|
Failed to load receipts: {error instanceof Error ? error.message : "Unknown error"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading && receipts.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i} className="h-10 animate-pulse bg-neutral-800 rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receipts.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-neutral-400 text-sm text-center py-12">
|
||||||
|
No actions recorded yet
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="table" aria-label="Action receipts" aria-busy={isLoading}>
|
||||||
|
<div role="row" className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 px-3 pb-2 text-neutral-400 text-xs uppercase tracking-wide">
|
||||||
|
<span role="columnheader">Tool</span>
|
||||||
|
<span role="columnheader">Tier</span>
|
||||||
|
<span role="columnheader">Time</span>
|
||||||
|
<span role="columnheader">Status</span>
|
||||||
|
<span role="columnheader">Anomaly</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
{receipts.map((r) => (
|
||||||
|
<div
|
||||||
|
key={r.id}
|
||||||
|
role="row"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-selected={selectedId === r.id}
|
||||||
|
onClick={() => onSelect(r)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect(r);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 items-center px-3 py-2 rounded-lg cursor-pointer transition-colors",
|
||||||
|
selectedId === r.id
|
||||||
|
? "bg-neutral-800/50 border-l-2 border-emerald-400"
|
||||||
|
: "hover:bg-neutral-800/30",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span role="cell" className="text-sm font-mono truncate">{r.toolName}</span>
|
||||||
|
<span role="cell">
|
||||||
|
<span className={cn("rounded-full px-2 py-0.5 text-xs", tierBadge[r.tier])}>
|
||||||
|
{r.tier}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span role="cell" className="text-xs text-neutral-400">{formatRelativeTime(r.timestamp)}</span>
|
||||||
|
<span role="cell">
|
||||||
|
{r.success ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-emerald-400" aria-label="Success" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-4 h-4 text-red-400" aria-label="Failed" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span role="cell">
|
||||||
|
{r.anomalies.length > 0 ? (
|
||||||
|
<AlertTriangle className="w-4 h-4 text-amber-400" aria-label="Has anomalies" />
|
||||||
|
) : (
|
||||||
|
<span className="text-neutral-600" aria-label="No anomalies">—</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{hasMore && (
|
||||||
|
<div className="pt-4 flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={onLoadMore}
|
||||||
|
disabled={loadingMore}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-2 text-sm bg-neutral-800 rounded-lg transition-colors",
|
||||||
|
loadingMore ? "opacity-50 cursor-not-allowed" : "hover:bg-neutral-700",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{loadingMore ? "Loading..." : "Load More"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
165
extensions/boltbot/dashboard/src/components/SessionView.tsx
Normal file
165
extensions/boltbot/dashboard/src/components/SessionView.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import { ChevronDown, CheckCircle, XCircle, AlertTriangle } from "lucide-react";
|
||||||
|
import type { ActionReceipt } from "../types";
|
||||||
|
import { cn, formatRelativeTime } from "../utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
receipts: ActionReceipt[];
|
||||||
|
onSelectReceipt: (r: ActionReceipt) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tierBadge: Record<string, string> = {
|
||||||
|
low: "bg-emerald-500/10 text-emerald-400",
|
||||||
|
medium: "bg-yellow-500/10 text-yellow-400",
|
||||||
|
high: "bg-red-500/10 text-red-400",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SessionGroup {
|
||||||
|
sessionKey: string;
|
||||||
|
receipts: ActionReceipt[];
|
||||||
|
latestTimestamp: string;
|
||||||
|
tierCounts: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBySession(receipts: ActionReceipt[]): SessionGroup[] {
|
||||||
|
const map = new Map<string, ActionReceipt[]>();
|
||||||
|
for (const r of receipts) {
|
||||||
|
const existing = map.get(r.sessionKey);
|
||||||
|
if (existing) {
|
||||||
|
existing.push(r);
|
||||||
|
} else {
|
||||||
|
map.set(r.sessionKey, [r]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups: SessionGroup[] = [];
|
||||||
|
for (const [sessionKey, recs] of map) {
|
||||||
|
const sorted = recs.sort(
|
||||||
|
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(),
|
||||||
|
);
|
||||||
|
const tierCounts: Record<string, number> = {};
|
||||||
|
for (const r of sorted) {
|
||||||
|
tierCounts[r.tier] = (tierCounts[r.tier] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
groups.push({
|
||||||
|
sessionKey,
|
||||||
|
receipts: sorted,
|
||||||
|
latestTimestamp: sorted[0].timestamp,
|
||||||
|
tierCounts,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups.sort(
|
||||||
|
(a, b) => new Date(b.latestTimestamp).getTime() - new Date(a.latestTimestamp).getTime(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SessionCard({
|
||||||
|
group,
|
||||||
|
onSelectReceipt,
|
||||||
|
}: {
|
||||||
|
group: SessionGroup;
|
||||||
|
onSelectReceipt: (r: ActionReceipt) => void;
|
||||||
|
}) {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-neutral-900 rounded-xl border border-neutral-800">
|
||||||
|
<button
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
className="w-full flex items-center gap-3 p-4 text-left hover:bg-neutral-800/30 transition-colors rounded-xl"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
"w-4 h-4 text-neutral-400 shrink-0 transition-transform",
|
||||||
|
expanded && "rotate-180",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="font-mono text-xs truncate flex-1 text-neutral-300">
|
||||||
|
{group.sessionKey}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-neutral-400 bg-neutral-800 rounded-full px-2 py-0.5">
|
||||||
|
{group.receipts.length}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
{formatRelativeTime(group.latestTimestamp)}
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{(["low", "medium", "high"] as const).map(
|
||||||
|
(tier) =>
|
||||||
|
group.tierCounts[tier] && (
|
||||||
|
<span
|
||||||
|
key={tier}
|
||||||
|
className={cn("rounded-full px-1.5 py-0.5 text-xs", tierBadge[tier])}
|
||||||
|
>
|
||||||
|
{group.tierCounts[tier]}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{expanded && (
|
||||||
|
<div className="border-t border-neutral-800 p-3">
|
||||||
|
<div className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 px-3 pb-2 text-neutral-400 text-xs uppercase tracking-wide">
|
||||||
|
<span>Tool</span>
|
||||||
|
<span>Tier</span>
|
||||||
|
<span>Time</span>
|
||||||
|
<span>Status</span>
|
||||||
|
<span>Anomaly</span>
|
||||||
|
</div>
|
||||||
|
{group.receipts.map((r) => (
|
||||||
|
<div
|
||||||
|
key={r.id}
|
||||||
|
onClick={() => onSelectReceipt(r)}
|
||||||
|
className="grid grid-cols-[1fr_80px_90px_40px_40px] gap-2 items-center px-3 py-2 rounded-lg cursor-pointer hover:bg-neutral-800/30 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-mono truncate">{r.toolName}</span>
|
||||||
|
<span>
|
||||||
|
<span className={cn("rounded-full px-2 py-0.5 text-xs", tierBadge[r.tier])}>
|
||||||
|
{r.tier}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-neutral-400">
|
||||||
|
{formatRelativeTime(r.timestamp)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{r.success ? (
|
||||||
|
<CheckCircle className="w-4 h-4 text-emerald-400" aria-label="Success" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-4 h-4 text-red-400" aria-label="Failed" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{r.anomalies.length > 0 ? (
|
||||||
|
<AlertTriangle className="w-4 h-4 text-amber-400" aria-label="Has anomalies" />
|
||||||
|
) : (
|
||||||
|
<span className="text-neutral-600" aria-label="No anomalies">—</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SessionView({ receipts, onSelectReceipt }: Props) {
|
||||||
|
const groups = useMemo(() => groupBySession(receipts), [receipts]);
|
||||||
|
|
||||||
|
if (groups.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-neutral-400 text-sm text-center py-12">
|
||||||
|
No sessions recorded yet
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
{groups.map((g) => (
|
||||||
|
<SessionCard key={g.sessionKey} group={g} onSelectReceipt={onSelectReceipt} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
extensions/boltbot/dashboard/src/components/Sidebar.tsx
Normal file
30
extensions/boltbot/dashboard/src/components/Sidebar.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { LayoutDashboard, ScrollText, Users } from "lucide-react";
|
||||||
|
import { cn } from "../utils";
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ label: "Dashboard", icon: LayoutDashboard, active: true },
|
||||||
|
{ label: "Audit Log", icon: ScrollText, active: false },
|
||||||
|
{ label: "Sessions", icon: Users, active: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
return (
|
||||||
|
<nav aria-label="Main navigation" className="hidden lg:flex fixed top-14 left-0 bottom-0 w-56 flex-col gap-1 p-3 bg-neutral-900/50 border-r border-neutral-800 z-40">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.label}
|
||||||
|
aria-current={item.active ? "page" : undefined}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 px-3 py-2 rounded-lg text-sm cursor-default select-none transition-colors text-left",
|
||||||
|
item.active
|
||||||
|
? "bg-neutral-800 text-white"
|
||||||
|
: "text-neutral-400 hover:bg-neutral-800/50 hover:text-neutral-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<item.icon className="w-4 h-4" aria-hidden="true" />
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
extensions/boltbot/dashboard/src/components/StatsCards.tsx
Normal file
54
extensions/boltbot/dashboard/src/components/StatsCards.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Activity, Shield, Layers, AlertTriangle } from "lucide-react";
|
||||||
|
import type { ReceiptStats } from "../types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stats: ReceiptStats | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{ label: "Total Actions", key: "total" as const, icon: Activity, color: "text-emerald-400" },
|
||||||
|
{ label: "Low Tier", key: "low" as const, icon: Shield, color: "text-emerald-400" },
|
||||||
|
{ label: "Medium Tier", key: "medium" as const, icon: Layers, color: "text-yellow-400" },
|
||||||
|
{ label: "High Tier", key: "high" as const, icon: AlertTriangle, color: "text-red-400" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function getValue(stats: ReceiptStats | undefined, key: string): number {
|
||||||
|
if (!stats) return 0;
|
||||||
|
if (key === "total") return stats.total;
|
||||||
|
return stats.byTier[key] ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StatsCards({ stats, isLoading, error }: Props) {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-red-400 text-sm p-4">
|
||||||
|
Failed to load stats: {error instanceof Error ? error.message : "Unknown error"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{cards.map((card) => (
|
||||||
|
<div
|
||||||
|
key={card.key}
|
||||||
|
className="bg-neutral-900 rounded-xl border border-neutral-800 p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<card.icon className={`w-4 h-4 ${card.color}`} />
|
||||||
|
<span className="text-xs text-neutral-400 uppercase tracking-wide">
|
||||||
|
{card.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{isLoading && !stats ? (
|
||||||
|
<div className="h-8 w-16 animate-pulse bg-neutral-800 rounded" />
|
||||||
|
) : (
|
||||||
|
<div className="text-2xl font-bold">{getValue(stats, card.key)}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
extensions/boltbot/dashboard/src/hooks.ts
Normal file
28
extensions/boltbot/dashboard/src/hooks.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import useSWR from "swr";
|
||||||
|
import type { ActionReceipt, ReceiptStats } from "./types";
|
||||||
|
|
||||||
|
// TODO: Telegram OAuth — filter receipts by authenticated user's sessionKey
|
||||||
|
|
||||||
|
const fetcher = (url: string) =>
|
||||||
|
fetch(url).then((r) => {
|
||||||
|
if (!r.ok) throw new Error(r.statusText);
|
||||||
|
return r.json();
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useStats() {
|
||||||
|
const { data, isLoading, error } = useSWR<ReceiptStats>(
|
||||||
|
"/boltbot/stats",
|
||||||
|
fetcher,
|
||||||
|
{ refreshInterval: 10000, keepPreviousData: true },
|
||||||
|
);
|
||||||
|
return { stats: data, isLoading, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useReceipts(limit: number, offset: number) {
|
||||||
|
const { data, isLoading, error } = useSWR<{ receipts: ActionReceipt[] }>(
|
||||||
|
`/boltbot/receipts?limit=${limit}&offset=${offset}`,
|
||||||
|
fetcher,
|
||||||
|
{ refreshInterval: 10000, keepPreviousData: true },
|
||||||
|
);
|
||||||
|
return { receipts: data?.receipts, isLoading, error };
|
||||||
|
}
|
||||||
8
extensions/boltbot/dashboard/src/index.css
Normal file
8
extensions/boltbot/dashboard/src/index.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #0a0a0a; /* neutral-950 */
|
||||||
|
color: #f5f5f5; /* neutral-100 */
|
||||||
|
font-family: "Space Grotesk", sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
10
extensions/boltbot/dashboard/src/main.tsx
Normal file
10
extensions/boltbot/dashboard/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import App from "./App";
|
||||||
|
import "./index.css";
|
||||||
|
|
||||||
|
createRoot(document.getElementById("root")!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
19
extensions/boltbot/dashboard/src/types.ts
Normal file
19
extensions/boltbot/dashboard/src/types.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export interface ActionReceipt {
|
||||||
|
id: string;
|
||||||
|
timestamp: string;
|
||||||
|
sessionKey: string;
|
||||||
|
tier: "low" | "medium" | "high";
|
||||||
|
toolName: string;
|
||||||
|
argumentsHash: string;
|
||||||
|
resultHash: string;
|
||||||
|
success: boolean;
|
||||||
|
durationMs: number;
|
||||||
|
anomalies: string[];
|
||||||
|
daCommitment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReceiptStats {
|
||||||
|
total: number;
|
||||||
|
byTier: Record<string, number>;
|
||||||
|
anomalyCount: number;
|
||||||
|
}
|
||||||
36
extensions/boltbot/dashboard/src/utils.ts
Normal file
36
extensions/boltbot/dashboard/src/utils.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export function formatRelativeTime(iso: string): string {
|
||||||
|
const now = Date.now();
|
||||||
|
const then = new Date(iso).getTime();
|
||||||
|
const diffMs = now - then;
|
||||||
|
|
||||||
|
if (diffMs < 0) return "just now";
|
||||||
|
|
||||||
|
const seconds = Math.floor(diffMs / 1000);
|
||||||
|
if (seconds < 60) return "just now";
|
||||||
|
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) return `${minutes}m ago`;
|
||||||
|
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
if (days < 30) return `${days}d ago`;
|
||||||
|
|
||||||
|
const months = Math.floor(days / 30);
|
||||||
|
if (months < 12) return `${months}mo ago`;
|
||||||
|
|
||||||
|
const years = Math.floor(months / 12);
|
||||||
|
return `${years}y ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDuration(ms: number): string {
|
||||||
|
if (ms >= 1000) {
|
||||||
|
return `${(ms / 1000).toFixed(1)}s`;
|
||||||
|
}
|
||||||
|
return `${ms}ms`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cn(...classes: (string | false | undefined | null)[]): string {
|
||||||
|
return classes.filter(Boolean).join(" ");
|
||||||
|
}
|
||||||
1
extensions/boltbot/dashboard/src/vite-env.d.ts
vendored
Normal file
1
extensions/boltbot/dashboard/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
16
extensions/boltbot/dashboard/tsconfig.json
Normal file
16
extensions/boltbot/dashboard/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
8
extensions/boltbot/dashboard/vite.config.ts
Normal file
8
extensions/boltbot/dashboard/vite.config.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
base: "/boltbot/dashboard/",
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
});
|
||||||
@ -4,6 +4,7 @@ import { eigenCloudProvider } from "./src/provider.js";
|
|||||||
import { createActionLogger } from "./src/action-logger.js";
|
import { createActionLogger } from "./src/action-logger.js";
|
||||||
import { createReceiptStore } from "./src/receipt-store.js";
|
import { createReceiptStore } from "./src/receipt-store.js";
|
||||||
import { registerBoltbotApi } from "./src/api.js";
|
import { registerBoltbotApi } from "./src/api.js";
|
||||||
|
import { registerDashboardRoutes } from "./src/dashboard-serve.js";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
id: "boltbot",
|
id: "boltbot",
|
||||||
@ -19,5 +20,6 @@ export default {
|
|||||||
api.on("after_tool_call", logger);
|
api.on("after_tool_call", logger);
|
||||||
|
|
||||||
registerBoltbotApi(api, store);
|
registerBoltbotApi(api, store);
|
||||||
|
registerDashboardRoutes(api);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,9 @@
|
|||||||
"moltbot": {
|
"moltbot": {
|
||||||
"extensions": ["./index.ts"]
|
"extensions": ["./index.ts"]
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build:dashboard": "cd dashboard && npm run build"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^11.0.0"
|
"better-sqlite3": "^11.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
88
extensions/boltbot/src/dashboard-serve.ts
Normal file
88
extensions/boltbot/src/dashboard-serve.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join, normalize, resolve, extname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
|
|
||||||
|
type PluginApi = {
|
||||||
|
registerHttpRoute: (params: {
|
||||||
|
path: string;
|
||||||
|
handler: (req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
|
||||||
|
}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIST_DIR = join(fileURLToPath(import.meta.url), "../../dashboard/dist");
|
||||||
|
|
||||||
|
const CONTENT_TYPES: Record<string, string> = {
|
||||||
|
".html": "text/html",
|
||||||
|
".js": "text/javascript",
|
||||||
|
".css": "text/css",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".json": "application/json",
|
||||||
|
".png": "image/png",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
".woff2": "font/woff2",
|
||||||
|
".woff": "font/woff",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function registerDashboardRoutes(api: PluginApi) {
|
||||||
|
api.registerHttpRoute({
|
||||||
|
path: "/boltbot/dashboard",
|
||||||
|
handler: (req, res) => {
|
||||||
|
const url = req.url ?? "/";
|
||||||
|
const basePath = "/boltbot/dashboard";
|
||||||
|
const idx = url.indexOf(basePath);
|
||||||
|
let filePath = idx !== -1 ? url.slice(idx + basePath.length) : "/";
|
||||||
|
|
||||||
|
// Strip query string
|
||||||
|
const qIdx = filePath.indexOf("?");
|
||||||
|
if (qIdx !== -1) filePath = filePath.slice(0, qIdx);
|
||||||
|
|
||||||
|
// Default to index.html
|
||||||
|
if (!filePath || filePath === "/") filePath = "/index.html";
|
||||||
|
|
||||||
|
// Sanitize: prevent directory traversal
|
||||||
|
const decoded = decodeURIComponent(filePath);
|
||||||
|
const absolutePath = join(DIST_DIR, normalize(decoded));
|
||||||
|
const resolvedDist = resolve(DIST_DIR);
|
||||||
|
if (!resolve(absolutePath).startsWith(resolvedDist + "/")) {
|
||||||
|
res.writeHead(403);
|
||||||
|
res.end("Forbidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content: Buffer;
|
||||||
|
let servingIndex = false;
|
||||||
|
try {
|
||||||
|
content = readFileSync(absolutePath);
|
||||||
|
} catch {
|
||||||
|
// SPA catch-all: serve index.html for unknown paths
|
||||||
|
try {
|
||||||
|
content = readFileSync(join(DIST_DIR, "index.html"));
|
||||||
|
servingIndex = true;
|
||||||
|
} catch {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end("Not Found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = servingIndex ? ".html" : extname(absolutePath);
|
||||||
|
const contentType = CONTENT_TYPES[ext] ?? "application/octet-stream";
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": contentType,
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-Frame-Options": "DENY",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (servingIndex || ext === ".html") {
|
||||||
|
headers["Cache-Control"] = "no-cache";
|
||||||
|
} else if (normalize(decoded).startsWith("/assets/")) {
|
||||||
|
headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, headers);
|
||||||
|
res.end(content);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
270
pnpm-lock.yaml
generated
270
pnpm-lock.yaml
generated
@ -264,6 +264,12 @@ importers:
|
|||||||
|
|
||||||
extensions/bluebubbles: {}
|
extensions/bluebubbles: {}
|
||||||
|
|
||||||
|
extensions/boltbot:
|
||||||
|
dependencies:
|
||||||
|
better-sqlite3:
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.10.0
|
||||||
|
|
||||||
extensions/copilot-proxy: {}
|
extensions/copilot-proxy: {}
|
||||||
|
|
||||||
extensions/diagnostics-otel:
|
extensions/diagnostics-otel:
|
||||||
@ -383,12 +389,12 @@ importers:
|
|||||||
'@microsoft/agents-hosting-extensions-teams':
|
'@microsoft/agents-hosting-extensions-teams':
|
||||||
specifier: ^1.2.2
|
specifier: ^1.2.2
|
||||||
version: 1.2.2
|
version: 1.2.2
|
||||||
moltbot:
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../..
|
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
|
moltbot:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../..
|
||||||
proper-lockfile:
|
proper-lockfile:
|
||||||
specifier: ^4.1.2
|
specifier: ^4.1.2
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
@ -3094,6 +3100,9 @@ packages:
|
|||||||
before-after-hook@4.0.0:
|
before-after-hook@4.0.0:
|
||||||
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
|
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
|
||||||
|
|
||||||
|
better-sqlite3@11.10.0:
|
||||||
|
resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==}
|
||||||
|
|
||||||
bignumber.js@9.3.1:
|
bignumber.js@9.3.1:
|
||||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||||
|
|
||||||
@ -3101,6 +3110,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
bluebird@3.7.2:
|
bluebird@3.7.2:
|
||||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||||
|
|
||||||
@ -3144,6 +3159,9 @@ packages:
|
|||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
@ -3195,6 +3213,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
|
|
||||||
|
chownr@1.1.4:
|
||||||
|
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||||
|
|
||||||
chownr@3.0.0:
|
chownr@3.0.0:
|
||||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -3214,11 +3235,6 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
clawdbot@2026.1.24-3:
|
|
||||||
resolution: {integrity: sha512-zt9BzhWXduq8ZZR4rfzQDurQWAgmijTTyPZCQGrn5ew6wCEwhxxEr2/NHG7IlCwcfRsKymsY4se9KMhoNz0JtQ==}
|
|
||||||
engines: {node: '>=22.12.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
cli-cursor@5.0.0:
|
cli-cursor@5.0.0:
|
||||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -3373,6 +3389,10 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decompress-response@6.0.0:
|
||||||
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
deep-extend@0.6.0:
|
deep-extend@0.6.0:
|
||||||
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
@ -3462,6 +3482,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
entities@4.5.0:
|
entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
@ -3530,6 +3553,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
|
|
||||||
|
expand-template@2.0.3:
|
||||||
|
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
expect-type@1.3.0:
|
expect-type@1.3.0:
|
||||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@ -3589,6 +3616,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
|
resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0:
|
||||||
|
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
||||||
|
|
||||||
filename-reserved-regex@3.0.0:
|
filename-reserved-regex@3.0.0:
|
||||||
resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==}
|
resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@ -3660,6 +3690,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
fs-constants@1.0.0:
|
||||||
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
fs-extra@11.3.3:
|
fs-extra@11.3.3:
|
||||||
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
|
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
@ -3712,6 +3745,9 @@ packages:
|
|||||||
getpass@0.1.7:
|
getpass@0.1.7:
|
||||||
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
||||||
|
|
||||||
|
github-from-package@0.0.0:
|
||||||
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -4322,6 +4358,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
mimic-response@3.1.0:
|
||||||
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
minimalistic-assert@1.0.1:
|
minimalistic-assert@1.0.1:
|
||||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||||
|
|
||||||
@ -4347,6 +4387,9 @@ packages:
|
|||||||
mitt@3.0.1:
|
mitt@3.0.1:
|
||||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3:
|
||||||
|
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||||
|
|
||||||
mkdirp@3.0.1:
|
mkdirp@3.0.1:
|
||||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -4389,6 +4432,9 @@ packages:
|
|||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0:
|
||||||
|
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||||
|
|
||||||
negotiator@0.6.3:
|
negotiator@0.6.3:
|
||||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@ -4397,6 +4443,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
node-abi@3.87.0:
|
||||||
|
resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
node-addon-api@8.5.0:
|
node-addon-api@8.5.0:
|
||||||
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
resolution: {integrity: sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==}
|
||||||
engines: {node: ^18 || ^20 || >= 21}
|
engines: {node: ^18 || ^20 || >= 21}
|
||||||
@ -4722,6 +4772,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
prebuild-install@7.1.3:
|
||||||
|
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
pretty-bytes@6.1.1:
|
pretty-bytes@6.1.1:
|
||||||
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
||||||
engines: {node: ^14.13.1 || >=16.0.0}
|
engines: {node: ^14.13.1 || >=16.0.0}
|
||||||
@ -4786,6 +4841,9 @@ packages:
|
|||||||
psl@1.15.0:
|
psl@1.15.0:
|
||||||
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==}
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
punycode.js@2.3.1:
|
punycode.js@2.3.1:
|
||||||
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -5029,6 +5087,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
signal-polyfill: ^0.2.0
|
signal-polyfill: ^0.2.0
|
||||||
|
|
||||||
|
simple-concat@1.0.1:
|
||||||
|
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||||
|
|
||||||
simple-git@3.30.0:
|
simple-git@3.30.0:
|
||||||
resolution: {integrity: sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==}
|
resolution: {integrity: sha512-q6lxyDsCmEal/MEGhP1aVyQ3oxnagGlBDOVSIB4XUVLl1iZh0Pah6ebC9V4xBap/RfgP2WlI8EKs0WS0rMEJHg==}
|
||||||
|
|
||||||
@ -5190,6 +5254,13 @@ packages:
|
|||||||
tailwindcss@4.1.17:
|
tailwindcss@4.1.17:
|
||||||
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
|
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tar@7.5.4:
|
tar@7.5.4:
|
||||||
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
|
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@ -8954,10 +9025,25 @@ snapshots:
|
|||||||
before-after-hook@4.0.0:
|
before-after-hook@4.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
better-sqlite3@11.10.0:
|
||||||
|
dependencies:
|
||||||
|
bindings: 1.5.0
|
||||||
|
prebuild-install: 7.1.3
|
||||||
|
|
||||||
bignumber.js@9.3.1: {}
|
bignumber.js@9.3.1: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
|
bindings@1.5.0:
|
||||||
|
dependencies:
|
||||||
|
file-uri-to-path: 1.0.0
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
buffer: 5.7.1
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
bluebird@3.7.2: {}
|
bluebird@3.7.2: {}
|
||||||
|
|
||||||
body-parser@1.20.4:
|
body-parser@1.20.4:
|
||||||
@ -9017,6 +9103,11 @@ snapshots:
|
|||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@ -9081,6 +9172,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 5.0.0
|
readdirp: 5.0.0
|
||||||
|
|
||||||
|
chownr@1.1.4: {}
|
||||||
|
|
||||||
chownr@3.0.0: {}
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
chromium-bidi@13.0.1(devtools-protocol@0.0.1561482):
|
chromium-bidi@13.0.1(devtools-protocol@0.0.1561482):
|
||||||
@ -9098,84 +9191,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
clawdbot@2026.1.24-3(@types/express@5.0.6)(audio-decode@2.2.3)(devtools-protocol@0.0.1561482)(typescript@5.9.3):
|
|
||||||
dependencies:
|
|
||||||
'@agentclientprotocol/sdk': 0.13.1(zod@4.3.6)
|
|
||||||
'@aws-sdk/client-bedrock': 3.975.0
|
|
||||||
'@buape/carbon': 0.14.0(hono@4.11.4)
|
|
||||||
'@clack/prompts': 0.11.0
|
|
||||||
'@grammyjs/runner': 2.0.3(grammy@1.39.3)
|
|
||||||
'@grammyjs/transformer-throttler': 1.2.1(grammy@1.39.3)
|
|
||||||
'@homebridge/ciao': 1.3.4
|
|
||||||
'@line/bot-sdk': 10.6.0
|
|
||||||
'@lydell/node-pty': 1.2.0-beta.3
|
|
||||||
'@mariozechner/pi-agent-core': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
|
||||||
'@mariozechner/pi-ai': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
|
||||||
'@mariozechner/pi-coding-agent': 0.49.3(ws@8.19.0)(zod@4.3.6)
|
|
||||||
'@mariozechner/pi-tui': 0.49.3
|
|
||||||
'@mozilla/readability': 0.6.0
|
|
||||||
'@sinclair/typebox': 0.34.47
|
|
||||||
'@slack/bolt': 4.6.0(@types/express@5.0.6)
|
|
||||||
'@slack/web-api': 7.13.0
|
|
||||||
'@whiskeysockets/baileys': 7.0.0-rc.9(audio-decode@2.2.3)(sharp@0.34.5)
|
|
||||||
ajv: 8.17.1
|
|
||||||
body-parser: 2.2.2
|
|
||||||
chalk: 5.6.2
|
|
||||||
chokidar: 5.0.0
|
|
||||||
chromium-bidi: 13.0.1(devtools-protocol@0.0.1561482)
|
|
||||||
cli-highlight: 2.1.11
|
|
||||||
commander: 14.0.2
|
|
||||||
croner: 9.1.0
|
|
||||||
detect-libc: 2.1.2
|
|
||||||
discord-api-types: 0.38.37
|
|
||||||
dotenv: 17.2.3
|
|
||||||
express: 5.2.1
|
|
||||||
file-type: 21.3.0
|
|
||||||
grammy: 1.39.3
|
|
||||||
hono: 4.11.4
|
|
||||||
jiti: 2.6.1
|
|
||||||
json5: 2.2.3
|
|
||||||
jszip: 3.10.1
|
|
||||||
linkedom: 0.18.12
|
|
||||||
long: 5.3.2
|
|
||||||
markdown-it: 14.1.0
|
|
||||||
node-edge-tts: 1.2.9
|
|
||||||
osc-progress: 0.3.0
|
|
||||||
pdfjs-dist: 5.4.530
|
|
||||||
playwright-core: 1.58.0
|
|
||||||
proper-lockfile: 4.1.2
|
|
||||||
qrcode-terminal: 0.12.0
|
|
||||||
sharp: 0.34.5
|
|
||||||
sqlite-vec: 0.1.7-alpha.2
|
|
||||||
tar: 7.5.4
|
|
||||||
tslog: 4.10.2
|
|
||||||
undici: 7.19.0
|
|
||||||
ws: 8.19.0
|
|
||||||
yaml: 2.8.2
|
|
||||||
zod: 4.3.6
|
|
||||||
optionalDependencies:
|
|
||||||
'@napi-rs/canvas': 0.1.88
|
|
||||||
node-llama-cpp: 3.15.0(typescript@5.9.3)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@discordjs/opus'
|
|
||||||
- '@modelcontextprotocol/sdk'
|
|
||||||
- '@types/express'
|
|
||||||
- audio-decode
|
|
||||||
- aws-crt
|
|
||||||
- bufferutil
|
|
||||||
- canvas
|
|
||||||
- debug
|
|
||||||
- devtools-protocol
|
|
||||||
- encoding
|
|
||||||
- ffmpeg-static
|
|
||||||
- jimp
|
|
||||||
- link-preview-js
|
|
||||||
- node-opus
|
|
||||||
- opusscript
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
- utf-8-validate
|
|
||||||
|
|
||||||
cli-cursor@5.0.0:
|
cli-cursor@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 5.1.0
|
restore-cursor: 5.1.0
|
||||||
@ -9329,8 +9344,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
deep-extend@0.6.0:
|
decompress-response@6.0.0:
|
||||||
optional: true
|
dependencies:
|
||||||
|
mimic-response: 3.1.0
|
||||||
|
|
||||||
|
deep-extend@0.6.0: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
@ -9407,6 +9425,10 @@ snapshots:
|
|||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
|
|
||||||
entities@7.0.1: {}
|
entities@7.0.1: {}
|
||||||
@ -9480,6 +9502,8 @@ snapshots:
|
|||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
|
expand-template@2.0.3: {}
|
||||||
|
|
||||||
expect-type@1.3.0: {}
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
express@4.22.1:
|
express@4.22.1:
|
||||||
@ -9598,6 +9622,8 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
file-uri-to-path@1.0.0: {}
|
||||||
|
|
||||||
filename-reserved-regex@3.0.0:
|
filename-reserved-regex@3.0.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -9683,6 +9709,8 @@ snapshots:
|
|||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fs-extra@11.3.3:
|
fs-extra@11.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@ -9757,6 +9785,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
assert-plus: 1.0.0
|
assert-plus: 1.0.0
|
||||||
|
|
||||||
|
github-from-package@0.0.0: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@ -9937,8 +9967,7 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
ini@1.3.8:
|
ini@1.3.8: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
@ -10385,6 +10414,8 @@ snapshots:
|
|||||||
mimic-function@5.0.1:
|
mimic-function@5.0.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
mimic-response@3.1.0: {}
|
||||||
|
|
||||||
minimalistic-assert@1.0.1: {}
|
minimalistic-assert@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.1.1:
|
minimatch@10.1.1:
|
||||||
@ -10395,8 +10426,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.0.2
|
brace-expansion: 2.0.2
|
||||||
|
|
||||||
minimist@1.2.8:
|
minimist@1.2.8: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
@ -10406,6 +10436,8 @@ snapshots:
|
|||||||
|
|
||||||
mitt@3.0.1: {}
|
mitt@3.0.1: {}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3: {}
|
||||||
|
|
||||||
mkdirp@3.0.1: {}
|
mkdirp@3.0.1: {}
|
||||||
|
|
||||||
module-details-from-path@1.0.4: {}
|
module-details-from-path@1.0.4: {}
|
||||||
@ -10457,10 +10489,16 @@ snapshots:
|
|||||||
nanoid@5.1.6:
|
nanoid@5.1.6:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0: {}
|
||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
|
node-abi@3.87.0:
|
||||||
|
dependencies:
|
||||||
|
semver: 7.7.3
|
||||||
|
|
||||||
node-addon-api@8.5.0:
|
node-addon-api@8.5.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -10826,6 +10864,21 @@ snapshots:
|
|||||||
|
|
||||||
postgres@3.4.8: {}
|
postgres@3.4.8: {}
|
||||||
|
|
||||||
|
prebuild-install@7.1.3:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
expand-template: 2.0.3
|
||||||
|
github-from-package: 0.0.0
|
||||||
|
minimist: 1.2.8
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
napi-build-utils: 2.0.0
|
||||||
|
node-abi: 3.87.0
|
||||||
|
pump: 3.0.3
|
||||||
|
rc: 1.2.8
|
||||||
|
simple-get: 4.0.1
|
||||||
|
tar-fs: 2.1.4
|
||||||
|
tunnel-agent: 0.6.0
|
||||||
|
|
||||||
pretty-bytes@6.1.1:
|
pretty-bytes@6.1.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@ -10911,6 +10964,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
punycode.js@2.3.1: {}
|
punycode.js@2.3.1: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
@ -10977,7 +11035,6 @@ snapshots:
|
|||||||
ini: 1.3.8
|
ini: 1.3.8
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
strip-json-comments: 2.0.1
|
strip-json-comments: 2.0.1
|
||||||
optional: true
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -10994,7 +11051,6 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
string_decoder: 1.3.0
|
string_decoder: 1.3.0
|
||||||
util-deprecate: 1.0.2
|
util-deprecate: 1.0.2
|
||||||
optional: true
|
|
||||||
|
|
||||||
readable-stream@4.5.2:
|
readable-stream@4.5.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11302,6 +11358,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
signal-polyfill: 0.2.2
|
signal-polyfill: 0.2.2
|
||||||
|
|
||||||
|
simple-concat@1.0.1: {}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
decompress-response: 6.0.0
|
||||||
|
once: 1.4.0
|
||||||
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
simple-git@3.30.0:
|
simple-git@3.30.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@kwsites/file-exists': 1.1.1
|
'@kwsites/file-exists': 1.1.1
|
||||||
@ -11442,8 +11506,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ansi-regex: 6.2.2
|
ansi-regex: 6.2.2
|
||||||
|
|
||||||
strip-json-comments@2.0.1:
|
strip-json-comments@2.0.1: {}
|
||||||
optional: true
|
|
||||||
|
|
||||||
strnum@2.1.2: {}
|
strnum@2.1.2: {}
|
||||||
|
|
||||||
@ -11470,6 +11533,21 @@ snapshots:
|
|||||||
|
|
||||||
tailwindcss@4.1.17: {}
|
tailwindcss@4.1.17: {}
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
dependencies:
|
||||||
|
chownr: 1.1.4
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
pump: 3.0.3
|
||||||
|
tar-stream: 2.2.0
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
bl: 4.1.0
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
fs-constants: 1.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
tar@7.5.4:
|
tar@7.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/fs-minipass': 4.0.1
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user