Compare commits
No commits in common. "b2507518889d78e7c80058204645e79a39fe9eaa" and "572418f0ea6f9f815895c626d9cdb8a49f1daa73" have entirely different histories.
b250751888
...
572418f0ea
13 changed files with 661 additions and 881 deletions
353
ARCHITECTURE.md
353
ARCHITECTURE.md
|
|
@ -1,17 +1,15 @@
|
|||
# Tiger Command Center — Architecture
|
||||
|
||||
*Last updated: 2026-06-10. Covers the gateway migration, real sub-agent
|
||||
spawning, the TASKS.md inbox loop, the Telegram transcript mirror, and the
|
||||
unified audit trail.*
|
||||
*Last updated: 2026-05-03. Covers all services through the hardening session.*
|
||||
|
||||
---
|
||||
|
||||
## 1. System Overview
|
||||
|
||||
Self-hosted AI agent orchestration on a Hetzner VPS (8 GB RAM, Helsinki;
|
||||
Tailscale 100.75.128.45). Three host services + one containerised AI
|
||||
runtime behind Traefik, with ALL model traffic routed through a self-hosted
|
||||
LiteLLM gateway — no third-party balance can silently kill the system.
|
||||
Self-hosted AI agent orchestration on a Hetzner VPS (77.42.82.225, 8 GB RAM, Helsinki).
|
||||
Three host services + one containerised AI runtime behind Traefik.
|
||||
|
||||
Topology:
|
||||
|
||||
```
|
||||
Internet/Manohar
|
||||
|
|
@ -20,120 +18,275 @@ Internet/Manohar
|
|||
dokploy-traefik (v3.6.7)
|
||||
|
|
||||
+-- agent.manohargupta.com --> tiger-dashboard (Next.js, :3100)
|
||||
| | /api/* proxies (token server-side)
|
||||
| v
|
||||
| tiger-bridge (Express+tsx, :3456, localhost)
|
||||
| | docker exec / volume reads
|
||||
| v
|
||||
| |
|
||||
| tiger-bridge (Express, :3456, 127.0.0.1 only)
|
||||
| | docker exec
|
||||
| tiger-openclaw (OpenClaw v2026.3.12)
|
||||
| |
|
||||
+-- llm.manohargupta.com ----> litellm-gateway <-- ALL model calls
|
||||
| |-- MiniMax API (own key): minimax-3 (primary),
|
||||
| | minimax-2.7, minimax-2.7-fast
|
||||
| +-- Anthropic API (own key): claude-haiku, claude-sonnet
|
||||
| MiniMax-M2.7 -> openrouter/auto -> trinity:free
|
||||
|
|
||||
+-- angel.manohargupta.com --> position-tracker (standalone repo/deploy)
|
||||
|
|
||||
Telegram @Tiger_4321_bot <--> OpenClaw native channel (long-polling, owns the bot)
|
||||
Telegram @Tiger_4321_bot <-- /tiger/notify <-- Tiger agent
|
||||
```
|
||||
|
||||
## 2. Model Routing (post-OpenRouter)
|
||||
---
|
||||
|
||||
OpenRouter was removed 2026-06-10 after its credits ran dry and silently
|
||||
broke both Tiger and the bridge's classifier. Everything now goes through
|
||||
the self-hosted gateway:
|
||||
## 2. Services
|
||||
|
||||
- **OpenClaw** (`openclaw.json`): custom provider `litellm`
|
||||
(`baseUrl: https://llm.manohargupta.com/v1`, `api: openai-completions`).
|
||||
Primary `litellm/minimax-3` (1M ctx), fallbacks `litellm/minimax-2.7` →
|
||||
`litellm/claude-haiku` (cross-provider: survives a MiniMax outage).
|
||||
- **Bridge** (`lib/llm.ts`): slugs starting `anthropic/` go to Anthropic
|
||||
direct; everything else goes to the gateway. Env: `LLM_GATEWAY_URL`,
|
||||
`LLM_GATEWAY_KEY`, `TIGER_ROUTER_MODEL` (default `minimax-3`).
|
||||
- **Gateway config**: `/root/litellm/litellm_config.yaml`
|
||||
(`request_timeout: 300` to match the cron budget).
|
||||
### 2.1 tiger-openclaw (Docker container)
|
||||
|
||||
## 3. Sub-Agent Execution (the orchestration layer)
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Image | ghcr.io/openclaw/openclaw:2026.3.12 |
|
||||
| Container | tiger-openclaw |
|
||||
| User | node (uid=1000) |
|
||||
| Config | /home/node/.openclaw/openclaw.json |
|
||||
| Workspace | /home/node/.openclaw/workspace/ |
|
||||
| Volumes | tiger-config, tiger-workspace |
|
||||
| Bind mount | /root/OpenClawDashboard -> /home/node/dashboard:rw |
|
||||
| Compose | /opt/tiger/docker-compose.yml |
|
||||
|
||||
`bridge/src/lib/agents.ts` is the canonical specialist registry:
|
||||
**cody** (code), **ethan** (research), **cathy** (writing), **elon** (PM).
|
||||
Legacy ids coder/researcher/writer/pm are accepted as aliases.
|
||||
Agents: Tiger (orchestrator), Cody (coder), Ethan (researcher), Cathy (writer), Elon (PM).
|
||||
|
||||
A spawn (`POST /tiger/spawn`) runs an isolated OpenClaw session
|
||||
(`--session-id spawn-<agent>-<id>`) with the specialist persona prepended.
|
||||
Message transport is docker-cp of a temp file (escaping-proof). Runs are
|
||||
tracked in the `executions` table and serialized (`MAX_CONCURRENT=1` —
|
||||
parallel turns push the 8GB host into swap and everything times out).
|
||||
Completion fires a Telegram notification via `/tiger/notify`.
|
||||
Model chain (agents.defaults.model in openclaw.json):
|
||||
primary : minimax/MiniMax-M2.7
|
||||
fallback1: openrouter/auto
|
||||
fallback2: openrouter/arcee-ai/trinity-large-preview:free (free - billing safety net)
|
||||
|
||||
Upgrade path: define real per-agent entries in `openclaw.json agents.list`
|
||||
(own IDENTITY.md + workspace each), then change the `--agent` flag in
|
||||
spawn.ts. Documented in lib/agents.ts; deferred until the RAM situation is
|
||||
resolved.
|
||||
Cron jobs (cron/jobs.json):
|
||||
Tiger: Hourly Task Check-in 0 * * * * IST 90s timeout
|
||||
Tiger: Weekly Digest 0 9 * * 1 IST 90s timeout
|
||||
|
||||
## 4. TASKS.md Inbox Loop
|
||||
Both use delivery.mode="none" — they notify via curl to /tiger/notify, not OpenClaw delivery channel.
|
||||
"none" = no channel opened at all (correct: cron delivers via curl)
|
||||
"silent" = suppresses chat display but still opens the channel (wrong model for cron)
|
||||
|
||||
`workspace/TASKS.md` has a `## 📥 INBOX` section. `bridge/src/lib/inbox.ts`
|
||||
checks every 30 min (09:00–20:00 IST): takes the first `- [ ]` line,
|
||||
classifies it (`classifyAgent`), spawns the specialist, rewrites the line to
|
||||
`- [⏳ run-id → agent]`. Manual trigger: `POST /tiger/inbox/drain`.
|
||||
Bridge-side scheduling means zero model tokens burned on empty checks and
|
||||
no bearer tokens embedded in cron prompts.
|
||||
### 2.2 tiger-bridge (systemd: tiger-bridge.service)
|
||||
|
||||
## 5. Telegram
|
||||
Language : TypeScript/Express -> bridge/dist/
|
||||
Port : 3456, 127.0.0.1 only (UFW blocks public access)
|
||||
Source : /root/OpenClawDashboard/bridge/src/
|
||||
Auth : Authorization: Bearer TIGER_BRIDGE_TOKEN (all routes)
|
||||
SQLite : /root/OpenClawDashboard/bridge/tiger.db
|
||||
Tables : tasks, projects, messages (chat history), agents
|
||||
|
||||
- **The bot is owned by OpenClaw's native channel** (long-polling). The
|
||||
bridge's `TelegramChannel`, `telegram-webhook.ts` and `chat-mirror.ts`
|
||||
are legacy: Telegram forbids webhook + getUpdates on one token, so the
|
||||
webhook design could never receive a message.
|
||||
- **The dashboard mirror reads the native session transcript** —
|
||||
`routes/chat-telegram.ts` resolves the `telegram:` session from
|
||||
`sessions.json` and serves the JSONL with cursor pagination and mtime
|
||||
caching. It filters to what Telegram actually saw: assistant messages
|
||||
carrying toolCall blocks (working narration) are skipped, thinking blocks
|
||||
ignored, injected metadata/system boilerplate stripped from user messages.
|
||||
Token shared with: dashboard (server-side only), Tiger cron curl commands, Tiger env var.
|
||||
|
||||
## 6. Audit Trail
|
||||
### 2.3 tiger-dashboard (systemd: tiger-dashboard.service)
|
||||
|
||||
`GET /tiger/activity/audit` merges, at read time, every durable action
|
||||
store: `executions` (spawns), `tasks` (lifecycle), `outputs` (artifacts),
|
||||
and OpenClaw's cron run JSONL. Cursor-paginated (`before=<ISO>`), type
|
||||
filters. The dashboard `/activity` page adds recent file-modification
|
||||
events on the first page. Read-time merging means history is complete
|
||||
retroactively and no action can happen without its audit row.
|
||||
Framework : Next.js 14, App Router
|
||||
Port : 3100
|
||||
URL : agent.manohargupta.com (via Traefik)
|
||||
Source : /root/OpenClawDashboard/dashboard/src/
|
||||
WorkingDir : /root/OpenClawDashboard/dashboard
|
||||
|
||||
## 7. Crons (OpenClaw, tz Asia/Kolkata)
|
||||
All API calls are server-side route handlers — bearer token never reaches the browser.
|
||||
|
||||
| Job | Schedule | Timeout |
|
||||
|---|---|---|
|
||||
| Trade Baseline Reset | 9:15 daily | 60s |
|
||||
| Trade P&L Monitor | every 2 min | 60s |
|
||||
| Hourly Trade Summary + News | hourly | 90s |
|
||||
| Hourly Task Check-in | 0 9-21 | 300s |
|
||||
| EOD Trade Summary | 16:00 Mon–Fri | 300s |
|
||||
| Weekly Digest | Mon 9:00 | 300s |
|
||||
Build discipline: NEVER run npm run build while next start is live.
|
||||
In-memory and on-disk manifests split-brain -> ChunkLoadError in browser. Correct:
|
||||
systemctl stop tiger-dashboard
|
||||
npm run build
|
||||
systemctl start tiger-dashboard
|
||||
|
||||
Timeout budget rationale: agent turns on this RAM-starved host can take
|
||||
minutes; 300s is the ceiling that made chronically-failing jobs pass.
|
||||
### 2.4 Traefik (dokploy-traefik v3.6.7)
|
||||
|
||||
File provider: /etc/dokploy/traefik/dynamic/ (host = container path, live reload).
|
||||
One .yml file per service. No restart needed on edits.
|
||||
|
||||
BasicAuth: single $ in bcrypt hash in YAML (not $$ — that is Docker label syntax).
|
||||
Generate: htpasswd -nbB manohar 'password'
|
||||
|
||||
UFW FORWARD — use subnet rules, not specific IPs (bridge IP changes on Traefik restart):
|
||||
ufw route allow proto tcp from any to 172.17.0.0/16 port 80
|
||||
ufw route allow proto tcp from any to 172.17.0.0/16 port 443
|
||||
|
||||
---
|
||||
|
||||
## 3. Full API Surface (40+ routes, all Bearer-token protected)
|
||||
|
||||
### Health
|
||||
GET /tiger/status container health, memory/CPU
|
||||
GET /tiger/logs SSE stream of container logs
|
||||
|
||||
### Config
|
||||
GET /tiger/config read openclaw.json
|
||||
POST /tiger/config update openclaw.json
|
||||
GET /tiger/config/models list LLM providers + models
|
||||
GET /tiger/config/models/agents per-agent model overrides
|
||||
PATCH /tiger/config/models/agents/:id update agent model
|
||||
|
||||
### File-Backed Tasks and Projects (canonical source of truth)
|
||||
GET /tiger/file-tasks TASKS.md JSON block -> tasks[]
|
||||
GET /tiger/file-tasks/active in-progress + pending-action only
|
||||
GET /tiger/file-tasks/completed completed section only
|
||||
GET /tiger/file-tasks/projects PROJECTS.md JSON block -> projects[]
|
||||
|
||||
Parser contract: TASKS.md must contain a fenced json TASKS block at end-of-file.
|
||||
Absent -> 502 "TASKS.md missing TASKS json block". No regex fallback.
|
||||
Tiger always emits this block on every TASKS.md write.
|
||||
|
||||
### SQLite Tasks and Projects (legacy, used for dispatch queue)
|
||||
GET /tiger/tasks list tasks
|
||||
GET /tiger/tasks/:id get task
|
||||
PUT /tiger/tasks/:id update task
|
||||
DELETE /tiger/tasks/:id delete task
|
||||
POST /tiger/tasks/:id/execute enqueue for execution
|
||||
GET /tiger/projects list projects
|
||||
POST /tiger/projects create project
|
||||
GET /tiger/projects/:id get project
|
||||
PUT /tiger/projects/:id update project
|
||||
DELETE /tiger/projects/:id delete project
|
||||
GET /tiger/projects/:id/tasks tasks in project
|
||||
POST /tiger/projects/:id/tasks add task to project
|
||||
|
||||
### Agents and Workspace
|
||||
GET /tiger/agents list configured agents
|
||||
GET /tiger/agents/:id/files list agent workspace files
|
||||
GET /tiger/agents/:id/file read specific agent file
|
||||
PUT /tiger/agents/:id/file write agent file
|
||||
GET /tiger/agents/activity recent agent activity log
|
||||
GET /tiger/workspace list workspace root files
|
||||
GET /tiger/files/:path read workspace file by path
|
||||
|
||||
### Chat (SSE streaming)
|
||||
POST /tiger/chat SSE stream chat -> Tiger agent
|
||||
GET /tiger/chat/history recent messages (SQLite)
|
||||
DELETE /tiger/chat/history clear history
|
||||
POST /tiger/chat/persist persist message to SQLite
|
||||
|
||||
Shell safety: tempfile pattern (not string interpolation):
|
||||
Write message -> /tmp/msg_ts.txt
|
||||
docker cp /tmp/msg.txt tiger-openclaw:/tmp/msg.txt
|
||||
docker exec openclaw agent -m "$(cat /tmp/msg.txt)"
|
||||
|
||||
### Dispatch
|
||||
POST /tiger/dispatch enqueue task -> SQLite + agent inbox file
|
||||
GET /tiger/dispatch/status/:id poll execution status
|
||||
|
||||
### Cron
|
||||
GET /tiger/cron list jobs.json
|
||||
POST /tiger/cron/:id/run fire job manually
|
||||
|
||||
### Notifications and Routing
|
||||
POST /tiger/notify send Telegram msg {message, chatId?}
|
||||
POST /tiger/route-task LLM router: which agent handles this?
|
||||
|
||||
### Keys
|
||||
GET /tiger/keys presence map only (no values returned)
|
||||
PATCH /tiger/keys upsert a key
|
||||
DELETE /tiger/keys/:name remove a key
|
||||
|
||||
### Ops
|
||||
POST /tiger/exec run command in container (auth-gated)
|
||||
POST /tiger/restart restart tiger-openclaw
|
||||
POST /tiger/deploy-dashboard git pull + build + restart dashboard
|
||||
ALL /api/gateway proxy to OpenClaw gateway port 18789
|
||||
|
||||
---
|
||||
|
||||
## 4. Data Flows
|
||||
|
||||
### Chat Message
|
||||
|
||||
Browser -> POST /tiger/chat (SSE)
|
||||
bridge writes message -> /tmp/msg_ts.txt
|
||||
docker cp -> tiger-openclaw:/tmp/msg_ts.txt
|
||||
docker exec openclaw agent --session-id id -m "$(cat /tmp/msg.txt)"
|
||||
OpenClaw -> MiniMax (or fallback chain)
|
||||
SSE tokens -> bridge -> browser
|
||||
POST /tiger/chat/persist -> SQLite messages
|
||||
|
||||
### Cron Job Notification
|
||||
|
||||
OpenClaw cron (hourly, IST)
|
||||
Tiger reads TASKS.md from workspace
|
||||
if active tasks:
|
||||
curl POST http://172.17.0.1:3456/tiger/notify
|
||||
Authorization: Bearer TOKEN
|
||||
body: {message: status update}
|
||||
bridge -> Telegram Bot API -> @Tiger_4321_bot -> Manohar
|
||||
if HEARTBEAT_OK:
|
||||
nothing sent
|
||||
|
||||
---
|
||||
|
||||
## 5. Failure Modes
|
||||
|
||||
| Scenario | What happens | Recovery |
|
||||
|----------|-------------|----------|
|
||||
| MiniMax timeout >90s | Falls to openrouter/auto | Automatic |
|
||||
| OpenRouter billing error | Falls to trinity-large:free | Automatic |
|
||||
| All LLMs fail | Chat 500; cron errors | Check /tiger/keys; top up credits |
|
||||
| tiger-openclaw dies | 500 on exec routes | docker restart tiger-openclaw |
|
||||
| Bridge EADDRINUSE | systemd restart fails (stale nohup) | pkill -f node.*dist/index then start |
|
||||
| SQLite locked | Dispatch write contention | Retryable; rare |
|
||||
| ChunkLoadError | Build ran while next start was live | systemctl restart tiger-dashboard |
|
||||
| Traefik bridge IP change | UFW FORWARD drops traffic | Use subnet rules not specific IPs |
|
||||
| TASKS.md missing JSON block | /tiger/file-tasks returns 502 | Tiger rewrites TASKS.md |
|
||||
|
||||
---
|
||||
|
||||
## 6. Deploy Workflow
|
||||
|
||||
On Mac:
|
||||
cd ~/MyProjects/NemoClawDashboard
|
||||
npm run build # preflight: catch errors locally first
|
||||
git add -p # atomic commits, no git add -A
|
||||
git push origin main
|
||||
|
||||
On server (scripts/deploy.sh):
|
||||
cd /root/OpenClawDashboard && git pull
|
||||
cd bridge && npx tsc --noEmit && npm run build
|
||||
systemctl restart tiger-bridge
|
||||
cd ../dashboard
|
||||
systemctl stop tiger-dashboard
|
||||
npm run build
|
||||
systemctl start tiger-dashboard
|
||||
bash /root/OpenClawDashboard/scripts/smoke-test.sh
|
||||
|
||||
Mutagen: pause before server-side edits, resume after verifying build.
|
||||
Bind-mount perms: chown -R 1000:1000 /root/OpenClawDashboard
|
||||
|
||||
---
|
||||
|
||||
## 7. File Layout
|
||||
|
||||
/root/OpenClawDashboard/ canonical source (has .git)
|
||||
/root/NemoClawDashboard/ HOLLOW / WRONG -- never use
|
||||
~/MyProjects/NemoClawDashboard Mac-side Mutagen source
|
||||
|
||||
bridge/src/
|
||||
index.ts entry point; full route list in file header comment
|
||||
auth.ts bearer token middleware
|
||||
tiger.ts docker exec wrapper; SSH prefix for local dev
|
||||
db.ts SQLite schema + helpers
|
||||
lib/llm.ts LLM routing + model fallback chain
|
||||
lib/telegram.ts Telegram Bot API client (tempfile pattern)
|
||||
routes/ one file per route group (40+ routes)
|
||||
|
||||
dashboard/src/
|
||||
app/ Next.js App Router pages
|
||||
components/ React components
|
||||
|
||||
scripts/smoke-test.sh run after every deploy
|
||||
ARCHITECTURE.md this file
|
||||
|
||||
/opt/tiger/docker-compose.yml OpenClaw container definition
|
||||
|
||||
/var/lib/docker/volumes/tiger_tiger-config/_data/
|
||||
openclaw.json live config
|
||||
*.bak.json auto-backups (keep latest 3)
|
||||
cron/jobs.json cron job definitions
|
||||
|
||||
---
|
||||
|
||||
## 8. Security Posture
|
||||
|
||||
- Bridge: Bearer auth on all routes; token in `bridge/.env` +
|
||||
`dashboard/.env.local` + embedded in cron payloads (rotate all four
|
||||
together — `jobs.json` has it twice). Rotated 2026-06-10 after the old
|
||||
token leaked via a hardcode in `agents-activity.ts` to the public GitHub
|
||||
mirror. NEVER hardcode tokens in source: this repo mirrors publicly.
|
||||
- Git: Forgejo (origin, SSH port 2222, key `id_ed25519_forgejo`) + GitHub
|
||||
mirror. Push both.
|
||||
- position-tracker binds 127.0.0.1:3457; public access via Traefik at
|
||||
angel.manohargupta.com.
|
||||
- Known weak spots: litellm-db password, `/opt/dashboard` fossil with a
|
||||
stale token, dual Telegram pollers (bridge poller should be disabled).
|
||||
UFW: 22, 80, 443 open publicly.
|
||||
3456 (bridge) only from Docker bridge subnets.
|
||||
3000 (Dokploy), 3100 (dashboard) not directly exposed -- only via Traefik.
|
||||
|
||||
## 9. Known Constraints
|
||||
|
||||
- **RAM**: ~13GB workload on 8GB physical; 6+GB swap in steady state. This
|
||||
is the root cause of historical cron timeouts and the reason spawn
|
||||
concurrency is 1. Decision pending: evict homelab services vs upgrade.
|
||||
- OpenClaw v2026.3.12 predates MiniMax-M3, hence the explicit
|
||||
`litellm/minimax-3` provider-prefixed model id.
|
||||
Bearer token: 64-char hex. Never logged, never sent to browser. Rotate via bridge/.env.
|
||||
Traefik BasicAuth: bcrypt, single $ in YAML files. Realm: Tiger Command Center.
|
||||
OpenClaw gateway: bind: lan (Docker bridge only). Token in openclaw.json.
|
||||
/tiger/exec: auth-gated. Arbitrary command execution requires bearer token.
|
||||
/tiger/keys GET: presence map only. Key values never returned by any endpoint.
|
||||
|
|
|
|||
121
README.md
121
README.md
|
|
@ -1,60 +1,91 @@
|
|||
# Tiger Command Center
|
||||
# Clawd Agent Dashboard
|
||||
|
||||
> Self-hosted AI orchestration: one Tiger, four specialists, every action audited.
|
||||
> A premium, dark-mode "Command Center" for the Clawd AI Agent.
|
||||
|
||||
The control plane for **Tiger**, an OpenClaw-based AI agent running on a
|
||||
Hetzner VPS, reachable at `agent.manohargupta.com`. Tiger orchestrates four
|
||||
specialist sub-agents — **Cody** (code), **Ethan** (research), **Cathy**
|
||||
(writing), **Elon** (planning) — handles Telegram, watches Angel One
|
||||
positions, and drains a TASKS.md inbox while you do real work.
|
||||

|
||||
|
||||
## What lives here
|
||||
## Overview
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `dashboard/` | Next.js 14 command center UI (`tiger-dashboard`, :3100) |
|
||||
| `bridge/` | Express control-plane API (`tiger-bridge`, :3456, localhost-only) |
|
||||
| `skills/` | OpenClaw skills (spawn-delegate, angel-positions, inbox-manager, sys-health, youtube-full) |
|
||||
| `ARCHITECTURE.md` | The real system map — read this first |
|
||||
| `TOOLS.md` | Tool/skill quick reference |
|
||||
The **Clawd Dashboard** is a centralized interface designed to monitor and interact with the Clawd AI agent. It provides real-time visibility into the agent's memory, logs, scheduled tasks (cron jobs), and capabilities (skills), all wrapped in a sleek, responsive UI.
|
||||
|
||||
## Core capabilities
|
||||
## Features
|
||||
|
||||
- **Sub-agent spawning** — `POST /tiger/spawn` runs a specialist in an
|
||||
isolated OpenClaw session; result lands on Telegram. Tracked in `executions`.
|
||||
- **TASKS.md inbox** — drop `- [ ]` lines under `## 📥 INBOX`; the bridge
|
||||
dispatches the top item to the right specialist every 30 min (9–20 IST).
|
||||
- **Telegram mirror** — the homepage thread reads OpenClaw's native session
|
||||
transcript: full history, both directions, perfectly in sync.
|
||||
- **Audit trail** — `/activity` merges spawns, cron runs, task lifecycle,
|
||||
and outputs into one paginated, filterable timeline.
|
||||
- **Own model gateway** — every model call routes through
|
||||
`llm.manohargupta.com` (LiteLLM on own MiniMax/Anthropic keys). Primary:
|
||||
MiniMax-M3.
|
||||
- **📊 System Status**: Real-time heartbeat monitoring of the `clawdbot` process.
|
||||
- **🧠 Memory Management**: View and edit the agent's core memory (`MEMORY.md`) and daily logs.
|
||||
- **🛠️ Skills Registry**: Browse, edit, and manage the agent's capabilities and MCP tools.
|
||||
- **⏱️ Cron Jobs**: detailed view and control over scheduled background tasks.
|
||||
- **💬 Chat Interface**: Integrated chat window to communicate directly with the agent.
|
||||
- **🌗 Dark Mode**: Built with a "Slate & Violet" aesthetic optimized for low-light environments.
|
||||
|
||||
## Running it
|
||||
## Tech Stack
|
||||
|
||||
Both services are systemd units on the host:
|
||||
- **Framework**: [Next.js 14](https://nextjs.org/) (App Router)
|
||||
- **UI Components**: [Shadcn/UI](https://ui.shadcn.com/) (Radix Primitives)
|
||||
- **Styling**: [Tailwind CSS](https://tailwindcss.com/)
|
||||
- **Icons**: [Lucide React](https://lucide.dev/)
|
||||
- **State Management**: [SWR](https://swr.vercel.app/) / React Query
|
||||
- **Backend**: Next.js API Routes (Serverless)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- npm or pnpm
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/manohar6839/clawd-dashboard.git
|
||||
cd clawd-dashboard
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
cd dashboard && npm install
|
||||
```
|
||||
|
||||
3. Configure Environment:
|
||||
- Copy example configs:
|
||||
```bash
|
||||
cp config/mcporter.example.json config/mcporter.json
|
||||
cp config/cron.example.json config/cron.json
|
||||
```
|
||||
|
||||
### Running the Dashboard
|
||||
|
||||
Start the development server:
|
||||
|
||||
```bash
|
||||
systemctl restart tiger-bridge # Express via tsx — no build step
|
||||
cd dashboard && npm run build && systemctl restart tiger-dashboard
|
||||
npm run dashboard
|
||||
```
|
||||
|
||||
Env contracts:
|
||||
- `bridge/.env` — `TIGER_BRIDGE_TOKEN`, `LLM_GATEWAY_URL`, `LLM_GATEWAY_KEY`,
|
||||
`TIGER_ROUTER_MODEL`, Telegram credentials
|
||||
- `dashboard/.env.local` — `TIGER_BRIDGE_URL`, `TIGER_BRIDGE_TOKEN`
|
||||
The dashboard will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
⚠️ The bridge token is also embedded in OpenClaw cron payloads
|
||||
(`cron/jobs.json`, twice). Rotate all four locations together.
|
||||
## Project Structure
|
||||
|
||||
## Git
|
||||
|
||||
Forgejo is canonical (`git.manohargupta.com/manohar/OpenClawDashboard`, SSH
|
||||
port 2222); GitHub (`manohar6839/NemoClawDashboard`) is a **public** mirror —
|
||||
never commit secrets. Push to both:
|
||||
|
||||
```bash
|
||||
git push origin main && git push github main
|
||||
```
|
||||
clawd/
|
||||
├── .agent/ # Agent self-knowledge & documentation
|
||||
├── dashboard/ # Next.js Application
|
||||
│ ├── src/app/ # App Router Pages (Memory, Skills, Cron, Chat)
|
||||
│ └── src/components/ # Shared UI Components
|
||||
├── config/ # Agent configuration (Cron, MCP)
|
||||
├── memory/ # Agent daily logs
|
||||
├── tools/ # External tool scripts
|
||||
└── MEMORY.md # Core Agent Memory
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`).
|
||||
3. Commit your changes (`git commit -m 'feat: Add amazing feature'`).
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`).
|
||||
5. Open a Pull Request.
|
||||
|
||||
## License
|
||||
|
||||
MIT © [Manohar Air](https://github.com/manohar6839)
|
||||
|
|
|
|||
21
TOOLS.md
21
TOOLS.md
|
|
@ -13,23 +13,4 @@
|
|||
- **Skill**: `youtube-full`
|
||||
- **Location**: `skills/youtube-full`
|
||||
- **Capabilities**: Search videos, get transcripts, monitor channels/playlists.
|
||||
- **Reference**: Read `skills/youtube-full/SKILL.md` for instructions.
|
||||
## Specialist Delegation
|
||||
|
||||
- **Skill**: `spawn-delegate` — hand work to Cody/Ethan/Cathy/Elon via the
|
||||
bridge; results arrive on Telegram. Read `skills/spawn-delegate/SKILL.md`.
|
||||
|
||||
## Trading Positions
|
||||
|
||||
- **Skill**: `angel-positions` — read-only live P&L from
|
||||
`angel.manohargupta.com/api/positions`. Never executes trades.
|
||||
|
||||
## Task Inbox
|
||||
|
||||
- **Skill**: `inbox-manager` — add/list/drain `## 📥 INBOX` items in
|
||||
TASKS.md; the bridge auto-dispatches the top item every 30 min.
|
||||
|
||||
## System Health
|
||||
|
||||
- **Skill**: `sys-health` — host RAM/swap from `/proc/meminfo`, LLM gateway
|
||||
liveliness, bridge status, recent audit events.
|
||||
- **Reference**: Read `skills/youtube-full/SKILL.md` for instructions.
|
||||
|
|
@ -149,8 +149,6 @@ app.use("/tiger/notify", notifyRouter);
|
|||
app.use("/tiger/dispatch", dispatchRouter);
|
||||
app.use("/tiger/agents", agentsRouter);
|
||||
app.use("/tiger/agents/activity", agentsActivityRouter);
|
||||
// Complete audit trail (executions + tasks + outputs + cron runs, paginated)
|
||||
app.use("/tiger/activity/audit", (await import("./routes/activity-audit.js")).default);
|
||||
app.use("/tiger/deploy-dashboard", deployRouter);
|
||||
app.use("/tiger/route-task", routeTaskRouter);
|
||||
app.use("/tiger/keys", keysRouter);
|
||||
|
|
|
|||
|
|
@ -1,252 +0,0 @@
|
|||
/**
|
||||
* activity-audit.ts — GET /tiger/activity/audit : the complete audit trail
|
||||
*
|
||||
* Purpose: ONE chronological, paginated record of everything the system DID,
|
||||
* so nothing slips by unaudited. Merged sources, each a durable store (the
|
||||
* old activity feed only showed recent in-memory file events):
|
||||
*
|
||||
* executions (sqlite) — every spawn / sub-agent run, with outcome
|
||||
* tasks (sqlite) — task lifecycle (created / status changes)
|
||||
* outputs (sqlite) — every artifact an agent wrote
|
||||
* cron runs (volume) — OpenClaw's JSONL run history for every job
|
||||
*
|
||||
* Event shape (normalized):
|
||||
* { id, ts (ISO), type, actor, summary, status?, ref? }
|
||||
* type ∈ spawn | task | output | cron
|
||||
*
|
||||
* Pagination: ?limit=100&before=<ISO ts> walks backwards through history.
|
||||
* Optional ?types=spawn,cron filters at the source.
|
||||
*
|
||||
* Design note: sources are merged at read time rather than double-written
|
||||
* into a new audit table — no write-path changes, no risk of an action
|
||||
* happening without its audit row, history is complete retroactively.
|
||||
*/
|
||||
|
||||
import { Router, Request, Response } from "express";
|
||||
import { readFileSync, readdirSync, existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import db from "../db.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
const DATA_DIR =
|
||||
process.env.OPENCLAW_DATA_DIR ||
|
||||
"/var/lib/docker/volumes/tiger_tiger-config/_data";
|
||||
|
||||
export interface AuditEvent {
|
||||
id: string;
|
||||
ts: string; // ISO timestamp
|
||||
type: "spawn" | "task" | "output" | "cron";
|
||||
actor: string;
|
||||
summary: string;
|
||||
status?: string;
|
||||
ref?: string;
|
||||
}
|
||||
|
||||
// ─── SQLite sources ──────────────────────────────────────────────────────────
|
||||
|
||||
function executionEvents(beforeIso: string | null, limit: number): AuditEvent[] {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, agent, command, exit_code, started_at, completed_at
|
||||
FROM executions
|
||||
${beforeIso ? "WHERE started_at < ?" : ""}
|
||||
ORDER BY started_at DESC LIMIT ?`,
|
||||
)
|
||||
.all(...(beforeIso ? [beforeIso, limit] : [limit])) as Array<{
|
||||
id: string;
|
||||
agent: string | null;
|
||||
command: string | null;
|
||||
exit_code: number | null;
|
||||
started_at: string;
|
||||
completed_at: string | null;
|
||||
}>;
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: `exec:${r.id}`,
|
||||
ts: toIso(r.started_at),
|
||||
type: "spawn" as const,
|
||||
actor: r.agent ?? "unknown",
|
||||
summary: (r.command ?? "").replace(/^spawn:\s*/, "").slice(0, 160),
|
||||
status:
|
||||
r.exit_code === null ? "running" : r.exit_code === 0 ? "done" : "error",
|
||||
ref: r.id,
|
||||
}));
|
||||
}
|
||||
|
||||
function taskEvents(beforeIso: string | null, limit: number): AuditEvent[] {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, title, status, assigned_agent, updated_at
|
||||
FROM tasks
|
||||
${beforeIso ? "WHERE updated_at < ?" : ""}
|
||||
ORDER BY updated_at DESC LIMIT ?`,
|
||||
)
|
||||
.all(...(beforeIso ? [beforeIso, limit] : [limit])) as Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
assigned_agent: string | null;
|
||||
updated_at: string;
|
||||
}>;
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: `task:${r.id}:${r.updated_at}`,
|
||||
ts: toIso(r.updated_at),
|
||||
type: "task" as const,
|
||||
actor: r.assigned_agent ?? "tiger",
|
||||
summary: r.title.slice(0, 160),
|
||||
status: r.status,
|
||||
ref: r.id,
|
||||
}));
|
||||
}
|
||||
|
||||
function outputEvents(beforeIso: string | null, limit: number): AuditEvent[] {
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT id, filename, file_path, execution_id, created_at
|
||||
FROM outputs
|
||||
${beforeIso ? "WHERE created_at < ?" : ""}
|
||||
ORDER BY created_at DESC LIMIT ?`,
|
||||
)
|
||||
.all(...(beforeIso ? [beforeIso, limit] : [limit])) as Array<{
|
||||
id: string;
|
||||
filename: string;
|
||||
file_path: string;
|
||||
execution_id: string | null;
|
||||
created_at: string;
|
||||
}>;
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: `output:${r.id}`,
|
||||
ts: toIso(r.created_at),
|
||||
type: "output" as const,
|
||||
actor: "agent",
|
||||
summary: `wrote ${r.filename} (${r.file_path})`.slice(0, 160),
|
||||
ref: r.execution_id ?? r.id,
|
||||
}));
|
||||
}
|
||||
|
||||
// ─── Cron run history (OpenClaw JSONL on the volume) ────────────────────────
|
||||
// Cached by directory listing + sizes; cron runs append-only files.
|
||||
|
||||
let cronCache: { stamp: string; events: AuditEvent[] } | null = null;
|
||||
|
||||
/** jobId → human name, from cron/jobs.json (cached per cron rebuild). */
|
||||
function loadJobNames(): Record<string, string> {
|
||||
try {
|
||||
const raw = JSON.parse(
|
||||
readFileSync(join(DATA_DIR, "cron", "jobs.json"), "utf-8"),
|
||||
) as { jobs?: Array<{ id?: string; name?: string }> };
|
||||
const map: Record<string, string> = {};
|
||||
for (const j of raw.jobs ?? []) {
|
||||
if (j.id && j.name) map[j.id] = j.name;
|
||||
}
|
||||
return map;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function cronEvents(): AuditEvent[] {
|
||||
const runsDir = join(DATA_DIR, "cron", "runs");
|
||||
if (!existsSync(runsDir)) return [];
|
||||
|
||||
let files: string[];
|
||||
try {
|
||||
files = readdirSync(runsDir).filter((f) => f.endsWith(".jsonl"));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Cheap cache key: file list + sizes via a stat pass would be ideal; the
|
||||
// run files are small, so name-count + latest mtime via re-read every 30s
|
||||
// would also be fine. Keep it simple: rebuild when the listing changes.
|
||||
const stamp = files.join("|");
|
||||
if (cronCache && cronCache.stamp === stamp) return cronCache.events;
|
||||
|
||||
const jobNames = loadJobNames();
|
||||
const events: AuditEvent[] = [];
|
||||
for (const file of files) {
|
||||
let content: string;
|
||||
try {
|
||||
content = readFileSync(join(runsDir, file), "utf-8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const line of content.split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
let run: Record<string, any>;
|
||||
try {
|
||||
run = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
// Run files log lifecycle actions; only "finished" carries the outcome.
|
||||
if (run.action && run.action !== "finished") continue;
|
||||
const ts = run.startedAt ?? run.ts ?? run.runAtMs ?? run.timestamp;
|
||||
if (!ts) continue;
|
||||
const iso = typeof ts === "number" ? new Date(ts).toISOString() : toIso(String(ts));
|
||||
const jobId = String(run.jobId ?? file.replace(/\.jsonl$/, ""));
|
||||
const name = run.jobName ?? run.name ?? jobNames[jobId] ?? jobId;
|
||||
const status = run.status ?? (run.error ? "error" : "ok");
|
||||
events.push({
|
||||
id: `cron:${file}:${iso}`,
|
||||
ts: iso,
|
||||
type: "cron",
|
||||
actor: "cron",
|
||||
summary: String(name).slice(0, 160),
|
||||
status: String(status),
|
||||
ref: jobId,
|
||||
});
|
||||
}
|
||||
}
|
||||
cronCache = { stamp, events };
|
||||
return events;
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** sqlite datetime('now') yields "YYYY-MM-DD HH:MM:SS" (UTC, no zone) — make it ISO. */
|
||||
function toIso(s: string): string {
|
||||
if (!s) return new Date(0).toISOString();
|
||||
if (s.includes("T")) return s;
|
||||
return s.replace(" ", "T") + "Z";
|
||||
}
|
||||
|
||||
// ─── Route ──────────────────────────────────────────────────────────────────
|
||||
|
||||
router.get("/", (req: Request, res: Response) => {
|
||||
const limit = Math.min(
|
||||
Math.max(parseInt(String(req.query.limit ?? "100"), 10) || 100, 1),
|
||||
500,
|
||||
);
|
||||
const before = req.query.before ? String(req.query.before) : null;
|
||||
const typeFilter = req.query.types
|
||||
? new Set(String(req.query.types).split(","))
|
||||
: null;
|
||||
|
||||
const wants = (t: AuditEvent["type"]) => !typeFilter || typeFilter.has(t);
|
||||
|
||||
let events: AuditEvent[] = [];
|
||||
if (wants("spawn")) events.push(...executionEvents(before, limit));
|
||||
if (wants("task")) events.push(...taskEvents(before, limit));
|
||||
if (wants("output")) events.push(...outputEvents(before, limit));
|
||||
if (wants("cron")) {
|
||||
let cron = cronEvents();
|
||||
if (before) cron = cron.filter((e) => e.ts < before);
|
||||
events.push(...cron);
|
||||
}
|
||||
|
||||
events.sort((a, b) => (a.ts < b.ts ? 1 : -1)); // newest first
|
||||
const page = events.slice(0, limit);
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
events: page,
|
||||
hasMore: events.length > page.length,
|
||||
oldestTs: page.length > 0 ? page[page.length - 1].ts : null,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
|
@ -51,38 +51,6 @@ interface SessionIndexEntry {
|
|||
updatedAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenClaw injects machinery into user messages before they reach the model:
|
||||
* - leading blocks like:
|
||||
* Conversation info (untrusted metadata): ```json {...}```
|
||||
* Sender (untrusted metadata): ```json {...}```
|
||||
* - whole synthetic messages ("A new session was started via /new...",
|
||||
* heartbeat prompts, system reminders)
|
||||
* None of that was typed by the human in Telegram, so the mirror strips it.
|
||||
* Returning "" makes the caller drop the message entirely.
|
||||
*/
|
||||
function cleanUserText(raw: string): string {
|
||||
let text = raw;
|
||||
|
||||
// Strip any leading "<Label> (untrusted metadata): ```json ... ```" blocks.
|
||||
// They always appear before the real message; loop in case there are several.
|
||||
const metaBlock = /^\s*[A-Za-z ]+\(untrusted metadata\):\s*```json[\s\S]*?```\s*/;
|
||||
while (metaBlock.test(text)) {
|
||||
text = text.replace(metaBlock, "");
|
||||
}
|
||||
|
||||
// Synthetic system messages — not human input, drop them outright.
|
||||
const synthetic = [
|
||||
/^A new session was started via/,
|
||||
/^\[?HEARTBEAT/i,
|
||||
/^System:/,
|
||||
/^GroupChat context/,
|
||||
];
|
||||
if (synthetic.some((re) => re.test(text.trim()))) return "";
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/** Newest telegram session: key + transcript path. */
|
||||
function resolveTelegramSession(): { key: string; file: string } | null {
|
||||
let index: Record<string, SessionIndexEntry>;
|
||||
|
|
@ -143,26 +111,16 @@ function parseTranscript(file: string): ThreadMessage[] {
|
|||
|
||||
const content: unknown = entry.message?.content;
|
||||
let text = "";
|
||||
let hasToolCall = false;
|
||||
if (typeof content === "string") {
|
||||
text = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const c of content) {
|
||||
if (!c) continue;
|
||||
if (c.type === "toolCall") hasToolCall = true;
|
||||
if (c.type === "text" && typeof c.text === "string") text += c.text + "\n";
|
||||
// type === "thinking" is deliberately ignored
|
||||
}
|
||||
text = content
|
||||
.filter((c) => c && c.type === "text" && typeof c.text === "string")
|
||||
.map((c) => c.text)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
// Assistant messages that carry toolCall blocks are intermediate working
|
||||
// turns ("Let me check if codexbar is available...") — OpenClaw never
|
||||
// sent those to Telegram, so the mirror must not show them either.
|
||||
if (role === "assistant" && hasToolCall) continue;
|
||||
|
||||
if (role === "user") text = cleanUserText(text);
|
||||
text = text.trim();
|
||||
if (!text) continue; // tool-call-only turns / fully-injected messages
|
||||
if (!text) continue; // tool-call-only assistant turns have no text
|
||||
|
||||
messages.push({
|
||||
seq,
|
||||
|
|
|
|||
|
|
@ -1,192 +1,112 @@
|
|||
"use client"
|
||||
|
||||
/**
|
||||
* /activity — the complete audit trail.
|
||||
*
|
||||
* Every durable action in one timeline: sub-agent spawns, task lifecycle,
|
||||
* artifacts written, cron runs, file modifications. Type filters + "Load
|
||||
* older" pagination walk the entire history so nothing escapes audit.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { ScrollText, ChevronDown } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ScrollText } from "lucide-react"
|
||||
|
||||
interface ActivityEntry {
|
||||
id: string
|
||||
ts: string
|
||||
type: string
|
||||
actor: string
|
||||
summary: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
const TYPES = [
|
||||
{ id: "spawn", label: "Spawns" },
|
||||
{ id: "cron", label: "Cron" },
|
||||
{ id: "task", label: "Tasks" },
|
||||
{ id: "output", label: "Outputs" },
|
||||
{ id: "file", label: "Files" },
|
||||
]
|
||||
|
||||
const TYPE_COLORS: Record<string, string> = {
|
||||
spawn: "text-violet-400 border-violet-400/30",
|
||||
cron: "text-sky-400 border-sky-400/30",
|
||||
task: "text-amber-400 border-amber-400/30",
|
||||
output: "text-emerald-400 border-emerald-400/30",
|
||||
file: "text-muted-foreground border-border",
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
done: "text-emerald-400",
|
||||
ok: "text-emerald-400",
|
||||
running: "text-sky-400",
|
||||
error: "text-red-400",
|
||||
timestamp: string
|
||||
description: string
|
||||
source: string
|
||||
}
|
||||
|
||||
export default function ActivityPage() {
|
||||
const [entries, setEntries] = useState<ActivityEntry[]>([])
|
||||
const [active, setActive] = useState<Set<string>>(new Set(TYPES.map((t) => t.id)))
|
||||
const [hasMore, setHasMore] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchPage = useCallback(
|
||||
async (before?: string) => {
|
||||
const qs = new URLSearchParams({ limit: "100" })
|
||||
if (before) qs.set("before", before)
|
||||
if (active.size < TYPES.length) qs.set("types", Array.from(active).join(","))
|
||||
const r = await fetch(`/api/activity?${qs.toString()}`)
|
||||
return r.json() as Promise<{
|
||||
entries?: ActivityEntry[]
|
||||
hasMore?: boolean
|
||||
oldestTs?: string | null
|
||||
error?: string
|
||||
}>
|
||||
},
|
||||
[active],
|
||||
)
|
||||
|
||||
// (Re)load whenever filters change
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
fetchPage()
|
||||
.then((data) => {
|
||||
if (data.entries) {
|
||||
fetch("/api/activity?limit=20")
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data?.entries) {
|
||||
setEntries(data.entries)
|
||||
setHasMore(Boolean(data.hasMore))
|
||||
} else setError(data.error || "No data")
|
||||
}
|
||||
setLoading(false)
|
||||
})
|
||||
.catch((e: Error) => setError(e.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [fetchPage])
|
||||
.catch(e => {
|
||||
console.error("Failed to load:", e)
|
||||
setError(e.message)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const loadOlder = async () => {
|
||||
if (loadingMore || entries.length === 0) return
|
||||
setLoadingMore(true)
|
||||
try {
|
||||
const data = await fetchPage(entries[entries.length - 1].ts)
|
||||
if (data.entries && data.entries.length > 0) {
|
||||
setEntries((prev) => {
|
||||
const known = new Set(prev.map((e) => e.id))
|
||||
return [...prev, ...data.entries!.filter((e) => !known.has(e.id))]
|
||||
})
|
||||
setHasMore(Boolean(data.hasMore))
|
||||
} else setHasMore(false)
|
||||
} finally {
|
||||
setLoadingMore(false)
|
||||
const formatDate = (ts: string) => {
|
||||
if (!ts) return ""
|
||||
return new Date(ts).toLocaleString()
|
||||
}
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
switch (type) {
|
||||
case "heartbeat": return "text-green-500"
|
||||
case "chat": return "text-blue-500"
|
||||
case "config": return "text-yellow-500"
|
||||
case "memory": return "text-purple-500"
|
||||
case "system": return "text-orange-500"
|
||||
case "cron": return "text-cyan-500"
|
||||
default: return "text-muted-foreground"
|
||||
}
|
||||
}
|
||||
|
||||
const toggle = (id: string) =>
|
||||
setActive((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(id)) {
|
||||
if (next.size > 1) next.delete(id) // never filter down to nothing
|
||||
} else next.add(id)
|
||||
return next
|
||||
})
|
||||
const getSourceLabel = (source: string) => {
|
||||
switch (source) {
|
||||
case "main": return "🐅 Tiger"
|
||||
case "coder": return "📦 Cody"
|
||||
case "researcher": return "🔬 Ethan"
|
||||
case "pm": return "📋 Elon"
|
||||
default: return source || "🤖"
|
||||
}
|
||||
}
|
||||
|
||||
const fmt = (ts: string) =>
|
||||
new Date(ts).toLocaleString([], {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ScrollText className="h-5 w-5" />
|
||||
<h1 className="text-2xl font-bold">Activity</h1>
|
||||
</div>
|
||||
<div className="text-muted-foreground">Loading activity log...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ScrollText className="h-5 w-5" />
|
||||
<h1 className="text-2xl font-bold">Activity</h1>
|
||||
</div>
|
||||
<div className="text-red-500">Failed to load activity log</div>
|
||||
<div className="text-sm text-muted-foreground mt-2">{error}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-4xl">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<ScrollText className="h-5 w-5 text-primary" />
|
||||
<h1 className="text-xl font-semibold">Activity</h1>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<ScrollText className="h-5 w-5" />
|
||||
<h1 className="text-2xl font-bold">Activity</h1>
|
||||
<span className="text-muted-foreground text-sm">({entries.length} entries)</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
Complete audit trail — spawns, cron runs, tasks, outputs, file changes.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-5">
|
||||
{TYPES.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => toggle(t.id)}
|
||||
className={`text-xs px-3 py-1 rounded-full border transition-colors ${
|
||||
active.has(t.id)
|
||||
? TYPE_COLORS[t.id] + " bg-muted/30"
|
||||
: "text-muted-foreground/40 border-border/40"
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
<div className="space-y-2">
|
||||
{entries.map((entry, i) => (
|
||||
<div key={i} className="flex items-start gap-3 p-3 rounded-lg border bg-card/30">
|
||||
<div className="text-lg">{getSourceLabel(entry.source)}</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm truncate">{entry.description}</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span className={getTypeColor(entry.type)}>{entry.type}</span>
|
||||
<span>•</span>
|
||||
<span>{formatDate(entry.timestamp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loading && <div className="text-sm text-muted-foreground py-8">Loading…</div>}
|
||||
{error && <div className="text-sm text-red-500 py-8">Error: {error}</div>}
|
||||
|
||||
{!loading && !error && (
|
||||
<div className="flex flex-col">
|
||||
{entries.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground py-8">No events.</div>
|
||||
)}
|
||||
{entries.map((e) => (
|
||||
<div
|
||||
key={e.id}
|
||||
className="flex items-start gap-3 py-2.5 border-b border-border/40 text-sm"
|
||||
>
|
||||
<span className="text-[11px] text-muted-foreground/70 w-28 shrink-0 pt-0.5">
|
||||
{fmt(e.ts)}
|
||||
</span>
|
||||
<span
|
||||
className={`text-[10px] uppercase tracking-wide border rounded px-1.5 py-0.5 shrink-0 ${TYPE_COLORS[e.type] ?? TYPE_COLORS.file}`}
|
||||
>
|
||||
{e.type}
|
||||
</span>
|
||||
<span className="flex-1 break-words">
|
||||
<span className="text-muted-foreground">{e.actor}</span>{" "}
|
||||
{e.summary}
|
||||
{e.status && (
|
||||
<span className={`ml-2 text-[11px] ${STATUS_COLORS[e.status] ?? "text-muted-foreground"}`}>
|
||||
{e.status}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{hasMore && (
|
||||
<button
|
||||
onClick={loadOlder}
|
||||
disabled={loadingMore}
|
||||
className="self-center mt-4 text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 py-1.5 px-3 rounded hover:bg-muted/40 transition-colors"
|
||||
>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
{loadingMore ? "Loading…" : "Load older"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +1,44 @@
|
|||
/**
|
||||
* /api/activity — unified audit feed for the Activity page.
|
||||
*
|
||||
* Merges two bridge sources:
|
||||
* /tiger/activity/audit durable history: spawns, tasks, outputs, cron
|
||||
* runs — paginated, complete
|
||||
* /tiger/agents/activity recent file-modification events (in-memory,
|
||||
* recent-only by nature; merged for first page)
|
||||
*
|
||||
* ?limit=100&before=<ISO>&types=spawn,cron,task,output,file
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server"
|
||||
import { bridgeGet } from "@/lib/bridge"
|
||||
|
||||
export const dynamic = "force-dynamic"
|
||||
|
||||
interface AuditEvent {
|
||||
id: string
|
||||
ts: string
|
||||
type: string
|
||||
actor: string
|
||||
summary: string
|
||||
status?: string
|
||||
ref?: string
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const url = new URL(request.url)
|
||||
const limit = Math.min(parseInt(url.searchParams.get("limit") || "100", 10), 500)
|
||||
const before = url.searchParams.get("before") || ""
|
||||
const types = url.searchParams.get("types") || ""
|
||||
|
||||
try {
|
||||
const qs = new URLSearchParams({ limit: String(limit) })
|
||||
if (before) qs.set("before", before)
|
||||
if (types) qs.set("types", types.split(",").filter((t) => t !== "file").join(","))
|
||||
const url = new URL(request.url)
|
||||
const limit = parseInt(url.searchParams.get("limit") || "50", 10)
|
||||
|
||||
const audit = (await bridgeGet(`/tiger/activity/audit?${qs.toString()}`)) as {
|
||||
// Get activity from bridge endpoint that already works
|
||||
const bridgeData = await bridgeGet("/tiger/agents/activity") as {
|
||||
ok: boolean
|
||||
events?: AuditEvent[]
|
||||
hasMore?: boolean
|
||||
oldestTs?: string | null
|
||||
events: Array<{
|
||||
agentId: string
|
||||
agentName: string
|
||||
agentEmoji: string
|
||||
path: string
|
||||
action: string
|
||||
ts: number
|
||||
}>
|
||||
}
|
||||
|
||||
let events: AuditEvent[] = audit?.events ?? []
|
||||
|
||||
// File events only exist for the recent window — merge them into the
|
||||
// first page (no `before` cursor) when not filtered out.
|
||||
const wantFiles = !types || types.split(",").includes("file")
|
||||
if (!before && wantFiles) {
|
||||
try {
|
||||
const fileData = (await bridgeGet("/tiger/agents/activity")) as {
|
||||
ok: boolean
|
||||
events?: Array<{ agentId: string; agentName: string; path: string; action: string; ts: number }>
|
||||
}
|
||||
if (fileData?.ok && fileData.events) {
|
||||
events = events.concat(
|
||||
fileData.events.map((e) => ({
|
||||
id: `file:${e.agentId}:${e.ts}`,
|
||||
ts: new Date(e.ts).toISOString(),
|
||||
type: "file",
|
||||
actor: e.agentName,
|
||||
summary: `${e.action || "modified"} ${e.path}`,
|
||||
})),
|
||||
)
|
||||
}
|
||||
} catch { /* file source down — audit sources still serve */ }
|
||||
if (!bridgeData?.ok || !bridgeData.events) {
|
||||
return NextResponse.json({ entries: [], total: 0 })
|
||||
}
|
||||
|
||||
events.sort((a, b) => (a.ts < b.ts ? 1 : -1))
|
||||
const page = events.slice(0, limit)
|
||||
// Transform bridge format to activity format
|
||||
const entries = bridgeData.events.slice(0, limit).map((e) => ({
|
||||
id: `${e.agentId}-${e.ts}`,
|
||||
type: "system",
|
||||
timestamp: new Date(e.ts).toISOString(),
|
||||
description: `${e.agentName} modified ${e.path}`,
|
||||
source: e.agentId,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
entries: page,
|
||||
hasMore: Boolean(audit?.hasMore) || events.length > page.length,
|
||||
oldestTs: page.length > 0 ? page[page.length - 1].ts : null,
|
||||
entries,
|
||||
total: bridgeData.events.length,
|
||||
})
|
||||
} catch {
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: "Failed to fetch activity" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +1,230 @@
|
|||
"use client"
|
||||
|
||||
/**
|
||||
* /positions — intentionally NOT a positions UI.
|
||||
*
|
||||
* Live positions have a dedicated home: the standalone position-tracker at
|
||||
* https://angel.manohargupta.com (its own repo, own deploy, market-hours
|
||||
* aware). This dashboard previously replicated that UI here, which meant
|
||||
* two implementations drifting against the same Angel One data. One owner
|
||||
* per concern: the tracker owns positions; this page just hands you over.
|
||||
*/
|
||||
import * as React from "react"
|
||||
import { TrendingUp, TrendingDown, RefreshCw, Activity, DollarSign, BarChart2, Layers } from "lucide-react"
|
||||
import { StatCard } from "@/components/stat-card"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { ExternalLink, TrendingUp } from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
interface Position {
|
||||
key: string
|
||||
tradingsymbol: string
|
||||
exchange: string
|
||||
instrumenttype: string
|
||||
producttype: string
|
||||
netqty: number
|
||||
ltp: number
|
||||
avg_price: number
|
||||
unrealised_pnl: number
|
||||
realised_pnl: number
|
||||
total_pnl: number
|
||||
is_closed: number
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
const TRACKER_URL = "https://angel.manohargupta.com"
|
||||
interface Summary {
|
||||
totalUnrealised: number
|
||||
totalRealised: number
|
||||
totalPnl: number
|
||||
openPositions: number
|
||||
asOf?: string
|
||||
}
|
||||
|
||||
function fmt(n: number) {
|
||||
const sign = n >= 0 ? "+" : ""
|
||||
return `${sign}₹${Math.abs(n).toLocaleString("en-IN", { maximumFractionDigits: 0 })}`
|
||||
}
|
||||
|
||||
function PnlCell({ value }: { value: number }) {
|
||||
return (
|
||||
<span className={cn("font-mono tabular-nums", value > 0 ? "text-emerald-500" : value < 0 ? "text-rose-500" : "text-muted-foreground")}>
|
||||
{fmt(value)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PositionsPage() {
|
||||
useEffect(() => {
|
||||
// Auto-redirect after a beat — the card below is the no-JS / slow-net fallback.
|
||||
const t = setTimeout(() => {
|
||||
window.location.href = TRACKER_URL
|
||||
}, 800)
|
||||
return () => clearTimeout(t)
|
||||
const [positions, setPositions] = React.useState<Position[]>([])
|
||||
const [summary, setSummary] = React.useState<Summary | null>(null)
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const [refreshing, setRefreshing] = React.useState(false)
|
||||
const [error, setError] = React.useState<string | null>(null)
|
||||
const [lastUpdated, setLastUpdated] = React.useState<Date | null>(null)
|
||||
|
||||
const load = React.useCallback(async (silent = false) => {
|
||||
if (!silent) setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch("/api/positions", { cache: "no-store" })
|
||||
const data = await res.json()
|
||||
if (!data.ok) throw new Error(data.error ?? "Failed to load")
|
||||
setPositions(data.positions ?? [])
|
||||
setSummary(data.summary ?? null)
|
||||
setLastUpdated(new Date())
|
||||
} catch (e: any) {
|
||||
setError(e.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
try {
|
||||
await fetch("/api/positions", { method: "POST" })
|
||||
} catch { /* ignore */ }
|
||||
await load(true)
|
||||
setRefreshing(false)
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
load()
|
||||
const id = setInterval(() => load(true), 30_000)
|
||||
return () => clearInterval(id)
|
||||
}, [load])
|
||||
|
||||
const open = positions.filter(p => p.netqty !== 0 && !p.is_closed)
|
||||
const closed = positions.filter(p => p.netqty === 0 && p.is_closed && p.realised_pnl !== 0)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center p-6">
|
||||
<Card className="bg-card/40 p-8 max-w-md text-center">
|
||||
<TrendingUp className="h-8 w-8 text-primary mx-auto mb-4" />
|
||||
<h1 className="text-lg font-semibold mb-2">Positions live on the tracker</h1>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
Redirecting you to the dedicated position tracker — single source of
|
||||
truth for live P&L, bands and alerts.
|
||||
</p>
|
||||
<a
|
||||
href={TRACKER_URL}
|
||||
className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Open angel.manohargupta.com
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||
<BarChart2 className="h-6 w-6 text-primary" />
|
||||
Positions
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{lastUpdated ? `Updated ${lastUpdated.toLocaleTimeString("en-IN", { timeZone: "Asia/Kolkata", hour12: false })} IST` : "Live positions from Angel One"}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={refreshing || loading}>
|
||||
<RefreshCw className={cn("h-4 w-4 mr-2", refreshing && "animate-spin")} />
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Card className="border-rose-500/30 bg-rose-500/10">
|
||||
<CardContent className="pt-4 text-sm text-rose-400">{error}</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Summary stat cards */}
|
||||
<div className="grid gap-4 grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
title="Total P&L"
|
||||
value={summary ? fmt(summary.totalPnl) : "—"}
|
||||
icon={summary && summary.totalPnl >= 0 ? TrendingUp : TrendingDown}
|
||||
className={summary && summary.totalPnl < 0 ? "border-rose-500/30" : "border-emerald-500/30"}
|
||||
/>
|
||||
<StatCard
|
||||
title="Unrealised"
|
||||
value={summary ? fmt(summary.totalUnrealised) : "—"}
|
||||
icon={Activity}
|
||||
description="Open positions"
|
||||
/>
|
||||
<StatCard
|
||||
title="Realised"
|
||||
value={summary ? fmt(summary.totalRealised) : "—"}
|
||||
icon={DollarSign}
|
||||
description="Closed today"
|
||||
/>
|
||||
<StatCard
|
||||
title="Open Positions"
|
||||
value={loading ? "…" : open.length}
|
||||
icon={Layers}
|
||||
description={closed.length > 0 ? `${closed.length} closed today` : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Open positions table */}
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">Open Positions ({open.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-muted-foreground text-sm">Loading…</div>
|
||||
) : open.length === 0 ? (
|
||||
<div className="p-8 text-center text-muted-foreground text-sm">No open positions</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-muted-foreground text-xs">
|
||||
<th className="px-4 py-2 text-left font-medium">Symbol</th>
|
||||
<th className="px-4 py-2 text-right font-medium">Qty</th>
|
||||
<th className="px-4 py-2 text-right font-medium">Avg</th>
|
||||
<th className="px-4 py-2 text-right font-medium">LTP</th>
|
||||
<th className="px-4 py-2 text-right font-medium">Unrealised</th>
|
||||
<th className="px-4 py-2 text-right font-medium">Total P&L</th>
|
||||
<th className="px-4 py-2 text-left font-medium">Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{open.map(p => (
|
||||
<tr key={p.key} className="hover:bg-muted/30 transition-colors">
|
||||
<td className="px-4 py-2.5 font-mono font-medium">{p.tradingsymbol}</td>
|
||||
<td className="px-4 py-2.5 text-right tabular-nums">
|
||||
<span className={p.netqty > 0 ? "text-emerald-500" : "text-rose-500"}>
|
||||
{p.netqty > 0 ? "+" : ""}{p.netqty}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right font-mono tabular-nums">₹{p.avg_price.toFixed(2)}</td>
|
||||
<td className="px-4 py-2.5 text-right font-mono tabular-nums">₹{p.ltp.toFixed(2)}</td>
|
||||
<td className="px-4 py-2.5 text-right"><PnlCell value={p.unrealised_pnl} /></td>
|
||||
<td className="px-4 py-2.5 text-right"><PnlCell value={p.total_pnl} /></td>
|
||||
<td className="px-4 py-2.5">
|
||||
<Badge variant="secondary" className="text-xs font-normal">
|
||||
{p.instrumenttype || p.producttype}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Closed today */}
|
||||
{!loading && closed.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base text-muted-foreground">Closed Today ({closed.length})</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-muted-foreground text-xs">
|
||||
<th className="px-4 py-2 text-left font-medium">Symbol</th>
|
||||
<th className="px-4 py-2 text-right font-medium">Realised P&L</th>
|
||||
<th className="px-4 py-2 text-left font-medium">Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{closed.map(p => (
|
||||
<tr key={p.key} className="hover:bg-muted/30 transition-colors">
|
||||
<td className="px-4 py-2.5 font-mono font-medium text-muted-foreground">{p.tradingsymbol}</td>
|
||||
<td className="px-4 py-2.5 text-right"><PnlCell value={p.realised_pnl} /></td>
|
||||
<td className="px-4 py-2.5">
|
||||
<Badge variant="outline" className="text-xs font-normal opacity-60">
|
||||
{p.instrumenttype || p.producttype}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
name: angel-positions
|
||||
description: Read Manohar's live Angel One trading positions and P&L from the standalone position-tracker. Use when asked about positions, profit/loss, exposure, or how the portfolio is doing right now. Read-only — this skill can never place, modify, or close trades.
|
||||
---
|
||||
|
||||
# Angel One Positions (read-only)
|
||||
|
||||
The standalone position-tracker (own repo/deploy) is the single source of
|
||||
truth for live positions. Endpoint:
|
||||
|
||||
```bash
|
||||
curl -s https://angel.manohargupta.com/api/positions
|
||||
```
|
||||
|
||||
Response: array of positions with `tradingsymbol`, `exchange`, `netqty`,
|
||||
`ltp`, `avg_price`, `unrealised_pnl` (₹), `realised_pnl` (₹).
|
||||
|
||||
## Reporting rules
|
||||
- Always report P&L in ₹ with the sign, per position and total.
|
||||
- Note market hours: NSE/BSE trade 09:15–15:30 IST Mon–Fri; outside those
|
||||
hours, LTP is stale — say so.
|
||||
- Alert threshold context: ₹5,000 absolute move is the alerting band the
|
||||
tracker uses; reference it when relevant.
|
||||
- NEVER suggest trades. Report and analyze only. If asked to execute
|
||||
anything, decline — execution stays with Manohar.
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
---
|
||||
name: inbox-manager
|
||||
description: Manage the TASKS.md INBOX — the queue the bridge drains every 30 minutes (9:00–20:00 IST), dispatching each item to the right specialist automatically. Use when Manohar says "add to my tasks/inbox/queue", wants something done later or recurring-ish, or asks what's pending. Also supports triggering an immediate drain.
|
||||
metadata:
|
||||
{
|
||||
"moltbot":
|
||||
{
|
||||
"emoji": "📥",
|
||||
"requires": { "env": ["TIGER_BRIDGE_TOKEN"] },
|
||||
"primaryEnv": "TIGER_BRIDGE_TOKEN",
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# INBOX Manager
|
||||
|
||||
`~/.openclaw/workspace/TASKS.md` has a `## 📥 INBOX` section. Each
|
||||
`- [ ] task` line gets auto-dispatched: the bridge classifies it, spawns the
|
||||
right specialist, rewrites the line to `- [⏳ run-id → agent]`, and the
|
||||
result lands on Telegram.
|
||||
|
||||
## Add a task
|
||||
Append ONE line under the `## 📥 INBOX` header (before the next `## `):
|
||||
```
|
||||
- [ ] research upcoming SECI BESS tenders and summarize timelines
|
||||
```
|
||||
Write tasks self-contained — the specialist gets no chat context.
|
||||
|
||||
## List pending
|
||||
Read TASKS.md and report the unchecked `- [ ]` lines under INBOX, in order
|
||||
(top item dispatches first).
|
||||
|
||||
## Drain now (don't wait for the 30-min cycle)
|
||||
```bash
|
||||
curl -s -X POST http://172.17.0.1:3456/tiger/inbox/drain \
|
||||
-H "Authorization: Bearer $TIGER_BRIDGE_TOKEN"
|
||||
```
|
||||
Dispatches exactly ONE item (the top one). Outside 9:00–20:00 IST the
|
||||
manual drain still works (force=true server-side).
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
---
|
||||
name: spawn-delegate
|
||||
description: Delegate work to Tiger's specialist sub-agents (Cody=code, Ethan=research, Cathy=writing, Elon=planning) and check on their runs. Use whenever a task is substantial enough to hand off — the specialist runs in its own session and reports to Telegram when done, so you stay free for the conversation. Also use to list recent runs or fetch a run's full result.
|
||||
metadata:
|
||||
{
|
||||
"moltbot":
|
||||
{
|
||||
"emoji": "🤝",
|
||||
"requires": { "env": ["TIGER_BRIDGE_TOKEN"] },
|
||||
"primaryEnv": "TIGER_BRIDGE_TOKEN",
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# Spawn / Delegate to Specialists
|
||||
|
||||
The bridge (host) exposes real sub-agent execution. You hold the bearer
|
||||
token in `$TIGER_BRIDGE_TOKEN`. Bridge base URL from inside this container:
|
||||
`http://172.17.0.1:3456`.
|
||||
|
||||
## Who does what
|
||||
- **cody** — code, debugging, devops, scripts, infra
|
||||
- **ethan** — research, market/policy analysis, due diligence
|
||||
- **cathy** — writing, summaries, reports, drafts
|
||||
- **elon** — planning, prioritization, breaking down projects
|
||||
|
||||
## Delegate a task
|
||||
```bash
|
||||
curl -s -X POST http://172.17.0.1:3456/tiger/spawn \
|
||||
-H "Authorization: Bearer $TIGER_BRIDGE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"agentId":"ethan","task":"<one clear, self-contained task>","context":"<optional background>"}'
|
||||
```
|
||||
Returns `{runId, sessionId, queued}`. Runs execute one at a time
|
||||
(RAM-constrained host) — `queued > 0` means it waits its turn.
|
||||
Completion is announced on Telegram automatically; you do NOT need to poll.
|
||||
|
||||
## Check runs
|
||||
```bash
|
||||
# recent runs + queue state
|
||||
curl -s http://172.17.0.1:3456/tiger/spawn/runs -H "Authorization: Bearer $TIGER_BRIDGE_TOKEN"
|
||||
# one run, full reply
|
||||
curl -s http://172.17.0.1:3456/tiger/spawn/runs/<runId> -H "Authorization: Bearer $TIGER_BRIDGE_TOKEN"
|
||||
```
|
||||
|
||||
## Rules
|
||||
- Write the task so it stands alone — the specialist has NO chat context.
|
||||
- One task per spawn. Split compound requests.
|
||||
- Don't spawn for things you can answer in one turn yourself.
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
name: sys-health
|
||||
description: Check the health of the Tiger Command Center itself — RAM/swap pressure on the VPS, LLM gateway reachability, bridge status, recent audit events, cron run outcomes. Use when asked "is everything running", "why is it slow", after failures, or proactively when your own turns feel sluggish.
|
||||
metadata:
|
||||
{
|
||||
"moltbot":
|
||||
{
|
||||
"emoji": "🩺",
|
||||
"requires": { "env": ["TIGER_BRIDGE_TOKEN"] },
|
||||
"primaryEnv": "TIGER_BRIDGE_TOKEN",
|
||||
},
|
||||
}
|
||||
---
|
||||
|
||||
# System Health
|
||||
|
||||
## Memory pressure (the #1 failure mode on this host)
|
||||
/proc inside the container shows HOST memory:
|
||||
```bash
|
||||
grep -E 'MemTotal|MemAvailable|SwapTotal|SwapFree' /proc/meminfo
|
||||
```
|
||||
Interpretation: MemAvailable under ~800MB or swap more than half used means
|
||||
agent turns will crawl and cron jobs may hit their 300s timeouts. Say so
|
||||
explicitly and recommend deferring heavy work.
|
||||
|
||||
## LLM gateway (all models route through it)
|
||||
```bash
|
||||
curl -s -o /dev/null -w '%{http_code}' https://llm.manohargupta.com/health/liveliness
|
||||
```
|
||||
Anything but 200 = every agent in the system is degraded.
|
||||
|
||||
## Bridge + recent activity
|
||||
```bash
|
||||
curl -s http://172.17.0.1:3456/tiger/status -H "Authorization: Bearer $TIGER_BRIDGE_TOKEN"
|
||||
# last 20 audited events (spawns, cron, tasks, outputs):
|
||||
curl -s 'http://172.17.0.1:3456/tiger/activity/audit?limit=20' -H "Authorization: Bearer $TIGER_BRIDGE_TOKEN"
|
||||
```
|
||||
|
||||
## Report format
|
||||
Lead with a one-line verdict (healthy / degraded / critical), then only the
|
||||
metrics that are off. Quantify with numbers and units.
|
||||
Loading…
Add table
Reference in a new issue