From 0569bbba7f29099e31eef8f7d982c0237235ecdd Mon Sep 17 00:00:00 2001 From: Pavel <39xanax@gmail.com> Date: Mon, 17 Aug 2026 03:32:48 +0500 Subject: [PATCH] chore: publish AgentHQ app --- .github/workflows/ci.yml | 58 + .gitignore | 25 + README.md | 88 + dash/backend/.env.example | 70 + dash/backend/agentos/consensus.go | 200 + dash/backend/agentos/governance.go | 468 ++ dash/backend/agentos/memory.go | 408 ++ dash/backend/agentos/ops.go | 142 + dash/backend/agentos/runtime.go | 628 +++ dash/backend/agentos/scheduler.go | 524 ++ dash/backend/agentos/service.go | 1164 +++++ dash/backend/agentos/types.go | 217 + dash/backend/build.sh | 80 + dash/backend/db/billing.go | 36 + dash/backend/db/database.go | 569 ++ dash/backend/db/users.go | 311 ++ dash/backend/go.mod | 40 + dash/backend/go.sum | 121 + dash/backend/handlers/admin.go | 99 + dash/backend/handlers/agentos.go | 1520 ++++++ dash/backend/handlers/ai.go | 2101 ++++++++ dash/backend/handlers/auth.go | 86 + dash/backend/handlers/billing.go | 195 + dash/backend/handlers/confirm.go | 68 + dash/backend/handlers/email.go | 225 + dash/backend/handlers/mcp.go | 15 + dash/backend/handlers/memory.go | 305 ++ dash/backend/handlers/ping.go | 142 + dash/backend/handlers/seed.go | 95 + dash/backend/handlers/settings.go | 215 + dash/backend/handlers/subagents.go | 208 + dash/backend/handlers/sys.go | 108 + dash/backend/handlers/terminal.go | 102 + dash/backend/handlers/user_auth.go | 299 ++ dash/backend/handlers/workspace.go | 314 ++ dash/backend/main.go | 162 + dash/backend/skills/deployment.md | 124 + dash/backend/skills/git_workflow.md | 93 + dash/backend/skills/go_debugging.md | 86 + dash/backend/skills/llm_integration.md | 128 + dash/backend/skills/react_patterns.md | 85 + dash/backend/skills/sql_tuning.md | 112 + dash/backend/tools/contextplus_native.go | 2841 ++++++++++ dash/backend/tools/exec.go | 54 + dash/backend/tools/fs.go | 525 ++ dash/backend/tools/mcp.go | 447 ++ dash/backend/tools/registry.go | 511 ++ dash/backend/tools/skills.go | 169 + dash/backend/tools/subagents.go | 56 + dash/backend/tools/web.go | 110 + dash/deploy.sh | 28 + dash/frontend/.gitignore | 24 + dash/frontend/README.md | 16 + dash/frontend/eslint.config.js | 32 + dash/frontend/index.html | 16 + dash/frontend/package-lock.json | 4574 +++++++++++++++++ dash/frontend/package.json | 39 + .../public/android-chrome-192x192.png | Bin 0 -> 47091 bytes .../public/android-chrome-512x512.png | Bin 0 -> 284873 bytes dash/frontend/public/apple-touch-icon.png | Bin 0 -> 42326 bytes dash/frontend/public/favicon-16x16.png | Bin 0 -> 746 bytes dash/frontend/public/favicon-32x32.png | Bin 0 -> 2121 bytes dash/frontend/public/favicon.ico | Bin 0 -> 15406 bytes dash/frontend/public/site.webmanifest | 1 + dash/frontend/public/vite.svg | 1 + dash/frontend/src/App.jsx | 41 + dash/frontend/src/components/AgentOSShell.jsx | 1549 ++++++ dash/frontend/src/components/Settings.jsx | 490 ++ dash/frontend/src/index.css | 1976 +++++++ dash/frontend/src/lib/auth-context.jsx | 3 + dash/frontend/src/lib/auth.jsx | 46 + dash/frontend/src/lib/useAuth.jsx | 6 + dash/frontend/src/main.jsx | 10 + dash/frontend/src/pages/Account.jsx | 415 ++ dash/frontend/src/pages/Admin.jsx | 213 + dash/frontend/src/pages/Auth.jsx | 183 + dash/frontend/src/pages/Dashboard.jsx | 133 + dash/frontend/src/pages/Landing.jsx | 156 + dash/frontend/vite.config.js | 20 + 79 files changed, 26721 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 dash/backend/.env.example create mode 100644 dash/backend/agentos/consensus.go create mode 100644 dash/backend/agentos/governance.go create mode 100644 dash/backend/agentos/memory.go create mode 100644 dash/backend/agentos/ops.go create mode 100644 dash/backend/agentos/runtime.go create mode 100644 dash/backend/agentos/scheduler.go create mode 100644 dash/backend/agentos/service.go create mode 100644 dash/backend/agentos/types.go create mode 100755 dash/backend/build.sh create mode 100644 dash/backend/db/billing.go create mode 100644 dash/backend/db/database.go create mode 100644 dash/backend/db/users.go create mode 100644 dash/backend/go.mod create mode 100644 dash/backend/go.sum create mode 100644 dash/backend/handlers/admin.go create mode 100644 dash/backend/handlers/agentos.go create mode 100644 dash/backend/handlers/ai.go create mode 100644 dash/backend/handlers/auth.go create mode 100644 dash/backend/handlers/billing.go create mode 100644 dash/backend/handlers/confirm.go create mode 100644 dash/backend/handlers/email.go create mode 100644 dash/backend/handlers/mcp.go create mode 100644 dash/backend/handlers/memory.go create mode 100644 dash/backend/handlers/ping.go create mode 100644 dash/backend/handlers/seed.go create mode 100644 dash/backend/handlers/settings.go create mode 100644 dash/backend/handlers/subagents.go create mode 100644 dash/backend/handlers/sys.go create mode 100644 dash/backend/handlers/terminal.go create mode 100644 dash/backend/handlers/user_auth.go create mode 100644 dash/backend/handlers/workspace.go create mode 100644 dash/backend/main.go create mode 100644 dash/backend/skills/deployment.md create mode 100644 dash/backend/skills/git_workflow.md create mode 100644 dash/backend/skills/go_debugging.md create mode 100644 dash/backend/skills/llm_integration.md create mode 100644 dash/backend/skills/react_patterns.md create mode 100644 dash/backend/skills/sql_tuning.md create mode 100644 dash/backend/tools/contextplus_native.go create mode 100644 dash/backend/tools/exec.go create mode 100644 dash/backend/tools/fs.go create mode 100644 dash/backend/tools/mcp.go create mode 100644 dash/backend/tools/registry.go create mode 100644 dash/backend/tools/skills.go create mode 100644 dash/backend/tools/subagents.go create mode 100644 dash/backend/tools/web.go create mode 100755 dash/deploy.sh create mode 100644 dash/frontend/.gitignore create mode 100644 dash/frontend/README.md create mode 100644 dash/frontend/eslint.config.js create mode 100644 dash/frontend/index.html create mode 100644 dash/frontend/package-lock.json create mode 100644 dash/frontend/package.json create mode 100644 dash/frontend/public/android-chrome-192x192.png create mode 100644 dash/frontend/public/android-chrome-512x512.png create mode 100644 dash/frontend/public/apple-touch-icon.png create mode 100644 dash/frontend/public/favicon-16x16.png create mode 100644 dash/frontend/public/favicon-32x32.png create mode 100644 dash/frontend/public/favicon.ico create mode 100644 dash/frontend/public/site.webmanifest create mode 100644 dash/frontend/public/vite.svg create mode 100644 dash/frontend/src/App.jsx create mode 100644 dash/frontend/src/components/AgentOSShell.jsx create mode 100644 dash/frontend/src/components/Settings.jsx create mode 100644 dash/frontend/src/index.css create mode 100644 dash/frontend/src/lib/auth-context.jsx create mode 100644 dash/frontend/src/lib/auth.jsx create mode 100644 dash/frontend/src/lib/useAuth.jsx create mode 100644 dash/frontend/src/main.jsx create mode 100644 dash/frontend/src/pages/Account.jsx create mode 100644 dash/frontend/src/pages/Admin.jsx create mode 100644 dash/frontend/src/pages/Auth.jsx create mode 100644 dash/frontend/src/pages/Dashboard.jsx create mode 100644 dash/frontend/src/pages/Landing.jsx create mode 100644 dash/frontend/vite.config.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..37e94eb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,58 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +permissions: + contents: read + +jobs: + backend: + name: Backend + runs-on: ubuntu-latest + defaults: + run: + working-directory: dash/backend + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: dash/backend/go.mod + cache: true + + - name: Run Go tests + run: go test ./... + + - name: Check Go formatting + run: test -z "$(gofmt -l .)" + + frontend: + name: Frontend + runs-on: ubuntu-latest + defaults: + run: + working-directory: dash/frontend + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: dash/frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f25650f --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Local configuration and secrets +.env +.env.* +!.env.example +*.key +*.pem +*.p12 + +# Runtime state and generated data +*.db +*.db-* +*.sqlite +*.sqlite3 +dash/backend/data/workspaces/ + +# Build output and dependencies +node_modules/ +dist/ +coverage/ +dash/backend/apollo-dash* + +# Local tooling and OS files +.claude/ +.playwright-mcp/ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d91834 --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# AgentHQ + +AgentHQ is a self-hosted dashboard for building and operating AI companies. It combines a Go backend with a React frontend for agent organization, tasks, memory, MCP tools, schedules, governance, and audit history. + +## Project layout + +```text +dash/ +├── backend/ Go API, SQLite persistence, workers, and static file server +└── frontend/ React + Vite dashboard +``` + +## Requirements + +- Go 1.25.6 or newer +- Node.js 20 or newer +- npm + +## Local development + +1. Create local backend configuration: + + ```bash + cp dash/backend/.env.example dash/backend/.env + ``` + + Set `DASHBOARD_PASSWORD` and add an `OPENROUTER_API_KEY` if you want to use hosted models. Keep `.env` local; it is ignored by Git. + +2. Install frontend dependencies and build the dashboard: + + ```bash + cd dash/frontend + npm ci + npm run build + ``` + +3. Start the backend: + + ```bash + cd dash/backend + go run . + ``` + + The dashboard is available at `http://localhost:4000`. + +For frontend hot reload, run `npm run dev` in `dash/frontend` and keep the backend running on port 4000. The Vite proxy forwards `/api` and `/ws` requests to the backend. + +## Verification + +Run the same checks used by CI: + +```bash +cd dash/backend +go test ./... + +cd ../frontend +npm run lint +npm run build +``` + +## Production build + +`dash/backend/build.sh` cross-compiles Linux AMD64 and ARM64 backend binaries with Zig, then builds the frontend. Install Zig first: + +```bash +brew install zig +cd dash/backend +./build.sh +``` + +The generated binaries, frontend bundle, database, runtime workspace, and credentials are intentionally excluded from version control. + +## Configuration + +The main backend settings are documented in `dash/backend/.env.example`: + +- `PORT` — HTTP port, default `4000` +- `DASHBOARD_PASSWORD` — admin password for protected routes +- `OPENROUTER_API_KEY` — hosted model and embedding access +- `OPENAI_API_KEY` — optional OpenAI-compatible fallback +- `RESEND_API_KEY` and `APP_URL` — optional email notifications +- `VULTA_API_KEY` and `VULTA_WEBHOOK_SECRET` — optional billing integration +- `AGENTHQ_WORKSPACE_ROOT` — optional override for workspace storage +- `OLLAMA_API_URL` — optional local Ollama-compatible endpoint +- `APOLLO_DEBUG_SYSTEM_LOG` — optional verbose model prompt logging +- `CONTEXTPLUS_EMBED_TRACKER` — optional embedding tracking toggle + +Never commit real credentials, private keys, SQLite databases, or built binaries. diff --git a/dash/backend/.env.example b/dash/backend/.env.example new file mode 100644 index 0000000..394b371 --- /dev/null +++ b/dash/backend/.env.example @@ -0,0 +1,70 @@ +# AgentHQ backend environment template +# +# Copy this file to .env for local development: +# cp .env.example .env +# +# Never commit .env or paste real credentials into this file. Use your +# deployment platform's secret manager in production. + +# ----------------------------------------------------------------------------- +# Server and authentication +# ----------------------------------------------------------------------------- + +# HTTP listen port. Defaults to 4000. +PORT=4000 + +# Required for admin access and API Basic Auth fallback. +# Generate a strong value with: openssl rand -base64 32 +DASHBOARD_PASSWORD=replace-with-a-strong-random-password + +# Public application URL used in email links. Leave commented for the built-in +# default, or set your local/deployed URL explicitly. +# APP_URL=http://localhost:4000 + +# ----------------------------------------------------------------------------- +# AI providers +# ----------------------------------------------------------------------------- + +# Recommended hosted provider. Required for OpenRouter-backed chat, agents, and +# embeddings unless a user-level key or local Ollama provider is configured. +OPENROUTER_API_KEY=replace-with-your-openrouter-api-key + +# Optional OpenAI-compatible fallback for embeddings and AI features. +# OPENAI_API_KEY=replace-with-your-openai-api-key + +# Optional local Ollama-compatible endpoint. The application defaults to +# http://localhost:11434 when this is not set. +# OLLAMA_API_URL=http://localhost:11434 + +# ----------------------------------------------------------------------------- +# Workspace and local storage +# ----------------------------------------------------------------------------- + +# Optional persistent workspace location. Defaults to ./data/workspaces. +# Use an absolute path for production deployments. +# AGENTHQ_WORKSPACE_ROOT=/var/lib/agenthq/workspaces + +# Legacy alias accepted by workspace tools. Prefer AGENTHQ_WORKSPACE_ROOT. +# APOLLO_WORKSPACE_ROOT=/var/lib/agenthq/workspaces + +# ----------------------------------------------------------------------------- +# Optional integrations +# ----------------------------------------------------------------------------- + +# Resend email delivery. Leave unset to disable email notifications. +# RESEND_API_KEY=replace-with-your-resend-api-key + +# Vulta billing API and webhook signing secret. Leave unset to disable billing. +# VULTA_API_KEY=replace-with-your-vulta-api-key +# VULTA_WEBHOOK_SECRET=replace-with-your-vulta-webhook-secret + +# ----------------------------------------------------------------------------- +# Diagnostics and indexing +# ----------------------------------------------------------------------------- + +# Include verbose model prompts in the system log panel. Keep disabled in +# production because prompts can contain sensitive workspace data. +APOLLO_DEBUG_SYSTEM_LOG=false + +# ContextPlus embedding tracking is enabled by default. Set false to disable it. +# CONTEXTPLUS_EMBED_TRACKER=false diff --git a/dash/backend/agentos/consensus.go b/dash/backend/agentos/consensus.go new file mode 100644 index 0000000..ff52a3b --- /dev/null +++ b/dash/backend/agentos/consensus.go @@ -0,0 +1,200 @@ +package agentos + +import ( + "fmt" + "strings" + "time" + + "github.com/google/uuid" +) + +func (s *Service) CreateConsensusRound(companyID, departmentID, topic, createdBy string) (ConsensusRound, error) { + if strings.TrimSpace(companyID) == "" || strings.TrimSpace(topic) == "" { + return ConsensusRound{}, fmt.Errorf("company_id and topic are required") + } + if strings.TrimSpace(createdBy) == "" { + createdBy = "owner" + } + id := uuid.NewString() + _, err := s.db.Exec(` + INSERT INTO agent_consensus_rounds (id, company_id, department_id, topic, status, created_by, created_at, closed_at) + VALUES (?, ?, ?, ?, 'open', ?, CURRENT_TIMESTAMP, '') + `, id, companyID, departmentID, topic, createdBy) + if err != nil { + return ConsensusRound{}, err + } + s.appendAudit("consensus_round_created", "consensus_round", id, createdBy, map[string]interface{}{"topic": topic}) + s.emitEvent(companyID, departmentID, "", "", "", "consensus_round_created", "info", map[string]interface{}{"round_id": id, "topic": topic}) + return s.GetConsensusRound(id) +} + +func (s *Service) GetConsensusRound(id string) (ConsensusRound, error) { + var r ConsensusRound + var createdAt string + err := s.db.QueryRow(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(topic,''), IFNULL(status,'open'), IFNULL(created_by,''), IFNULL(created_at,''), IFNULL(closed_at,'') + FROM agent_consensus_rounds WHERE id = ? + `, id).Scan(&r.ID, &r.CompanyID, &r.DepartmentID, &r.Topic, &r.Status, &r.CreatedBy, &createdAt, &r.ClosedAt) + if err != nil { + return ConsensusRound{}, err + } + r.CreatedAt = parseDBTime(createdAt) + return r, nil +} + +func (s *Service) ListConsensusRounds(companyID, departmentID string, limit int) ([]ConsensusRound, error) { + if limit <= 0 || limit > 200 { + limit = 50 + } + query := ` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(topic,''), IFNULL(status,'open'), IFNULL(created_by,''), IFNULL(created_at,''), IFNULL(closed_at,'') + FROM agent_consensus_rounds WHERE 1=1` + args := []interface{}{} + if strings.TrimSpace(companyID) != "" { + query += " AND company_id = ?" + args = append(args, companyID) + } + if strings.TrimSpace(departmentID) != "" { + query += " AND department_id = ?" + args = append(args, departmentID) + } + query += " ORDER BY created_at DESC LIMIT ?" + args = append(args, limit) + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []ConsensusRound{} + for rows.Next() { + var r ConsensusRound + var createdAt string + if err := rows.Scan(&r.ID, &r.CompanyID, &r.DepartmentID, &r.Topic, &r.Status, &r.CreatedBy, &createdAt, &r.ClosedAt); err != nil { + continue + } + r.CreatedAt = parseDBTime(createdAt) + out = append(out, r) + } + return out, nil +} + +func (s *Service) VoteConsensus(roundID, agentID, option string, confidence float64, rationale string) (ConsensusVote, error) { + if strings.TrimSpace(roundID) == "" || strings.TrimSpace(agentID) == "" || strings.TrimSpace(option) == "" { + return ConsensusVote{}, fmt.Errorf("round_id, agent_id and option are required") + } + round, err := s.GetConsensusRound(roundID) + if err != nil { + return ConsensusVote{}, err + } + if strings.ToLower(strings.TrimSpace(round.Status)) != "open" { + return ConsensusVote{}, fmt.Errorf("consensus round is closed") + } + if confidence <= 0 { + confidence = 0.5 + } + if confidence > 1 { + confidence = 1 + } + + id := uuid.NewString() + _, err = s.db.Exec(` + INSERT INTO agent_consensus_votes (id, round_id, agent_id, option, confidence, rationale, created_at) + VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(round_id, agent_id) DO UPDATE SET + option=excluded.option, + confidence=excluded.confidence, + rationale=excluded.rationale, + created_at=CURRENT_TIMESTAMP + `, id, roundID, agentID, option, confidence, rationale) + if err != nil { + return ConsensusVote{}, err + } + s.appendAudit("consensus_vote_cast", "consensus_round", roundID, agentID, map[string]interface{}{"option": option, "confidence": confidence}) + s.emitEvent(round.CompanyID, round.DepartmentID, agentID, "", "", "consensus_vote_cast", "info", map[string]interface{}{"round_id": roundID, "option": option}) + + votes, _ := s.ListConsensusVotes(roundID) + for _, v := range votes { + if v.AgentID == agentID { + return v, nil + } + } + return ConsensusVote{}, fmt.Errorf("vote not found after write") +} + +func (s *Service) ListConsensusVotes(roundID string) ([]ConsensusVote, error) { + rows, err := s.db.Query(` + SELECT id, IFNULL(round_id,''), IFNULL(agent_id,''), IFNULL(option,''), IFNULL(confidence,0.5), IFNULL(rationale,''), IFNULL(created_at,'') + FROM agent_consensus_votes + WHERE round_id = ? + ORDER BY created_at ASC + `, roundID) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []ConsensusVote{} + for rows.Next() { + var v ConsensusVote + var createdAt string + if err := rows.Scan(&v.ID, &v.RoundID, &v.AgentID, &v.Option, &v.Confidence, &v.Rationale, &createdAt); err != nil { + continue + } + v.CreatedAt = parseDBTime(createdAt) + out = append(out, v) + } + return out, nil +} + +func (s *Service) CloseConsensusRound(roundID string) (ConsensusRound, error) { + round, err := s.GetConsensusRound(roundID) + if err != nil { + return ConsensusRound{}, err + } + if strings.ToLower(strings.TrimSpace(round.Status)) == "closed" { + return round, nil + } + _, err = s.db.Exec("UPDATE agent_consensus_rounds SET status='closed', closed_at=? WHERE id=?", time.Now().UTC().Format(time.RFC3339), roundID) + if err != nil { + return ConsensusRound{}, err + } + s.appendAudit("consensus_round_closed", "consensus_round", roundID, "owner", nil) + s.emitEvent(round.CompanyID, round.DepartmentID, "", "", "", "consensus_round_closed", "info", map[string]interface{}{"round_id": roundID}) + return s.GetConsensusRound(roundID) +} + +func (s *Service) ConsensusDecision(roundID string) (map[string]interface{}, error) { + round, err := s.GetConsensusRound(roundID) + if err != nil { + return nil, err + } + votes, err := s.ListConsensusVotes(roundID) + if err != nil { + return nil, err + } + byOption := map[string]int{} + for _, v := range votes { + opt := strings.TrimSpace(v.Option) + if opt == "" { + continue + } + byOption[opt]++ + } + winner := "" + winnerCount := 0 + for option, count := range byOption { + if count > winnerCount { + winner = option + winnerCount = count + } + } + return map[string]interface{}{ + "round": round, + "votes": votes, + "counts": byOption, + "winner_option": winner, + "winner_votes": winnerCount, + }, nil +} diff --git a/dash/backend/agentos/governance.go b/dash/backend/agentos/governance.go new file mode 100644 index 0000000..a6947ae --- /dev/null +++ b/dash/backend/agentos/governance.go @@ -0,0 +1,468 @@ +package agentos + +import ( + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/google/uuid" +) + +type PolicyDecision struct { + Allowed bool `json:"allowed"` + MatchedRule string `json:"matched_rule"` + ApprovalTier string `json:"approval_tier"` + Reason string `json:"reason"` +} + +func normalizeTier(t string) string { + t = strings.ToLower(strings.TrimSpace(t)) + switch t { + case "none", "tier1", "tier2", "tier3": + return t + default: + return "none" + } +} + +func maxTier(a, b string) string { + order := map[string]int{"none": 0, "tier1": 1, "tier2": 2, "tier3": 3} + na := normalizeTier(a) + nb := normalizeTier(b) + if order[na] >= order[nb] { + return na + } + return nb +} + +func wildcardMatch(pattern, value string) bool { + pattern = strings.TrimSpace(pattern) + value = strings.TrimSpace(value) + if pattern == "" || pattern == "*" { + return true + } + if ok, err := filepath.Match(pattern, value); err == nil && ok { + return true + } + if strings.HasSuffix(pattern, "/") { + return strings.HasPrefix(value, pattern) + } + needle := strings.Trim(pattern, "*") + if needle == "" { + return true + } + return strings.Contains(value, needle) +} + +func (s *Service) UpsertPolicy(rule PolicyRule) (PolicyRule, error) { + if strings.TrimSpace(rule.ID) == "" { + rule.ID = uuid.NewString() + } + if strings.TrimSpace(rule.Action) == "" { + return PolicyRule{}, fmt.Errorf("action is required") + } + rule.Effect = strings.ToLower(strings.TrimSpace(rule.Effect)) + if rule.Effect == "" { + rule.Effect = "allow" + } + if rule.Effect != "allow" && rule.Effect != "deny" { + return PolicyRule{}, fmt.Errorf("effect must be allow or deny") + } + if strings.TrimSpace(rule.ScopePattern) == "" { + rule.ScopePattern = "*" + } + rule.ApprovalTier = normalizeTier(rule.ApprovalTier) + + _, err := s.db.Exec(` + INSERT INTO capability_policies (id, company_id, department_id, agent_id, action, effect, scope_pattern, approval_tier, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT(id) DO UPDATE SET + company_id=excluded.company_id, + department_id=excluded.department_id, + agent_id=excluded.agent_id, + action=excluded.action, + effect=excluded.effect, + scope_pattern=excluded.scope_pattern, + approval_tier=excluded.approval_tier, + updated_at=CURRENT_TIMESTAMP + `, rule.ID, rule.CompanyID, rule.DepartmentID, rule.AgentID, rule.Action, rule.Effect, rule.ScopePattern, rule.ApprovalTier) + if err != nil { + return PolicyRule{}, err + } + s.appendAudit("policy_upsert", "policy", rule.ID, "owner", rule) + return s.GetPolicy(rule.ID) +} + +func (s *Service) GetPolicy(id string) (PolicyRule, error) { + var p PolicyRule + var createdAt, updatedAt string + err := s.db.QueryRow(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(action,''), IFNULL(effect,''), IFNULL(scope_pattern,'*'), IFNULL(approval_tier,'none'), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM capability_policies WHERE id = ? + `, id).Scan(&p.ID, &p.CompanyID, &p.DepartmentID, &p.AgentID, &p.Action, &p.Effect, &p.ScopePattern, &p.ApprovalTier, &createdAt, &updatedAt) + if err != nil { + return PolicyRule{}, err + } + p.CreatedAt = parseDBTime(createdAt) + p.UpdatedAt = parseDBTime(updatedAt) + return p, nil +} + +func (s *Service) ListPolicies(companyID, departmentID, agentID string) ([]PolicyRule, error) { + query := ` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(action,''), IFNULL(effect,''), IFNULL(scope_pattern,'*'), IFNULL(approval_tier,'none'), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM capability_policies WHERE 1=1` + args := []interface{}{} + if strings.TrimSpace(companyID) != "" { + query += " AND company_id = ?" + args = append(args, companyID) + } + if strings.TrimSpace(departmentID) != "" { + query += " AND department_id = ?" + args = append(args, departmentID) + } + if strings.TrimSpace(agentID) != "" { + query += " AND agent_id = ?" + args = append(args, agentID) + } + query += " ORDER BY updated_at DESC" + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []PolicyRule{} + for rows.Next() { + var p PolicyRule + var createdAt, updatedAt string + if err := rows.Scan(&p.ID, &p.CompanyID, &p.DepartmentID, &p.AgentID, &p.Action, &p.Effect, &p.ScopePattern, &p.ApprovalTier, &createdAt, &updatedAt); err != nil { + continue + } + p.CreatedAt = parseDBTime(createdAt) + p.UpdatedAt = parseDBTime(updatedAt) + out = append(out, p) + } + return out, nil +} + +func (s *Service) EvaluatePolicy(companyID, departmentID, agentID, action, scope string) PolicyDecision { + action = strings.ToLower(strings.TrimSpace(action)) + rules, err := s.ListPolicies(companyID, "", "") + if err != nil { + return PolicyDecision{Allowed: false, ApprovalTier: "tier3", Reason: "policy query failed"} + } + + matched := []PolicyRule{} + for _, r := range rules { + if strings.TrimSpace(r.CompanyID) != "" && r.CompanyID != companyID { + continue + } + if strings.TrimSpace(r.DepartmentID) != "" && r.DepartmentID != departmentID { + continue + } + if strings.TrimSpace(r.AgentID) != "" && r.AgentID != agentID { + continue + } + ra := strings.ToLower(strings.TrimSpace(r.Action)) + if ra != "*" && ra != action { + continue + } + if !wildcardMatch(r.ScopePattern, scope) { + continue + } + matched = append(matched, r) + } + + for _, r := range matched { + if strings.EqualFold(r.Effect, "deny") { + return PolicyDecision{Allowed: false, MatchedRule: r.ID, ApprovalTier: maxTier(r.ApprovalTier, "tier3"), Reason: "explicit deny"} + } + } + for _, r := range matched { + if strings.EqualFold(r.Effect, "allow") { + return PolicyDecision{Allowed: true, MatchedRule: r.ID, ApprovalTier: normalizeTier(r.ApprovalTier), Reason: "explicit allow"} + } + } + + enforcement := strings.ToLower(strings.TrimSpace(s.getSettingString("agentos_policy_enforcement", "deny_default"))) + if enforcement == "allow_default" { + return PolicyDecision{Allowed: true, ApprovalTier: "none", Reason: "allow-default"} + } + return PolicyDecision{Allowed: false, ApprovalTier: "tier2", Reason: "deny-by-default"} +} + +func (s *Service) TestPolicy(companyID, departmentID, agentID, action, scope string) map[string]interface{} { + dec := s.EvaluatePolicy(companyID, departmentID, agentID, action, scope) + return map[string]interface{}{ + "input": map[string]string{ + "company_id": companyID, + "department_id": departmentID, + "agent_id": agentID, + "action": action, + "scope": scope, + }, + "decision": dec, + } +} + +func (s *Service) RequestApproval(companyID, taskID, action, tier, reason string, payload interface{}) error { + if strings.TrimSpace(tier) == "" { + tier = "tier2" + } + payloadJSON := "{}" + if payload != nil { + if b, err := json.Marshal(payload); err == nil { + payloadJSON = string(b) + } + } + id := uuid.NewString() + _, err := s.db.Exec(` + INSERT INTO approval_requests (id, company_id, task_id, action, tier, reason, payload_json, status, requested_at, resolved_at, resolved_by) + VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', CURRENT_TIMESTAMP, NULL, '') + `, id, companyID, taskID, action, normalizeTier(tier), reason, payloadJSON) + if err != nil { + return err + } + s.appendAudit("approval_requested", "approval", id, "system", map[string]interface{}{"task_id": taskID, "action": action, "tier": tier}) + var departmentID, agentID, threadID string + _ = s.db.QueryRow("SELECT IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(thread_id,'') FROM agent_tasks WHERE id = ?", taskID).Scan(&departmentID, &agentID, &threadID) + s.emitEvent(companyID, departmentID, agentID, threadID, taskID, "approval_requested", "warn", map[string]interface{}{"approval_id": id, "tier": tier, "reason": reason}) + return nil +} + +func (s *Service) ListApprovals(companyID, status string) ([]ApprovalRequest, error) { + query := `SELECT id, IFNULL(company_id,''), IFNULL(task_id,''), IFNULL(action,''), IFNULL(tier,''), IFNULL(reason,''), IFNULL(payload_json,'{}'), IFNULL(status,''), IFNULL(requested_at,''), IFNULL(resolved_at,''), IFNULL(resolved_by,'') FROM approval_requests WHERE 1=1` + args := []interface{}{} + if strings.TrimSpace(companyID) != "" { + query += " AND company_id = ?" + args = append(args, companyID) + } + if strings.TrimSpace(status) != "" { + query += " AND status = ?" + args = append(args, status) + } + query += " ORDER BY requested_at DESC" + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []ApprovalRequest{} + for rows.Next() { + var a ApprovalRequest + if err := rows.Scan(&a.ID, &a.CompanyID, &a.TaskID, &a.Action, &a.Tier, &a.Reason, &a.PayloadJSON, &a.Status, &a.RequestedAt, &a.ResolvedAt, &a.ResolvedBy); err != nil { + continue + } + out = append(out, a) + } + return out, nil +} + +func (s *Service) GetApproval(approvalID string) (ApprovalRequest, error) { + var a ApprovalRequest + err := s.db.QueryRow(`SELECT id, IFNULL(company_id,''), IFNULL(task_id,''), IFNULL(action,''), IFNULL(tier,''), IFNULL(reason,''), IFNULL(payload_json,'{}'), IFNULL(status,''), IFNULL(requested_at,''), IFNULL(resolved_at,''), IFNULL(resolved_by,'') FROM approval_requests WHERE id = ?`, approvalID). + Scan(&a.ID, &a.CompanyID, &a.TaskID, &a.Action, &a.Tier, &a.Reason, &a.PayloadJSON, &a.Status, &a.RequestedAt, &a.ResolvedAt, &a.ResolvedBy) + if err != nil { + return ApprovalRequest{}, err + } + return a, nil +} + +func (s *Service) ResolveApproval(approvalID string, approve bool, actor string) error { + if strings.TrimSpace(approvalID) == "" { + return fmt.Errorf("approval_id is required") + } + status := "rejected" + if approve { + status = "approved" + } + if strings.TrimSpace(actor) == "" { + actor = "owner" + } + + var companyID, taskID, action string + err := s.db.QueryRow(`SELECT IFNULL(company_id,''), IFNULL(task_id,''), IFNULL(action,'') FROM approval_requests WHERE id = ?`, approvalID).Scan(&companyID, &taskID, &action) + if err != nil { + if err == sql.ErrNoRows { + return fmt.Errorf("approval not found") + } + return err + } + + _, err = s.db.Exec("UPDATE approval_requests SET status = ?, resolved_at = CURRENT_TIMESTAMP, resolved_by = ? WHERE id = ?", status, actor, approvalID) + if err != nil { + return err + } + + if status == "approved" && strings.TrimSpace(taskID) != "" { + _, _ = s.db.Exec("UPDATE agent_tasks SET status='queued', blocked_reason='', updated_at=CURRENT_TIMESTAMP WHERE id = ? AND status='blocked'", taskID) + } + + s.appendAudit("approval_resolved", "approval", approvalID, actor, map[string]interface{}{"status": status, "task_id": taskID, "action": action}) + var departmentID, agentID, threadID string + _ = s.db.QueryRow("SELECT IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(thread_id,'') FROM agent_tasks WHERE id = ?", taskID).Scan(&departmentID, &agentID, &threadID) + s.emitEvent(companyID, departmentID, agentID, threadID, taskID, "approval_resolved", "info", map[string]interface{}{"approval_id": approvalID, "status": status}) + return nil +} + +func canonicalJSON(raw []byte) string { + var v interface{} + if err := json.Unmarshal(raw, &v); err != nil { + return string(raw) + } + b, err := json.Marshal(v) + if err != nil { + return string(raw) + } + return string(b) +} + +func hashAudit(eventType, entityType, entityID, actor, prevHash, payloadJSON, ts string) string { + raw := strings.Join([]string{eventType, entityType, entityID, actor, prevHash, payloadJSON, ts}, "|") + sum := sha256.Sum256([]byte(raw)) + return hex.EncodeToString(sum[:]) +} + +func (s *Service) appendAudit(eventType, entityType, entityID, actor string, payload interface{}) { + payloadJSON := "{}" + if payload != nil { + if b, err := json.Marshal(payload); err == nil { + payloadJSON = canonicalJSON(b) + } + } + prevHash := "" + _ = s.db.QueryRow("SELECT IFNULL(event_hash,'') FROM audit_log ORDER BY id DESC LIMIT 1").Scan(&prevHash) + now := time.Now().UTC().Format(time.RFC3339Nano) + eventHash := hashAudit(eventType, entityType, entityID, actor, prevHash, payloadJSON, now) + sig, pubID := s.signAudit(eventHash) + _, _ = s.db.Exec(` + INSERT INTO audit_log (event_type, entity_type, entity_id, actor_type, actor_id, payload_json, prev_hash, event_hash, signature, pubkey_id, created_at) + VALUES (?, ?, ?, 'agent', ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `, eventType, entityType, entityID, actor, payloadJSON, prevHash, eventHash, sig, pubID) +} + +func (s *Service) signAudit(eventHash string) (string, string) { + priv, _, pubID := s.loadOrInitAuditKeypair() + if len(priv) == 0 { + return "", pubID + } + sig := ed25519.Sign(priv, []byte(eventHash)) + return base64.StdEncoding.EncodeToString(sig), pubID +} + +func (s *Service) loadOrInitAuditKeypair() (ed25519.PrivateKey, ed25519.PublicKey, string) { + keyPath := strings.TrimSpace(s.getSettingString("agentos_audit_signing_key_path", "./agentos_ed25519.key")) + pubID := strings.TrimSpace(s.getSettingString("agentos_audit_pubkey_id", "local-ed25519")) + + if raw, err := os.ReadFile(keyPath); err == nil { + decoded, derr := base64.StdEncoding.DecodeString(strings.TrimSpace(string(raw))) + if derr == nil && len(decoded) == ed25519.PrivateKeySize { + priv := ed25519.PrivateKey(decoded) + pub := priv.Public().(ed25519.PublicKey) + return priv, pub, pubID + } + } + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, pubID + } + _ = os.WriteFile(keyPath, []byte(base64.StdEncoding.EncodeToString(priv)), 0600) + return priv, pub, pubID +} + +func (s *Service) AuditVerify() map[string]interface{} { + rows, err := s.db.Query(` + SELECT id, IFNULL(event_hash,''), IFNULL(prev_hash,''), IFNULL(signature,''), IFNULL(created_at,'') + FROM audit_log ORDER BY id ASC + `) + if err != nil { + return map[string]interface{}{"ok": false, "error": err.Error()} + } + defer rows.Close() + + _, pub, _ := s.loadOrInitAuditKeypair() + if len(pub) == 0 { + return map[string]interface{}{"ok": false, "error": "audit public key unavailable"} + } + + lastHash := "" + count := 0 + issues := []string{} + for rows.Next() { + count++ + var id int64 + var eventHash, prevHash, sig, createdAt string + if err := rows.Scan(&id, &eventHash, &prevHash, &sig, &createdAt); err != nil { + issues = append(issues, fmt.Sprintf("row %d scan failed", id)) + continue + } + if prevHash != lastHash { + issues = append(issues, fmt.Sprintf("row %d chain mismatch", id)) + } + sigBytes, err := base64.StdEncoding.DecodeString(sig) + if err != nil || !ed25519.Verify(pub, []byte(eventHash), sigBytes) { + issues = append(issues, fmt.Sprintf("row %d bad signature", id)) + } + lastHash = eventHash + } + + return map[string]interface{}{ + "ok": len(issues) == 0, + "entries": count, + "issues": issues, + "last_hash": lastHash, + "verified_at": time.Now().UTC().Format(time.RFC3339), + } +} + +func (s *Service) ListAudit(sinceID int64, limit int) ([]map[string]interface{}, error) { + if limit <= 0 || limit > 500 { + limit = 200 + } + rows, err := s.db.Query(` + SELECT id, IFNULL(event_type,''), IFNULL(entity_type,''), IFNULL(entity_id,''), IFNULL(actor_type,''), IFNULL(actor_id,''), IFNULL(payload_json,'{}'), IFNULL(prev_hash,''), IFNULL(event_hash,''), IFNULL(signature,''), IFNULL(pubkey_id,''), IFNULL(created_at,'') + FROM audit_log WHERE id > ? ORDER BY id ASC LIMIT ? + `, sinceID, limit) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []map[string]interface{}{} + for rows.Next() { + var id int64 + var eventType, entityType, entityID, actorType, actorID, payloadJSON, prevHash, eventHash, signature, pubkeyID, createdAt string + if err := rows.Scan(&id, &eventType, &entityType, &entityID, &actorType, &actorID, &payloadJSON, &prevHash, &eventHash, &signature, &pubkeyID, &createdAt); err != nil { + continue + } + out = append(out, map[string]interface{}{ + "id": id, + "event_type": eventType, + "entity_type": entityType, + "entity_id": entityID, + "actor_type": actorType, + "actor_id": actorID, + "payload_json": payloadJSON, + "prev_hash": prevHash, + "event_hash": eventHash, + "signature": signature, + "pubkey_id": pubkeyID, + "created_at": createdAt, + }) + } + return out, nil +} diff --git a/dash/backend/agentos/memory.go b/dash/backend/agentos/memory.go new file mode 100644 index 0000000..343f2e6 --- /dev/null +++ b/dash/backend/agentos/memory.go @@ -0,0 +1,408 @@ +package agentos + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "os" + "sort" + "strings" + "time" + + "github.com/google/uuid" +) + +type memoryQueryHit struct { + Entry MemoryEntry + Score float64 +} + +func (s *Service) WriteMemoryEntry(entry MemoryEntry) error { + entry.ScopeType = strings.TrimSpace(strings.ToLower(entry.ScopeType)) + entry.ScopeID = strings.TrimSpace(entry.ScopeID) + entry.Content = strings.TrimSpace(entry.Content) + entry.SourceType = strings.TrimSpace(entry.SourceType) + entry.AuthorAgentID = strings.TrimSpace(entry.AuthorAgentID) + + if entry.ScopeType == "" || entry.ScopeID == "" { + return fmt.Errorf("scope_type and scope_id are required") + } + if entry.Content == "" { + return nil + } + if len(entry.Content) < 18 { + return nil + } + if entry.ID == "" { + entry.ID = uuid.NewString() + } + if entry.SourceType == "" { + entry.SourceType = "agent_note" + } + if entry.TagsJSON == "" { + entry.TagsJSON = "{}" + } + if entry.Importance <= 0 { + entry.Importance = 0.5 + } + + allowedScopes := map[string]bool{ + "global_user": true, + "company": true, + "department": true, + "agent_short_term": true, + "agent_long_term": true, + } + if !allowedScopes[entry.ScopeType] { + return fmt.Errorf("unsupported scope_type %q", entry.ScopeType) + } + + // Duplicate suppression within same scope. + var existing string + _ = s.db.QueryRow(` + SELECT id FROM memory_entries + WHERE scope_type = ? AND scope_id = ? AND content = ? + ORDER BY created_at DESC LIMIT 1 + `, entry.ScopeType, entry.ScopeID, entry.Content).Scan(&existing) + if existing != "" { + return nil + } + + if entry.TTLAt != "" { + if _, err := time.Parse(time.RFC3339, entry.TTLAt); err != nil { + entry.TTLAt = "" + } + } + + emb, err := s.generateEmbedding(entry.Content) + if err == nil && len(emb) > 0 { + b, _ := json.Marshal(emb) + entry.Embedding = string(b) + } else { + entry.Embedding = "" + } + + _, err = s.db.Exec(` + INSERT INTO memory_entries (id, scope_type, scope_id, source_type, author_agent_id, content, embedding, tags_json, importance, ttl_at, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NULLIF(?, ''), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, entry.ID, entry.ScopeType, entry.ScopeID, entry.SourceType, entry.AuthorAgentID, entry.Content, entry.Embedding, entry.TagsJSON, entry.Importance, entry.TTLAt) + if err != nil { + return err + } + + companyID, departmentID := s.scopeContext(entry.ScopeType, entry.ScopeID) + s.appendAudit("memory_write", "memory_entry", entry.ID, entry.AuthorAgentID, map[string]interface{}{ + "scope_type": entry.ScopeType, + "scope_id": entry.ScopeID, + "source_type": entry.SourceType, + }) + s.emitEvent(companyID, departmentID, entry.AuthorAgentID, "", "", "memory_written", "info", map[string]interface{}{ + "memory_id": entry.ID, + "scope_type": entry.ScopeType, + "scope_id": entry.ScopeID, + "importance": entry.Importance, + "source_type": entry.SourceType, + }) + + return nil +} + +func (s *Service) scopeContext(scopeType, scopeID string) (companyID string, departmentID string) { + switch scopeType { + case "company": + return scopeID, "" + case "department": + _ = s.db.QueryRow("SELECT IFNULL(company_id,'') FROM departments WHERE id = ?", scopeID).Scan(&companyID) + return companyID, scopeID + case "agent_short_term", "agent_long_term": + _ = s.db.QueryRow("SELECT IFNULL(company_id,''), IFNULL(department_id,'') FROM agents WHERE id = ?", scopeID).Scan(&companyID, &departmentID) + return companyID, departmentID + default: + return "", "" + } +} + +func (s *Service) ListMemoryTimeline(companyID, departmentID, agentID string, limit int) ([]MemoryEntry, error) { + if limit <= 0 || limit > 400 { + limit = 120 + } + query := ` + SELECT id, IFNULL(scope_type,''), IFNULL(scope_id,''), IFNULL(source_type,''), IFNULL(author_agent_id,''), IFNULL(content,''), IFNULL(embedding,''), IFNULL(tags_json,'{}'), IFNULL(importance,0.5), IFNULL(ttl_at,''), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM memory_entries WHERE 1=1` + args := []interface{}{} + + if strings.TrimSpace(agentID) != "" { + query += " AND ((scope_type IN ('agent_short_term','agent_long_term') AND scope_id = ?) OR author_agent_id = ?)" + args = append(args, agentID, agentID) + } else if strings.TrimSpace(departmentID) != "" { + query += " AND ( (scope_type = 'department' AND scope_id = ?) OR (scope_type IN ('agent_short_term','agent_long_term') AND scope_id IN (SELECT id FROM agents WHERE department_id = ?)) )" + args = append(args, departmentID, departmentID) + } else if strings.TrimSpace(companyID) != "" { + query += " AND ( (scope_type = 'company' AND scope_id = ?) OR (scope_type = 'department' AND scope_id IN (SELECT id FROM departments WHERE company_id = ?)) OR (scope_type IN ('agent_short_term','agent_long_term') AND scope_id IN (SELECT id FROM agents WHERE company_id = ?)) )" + args = append(args, companyID, companyID, companyID) + } + + query += " ORDER BY created_at DESC LIMIT ?" + args = append(args, limit) + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []MemoryEntry{} + for rows.Next() { + var e MemoryEntry + var ttlAt, createdAt, updatedAt string + if err := rows.Scan(&e.ID, &e.ScopeType, &e.ScopeID, &e.SourceType, &e.AuthorAgentID, &e.Content, &e.Embedding, &e.TagsJSON, &e.Importance, &ttlAt, &createdAt, &updatedAt); err != nil { + continue + } + e.TTLAt = strings.TrimSpace(ttlAt) + e.CreatedAt = parseDBTime(createdAt) + e.UpdatedAt = parseDBTime(updatedAt) + out = append(out, e) + } + return out, nil +} + +func (s *Service) QueryMemory(companyID, departmentID, agentID, query string, limit int) ([]MemoryEntry, error) { + if limit <= 0 || limit > 120 { + limit = 24 + } + entries, err := s.ListMemoryTimeline(companyID, departmentID, agentID, 500) + if err != nil { + return nil, err + } + if len(entries) == 0 { + return []MemoryEntry{}, nil + } + query = strings.TrimSpace(query) + if query == "" { + if len(entries) > limit { + entries = entries[:limit] + } + return entries, nil + } + + queryEmb, embErr := s.generateEmbedding(query) + qLower := strings.ToLower(query) + + hits := []memoryQueryHit{} + for _, e := range entries { + score := 0.0 + contentLower := strings.ToLower(e.Content) + if strings.Contains(contentLower, qLower) { + score += 0.45 + } + for _, tok := range strings.Fields(qLower) { + if tok != "" && strings.Contains(contentLower, tok) { + score += 0.08 + } + } + if embErr == nil && len(queryEmb) > 0 && strings.TrimSpace(e.Embedding) != "" { + var emb []float32 + if err := json.Unmarshal([]byte(e.Embedding), &emb); err == nil { + score += 0.7 * cosineSimilarity(queryEmb, emb) + } + } + if score > 0 { + hits = append(hits, memoryQueryHit{Entry: e, Score: score}) + } + } + + sort.Slice(hits, func(i, j int) bool { + if hits[i].Score == hits[j].Score { + return hits[i].Entry.CreatedAt.After(hits[j].Entry.CreatedAt) + } + return hits[i].Score > hits[j].Score + }) + + out := make([]MemoryEntry, 0, limit) + for _, h := range hits { + out = append(out, h.Entry) + if len(out) >= limit { + break + } + } + return out, nil +} + +func (s *Service) memoryContextForAgent(companyID, departmentID, agentID string) string { + sections := []struct { + Title string + Scope string + ID string + Limit int + }{ + {Title: "Global User Memory", Scope: "global_user", ID: "owner", Limit: 3}, + {Title: "Company Memory", Scope: "company", ID: companyID, Limit: 4}, + {Title: "Department Memory", Scope: "department", ID: departmentID, Limit: 4}, + {Title: "Agent Long-Term Memory", Scope: "agent_long_term", ID: agentID, Limit: 5}, + {Title: "Agent Short-Term Memory", Scope: "agent_short_term", ID: agentID, Limit: 5}, + } + + var b strings.Builder + b.WriteString("Scoped memory context for this run. Use it as guidance, not absolute truth.\n\n") + + for _, sec := range sections { + if strings.TrimSpace(sec.ID) == "" { + continue + } + rows, err := s.db.Query(` + SELECT IFNULL(content,''), IFNULL(source_type,''), IFNULL(created_at,'') + FROM memory_entries + WHERE scope_type = ? AND scope_id = ? + ORDER BY created_at DESC LIMIT ? + `, sec.Scope, sec.ID, sec.Limit) + if err != nil { + continue + } + + items := []string{} + for rows.Next() { + var content, source, createdAt string + if err := rows.Scan(&content, &source, &createdAt); err != nil { + continue + } + content = strings.TrimSpace(content) + if content == "" { + continue + } + if len(content) > 280 { + content = content[:280] + "..." + } + items = append(items, fmt.Sprintf("- (%s) %s", source, content)) + } + rows.Close() + if len(items) == 0 { + continue + } + b.WriteString(sec.Title) + b.WriteString(":\n") + b.WriteString(strings.Join(items, "\n")) + b.WriteString("\n\n") + } + + // Add indexed knowledge chunks from contextplus-native indexing where available. + if strings.TrimSpace(companyID) != "" { + rows, err := s.db.Query(` + SELECT IFNULL(k.chunk_text,'') + FROM knowledge_index_chunks k + JOIN knowledge_assets a ON a.id = k.asset_id + WHERE a.company_id = ? + ORDER BY k.created_at DESC + LIMIT 6 + `, companyID) + if err == nil { + defer rows.Close() + chunks := []string{} + for rows.Next() { + var chunk string + if err := rows.Scan(&chunk); err != nil { + continue + } + chunk = strings.TrimSpace(chunk) + if chunk == "" { + continue + } + if len(chunk) > 220 { + chunk = chunk[:220] + "..." + } + chunks = append(chunks, "- "+chunk) + } + if len(chunks) > 0 { + b.WriteString("Company Knowledge Index Hints:\n") + b.WriteString(strings.Join(chunks, "\n")) + b.WriteString("\n") + } + } + } + + return strings.TrimSpace(b.String()) +} + +func cosineSimilarity(a, b []float32) float64 { + if len(a) == 0 || len(a) != len(b) { + return 0 + } + var dot, na, nb float64 + for i := range a { + av := float64(a[i]) + bv := float64(b[i]) + dot += av * bv + na += av * av + nb += bv * bv + } + if na == 0 || nb == 0 { + return 0 + } + return dot / (math.Sqrt(na) * math.Sqrt(nb)) +} + +func (s *Service) generateEmbedding(text string) ([]float32, error) { + text = strings.TrimSpace(text) + if text == "" { + return nil, nil + } + apiURL := strings.TrimSpace(s.getSettingString("embedding_api_url", "http://localhost:11434")) + model := strings.TrimSpace(s.getSettingString("embedding_model", "nomic-embed-text")) + if apiURL == "" || model == "" { + return nil, fmt.Errorf("embedding provider is not configured") + } + apiURL = strings.TrimRight(apiURL, "/") + + isOpenAICompatible := strings.Contains(apiURL, "/v1") || strings.Contains(apiURL, "openrouter.ai") + endpoint := apiURL + "/api/embeddings" + payload := map[string]interface{}{"model": model, "prompt": text} + if isOpenAICompatible { + endpoint = apiURL + "/embeddings" + payload = map[string]interface{}{"model": model, "input": text} + } + b, _ := json.Marshal(payload) + + req, _ := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + if isOpenAICompatible { + apiKey := strings.TrimSpace(os.Getenv("OPENROUTER_API_KEY")) + if apiKey == "" || apiKey == "your_openrouter_api_key_here" { + apiKey = strings.TrimSpace(os.Getenv("OPENAI_API_KEY")) + } + if apiKey != "" { + req.Header.Set("Authorization", "Bearer "+apiKey) + } + } + client := &http.Client{Timeout: 45 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode/100 != 2 { + return nil, fmt.Errorf("embedding endpoint status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + + var ollamaOut struct { + Embedding []float32 `json:"embedding"` + } + if err := json.Unmarshal(body, &ollamaOut); err == nil && len(ollamaOut.Embedding) > 0 { + return ollamaOut.Embedding, nil + } + + var openAIOut struct { + Data []struct { + Embedding []float32 `json:"embedding"` + } `json:"data"` + } + if err := json.Unmarshal(body, &openAIOut); err != nil { + return nil, err + } + if len(openAIOut.Data) == 0 || len(openAIOut.Data[0].Embedding) == 0 { + return nil, fmt.Errorf("empty embedding vector") + } + return openAIOut.Data[0].Embedding, nil +} diff --git a/dash/backend/agentos/ops.go b/dash/backend/agentos/ops.go new file mode 100644 index 0000000..daa6d9e --- /dev/null +++ b/dash/backend/agentos/ops.go @@ -0,0 +1,142 @@ +package agentos + +import ( + "fmt" + "sort" + "strings" + + "github.com/google/uuid" +) + +func (s *Service) Topology(companyID string) map[string]interface{} { + departments, _ := s.ListDepartments(companyID) + agents, _ := s.ListAgents(companyID, "") + tasks, _ := s.ListTasks(companyID, "", "", 400) + + agentsByDepartment := map[string][]Agent{} + for _, ag := range agents { + agentsByDepartment[ag.DepartmentID] = append(agentsByDepartment[ag.DepartmentID], ag) + } + for depID := range agentsByDepartment { + sort.Slice(agentsByDepartment[depID], func(i, j int) bool { + return agentsByDepartment[depID][i].CreatedAt.Before(agentsByDepartment[depID][j].CreatedAt) + }) + } + + taskCounts := map[string]int{} + for _, t := range tasks { + taskCounts[t.Status]++ + } + + return map[string]interface{}{ + "departments": departments, + "agents": map[string]interface{}{ + "items": agents, + "by_department": agentsByDepartment, + }, + "tasks": map[string]interface{}{ + "items": tasks, + "status_counts": taskCounts, + }, + } +} + +func (s *Service) GetTaskDetails(taskID string) (map[string]interface{}, error) { + task, err := s.GetTask(taskID) + if err != nil { + return nil, err + } + runs, _ := s.ListRuns(taskID) + children, _ := s.db.Query(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(requested_by,''), IFNULL(parent_task_id,''), IFNULL(thread_id,''), IFNULL(type,''), IFNULL(status,''), IFNULL(priority,50), IFNULL(input_json,''), IFNULL(result_json,''), IFNULL(blocked_reason,''), IFNULL(created_at,''), IFNULL(updated_at,''), IFNULL(completed_at,'') + FROM agent_tasks WHERE parent_task_id = ? ORDER BY created_at ASC + `, taskID) + childTasks := []AgentTask{} + if children != nil { + defer children.Close() + for children.Next() { + t, err := scanTask(children) + if err == nil { + childTasks = append(childTasks, t) + } + } + } + + dRows, _ := s.db.Query(`SELECT id, IFNULL(parent_task_id,''), IFNULL(from_agent_id,''), IFNULL(to_agent_id,''), IFNULL(instruction,''), IFNULL(status,''), IFNULL(created_at,''), IFNULL(updated_at,'') FROM agent_delegations WHERE parent_task_id = ? ORDER BY created_at ASC`, taskID) + delegations := []AgentDelegation{} + if dRows != nil { + defer dRows.Close() + for dRows.Next() { + var d AgentDelegation + var createdAt, updatedAt string + if err := dRows.Scan(&d.ID, &d.ParentTaskID, &d.FromAgentID, &d.ToAgentID, &d.Instruction, &d.Status, &createdAt, &updatedAt); err == nil { + d.CreatedAt = parseDBTime(createdAt) + d.UpdatedAt = parseDBTime(updatedAt) + delegations = append(delegations, d) + } + } + } + + events, _ := s.ListEvents(task.CompanyID, taskID, 0, 200) + return map[string]interface{}{ + "task": task, + "runs": runs, + "children": childTasks, + "delegations": delegations, + "events": events, + }, nil +} + +func (s *Service) DelegateTask(parentTaskID, toAgentID, instruction, requestedBy string) (AgentTask, error) { + parent, err := s.GetTask(parentTaskID) + if err != nil { + return AgentTask{}, err + } + if strings.TrimSpace(toAgentID) == "" { + return AgentTask{}, fmt.Errorf("to_agent_id is required") + } + target, err := s.GetAgent(toAgentID) + if err != nil { + return AgentTask{}, err + } + if target.CompanyID != parent.CompanyID { + return AgentTask{}, fmt.Errorf("target agent belongs to another company") + } + if target.DepartmentID != parent.DepartmentID { + allowed := s.EvaluatePolicy(parent.CompanyID, parent.DepartmentID, parent.AgentID, "delegate_cross_department", fmt.Sprintf("to_agent:%s", target.ID)) + if !allowed.Allowed { + return AgentTask{}, fmt.Errorf("cross-department delegation denied") + } + } + if strings.TrimSpace(instruction) == "" { + instruction = parent.InputJSON + if strings.TrimSpace(instruction) == "" { + instruction = `{"prompt":"Delegated task"}` + } + } + if strings.TrimSpace(requestedBy) == "" { + requestedBy = parent.AgentID + } + + child := AgentTask{ + ID: uuid.NewString(), + CompanyID: parent.CompanyID, + DepartmentID: target.DepartmentID, + AgentID: target.ID, + RequestedBy: requestedBy, + ParentTaskID: parent.ID, + ThreadID: parent.ThreadID, + Type: "delegated", + Status: "queued", + Priority: parent.Priority + 5, + InputJSON: instruction, + } + created, err := s.CreateTask(child) + if err != nil { + return AgentTask{}, err + } + _, _ = s.db.Exec(`INSERT INTO agent_delegations (id, parent_task_id, from_agent_id, to_agent_id, instruction, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'queued', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, uuid.NewString(), parent.ID, parent.AgentID, target.ID, instruction) + s.appendAudit("task_delegated", "task", parent.ID, requestedBy, map[string]interface{}{"to_agent_id": target.ID, "child_task_id": created.ID}) + s.emitEvent(parent.CompanyID, parent.DepartmentID, parent.AgentID, parent.ThreadID, parent.ID, "task_delegated", "info", map[string]interface{}{"to_agent_id": target.ID, "child_task_id": created.ID}) + return created, nil +} diff --git a/dash/backend/agentos/runtime.go b/dash/backend/agentos/runtime.go new file mode 100644 index 0000000..8f8a84a --- /dev/null +++ b/dash/backend/agentos/runtime.go @@ -0,0 +1,628 @@ +package agentos + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "regexp" + "strings" + "time" + + "github.com/google/uuid" +) + +type runtimeTaskInput struct { + Prompt string `json:"prompt"` +} + +type llmMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +func (s *Service) processQueue() { + row := s.db.QueryRow(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(requested_by,''), IFNULL(parent_task_id,''), IFNULL(thread_id,''), IFNULL(type,''), IFNULL(status,''), IFNULL(priority,50), IFNULL(input_json,''), IFNULL(result_json,''), IFNULL(blocked_reason,''), IFNULL(created_at,''), IFNULL(updated_at,''), IFNULL(completed_at,'') + FROM agent_tasks WHERE status = 'queued' + ORDER BY priority ASC, created_at ASC + LIMIT 1 + `) + task, err := scanTask(row) + if err != nil { + return + } + if ok, reason := s.schedulerAdmission(&task); !ok { + // Resource pressure should throttle admission without mutating task state. + if reason == "cpu_hard" || reason == "ram_hard" || reason == "max_workers" { + return + } + if reason != "" && task.Status == "queued" { + _, _ = s.db.Exec("UPDATE agent_tasks SET status='blocked', blocked_reason=?, updated_at=CURRENT_TIMESTAMP WHERE id = ?", reason, task.ID) + s.emitEvent(task.CompanyID, task.DepartmentID, task.AgentID, task.ThreadID, task.ID, "task_blocked", "warn", map[string]interface{}{"reason": reason}) + } + return + } + + s.mu.Lock() + if _, ok := s.running[task.ID]; ok { + s.mu.Unlock() + return + } + s.running[task.ID] = struct{}{} + s.mu.Unlock() + + go func(t AgentTask) { + defer func() { + s.mu.Lock() + delete(s.running, t.ID) + s.mu.Unlock() + }() + s.runTask(t) + }(task) +} + +func (s *Service) runTask(task AgentTask) { + agent, err := s.GetAgent(task.AgentID) + if err != nil { + s.failTask(task, "agent_not_found", err.Error()) + return + } + if !agent.IsActive { + s.failTask(task, "agent_inactive", "agent is inactive") + return + } + + if !s.policyAllowed(task, "task_run", fmt.Sprintf("company:%s/department:%s/agent:%s", task.CompanyID, task.DepartmentID, task.AgentID), "tier2") { + return + } + + _, _ = s.db.Exec("UPDATE agent_tasks SET status='running', blocked_reason='', updated_at=CURRENT_TIMESTAMP WHERE id = ?", task.ID) + s.emitEvent(task.CompanyID, task.DepartmentID, task.AgentID, task.ThreadID, task.ID, "task_running", "info", map[string]interface{}{"task_id": task.ID}) + + runID := uuid.NewString() + _, _ = s.db.Exec(`INSERT INTO agent_runs (id, task_id, attempt, status, provider, model, started_at, ended_at, summary, error) VALUES (?, ?, 1, 'running', '', '', CURRENT_TIMESTAMP, NULL, '', '')`, runID, task.ID) + + if strings.EqualFold(agent.RoleType, "manager") { + if done := s.processManagerTask(task, agent, runID); done { + return + } + } + + output, provider, model, runErr := s.generateAgentOutput(task, agent) + if runErr != nil { + s.failRunAndTask(runID, task, provider, model, runErr) + return + } + + s.extractAndApplySelfSchedule(output, task, agent) + s.extractAndApplyInterAgentMessages(output, task, agent) + + result := map[string]interface{}{ + "output": output, + "provider": provider, + "model": model, + } + resJSON, _ := json.Marshal(result) + + completedAt := time.Now().UTC() + _, _ = s.db.Exec("UPDATE agent_tasks SET status='done', result_json=?, updated_at=CURRENT_TIMESTAMP, completed_at=CURRENT_TIMESTAMP WHERE id = ?", string(resJSON), task.ID) + _, _ = s.db.Exec("UPDATE agent_runs SET status='done', provider=?, model=?, ended_at=CURRENT_TIMESTAMP, summary=? WHERE id = ?", provider, model, summarize(output, 500), runID) + _, _ = s.db.Exec("UPDATE agents SET status='idle', updated_at=CURRENT_TIMESTAMP WHERE id = ?", agent.ID) + + if task.ThreadID != "" { + _, _ = s.AddThreadMessage(task.ThreadID, "agent", output, "text") + } + + _ = s.WriteMemoryEntry(MemoryEntry{ + ID: uuid.NewString(), + ScopeType: "agent_long_term", + ScopeID: agent.ID, + SourceType: "task_result", + AuthorAgentID: agent.ID, + Content: summarize(output, 1600), + TagsJSON: `{"origin":"task","task_id":"` + task.ID + `"}`, + Importance: 0.7, + }) + + if task.ParentTaskID != "" { + _, _ = s.db.Exec("UPDATE agent_delegations SET status='done', updated_at=CURRENT_TIMESTAMP WHERE parent_task_id = ? AND to_agent_id = ?", task.ParentTaskID, task.AgentID) + } + + s.emitEvent(task.CompanyID, task.DepartmentID, task.AgentID, task.ThreadID, task.ID, "task_done", "info", map[string]interface{}{"completed_at": completedAt.Format(time.RFC3339)}) + s.appendAudit("task_done", "task", task.ID, agent.ID, map[string]interface{}{"provider": provider, "model": model}) +} + +func (s *Service) processManagerTask(task AgentTask, agent Agent, runID string) bool { + // If manager has active subtasks, wait for completion and synthesize later. + var totalChildren, pendingChildren int + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE parent_task_id = ?", task.ID).Scan(&totalChildren) + if totalChildren > 0 { + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE parent_task_id = ? AND status IN ('queued','running','blocked','waiting_input')", task.ID).Scan(&pendingChildren) + if pendingChildren > 0 { + _, _ = s.db.Exec("UPDATE agent_tasks SET status='waiting_input', blocked_reason='awaiting_worker_tasks', updated_at=CURRENT_TIMESTAMP WHERE id = ?", task.ID) + _, _ = s.db.Exec("UPDATE agent_runs SET status='done', provider='manager', model='manager', ended_at=CURRENT_TIMESTAMP, summary='waiting for worker tasks' WHERE id = ?", runID) + s.emitEvent(task.CompanyID, task.DepartmentID, task.AgentID, task.ThreadID, task.ID, "task_waiting_workers", "info", map[string]interface{}{"pending_children": pendingChildren}) + return true + } + + rows, err := s.db.Query("SELECT IFNULL(result_json,'') FROM agent_tasks WHERE parent_task_id = ? ORDER BY created_at ASC", task.ID) + if err != nil { + s.failRunAndTask(runID, task, "manager", "manager", err) + return true + } + defer rows.Close() + parts := []string{} + for rows.Next() { + var r string + if err := rows.Scan(&r); err == nil { + parts = append(parts, r) + } + } + summary := "Manager synthesis:\n\n" + strings.Join(parts, "\n\n") + result := map[string]interface{}{"output": summary, "provider": "manager", "model": "manager"} + resJSON, _ := json.Marshal(result) + _, _ = s.db.Exec("UPDATE agent_tasks SET status='done', result_json=?, updated_at=CURRENT_TIMESTAMP, completed_at=CURRENT_TIMESTAMP WHERE id = ?", string(resJSON), task.ID) + _, _ = s.db.Exec("UPDATE agent_runs SET status='done', provider='manager', model='manager', ended_at=CURRENT_TIMESTAMP, summary=? WHERE id = ?", summarize(summary, 500), runID) + if task.ThreadID != "" { + _, _ = s.AddThreadMessage(task.ThreadID, "agent", summary, "text") + } + s.emitEvent(task.CompanyID, task.DepartmentID, task.AgentID, task.ThreadID, task.ID, "task_done", "info", map[string]interface{}{"mode": "manager_synthesis"}) + return true + } + + workers, err := s.ListAgents(task.CompanyID, task.DepartmentID) + if err != nil { + s.failRunAndTask(runID, task, "manager", "manager", err) + return true + } + eligible := []Agent{} + for _, w := range workers { + if w.RoleType != "worker" || !w.IsActive { + continue + } + if strings.TrimSpace(w.ParentAgentID) != "" && w.ParentAgentID != agent.ID { + continue + } + eligible = append(eligible, w) + } + if len(eligible) == 0 { + // No workers; manager executes directly as regular agent. + return false + } + + instruction := task.InputJSON + if strings.TrimSpace(instruction) == "" { + instruction = `{"prompt":"Execute delegated manager task"}` + } + for _, w := range eligible { + if !s.policyAllowed(task, "delegate", fmt.Sprintf("to_agent:%s", w.ID), "tier1") { + continue + } + child := AgentTask{ + ID: uuid.NewString(), + CompanyID: task.CompanyID, + DepartmentID: task.DepartmentID, + AgentID: w.ID, + RequestedBy: agent.ID, + ParentTaskID: task.ID, + ThreadID: task.ThreadID, + Type: "delegated", + Status: "queued", + Priority: task.Priority + 5, + InputJSON: instruction, + } + if _, err := s.CreateTask(child); err == nil { + _, _ = s.db.Exec(`INSERT INTO agent_delegations (id, parent_task_id, from_agent_id, to_agent_id, instruction, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'queued', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, uuid.NewString(), task.ID, agent.ID, w.ID, summarize(instruction, 500)) + s.emitEvent(task.CompanyID, task.DepartmentID, agent.ID, task.ThreadID, task.ID, "task_delegated", "info", map[string]interface{}{"to_agent_id": w.ID, "child_task_id": child.ID}) + } + } + + _, _ = s.db.Exec("UPDATE agent_tasks SET status='waiting_input', blocked_reason='awaiting_worker_tasks', updated_at=CURRENT_TIMESTAMP WHERE id = ?", task.ID) + _, _ = s.db.Exec("UPDATE agent_runs SET status='done', provider='manager', model='manager', ended_at=CURRENT_TIMESTAMP, summary='delegated to worker agents' WHERE id = ?", runID) + return true +} + +func (s *Service) resumeBlockedManagers() { + rows, err := s.db.Query(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(requested_by,''), IFNULL(parent_task_id,''), IFNULL(thread_id,''), IFNULL(type,''), IFNULL(status,''), IFNULL(priority,50), IFNULL(input_json,''), IFNULL(result_json,''), IFNULL(blocked_reason,''), IFNULL(created_at,''), IFNULL(updated_at,''), IFNULL(completed_at,'') + FROM agent_tasks WHERE status = 'waiting_input' AND blocked_reason = 'awaiting_worker_tasks' + `) + if err != nil { + return + } + defer rows.Close() + for rows.Next() { + t, err := scanTask(rows) + if err != nil { + continue + } + var pending int + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE parent_task_id = ? AND status IN ('queued','running','blocked','waiting_input')", t.ID).Scan(&pending) + if pending == 0 { + _, _ = s.db.Exec("UPDATE agent_tasks SET status='queued', blocked_reason='', updated_at=CURRENT_TIMESTAMP WHERE id = ?", t.ID) + s.emitEvent(t.CompanyID, t.DepartmentID, t.AgentID, t.ThreadID, t.ID, "task_resumed", "info", map[string]interface{}{"reason": "workers_completed"}) + } + } +} + +func (s *Service) failRunAndTask(runID string, task AgentTask, provider string, model string, err error) { + if runID != "" { + _, _ = s.db.Exec("UPDATE agent_runs SET status='error', provider=?, model=?, ended_at=CURRENT_TIMESTAMP, error=?, summary=? WHERE id = ?", provider, model, err.Error(), summarize(err.Error(), 500), runID) + } + s.failTask(task, "runtime_error", err.Error()) +} + +func (s *Service) failTask(task AgentTask, reason string, details string) { + _, _ = s.db.Exec("UPDATE agent_tasks SET status='failed', blocked_reason=?, updated_at=CURRENT_TIMESTAMP, completed_at=CURRENT_TIMESTAMP WHERE id = ?", reason, task.ID) + _, _ = s.db.Exec("UPDATE agents SET status='error', updated_at=CURRENT_TIMESTAMP WHERE id = ?", task.AgentID) + s.emitEvent(task.CompanyID, task.DepartmentID, task.AgentID, task.ThreadID, task.ID, "task_failed", "error", map[string]interface{}{"reason": reason, "details": details}) + s.appendAudit("task_failed", "task", task.ID, task.AgentID, map[string]interface{}{"reason": reason, "details": details}) +} + +func (s *Service) generateAgentOutput(task AgentTask, agent Agent) (string, string, string, error) { + binding, profile, err := s.getBinding(agent.ID) + if err != nil { + return "", "", "", err + } + + messages := []llmMessage{} + messages = append(messages, llmMessage{Role: "system", Content: strings.TrimSpace(agent.IdentityPrompt)}) + messages = append(messages, llmMessage{Role: "system", Content: "Mode: AgentOS v1. You can reason, write memory, and use two special output blocks:\n\nTo schedule a future task for yourself, output:\n````agenthq_schedule\n{\"type\":\"cron\",\"cron_expr\":\"0 9 * * MON\",\"message\":\"What to do\"}\n````\nor for a one-time trigger:\n````agenthq_schedule\n{\"type\":\"once\",\"start_at\":\"2026-05-28T09:00:00Z\",\"message\":\"What to do\"}\n````\n\nTo send a direct message to another agent, output:\n````agenthq_message\n{\"to_agent_id\":\"\",\"content\":\"Your message here\"}\n````"}) + messages = append(messages, llmMessage{Role: "system", Content: s.memoryContextForAgent(task.CompanyID, task.DepartmentID, agent.ID)}) + + if task.ThreadID != "" { + recent, _ := s.ListThreadMessages(task.ThreadID) + if len(recent) > 24 { + recent = recent[len(recent)-24:] + } + for _, m := range recent { + role := "user" + switch m.Role { + case "agent": + role = "assistant" + case "system": + role = "system" + default: + role = "user" + } + messages = append(messages, llmMessage{Role: role, Content: m.Content}) + } + } + + if strings.TrimSpace(task.InputJSON) != "" { + var in runtimeTaskInput + if err := json.Unmarshal([]byte(task.InputJSON), &in); err == nil && strings.TrimSpace(in.Prompt) != "" { + messages = append(messages, llmMessage{Role: "user", Content: in.Prompt}) + } else { + messages = append(messages, llmMessage{Role: "user", Content: task.InputJSON}) + } + } + + // Resolve owner user ID for per-user API key lookup + ownerUserID := "admin" + _ = s.db.QueryRow(`SELECT IFNULL(owner_user_id,'admin') FROM companies WHERE id = ?`, task.CompanyID).Scan(&ownerUserID) + + output, provider, model, err := s.callProvider(ownerUserID, profile, binding, messages) + if err != nil { + return "", provider, model, err + } + return output, provider, model, nil +} + +func (s *Service) callProvider(ownerUserID string, profile AgentModelProfile, binding AgentModelBinding, messages []llmMessage) (string, string, string, error) { + provider := strings.ToLower(strings.TrimSpace(profile.Provider)) + model := strings.TrimSpace(profile.Model) + if provider == "" { + provider = "openrouter" + } + if model == "" { + model = s.getUserSettingString(ownerUserID, "default_model", "") + } + + chain := []string{provider} + var fallback []string + _ = json.Unmarshal([]byte(profile.FallbackChainJSON), &fallback) + for _, p := range fallback { + p = strings.TrimSpace(strings.ToLower(p)) + if p != "" && p != provider { + chain = append(chain, p) + } + } + + var lastErr error + for _, candidate := range unique(chain) { + out, usedModel, err := s.callProviderOnce(ownerUserID, candidate, model, binding, messages) + if err == nil { + return strings.TrimSpace(out), candidate, usedModel, nil + } + lastErr = err + } + if lastErr == nil { + lastErr = fmt.Errorf("no provider available") + } + return "", provider, model, lastErr +} + +func (s *Service) callProviderOnce(ownerUserID, provider, model string, binding AgentModelBinding, messages []llmMessage) (string, string, error) { + switch provider { + case "openrouter": + return s.callOpenRouter(ownerUserID, model, binding, messages) + case "local": + return s.callLocal(model, binding, messages) + case "cli_codex": + return s.callCLI("codex", model, messages) + case "cli_claude": + return s.callCLI("claude", model, messages) + case "cli_gemini": + return s.callCLI("gemini", model, messages) + default: + return "", model, fmt.Errorf("unsupported provider %s", provider) + } +} + +func (s *Service) callOpenRouter(ownerUserID, model string, binding AgentModelBinding, messages []llmMessage) (string, string, error) { + // User's personal key takes priority over server-wide env key + apiKey := "" + if ownerUserID != "" && ownerUserID != "admin" { + _ = s.db.QueryRow(`SELECT COALESCE(openrouter_api_key,'') FROM users WHERE id = ?`, ownerUserID).Scan(&apiKey) + apiKey = strings.TrimSpace(apiKey) + } + if apiKey == "" { + apiKey = strings.TrimSpace(os.Getenv("OPENROUTER_API_KEY")) + } + if apiKey == "" || apiKey == "your_openrouter_api_key_here" { + return "", model, fmt.Errorf("OPENROUTER_API_KEY missing") + } + payload := map[string]interface{}{ + "model": model, + "messages": messages, + "stream": false, + } + if binding.MaxTokens > 0 { + payload["max_tokens"] = binding.MaxTokens + } + if binding.Temperature > 0 { + payload["temperature"] = binding.Temperature + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest(http.MethodPost, "https://openrouter.ai/api/v1/chat/completions", bytes.NewBuffer(body)) + req.Header.Set("Authorization", "Bearer "+apiKey) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Title", "Apollo AgentOS") + client := &http.Client{Timeout: 120 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", model, err + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return "", model, fmt.Errorf("openrouter status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(respBody))) + } + var parsed struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + if err := json.Unmarshal(respBody, &parsed); err != nil { + return "", model, err + } + if len(parsed.Choices) == 0 { + return "", model, fmt.Errorf("empty choices") + } + return parsed.Choices[0].Message.Content, model, nil +} + +func (s *Service) callLocal(model string, binding AgentModelBinding, messages []llmMessage) (string, string, error) { + baseURL := strings.TrimSpace(os.Getenv("OLLAMA_API_URL")) + if baseURL == "" { + baseURL = strings.TrimSpace(s.getSettingString("embedding_api_url", "http://localhost:11434")) + } + if strings.HasSuffix(baseURL, "/api") { + baseURL = strings.TrimSuffix(baseURL, "/api") + } + payload := map[string]interface{}{ + "model": model, + "messages": messages, + "stream": false, + } + if binding.MaxTokens > 0 { + payload["max_tokens"] = binding.MaxTokens + } + if binding.Temperature > 0 { + payload["temperature"] = binding.Temperature + } + body, _ := json.Marshal(payload) + req, _ := http.NewRequest(http.MethodPost, strings.TrimRight(baseURL, "/")+"/v1/chat/completions", bytes.NewBuffer(body)) + req.Header.Set("Authorization", "Bearer local") + req.Header.Set("Content-Type", "application/json") + client := &http.Client{Timeout: 120 * time.Second} + resp, err := client.Do(req) + if err != nil { + return "", model, err + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return "", model, fmt.Errorf("local status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(respBody))) + } + var parsed struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + if err := json.Unmarshal(respBody, &parsed); err != nil { + return "", model, err + } + if len(parsed.Choices) == 0 { + return "", model, fmt.Errorf("empty choices") + } + return parsed.Choices[0].Message.Content, model, nil +} + +func (s *Service) callCLI(cliName, model string, messages []llmMessage) (string, string, error) { + if _, err := exec.LookPath(cliName); err != nil { + return "", model, fmt.Errorf("%s not installed", cliName) + } + prompt := "" + for _, m := range messages { + if m.Role == "system" { + prompt += "[SYSTEM]\n" + m.Content + "\n\n" + } else if m.Role == "assistant" { + prompt += "[ASSISTANT]\n" + m.Content + "\n\n" + } else { + prompt += "[USER]\n" + m.Content + "\n\n" + } + } + ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute) + defer cancel() + + var cmd *exec.Cmd + switch cliName { + case "codex": + cmd = exec.CommandContext(ctx, "codex", "--model", model, "--dangerously-bypass-approvals-and-sandbox", prompt) + case "claude": + cmd = exec.CommandContext(ctx, "claude", "--model", model, "--dangerously-skip-permissions", "-p", prompt) + default: + cmd = exec.CommandContext(ctx, cliName, "-p", prompt) + } + out, err := cmd.CombinedOutput() + if err != nil { + return "", model, fmt.Errorf("%s failed: %s", cliName, strings.TrimSpace(string(out))) + } + return string(out), model, nil +} + +func summarize(text string, max int) string { + clean := strings.TrimSpace(text) + if max <= 0 || len(clean) <= max { + return clean + } + return clean[:max] + "..." +} + +func unique(items []string) []string { + seen := map[string]struct{}{} + out := make([]string, 0, len(items)) + for _, it := range items { + it = strings.TrimSpace(it) + if it == "" { + continue + } + if _, ok := seen[it]; ok { + continue + } + seen[it] = struct{}{} + out = append(out, it) + } + return out +} + +func (s *Service) policyAllowed(task AgentTask, action string, scope string, fallbackTier string) bool { + dec := s.EvaluatePolicy(task.CompanyID, task.DepartmentID, task.AgentID, action, scope) + if dec.Allowed && normalizeTier(dec.ApprovalTier) == "none" { + return true + } + tier := normalizeTier(dec.ApprovalTier) + if tier == "none" { + tier = normalizeTier(fallbackTier) + } + if tier == "none" { + tier = "tier2" + } + _ = s.RequestApproval(task.CompanyID, task.ID, action, tier, dec.Reason, map[string]interface{}{"scope": scope, "decision": dec}) + _, _ = s.db.Exec("UPDATE agent_tasks SET status='blocked', blocked_reason='approval_pending', updated_at=CURRENT_TIMESTAMP WHERE id = ?", task.ID) + s.emitEvent(task.CompanyID, task.DepartmentID, task.AgentID, task.ThreadID, task.ID, "task_blocked", "warn", map[string]interface{}{"reason": "approval_pending", "action": action}) + return false +} + +func (s *Service) threadForTask(taskID string) (AgentTask, AgentThread, error) { + task, err := s.GetTask(taskID) + if err != nil { + return AgentTask{}, AgentThread{}, err + } + if strings.TrimSpace(task.ThreadID) == "" { + return task, AgentThread{}, sql.ErrNoRows + } + thread, err := s.GetThread(task.ThreadID) + return task, thread, err +} + +func (s *Service) BuildTaskPromptInput(prompt string) string { + payload := runtimeTaskInput{Prompt: strings.TrimSpace(prompt)} + b, _ := json.Marshal(payload) + return string(b) +} + +// extractAndApplySelfSchedule parses ````agenthq_schedule ... ```` blocks from agent output +// and creates schedules targeting the agent itself. +func (s *Service) extractAndApplySelfSchedule(output string, task AgentTask, agent Agent) { + re := regexp.MustCompile("(?s)````agenthq_schedule\n(.*?)````") + matches := re.FindAllStringSubmatch(output, -1) + for _, m := range matches { + var cmd struct { + Type string `json:"type"` + CronExpr string `json:"cron_expr"` + StartAt string `json:"start_at"` + Message string `json:"message"` + } + if err := json.Unmarshal([]byte(strings.TrimSpace(m[1])), &cmd); err != nil { + continue + } + schedType := cmd.Type + if schedType == "" { + if cmd.CronExpr != "" { + schedType = "cron" + } else { + schedType = "once" + } + } + payload, _ := json.Marshal(map[string]string{"prompt": cmd.Message}) + sch := Schedule{ + ID: uuid.NewString(), + CompanyID: task.CompanyID, + DepartmentID: task.DepartmentID, + TargetAgentID: agent.ID, + ScheduleType: schedType, + CronExpr: cmd.CronExpr, + StartAt: cmd.StartAt, + Timezone: "UTC", + PayloadJSON: string(payload), + IsActive: true, + } + if _, err := s.CreateSchedule(sch); err == nil { + s.emitEvent(task.CompanyID, task.DepartmentID, agent.ID, task.ThreadID, task.ID, "agent_self_scheduled", "info", map[string]interface{}{"schedule_type": schedType}) + } + } +} + +// extractAndApplyInterAgentMessages parses ````agenthq_message ... ```` blocks and delivers them. +func (s *Service) extractAndApplyInterAgentMessages(output string, task AgentTask, agent Agent) { + re := regexp.MustCompile("(?s)````agenthq_message\n(.*?)````") + matches := re.FindAllStringSubmatch(output, -1) + for _, m := range matches { + var cmd struct { + ToAgentID string `json:"to_agent_id"` + Content string `json:"content"` + } + if err := json.Unmarshal([]byte(strings.TrimSpace(m[1])), &cmd); err != nil { + continue + } + if cmd.ToAgentID == "" || cmd.Content == "" { + continue + } + if err := s.PostInterAgentMessage(task.CompanyID, agent.ID, cmd.ToAgentID, cmd.Content); err == nil { + s.emitEvent(task.CompanyID, task.DepartmentID, agent.ID, task.ThreadID, task.ID, "inter_agent_message_sent", "info", map[string]interface{}{"to_agent_id": cmd.ToAgentID}) + } + } +} diff --git a/dash/backend/agentos/scheduler.go b/dash/backend/agentos/scheduler.go new file mode 100644 index 0000000..1aa87d9 --- /dev/null +++ b/dash/backend/agentos/scheduler.go @@ -0,0 +1,524 @@ +package agentos + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/mem" +) + +type SchedulerConfig struct { + Mode string `json:"mode"` + MinWorkers int `json:"min_workers"` + MaxWorkers int `json:"max_workers"` + MinReviewers int `json:"min_reviewers"` + MaxReviewers int `json:"max_reviewers"` + CPUSoft float64 `json:"cpu_soft"` + CPUHard float64 `json:"cpu_hard"` + RAMSoft float64 `json:"ram_soft"` + RAMHard float64 `json:"ram_hard"` +} + +func (s *Service) schedulerConfig() SchedulerConfig { + cfg := SchedulerConfig{ + Mode: s.getSettingString("agentos_scheduler_mode", "adaptive"), + MinWorkers: toInt(s.getSettingString("agentos_scheduler_min_workers", "1"), 1), + MaxWorkers: toInt(s.getSettingString("agentos_scheduler_max_workers", "4"), 4), + MinReviewers: toInt(s.getSettingString("agentos_scheduler_min_reviewers", "1"), 1), + MaxReviewers: toInt(s.getSettingString("agentos_scheduler_max_reviewers", "2"), 2), + CPUSoft: toFloat(s.getSettingString("agentos_scheduler_cpu_soft", "70"), 70), + CPUHard: toFloat(s.getSettingString("agentos_scheduler_cpu_hard", "90"), 90), + RAMSoft: toFloat(s.getSettingString("agentos_scheduler_ram_soft", "75"), 75), + RAMHard: toFloat(s.getSettingString("agentos_scheduler_ram_hard", "90"), 90), + } + if cfg.MinWorkers < 1 { + cfg.MinWorkers = 1 + } + if cfg.MaxWorkers < cfg.MinWorkers { + cfg.MaxWorkers = cfg.MinWorkers + } + if cfg.MaxReviewers < cfg.MinReviewers { + cfg.MaxReviewers = cfg.MinReviewers + } + return cfg +} + +func (s *Service) runningTaskCount() int { + var n int + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status = 'running'").Scan(&n) + return n +} + +func (s *Service) currentResourcePressure() (cpuPercent float64, ramPercent float64) { + cpuPercent = 25 + ramPercent = 35 + if samples, err := cpu.Percent(0, false); err == nil && len(samples) > 0 { + cpuPercent = samples[0] + } + if vm, err := mem.VirtualMemory(); err == nil { + ramPercent = vm.UsedPercent + } + return cpuPercent, ramPercent +} + +func (s *Service) schedulerAdmission(task *AgentTask) (bool, string) { + if strings.EqualFold(s.getSettingString("agentos_kill_switch", "false"), "true") { + return false, "kill_switch" + } + cfg := s.schedulerConfig() + cpuP, ramP := s.currentResourcePressure() + if cpuP >= cfg.CPUHard { + return false, "cpu_hard" + } + if ramP >= cfg.RAMHard { + return false, "ram_hard" + } + if s.runningTaskCount() >= cfg.MaxWorkers { + return false, "max_workers" + } + if task != nil { + if task.Status == "blocked" && task.BlockedReason != "" { + return false, task.BlockedReason + } + } + return true, "" +} + +func (s *Service) SchedulerState() map[string]interface{} { + cfg := s.schedulerConfig() + cpuP, ramP := s.currentResourcePressure() + queued := 0 + running := 0 + blocked := 0 + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status = 'queued'").Scan(&queued) + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status = 'running'").Scan(&running) + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status = 'blocked'").Scan(&blocked) + + return map[string]interface{}{ + "config": cfg, + "host_pressure": map[string]interface{}{ + "cpu_percent": cpuP, + "ram_percent": ramP, + "updated_at": time.Now().UTC().Format(time.RFC3339), + }, + "queue": map[string]interface{}{ + "queued": queued, + "running": running, + "blocked": blocked, + }, + } +} + +func (s *Service) UpdateSchedulerConfig(payload map[string]interface{}) error { + for _, key := range []string{ + "agentos_scheduler_mode", + "agentos_scheduler_min_workers", + "agentos_scheduler_max_workers", + "agentos_scheduler_min_reviewers", + "agentos_scheduler_max_reviewers", + "agentos_scheduler_cpu_soft", + "agentos_scheduler_cpu_hard", + "agentos_scheduler_ram_soft", + "agentos_scheduler_ram_hard", + } { + if v, ok := payload[key]; ok { + _, _ = s.db.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", key, stringify(v)) + } + } + s.appendAudit("scheduler_updated", "scheduler", "agentos", "owner", payload) + return nil +} + +func stringify(v interface{}) string { + switch t := v.(type) { + case string: + return t + case float64: + return strconv.FormatFloat(t, 'f', -1, 64) + case float32: + return strconv.FormatFloat(float64(t), 'f', -1, 64) + case int: + return strconv.Itoa(t) + case int64: + return strconv.FormatInt(t, 10) + case bool: + if t { + return "true" + } + return "false" + default: + b, _ := json.Marshal(v) + return string(b) + } +} + +func toInt(v string, fallback int) int { + i, err := strconv.Atoi(strings.TrimSpace(v)) + if err != nil { + return fallback + } + return i +} + +func toFloat(v string, fallback float64) float64 { + f, err := strconv.ParseFloat(strings.TrimSpace(v), 64) + if err != nil { + return fallback + } + return f +} + +func parseAnyTime(value string, fallback time.Time) time.Time { + value = strings.TrimSpace(value) + if value == "" { + return fallback + } + layouts := []string{time.RFC3339, time.RFC3339Nano, "2006-01-02 15:04:05", "2006-01-02T15:04:05", "2006-01-02"} + for _, layout := range layouts { + if t, err := time.Parse(layout, value); err == nil { + return t + } + } + return fallback +} + +func nextFromCron(expr string, from time.Time, loc *time.Location) time.Time { + parts := strings.Fields(strings.TrimSpace(expr)) + if len(parts) != 5 { + return from.Add(1 * time.Hour) + } + minute := parts[0] + hour := parts[1] + + base := from.In(loc).Truncate(time.Minute).Add(time.Minute) + + if minute == "*" && hour == "*" { + return base + } + if strings.HasPrefix(minute, "*/") && hour == "*" { + n, err := strconv.Atoi(strings.TrimPrefix(minute, "*/")) + if err != nil || n <= 0 { + return from.Add(1 * time.Hour) + } + m := base.Minute() + nextMin := ((m / n) + 1) * n + delta := nextMin - m + if delta <= 0 { + delta += n + } + return base.Add(time.Duration(delta) * time.Minute) + } + if hour == "*" { + m, err := strconv.Atoi(minute) + if err != nil || m < 0 || m > 59 { + return from.Add(1 * time.Hour) + } + t := time.Date(base.Year(), base.Month(), base.Day(), base.Hour(), m, 0, 0, loc) + if !t.After(from.In(loc)) { + t = t.Add(1 * time.Hour) + } + return t + } + + m, errM := strconv.Atoi(minute) + h, errH := strconv.Atoi(hour) + if errM != nil || errH != nil || m < 0 || m > 59 || h < 0 || h > 23 { + return from.Add(1 * time.Hour) + } + t := time.Date(base.Year(), base.Month(), base.Day(), h, m, 0, 0, loc) + if !t.After(from.In(loc)) { + t = t.Add(24 * time.Hour) + } + return t +} + +func nextFromCalendar(rrule string, start time.Time, from time.Time) time.Time { + rule := strings.ToUpper(strings.TrimSpace(rrule)) + if rule == "" { + if start.After(from) { + return start + } + return from.Add(24 * time.Hour) + } + + freq := "DAILY" + interval := 1 + for _, chunk := range strings.Split(rule, ";") { + parts := strings.SplitN(strings.TrimSpace(chunk), "=", 2) + if len(parts) != 2 { + continue + } + switch parts[0] { + case "FREQ": + freq = strings.TrimSpace(parts[1]) + case "INTERVAL": + if iv, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil && iv > 0 { + interval = iv + } + } + } + + next := start + if next.IsZero() { + next = from + } + for !next.After(from) { + switch freq { + case "HOURLY": + next = next.Add(time.Duration(interval) * time.Hour) + case "WEEKLY": + next = next.AddDate(0, 0, 7*interval) + case "MONTHLY": + next = next.AddDate(0, interval, 0) + default: + next = next.AddDate(0, 0, interval) + } + } + return next +} + +func (s *Service) CreateSchedule(input Schedule) (Schedule, error) { + if strings.TrimSpace(input.CompanyID) == "" || strings.TrimSpace(input.TargetAgentID) == "" { + return Schedule{}, fmt.Errorf("company_id and target_agent_id are required") + } + input.ScheduleType = strings.ToLower(strings.TrimSpace(input.ScheduleType)) + if input.ScheduleType == "" { + input.ScheduleType = "calendar" + } + if input.ScheduleType != "cron" && input.ScheduleType != "calendar" && input.ScheduleType != "once" { + return Schedule{}, fmt.Errorf("schedule_type must be cron, calendar, or once") + } + if strings.TrimSpace(input.Timezone) == "" { + input.Timezone = "UTC" + } + loc, err := time.LoadLocation(input.Timezone) + if err != nil { + loc = time.UTC + input.Timezone = "UTC" + } + if strings.TrimSpace(input.PayloadJSON) == "" { + input.PayloadJSON = `{"prompt":"Scheduled agent task"}` + } + if input.ID == "" { + input.ID = uuid.NewString() + } + + now := time.Now().In(loc) + start := parseAnyTime(input.StartAt, now) + nextRun := start + if input.ScheduleType == "once" { + if strings.TrimSpace(input.StartAt) == "" { + return Schedule{}, fmt.Errorf("start_at is required for schedule_type once") + } + nextRun = start + } else if input.ScheduleType == "cron" { + nextRun = nextFromCron(input.CronExpr, now, loc) + } else { + nextRun = nextFromCalendar(input.RRule, start, now) + } + + activeInt := 1 + if !input.IsActive && input.IsActive != true { + activeInt = 0 + } + + _, err = s.db.Exec(` + INSERT INTO schedules (id, company_id, department_id, target_agent_id, schedule_type, cron_expr, rrule, start_at, next_run_at, timezone, payload_json, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, input.ID, input.CompanyID, input.DepartmentID, input.TargetAgentID, input.ScheduleType, input.CronExpr, input.RRule, start.UTC().Format(time.RFC3339), nextRun.UTC().Format(time.RFC3339), input.Timezone, input.PayloadJSON, activeInt) + if err != nil { + return Schedule{}, err + } + s.appendAudit("schedule_created", "schedule", input.ID, "owner", map[string]interface{}{"company_id": input.CompanyID, "target_agent_id": input.TargetAgentID}) + s.emitEvent(input.CompanyID, input.DepartmentID, input.TargetAgentID, "", "", "schedule_created", "info", map[string]interface{}{"schedule_id": input.ID}) + return s.GetSchedule(input.ID) +} + +func (s *Service) GetSchedule(id string) (Schedule, error) { + var sch Schedule + var activeInt int + var createdAt, updatedAt string + err := s.db.QueryRow(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(target_agent_id,''), IFNULL(schedule_type,'calendar'), IFNULL(cron_expr,''), IFNULL(rrule,''), IFNULL(start_at,''), IFNULL(next_run_at,''), IFNULL(timezone,'UTC'), IFNULL(payload_json,'{}'), IFNULL(is_active,1), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM schedules WHERE id = ? + `, id).Scan(&sch.ID, &sch.CompanyID, &sch.DepartmentID, &sch.TargetAgentID, &sch.ScheduleType, &sch.CronExpr, &sch.RRule, &sch.StartAt, &sch.NextRunAt, &sch.Timezone, &sch.PayloadJSON, &activeInt, &createdAt, &updatedAt) + if err != nil { + return Schedule{}, err + } + sch.IsActive = activeInt == 1 + sch.CreatedAt = parseDBTime(createdAt) + sch.UpdatedAt = parseDBTime(updatedAt) + return sch, nil +} + +func (s *Service) ListSchedules(companyID, departmentID string) ([]Schedule, error) { + query := ` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(target_agent_id,''), IFNULL(schedule_type,'calendar'), IFNULL(cron_expr,''), IFNULL(rrule,''), IFNULL(start_at,''), IFNULL(next_run_at,''), IFNULL(timezone,'UTC'), IFNULL(payload_json,'{}'), IFNULL(is_active,1), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM schedules WHERE 1=1` + args := []interface{}{} + if strings.TrimSpace(companyID) != "" { + query += " AND company_id = ?" + args = append(args, companyID) + } + if strings.TrimSpace(departmentID) != "" { + query += " AND department_id = ?" + args = append(args, departmentID) + } + query += " ORDER BY next_run_at ASC" + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []Schedule{} + for rows.Next() { + var sch Schedule + var activeInt int + var createdAt, updatedAt string + if err := rows.Scan(&sch.ID, &sch.CompanyID, &sch.DepartmentID, &sch.TargetAgentID, &sch.ScheduleType, &sch.CronExpr, &sch.RRule, &sch.StartAt, &sch.NextRunAt, &sch.Timezone, &sch.PayloadJSON, &activeInt, &createdAt, &updatedAt); err != nil { + continue + } + sch.IsActive = activeInt == 1 + sch.CreatedAt = parseDBTime(createdAt) + sch.UpdatedAt = parseDBTime(updatedAt) + out = append(out, sch) + } + return out, nil +} + +func (s *Service) UpdateSchedule(id string, patch Schedule) (Schedule, error) { + cur, err := s.GetSchedule(id) + if err != nil { + return Schedule{}, err + } + if strings.TrimSpace(patch.ScheduleType) != "" { + cur.ScheduleType = strings.ToLower(strings.TrimSpace(patch.ScheduleType)) + } + if strings.TrimSpace(patch.CronExpr) != "" { + cur.CronExpr = patch.CronExpr + } + if strings.TrimSpace(patch.RRule) != "" { + cur.RRule = patch.RRule + } + if strings.TrimSpace(patch.StartAt) != "" { + cur.StartAt = patch.StartAt + } + if strings.TrimSpace(patch.Timezone) != "" { + cur.Timezone = patch.Timezone + } + if strings.TrimSpace(patch.PayloadJSON) != "" { + cur.PayloadJSON = patch.PayloadJSON + } + + loc, err := time.LoadLocation(cur.Timezone) + if err != nil { + loc = time.UTC + cur.Timezone = "UTC" + } + now := time.Now().In(loc) + start := parseAnyTime(cur.StartAt, now) + next := start + if cur.ScheduleType == "cron" { + next = nextFromCron(cur.CronExpr, now, loc) + } else { + next = nextFromCalendar(cur.RRule, start, now) + } + + _, err = s.db.Exec(` + UPDATE schedules + SET schedule_type = ?, cron_expr = ?, rrule = ?, start_at = ?, next_run_at = ?, timezone = ?, payload_json = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + `, cur.ScheduleType, cur.CronExpr, cur.RRule, start.UTC().Format(time.RFC3339), next.UTC().Format(time.RFC3339), cur.Timezone, cur.PayloadJSON, id) + if err != nil { + return Schedule{}, err + } + s.appendAudit("schedule_updated", "schedule", id, "owner", cur) + return s.GetSchedule(id) +} + +func (s *Service) ToggleSchedule(id string, enabled bool) error { + v := 0 + if enabled { + v = 1 + } + _, err := s.db.Exec("UPDATE schedules SET is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", v, id) + if err == nil { + s.appendAudit("schedule_toggled", "schedule", id, "owner", map[string]interface{}{"enabled": enabled}) + } + return err +} + +func (s *Service) runSchedules() { + now := time.Now().UTC() + rows, err := s.db.Query(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(target_agent_id,''), IFNULL(schedule_type,'calendar'), IFNULL(cron_expr,''), IFNULL(rrule,''), IFNULL(start_at,''), IFNULL(next_run_at,''), IFNULL(timezone,'UTC'), IFNULL(payload_json,'{}'), IFNULL(is_active,1), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM schedules + WHERE is_active = 1 AND next_run_at != '' AND next_run_at <= ? + ORDER BY next_run_at ASC + LIMIT 20 + `, now.Format(time.RFC3339)) + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var sch Schedule + var activeInt int + var createdAt, updatedAt string + if err := rows.Scan(&sch.ID, &sch.CompanyID, &sch.DepartmentID, &sch.TargetAgentID, &sch.ScheduleType, &sch.CronExpr, &sch.RRule, &sch.StartAt, &sch.NextRunAt, &sch.Timezone, &sch.PayloadJSON, &activeInt, &createdAt, &updatedAt); err != nil { + continue + } + sch.IsActive = activeInt == 1 + + payload := strings.TrimSpace(sch.PayloadJSON) + if payload == "" { + payload = `{"prompt":"Scheduled task"}` + } + task := AgentTask{ + ID: uuid.NewString(), + CompanyID: sch.CompanyID, + DepartmentID: sch.DepartmentID, + AgentID: sch.TargetAgentID, + RequestedBy: "scheduler", + Type: "scheduled", + Status: "queued", + Priority: 40, + InputJSON: payload, + } + createdTask, err := s.CreateTask(task) + if err != nil { + _, _ = s.db.Exec(`INSERT INTO schedule_runs (id, schedule_id, task_id, status, triggered_at, finished_at, error) VALUES (?, ?, '', 'failed', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?)`, uuid.NewString(), sch.ID, err.Error()) + s.emitEvent(sch.CompanyID, sch.DepartmentID, sch.TargetAgentID, "", "", "schedule_failed", "error", map[string]interface{}{"schedule_id": sch.ID, "error": err.Error()}) + continue + } + _, _ = s.db.Exec(`INSERT INTO schedule_runs (id, schedule_id, task_id, status, triggered_at, finished_at, error) VALUES (?, ?, ?, 'triggered', CURRENT_TIMESTAMP, NULL, '')`, uuid.NewString(), sch.ID, createdTask.ID) + + if sch.ScheduleType == "once" { + _, _ = s.db.Exec("UPDATE schedules SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?", sch.ID) + s.appendAudit("schedule_triggered", "schedule", sch.ID, "scheduler", map[string]interface{}{"task_id": createdTask.ID, "once": true}) + } else { + loc, err := time.LoadLocation(sch.Timezone) + if err != nil { + loc = time.UTC + } + next := now.In(loc) + if sch.ScheduleType == "cron" { + next = nextFromCron(sch.CronExpr, now.In(loc), loc) + } else { + start := parseAnyTime(sch.StartAt, now.In(loc)) + next = nextFromCalendar(sch.RRule, start, now.In(loc)) + } + _, _ = s.db.Exec("UPDATE schedules SET next_run_at = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", next.UTC().Format(time.RFC3339), sch.ID) + s.appendAudit("schedule_triggered", "schedule", sch.ID, "scheduler", map[string]interface{}{"task_id": createdTask.ID, "next_run_at": next.UTC().Format(time.RFC3339)}) + } + s.emitEvent(sch.CompanyID, sch.DepartmentID, sch.TargetAgentID, createdTask.ThreadID, createdTask.ID, "schedule_triggered", "info", map[string]interface{}{"schedule_id": sch.ID, "task_id": createdTask.ID}) + } +} diff --git a/dash/backend/agentos/service.go b/dash/backend/agentos/service.go new file mode 100644 index 0000000..71a412b --- /dev/null +++ b/dash/backend/agentos/service.go @@ -0,0 +1,1164 @@ +package agentos + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "log" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/google/uuid" +) + +type Service struct { + db *sql.DB + workspaceRoot string + + mu sync.Mutex + running map[string]struct{} + subscribers map[string]map[chan AgentEvent]struct{} + + queueTick time.Duration + schedulerTick time.Duration +} + +func NewService(db *sql.DB, workspaceRoot string) *Service { + return &Service{ + db: db, + workspaceRoot: workspaceRoot, + running: map[string]struct{}{}, + subscribers: map[string]map[chan AgentEvent]struct{}{}, + queueTick: 2 * time.Second, + schedulerTick: 1 * time.Minute, + } +} + +func (s *Service) Start(ctx context.Context) { + s.migrateLegacyChats() + + qTicker := time.NewTicker(s.queueTick) + sTicker := time.NewTicker(s.schedulerTick) + defer qTicker.Stop() + defer sTicker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-qTicker.C: + s.processQueue() + s.resumeBlockedManagers() + case <-sTicker.C: + s.runSchedules() + } + } +} + +func (s *Service) Subscribe(companyID string) (<-chan AgentEvent, func()) { + ch := make(chan AgentEvent, 128) + + s.mu.Lock() + if s.subscribers[companyID] == nil { + s.subscribers[companyID] = map[chan AgentEvent]struct{}{} + } + s.subscribers[companyID][ch] = struct{}{} + s.mu.Unlock() + + cancel := func() { + s.mu.Lock() + if subs := s.subscribers[companyID]; subs != nil { + delete(subs, ch) + if len(subs) == 0 { + delete(s.subscribers, companyID) + } + } + s.mu.Unlock() + close(ch) + } + + return ch, cancel +} + +func (s *Service) emitEvent(companyID, departmentID, agentID, threadID, taskID, eventType, severity string, payload interface{}) { + if strings.TrimSpace(severity) == "" { + severity = "info" + } + payloadJSON := "{}" + if payload != nil { + if b, err := json.Marshal(payload); err == nil { + payloadJSON = string(b) + } + } + + res, err := s.db.Exec(` + INSERT INTO agent_events (company_id, department_id, agent_id, thread_id, task_id, event_type, severity, payload_json, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) + `, companyID, departmentID, agentID, threadID, taskID, eventType, severity, payloadJSON) + if err != nil { + log.Printf("agentos event insert failed: %v", err) + return + } + id, _ := res.LastInsertId() + ev := AgentEvent{ + ID: id, + CompanyID: companyID, + DepartmentID: departmentID, + AgentID: agentID, + ThreadID: threadID, + TaskID: taskID, + EventType: eventType, + Severity: severity, + PayloadJSON: payloadJSON, + CreatedAt: time.Now(), + } + + s.mu.Lock() + for cid, subs := range s.subscribers { + if cid != "" && cid != companyID { + continue + } + for ch := range subs { + select { + case ch <- ev: + default: + } + } + } + s.mu.Unlock() +} + +func (s *Service) ListEvents(companyID, taskID string, sinceID int64, limit int) ([]AgentEvent, error) { + if limit <= 0 || limit > 500 { + limit = 200 + } + query := ` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(thread_id,''), IFNULL(task_id,''), IFNULL(event_type,''), IFNULL(severity,'info'), IFNULL(payload_json,'{}'), IFNULL(created_at,'') + FROM agent_events WHERE id > ?` + args := []interface{}{sinceID} + if strings.TrimSpace(companyID) != "" { + query += " AND company_id = ?" + args = append(args, companyID) + } + if strings.TrimSpace(taskID) != "" { + query += " AND task_id = ?" + args = append(args, taskID) + } + query += " ORDER BY id ASC LIMIT ?" + args = append(args, limit) + + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []AgentEvent{} + for rows.Next() { + var ev AgentEvent + var createdAt string + if err := rows.Scan(&ev.ID, &ev.CompanyID, &ev.DepartmentID, &ev.AgentID, &ev.ThreadID, &ev.TaskID, &ev.EventType, &ev.Severity, &ev.PayloadJSON, &createdAt); err != nil { + continue + } + ev.CreatedAt = parseDBTime(createdAt) + out = append(out, ev) + } + return out, nil +} + +func (s *Service) HealthStatus() map[string]interface{} { + var queued, running, blocked, waiting int + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status='queued'").Scan(&queued) + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status='running'").Scan(&running) + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status='blocked'").Scan(&blocked) + _ = s.db.QueryRow("SELECT COUNT(1) FROM agent_tasks WHERE status='waiting_input'").Scan(&waiting) + + return map[string]interface{}{ + "queue": map[string]interface{}{ + "queued": queued, + "running": running, + "blocked": blocked, + "waiting_input": waiting, + }, + "scheduler": s.SchedulerState(), + "audit": s.AuditVerify(), + "updated_at": time.Now().UTC().Format(time.RFC3339), + } +} + +func (s *Service) cleanWorkspacePath(raw string) string { + path := strings.TrimSpace(raw) + if path == "" { + return "" + } + if !filepath.IsAbs(path) && strings.TrimSpace(s.workspaceRoot) != "" { + path = filepath.Join(s.workspaceRoot, path) + } + if abs, err := filepath.Abs(path); err == nil { + path = abs + } + return filepath.Clean(path) +} + +func (s *Service) CreateCompany(ownerUserID, name, description, timezone, workspacePath, deployCommand string) (Company, error) { + ownerUserID = strings.TrimSpace(ownerUserID) + if ownerUserID == "" { + return Company{}, fmt.Errorf("owner_user_id is required") + } + name = strings.TrimSpace(name) + if name == "" { + return Company{}, fmt.Errorf("name is required") + } + if strings.TrimSpace(timezone) == "" { + timezone = "UTC" + } + workspacePath = s.cleanWorkspacePath(workspacePath) + deployCommand = strings.TrimSpace(deployCommand) + id := uuid.NewString() + slug := slugify(name + "-" + ownerUserID) + _, err := s.db.Exec(` + INSERT INTO companies (id, owner_user_id, name, slug, description, status, timezone, workspace_path, deploy_command, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, id, ownerUserID, name, slug, description, timezone, workspacePath, deployCommand) + if err != nil { + return Company{}, err + } + + dept, err := s.CreateDepartment(id, "General", "general", "Default department") + if err == nil { + _, _ = s.CreateAgent(id, dept.ID, "Core Agent", "manager", "", "Primary coordinator agent") + } + // Bootstrap allow rules so a newly created company is operational without manual policy seeding. + for _, action := range []string{"task_run", "delegate", "memory_write", "schedule_mutate", "hierarchy_mutate", "model_bind", "thread_write"} { + _, _ = s.UpsertPolicy(PolicyRule{ + CompanyID: id, + DepartmentID: "", + AgentID: "", + Action: action, + Effect: "allow", + ScopePattern: "*", + ApprovalTier: "none", + }) + } + + s.appendAudit("company_created", "company", id, "owner", map[string]interface{}{"name": name, "workspace_path": workspacePath}) + s.emitEvent(id, "", "", "", "", "company_created", "info", map[string]interface{}{"company_id": id, "name": name, "workspace_path": workspacePath}) + return s.GetCompany(id) +} + +func (s *Service) ListCompanies(ownerUserID string) ([]Company, error) { + ownerUserID = strings.TrimSpace(ownerUserID) + rows, err := s.db.Query(` + SELECT id, IFNULL(owner_user_id,''), IFNULL(name,''), IFNULL(slug,''), IFNULL(description,''), IFNULL(status,'active'), IFNULL(timezone,'UTC'), IFNULL(workspace_path,''), IFNULL(deploy_command,''), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM companies WHERE owner_user_id = ? ORDER BY created_at DESC`, ownerUserID) + if err != nil { + return nil, err + } + defer rows.Close() + + out := []Company{} + for rows.Next() { + var c Company + var createdAt, updatedAt string + if err := rows.Scan(&c.ID, &c.OwnerUserID, &c.Name, &c.Slug, &c.Description, &c.Status, &c.Timezone, &c.WorkspacePath, &c.DeployCommand, &createdAt, &updatedAt); err != nil { + continue + } + c.CreatedAt = parseDBTime(createdAt) + c.UpdatedAt = parseDBTime(updatedAt) + out = append(out, c) + } + return out, nil +} + +func (s *Service) GetCompany(id string) (Company, error) { + var c Company + var createdAt, updatedAt string + err := s.db.QueryRow(` + SELECT id, IFNULL(owner_user_id,''), IFNULL(name,''), IFNULL(slug,''), IFNULL(description,''), IFNULL(status,'active'), IFNULL(timezone,'UTC'), IFNULL(workspace_path,''), IFNULL(deploy_command,''), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM companies WHERE id = ? + `, id).Scan(&c.ID, &c.OwnerUserID, &c.Name, &c.Slug, &c.Description, &c.Status, &c.Timezone, &c.WorkspacePath, &c.DeployCommand, &createdAt, &updatedAt) + if err != nil { + return Company{}, err + } + c.CreatedAt = parseDBTime(createdAt) + c.UpdatedAt = parseDBTime(updatedAt) + return c, nil +} + +func (s *Service) UserOwnsCompany(ownerUserID, companyID string) bool { + ownerUserID = strings.TrimSpace(ownerUserID) + companyID = strings.TrimSpace(companyID) + if ownerUserID == "" || companyID == "" { + return false + } + var count int + _ = s.db.QueryRow(`SELECT COUNT(1) FROM companies WHERE id = ? AND owner_user_id = ?`, companyID, ownerUserID).Scan(&count) + return count > 0 +} + +func (s *Service) UpdateCompany(id, name, description, status, timezone, workspacePath, deployCommand string) (Company, error) { + current, err := s.GetCompany(id) + if err != nil { + return Company{}, err + } + if strings.TrimSpace(name) == "" { + name = current.Name + } + if strings.TrimSpace(status) == "" { + status = current.Status + } + if strings.TrimSpace(timezone) == "" { + timezone = current.Timezone + } + if strings.TrimSpace(workspacePath) == "" { + workspacePath = current.WorkspacePath + } else { + workspacePath = s.cleanWorkspacePath(workspacePath) + } + if strings.TrimSpace(deployCommand) == "" { + deployCommand = current.DeployCommand + } else { + deployCommand = strings.TrimSpace(deployCommand) + } + _, err = s.db.Exec(`UPDATE companies SET name = ?, slug = ?, description = ?, status = ?, timezone = ?, workspace_path = ?, deploy_command = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + name, slugify(name+"-"+current.OwnerUserID), description, status, timezone, workspacePath, deployCommand, id) + if err != nil { + return Company{}, err + } + s.appendAudit("company_updated", "company", id, "owner", map[string]interface{}{"name": name, "status": status, "workspace_path": workspacePath}) + return s.GetCompany(id) +} + +func (s *Service) CreateDepartment(companyID, name, depType, description string) (Department, error) { + if strings.TrimSpace(companyID) == "" || strings.TrimSpace(name) == "" { + return Department{}, fmt.Errorf("company_id and name are required") + } + if strings.TrimSpace(depType) == "" { + depType = "general" + } + id := uuid.NewString() + _, err := s.db.Exec(` + INSERT INTO departments (id, company_id, name, type, description, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, id, companyID, name, depType, description) + if err != nil { + return Department{}, err + } + s.appendAudit("department_created", "department", id, "owner", map[string]interface{}{"company_id": companyID, "name": name}) + s.emitEvent(companyID, id, "", "", "", "department_created", "info", map[string]interface{}{"department_id": id, "name": name}) + return s.GetDepartment(id) +} + +func (s *Service) ListDepartments(companyID string) ([]Department, error) { + query := `SELECT id, IFNULL(company_id,''), IFNULL(name,''), IFNULL(type,''), IFNULL(description,''), IFNULL(status,'active'), IFNULL(created_at,''), IFNULL(updated_at,'') FROM departments` + args := []interface{}{} + if strings.TrimSpace(companyID) != "" { + query += " WHERE company_id = ?" + args = append(args, companyID) + } + query += " ORDER BY created_at ASC" + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []Department{} + for rows.Next() { + var d Department + var createdAt, updatedAt string + if err := rows.Scan(&d.ID, &d.CompanyID, &d.Name, &d.Type, &d.Description, &d.Status, &createdAt, &updatedAt); err != nil { + continue + } + d.CreatedAt = parseDBTime(createdAt) + d.UpdatedAt = parseDBTime(updatedAt) + out = append(out, d) + } + return out, nil +} + +func (s *Service) GetDepartment(id string) (Department, error) { + var d Department + var createdAt, updatedAt string + err := s.db.QueryRow(`SELECT id, IFNULL(company_id,''), IFNULL(name,''), IFNULL(type,''), IFNULL(description,''), IFNULL(status,'active'), IFNULL(created_at,''), IFNULL(updated_at,'') FROM departments WHERE id = ?`, id). + Scan(&d.ID, &d.CompanyID, &d.Name, &d.Type, &d.Description, &d.Status, &createdAt, &updatedAt) + if err != nil { + return Department{}, err + } + d.CreatedAt = parseDBTime(createdAt) + d.UpdatedAt = parseDBTime(updatedAt) + return d, nil +} + +func (s *Service) UpdateDepartment(id, name, depType, description, status string) (Department, error) { + cur, err := s.GetDepartment(id) + if err != nil { + return Department{}, err + } + if strings.TrimSpace(name) == "" { + name = cur.Name + } + if strings.TrimSpace(depType) == "" { + depType = cur.Type + } + if strings.TrimSpace(description) == "" { + description = cur.Description + } + if strings.TrimSpace(status) == "" { + status = cur.Status + } + _, err = s.db.Exec(`UPDATE departments SET name = ?, type = ?, description = ?, status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, name, depType, description, status, id) + if err != nil { + return Department{}, err + } + s.appendAudit("department_updated", "department", id, "owner", map[string]interface{}{"name": name, "status": status}) + return s.GetDepartment(id) +} + +func (s *Service) CreateAgent(companyID, departmentID, name, roleType, parentAgentID, identityPrompt string) (Agent, error) { + if strings.TrimSpace(companyID) == "" || strings.TrimSpace(departmentID) == "" || strings.TrimSpace(name) == "" { + return Agent{}, fmt.Errorf("company_id, department_id and name are required") + } + roleType = strings.ToLower(strings.TrimSpace(roleType)) + if roleType == "" { + roleType = "worker" + } + if roleType != "manager" && roleType != "worker" { + return Agent{}, fmt.Errorf("role_type must be manager or worker") + } + id := uuid.NewString() + _, err := s.db.Exec(` + INSERT INTO agents (id, company_id, department_id, name, role_type, parent_agent_id, identity_prompt, status, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, 'idle', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, id, companyID, departmentID, name, roleType, parentAgentID, identityPrompt) + if err != nil { + return Agent{}, err + } + + profileID := uuid.NewString() + ownerUserID := "admin" + _ = s.db.QueryRow(`SELECT IFNULL(owner_user_id,'admin') FROM companies WHERE id = ?`, companyID).Scan(&ownerUserID) + defaultProvider := "openrouter" + defaultModel := s.getUserSettingString(ownerUserID, "default_model", "") + if strings.TrimSpace(defaultModel) == "" { + defaultProvider = "local" + defaultModel = "llama3.1:8b" + } + _, _ = s.db.Exec(`INSERT INTO agent_model_profiles (id, owner_user_id, provider, model, settings_json, fallback_chain_json, created_at, updated_at) + VALUES (?, ?, ?, ?, '{}', '["openrouter","local","cli_codex","cli_claude"]', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, profileID, ownerUserID, defaultProvider, defaultModel) + _, _ = s.db.Exec(`INSERT OR REPLACE INTO agent_model_bindings (agent_id, primary_profile_id, temperature, max_tokens, reasoning_effort) VALUES (?, ?, ?, ?, ?)`, id, profileID, 0.2, 1200, "standard") + + s.appendAudit("agent_created", "agent", id, "owner", map[string]interface{}{"name": name, "role_type": roleType}) + s.emitEvent(companyID, departmentID, id, "", "", "agent_created", "info", map[string]interface{}{"agent_id": id, "name": name}) + return s.GetAgent(id) +} + +func (s *Service) ListAgents(companyID, departmentID string) ([]Agent, error) { + query := `SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(name,''), IFNULL(role_type,'worker'), IFNULL(parent_agent_id,''), IFNULL(identity_prompt,''), IFNULL(status,'idle'), IFNULL(is_active,1), IFNULL(created_at,''), IFNULL(updated_at,'') FROM agents WHERE 1=1` + args := []interface{}{} + if strings.TrimSpace(companyID) != "" { + query += " AND company_id = ?" + args = append(args, companyID) + } + if strings.TrimSpace(departmentID) != "" { + query += " AND department_id = ?" + args = append(args, departmentID) + } + query += " ORDER BY created_at ASC" + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []Agent{} + for rows.Next() { + ag, err := scanAgent(rows) + if err != nil { + continue + } + out = append(out, ag) + } + return out, nil +} + +func (s *Service) GetAgent(id string) (Agent, error) { + row := s.db.QueryRow(`SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(name,''), IFNULL(role_type,'worker'), IFNULL(parent_agent_id,''), IFNULL(identity_prompt,''), IFNULL(status,'idle'), IFNULL(is_active,1), IFNULL(created_at,''), IFNULL(updated_at,'') FROM agents WHERE id = ?`, id) + return scanAgent(row) +} + +func (s *Service) UpdateAgent(id, name, roleType, parentAgentID, identityPrompt, status string, isActive *bool) (Agent, error) { + ag, err := s.GetAgent(id) + if err != nil { + return Agent{}, err + } + if strings.TrimSpace(name) == "" { + name = ag.Name + } + if strings.TrimSpace(roleType) == "" { + roleType = ag.RoleType + } + if strings.TrimSpace(identityPrompt) == "" { + identityPrompt = ag.IdentityPrompt + } + if strings.TrimSpace(status) == "" { + status = ag.Status + } + if parentAgentID == "" { + parentAgentID = ag.ParentAgentID + } + active := ag.IsActive + if isActive != nil { + active = *isActive + } + activeInt := 0 + if active { + activeInt = 1 + } + _, err = s.db.Exec(`UPDATE agents SET name = ?, role_type = ?, parent_agent_id = ?, identity_prompt = ?, status = ?, is_active = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, name, roleType, parentAgentID, identityPrompt, status, activeInt, id) + if err != nil { + return Agent{}, err + } + s.appendAudit("agent_updated", "agent", id, "owner", map[string]interface{}{"name": name, "status": status}) + return s.GetAgent(id) +} + +func (s *Service) AssignManager(agentID, managerID string) error { + if strings.TrimSpace(agentID) == "" || strings.TrimSpace(managerID) == "" { + return fmt.Errorf("agent_id and manager_id are required") + } + agent, err := s.GetAgent(agentID) + if err != nil { + return err + } + manager, err := s.GetAgent(managerID) + if err != nil { + return err + } + if manager.RoleType != "manager" { + return fmt.Errorf("parent must be a manager") + } + if agent.DepartmentID != manager.DepartmentID { + return fmt.Errorf("department-scoped hierarchy: manager and agent must belong to same department") + } + if agent.ID == manager.ID { + return fmt.Errorf("agent cannot manage itself") + } + if s.pathWouldCycle(agentID, managerID) { + return fmt.Errorf("hierarchy cycle detected") + } + _, err = s.db.Exec("UPDATE agents SET parent_agent_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", managerID, agentID) + if err == nil { + s.appendAudit("hierarchy_assigned", "agent", agentID, "owner", map[string]interface{}{"manager_id": managerID}) + s.emitEvent(agent.CompanyID, agent.DepartmentID, agentID, "", "", "hierarchy_assigned", "info", map[string]interface{}{"manager_id": managerID}) + } + return err +} + +func (s *Service) pathWouldCycle(agentID, managerID string) bool { + current := managerID + for i := 0; i < 256; i++ { + if current == "" { + return false + } + if current == agentID { + return true + } + var next string + err := s.db.QueryRow("SELECT IFNULL(parent_agent_id,'') FROM agents WHERE id = ?", current).Scan(&next) + if err != nil { + return false + } + current = next + } + return true +} + +func (s *Service) CreateThread(companyID, departmentID, agentID, title string) (AgentThread, error) { + if strings.TrimSpace(companyID) == "" || strings.TrimSpace(agentID) == "" { + return AgentThread{}, fmt.Errorf("company_id and agent_id are required") + } + if strings.TrimSpace(title) == "" { + title = "New Thread" + } + id := uuid.NewString() + _, err := s.db.Exec(`INSERT INTO agent_threads (id, company_id, department_id, agent_id, title, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, id, companyID, departmentID, agentID, title) + if err != nil { + return AgentThread{}, err + } + s.emitEvent(companyID, departmentID, agentID, id, "", "thread_created", "info", map[string]interface{}{"thread_id": id}) + return s.GetThread(id) +} + +func (s *Service) ListThreads(agentID string) ([]AgentThread, error) { + query := `SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(title,''), IFNULL(status,'active'), IFNULL(created_at,''), IFNULL(updated_at,'') FROM agent_threads` + args := []interface{}{} + if strings.TrimSpace(agentID) != "" { + query += " WHERE agent_id = ?" + args = append(args, agentID) + } + query += " ORDER BY updated_at DESC" + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []AgentThread{} + for rows.Next() { + th, err := scanThread(rows) + if err != nil { + continue + } + out = append(out, th) + } + return out, nil +} + +func (s *Service) GetThread(id string) (AgentThread, error) { + row := s.db.QueryRow(`SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(title,''), IFNULL(status,'active'), IFNULL(created_at,''), IFNULL(updated_at,'') FROM agent_threads WHERE id = ?`, id) + return scanThread(row) +} + +func (s *Service) ListThreadMessages(threadID string) ([]AgentMessage, error) { + rows, err := s.db.Query(`SELECT id, IFNULL(thread_id,''), IFNULL(role,''), IFNULL(content,''), IFNULL(content_type,'text'), IFNULL(created_at,'') FROM agent_messages WHERE thread_id = ? ORDER BY id ASC`, threadID) + if err != nil { + return nil, err + } + defer rows.Close() + out := []AgentMessage{} + for rows.Next() { + var m AgentMessage + var createdAt string + if err := rows.Scan(&m.ID, &m.ThreadID, &m.Role, &m.Content, &m.ContentType, &createdAt); err != nil { + continue + } + m.CreatedAt = parseDBTime(createdAt) + out = append(out, m) + } + return out, nil +} + +func (s *Service) AddThreadMessage(threadID, role, content, contentType string) (AgentMessage, error) { + if strings.TrimSpace(threadID) == "" || strings.TrimSpace(role) == "" { + return AgentMessage{}, fmt.Errorf("thread_id and role are required") + } + if strings.TrimSpace(contentType) == "" { + contentType = "text" + } + res, err := s.db.Exec(`INSERT INTO agent_messages (thread_id, role, content, content_type, text_embedding, created_at) VALUES (?, ?, ?, ?, '', CURRENT_TIMESTAMP)`, threadID, role, content, contentType) + if err != nil { + return AgentMessage{}, err + } + _, _ = s.db.Exec("UPDATE agent_threads SET updated_at = CURRENT_TIMESTAMP WHERE id = ?", threadID) + id, _ := res.LastInsertId() + row := s.db.QueryRow(`SELECT id, IFNULL(thread_id,''), IFNULL(role,''), IFNULL(content,''), IFNULL(content_type,'text'), IFNULL(created_at,'') FROM agent_messages WHERE id = ?`, id) + var m AgentMessage + var createdAt string + if err := row.Scan(&m.ID, &m.ThreadID, &m.Role, &m.Content, &m.ContentType, &createdAt); err != nil { + return AgentMessage{}, err + } + m.CreatedAt = parseDBTime(createdAt) + return m, nil +} + +func (s *Service) CreateTask(input AgentTask) (AgentTask, error) { + if strings.TrimSpace(input.CompanyID) == "" || strings.TrimSpace(input.AgentID) == "" { + return AgentTask{}, fmt.Errorf("company_id and agent_id are required") + } + if strings.TrimSpace(input.Type) == "" { + input.Type = "conversation" + } + if strings.TrimSpace(input.Status) == "" { + input.Status = "queued" + } + if input.Priority <= 0 { + input.Priority = 50 + } + if strings.TrimSpace(input.ID) == "" { + input.ID = uuid.NewString() + } + _, err := s.db.Exec(` + INSERT INTO agent_tasks (id, company_id, department_id, agent_id, requested_by, parent_task_id, thread_id, type, status, priority, input_json, result_json, blocked_reason, created_at, updated_at, completed_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, IFNULL(?,''), '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL) + `, input.ID, input.CompanyID, input.DepartmentID, input.AgentID, input.RequestedBy, input.ParentTaskID, input.ThreadID, input.Type, input.Status, input.Priority, input.InputJSON, input.ResultJSON) + if err != nil { + return AgentTask{}, err + } + s.emitEvent(input.CompanyID, input.DepartmentID, input.AgentID, input.ThreadID, input.ID, "task_created", "info", map[string]interface{}{"task_id": input.ID, "type": input.Type}) + return s.GetTask(input.ID) +} + +func (s *Service) GetTask(taskID string) (AgentTask, error) { + row := s.db.QueryRow(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(requested_by,''), IFNULL(parent_task_id,''), IFNULL(thread_id,''), IFNULL(type,''), IFNULL(status,''), IFNULL(priority,50), IFNULL(input_json,''), IFNULL(result_json,''), IFNULL(blocked_reason,''), IFNULL(created_at,''), IFNULL(updated_at,''), IFNULL(completed_at,'') + FROM agent_tasks WHERE id = ? + `, taskID) + return scanTask(row) +} + +func (s *Service) ListTasks(companyID, agentID, status string, limit int) ([]AgentTask, error) { + if limit <= 0 || limit > 400 { + limit = 120 + } + query := `SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(requested_by,''), IFNULL(parent_task_id,''), IFNULL(thread_id,''), IFNULL(type,''), IFNULL(status,''), IFNULL(priority,50), IFNULL(input_json,''), IFNULL(result_json,''), IFNULL(blocked_reason,''), IFNULL(created_at,''), IFNULL(updated_at,''), IFNULL(completed_at,'') FROM agent_tasks WHERE 1=1` + args := []interface{}{} + if strings.TrimSpace(companyID) != "" { + query += " AND company_id = ?" + args = append(args, companyID) + } + if strings.TrimSpace(agentID) != "" { + query += " AND agent_id = ?" + args = append(args, agentID) + } + if strings.TrimSpace(status) != "" { + query += " AND status = ?" + args = append(args, status) + } + query += " ORDER BY priority ASC, created_at ASC LIMIT ?" + args = append(args, limit) + rows, err := s.db.Query(query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + out := []AgentTask{} + for rows.Next() { + t, err := scanTask(rows) + if err != nil { + continue + } + out = append(out, t) + } + return out, nil +} + +func (s *Service) CancelTask(taskID string) error { + t, err := s.GetTask(taskID) + if err != nil { + return err + } + _, err = s.db.Exec("UPDATE agent_tasks SET status='canceled', completed_at=CURRENT_TIMESTAMP, updated_at=CURRENT_TIMESTAMP WHERE id = ?", taskID) + if err == nil { + s.emitEvent(t.CompanyID, t.DepartmentID, t.AgentID, t.ThreadID, t.ID, "task_canceled", "warn", map[string]interface{}{"task_id": taskID}) + s.appendAudit("task_canceled", "task", taskID, t.RequestedBy, nil) + } + return err +} + +func (s *Service) RetryTask(taskID string) error { + t, err := s.GetTask(taskID) + if err != nil { + return err + } + _, err = s.db.Exec("UPDATE agent_tasks SET status='queued', blocked_reason='', updated_at=CURRENT_TIMESTAMP, completed_at=NULL WHERE id = ?", taskID) + if err == nil { + s.emitEvent(t.CompanyID, t.DepartmentID, t.AgentID, t.ThreadID, t.ID, "task_retried", "info", map[string]interface{}{"task_id": taskID}) + s.appendAudit("task_retried", "task", taskID, t.RequestedBy, nil) + } + return err +} + +func (s *Service) ListRuns(taskID string) ([]AgentRun, error) { + rows, err := s.db.Query(`SELECT id, IFNULL(task_id,''), IFNULL(attempt,1), IFNULL(status,''), IFNULL(provider,''), IFNULL(model,''), IFNULL(started_at,''), IFNULL(ended_at,''), IFNULL(summary,''), IFNULL(error,'') FROM agent_runs WHERE task_id = ? ORDER BY started_at DESC`, taskID) + if err != nil { + return nil, err + } + defer rows.Close() + out := []AgentRun{} + for rows.Next() { + var r AgentRun + var startedAt, endedAt string + if err := rows.Scan(&r.ID, &r.TaskID, &r.Attempt, &r.Status, &r.Provider, &r.Model, &startedAt, &endedAt, &r.Summary, &r.Error); err != nil { + continue + } + r.StartedAt = parseDBTime(startedAt) + if strings.TrimSpace(endedAt) != "" { + t := parseDBTime(endedAt) + r.EndedAt = &t + } + out = append(out, r) + } + return out, nil +} + +func (s *Service) CreateModelProfile(ownerUserID, provider, model, settingsJSON, fallbackChainJSON string) (AgentModelProfile, error) { + ownerUserID = strings.TrimSpace(ownerUserID) + if ownerUserID == "" { + return AgentModelProfile{}, fmt.Errorf("owner_user_id is required") + } + if strings.TrimSpace(provider) == "" || strings.TrimSpace(model) == "" { + return AgentModelProfile{}, fmt.Errorf("provider and model are required") + } + if strings.TrimSpace(settingsJSON) == "" { + settingsJSON = "{}" + } + if strings.TrimSpace(fallbackChainJSON) == "" { + fallbackChainJSON = "[]" + } + id := uuid.NewString() + _, err := s.db.Exec(`INSERT INTO agent_model_profiles (id, owner_user_id, provider, model, settings_json, fallback_chain_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`, id, ownerUserID, provider, model, settingsJSON, fallbackChainJSON) + if err != nil { + return AgentModelProfile{}, err + } + return s.GetModelProfile(id) +} + +func (s *Service) ListModelProfiles(ownerUserID string) ([]AgentModelProfile, error) { + rows, err := s.db.Query(`SELECT id, IFNULL(owner_user_id,''), IFNULL(provider,''), IFNULL(model,''), IFNULL(settings_json,'{}'), IFNULL(fallback_chain_json,'[]'), IFNULL(created_at,''), IFNULL(updated_at,'') FROM agent_model_profiles WHERE owner_user_id = ? ORDER BY updated_at DESC`, ownerUserID) + if err != nil { + return nil, err + } + defer rows.Close() + out := []AgentModelProfile{} + for rows.Next() { + var p AgentModelProfile + var createdAt, updatedAt string + if err := rows.Scan(&p.ID, &p.OwnerUserID, &p.Provider, &p.Model, &p.SettingsJSON, &p.FallbackChainJSON, &createdAt, &updatedAt); err != nil { + continue + } + p.CreatedAt = parseDBTime(createdAt) + p.UpdatedAt = parseDBTime(updatedAt) + out = append(out, p) + } + return out, nil +} + +func (s *Service) GetModelProfile(id string) (AgentModelProfile, error) { + var p AgentModelProfile + var createdAt, updatedAt string + err := s.db.QueryRow(`SELECT id, IFNULL(owner_user_id,''), IFNULL(provider,''), IFNULL(model,''), IFNULL(settings_json,'{}'), IFNULL(fallback_chain_json,'[]'), IFNULL(created_at,''), IFNULL(updated_at,'') FROM agent_model_profiles WHERE id = ?`, id). + Scan(&p.ID, &p.OwnerUserID, &p.Provider, &p.Model, &p.SettingsJSON, &p.FallbackChainJSON, &createdAt, &updatedAt) + if err != nil { + return AgentModelProfile{}, err + } + p.CreatedAt = parseDBTime(createdAt) + p.UpdatedAt = parseDBTime(updatedAt) + return p, nil +} + +func (s *Service) BindAgentModel(agentID, profileID string, temperature float64, maxTokens int, reasoningEffort string) error { + if strings.TrimSpace(agentID) == "" || strings.TrimSpace(profileID) == "" { + return fmt.Errorf("agent_id and primary_profile_id are required") + } + if maxTokens <= 0 { + maxTokens = 1200 + } + if strings.TrimSpace(reasoningEffort) == "" { + reasoningEffort = "standard" + } + _, err := s.db.Exec(`INSERT OR REPLACE INTO agent_model_bindings (agent_id, primary_profile_id, temperature, max_tokens, reasoning_effort) VALUES (?, ?, ?, ?, ?)`, agentID, profileID, temperature, maxTokens, reasoningEffort) + if err == nil { + s.appendAudit("agent_model_bound", "agent", agentID, "owner", map[string]interface{}{"profile_id": profileID, "temperature": temperature}) + } + return err +} + +// GetAgentBinding is the public version of getBinding for use by handlers. +func (s *Service) GetAgentBinding(agentID string) (AgentModelBinding, AgentModelProfile, error) { + return s.getBinding(agentID) +} + +func (s *Service) getBinding(agentID string) (AgentModelBinding, AgentModelProfile, error) { + var b AgentModelBinding + err := s.db.QueryRow(`SELECT IFNULL(agent_id,''), IFNULL(primary_profile_id,''), IFNULL(temperature,0.2), IFNULL(max_tokens,1200), IFNULL(reasoning_effort,'standard') FROM agent_model_bindings WHERE agent_id = ?`, agentID). + Scan(&b.AgentID, &b.PrimaryProfileID, &b.Temperature, &b.MaxTokens, &b.ReasoningEffort) + if err != nil { + return AgentModelBinding{}, AgentModelProfile{}, err + } + p, err := s.GetModelProfile(b.PrimaryProfileID) + if err != nil { + return AgentModelBinding{}, AgentModelProfile{}, err + } + return b, p, nil +} + +func (s *Service) getSettingString(key, fallback string) string { + var value string + err := s.db.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&value) + if err != nil || strings.TrimSpace(value) == "" { + return fallback + } + return value +} + +// getUserSettingString reads from user_settings for a specific user; falls back to global settings. +func (s *Service) getUserSettingString(userID, key, fallback string) string { + if userID != "" && userID != "admin" { + var value string + err := s.db.QueryRow("SELECT value FROM user_settings WHERE user_id = ? AND key = ?", userID, key).Scan(&value) + if err == nil && strings.TrimSpace(value) != "" { + return value + } + } + return s.getSettingString(key, fallback) +} + +func parseDBTime(value string) time.Time { + value = strings.TrimSpace(value) + if value == "" { + return time.Time{} + } + formats := []string{time.RFC3339Nano, time.RFC3339, "2006-01-02 15:04:05", "2006-01-02T15:04:05"} + for _, f := range formats { + if t, err := time.Parse(f, value); err == nil { + return t + } + } + return time.Now() +} + +func slugify(value string) string { + v := strings.ToLower(strings.TrimSpace(value)) + if v == "" { + return "item" + } + v = strings.ReplaceAll(v, " ", "-") + out := make([]rune, 0, len(v)) + for _, r := range v { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' { + out = append(out, r) + } else { + out = append(out, '-') + } + } + res := strings.Trim(strings.ReplaceAll(string(out), "--", "-"), "-") + if res == "" { + return "item" + } + if len(res) > 64 { + return res[:64] + } + return res +} + +func scanAgent(scanner interface { + Scan(dest ...interface{}) error +}) (Agent, error) { + var ag Agent + var createdAt, updatedAt string + var isActive int + err := scanner.Scan(&ag.ID, &ag.CompanyID, &ag.DepartmentID, &ag.Name, &ag.RoleType, &ag.ParentAgentID, &ag.IdentityPrompt, &ag.Status, &isActive, &createdAt, &updatedAt) + if err != nil { + return Agent{}, err + } + ag.IsActive = isActive == 1 + ag.CreatedAt = parseDBTime(createdAt) + ag.UpdatedAt = parseDBTime(updatedAt) + return ag, nil +} + +func scanThread(scanner interface { + Scan(dest ...interface{}) error +}) (AgentThread, error) { + var t AgentThread + var createdAt, updatedAt string + err := scanner.Scan(&t.ID, &t.CompanyID, &t.DepartmentID, &t.AgentID, &t.Title, &t.Status, &createdAt, &updatedAt) + if err != nil { + return AgentThread{}, err + } + t.CreatedAt = parseDBTime(createdAt) + t.UpdatedAt = parseDBTime(updatedAt) + return t, nil +} + +func scanTask(scanner interface { + Scan(dest ...interface{}) error +}) (AgentTask, error) { + var t AgentTask + var createdAt, updatedAt, completedAt string + err := scanner.Scan(&t.ID, &t.CompanyID, &t.DepartmentID, &t.AgentID, &t.RequestedBy, &t.ParentTaskID, &t.ThreadID, &t.Type, &t.Status, &t.Priority, &t.InputJSON, &t.ResultJSON, &t.BlockedReason, &createdAt, &updatedAt, &completedAt) + if err != nil { + return AgentTask{}, err + } + t.CreatedAt = parseDBTime(createdAt) + t.UpdatedAt = parseDBTime(updatedAt) + if strings.TrimSpace(completedAt) != "" { + tm := parseDBTime(completedAt) + t.CompletedAt = &tm + } + return t, nil +} + +func (s *Service) migrateLegacyChats() { + var companiesCount int + _ = s.db.QueryRow("SELECT COUNT(1) FROM companies").Scan(&companiesCount) + if companiesCount > 0 { + return + } + + var legacyCount int + _ = s.db.QueryRow("SELECT COUNT(1) FROM chat_sessions").Scan(&legacyCount) + if legacyCount == 0 { + return + } + + log.Printf("agentos migration: migrating %d legacy chat sessions", legacyCount) + + settingsRaw := s.getSettingString("managed_projects", "[]") + var projects []struct { + Name string `json:"name"` + Path string `json:"path"` + } + _ = json.Unmarshal([]byte(settingsRaw), &projects) + + companyByPath := map[string]string{} + if len(projects) == 0 { + comp, err := s.CreateCompany("admin", "Migrated Workspace", "Auto-migrated from legacy chats", "UTC", "", "") + if err == nil { + companyByPath[""] = comp.ID + } + } else { + for _, p := range projects { + comp, err := s.CreateCompany("admin", p.Name, "Auto-migrated from managed project", "UTC", p.Path, "") + if err == nil { + companyByPath[strings.TrimSpace(p.Path)] = comp.ID + } + } + } + + rows, err := s.db.Query("SELECT id, IFNULL(title,''), IFNULL(project_path,'') FROM chat_sessions ORDER BY created_at ASC") + if err != nil { + return + } + defer rows.Close() + + for rows.Next() { + var sid, title, projectPath string + if err := rows.Scan(&sid, &title, &projectPath); err != nil { + continue + } + companyID := companyByPath[strings.TrimSpace(projectPath)] + if companyID == "" { + for _, cid := range companyByPath { + companyID = cid + break + } + } + if companyID == "" { + continue + } + depts, _ := s.ListDepartments(companyID) + if len(depts) == 0 { + continue + } + agents, _ := s.ListAgents(companyID, depts[0].ID) + if len(agents) == 0 { + continue + } + thread, err := s.CreateThread(companyID, depts[0].ID, agents[0].ID, title) + if err != nil { + continue + } + + msgRows, err := s.db.Query("SELECT role, content FROM chat_messages WHERE session_id = ? ORDER BY id ASC", sid) + if err == nil { + for msgRows.Next() { + var role, content string + if err := msgRows.Scan(&role, &content); err != nil { + continue + } + mappedRole := role + if role == "assistant" { + mappedRole = "agent" + } + _, _ = s.AddThreadMessage(thread.ID, mappedRole, content, "text") + } + msgRows.Close() + } + + _, _ = s.db.Exec("INSERT INTO legacy_migration_map (legacy_type, legacy_id, new_type, new_id, created_at) VALUES ('chat_session', ?, 'agent_thread', ?, CURRENT_TIMESTAMP)", sid, thread.ID) + } +} + +// GetOrCreateInterAgentThread returns the shared thread between two agents (or creates one). +func (s *Service) GetOrCreateInterAgentThread(companyID, agentA, agentB string) (AgentThread, error) { + var thread AgentThread + var createdAt, updatedAt string + err := s.db.QueryRow(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(title,''), IFNULL(status,''), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM agent_threads + WHERE company_id = ? AND title = ? AND status != 'archived' + LIMIT 1 + `, companyID, "inter:"+agentA+":"+agentB).Scan( + &thread.ID, &thread.CompanyID, &thread.DepartmentID, &thread.AgentID, + &thread.Title, &thread.Status, &createdAt, &updatedAt) + if err == nil { + thread.CreatedAt = parseDBTime(createdAt) + thread.UpdatedAt = parseDBTime(updatedAt) + return thread, nil + } + // Try reverse order too + err = s.db.QueryRow(` + SELECT id, IFNULL(company_id,''), IFNULL(department_id,''), IFNULL(agent_id,''), IFNULL(title,''), IFNULL(status,''), IFNULL(created_at,''), IFNULL(updated_at,'') + FROM agent_threads + WHERE company_id = ? AND title = ? AND status != 'archived' + LIMIT 1 + `, companyID, "inter:"+agentB+":"+agentA).Scan( + &thread.ID, &thread.CompanyID, &thread.DepartmentID, &thread.AgentID, + &thread.Title, &thread.Status, &createdAt, &updatedAt) + if err == nil { + thread.CreatedAt = parseDBTime(createdAt) + thread.UpdatedAt = parseDBTime(updatedAt) + return thread, nil + } + // Create new thread + id := uuid.NewString() + _, err = s.db.Exec(` + INSERT INTO agent_threads (id, company_id, department_id, agent_id, title, status, created_at, updated_at) + VALUES (?, ?, '', ?, ?, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + `, id, companyID, agentA, "inter:"+agentA+":"+agentB) + if err != nil { + return AgentThread{}, err + } + return s.GetThread(id) +} + +// PostInterAgentMessage sends a direct message from one agent to another. +func (s *Service) PostInterAgentMessage(companyID, fromAgentID, toAgentID, content string) error { + thread, err := s.GetOrCreateInterAgentThread(companyID, fromAgentID, toAgentID) + if err != nil { + return err + } + _, err = s.db.Exec(` + INSERT INTO agent_messages (thread_id, role, content, content_type, text_embedding, created_at) + VALUES (?, ?, ?, 'text', '', CURRENT_TIMESTAMP) + `, thread.ID, "agent:"+fromAgentID, content) + return err +} + +// GetAgentInbox returns all inter-agent threads for an agent (as sender or receiver). +func (s *Service) GetAgentInbox(companyID, agentID string) ([]map[string]interface{}, error) { + rows, err := s.db.Query(` + SELECT t.id, t.title, t.status, t.created_at, + m.role, m.content, m.created_at as msg_at + FROM agent_threads t + LEFT JOIN agent_messages m ON m.thread_id = t.id + WHERE t.company_id = ? AND (t.title LIKE ? OR t.title LIKE ?) + AND t.title LIKE 'inter:%' + ORDER BY m.created_at DESC + LIMIT 200 + `, companyID, "inter:"+agentID+":%", "inter:%:"+agentID) + if err != nil { + return nil, err + } + defer rows.Close() + var result []map[string]interface{} + for rows.Next() { + var threadID, title, status, createdAt, role, content, msgAt string + if err := rows.Scan(&threadID, &title, &status, &createdAt, &role, &content, &msgAt); err != nil { + continue + } + result = append(result, map[string]interface{}{ + "thread_id": threadID, + "title": title, + "role": role, + "content": content, + "created_at": msgAt, + }) + } + if result == nil { + result = []map[string]interface{}{} + } + return result, nil +} diff --git a/dash/backend/agentos/types.go b/dash/backend/agentos/types.go new file mode 100644 index 0000000..b9a9252 --- /dev/null +++ b/dash/backend/agentos/types.go @@ -0,0 +1,217 @@ +package agentos + +import "time" + +type Company struct { + ID string `json:"id"` + OwnerUserID string `json:"owner_user_id,omitempty"` + Name string `json:"name"` + Slug string `json:"slug"` + Description string `json:"description"` + Status string `json:"status"` + Timezone string `json:"timezone"` + WorkspacePath string `json:"workspace_path"` + DeployCommand string `json:"deploy_command"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Department struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Agent struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + Name string `json:"name"` + RoleType string `json:"role_type"` + ParentAgentID string `json:"parent_agent_id"` + IdentityPrompt string `json:"identity_prompt"` + Status string `json:"status"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type AgentModelProfile struct { + ID string `json:"id"` + OwnerUserID string `json:"owner_user_id,omitempty"` + Provider string `json:"provider"` + Model string `json:"model"` + SettingsJSON string `json:"settings_json"` + FallbackChainJSON string `json:"fallback_chain_json"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type AgentModelBinding struct { + AgentID string `json:"agent_id"` + PrimaryProfileID string `json:"primary_profile_id"` + Temperature float64 `json:"temperature"` + MaxTokens int `json:"max_tokens"` + ReasoningEffort string `json:"reasoning_effort"` +} + +type AgentThread struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + AgentID string `json:"agent_id"` + Title string `json:"title"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type AgentMessage struct { + ID int64 `json:"id"` + ThreadID string `json:"thread_id"` + Role string `json:"role"` + Content string `json:"content"` + ContentType string `json:"content_type"` + CreatedAt time.Time `json:"created_at"` +} + +type AgentTask struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + AgentID string `json:"agent_id"` + RequestedBy string `json:"requested_by"` + ParentTaskID string `json:"parent_task_id"` + ThreadID string `json:"thread_id"` + Type string `json:"type"` + Status string `json:"status"` + Priority int `json:"priority"` + InputJSON string `json:"input_json"` + ResultJSON string `json:"result_json"` + BlockedReason string `json:"blocked_reason"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + CompletedAt *time.Time `json:"completed_at,omitempty"` +} + +type AgentRun struct { + ID string `json:"id"` + TaskID string `json:"task_id"` + Attempt int `json:"attempt"` + Status string `json:"status"` + Provider string `json:"provider"` + Model string `json:"model"` + StartedAt time.Time `json:"started_at"` + EndedAt *time.Time `json:"ended_at,omitempty"` + Summary string `json:"summary"` + Error string `json:"error"` +} + +type AgentDelegation struct { + ID string `json:"id"` + ParentTaskID string `json:"parent_task_id"` + FromAgentID string `json:"from_agent_id"` + ToAgentID string `json:"to_agent_id"` + Instruction string `json:"instruction"` + Status string `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type Schedule struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + TargetAgentID string `json:"target_agent_id"` + ScheduleType string `json:"schedule_type"` + CronExpr string `json:"cron_expr"` + RRule string `json:"rrule"` + StartAt string `json:"start_at"` + NextRunAt string `json:"next_run_at"` + Timezone string `json:"timezone"` + PayloadJSON string `json:"payload_json"` + IsActive bool `json:"is_active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type MemoryEntry struct { + ID string `json:"id"` + ScopeType string `json:"scope_type"` + ScopeID string `json:"scope_id"` + SourceType string `json:"source_type"` + AuthorAgentID string `json:"author_agent_id"` + Content string `json:"content"` + Embedding string `json:"embedding"` + TagsJSON string `json:"tags_json"` + Importance float64 `json:"importance"` + TTLAt string `json:"ttl_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ConsensusRound struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + Topic string `json:"topic"` + Status string `json:"status"` + CreatedBy string `json:"created_by"` + CreatedAt time.Time `json:"created_at"` + ClosedAt string `json:"closed_at"` +} + +type ConsensusVote struct { + ID string `json:"id"` + RoundID string `json:"round_id"` + AgentID string `json:"agent_id"` + Option string `json:"option"` + Confidence float64 `json:"confidence"` + Rationale string `json:"rationale"` + CreatedAt time.Time `json:"created_at"` +} + +type PolicyRule struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + AgentID string `json:"agent_id"` + Action string `json:"action"` + Effect string `json:"effect"` + ScopePattern string `json:"scope_pattern"` + ApprovalTier string `json:"approval_tier"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ApprovalRequest struct { + ID string `json:"id"` + CompanyID string `json:"company_id"` + TaskID string `json:"task_id"` + Action string `json:"action"` + Tier string `json:"tier"` + Reason string `json:"reason"` + PayloadJSON string `json:"payload_json"` + Status string `json:"status"` + RequestedAt string `json:"requested_at"` + ResolvedAt string `json:"resolved_at"` + ResolvedBy string `json:"resolved_by"` +} + +type AgentEvent struct { + ID int64 `json:"id"` + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + AgentID string `json:"agent_id"` + ThreadID string `json:"thread_id"` + TaskID string `json:"task_id"` + EventType string `json:"event_type"` + Severity string `json:"severity"` + PayloadJSON string `json:"payload_json"` + CreatedAt time.Time `json:"created_at"` +} diff --git a/dash/backend/build.sh b/dash/backend/build.sh new file mode 100755 index 0000000..0111565 --- /dev/null +++ b/dash/backend/build.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Unified build script for Apollo Dashboard + +# Ensure we are in the script's directory (dash/backend) +cd "$(dirname "$0")" + +echo "----------------------------------------" +echo "🚀 Starting Full Production Build..." +echo "----------------------------------------" + +# 1. Build Backend for Linux (AMD64 + ARM64) using Zig cross-compiler +# This avoids macOS Mach-O binaries being deployed to Linux servers. +if ! command -v zig >/dev/null 2>&1; then + echo "❌ Error: 'zig' is required for Linux cross-builds with cgo." + echo "Install it once via: brew install zig" + exit 1 +fi + +echo "🔨 Building Backend (Linux AMD64)..." +CC='zig cc -target x86_64-linux-gnu' \ +CXX='zig c++ -target x86_64-linux-gnu' \ +CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \ +go build -o apollo-dash-linux-amd64 . +if [ $? -ne 0 ]; then + echo "❌ Error: Backend AMD64 build failed." + exit 1 +fi +echo "✅ Success: apollo-dash-linux-amd64 created." + +echo "🔨 Building Backend (Linux ARM64)..." +CC='zig cc -target aarch64-linux-gnu' \ +CXX='zig c++ -target aarch64-linux-gnu' \ +CGO_ENABLED=1 GOOS=linux GOARCH=arm64 \ +go build -o apollo-dash-linux-arm64 . +if [ $? -ne 0 ]; then + echo "❌ Error: Backend ARM64 build failed." + exit 1 +fi +echo "✅ Success: apollo-dash-linux-arm64 created." + +# 1b. Create runtime launcher expected by systemd at /opt/apollo-dash/backend/apollo-dash +cat > apollo-dash <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +ARCH="$(uname -m)" +BASE_DIR="$(cd "$(dirname "$0")" && pwd)" +case "$ARCH" in + x86_64|amd64) + exec "$BASE_DIR/apollo-dash-linux-amd64" "$@" + ;; + aarch64|arm64) + exec "$BASE_DIR/apollo-dash-linux-arm64" "$@" + ;; + *) + echo "Unsupported architecture: $ARCH" >&2 + exit 1 + ;; +esac +EOF +chmod +x apollo-dash +echo "✅ Success: launcher 'apollo-dash' created." + +# 2. Build Frontend Production Bundle +echo "🌐 Building Frontend..." +cd ../frontend +# We run npm run build to generate the dist/ folder +# Use npm install only if needed, but usually assume environment is ready +npm run build +if [ $? -eq 0 ]; then + echo "✅ Success: Frontend built." +else + echo "❌ Error: Frontend build failed." + exit 1 +fi + +echo "----------------------------------------" +echo "🎉 Build Complete!" +echo "Next step: Sync to your Ubuntu server using rsync (CAUTION: EXCLUDING .db TO PREVENT DATA LOSS):" +echo "rsync -avz --exclude 'frontend/node_modules' --exclude '*.db' dash/ dan@apollo:/opt/apollo-dash/" +echo "----------------------------------------" diff --git a/dash/backend/db/billing.go b/dash/backend/db/billing.go new file mode 100644 index 0000000..325c630 --- /dev/null +++ b/dash/backend/db/billing.go @@ -0,0 +1,36 @@ +package db + +import "time" + +func CreateBillingSession(id, userID, paymentRequestID string) error { + _, err := DB.Exec( + `INSERT INTO billing_sessions (id, user_id, payment_request_id, status) VALUES (?, ?, ?, 'pending')`, + id, userID, paymentRequestID, + ) + return err +} + +func GetBillingSessionByPaymentRequestID(paymentRequestID string) (userID string, err error) { + err = DB.QueryRow( + `SELECT user_id FROM billing_sessions WHERE payment_request_id = ?`, paymentRequestID, + ).Scan(&userID) + return +} + +func ConfirmBillingSession(paymentRequestID string) error { + _, err := DB.Exec( + `UPDATE billing_sessions SET status = 'confirmed', updated_at = CURRENT_TIMESTAMP WHERE payment_request_id = ?`, + paymentRequestID, + ) + return err +} + +// ActivateProPlan upgrades a user to pro for 30 days and resets renewal warning flag. +func ActivateProPlan(userID string) error { + endsAt := time.Now().UTC().AddDate(0, 0, 30).Format("2006-01-02 15:04:05") + _, err := DB.Exec( + `UPDATE users SET plan = 'pro', subscription_ends_at = ?, renewal_warning_sent = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + endsAt, userID, + ) + return err +} diff --git a/dash/backend/db/database.go b/dash/backend/db/database.go new file mode 100644 index 0000000..09ac183 --- /dev/null +++ b/dash/backend/db/database.go @@ -0,0 +1,569 @@ +package db + +import ( + "database/sql" + "log" + "os" + "path/filepath" + + _ "modernc.org/sqlite" +) + +var DB *sql.DB + +func InitDB() { + // Create database locally in backend dir + dbPath := filepath.Join(".", "apollo.db") + var err error + + // Create file if it doesn't exist + if _, err := os.Stat(dbPath); os.IsNotExist(err) { + file, err := os.Create(dbPath) + if err != nil { + log.Fatal(err) + } + file.Close() + } + + DB, err = sql.Open("sqlite", dbPath) + if err != nil { + log.Fatalf("Error opening database: %v\n", err) + } + // Single connection: serialises all reads+writes at the Go level. + // Eliminates SQLITE_BUSY entirely without WAL (which causes stale reads on a single conn). + DB.SetMaxOpenConns(1) + + createTables() +} + +func createTables() { + createVpsHostsTable := ` + CREATE TABLE IF NOT EXISTS vps_hosts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + ip TEXT NOT NULL UNIQUE + );` + + createChatSessionsTable := ` + CREATE TABLE IF NOT EXISTS chat_sessions ( + id TEXT PRIMARY KEY, /* UUID */ + title TEXT NOT NULL, + model TEXT DEFAULT '', + project_path TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createChatMessagesTable := ` + CREATE TABLE IF NOT EXISTS chat_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + role TEXT NOT NULL, /* 'user' or 'assistant' */ + content TEXT NOT NULL, + vector_embedding TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE + );` + + createSettingsTable := ` + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + value TEXT NOT NULL + );` + + createPersonalityFactsTable := ` + CREATE TABLE IF NOT EXISTS personality_facts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + fact TEXT NOT NULL UNIQUE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + // Retry on write contention instead of immediately failing with SQLITE_BUSY + DB.Exec("PRAGMA busy_timeout = 5000;") + + // Enable foreign keys + _, err := DB.Exec("PRAGMA foreign_keys = ON;") + if err != nil { + log.Fatal("Could not enable foreign keys: ", err) + } + + createSubagentsTable := ` + CREATE TABLE IF NOT EXISTS subagents ( + id TEXT PRIMARY KEY, + session_id TEXT, + name TEXT NOT NULL, + task TEXT NOT NULL, + status TEXT DEFAULT 'running', + output TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createCompaniesTable := ` + CREATE TABLE IF NOT EXISTS companies ( + id TEXT PRIMARY KEY, + owner_user_id TEXT DEFAULT '', + name TEXT NOT NULL, + slug TEXT NOT NULL, + description TEXT DEFAULT '', + status TEXT DEFAULT 'active', + timezone TEXT DEFAULT 'UTC', + workspace_path TEXT DEFAULT '', + deploy_command TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createDepartmentsTable := ` + CREATE TABLE IF NOT EXISTS departments ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + name TEXT NOT NULL, + type TEXT DEFAULT 'general', + description TEXT DEFAULT '', + status TEXT DEFAULT 'active', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(company_id) REFERENCES companies(id) ON DELETE CASCADE + );` + + createAgentsTable := ` + CREATE TABLE IF NOT EXISTS agents ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + department_id TEXT NOT NULL, + name TEXT NOT NULL, + role_type TEXT DEFAULT 'worker', + parent_agent_id TEXT DEFAULT '', + identity_prompt TEXT DEFAULT '', + status TEXT DEFAULT 'idle', + is_active INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(company_id) REFERENCES companies(id) ON DELETE CASCADE, + FOREIGN KEY(department_id) REFERENCES departments(id) ON DELETE CASCADE + );` + + createAgentModelProfilesTable := ` + CREATE TABLE IF NOT EXISTS agent_model_profiles ( + id TEXT PRIMARY KEY, + owner_user_id TEXT DEFAULT '', + provider TEXT NOT NULL, + model TEXT NOT NULL, + settings_json TEXT DEFAULT '{}', + fallback_chain_json TEXT DEFAULT '[]', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createAgentModelBindingsTable := ` + CREATE TABLE IF NOT EXISTS agent_model_bindings ( + agent_id TEXT PRIMARY KEY, + primary_profile_id TEXT NOT NULL, + temperature REAL DEFAULT 0.2, + max_tokens INTEGER DEFAULT 1200, + reasoning_effort TEXT DEFAULT 'standard', + FOREIGN KEY(agent_id) REFERENCES agents(id) ON DELETE CASCADE, + FOREIGN KEY(primary_profile_id) REFERENCES agent_model_profiles(id) ON DELETE CASCADE + );` + + createAgentThreadsTable := ` + CREATE TABLE IF NOT EXISTS agent_threads ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + department_id TEXT DEFAULT '', + agent_id TEXT NOT NULL, + title TEXT NOT NULL, + status TEXT DEFAULT 'active', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(company_id) REFERENCES companies(id) ON DELETE CASCADE, + FOREIGN KEY(agent_id) REFERENCES agents(id) ON DELETE CASCADE + );` + + createAgentMessagesTable := ` + CREATE TABLE IF NOT EXISTS agent_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + thread_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL, + content_type TEXT DEFAULT 'text', + text_embedding TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(thread_id) REFERENCES agent_threads(id) ON DELETE CASCADE + );` + + createAgentTasksTable := ` + CREATE TABLE IF NOT EXISTS agent_tasks ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + department_id TEXT DEFAULT '', + agent_id TEXT NOT NULL, + requested_by TEXT DEFAULT '', + parent_task_id TEXT DEFAULT '', + thread_id TEXT DEFAULT '', + type TEXT DEFAULT 'conversation', + status TEXT NOT NULL, + priority INTEGER DEFAULT 50, + input_json TEXT DEFAULT '', + result_json TEXT DEFAULT '', + blocked_reason TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + completed_at DATETIME NULL, + FOREIGN KEY(company_id) REFERENCES companies(id) ON DELETE CASCADE, + FOREIGN KEY(agent_id) REFERENCES agents(id) ON DELETE CASCADE + );` + + createAgentRunsTable := ` + CREATE TABLE IF NOT EXISTS agent_runs ( + id TEXT PRIMARY KEY, + task_id TEXT NOT NULL, + attempt INTEGER DEFAULT 1, + status TEXT NOT NULL, + provider TEXT DEFAULT '', + model TEXT DEFAULT '', + started_at DATETIME DEFAULT CURRENT_TIMESTAMP, + ended_at DATETIME NULL, + summary TEXT DEFAULT '', + error TEXT DEFAULT '', + FOREIGN KEY(task_id) REFERENCES agent_tasks(id) ON DELETE CASCADE + );` + + createAgentDelegationsTable := ` + CREATE TABLE IF NOT EXISTS agent_delegations ( + id TEXT PRIMARY KEY, + parent_task_id TEXT NOT NULL, + from_agent_id TEXT NOT NULL, + to_agent_id TEXT NOT NULL, + instruction TEXT DEFAULT '', + status TEXT DEFAULT 'queued', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(parent_task_id) REFERENCES agent_tasks(id) ON DELETE CASCADE + );` + + createAgentConsensusRoundsTable := ` + CREATE TABLE IF NOT EXISTS agent_consensus_rounds ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + department_id TEXT DEFAULT '', + topic TEXT NOT NULL, + status TEXT DEFAULT 'open', + created_by TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + closed_at TEXT DEFAULT '' + );` + + createAgentConsensusVotesTable := ` + CREATE TABLE IF NOT EXISTS agent_consensus_votes ( + id TEXT PRIMARY KEY, + round_id TEXT NOT NULL, + agent_id TEXT NOT NULL, + option TEXT NOT NULL, + confidence REAL DEFAULT 0.5, + rationale TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(round_id) REFERENCES agent_consensus_rounds(id) ON DELETE CASCADE + );` + + createAgentEventsTable := ` + CREATE TABLE IF NOT EXISTS agent_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + company_id TEXT DEFAULT '', + department_id TEXT DEFAULT '', + agent_id TEXT DEFAULT '', + thread_id TEXT DEFAULT '', + task_id TEXT DEFAULT '', + event_type TEXT NOT NULL, + severity TEXT DEFAULT 'info', + payload_json TEXT DEFAULT '{}', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createMemoryEntriesTable := ` + CREATE TABLE IF NOT EXISTS memory_entries ( + id TEXT PRIMARY KEY, + scope_type TEXT NOT NULL, + scope_id TEXT NOT NULL, + source_type TEXT DEFAULT '', + author_agent_id TEXT DEFAULT '', + content TEXT NOT NULL, + embedding TEXT DEFAULT '', + tags_json TEXT DEFAULT '{}', + importance REAL DEFAULT 0.5, + ttl_at TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createMemoryLinksTable := ` + CREATE TABLE IF NOT EXISTS memory_links ( + id TEXT PRIMARY KEY, + from_entry_id TEXT NOT NULL, + to_entry_id TEXT NOT NULL, + relation_type TEXT DEFAULT '', + score REAL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createKnowledgeAssetsTable := ` + CREATE TABLE IF NOT EXISTS knowledge_assets ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + department_id TEXT DEFAULT '', + path_or_uri TEXT NOT NULL, + asset_type TEXT DEFAULT '', + status TEXT DEFAULT 'indexed', + hash TEXT DEFAULT '', + metadata_json TEXT DEFAULT '{}', + indexed_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createKnowledgeIndexChunksTable := ` + CREATE TABLE IF NOT EXISTS knowledge_index_chunks ( + id TEXT PRIMARY KEY, + asset_id TEXT NOT NULL, + chunk_text TEXT NOT NULL, + embedding TEXT DEFAULT '', + symbols_json TEXT DEFAULT '{}', + cluster_id TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(asset_id) REFERENCES knowledge_assets(id) ON DELETE CASCADE + );` + + createSchedulesTable := ` + CREATE TABLE IF NOT EXISTS schedules ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + department_id TEXT DEFAULT '', + target_agent_id TEXT NOT NULL, + schedule_type TEXT NOT NULL, + cron_expr TEXT DEFAULT '', + rrule TEXT DEFAULT '', + start_at TEXT DEFAULT '', + next_run_at TEXT DEFAULT '', + timezone TEXT DEFAULT 'UTC', + payload_json TEXT DEFAULT '{}', + is_active INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createScheduleRunsTable := ` + CREATE TABLE IF NOT EXISTS schedule_runs ( + id TEXT PRIMARY KEY, + schedule_id TEXT NOT NULL, + task_id TEXT DEFAULT '', + status TEXT NOT NULL, + triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP, + finished_at DATETIME NULL, + error TEXT DEFAULT '', + FOREIGN KEY(schedule_id) REFERENCES schedules(id) ON DELETE CASCADE + );` + + createCapabilityPoliciesTable := ` + CREATE TABLE IF NOT EXISTS capability_policies ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + department_id TEXT DEFAULT '', + agent_id TEXT DEFAULT '', + action TEXT NOT NULL, + effect TEXT NOT NULL, + scope_pattern TEXT DEFAULT '*', + approval_tier TEXT DEFAULT 'none', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createApprovalRequestsTable := ` + CREATE TABLE IF NOT EXISTS approval_requests ( + id TEXT PRIMARY KEY, + company_id TEXT NOT NULL, + task_id TEXT DEFAULT '', + action TEXT NOT NULL, + tier TEXT NOT NULL, + reason TEXT DEFAULT '', + payload_json TEXT DEFAULT '{}', + status TEXT DEFAULT 'pending', + requested_at DATETIME DEFAULT CURRENT_TIMESTAMP, + resolved_at DATETIME NULL, + resolved_by TEXT DEFAULT '' + );` + + createAuditLogTable := ` + CREATE TABLE IF NOT EXISTS audit_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_type TEXT NOT NULL, + entity_type TEXT NOT NULL, + entity_id TEXT NOT NULL, + actor_type TEXT DEFAULT '', + actor_id TEXT DEFAULT '', + payload_json TEXT NOT NULL, + prev_hash TEXT DEFAULT '', + event_hash TEXT NOT NULL, + signature TEXT DEFAULT '', + pubkey_id TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createLegacyMigrationMapTable := ` + CREATE TABLE IF NOT EXISTS legacy_migration_map ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + legacy_type TEXT NOT NULL, + legacy_id TEXT NOT NULL, + new_type TEXT NOT NULL, + new_id TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createUsersTable := ` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + name TEXT DEFAULT '', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + );` + + createUserTokensTable := ` + CREATE TABLE IF NOT EXISTS user_tokens ( + token TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + );` + + for _, stmt := range []string{ + createVpsHostsTable, + createChatSessionsTable, + createChatMessagesTable, + createSettingsTable, + createPersonalityFactsTable, + createSubagentsTable, + createCompaniesTable, + createDepartmentsTable, + createAgentsTable, + createAgentModelProfilesTable, + createAgentModelBindingsTable, + createAgentThreadsTable, + createAgentMessagesTable, + createAgentTasksTable, + createAgentRunsTable, + createAgentDelegationsTable, + createAgentConsensusRoundsTable, + createAgentConsensusVotesTable, + createAgentEventsTable, + createMemoryEntriesTable, + createMemoryLinksTable, + createKnowledgeAssetsTable, + createKnowledgeIndexChunksTable, + createSchedulesTable, + createScheduleRunsTable, + createCapabilityPoliciesTable, + createApprovalRequestsTable, + createAuditLogTable, + createLegacyMigrationMapTable, + createUsersTable, + createUserTokensTable, + } { + if _, err := DB.Exec(stmt); err != nil { + log.Fatalf("Error creating table: %v\n", err) + } + } + + // AgentOS indexes + DB.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_companies_slug ON companies(slug)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_companies_owner_user_id ON companies(owner_user_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_departments_company_id ON departments(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agents_company_id ON agents(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agents_department_id ON agents(department_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agents_parent_agent_id ON agents(parent_agent_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_model_profiles_owner_user_id ON agent_model_profiles(owner_user_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_threads_agent_id ON agent_threads(agent_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_threads_company_id ON agent_threads(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_messages_thread_id ON agent_messages(thread_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_tasks_company_id ON agent_tasks(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_tasks_agent_id ON agent_tasks(agent_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_tasks_parent_task_id ON agent_tasks(parent_task_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_runs_task_id ON agent_runs(task_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_delegations_parent_task_id ON agent_delegations(parent_task_id)") + DB.Exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_consensus_votes_round_agent ON agent_consensus_votes(round_id, agent_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_events_company_id_id ON agent_events(company_id, id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_agent_events_task_id_id ON agent_events(task_id, id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_memory_entries_scope ON memory_entries(scope_type, scope_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_memory_entries_created_at ON memory_entries(created_at)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_knowledge_assets_company_id ON knowledge_assets(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_asset_id ON knowledge_index_chunks(asset_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_schedules_company_id ON schedules(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_schedules_next_run_at ON schedules(next_run_at)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_schedule_runs_schedule_id ON schedule_runs(schedule_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_capability_policies_company_id ON capability_policies(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_capability_policies_action ON capability_policies(action)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_approval_requests_company_id ON approval_requests(company_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_approval_requests_task_id ON approval_requests(task_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log(created_at)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_legacy_migration_map_legacy ON legacy_migration_map(legacy_type, legacy_id)") + + // Schema migrations for existing databases + DB.Exec("ALTER TABLE chat_sessions ADD COLUMN model TEXT DEFAULT ''") + DB.Exec("ALTER TABLE chat_sessions ADD COLUMN project_path TEXT DEFAULT ''") + DB.Exec("ALTER TABLE chat_sessions ADD COLUMN reasoning_effort TEXT DEFAULT 'None'") + DB.Exec("ALTER TABLE chat_sessions ADD COLUMN execution_mode TEXT DEFAULT 'Plan'") + DB.Exec("ALTER TABLE chat_messages ADD COLUMN vector_embedding TEXT DEFAULT ''") + DB.Exec("ALTER TABLE companies ADD COLUMN workspace_path TEXT DEFAULT ''") + DB.Exec("ALTER TABLE companies ADD COLUMN deploy_command TEXT DEFAULT ''") + DB.Exec("ALTER TABLE companies ADD COLUMN owner_user_id TEXT DEFAULT ''") + DB.Exec("UPDATE companies SET owner_user_id = 'admin' WHERE TRIM(IFNULL(owner_user_id,'')) = ''") + DB.Exec("ALTER TABLE agent_model_profiles ADD COLUMN owner_user_id TEXT DEFAULT ''") + DB.Exec("UPDATE agent_model_profiles SET owner_user_id = 'admin' WHERE TRIM(IFNULL(owner_user_id,'')) = ''") + + // AgentOS defaults + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_enabled', 'true')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_policy_enforcement', 'deny_default')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_mode', 'adaptive')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_min_workers', '1')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_max_workers', '4')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_min_reviewers', '1')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_max_reviewers', '2')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_cpu_soft', '70')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_cpu_hard', '90')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_ram_soft', '75')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_scheduler_ram_hard', '90')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_audit_signing_key_path', './agentos_ed25519.key')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_audit_pubkey_id', 'local-ed25519')") + DB.Exec("INSERT OR IGNORE INTO settings (key, value) VALUES ('agentos_kill_switch', 'false')") + + // User feature migrations + DB.Exec("ALTER TABLE users ADD COLUMN openrouter_api_key TEXT DEFAULT ''") + DB.Exec("ALTER TABLE users ADD COLUMN trial_ends_at TEXT DEFAULT ''") + DB.Exec("UPDATE users SET trial_ends_at = datetime(created_at, '+3 days') WHERE TRIM(IFNULL(trial_ends_at,'')) = ''") + DB.Exec("ALTER TABLE users ADD COLUMN is_blocked INTEGER DEFAULT 0") + DB.Exec("ALTER TABLE users ADD COLUMN plan TEXT DEFAULT 'trial'") + DB.Exec("ALTER TABLE users ADD COLUMN subscription_ends_at TEXT DEFAULT ''") + DB.Exec("ALTER TABLE users ADD COLUMN trial_warning_sent INTEGER DEFAULT 0") + DB.Exec("ALTER TABLE users ADD COLUMN renewal_warning_sent INTEGER DEFAULT 0") + + // Billing sessions + DB.Exec(`CREATE TABLE IF NOT EXISTS billing_sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + payment_request_id TEXT NOT NULL UNIQUE, + status TEXT DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`) + DB.Exec("CREATE INDEX IF NOT EXISTS idx_billing_sessions_user_id ON billing_sessions(user_id)") + DB.Exec("CREATE INDEX IF NOT EXISTS idx_billing_sessions_payment_request_id ON billing_sessions(payment_request_id)") + + // Per-user settings (overrides global settings per user) + DB.Exec(`CREATE TABLE IF NOT EXISTS user_settings ( + user_id TEXT NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY (user_id, key) + )`) +} diff --git a/dash/backend/db/users.go b/dash/backend/db/users.go new file mode 100644 index 0000000..51dcd5d --- /dev/null +++ b/dash/backend/db/users.go @@ -0,0 +1,311 @@ +package db + +import ( + "database/sql" + "time" +) + +type User struct { + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + OpenrouterAPIKey string `json:"openrouter_api_key,omitempty"` + TrialEndsAt string `json:"trial_ends_at,omitempty"` + IsBlocked int `json:"is_blocked"` + Plan string `json:"plan"` + SubscriptionEndsAt string `json:"subscription_ends_at,omitempty"` + CreatedAt time.Time `json:"created_at"` +} + +type UserAdminView struct { + User + CompanyCount int `json:"company_count"` + AgentCount int `json:"agent_count"` + TaskCount int `json:"task_count"` +} + +func scanUser(row *sql.Row) (*User, error) { + u := &User{} + var createdAtStr string + err := row.Scan( + &u.ID, &u.Email, &u.Name, + &u.OpenrouterAPIKey, &u.TrialEndsAt, + &u.IsBlocked, &u.Plan, &u.SubscriptionEndsAt, + &createdAtStr, + ) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + u.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAtStr) + return u, nil +} + +// NOTE: prefix with table alias (e.g. "u.") when used in a JOIN to avoid ambiguous column errors. +// The "created_at" at the end must be qualified at the call site when there are other tables in the query. +const userSelectCols = `id, email, name, + COALESCE(openrouter_api_key,''), COALESCE(trial_ends_at,''), + COALESCE(is_blocked,0), COALESCE(plan,'trial'), COALESCE(subscription_ends_at,''), + created_at` + +// userSelectColsQualified is the same as userSelectCols but with all columns qualified with alias "u". +const userSelectColsQualified = `u.id, u.email, u.name, + COALESCE(u.openrouter_api_key,''), COALESCE(u.trial_ends_at,''), + COALESCE(u.is_blocked,0), COALESCE(u.plan,'trial'), COALESCE(u.subscription_ends_at,''), + u.created_at` + +func CreateUser(id, email, passwordHash, name string) (*User, error) { + _, err := DB.Exec( + `INSERT INTO users (id, email, password_hash, name, trial_ends_at, plan) + VALUES (?, ?, ?, ?, datetime('now', '+3 days'), 'trial')`, + id, email, passwordHash, name, + ) + if err != nil { + return nil, err + } + return GetUserByID(id) +} + +func GetUserByEmail(email string) (id, emailOut, passwordHash, name string, isBlocked int, err error) { + row := DB.QueryRow( + `SELECT id, email, password_hash, name, COALESCE(is_blocked,0) FROM users WHERE email = ?`, email, + ) + err = row.Scan(&id, &emailOut, &passwordHash, &name, &isBlocked) + return +} + +func GetUserByID(id string) (*User, error) { + row := DB.QueryRow(`SELECT `+userSelectCols+` FROM users WHERE id = ?`, id) + return scanUser(row) +} + +func CreateUserToken(token, userID string) error { + _, err := DB.Exec( + `INSERT INTO user_tokens (token, user_id) VALUES (?, ?)`, + token, userID, + ) + return err +} + +func GetUserByToken(token string) (*User, error) { + row := DB.QueryRow( + `SELECT `+userSelectColsQualified+` + FROM user_tokens t + JOIN users u ON u.id = t.user_id + WHERE t.token = ?`, token, + ) + return scanUser(row) +} + +func DeleteUserToken(token string) error { + _, err := DB.Exec(`DELETE FROM user_tokens WHERE token = ?`, token) + return err +} + +func UpdateUserName(id, name string) error { + _, err := DB.Exec( + `UPDATE users SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + name, id, + ) + return err +} + +func UpdateUserOpenrouterKey(id, key string) error { + _, err := DB.Exec( + `UPDATE users SET openrouter_api_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + key, id, + ) + return err +} + +func UpdateUserPassword(id, hash string) error { + _, err := DB.Exec( + `UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + hash, id, + ) + return err +} + +func GetUserOpenrouterKey(id string) string { + var key string + DB.QueryRow(`SELECT COALESCE(openrouter_api_key,'') FROM users WHERE id = ?`, id).Scan(&key) + return key +} + +func GetUserPasswordHash(id string) (string, error) { + var hash string + err := DB.QueryRow(`SELECT password_hash FROM users WHERE id = ?`, id).Scan(&hash) + return hash, err +} + +func EmailExists(email string) bool { + var count int + DB.QueryRow(`SELECT COUNT(1) FROM users WHERE email = ?`, email).Scan(&count) + return count > 0 +} + +func DeleteUser(id string) error { + _, err := DB.Exec(`DELETE FROM users WHERE id = ?`, id) + return err +} + +func SetUserBlocked(id string, blocked bool) error { + val := 0 + if blocked { + val = 1 + } + _, err := DB.Exec( + `UPDATE users SET is_blocked = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + val, id, + ) + return err +} + +func SetUserPlan(id, plan, subscriptionEndsAt string) error { + _, err := DB.Exec( + `UPDATE users SET plan = ?, subscription_ends_at = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, + plan, subscriptionEndsAt, id, + ) + return err +} + +func ListUsersAdmin() ([]UserAdminView, error) { + rows, err := DB.Query(` + SELECT ` + userSelectColsQualified + `, + (SELECT COUNT(*) FROM companies c WHERE c.owner_user_id = u.id) AS company_count, + (SELECT COUNT(*) FROM agents a JOIN companies c ON a.company_id = c.id WHERE c.owner_user_id = u.id) AS agent_count, + (SELECT COUNT(*) FROM agent_tasks t JOIN companies c ON t.company_id = c.id WHERE c.owner_user_id = u.id) AS task_count + FROM users u + ORDER BY u.created_at DESC + `) + if err != nil { + return nil, err + } + defer rows.Close() + + var users []UserAdminView + for rows.Next() { + var u UserAdminView + var createdAtStr string + if err := rows.Scan( + &u.ID, &u.Email, &u.Name, + &u.OpenrouterAPIKey, &u.TrialEndsAt, + &u.IsBlocked, &u.Plan, &u.SubscriptionEndsAt, + &createdAtStr, + &u.CompanyCount, &u.AgentCount, &u.TaskCount, + ); err != nil { + continue + } + u.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAtStr) + u.OpenrouterAPIKey = "" // never expose in admin list + users = append(users, u) + } + if users == nil { + users = []UserAdminView{} + } + return users, nil +} + +// UserNotifyRow is used by the email notifier background job. +type UserNotifyRow struct { + ID string + Email string + Name string + Plan string + TrialEndsAt string + SubscriptionEndsAt string + TrialWarningSent int + RenewalWarningSent int +} + +// UsersNeedingTrialWarning returns trial users whose trial ends within 28 hours +// and haven't been sent a warning yet. +func UsersNeedingTrialWarning() ([]UserNotifyRow, error) { + rows, err := DB.Query(` + SELECT id, email, COALESCE(name,''), COALESCE(plan,'trial'), + COALESCE(trial_ends_at,''), COALESCE(subscription_ends_at,''), + COALESCE(trial_warning_sent,0), COALESCE(renewal_warning_sent,0) + FROM users + WHERE plan = 'trial' + AND is_blocked = 0 + AND TRIM(IFNULL(trial_ends_at,'')) != '' + AND datetime(trial_ends_at) > datetime('now') + AND datetime(trial_ends_at) <= datetime('now', '+28 hours') + AND COALESCE(trial_warning_sent,0) = 0 + `) + return scanNotifyRows(rows, err) +} + +// UsersNeedingRenewalWarning returns pro users whose subscription ends within 4 days +// and haven't been sent a renewal warning yet. +func UsersNeedingRenewalWarning() ([]UserNotifyRow, error) { + rows, err := DB.Query(` + SELECT id, email, COALESCE(name,''), COALESCE(plan,'trial'), + COALESCE(trial_ends_at,''), COALESCE(subscription_ends_at,''), + COALESCE(trial_warning_sent,0), COALESCE(renewal_warning_sent,0) + FROM users + WHERE plan = 'pro' + AND is_blocked = 0 + AND TRIM(IFNULL(subscription_ends_at,'')) != '' + AND datetime(subscription_ends_at) > datetime('now') + AND datetime(subscription_ends_at) <= datetime('now', '+4 days') + AND COALESCE(renewal_warning_sent,0) = 0 + `) + return scanNotifyRows(rows, err) +} + +func scanNotifyRows(rows interface { + Scan(...interface{}) error + Next() bool + Close() error +}, err error) ([]UserNotifyRow, error) { + if err != nil { + return nil, err + } + defer rows.Close() + var out []UserNotifyRow + for rows.Next() { + var r UserNotifyRow + if e := rows.Scan(&r.ID, &r.Email, &r.Name, &r.Plan, &r.TrialEndsAt, &r.SubscriptionEndsAt, &r.TrialWarningSent, &r.RenewalWarningSent); e != nil { + continue + } + out = append(out, r) + } + return out, nil +} + +func MarkTrialWarningSent(id string) { + DB.Exec(`UPDATE users SET trial_warning_sent = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, id) +} + +func MarkRenewalWarningSent(id string) { + DB.Exec(`UPDATE users SET renewal_warning_sent = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, id) +} + +type PlatformStats struct { + TotalUsers int `json:"total_users"` + ActiveTrials int `json:"active_trials"` + PaidUsers int `json:"paid_users"` + BlockedUsers int `json:"blocked_users"` + TotalCompanies int `json:"total_companies"` + TotalDepartments int `json:"total_departments"` + TotalAgents int `json:"total_agents"` + TotalTasks int `json:"total_tasks"` + TotalMemories int `json:"total_memories"` +} + +func GetPlatformStats() PlatformStats { + var s PlatformStats + DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&s.TotalUsers) + DB.QueryRow(`SELECT COUNT(*) FROM users WHERE plan = 'trial' AND is_blocked = 0`).Scan(&s.ActiveTrials) + DB.QueryRow(`SELECT COUNT(*) FROM users WHERE plan = 'pro' AND is_blocked = 0`).Scan(&s.PaidUsers) + DB.QueryRow(`SELECT COUNT(*) FROM users WHERE is_blocked = 1`).Scan(&s.BlockedUsers) + DB.QueryRow(`SELECT COUNT(*) FROM companies`).Scan(&s.TotalCompanies) + DB.QueryRow(`SELECT COUNT(*) FROM departments`).Scan(&s.TotalDepartments) + DB.QueryRow(`SELECT COUNT(*) FROM agents`).Scan(&s.TotalAgents) + DB.QueryRow(`SELECT COUNT(*) FROM agent_tasks`).Scan(&s.TotalTasks) + DB.QueryRow(`SELECT COUNT(*) FROM memory_entries`).Scan(&s.TotalMemories) + return s +} diff --git a/dash/backend/go.mod b/dash/backend/go.mod new file mode 100644 index 0000000..f6098cd --- /dev/null +++ b/dash/backend/go.mod @@ -0,0 +1,40 @@ +module github.com/danilrybalkin/apollo-dash + +go 1.25.6 + +require ( + github.com/creack/pty v1.1.24 + github.com/go-ping/ping v1.2.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/joho/godotenv v1.5.1 + github.com/microcosm-cc/bluemonday v1.0.27 + github.com/shirou/gopsutil/v3 v3.24.5 + github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 + gonum.org/v1/gonum v0.17.0 + modernc.org/sqlite v1.46.1 +) + +require ( + github.com/aymerick/douceur v0.2.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.45.0 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/dash/backend/go.sum b/dash/backend/go.sum new file mode 100644 index 0000000..f032a58 --- /dev/null +++ b/dash/backend/go.sum @@ -0,0 +1,121 @@ +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ping/ping v1.2.0 h1:vsJ8slZBZAXNCK4dPcI2PEE9eM9n9RbXbGouVQ/Y4yQ= +github.com/go-ping/ping v1.2.0/go.mod h1:xIFjORFzTxqIV/tDVGO4eDy/bLuSyawEeojSm3GfRGk= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4= +github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/dash/backend/handlers/admin.go b/dash/backend/handlers/admin.go new file mode 100644 index 0000000..a6575e3 --- /dev/null +++ b/dash/backend/handlers/admin.go @@ -0,0 +1,99 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/danilrybalkin/apollo-dash/db" +) + +func requireAdmin(w http.ResponseWriter, r *http.Request) bool { + uid := CurrentUserID(r) + if uid != "admin" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "admin only"}) + return false + } + return true +} + +// GET /api/admin/stats +func AdminStatsHandler(w http.ResponseWriter, r *http.Request) { + if !requireAdmin(w, r) { + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(db.GetPlatformStats()) +} + +// GET /api/admin/users +func AdminUsersHandler(w http.ResponseWriter, r *http.Request) { + if !requireAdmin(w, r) { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + users, err := db.ListUsersAdmin() + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(users) +} + +// PATCH /api/admin/users/{id} — update plan, block status +// DELETE /api/admin/users/{id} — delete user +func AdminUserByIDHandler(w http.ResponseWriter, r *http.Request) { + if !requireAdmin(w, r) { + return + } + // Extract user ID from path: /api/admin/users/ + parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/admin/users/"), "/") + userID := parts[0] + if userID == "" { + http.Error(w, "user id required", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodPatch: + var body struct { + Plan string `json:"plan"` + SubscriptionEndsAt string `json:"subscription_ends_at"` + IsBlocked *bool `json:"is_blocked"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if body.Plan != "" { + if err := db.SetUserPlan(userID, body.Plan, body.SubscriptionEndsAt); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + } + if body.IsBlocked != nil { + if err := db.SetUserBlocked(userID, *body.IsBlocked); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + } + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) + + case http.MethodDelete: + if err := db.DeleteUser(userID); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}) + + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/dash/backend/handlers/agentos.go b/dash/backend/handlers/agentos.go new file mode 100644 index 0000000..c4438bb --- /dev/null +++ b/dash/backend/handlers/agentos.go @@ -0,0 +1,1520 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strconv" + "strings" + "time" + + "github.com/danilrybalkin/apollo-dash/agentos" +) + +var agentOSService *agentos.Service + +func SetAgentOSService(svc *agentos.Service) { + agentOSService = svc +} + +func ensureAgentOSAvailable(w http.ResponseWriter) bool { + if agentOSService == nil { + http.Error(w, "agentos is not initialized", http.StatusServiceUnavailable) + return false + } + if !getSettingBool("agentos_enabled", true) { + http.Error(w, "agentos is disabled in settings", http.StatusServiceUnavailable) + return false + } + return true +} + +func writeJSON(w http.ResponseWriter, status int, payload interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(payload) +} + +func requireTenant(w http.ResponseWriter, r *http.Request) (string, bool) { + userID := strings.TrimSpace(CurrentUserID(r)) + if userID == "" { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return "", false + } + return userID, true +} + +func requireCompanyAccess(w http.ResponseWriter, ownerUserID, companyID string) bool { + if strings.TrimSpace(companyID) == "" { + http.Error(w, "company_id is required", http.StatusBadRequest) + return false + } + if !agentOSService.UserOwnsCompany(ownerUserID, companyID) { + http.Error(w, "not found", http.StatusNotFound) + return false + } + return true +} + +func requireDepartmentAccess(w http.ResponseWriter, ownerUserID, departmentID string) (agentos.Department, bool) { + item, err := agentOSService.GetDepartment(departmentID) + if err != nil || !agentOSService.UserOwnsCompany(ownerUserID, item.CompanyID) { + http.Error(w, "not found", http.StatusNotFound) + return agentos.Department{}, false + } + return item, true +} + +func requireAgentAccess(w http.ResponseWriter, ownerUserID, agentID string) (agentos.Agent, bool) { + item, err := agentOSService.GetAgent(agentID) + if err != nil || !agentOSService.UserOwnsCompany(ownerUserID, item.CompanyID) { + http.Error(w, "not found", http.StatusNotFound) + return agentos.Agent{}, false + } + return item, true +} + +func requireThreadAccess(w http.ResponseWriter, ownerUserID, threadID string) (agentos.AgentThread, bool) { + item, err := agentOSService.GetThread(threadID) + if err != nil || !agentOSService.UserOwnsCompany(ownerUserID, item.CompanyID) { + http.Error(w, "not found", http.StatusNotFound) + return agentos.AgentThread{}, false + } + return item, true +} + +func requireTaskAccess(w http.ResponseWriter, ownerUserID, taskID string) (agentos.AgentTask, bool) { + item, err := agentOSService.GetTask(taskID) + if err != nil || !agentOSService.UserOwnsCompany(ownerUserID, item.CompanyID) { + http.Error(w, "not found", http.StatusNotFound) + return agentos.AgentTask{}, false + } + return item, true +} + +func requireScheduleAccess(w http.ResponseWriter, ownerUserID, scheduleID string) (agentos.Schedule, bool) { + item, err := agentOSService.GetSchedule(scheduleID) + if err != nil || !agentOSService.UserOwnsCompany(ownerUserID, item.CompanyID) { + http.Error(w, "not found", http.StatusNotFound) + return agentos.Schedule{}, false + } + return item, true +} + +func AgentOSCompaniesHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + items, err := agentOSService.ListCompanies(ownerUserID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + Name string `json:"name"` + Description string `json:"description"` + Timezone string `json:"timezone"` + WorkspacePath string `json:"workspace_path"` + DeployCommand string `json:"deploy_command"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + item, err := agentOSService.CreateCompany(ownerUserID, req.Name, req.Description, req.Timezone, req.WorkspacePath, req.DeployCommand) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSCompanyByIDHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + id := strings.TrimPrefix(r.URL.Path, "/api/companies/") + id = strings.TrimSpace(strings.Trim(id, "/")) + if id == "" { + http.Error(w, "company id is required", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, id) { + return + } + switch r.Method { + case http.MethodGet: + item, err := agentOSService.GetCompany(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, item) + case http.MethodPatch: + var req struct { + Name string `json:"name"` + Description string `json:"description"` + Status string `json:"status"` + Timezone string `json:"timezone"` + WorkspacePath string `json:"workspace_path"` + DeployCommand string `json:"deploy_command"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + item, err := agentOSService.UpdateCompany(id, req.Name, req.Description, req.Status, req.Timezone, req.WorkspacePath, req.DeployCommand) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSDepartmentsHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if companyID != "" && !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + if companyID == "" { + companies, err := agentOSService.ListCompanies(ownerUserID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var all []agentos.Department + for _, company := range companies { + items, err := agentOSService.ListDepartments(company.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + all = append(all, items...) + } + writeJSON(w, http.StatusOK, all) + return + } + items, err := agentOSService.ListDepartments(companyID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + CompanyID string `json:"company_id"` + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + item, err := agentOSService.CreateDepartment(req.CompanyID, req.Name, req.Type, req.Description) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSDepartmentByIDHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + id := strings.TrimPrefix(r.URL.Path, "/api/departments/") + id = strings.TrimSpace(strings.Trim(id, "/")) + if id == "" { + http.Error(w, "department id is required", http.StatusBadRequest) + return + } + if _, ok := requireDepartmentAccess(w, ownerUserID, id); !ok { + return + } + switch r.Method { + case http.MethodGet: + item, err := agentOSService.GetDepartment(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, item) + case http.MethodPatch: + var req struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Status string `json:"status"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + item, err := agentOSService.UpdateDepartment(id, req.Name, req.Type, req.Description, req.Status) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSAgentsHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + departmentID := strings.TrimSpace(r.URL.Query().Get("department_id")) + if companyID != "" && !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + if departmentID != "" { + dept, ok := requireDepartmentAccess(w, ownerUserID, departmentID) + if !ok { + return + } + if companyID == "" { + companyID = dept.CompanyID + } + } + if companyID == "" { + companies, err := agentOSService.ListCompanies(ownerUserID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var all []agentos.Agent + for _, company := range companies { + items, err := agentOSService.ListAgents(company.ID, departmentID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + all = append(all, items...) + } + writeJSON(w, http.StatusOK, all) + return + } + items, err := agentOSService.ListAgents(companyID, departmentID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + Name string `json:"name"` + RoleType string `json:"role_type"` + ParentAgentID string `json:"parent_agent_id"` + IdentityPrompt string `json:"identity_prompt"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + dept, ok := requireDepartmentAccess(w, ownerUserID, req.DepartmentID) + if !ok { + return + } + if dept.CompanyID != req.CompanyID { + http.Error(w, "department does not belong to company", http.StatusBadRequest) + return + } + if strings.TrimSpace(req.ParentAgentID) != "" { + parent, ok := requireAgentAccess(w, ownerUserID, req.ParentAgentID) + if !ok { + return + } + if parent.CompanyID != req.CompanyID { + http.Error(w, "parent agent does not belong to company", http.StatusBadRequest) + return + } + } + item, err := agentOSService.CreateAgent(req.CompanyID, req.DepartmentID, req.Name, req.RoleType, req.ParentAgentID, req.IdentityPrompt) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSAgentByIDHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + path := strings.TrimPrefix(r.URL.Path, "/api/agents/") + path = strings.Trim(path, "/") + if path == "" { + http.Error(w, "agent id is required", http.StatusBadRequest) + return + } + parts := strings.Split(path, "/") + id := parts[0] + if _, ok := requireAgentAccess(w, ownerUserID, id); !ok { + return + } + + if len(parts) == 3 && parts[1] == "hierarchy" && parts[2] == "assign" { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + ManagerID string `json:"manager_id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.ManagerID == "" { + http.Error(w, "manager_id is required", http.StatusBadRequest) + return + } + if _, ok := requireAgentAccess(w, ownerUserID, req.ManagerID); !ok { + return + } + if err := agentOSService.AssignManager(id, req.ManagerID); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) + return + } + if len(parts) == 2 && parts[1] == "model-bind" { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + PrimaryProfileID string `json:"primary_profile_id"` + Temperature float64 `json:"temperature"` + MaxTokens int `json:"max_tokens"` + ReasoningEffort string `json:"reasoning_effort"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.PrimaryProfileID) == "" { + http.Error(w, "primary_profile_id is required", http.StatusBadRequest) + return + } + profile, err := agentOSService.GetModelProfile(req.PrimaryProfileID) + if err != nil || profile.OwnerUserID != ownerUserID { + http.Error(w, "model profile not found", http.StatusNotFound) + return + } + if err := agentOSService.BindAgentModel(id, req.PrimaryProfileID, req.Temperature, req.MaxTokens, req.ReasoningEffort); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) + return + } + + switch r.Method { + case http.MethodGet: + item, err := agentOSService.GetAgent(id) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, item) + case http.MethodPatch: + var req struct { + Name string `json:"name"` + RoleType string `json:"role_type"` + ParentAgentID string `json:"parent_agent_id"` + IdentityPrompt string `json:"identity_prompt"` + Status string `json:"status"` + IsActive *bool `json:"is_active"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if strings.TrimSpace(req.ParentAgentID) != "" { + if _, ok := requireAgentAccess(w, ownerUserID, req.ParentAgentID); !ok { + return + } + } + item, err := agentOSService.UpdateAgent(id, req.Name, req.RoleType, req.ParentAgentID, req.IdentityPrompt, req.Status, req.IsActive) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSModelProfilesHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + items, err := agentOSService.ListModelProfiles(ownerUserID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + Provider string `json:"provider"` + Model string `json:"model"` + SettingsJSON string `json:"settings_json"` + FallbackChainJSON string `json:"fallback_chain_json"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + item, err := agentOSService.CreateModelProfile(ownerUserID, req.Provider, req.Model, req.SettingsJSON, req.FallbackChainJSON) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSThreadsHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + agentID := strings.TrimSpace(r.URL.Query().Get("agent_id")) + if agentID == "" { + writeJSON(w, http.StatusOK, []agentos.AgentThread{}) + return + } + if _, ok := requireAgentAccess(w, ownerUserID, agentID); !ok { + return + } + items, err := agentOSService.ListThreads(agentID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + AgentID string `json:"agent_id"` + Title string `json:"title"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + if strings.TrimSpace(req.DepartmentID) != "" { + dept, ok := requireDepartmentAccess(w, ownerUserID, req.DepartmentID) + if !ok { + return + } + if dept.CompanyID != req.CompanyID { + http.Error(w, "department does not belong to company", http.StatusBadRequest) + return + } + } + agent, ok := requireAgentAccess(w, ownerUserID, req.AgentID) + if !ok { + return + } + if agent.CompanyID != req.CompanyID { + http.Error(w, "agent does not belong to company", http.StatusBadRequest) + return + } + item, err := agentOSService.CreateThread(req.CompanyID, req.DepartmentID, req.AgentID, req.Title) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSThreadMessagesHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + path := strings.TrimPrefix(r.URL.Path, "/api/threads/") + path = strings.Trim(path, "/") + parts := strings.Split(path, "/") + if len(parts) != 2 || parts[1] != "messages" { + http.Error(w, "invalid path", http.StatusBadRequest) + return + } + threadID := parts[0] + thread, ok := requireThreadAccess(w, ownerUserID, threadID) + if !ok { + return + } + + switch r.Method { + case http.MethodGet: + items, err := agentOSService.ListThreadMessages(threadID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + Content string `json:"content"` + Role string `json:"role"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Content) == "" { + http.Error(w, "content is required", http.StatusBadRequest) + return + } + if strings.TrimSpace(req.Role) == "" { + req.Role = "user" + } + msg, err := agentOSService.AddThreadMessage(threadID, req.Role, req.Content, "text") + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if req.Role == "user" { + task, err := agentOSService.CreateTask(agentos.AgentTask{ + CompanyID: thread.CompanyID, + DepartmentID: thread.DepartmentID, + AgentID: thread.AgentID, + RequestedBy: "user", + ThreadID: thread.ID, + Type: "conversation", + Status: "queued", + Priority: 50, + InputJSON: agentOSService.BuildTaskPromptInput(req.Content), + }) + if err != nil { + writeJSON(w, http.StatusOK, map[string]interface{}{"message": msg, "task_error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]interface{}{"message": msg, "task": task}) + return + } + writeJSON(w, http.StatusOK, map[string]interface{}{"message": msg}) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSTasksHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + agentID := strings.TrimSpace(r.URL.Query().Get("agent_id")) + if companyID != "" && !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + if agentID != "" { + agent, ok := requireAgentAccess(w, ownerUserID, agentID) + if !ok { + return + } + if companyID == "" { + companyID = agent.CompanyID + } + } + if companyID == "" { + companies, err := agentOSService.ListCompanies(ownerUserID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + var all []agentos.AgentTask + for _, company := range companies { + items, err := agentOSService.ListTasks(company.ID, agentID, r.URL.Query().Get("status"), limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + all = append(all, items...) + } + writeJSON(w, http.StatusOK, all) + return + } + items, err := agentOSService.ListTasks(companyID, agentID, r.URL.Query().Get("status"), limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + AgentID string `json:"agent_id"` + RequestedBy string `json:"requested_by"` + ThreadID string `json:"thread_id"` + Type string `json:"type"` + Priority int `json:"priority"` + Prompt string `json:"prompt"` + InputJSON string `json:"input_json"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + agent, ok := requireAgentAccess(w, ownerUserID, req.AgentID) + if !ok { + return + } + if agent.CompanyID != req.CompanyID { + http.Error(w, "agent does not belong to company", http.StatusBadRequest) + return + } + if strings.TrimSpace(req.ThreadID) != "" { + thread, ok := requireThreadAccess(w, ownerUserID, req.ThreadID) + if !ok { + return + } + if thread.CompanyID != req.CompanyID { + http.Error(w, "thread does not belong to company", http.StatusBadRequest) + return + } + } + inputJSON := req.InputJSON + if strings.TrimSpace(inputJSON) == "" { + inputJSON = agentOSService.BuildTaskPromptInput(req.Prompt) + } + created, err := agentOSService.CreateTask(agentos.AgentTask{ + CompanyID: req.CompanyID, + DepartmentID: req.DepartmentID, + AgentID: req.AgentID, + RequestedBy: req.RequestedBy, + ThreadID: req.ThreadID, + Type: req.Type, + Status: "queued", + Priority: req.Priority, + InputJSON: inputJSON, + }) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, created) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSTaskByIDHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + path := strings.TrimPrefix(r.URL.Path, "/api/tasks/") + path = strings.Trim(path, "/") + if path == "" { + http.Error(w, "task id is required", http.StatusBadRequest) + return + } + parts := strings.Split(path, "/") + taskID := parts[0] + if _, ok := requireTaskAccess(w, ownerUserID, taskID); !ok { + return + } + + if len(parts) == 1 { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + details, err := agentOSService.GetTaskDetails(taskID) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, details) + return + } + + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + action := parts[1] + switch action { + case "cancel": + if err := agentOSService.CancelTask(taskID); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "canceled"}) + case "retry": + if err := agentOSService.RetryTask(taskID); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "queued"}) + case "delegate": + var req struct { + ToAgentID string `json:"to_agent_id"` + Instruction string `json:"instruction"` + RequestedBy string `json:"requested_by"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.ToAgentID) == "" { + http.Error(w, "to_agent_id is required", http.StatusBadRequest) + return + } + if _, ok := requireAgentAccess(w, ownerUserID, req.ToAgentID); !ok { + return + } + child, err := agentOSService.DelegateTask(taskID, req.ToAgentID, req.Instruction, req.RequestedBy) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, child) + default: + http.Error(w, "unknown action", http.StatusBadRequest) + } +} + +func AgentOSConsensusRoundsHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + switch r.Method { + case http.MethodGet: + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + items, err := agentOSService.ListConsensusRounds(r.URL.Query().Get("company_id"), r.URL.Query().Get("department_id"), limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req struct { + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + Topic string `json:"topic"` + CreatedBy string `json:"created_by"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + item, err := agentOSService.CreateConsensusRound(req.CompanyID, req.DepartmentID, req.Topic, req.CreatedBy) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSConsensusRoundByIDHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + path := strings.TrimPrefix(r.URL.Path, "/api/consensus/rounds/") + path = strings.Trim(path, "/") + parts := strings.Split(path, "/") + if len(parts) == 0 || strings.TrimSpace(parts[0]) == "" { + http.Error(w, "round id is required", http.StatusBadRequest) + return + } + roundID := parts[0] + + if len(parts) == 1 { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + decision, err := agentOSService.ConsensusDecision(roundID) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, decision) + return + } + + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + switch parts[1] { + case "vote": + var req struct { + AgentID string `json:"agent_id"` + Option string `json:"option"` + Confidence float64 `json:"confidence"` + Rationale string `json:"rationale"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + vote, err := agentOSService.VoteConsensus(roundID, req.AgentID, req.Option, req.Confidence, req.Rationale) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, vote) + case "close": + round, err := agentOSService.CloseConsensusRound(roundID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, round) + default: + http.Error(w, "unknown action", http.StatusBadRequest) + } +} + +func AgentOSMemoryQueryHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + if departmentID := strings.TrimSpace(r.URL.Query().Get("department_id")); departmentID != "" { + if _, ok := requireDepartmentAccess(w, ownerUserID, departmentID); !ok { + return + } + } + if agentID := strings.TrimSpace(r.URL.Query().Get("agent_id")); agentID != "" { + if _, ok := requireAgentAccess(w, ownerUserID, agentID); !ok { + return + } + } + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + items, err := agentOSService.QueryMemory( + companyID, + r.URL.Query().Get("department_id"), + r.URL.Query().Get("agent_id"), + r.URL.Query().Get("query"), + limit, + ) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) +} + +func AgentOSMemoryWriteHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var entry agentos.MemoryEntry + if err := json.NewDecoder(r.Body).Decode(&entry); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if entry.ScopeType == "company" && !requireCompanyAccess(w, ownerUserID, entry.ScopeID) { + return + } + if entry.ScopeType == "department" { + if _, ok := requireDepartmentAccess(w, ownerUserID, entry.ScopeID); !ok { + return + } + } + if strings.HasPrefix(entry.ScopeType, "agent_") { + if _, ok := requireAgentAccess(w, ownerUserID, entry.ScopeID); !ok { + return + } + } + if err := agentOSService.WriteMemoryEntry(entry); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func AgentOSMemoryTimelineHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + items, err := agentOSService.ListMemoryTimeline(companyID, r.URL.Query().Get("department_id"), r.URL.Query().Get("agent_id"), limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) +} + +func AgentOSSchedulesHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + items, err := agentOSService.ListSchedules(companyID, r.URL.Query().Get("department_id")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req agentos.Schedule + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + agent, ok := requireAgentAccess(w, ownerUserID, req.TargetAgentID) + if !ok { + return + } + if agent.CompanyID != req.CompanyID { + http.Error(w, "agent does not belong to company", http.StatusBadRequest) + return + } + item, err := agentOSService.CreateSchedule(req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSScheduleByIDHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + path := strings.TrimPrefix(r.URL.Path, "/api/schedules/") + path = strings.Trim(path, "/") + parts := strings.Split(path, "/") + if len(parts) == 0 || strings.TrimSpace(parts[0]) == "" { + http.Error(w, "schedule id is required", http.StatusBadRequest) + return + } + id := parts[0] + if _, ok := requireScheduleAccess(w, ownerUserID, id); !ok { + return + } + + if len(parts) == 1 { + if r.Method != http.MethodPatch { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req agentos.Schedule + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + item, err := agentOSService.UpdateSchedule(id, req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + return + } + + if len(parts) == 2 && parts[1] == "toggle" { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + Enabled bool `json:"enabled"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if err := agentOSService.ToggleSchedule(id, req.Enabled); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) + return + } + + http.Error(w, "invalid path", http.StatusBadRequest) +} + +func AgentOSEventsHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + sinceID, _ := strconv.ParseInt(r.URL.Query().Get("since_id"), 10, 64) + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + items, err := agentOSService.ListEvents(companyID, r.URL.Query().Get("task_id"), sinceID, limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) +} + +func AgentOSEventsStreamHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming is unsupported", http.StatusInternalServerError) + return + } + + companyID := r.URL.Query().Get("company_id") + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + ch, cancel := agentOSService.Subscribe(companyID) + defer cancel() + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + writeSSE := func(event string, data interface{}) { + b, _ := json.Marshal(data) + _, _ = fmt.Fprintf(w, "event: %s\n", event) + _, _ = fmt.Fprintf(w, "data: %s\n\n", string(b)) + flusher.Flush() + } + + writeSSE("ready", map[string]interface{}{"company_id": companyID, "ts": time.Now().UTC().Format(time.RFC3339)}) + keepAlive := time.NewTicker(20 * time.Second) + defer keepAlive.Stop() + + ctx := r.Context() + for { + select { + case <-ctx.Done(): + return + case ev := <-ch: + writeSSE("event", ev) + case <-keepAlive.C: + writeSSE("ping", map[string]interface{}{"ts": time.Now().UTC().Format(time.RFC3339)}) + } + } +} + +func AgentOSPoliciesHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + switch r.Method { + case http.MethodGet: + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + items, err := agentOSService.ListPolicies(companyID, r.URL.Query().Get("department_id"), r.URL.Query().Get("agent_id")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) + case http.MethodPost: + var req agentos.PolicyRule + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + item, err := agentOSService.UpsertPolicy(req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, item) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func AgentOSPolicyTestHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + CompanyID string `json:"company_id"` + DepartmentID string `json:"department_id"` + AgentID string `json:"agent_id"` + Action string `json:"action"` + Scope string `json:"scope"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + writeJSON(w, http.StatusOK, agentOSService.TestPolicy(req.CompanyID, req.DepartmentID, req.AgentID, req.Action, req.Scope)) +} + +func AgentOSApprovalsHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + items, err := agentOSService.ListApprovals(companyID, r.URL.Query().Get("status")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) +} + +func AgentOSApprovalsResolveHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var req struct { + ApprovalID string `json:"approval_id"` + Decision string `json:"decision"` + Actor string `json:"actor"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.ApprovalID) == "" { + http.Error(w, "approval_id is required", http.StatusBadRequest) + return + } + approval, err := agentOSService.GetApproval(req.ApprovalID) + if err != nil || !agentOSService.UserOwnsCompany(ownerUserID, approval.CompanyID) { + http.Error(w, "not found", http.StatusNotFound) + return + } + approve := strings.EqualFold(strings.TrimSpace(req.Decision), "approve") + if err := agentOSService.ResolveApproval(req.ApprovalID, approve, req.Actor); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +func AgentOSAuditHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + sinceID, _ := strconv.ParseInt(r.URL.Query().Get("since_id"), 10, 64) + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + items, err := agentOSService.ListAudit(sinceID, limit) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, items) +} + +func AgentOSAuditVerifyHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + writeJSON(w, http.StatusOK, agentOSService.AuditVerify()) +} + +func AgentOSHealthHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + writeJSON(w, http.StatusOK, agentOSService.HealthStatus()) +} + +func AgentOSTopologyHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + companyID := strings.TrimSpace(r.URL.Query().Get("company_id")) + if companyID == "" { + items, _ := agentOSService.ListCompanies(ownerUserID) + if len(items) > 0 { + companyID = items[0].ID + } + } + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + writeJSON(w, http.StatusOK, agentOSService.Topology(companyID)) +} + +// AgentInboxHandler handles GET/POST for inter-agent messages. +func AgentInboxHandler(w http.ResponseWriter, r *http.Request) { + if !ensureAgentOSAvailable(w) { + return + } + ownerUserID, ok := requireTenant(w, r) + if !ok { + return + } + agentID := r.URL.Query().Get("agent_id") + companyID := r.URL.Query().Get("company_id") + if r.Method == http.MethodGet && agentID == "" { + http.Error(w, "agent_id required", http.StatusBadRequest) + return + } + switch r.Method { + case http.MethodGet: + if !requireCompanyAccess(w, ownerUserID, companyID) { + return + } + agent, ok := requireAgentAccess(w, ownerUserID, agentID) + if !ok { + return + } + if agent.CompanyID != companyID { + http.Error(w, "agent does not belong to company", http.StatusBadRequest) + return + } + msgs, err := agentOSService.GetAgentInbox(companyID, agentID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, msgs) + case http.MethodPost: + var req struct { + FromAgentID string `json:"from_agent_id"` + ToAgentID string `json:"to_agent_id"` + Content string `json:"content"` + CompanyID string `json:"company_id"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid request", http.StatusBadRequest) + return + } + if req.Content == "" || req.FromAgentID == "" || req.ToAgentID == "" { + http.Error(w, "from_agent_id, to_agent_id, content required", http.StatusBadRequest) + return + } + if !requireCompanyAccess(w, ownerUserID, req.CompanyID) { + return + } + fromAgent, ok := requireAgentAccess(w, ownerUserID, req.FromAgentID) + if !ok { + return + } + toAgent, ok := requireAgentAccess(w, ownerUserID, req.ToAgentID) + if !ok { + return + } + if fromAgent.CompanyID != req.CompanyID || toAgent.CompanyID != req.CompanyID { + http.Error(w, "agents do not belong to company", http.StatusBadRequest) + return + } + if err := agentOSService.PostInterAgentMessage(req.CompanyID, req.FromAgentID, req.ToAgentID, req.Content); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/dash/backend/handlers/ai.go b/dash/backend/handlers/ai.go new file mode 100644 index 0000000..c8e475d --- /dev/null +++ b/dash/backend/handlers/ai.go @@ -0,0 +1,2101 @@ +package handlers + +import ( + "bufio" + "bytes" + "database/sql" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/danilrybalkin/apollo-dash/db" + "github.com/danilrybalkin/apollo-dash/tools" + "github.com/google/uuid" +) + +type ChatSession struct { + ID string `json:"id"` + Title string `json:"title"` + Model string `json:"model"` + ProjectPath string `json:"project_path"` + ReasoningEffort string `json:"reasoning_effort"` + ExecutionMode string `json:"execution_mode"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type ChatMessage struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type Attachment struct { + Name string `json:"name"` + Type string `json:"type"` + Data string `json:"data,omitempty"` + Content string `json:"content,omitempty"` +} + +type AiChatRequest struct { + SessionID string `json:"sessionId"` + Model string `json:"model"` + Message string `json:"message"` + Mode string `json:"mode"` + Attachments []Attachment `json:"attachments"` +} + +const ( + reasoningTierNone = "none" + reasoningTierStandard = "standard" + reasoningTierHigh = "high" +) + +func normalizeReasoningEffort(value string) string { + switch strings.ToLower(strings.TrimSpace(value)) { + case reasoningTierHigh: + return reasoningTierHigh + case reasoningTierStandard: + return reasoningTierStandard + case "", reasoningTierNone: + return reasoningTierNone + default: + return reasoningTierNone + } +} + +func displayReasoningEffort(value string) string { + switch normalizeReasoningEffort(value) { + case reasoningTierHigh: + return "High" + case reasoningTierStandard: + return "Standard" + default: + return "None" + } +} + +func isHighReasoningEffort(value string) bool { + return normalizeReasoningEffort(value) == reasoningTierHigh +} + +func usesReasoningDirective(value string) bool { + tier := normalizeReasoningEffort(value) + return tier == reasoningTierStandard || tier == reasoningTierHigh +} + +// Handler for fetching existing sessions +func AiSessionsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodGet { + projectPath := r.URL.Query().Get("project_path") + query := "SELECT id, title, IFNULL(model, ''), IFNULL(project_path, ''), IFNULL(reasoning_effort, 'None'), IFNULL(execution_mode, 'Plan'), created_at, updated_at FROM chat_sessions ORDER BY updated_at DESC" + var rows *sql.Rows + var err error + + if projectPath != "" { + query = "SELECT id, title, IFNULL(model, ''), IFNULL(project_path, ''), IFNULL(reasoning_effort, 'None'), IFNULL(execution_mode, 'Plan'), created_at, updated_at FROM chat_sessions WHERE project_path = ? ORDER BY updated_at DESC" + rows, err = db.DB.Query(query, projectPath) + } else { + query = "SELECT id, title, IFNULL(model, ''), IFNULL(project_path, ''), IFNULL(reasoning_effort, 'None'), IFNULL(execution_mode, 'Plan'), created_at, updated_at FROM chat_sessions WHERE project_path = '' OR project_path IS NULL ORDER BY updated_at DESC" + rows, err = db.DB.Query(query) + } + + if err != nil { + http.Error(w, "Failed to fetch sessions", http.StatusInternalServerError) + return + } + defer rows.Close() + + var sessions []ChatSession + for rows.Next() { + var s ChatSession + if err := rows.Scan(&s.ID, &s.Title, &s.Model, &s.ProjectPath, &s.ReasoningEffort, &s.ExecutionMode, &s.CreatedAt, &s.UpdatedAt); err != nil { + continue + } + s.ReasoningEffort = displayReasoningEffort(s.ReasoningEffort) + sessions = append(sessions, s) + } + if sessions == nil { + sessions = []ChatSession{} + } + json.NewEncoder(w).Encode(sessions) + return + } + + if r.Method == http.MethodPost { + var reqBody struct { + ProjectPath string `json:"project_path"` + ReasoningEffort string `json:"reasoning_effort"` + ExecutionMode string `json:"execution_mode"` + } + json.NewDecoder(r.Body).Decode(&reqBody) + + reqBody.ReasoningEffort = displayReasoningEffort(reqBody.ReasoningEffort) + if reqBody.ExecutionMode == "" { + reqBody.ExecutionMode = "Plan" + } + + id := uuid.New().String() + title := "New Chat" + _, err := db.DB.Exec("INSERT INTO chat_sessions (id, title, project_path, reasoning_effort, execution_mode) VALUES (?, ?, ?, ?, ?)", id, title, reqBody.ProjectPath, reqBody.ReasoningEffort, reqBody.ExecutionMode) + if err != nil { + http.Error(w, "Failed to create session", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(ChatSession{ + ID: id, + Title: title, + ProjectPath: reqBody.ProjectPath, + ReasoningEffort: reqBody.ReasoningEffort, + ExecutionMode: reqBody.ExecutionMode, + }) + return + } + + if r.Method == http.MethodPut { + var reqBody struct { + ID string `json:"id"` + Model string `json:"model"` + ReasoningEffort string `json:"reasoning_effort"` + ExecutionMode string `json:"execution_mode"` + } + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + reqBody.ReasoningEffort = displayReasoningEffort(reqBody.ReasoningEffort) + + // Update only the provided fields. + // For simplicity, we assume frontend sends the full new state. + _, err := db.DB.Exec("UPDATE chat_sessions SET model = ?, reasoning_effort = ?, execution_mode = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", + reqBody.Model, reqBody.ReasoningEffort, reqBody.ExecutionMode, reqBody.ID) + if err != nil { + http.Error(w, "Failed to update session", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"status": "updated"}) + return + } + + if r.Method == http.MethodDelete { + sid := r.URL.Query().Get("id") + if sid == "" { + http.Error(w, "Session ID required", http.StatusBadRequest) + return + } + _, err := db.DB.Exec("DELETE FROM chat_sessions WHERE id = ?", sid) // cascades to messages + if err != nil { + http.Error(w, "Failed to delete session", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}) + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +// Fetch messages for a specific session +func AiMessagesHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + sid := r.URL.Query().Get("sessionId") + if sid == "" { + http.Error(w, "Session ID required", http.StatusBadRequest) + return + } + + rows, err := db.DB.Query("SELECT role, content FROM chat_messages WHERE session_id = ? ORDER BY id ASC", sid) + if err != nil { + http.Error(w, "Failed to fetch messages", http.StatusInternalServerError) + return + } + defer rows.Close() + + var msgs []ChatMessage + for rows.Next() { + var m ChatMessage + if err := rows.Scan(&m.Role, &m.Content); err != nil { + continue + } + if m.Role == "assistant" { + systemLogRe := regexp.MustCompile("(?is)````system_log[\\s\\S]*?````") + m.Content = strings.TrimSpace(systemLogRe.ReplaceAllString(m.Content, "")) + if strings.Contains(m.Content, "[Asked User:") && !strings.Contains(m.Content, "````question") { + askedRe := regexp.MustCompile(`(?is)\[Asked User:\s*(.*?)\]`) + qtxt := "" + if match := askedRe.FindStringSubmatch(m.Content); len(match) > 1 { + qtxt = strings.TrimSpace(match[1]) + } else { + lowered := strings.ToLower(m.Content) + if idx := strings.Index(lowered, "[asked user:"); idx >= 0 { + qtxt = strings.TrimSpace(m.Content[idx+len("[Asked User:"):]) + qtxt = strings.TrimSpace(strings.TrimSuffix(qtxt, "]")) + } + } + if qtxt != "" { + qPayload := map[string]interface{}{ + "question": qtxt, + "options": []string{"Answer in chat"}, + } + qJSON, _ := json.Marshal(qPayload) + if askedRe.MatchString(m.Content) { + m.Content = askedRe.ReplaceAllString(m.Content, fmt.Sprintf("````question\n%s\n````", string(qJSON))) + } else { + m.Content = fmt.Sprintf("````question\n%s\n````", string(qJSON)) + } + } + } + } + msgs = append(msgs, m) + } + + if msgs == nil { + msgs = []ChatMessage{} + } + json.NewEncoder(w).Encode(msgs) +} + +// Fetches available OpenRouter Models and Local Ollama Models +func AiModelsHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + apiKey := ResolveOpenRouterKey(CurrentUserID(r)) + + var orResp map[string]interface{} + + if apiKey != "" && apiKey != "your_openrouter_api_key_here" { + req, _ := http.NewRequest("GET", "https://openrouter.ai/api/v1/models", nil) + req.Header.Set("Authorization", "Bearer "+apiKey) + client := &http.Client{Timeout: 5 * time.Second} + if resp, err := client.Do(req); err == nil && resp.StatusCode == 200 { + defer resp.Body.Close() + json.NewDecoder(resp.Body).Decode(&orResp) + } + } + + if orResp == nil { + orResp = map[string]interface{}{"data": []interface{}{}} + } + + dataList, ok := orResp["data"].([]interface{}) + if !ok { + dataList = []interface{}{} + } + + ollamaUrl := os.Getenv("OLLAMA_API_URL") + if ollamaUrl != "" { + req, _ := http.NewRequest("GET", ollamaUrl+"/api/tags", nil) + client := &http.Client{Timeout: 2 * time.Second} + if resp, err := client.Do(req); err == nil && resp.StatusCode == 200 { + defer resp.Body.Close() + var ollamaResp struct { + Models []struct { + Name string `json:"name"` + } `json:"models"` + } + if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err == nil { + for _, m := range ollamaResp.Models { + localModel := map[string]interface{}{ + "id": "local/" + m.Name, + "name": "(Local) " + m.Name, + "architecture": map[string]interface{}{ + // We can assume text, maybe add image if the ID matches llava + "input_modalities": []string{"text"}, + }, + } + // If the user's running a vision model like llava + if strings.Contains(strings.ToLower(m.Name), "llava") { + localModel["architecture"].(map[string]interface{})["input_modalities"] = []string{"text", "image"} + } + dataList = append([]interface{}{localModel}, dataList...) + } + } + } + } + + // Filter by Featured Models unless ?all=1 is passed + settings := GetCurrentSettings(CurrentUserID(r)) + if r.URL.Query().Get("all") != "1" && len(settings.FeaturedModels) > 0 { + featuredMap := make(map[string]bool) + for _, fm := range settings.FeaturedModels { + featuredMap[fm] = true + } + + var filteredList []interface{} + for _, m := range dataList { + mMap, ok := m.(map[string]interface{}) + if !ok { + continue + } + idStr, _ := mMap["id"].(string) + if featuredMap[idStr] { + filteredList = append(filteredList, m) + } + } + dataList = filteredList + } + + orResp["data"] = dataList + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(orResp) +} + +// Constructs the master personality block from Settings and SQLite Facts +func ConstructPersonalityPrompt(userID string, mode string, reasoningEffort string, executionMode string, projectPath string) string { + settings := GetCurrentSettings(userID) + var builder strings.Builder + + builder.WriteString(settings.SystemPrompt) + builder.WriteString("\n\n") + + rows, err := db.DB.Query("SELECT fact FROM personality_facts ORDER BY id ASC") + if err == nil { + var facts []string + for rows.Next() { + var fact string + if err := rows.Scan(&fact); err == nil { + facts = append(facts, fact) + } + } + rows.Close() + + if len(facts) > 0 { + builder.WriteString("\n") + builder.WriteString("You have permanently memorized the following facts across all sessions:\n") + for _, f := range facts { + builder.WriteString("- " + f + "\n") + } + builder.WriteString("\n\n") + } + } + + builder.WriteString("CORE DIRECTIVE: If the user explicitly tells you something you should remember permanently, or you establish a permanent detail about your persona, output it anywhere in your response wrapped in `fact here` tags so it can be committed to your core memory bank.\n\n") + builder.WriteString(fmt.Sprintf("SYSTEM CONTEXT: The current local server time is %s.\n\n", time.Now().Format("Monday, 02 Jan 2006 15:04:05 MST"))) + + if skillsManifest := tools.GetSkillsManifest(); skillsManifest != "" { + builder.WriteString(skillsManifest) + builder.WriteString("\n\n") + } + + if usesReasoningDirective(reasoningEffort) { + builder.WriteString(` +========================================= +MANDATORY REASONING DIRECTIVE +========================================= +Before answering any request, you MUST engage in deep, step-by-step Chain-of-Thought reasoning. +You must break down the problem, analyze constraints, and explore potential solutions. +If during your reasoning you realize a step is flawed, you must explicitly write out your Self-Correction and pivot your approach. +This ensures highly accurate, logical, and thoroughly vetted intelligent responses. + +You MUST enclose your internal thought process inside a markdown code block with the language "reasoning". +Example: +` + "````" + `reasoning +1. First I need to analyze the user's request... +2. The user wants X. Let's break this down... +Wait, if I do X, it might break Y. Let me self-correct and approach this via Z instead... +` + "````" + ` + +After closing the reasoning block, output your final, perfectly formatted direct response to the user. +`) + } + if mode == "coding" { + builder.WriteString("\n\nMODE: CODING (ACTIVE)\n") + builder.WriteString(fmt.Sprintf("\nWORKING DIRECTORY: %s\n", projectPath)) + builder.WriteString("You are currently in autonomous coding mode. Your primary objective is to execute tasks using your tools. Do not just talk—ACT!\n") + builder.WriteString(` +AGENTIC LOOP: +Work through every task in three blended phases — chain as many tool calls as needed: +1. GATHER: Understand the codebase first. Read files, list directories, grep for patterns, find files, check types. Never skip this phase on unfamiliar code. +2. ACT: Make changes. Edit files, write new files, rename/move things, run commands, run tests. One action per tool call — be surgical and precise. +3. VERIFY: After every change, confirm it worked. Run the relevant check_code, execute tests, read the modified file back, diff it. If verification fails, loop back to ACT with the corrected approach. + +SKILL-BASED LEARNING: +You have a dynamic skill library. When a skill is needed, call load_skill(name) to load its full content. Skills provide deep, specialized knowledge on demand without bloating every conversation. + +TOOL DISCIPLINE: +- After ANY file edit, immediately call check_code on the modified file to catch errors. +- After ANY execute_command that should produce output, verify the output matches expectations. +- If a test fails, re-read the relevant source files before attempting a fix — never guess. +- Chain dozens of tool calls confidently. Stop only when the task is fully verified complete. +- If you receive a user message mid-task, STOP immediately and address it before continuing. +`) + builder.WriteString(` +PLANNING DIRECTIVE: +If a user request requires a complex or multi-component build (like "create a web app"), DO NOT guess the architecture, tech stack, or design preferences. +Instead, FIRST use the 'ask_user_question' tool to prompt the user for their preferences (e.g. asking them to choose between React/Vue, or asking about a specific visual style). +You can ask as many questions as you need using the 'ask_user_question' tool. Wait for their answers. + +Once you have gathered enough context, you must draft a concrete, self-directed architecture spec. +Then, ALWAYS propose an execution plan. +Before the plan block, include a short architecture summary (3-6 bullets). +The FINAL content of your message MUST be exactly one markdown code block with language "plan" containing ONLY valid JSON. Nothing should appear after that plan block. +` + "````" + `plan +{"title":"","summary":"<1-2 sentence summary>","steps":[{"index":0,"label":"","details":"","validation":"","status":"pending"}]} +` + "````" + ` +After outputting the plan block, stop and wait for the user to respond. +If the user responds with approval (for example "yes proceed"), treat it as PLAN_APPROVED and execute each step in sequence. +After completing each step, output a plan step update: +` + "````" + `step_update +{"planId": "", "index": , "status": "done"} +` + "````" + ` +If the user suggests changes instead, revise the plan summary + JSON and output a full new plan block (same format). + +SUBAGENT DIRECTIVE: +You have access to a spawn_subagent tool. You should PROACTIVELY decide to use it — you do NOT need to be asked. +Spawn a background subagent when any of the following is true: +- The task involves deep research (e.g. "find the best X", "compare Y options", "analyze Z codebase") +- The task can be parallelized safely (e.g. "review all these files" → spawn one reviewer per major file/module) +- The task is expected to take many tool calls and would block the main conversation for a long time +- The user's main task benefits from concurrent validation, testing, or fact-checking +When spawning: use a descriptive name (e.g. "Researcher-1", "CodeReviewer", "Tester"), write a precise self-contained task description, and ALWAYS pass the current session_id so the user gets notified when it finishes. +After spawning, immediately tell the user you've spawned an agent and continue the main thread without waiting. +`) + if !isGitWorkspace(projectPath) { + builder.WriteString(` +GITLESS WORKSPACE OVERRIDE: +- This workspace is not a git repository. +- Do NOT ask the user to create branches, commits, pull requests, or run git commands. +- Do NOT rely on worktrees or PR-based orchestration. +- Execute tasks through direct file edits + validation commands only. +`) + } + } else if mode == "talking" { + builder.WriteString("\n\nMODE: TALKING (ACTIVE)\n") + builder.WriteString("You are currently in conversational talking mode. Your objective is communication, brainstorming, or clarification. Only use tools if strictly necessary for answering a question. If asked to write or edit code, provide the code snippets directly in your response chat without triggering actual file system edits.\n") + } + + return builder.String() +} + +func isGitWorkspace(projectPath string) bool { + projectPath = strings.TrimSpace(projectPath) + if projectPath == "" { + return false + } + gitPath := filepath.Join(projectPath, ".git") + info, err := os.Stat(gitPath) + if err != nil { + return false + } + // .git can be a directory or a file (worktree/submodule pointer) + return info.IsDir() || info.Mode().IsRegular() +} + +func extractQuestionFromAssistantContent(content string) string { + content = strings.TrimSpace(content) + if content == "" { + return "" + } + + questionBlockRe := regexp.MustCompile("(?is)````question\\s*([\\s\\S]*?)\\s*````") + if blocks := questionBlockRe.FindAllStringSubmatch(content, -1); len(blocks) > 0 { + for i := len(blocks) - 1; i >= 0; i-- { + raw := strings.TrimSpace(blocks[i][1]) + var payload struct { + Question string `json:"question"` + } + if json.Unmarshal([]byte(raw), &payload) == nil { + if q := strings.TrimSpace(payload.Question); q != "" { + return q + } + } + } + } + + askedRe := regexp.MustCompile(`(?is)\[Asked User:\s*(.*?)\]`) + if m := askedRe.FindStringSubmatch(content); len(m) > 1 { + return strings.TrimSpace(m[1]) + } + return "" +} + +func hasPlanBlock(content string) bool { + return regexp.MustCompile("(?is)````plan\\s*[\\s\\S]*?````").MatchString(content) +} + +func looksLikePlanApproval(msg string) bool { + m := strings.ToLower(strings.TrimSpace(msg)) + if m == "" { + return false + } + if strings.EqualFold(strings.TrimSpace(msg), "PLAN_APPROVED") { + return true + } + disqualifiers := []string{"but", "except", "instead", "change", "edit", "revise", "?"} + for _, bad := range disqualifiers { + if strings.Contains(m, bad) { + return false + } + } + approvals := []string{ + "yes", "yep", "yeah", "proceed", "go ahead", "approved", "approve", + "looks good", "ship it", "continue", "do it", "ok proceed", "you can proceed", + } + for _, token := range approvals { + if strings.Contains(m, token) { + return true + } + } + return false +} + +// Process Chat Generation and DB storage +func AiChatHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + apiKey := ResolveOpenRouterKey(CurrentUserID(r)) + ollamaUrl := os.Getenv("OLLAMA_API_URL") + + var chatReq AiChatRequest + if err := json.NewDecoder(r.Body).Decode(&chatReq); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + if chatReq.SessionID == "" || chatReq.Message == "" { + http.Error(w, "SessionID and Message are required", http.StatusBadRequest) + return + } + + sendSSEChunk := func(w http.ResponseWriter, content string) { + var chunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + chunk.Choices = append(chunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + chunk.Choices[0].Delta.Content = content + b, _ := json.Marshal(chunk) + w.Write([]byte("data: ")) + w.Write(b) + w.Write([]byte("\n\n")) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + } + + extractAskedUserQuestion := func(input string) (string, bool) { + re := regexp.MustCompile(`(?is)\[Asked User:\s*(.*?)\]`) + m := re.FindStringSubmatch(input) + if len(m) < 2 { + return "", false + } + return strings.TrimSpace(m[1]), true + } + + var reasoningEffort, executionMode, projectPath string + + // 1. Process documents first + for _, att := range chatReq.Attachments { + if att.Type == "file" { + chatReq.Message += "\n\n\n" + att.Content + "\n" + } + } + + // 2. Fetch past messages for this session + questionBlockRe := regexp.MustCompile("(?is)````question\\s*([\\s\\S]*?)\\s*````") + uiBlockRe := regexp.MustCompile("(?is)````(?:reasoning|tool_action|compacting|step_update|plan|checkpoint|terminal|system_log)[\\s\\S]*?````") + rows, err := db.DB.Query("SELECT role, content FROM chat_messages WHERE session_id = ? ORDER BY id ASC", chatReq.SessionID) + var historyRows []ChatMessage + var openRouterMsgs []map[string]interface{} + if err == nil { + for rows.Next() { + var role, content string + rows.Scan(&role, &content) + historyRows = append(historyRows, ChatMessage{Role: role, Content: content}) + } + rows.Close() + } + + var lastAssistantRaw string + for i := len(historyRows) - 1; i >= 0; i-- { + if historyRows[i].Role == "assistant" { + lastAssistantRaw = historyRows[i].Content + break + } + } + lastAssistantQuestion := extractQuestionFromAssistantContent(lastAssistantRaw) + lastAssistantHasPlan := hasPlanBlock(lastAssistantRaw) + + for _, hr := range historyRows { + role := hr.Role + content := hr.Content + + // Context Hygiene: Strip UI-only blocks from assistant history before sending to LLM + if role == "assistant" { + content = questionBlockRe.ReplaceAllString(content, "") + content = uiBlockRe.ReplaceAllString(content, "") + askedRe := regexp.MustCompile(`(?is)\[Asked User:\s*(.*?)\]`) + content = askedRe.ReplaceAllString(content, "") + re := regexp.MustCompile(`(?is).*?`) + content = re.ReplaceAllString(content, "") + content = strings.TrimSpace(content) + } + + msgObj := map[string]interface{}{"role": role} + if len(content) > 0 && content[0] == '[' { + var contentArr []map[string]interface{} + if err := json.Unmarshal([]byte(content), &contentArr); err == nil { + msgObj["content"] = contentArr + } else { + msgObj["content"] = content + } + } else { + msgObj["content"] = content + } + openRouterMsgs = append(openRouterMsgs, msgObj) + } + + effectiveUserMessage := chatReq.Message + var orchestrationHints []string + if lastAssistantQuestion != "" && strings.TrimSpace(chatReq.Message) != "" { + orchestrationHints = append(orchestrationHints, fmt.Sprintf("The assistant previously asked the user: %q. The user's latest message is the answer: %q. Treat it as answered and continue. Do NOT repeat the same question unless the user's answer is empty/ambiguous.", lastAssistantQuestion, chatReq.Message)) + } + if lastAssistantHasPlan { + if looksLikePlanApproval(chatReq.Message) { + effectiveUserMessage = "PLAN_APPROVED" + orchestrationHints = append(orchestrationHints, "The user approved the latest plan. Start execution now. Do not ask a new clarification question unless a hard blocker appears.") + } else if strings.TrimSpace(chatReq.Message) != "" { + orchestrationHints = append(orchestrationHints, "The user requested plan changes. Produce a fully revised plan with updated summary and detailed steps, and place the plan block at the end of the message.") + } + } + + // 3. Construct new user message content + var imageAttachments []map[string]interface{} + for _, att := range chatReq.Attachments { + if att.Type == "image" { + imageAttachments = append(imageAttachments, map[string]interface{}{ + "type": "image_url", + "image_url": map[string]string{ + "url": att.Data, + }, + }) + } + } + + var dbContentStr string + if len(imageAttachments) > 0 { + var contentArr []map[string]interface{} + contentArr = append(contentArr, map[string]interface{}{ + "type": "text", + "text": effectiveUserMessage, + }) + for _, img := range imageAttachments { + contentArr = append(contentArr, img) + } + + // For OpenRouter + openRouterMsgs = append(openRouterMsgs, map[string]interface{}{"role": "user", "content": contentArr}) + + // For SQLite + b, _ := json.Marshal(contentArr) + dbContentStr = string(b) + } else { + // Normal text + openRouterMsgs = append(openRouterMsgs, map[string]interface{}{"role": "user", "content": effectiveUserMessage}) + dbContentStr = chatReq.Message + } + + // 4. Save new user message to DB + db.DB.Exec("INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'user', ?)", chatReq.SessionID, dbContentStr) + db.DB.Exec("UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ?", chatReq.SessionID) + + // If this is the FIRST message in a session (len == 1), update the title and model automatically + if len(openRouterMsgs) == 1 { + title := chatReq.Message + if len(title) > 30 { + title = title[:27] + "..." + } + db.DB.Exec("UPDATE chat_sessions SET title = ?, model = ? WHERE id = ?", title, chatReq.Model, chatReq.SessionID) + } + + // Load session runtime mode/config once and reuse below. + err = db.DB.QueryRow("SELECT IFNULL(reasoning_effort, 'None'), IFNULL(execution_mode, 'Plan'), IFNULL(project_path, '') FROM chat_sessions WHERE id = ?", chatReq.SessionID).Scan(&reasoningEffort, &executionMode, &projectPath) + if err != nil { + reasoningEffort = "None" + executionMode = "Plan" + projectPath = "" + } + reasoningEffort = displayReasoningEffort(reasoningEffort) + gitWorkspace := isGitWorkspace(projectPath) + if !gitWorkspace && strings.EqualFold(chatReq.Mode, "coding") { + orchestrationHints = append(orchestrationHints, "Environment note: this project is gitless. Do not ask for git actions or PR flow; continue with direct code edits and local validation only.") + } + + if len(orchestrationHints) > 0 { + for i := len(orchestrationHints) - 1; i >= 0; i-- { + openRouterMsgs = append([]map[string]interface{}{ + {"role": "system", "content": orchestrationHints[i]}, + }, openRouterMsgs...) + } + } + + if (apiKey == "" || apiKey == "your_openrouter_api_key_here") && ollamaUrl == "" { + http.Error(w, `{"error": "No AI Providers configured (Missing OpenRouter API Key and OLLAMA_API_URL)"}`, http.StatusServiceUnavailable) + return + } + + // Persist assistant output in DB incrementally so reloads can recover ongoing work. + var assistantRowID int64 + if res, err := db.DB.Exec("INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', '')", chatReq.SessionID); err == nil { + if id, idErr := res.LastInsertId(); idErr == nil { + assistantRowID = id + } + } + lastPersistedAssistant := "" + persistAssistant := func(content string) { + if content == lastPersistedAssistant { + return + } + lastPersistedAssistant = content + if assistantRowID > 0 { + db.DB.Exec("UPDATE chat_messages SET content = ? WHERE id = ?", content, assistantRowID) + } else { + db.DB.Exec("INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'assistant', ?)", chatReq.SessionID, content) + } + db.DB.Exec("UPDATE chat_sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = ?", chatReq.SessionID) + } + + // 4. Send request to either Local Ollama or OpenRouter + model := chatReq.Model + var targetUrl string + var reqApiKey string + + if strings.HasPrefix(model, "local/") { + if ollamaUrl == "" { + http.Error(w, "OLLAMA_API_URL is not configured", http.StatusBadRequest) + return + } + model = strings.TrimPrefix(model, "local/") + targetUrl = ollamaUrl + "/v1/chat/completions" + reqApiKey = "Bearer local" + } else { + if model == "" { + settings := GetCurrentSettings(CurrentUserID(r)) + if settings.DefaultModel != "" { + model = settings.DefaultModel + } else { + model = "meta-llama/llama-3-8b-instruct:free" // fallback + } + } + targetUrl = "https://openrouter.ai/api/v1/chat/completions" + reqApiKey = "Bearer " + apiKey + } + + // 5. Inject RAG Episodes + episodes := SearchEpisodes(chatReq.Message, chatReq.SessionID) + if episodes != "" { + sysMsg := map[string]interface{}{"role": "system", "content": episodes} + openRouterMsgs = append([]map[string]interface{}{sysMsg}, openRouterMsgs...) + log.Println("Memory System: Injected chronological memory RAG block into context stream.") + } + + // 5. Inject Global Personality Prompt + personaPrompt := ConstructPersonalityPrompt(CurrentUserID(r), chatReq.Mode, reasoningEffort, executionMode, projectPath) + if personaPrompt != "" { + sysMsg := map[string]interface{}{"role": "system", "content": personaPrompt} + openRouterMsgs = append([]map[string]interface{}{sysMsg}, openRouterMsgs...) + } + + // --- SMART CONTEXT COMPRESSION --- + // Estimate total token usage. If over threshold, compress the middle of the conversation. + { + totalChars := 0 + for _, msg := range openRouterMsgs { + if c, ok := msg["content"].(string); ok { + totalChars += len(c) + } + } + estimatedTokens := totalChars / 4 + + settings := GetCurrentSettings(CurrentUserID(r)) + compressionThreshold := settings.AutoCompactTokens + if compressionThreshold <= 0 { + compressionThreshold = 80000 + } + + if estimatedTokens > compressionThreshold && len(openRouterMsgs) > 6 { + log.Printf("Context Compression: Estimated %d tokens — compressing history.", estimatedTokens) + + // Stream compacting indicator to UI + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + var compactChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + compactChunk.Choices = append(compactChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + compactChunk.Choices[0].Delta.Content = "````compacting\nCompacting context...\n````\n\n" + compactB, _ := json.Marshal(compactChunk) + w.Write([]byte("data: ")) + w.Write(compactB) + w.Write([]byte("\n\n")) + if f, ok := w.(http.Flusher); ok { + f.Flush() + } + + // Build a compression prompt from the middle of the conversation + sysMessages := []map[string]interface{}{} + var histBuilder strings.Builder + for _, msg := range openRouterMsgs { + if role, ok := msg["role"].(string); ok && role == "system" { + sysMessages = append(sysMessages, msg) + continue + } + if c, ok := msg["content"].(string); ok { + role, _ := msg["role"].(string) + histBuilder.WriteString(fmt.Sprintf("%s: %s\n\n", role, c)) + } + } + compressionMsgs := []map[string]interface{}{ + {"role": "system", "content": "You are a context compression engine. Summarize the following conversation history into a dense, factual memory block. Preserve all key decisions, code changes, files edited, commands run, and important context. Output plain text only."}, + {"role": "user", "content": histBuilder.String()}, + } + compressPayload, _ := json.Marshal(map[string]interface{}{ + "model": model, + "messages": compressionMsgs, + "stream": false, + }) + cReq, _ := http.NewRequest("POST", targetUrl, bytes.NewBuffer(compressPayload)) + cReq.Header.Set("Authorization", reqApiKey) + cReq.Header.Set("Content-Type", "application/json") + cClient := &http.Client{Timeout: 60 * time.Second} + cResp, cErr := cClient.Do(cReq) + if cErr == nil && cResp.StatusCode == 200 { + var cResult struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + json.NewDecoder(cResp.Body).Decode(&cResult) + cResp.Body.Close() + if len(cResult.Choices) > 0 { + summaryContent := cResult.Choices[0].Message.Content + summaryMsg := map[string]interface{}{ + "role": "system", + "content": "\n" + summaryContent + "\n", + } + // Rebuild: system messages + compressed summary + last 4 messages + lastN := openRouterMsgs + if len(lastN) > 4 { + lastN = lastN[len(lastN)-4:] + } + openRouterMsgs = append(sysMessages, summaryMsg) + openRouterMsgs = append(openRouterMsgs, lastN...) + log.Println("Context Compression: History compressed successfully.") + } + } + } + } + + // --- TEST-TIME COMPUTE ORCHESTRATOR (HIGH EFFORT) --- + // --- TEST-TIME COMPUTE ORCHESTRATOR (HIGH EFFORT) --- + if isHighReasoningEffort(reasoningEffort) { + log.Println("Test-Time Compute: High Effort Orchestrator Initiated.") + + orchestratorStartTime := time.Now() + + // Phase 1: Decomposition + decompMsgs := []map[string]interface{}{ + {"role": "system", "content": "You are an analytical Engine. The user has submitted a prompt. Your ONLY objective is to break their request down into a sequential array of abstract reasoning steps required to perfectly solve it. If the request requires any factual lookup, explicitly include steps to search for and double-check those facts. Output ONLY a valid JSON array of strings, nothing else. Example: [\"Analyze constraints\", \"Search for facts\", \"Critique logic\", \"Finalize\"]"}, + {"role": "user", "content": chatReq.Message}, + } + + decompPayload, _ := json.Marshal(map[string]interface{}{ + "model": model, + "messages": decompMsgs, + "stream": false, + }) + + dReq, _ := http.NewRequest("POST", targetUrl, bytes.NewBuffer(decompPayload)) + dReq.Header.Set("Authorization", reqApiKey) + dReq.Header.Set("Content-Type", "application/json") + dClient := &http.Client{Timeout: 60 * time.Second} + dResp, dErr := dClient.Do(dReq) + + var steps []string + if dErr == nil && dResp.StatusCode == 200 { + var dResult struct { + Choices []struct { + Message struct { + Content string `json:"content"` + } `json:"message"` + } `json:"choices"` + } + json.NewDecoder(dResp.Body).Decode(&dResult) + dResp.Body.Close() + + if len(dResult.Choices) > 0 { + rawJson := strings.TrimPrefix(dResult.Choices[0].Message.Content, "```json") + rawJson = strings.TrimPrefix(rawJson, "```") + rawJson = strings.TrimSuffix(rawJson, "```") + json.Unmarshal([]byte(strings.TrimSpace(rawJson)), &steps) + } + } + + if len(steps) == 0 { + steps = []string{"Analyze the request logically", "Determine the correct response carefully"} + } + + // Phase 2: Orchestration + var fullThoughtChain strings.Builder + fullThoughtChain.WriteString("\n") + + sendSSEChunk(w, "````reasoning\n") + + orchestratorContext := append([]map[string]interface{}(nil), openRouterMsgs...) + + for i, step := range steps { + fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("\n> **Step %d:** %s\n", i+1, step)) + w.(http.Flusher).Flush() + + stepPrompt := fmt.Sprintf("You are currently executing Step %d of your reasoning plan: '%s'.\n\nPlease output your internal thoughts processing *only* this step. You must gather facts and double-check logic. Do not execute the final answer yet. Just think aloud.", i+1, step) + orchestratorContext = append(orchestratorContext, map[string]interface{}{"role": "user", "content": stepPrompt}) + + stepPayload, _ := json.Marshal(map[string]interface{}{ + "model": model, + "messages": orchestratorContext, + "stream": false, + "tools": tools.GetAvailableTools(), // Give reasoning mid-thought access + }) + sReq, _ := http.NewRequest("POST", targetUrl, bytes.NewBuffer(stepPayload)) + sReq.Header.Set("Authorization", reqApiKey) + sReq.Header.Set("Content-Type", "application/json") + sResp, sErr := dClient.Do(sReq) + + if sErr == nil && sResp.StatusCode == 200 { + var sResult struct { + Choices []struct { + Message struct { + Content string `json:"content"` + ToolCalls []struct { + Id string `json:"id"` + Type string `json:"type"` + Function struct { + Name string `json:"name"` + Arguments string `json:"arguments"` + } `json:"function"` + } `json:"tool_calls"` + } `json:"message"` + } `json:"choices"` + } + json.NewDecoder(sResp.Body).Decode(&sResult) + sResp.Body.Close() + + if len(sResult.Choices) > 0 { + msg := sResult.Choices[0].Message + + // Handle Mid-Reasoning Tool Use! + if len(msg.ToolCalls) > 0 { + for _, tc := range msg.ToolCalls { + var args map[string]interface{} + json.Unmarshal([]byte(tc.Function.Arguments), &args) + + sendSSEChunk(w, fmt.Sprintf("*(Calling tool %s to gather facts...)*\n", tc.Function.Name)) + + toolRes := tools.ExecuteTool(tc.Function.Name, tc.Function.Arguments, projectPath) + + // Inject back into step + orchestratorContext = append(orchestratorContext, map[string]interface{}{ + "role": "assistant", "content": "", "tool_calls": msg.ToolCalls, + }) + orchestratorContext = append(orchestratorContext, map[string]interface{}{ + "role": "tool", "content": toolRes, "tool_call_id": tc.Id, + }) + } + // We don't formally re-loop here to keep it simple; we just inject the tools so it has them in Phase 4 Synthesis! + } + + thought := msg.Content + if thought != "" { + orchestratorContext = append(orchestratorContext, map[string]interface{}{"role": "assistant", "content": thought}) + fullThoughtChain.WriteString(fmt.Sprintf("-- Step %d: %s --\n%s\n\n", i+1, step, thought)) + + sendSSEChunk(w, thought) + } + } + } + } + + elapsedSecs := time.Since(orchestratorStartTime).Seconds() + fullThoughtChain.WriteString("") + + sendSSEChunk(w, fmt.Sprintf("\n\n[Completed in %.1fs]\n````\n\n", elapsedSecs)) + + // Phase 4: Synthesis + // Prepend the entire massively generated thought process to the user's final message in the Tool Loop + lastUserIdx := len(openRouterMsgs) - 1 + if openRouterMsgs[lastUserIdx]["role"] == "user" { + openRouterMsgs[lastUserIdx]["content"] = fmt.Sprintf("%s\n\nUser Request: %s", fullThoughtChain.String(), chatReq.Message) + } + } + + // --- MAIN TOOL LOOP --- + // Models can call multiple tools in sequence. We loop until the model ceases calling tools. + debugSystemLogs := strings.EqualFold(strings.TrimSpace(os.Getenv("APOLLO_DEBUG_SYSTEM_LOG")), "1") || + strings.EqualFold(strings.TrimSpace(os.Getenv("APOLLO_DEBUG_SYSTEM_LOG")), "true") + timelineResponse := "" + for attempt := 0; attempt < 20; attempt++ { + // Log the prompt to the system log panel + if chatReq.Mode == "coding" && debugSystemLogs { + promptLog, _ := json.MarshalIndent(openRouterMsgs, "", " ") + sendSSEChunk(w, fmt.Sprintf("````system_log\n[LLM PROMPT - Turn %d]\n%s\n````\n", attempt+1, string(promptLog))) + } + + payload, _ := json.Marshal(map[string]interface{}{ + "model": model, + "messages": openRouterMsgs, + "stream": true, + "tools": tools.GetAvailableTools(), + }) + + req, _ := http.NewRequest("POST", targetUrl, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", reqApiKey) + req.Header.Set("Content-Type", "application/json") + if !strings.HasPrefix(chatReq.Model, "local/") { + req.Header.Set("X-Title", "Apollo Dashboard") + } + + client := &http.Client{Timeout: 300 * time.Second} + resp, err := client.Do(req) + + if err != nil { + if attempt == 0 { + http.Error(w, "Upstream AI error", http.StatusBadGateway) + } + return + } + + if resp.StatusCode != 200 { + if attempt == 0 { + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) + } + resp.Body.Close() + return + } + + if attempt == 0 { + // Only send headers on the very first loop before stream begins + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + } + + flusher, ok := w.(http.Flusher) + if !ok && attempt == 0 { + http.Error(w, "Streaming unsupported", http.StatusInternalServerError) + resp.Body.Close() + return + } + + reader := bufio.NewReader(resp.Body) + var fullResponse string + var toolCalls []map[string]interface{} + var hiddenBuffer string + askedLeakBuffer := "" + inAskedLeak := false + + for { + line, err := reader.ReadBytes('\n') + if err != nil { + break + } + + // Parse strictly to see if this is a tool payload or a text payload + cleanLine := bytes.TrimSpace(line) + isToolChunk := false + + if bytes.HasPrefix(cleanLine, []byte("data: ")) && string(cleanLine) != "data: [DONE]" { + data := bytes.TrimPrefix(cleanLine, []byte("data: ")) + var chunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + Thought string `json:"thought"` + ToolCalls []map[string]interface{} `json:"tool_calls"` + } `json:"delta"` + } `json:"choices"` + } + + if err := json.Unmarshal(data, &chunk); err == nil && len(chunk.Choices) > 0 { + delta := chunk.Choices[0].Delta + if len(delta.ToolCalls) > 0 { + isToolChunk = true + + // Buffer tool calls (SSE models stream JSON args in chunks) + for _, tc := range delta.ToolCalls { + idxFloat, ok := tc["index"].(float64) + if !ok { + continue + } + idx := int(idxFloat) + + for len(toolCalls) <= idx { + toolCalls = append(toolCalls, make(map[string]interface{})) + } + + if id, exists := tc["id"].(string); exists { + toolCalls[idx]["id"] = id + } + if function, exists := tc["function"].(map[string]interface{}); exists { + if _, fExists := toolCalls[idx]["function"]; !fExists { + toolCalls[idx]["function"] = make(map[string]interface{}) + } + funcPtr := toolCalls[idx]["function"].(map[string]interface{}) + + if n, nExists := function["name"].(string); nExists { + funcPtr["name"] = n + } + if argChunk, argExists := function["arguments"].(string); argExists { + if existingArgs, ex := funcPtr["arguments"].(string); ex { + funcPtr["arguments"] = existingArgs + argChunk + } else { + funcPtr["arguments"] = argChunk + } + } + } + } + } else if delta.Content != "" || delta.ReasoningContent != "" || delta.Thought != "" { + combinedText := delta.Content + if delta.ReasoningContent != "" { + combinedText = "````reasoning\n" + delta.ReasoningContent + "\n````\n" + combinedText + } else if delta.Thought != "" { + combinedText = "````reasoning\n" + delta.Thought + "\n````\n" + combinedText + } + + // Safety filters to prevent internal/debug leakage in user-visible chat. + if strings.Contains(strings.ToLower(combinedText), "[llm prompt - turn") { + combinedText = "" + } + sysLogInlineRe := regexp.MustCompile("(?is)````system_log[\\s\\S]*?````") + combinedText = sysLogInlineRe.ReplaceAllString(combinedText, "") + + // Convert legacy "[Asked User: ...]" leakage into first-class question block. + if inAskedLeak || strings.Contains(combinedText, "[Asked User:") { + inAskedLeak = true + askedLeakBuffer += combinedText + if strings.Contains(askedLeakBuffer, "]") { + if qtxt, ok := extractAskedUserQuestion(askedLeakBuffer); ok && qtxt != "" { + qPayload := map[string]interface{}{ + "question": qtxt, + "options": []string{"Answer in chat"}, + } + qJSON, _ := json.Marshal(qPayload) + combinedText = fmt.Sprintf("````question\n%s\n````\n", string(qJSON)) + } else { + combinedText = "" + } + askedLeakBuffer = "" + inAskedLeak = false + } else { + combinedText = "" + } + } + + displayChunk := combinedText + fullResponse += displayChunk + persistAssistant(timelineResponse + fullResponse) + + if !isToolChunk && len(toolCalls) == 0 { + lowerResp := strings.ToLower(fullResponse) + startIdx := strings.LastIndex(lowerResp, "") + endIdx := strings.LastIndex(lowerResp, "") + + inTag := false + if startIdx != -1 && (endIdx == -1 || endIdx < startIdx) { + inTag = true + } + + inPartial := false + if !inTag { + last10 := lowerResp + if len(last10) > 10 { + last10 = last10[len(last10)-10:] + } + for i := 1; i < len(""); i++ { + if strings.HasSuffix(last10, ""[:i]) { + inPartial = true + break + } + } + } + + if inTag || (endIdx != -1 && strings.HasSuffix(lowerResp, "")) { + // Completely drop the token chunk from the UI + } else if inPartial { + hiddenBuffer += displayChunk + } else { + // Fully render chunk + any accumulated false-positive buffer + contentToStream := hiddenBuffer + displayChunk + hiddenBuffer = "" + + chunk.Choices[0].Delta.Content = contentToStream + b, _ := json.Marshal(chunk) + w.Write([]byte("data: ")) + w.Write(b) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + } + } + } + } + } + + if string(cleanLine) == "data: [DONE]" { + if hiddenBuffer != "" { + var finalChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + finalChunk.Choices = append(finalChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + finalChunk.Choices[0].Delta.Content = hiddenBuffer + b, _ := json.Marshal(finalChunk) + w.Write([]byte("data: ")) + w.Write(b) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + } + w.Write([]byte("data: [DONE]\n\n")) + if ok { + flusher.Flush() + } + break + } + } + + resp.Body.Close() + + // If the AI invoked tools, we must NOT exit. We execute them, append to memory, and loop! + if len(toolCalls) > 0 { + + // 1. Append the AI's tool request to openRouterMsgs + assistantToolMsg := map[string]interface{}{ + "role": "assistant", + "content": fullResponse, // Can be null if it just straight up called a tool + "tool_calls": toolCalls, + } + for _, tc := range toolCalls { + tc["type"] = "function" // OpenRouter spec enforcement + } + openRouterMsgs = append(openRouterMsgs, assistantToolMsg) + + // 2. Execute Go Tools + for _, tc := range toolCalls { + id, _ := tc["id"].(string) + funcObj, _ := tc["function"].(map[string]interface{}) + name, _ := funcObj["name"].(string) + args, _ := funcObj["arguments"].(string) + + var parsedArgs map[string]interface{} + json.Unmarshal([]byte(args), &parsedArgs) + + // -- ASK USER QUESTION INTERCEPT -- + if name == "ask_user_question" { + var qChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + qChunk.Choices = append(qChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + + // Re-standardize for frontend + qPayload := map[string]interface{}{ + "question": parsedArgs["question"], + } + if opt, ok := parsedArgs["options"]; ok { + qPayload["options"] = opt + } else if ch, ok := parsedArgs["choices"]; ok { + qPayload["options"] = ch + } else { + qPayload["options"] = []string{"Yes", "No"} + } + qPayloadB, _ := json.Marshal(qPayload) + + qBlock := fmt.Sprintf("\n\n````question\n%s\n````\n", string(qPayloadB)) + qChunk.Choices[0].Delta.Content = qBlock + qB, _ := json.Marshal(qChunk) + w.Write([]byte("data: ")) + w.Write(qB) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + + // Persist question blocks and tool actions across reloads. + askActionJSON, _ := json.Marshal(map[string]string{"action": "Asked Question", "target": "user_input"}) + askActionBlock := fmt.Sprintf("````tool_action\n%s\n````\n", string(askActionJSON)) + fullResponse += askActionBlock + qBlock + + persistAssistant(timelineResponse + fullResponse) + resp.Body.Close() + return + } + + // -- TIMELINE: Stream a tool_action block BEFORE execution -- + type toolActionPayload struct { + Action string `json:"action"` + Target string `json:"target"` + } + tap := toolActionPayload{Action: name} + switch name { + case "execute_command": + tap.Action = "Ran Command" + if c, ok := parsedArgs["command"].(string); ok { + tap.Target = c + } + case "edit_file": + tap.Action = "Edited" + if p, ok := parsedArgs["path"].(string); ok { + tap.Target = p + } + case "write_file": + tap.Action = "Created" + if p, ok := parsedArgs["path"].(string); ok { + tap.Target = p + } + case "read_file": + tap.Action = "Read" + if p, ok := parsedArgs["path"].(string); ok { + tap.Target = p + } + case "list_files": + tap.Action = "Listed Files" + if p, ok := parsedArgs["path"].(string); ok { + tap.Target = p + } + case "grep_search": + tap.Action = "Searched Code" + if q, ok := parsedArgs["query"].(string); ok { + tap.Target = q + } + case "web_search": + tap.Action = "Searched Web" + if q, ok := parsedArgs["query"].(string); ok { + tap.Target = q + } + case "web_scrape": + tap.Action = "Scraped URL" + if u, ok := parsedArgs["url"].(string); ok { + tap.Target = u + } + case "undo_checkpoint": + tap.Action = "Reverted Checkpoint" + case "propose_commit": + tap.Action = "Proposed Commit" + if p, ok := parsedArgs["file_path"].(string); ok { + tap.Target = p + } + case "undo_change": + tap.Action = "Undid Change" + if p, ok := parsedArgs["point_id"].(string); ok { + tap.Target = p + } + case "get_context_tree": + tap.Action = "Mapped Context Tree" + case "get_file_skeleton": + tap.Action = "Generated File Skeleton" + if p, ok := parsedArgs["file_path"].(string); ok { + tap.Target = p + } + case "semantic_code_search": + tap.Action = "Semantic Search" + if q, ok := parsedArgs["query"].(string); ok { + tap.Target = q + } + case "semantic_identifier_search": + tap.Action = "Identifier Search" + if q, ok := parsedArgs["query"].(string); ok { + tap.Target = q + } + case "get_blast_radius": + tap.Action = "Computed Blast Radius" + if s, ok := parsedArgs["symbol_name"].(string); ok { + tap.Target = s + } + case "run_static_analysis": + tap.Action = "Ran Static Analysis" + case "semantic_navigate": + tap.Action = "Semantic Navigation" + case "get_feature_hub": + tap.Action = "Feature Hub Lookup" + } + tActionJSON, _ := json.Marshal(tap) + tActionBlock := fmt.Sprintf("````tool_action\n%s\n````\n", string(tActionJSON)) + var taChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + taChunk.Choices = append(taChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + taChunk.Choices[0].Delta.Content = tActionBlock + taB, _ := json.Marshal(taChunk) + w.Write([]byte("data: ")) + w.Write(taB) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + fullResponse += tActionBlock + timelineResponse += tActionBlock + persistAssistant(timelineResponse + fullResponse) + + // -- Checkpoint: create git snapshot before destructive actions -- + var hash string + if name == "execute_command" { + cmdName := "command" + if c, ok := parsedArgs["command"].(string); ok { + cmdName = c + } + hash = tools.CreateCheckpoint("Before running: " + cmdName) + } else if name == "write_file" || name == "edit_file" || name == "propose_commit" { + pathName := "file" + if p, ok := parsedArgs["path"].(string); ok { + pathName = p + } else if p, ok := parsedArgs["file_path"].(string); ok { + pathName = p + } + hash = tools.CreateCheckpoint("Before modifying: " + pathName) + } + + // --- EXECUTION MODE SAFETY GATE --- + execMode := strings.ToLower(executionMode) + if execMode == "" { + execMode = "default" + } + destructiveWrite := name == "write_file" || name == "edit_file" || name == "rename_file" || name == "propose_commit" || name == "undo_change" + destructiveExec := name == "execute_command" + if execMode == "plan" && (destructiveWrite || destructiveExec) { + openRouterMsgs = append(openRouterMsgs, map[string]interface{}{ + "role": "tool", "tool_call_id": id, "name": name, + "content": fmt.Sprintf("[PLAN MODE] Tool '%s' blocked. Read-only mode active.", name), + }) + continue + } + needsConfirm := (execMode == "default" && (destructiveWrite || destructiveExec)) || + (execMode == "auto_accept" && destructiveExec) + if needsConfirm { + confirmID := fmt.Sprintf("conf-%d-%s", time.Now().UnixNano(), name) + confirmPayload, _ := json.Marshal(map[string]interface{}{"id": confirmID, "tool": name, "args": parsedArgs}) + var cfChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + cfChunk.Choices = append(cfChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + cfChunk.Choices[0].Delta.Content = fmt.Sprintf("````confirm\n%s\n````\n", string(confirmPayload)) + cfB, _ := json.Marshal(cfChunk) + w.Write([]byte("data: ")) + w.Write(cfB) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + ch := RegisterConfirmation(confirmID) + if approved := <-ch; !approved { + openRouterMsgs = append(openRouterMsgs, map[string]interface{}{ + "role": "tool", "tool_call_id": id, "name": name, + "content": fmt.Sprintf("[Rejected] User did not approve '%s'.", name), + }) + continue + } + } + resultStr := tools.ExecuteTool(name, args, projectPath) + + // -- Post-execution visual feedback for terminal output and checkpoints -- + var visualMsg string + switch name { + case "execute_command": + cmdName := "command" + if c, ok := parsedArgs["command"].(string); ok { + cmdName = c + } + visualMsg = fmt.Sprintf("\n\n```terminal\n$ %s\n%s\n```\n", cmdName, resultStr) + case "undo_checkpoint": + visualMsg = "\n\n> ⏪ **Reverted sandbox to previous checkpoint.**\n" + } + + if hash != "" { + visualMsg = fmt.Sprintf("\n\n````checkpoint\n%s\n````\n", hash) + visualMsg + } + + if visualMsg != "" { + var streamChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + streamChunk.Choices = append(streamChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + streamChunk.Choices[0].Delta.Content = visualMsg + b, _ := json.Marshal(streamChunk) + w.Write([]byte("data: ")) + w.Write(b) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + fullResponse += visualMsg + timelineResponse += visualMsg + persistAssistant(timelineResponse + fullResponse) + } + + // 3. Append the physical tool output + toolResultMsg := map[string]interface{}{ + "role": "tool", + "tool_call_id": id, + "name": name, + "content": resultStr, + } + openRouterMsgs = append(openRouterMsgs, toolResultMsg) + + if chatReq.Mode == "coding" && debugSystemLogs { + sendSSEChunk(w, fmt.Sprintf("````system_log\n[TOOL RESULT: %s]\n%s\n````\n", name, resultStr)) + } + } + + // DO NOT BREAK. Let the `for` loop spin again and send the complete array back to OpenRouter! + continue + } + + // --- END OF TOOL LOOP --- + // If the AI has reached here, it didn't pick any more tools. + // BUT: if the fullResponse is empty or purely technical, the user sees nothing! + // We detect if there's no "human" text and force one final synthesis. + cleanText := uiBlockRe.ReplaceAllString(fullResponse, "") + cleanText = strings.TrimSpace(cleanText) + if cleanText == "" && attempt > 0 { + // Nudge the model to synthesize + openRouterMsgs = append(openRouterMsgs, map[string]interface{}{"role": "user", "content": "The tools have finished. Please provide a concise, final summary of what was accomplished for the user."}) + continue // One more spin! + } + + // --- END OF TOOL LOOP --- + + // If we reach here, the AI is done picking tools and has sent us a final text answer. + + // 5. Personality Engine Extraction + if fullResponse != "" { + re := regexp.MustCompile(`(?is)(.*?)`) + matches := re.FindAllStringSubmatch(fullResponse, -1) + for _, match := range matches { + if len(match) > 1 { + fact := strings.TrimSpace(match[1]) + if fact != "" { + db.DB.Exec("INSERT OR IGNORE INTO personality_facts (fact) VALUES (?)", fact) + log.Println("Personality Engine: Immortalized new fact:", fact) + } + } + } + } + + storedResponse := timelineResponse + fullResponse + persistAssistant(storedResponse) + return + } +} + +// Proxies raw OpenAI-format requests from external tools (VSCode, Mobile) out to the correct provider +func AiExternalChatHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + apiKey := ResolveOpenRouterKey(CurrentUserID(r)) + ollamaUrl := os.Getenv("OLLAMA_API_URL") + + if (apiKey == "" || apiKey == "your_openrouter_api_key_here") && ollamaUrl == "" { + http.Error(w, `{"error": "No AI Providers configured"}`, http.StatusServiceUnavailable) + return + } + + // Read verbatim incoming JSON payload + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error reading request body", http.StatusInternalServerError) + return + } + + var payloadMap map[string]interface{} + if err := json.Unmarshal(bodyBytes, &payloadMap); err != nil { + http.Error(w, "Invalid JSON payload", http.StatusBadRequest) + return + } + + model, _ := payloadMap["model"].(string) + + // Agent routing: check X-Agent-ID header, ?agent_id= param, or default setting + routingAgentID := r.Header.Get("X-Agent-ID") + if routingAgentID == "" { + routingAgentID = r.URL.Query().Get("agent_id") + } + if routingAgentID == "" { + routingAgentID = getSettingString("external_default_agent_id", "") + } + if routingAgentID != "" && agentOSService != nil { + if routingAgent, err := agentOSService.GetAgent(routingAgentID); err == nil { + if strings.TrimSpace(routingAgent.IdentityPrompt) != "" { + agentSysMsg := map[string]interface{}{"role": "system", "content": routingAgent.IdentityPrompt} + if messages, ok := payloadMap["messages"].([]interface{}); ok { + payloadMap["messages"] = append([]interface{}{agentSysMsg}, messages...) + } + } + // Use agent's model if set + if binding, profile, err2 := agentOSService.GetAgentBinding(routingAgent.ID); err2 == nil { + if profile.Model != "" && model == "" { + payloadMap["model"] = profile.Model + model = profile.Model + _ = binding + } + } + } + } + + // Inject RAG episodes for external tooling + if messages, ok := payloadMap["messages"].([]interface{}); ok && len(messages) > 0 { + lastMsg, _ := messages[len(messages)-1].(map[string]interface{}) + if contentStr, ok := lastMsg["content"].(string); ok { + episodes := SearchEpisodes(contentStr, "external_api") + if episodes != "" { + sysMsg := map[string]interface{}{"role": "system", "content": episodes} + payloadMap["messages"] = append([]interface{}{sysMsg}, messages...) + log.Println("Memory System: Injected chronological memory RAG block into EXTERNAL context stream.") + } + } + } + + // Inject Global Personality Prompt for external tooling + personaPrompt := ConstructPersonalityPrompt(CurrentUserID(r), "talking", "None", "Plan", "") + if personaPrompt != "" { + sysMsg := map[string]interface{}{"role": "system", "content": personaPrompt} + if messages, ok := payloadMap["messages"].([]interface{}); ok { + payloadMap["messages"] = append([]interface{}{sysMsg}, messages...) + } + } + + payloadMap["tools"] = tools.GetAvailableTools() + + var targetUrl string + var reqApiKey string + + if strings.HasPrefix(model, "local/") { + if ollamaUrl == "" { + http.Error(w, "OLLAMA_API_URL is not configured", http.StatusBadRequest) + return + } + // Rewrite model internally + payloadMap["model"] = strings.TrimPrefix(model, "local/") + targetUrl = ollamaUrl + "/v1/chat/completions" + reqApiKey = "Bearer local" + } else { + if model == "" { + settings := GetCurrentSettings(CurrentUserID(r)) + if settings.DefaultModel != "" { + payloadMap["model"] = settings.DefaultModel + } else { + payloadMap["model"] = "meta-llama/llama-3-8b-instruct:free" + } + } + targetUrl = "https://openrouter.ai/api/v1/chat/completions" + reqApiKey = "Bearer " + apiKey + } + + for attempt := 0; attempt < 5; attempt++ { + // Re-marshal to send upstream + upstreamPayload, _ := json.Marshal(payloadMap) + + req, _ := http.NewRequestWithContext(r.Context(), "POST", targetUrl, bytes.NewBuffer(upstreamPayload)) + req.Header.Set("Authorization", reqApiKey) + req.Header.Set("Content-Type", "application/json") + if !strings.HasPrefix(model, "local/") { + req.Header.Set("X-Title", "AgentHQ API Gateway") + } + + client := &http.Client{Timeout: 300 * time.Second} + resp, err := client.Do(req) + + if err != nil { + if attempt == 0 { + http.Error(w, "Upstream AI error", http.StatusBadGateway) + } + return + } + + if attempt == 0 { + // Only send headers on the very first loop before stream begins + w.WriteHeader(resp.StatusCode) + } + + if resp.StatusCode != 200 { + if attempt == 0 { + // We already wrote the header above, just pipe body + io.Copy(w, resp.Body) + } + resp.Body.Close() + return + } + + reader := bufio.NewReader(resp.Body) + var fullResponse string + var toolCalls []map[string]interface{} + var hiddenBuffer string + flusher, ok := w.(http.Flusher) + + for { + line, err := reader.ReadBytes('\n') + if err != nil { + break + } + + cleanLine := bytes.TrimSpace(line) + isToolChunk := false + + if bytes.HasPrefix(cleanLine, []byte("data: ")) && string(cleanLine) != "data: [DONE]" { + data := bytes.TrimPrefix(cleanLine, []byte("data: ")) + var chunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + ToolCalls []map[string]interface{} `json:"tool_calls"` + } `json:"delta"` + } `json:"choices"` + } + if json.Unmarshal(data, &chunk) == nil && len(chunk.Choices) > 0 { + delta := chunk.Choices[0].Delta + if len(delta.ToolCalls) > 0 { + isToolChunk = true + for _, tc := range delta.ToolCalls { + idxFloat, ok := tc["index"].(float64) + if !ok { + continue + } + idx := int(idxFloat) + for len(toolCalls) <= idx { + toolCalls = append(toolCalls, make(map[string]interface{})) + } + if id, exists := tc["id"].(string); exists { + toolCalls[idx]["id"] = id + } + if function, exists := tc["function"].(map[string]interface{}); exists { + if _, fExists := toolCalls[idx]["function"]; !fExists { + toolCalls[idx]["function"] = make(map[string]interface{}) + } + funcPtr := toolCalls[idx]["function"].(map[string]interface{}) + + if n, nExists := function["name"].(string); nExists { + funcPtr["name"] = n + } + if argChunk, argExists := function["arguments"].(string); argExists { + if existingArgs, ex := funcPtr["arguments"].(string); ex { + funcPtr["arguments"] = existingArgs + argChunk + } else { + funcPtr["arguments"] = argChunk + } + } + } + } + } else if delta.Content != "" { + fullResponse += delta.Content + + if !isToolChunk && len(toolCalls) == 0 { + lowerResp := strings.ToLower(fullResponse) + startIdx := strings.LastIndex(lowerResp, "") + endIdx := strings.LastIndex(lowerResp, "") + + inTag := false + if startIdx != -1 && (endIdx == -1 || endIdx < startIdx) { + inTag = true + } + + inPartial := false + if !inTag { + last10 := lowerResp + if len(last10) > 10 { + last10 = last10[len(last10)-10:] + } + for i := 1; i < len(""); i++ { + if strings.HasSuffix(last10, ""[:i]) { + inPartial = true + break + } + } + } + + if inTag || (endIdx != -1 && strings.HasSuffix(lowerResp, "")) { + // Drop token + } else if inPartial { + hiddenBuffer += delta.Content + } else { + // Render + contentToStream := hiddenBuffer + delta.Content + hiddenBuffer = "" + + chunk.Choices[0].Delta.Content = contentToStream + b, _ := json.Marshal(chunk) + w.Write([]byte("data: ")) + w.Write(b) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + } + } + } + } + } + + if string(cleanLine) == "data: [DONE]" { + if hiddenBuffer != "" { + var finalChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + finalChunk.Choices = append(finalChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + finalChunk.Choices[0].Delta.Content = hiddenBuffer + b, _ := json.Marshal(finalChunk) + w.Write([]byte("data: ")) + w.Write(b) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + } + break + } + } + + resp.Body.Close() + + if len(toolCalls) > 0 { + assistantToolMsg := map[string]interface{}{ + "role": "assistant", + "content": fullResponse, + "tool_calls": toolCalls, + } + for _, tc := range toolCalls { + tc["type"] = "function" + } + + if messagesArray, ok := payloadMap["messages"].([]interface{}); ok { + messagesArray = append(messagesArray, assistantToolMsg) + + for _, tc := range toolCalls { + id, _ := tc["id"].(string) + funcObj, _ := tc["function"].(map[string]interface{}) + name, _ := funcObj["name"].(string) + args, _ := funcObj["arguments"].(string) + + var parsedArgs map[string]interface{} + json.Unmarshal([]byte(args), &parsedArgs) + + var hash string + if name == "execute_command" { + cmdName := "command" + if c, ok := parsedArgs["command"].(string); ok { + cmdName = c + } + hash = tools.CreateCheckpoint("Before running: " + cmdName) + } else if name == "write_file" || name == "edit_file" || name == "propose_commit" { + pathName := "file" + if p, ok := parsedArgs["path"].(string); ok { + pathName = p + } else if p, ok := parsedArgs["file_path"].(string); ok { + pathName = p + } + hash = tools.CreateCheckpoint("Before modifying: " + pathName) + } + + resultStr := tools.ExecuteTool(name, args, "") // External API doesn't have project root yet + + var visualMsg string + + switch name { + case "execute_command": + cmdName := "command" + if c, ok := parsedArgs["command"].(string); ok { + cmdName = c + } + visualMsg = fmt.Sprintf("\n\n> 🛠️ **Ran Command:** `%s`\n```terminal\n%s\n```\n", cmdName, resultStr) + case "undo_checkpoint": + visualMsg = "\n\n> ⏪ **Reverted sandbox to previous checkpoint.**\n" + case "edit_file": + pathName := "file" + if p, ok := parsedArgs["path"].(string); ok { + pathName = p + } + visualMsg = fmt.Sprintf("\n\n> 📝 **Edited `%s`**\n", pathName) + case "write_file": + pathName := "file" + if p, ok := parsedArgs["path"].(string); ok { + pathName = p + } + visualMsg = fmt.Sprintf("\n\n> 📝 **Created `%s`**\n", pathName) + case "propose_commit": + pathName := "file" + if p, ok := parsedArgs["file_path"].(string); ok { + pathName = p + } + visualMsg = fmt.Sprintf("\n\n> 📝 **Proposed Commit `%s`**\n", pathName) + case "undo_change": + pointID := "restore point" + if p, ok := parsedArgs["point_id"].(string); ok { + pointID = p + } + visualMsg = fmt.Sprintf("\n\n> ⏪ **Restored `%s`**\n", pointID) + case "read_file": + pathName := "file" + if p, ok := parsedArgs["path"].(string); ok { + pathName = p + } + visualMsg = fmt.Sprintf("\n\n> 📖 **Read `%s`**\n", pathName) + case "list_files": + visualMsg = "\n\n> 📂 **Listed Files**\n" + case "grep_search": + visualMsg = "\n\n> 🔍 **Searched Files**\n" + case "web_scrape": + visualMsg = "\n\n> 🌐 **Scraped Webpage**\n" + } + + if visualMsg != "" && hash != "" { + visualMsg = fmt.Sprintf("\n\n````checkpoint\n%s\n````\n", hash) + visualMsg + } + + if visualMsg != "" { + var streamChunk struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + } `json:"choices"` + } + streamChunk.Choices = append(streamChunk.Choices, struct { + Delta struct { + Content string `json:"content"` + } `json:"delta"` + }{}) + streamChunk.Choices[0].Delta.Content = visualMsg + b, _ := json.Marshal(streamChunk) + w.Write([]byte("data: ")) + w.Write(b) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + fullResponse += visualMsg + } + + toolResultMsg := map[string]interface{}{ + "role": "tool", + "tool_call_id": id, + "name": name, + "content": resultStr, + } + messagesArray = append(messagesArray, toolResultMsg) + } + payloadMap["messages"] = messagesArray + } + + continue + } + + // 5. Personality Engine Extraction against proxied stream + if fullResponse != "" { + re := regexp.MustCompile(`(?is)(.*?)`) + matches := re.FindAllStringSubmatch(fullResponse, -1) + for _, match := range matches { + if len(match) > 1 { + fact := strings.TrimSpace(match[1]) + if fact != "" { + db.DB.Exec("INSERT OR IGNORE INTO personality_facts (fact) VALUES (?)", fact) + log.Println("Personality Engine: Immortalized EXTERNAL proxied fact:", fact) + } + } + } + } + + w.Write([]byte("data: [DONE]\n\n")) + if ok { + flusher.Flush() + } + } +} diff --git a/dash/backend/handlers/auth.go b/dash/backend/handlers/auth.go new file mode 100644 index 0000000..0d64de8 --- /dev/null +++ b/dash/backend/handlers/auth.go @@ -0,0 +1,86 @@ +package handlers + +import ( + "context" + "crypto/subtle" + "net/http" + "os" + "strings" + + "github.com/danilrybalkin/apollo-dash/db" +) + +type contextKey string + +const currentUserIDKey contextKey = "agenthq_user_id" + +func CurrentUserID(r *http.Request) string { + if v, ok := r.Context().Value(currentUserIDKey).(string); ok { + return v + } + return "" +} + +func BasicAuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Access-Control-Allow-Origin", "*") + + // Public endpoints — no auth check + if strings.HasPrefix(r.URL.Path, "/api/auth/") { + next.ServeHTTP(w, r) + return + } + // Vulta webhook — authenticated by HMAC signature in handler itself + if r.URL.Path == "/api/billing/webhook" { + next.ServeHTTP(w, r) + return + } + + // Let the React app and static assets load. Protected pages still require a + // user token client-side, and all API/WebSocket routes remain guarded here. + if !strings.HasPrefix(r.URL.Path, "/api/") && !strings.HasPrefix(r.URL.Path, "/ws/") { + next.ServeHTTP(w, r) + return + } + + // User session token check + authHeader := r.Header.Get("Authorization") + if strings.HasPrefix(authHeader, "Bearer ") { + token := strings.TrimPrefix(authHeader, "Bearer ") + // Try user token first + user, err := db.GetUserByToken(token) + if err == nil && user != nil { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), currentUserIDKey, user.ID))) + return + } + // Then try admin password + envPass := os.Getenv("DASHBOARD_PASSWORD") + if envPass != "" && subtle.ConstantTimeCompare([]byte(token), []byte(envPass)) == 1 { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), currentUserIDKey, "admin"))) + return + } + } + + // Basic Auth fallback + envPass := os.Getenv("DASHBOARD_PASSWORD") + if envPass == "" { + next.ServeHTTP(w, r) + return + } + user, pass, ok := r.BasicAuth() + if ok && subtle.ConstantTimeCompare([]byte(pass), []byte(envPass)) == 1 && user == "admin" { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), currentUserIDKey, "admin"))) + return + } + + w.Header().Set("WWW-Authenticate", `Basic realm="AgentHQ"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} diff --git a/dash/backend/handlers/billing.go b/dash/backend/handlers/billing.go new file mode 100644 index 0000000..709b093 --- /dev/null +++ b/dash/backend/handlers/billing.go @@ -0,0 +1,195 @@ +package handlers + +import ( + "bytes" + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "io" + "log" + "net/http" + "os" + "time" + + "github.com/danilrybalkin/apollo-dash/db" + "github.com/google/uuid" +) + +const vultaAPIBase = "https://vulta.one/api" + +// ── Vulta API client ────────────────────────────────────────────────────────── + +func vultaAPIKey() string { return os.Getenv("VULTA_API_KEY") } +func vultaWebhookSecret() string { return os.Getenv("VULTA_WEBHOOK_SECRET") } + +func vultaPost(ctx context.Context, path string, body interface{}) (map[string]interface{}, int, error) { + b, _ := json.Marshal(body) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, vultaAPIBase+path, bytes.NewReader(b)) + if err != nil { + return nil, 0, err + } + req.Header.Set("Authorization", "Bearer "+vultaAPIKey()) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 15 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, 0, err + } + defer resp.Body.Close() + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + return result, resp.StatusCode, nil +} + +// ── POST /api/billing/checkout ──────────────────────────────────────────────── + +func BillingCheckoutHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + token := extractBearerToken(r) + if token == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + user, err := db.GetUserByToken(token) + if err != nil || user == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + + if vultaAPIKey() == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusServiceUnavailable) + json.NewEncoder(w).Encode(map[string]string{"error": "billing not configured"}) + return + } + + // Use checkout sessions API with use_all_payout_destinations — no dest ID needed + payload := map[string]interface{}{ + "amount_fiat": "39.00", + "fiat_currency": "USD", + "external_reference_id": "agenthq_user_" + user.ID, + "use_all_payout_destinations": true, + } + + result, status, err := vultaPost(r.Context(), "/checkout/sessions", payload) + if err != nil || status >= 400 { + errMsg := "failed to create checkout session" + if e, ok := result["error"].(string); ok { + errMsg = e + } + log.Printf("Billing: Vulta error for user %s: status=%d err=%v result=%v", user.ID, status, err, result) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadGateway) + json.NewEncoder(w).Encode(map[string]string{"error": errMsg}) + return + } + + // Checkout session returns { "id": "...", "checkout_url": "..." } + sessionID, _ := result["id"].(string) + checkoutURL, _ := result["checkout_url"].(string) + + if sessionID == "" || checkoutURL == "" { + log.Printf("Billing: unexpected Vulta response: %v", result) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadGateway) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid response from payment provider"}) + return + } + + // Store billing session so webhook can map back to user + _ = db.CreateBillingSession(uuid.New().String(), user.ID, sessionID) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"checkout_url": checkoutURL}) +} + +// ── POST /api/billing/webhook ───────────────────────────────────────────────── + +type vultaWebhookPayload struct { + EventType string `json:"event_type"` + PaymentRequestID string `json:"payment_request_id"` + ExternalRefID string `json:"external_reference_id"` + AmountFiat string `json:"amount_fiat"` + FiatCurrency string `json:"fiat_currency"` + Status string `json:"status"` + MerchantID string `json:"merchant_id"` +} + +func BillingWebhookHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + rawBody, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "read error", http.StatusBadRequest) + return + } + + // Verify HMAC-SHA256 signature if webhook secret is configured + secret := vultaWebhookSecret() + if secret != "" { + sig := r.Header.Get("X-Vulta-Signature") + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(rawBody) + expected := hex.EncodeToString(mac.Sum(nil)) + if !hmac.Equal([]byte(sig), []byte(expected)) { + log.Printf("Billing: invalid webhook signature") + http.Error(w, "invalid signature", http.StatusUnauthorized) + return + } + } + + var payload vultaWebhookPayload + if err := json.Unmarshal(rawBody, &payload); err != nil { + http.Error(w, "bad payload", http.StatusBadRequest) + return + } + + log.Printf("Billing: webhook event_type=%s status=%s payment_request=%s ref=%s", + payload.EventType, payload.Status, payload.PaymentRequestID, payload.ExternalRefID) + + // Only act on confirmed payments (event_type from webhook, status from payload) + isConfirmed := payload.EventType == "payment.confirmed" || payload.Status == "payment.confirmed" || payload.Status == "CONFIRMED" + if !isConfirmed { + w.WriteHeader(http.StatusOK) + return + } + + // Look up user by payment request ID + userID, err := db.GetBillingSessionByPaymentRequestID(payload.PaymentRequestID) + if err != nil { + log.Printf("Billing: no billing session for payment_request %s: %v", payload.PaymentRequestID, err) + w.WriteHeader(http.StatusOK) // ack anyway to prevent retries + return + } + + // Activate pro plan + if err := db.ActivateProPlan(userID); err != nil { + log.Printf("Billing: failed to activate pro for user %s: %v", userID, err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + _ = db.ConfirmBillingSession(payload.PaymentRequestID) + + // Send confirmation email + user, err := db.GetUserByID(userID) + if err == nil && user != nil { + SendPaymentConfirmedEmail(context.Background(), user.Email, user.Name) + } + + log.Printf("Billing: activated pro plan for user %s", userID) + w.WriteHeader(http.StatusOK) +} diff --git a/dash/backend/handlers/confirm.go b/dash/backend/handlers/confirm.go new file mode 100644 index 0000000..bbf6bab --- /dev/null +++ b/dash/backend/handlers/confirm.go @@ -0,0 +1,68 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "sync" + "time" +) + +// ─── Pending Confirmation Registry ────────────────────────────────────────── +// Maps confirmation ID → response channel. The tool loop blocks on the channel. +var ( + pendingConfirms = map[string]chan bool{} + pendingConfirmsMu sync.Mutex +) + +// RegisterConfirmation creates a channel for the given ID. The tool loop reads it. +func RegisterConfirmation(id string) chan bool { + ch := make(chan bool, 1) + pendingConfirmsMu.Lock() + pendingConfirms[id] = ch + pendingConfirmsMu.Unlock() + // Auto-reject after 5 minutes + go func() { + time.Sleep(5 * time.Minute) + pendingConfirmsMu.Lock() + if ch, ok := pendingConfirms[id]; ok { + select { + case ch <- false: // auto-reject + default: + } + delete(pendingConfirms, id) + } + pendingConfirmsMu.Unlock() + }() + return ch +} + +// ConfirmHandler: POST /api/confirm?id=xxx&action=approve|reject +func ConfirmHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + id := r.URL.Query().Get("id") + action := r.URL.Query().Get("action") + if id == "" || action == "" { + http.Error(w, "id and action required", http.StatusBadRequest) + return + } + + pendingConfirmsMu.Lock() + ch, ok := pendingConfirms[id] + if ok { + delete(pendingConfirms, id) + } + pendingConfirmsMu.Unlock() + + if !ok { + http.Error(w, "confirmation not found or expired", http.StatusNotFound) + return + } + + approved := action == "approve" + ch <- approved + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]bool{"approved": approved}) +} diff --git a/dash/backend/handlers/email.go b/dash/backend/handlers/email.go new file mode 100644 index 0000000..0ad76e1 --- /dev/null +++ b/dash/backend/handlers/email.go @@ -0,0 +1,225 @@ +package handlers + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/danilrybalkin/apollo-dash/db" +) + +// ── Resend client ───────────────────────────────────────────────────────────── + +type resendClient struct { + apiKey string + from string + client *http.Client +} + +var emailer *resendClient + +func InitEmailer() { + key := os.Getenv("RESEND_API_KEY") + if key == "" { + log.Println("Email: RESEND_API_KEY not set — emails disabled") + return + } + emailer = &resendClient{ + apiKey: key, + from: "AgentHQ ", + client: &http.Client{Timeout: 10 * time.Second}, + } + log.Println("Email: Resend client ready") +} + +func sendEmail(ctx context.Context, to, subject, htmlBody string) { + if emailer == nil { + return + } + go func() { + payload := map[string]interface{}{ + "from": emailer.from, + "to": []string{to}, + "subject": subject, + "html": htmlBody, + } + body, _ := json.Marshal(payload) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.resend.com/emails", bytes.NewReader(body)) + if err != nil { + log.Printf("Email: build request: %v", err) + return + } + req.Header.Set("Authorization", "Bearer "+emailer.apiKey) + req.Header.Set("Content-Type", "application/json") + resp, err := emailer.client.Do(req) + if err != nil { + log.Printf("Email: send to %s: %v", to, err) + return + } + resp.Body.Close() + if resp.StatusCode >= 400 { + log.Printf("Email: Resend returned %d for %s", resp.StatusCode, to) + } + }() +} + +// ── Templates ───────────────────────────────────────────────────────────────── + +func appURL() string { + if u := os.Getenv("APP_URL"); u != "" { + return u + } + return "https://agenthq.ai" +} + +func wrapper(content string) string { + return fmt.Sprintf(` + + +
+
+ AgentHQ +
+ %s +

+ AgentHQ · Questions? support@agenthq.ai +

+
+ +`, content) +} + +func SendWelcomeEmail(ctx context.Context, to, name string) { + first := name + if first == "" { + first = "there" + } + body := wrapper(fmt.Sprintf(` +

Welcome to AgentHQ

+

Hi %s,

+

+ Your account is ready. You have a 3-day free trial — no credit card required. + A demo company with an AI agent is already waiting for you inside. +

+ Open dashboard → +

+ Your trial gives you full access to all features. Add your OpenRouter API key in + Account → API Keys to enable AI tasks. +

`, first, appURL())) + sendEmail(ctx, to, "Welcome to AgentHQ — your trial has started", body) +} + +func SendTrialExpiryWarningEmail(ctx context.Context, to, name string, hoursLeft int) { + first := name + if first == "" { + first = "there" + } + timeStr := "tomorrow" + if hoursLeft <= 6 { + timeStr = "in a few hours" + } + body := wrapper(fmt.Sprintf(` +

Your trial ends %s

+

Hi %s,

+

+ Your AgentHQ free trial expires %s. Upgrade to keep your agents running — all your data, companies, and memory stay intact. +

+ Upgrade now → +

+ After your trial ends, your account and all data are preserved. Agents will pause until you upgrade. +

`, timeStr, first, timeStr, appURL())) + sendEmail(ctx, to, "Your AgentHQ trial expires "+timeStr, body) +} + +func SendPaymentConfirmedEmail(ctx context.Context, to, name string) { + first := name + if first == "" { + first = "there" + } + body := wrapper(fmt.Sprintf(` +

You're now on Pro

+

Hi %s,

+

+ Payment confirmed. Your AgentHQ account is now on the Pro plan for the next 30 days. + All agents are active and running. +

+ Go to dashboard → +

+ To renew, visit Account → Subscription before your plan expires. Questions? Reply to this email. +

`, first, appURL())) + sendEmail(ctx, to, "Payment confirmed — AgentHQ Pro is active", body) +} + +func SendRenewalReminderEmail(ctx context.Context, to, name string, daysLeft int) { + first := name + if first == "" { + first = "there" + } + body := wrapper(fmt.Sprintf(` +

Your Pro plan renews in %d days

+

Hi %s,

+

+ Your AgentHQ Pro subscription expires in %d days. Renew now to keep your agents running without interruption. +

+ Renew subscription →`, daysLeft, first, daysLeft, appURL())) + sendEmail(ctx, to, fmt.Sprintf("AgentHQ Pro renews in %d days", daysLeft), body) +} + +// ── Trial expiry background job ─────────────────────────────────────────────── + +func StartEmailNotifier(ctx context.Context) { + go func() { + // Run once at startup after a brief delay, then every 12 hours + time.Sleep(30 * time.Second) + runNotifierCycle(ctx) + ticker := time.NewTicker(12 * time.Hour) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + runNotifierCycle(ctx) + } + } + }() +} + +func runNotifierCycle(ctx context.Context) { + if emailer == nil { + return + } + notifyTrialExpiries(ctx) + notifyRenewalReminders(ctx) +} + +func notifyTrialExpiries(ctx context.Context) { + users, err := db.UsersNeedingTrialWarning() + if err != nil { + log.Printf("Notifier: trial warning query: %v", err) + return + } + for _, u := range users { + SendTrialExpiryWarningEmail(ctx, u.Email, u.Name, 24) + db.MarkTrialWarningSent(u.ID) + log.Printf("Notifier: sent trial warning to %s", u.Email) + } +} + +func notifyRenewalReminders(ctx context.Context) { + users, err := db.UsersNeedingRenewalWarning() + if err != nil { + log.Printf("Notifier: renewal reminder query: %v", err) + return + } + for _, u := range users { + SendRenewalReminderEmail(ctx, u.Email, u.Name, 3) + db.MarkRenewalWarningSent(u.ID) + log.Printf("Notifier: sent renewal reminder to %s", u.Email) + } +} diff --git a/dash/backend/handlers/mcp.go b/dash/backend/handlers/mcp.go new file mode 100644 index 0000000..ad87b5b --- /dev/null +++ b/dash/backend/handlers/mcp.go @@ -0,0 +1,15 @@ +package handlers + +import ( + "net/http" + + "github.com/danilrybalkin/apollo-dash/tools" +) + +func MCPHandler(w http.ResponseWriter, r *http.Request) { + tools.MCPHandler(w, r) +} + +func InitMCPServers() { + tools.InitMCPServers() +} diff --git a/dash/backend/handlers/memory.go b/dash/backend/handlers/memory.go new file mode 100644 index 0000000..d8a9902 --- /dev/null +++ b/dash/backend/handlers/memory.go @@ -0,0 +1,305 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "math" + "net/http" + "os" + "sort" + "strings" + "sync" + "time" + + "github.com/danilrybalkin/apollo-dash/db" +) + +type MemoryNode struct { + ID int + SessionID string + Content string + Vector []float32 +} + +var ( + MemoryBank []MemoryNode + memoryLock sync.RWMutex +) + +func buildEmbeddingRequest(baseURL, model, text string) (*http.Request, error) { + baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") + isOpenAICompatible := strings.Contains(baseURL, "/v1") || strings.Contains(baseURL, "openrouter.ai") + endpoint := baseURL + "/api/embeddings" + payload := map[string]interface{}{ + "model": model, + "prompt": text, + } + if isOpenAICompatible { + endpoint = baseURL + "/embeddings" + payload = map[string]interface{}{ + "model": model, + "input": text, + } + } + + payloadBytes, _ := json.Marshal(payload) + req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + if isOpenAICompatible { + apiKey := strings.TrimSpace(os.Getenv("OPENROUTER_API_KEY")) + if apiKey == "" || apiKey == "your_openrouter_api_key_here" { + apiKey = strings.TrimSpace(os.Getenv("OPENAI_API_KEY")) + } + if apiKey != "" { + req.Header.Set("Authorization", "Bearer "+apiKey) + } + } + return req, nil +} + +func decodeEmbeddingResponse(resp *http.Response) ([]float32, error) { + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode/100 != 2 { + return nil, fmt.Errorf("embedding endpoint status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + + var ollamaResult struct { + Embedding []float32 `json:"embedding"` + } + if err := json.Unmarshal(body, &ollamaResult); err == nil && len(ollamaResult.Embedding) > 0 { + return ollamaResult.Embedding, nil + } + + var openAIResult struct { + Data []struct { + Embedding []float32 `json:"embedding"` + } `json:"data"` + } + if err := json.Unmarshal(body, &openAIResult); err != nil { + return nil, err + } + if len(openAIResult.Data) == 0 || len(openAIResult.Data[0].Embedding) == 0 { + return nil, fmt.Errorf("empty embedding vector") + } + return openAIResult.Data[0].Embedding, nil +} + +// Generates an embedding array for a given text using Ollama or OpenAI-compatible APIs. +func GenerateEmbedding(text string) ([]float32, error) { + settings := GetCurrentSettings("") + + req, err := buildEmbeddingRequest(settings.EmbeddingApiUrl, settings.EmbeddingModel, text) + if err != nil { + return nil, err + } + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + return decodeEmbeddingResponse(resp) +} + +// Bootstraps the in-memory cache from the SQLite database +func LoadMemoryBank() { + memoryLock.Lock() + defer memoryLock.Unlock() + + MemoryBank = []MemoryNode{} + + rows, err := db.DB.Query("SELECT id, session_id, content, vector_embedding FROM chat_messages WHERE vector_embedding != '' AND vector_embedding IS NOT NULL") + if err != nil { + log.Println("Memory System: Failed to load embeddings from DB:", err) + return + } + defer rows.Close() + + count := 0 + for rows.Next() { + var node MemoryNode + var vectorStr string + + if err := rows.Scan(&node.ID, &node.SessionID, &node.Content, &vectorStr); err != nil { + continue + } + + if err := json.Unmarshal([]byte(vectorStr), &node.Vector); err == nil { + MemoryBank = append(MemoryBank, node) + count++ + } + } + + log.Printf("Memory System: Loaded %d vectorized nodes into RAM\n", count) +} + +// Background thread that hunts for unembedded messages and calculates their RAG matrices +func StartMemoryIndexer() { + for { + time.Sleep(3 * time.Minute) + + settings := GetCurrentSettings("") + if settings.EmbeddingApiUrl == "" || settings.EmbeddingModel == "" { + continue + } + + rows, err := db.DB.Query("SELECT id, session_id, content FROM chat_messages WHERE vector_embedding = '' OR vector_embedding IS NULL LIMIT 10") + if err != nil { + continue + } + + var pending []MemoryNode + for rows.Next() { + var node MemoryNode + if err := rows.Scan(&node.ID, &node.SessionID, &node.Content); err == nil { + pending = append(pending, node) + } + } + rows.Close() + + for _, node := range pending { + plainText := node.Content + if len(plainText) > 0 && plainText[0] == '[' { + var contentArr []map[string]interface{} + if err := json.Unmarshal([]byte(plainText), &contentArr); err == nil { + for _, item := range contentArr { + if t, ok := item["type"].(string); ok && t == "text" { + if textStr, ok := item["text"].(string); ok { + plainText = textStr + break + } + } + } + } + } + + // Don't embed completely empty strings + if len(plainText) < 2 { + db.DB.Exec("UPDATE chat_messages SET vector_embedding = '[]' WHERE id = ?", node.ID) + continue + } + + vector, err := GenerateEmbedding(plainText) + if err != nil || len(vector) == 0 { + log.Println("Memory System Indexer: Failed to embed message", node.ID, err) + continue + } + + vecBytes, _ := json.Marshal(vector) + + _, err = db.DB.Exec("UPDATE chat_messages SET vector_embedding = ? WHERE id = ?", string(vecBytes), node.ID) + if err == nil { + node.Vector = vector + memoryLock.Lock() + MemoryBank = append(MemoryBank, node) + memoryLock.Unlock() + log.Println("Memory System Indexer: Successfully mapped conversation block", node.ID) + } + } + } +} +func cosineSimilarity(a, b []float32) float32 { + if len(a) != len(b) { + return 0.0 + } + var dotProduct, normA, normB float32 + for i := range a { + dotProduct += a[i] * b[i] + normA += a[i] * a[i] + normB += b[i] * b[i] + } + if normA == 0 || normB == 0 { + return 0.0 + } + return dotProduct / float32(math.Sqrt(float64(normA))*math.Sqrt(float64(normB))) +} + +type MemoryHit struct { + Node MemoryNode + Score float32 +} + +// Builds the background RAG injection string dynamically by sweeping RAM matrices +func SearchEpisodes(query string, excludeSession string) string { + vector, err := GenerateEmbedding(query) + if err != nil || len(vector) == 0 { + return "" + } + + memoryLock.RLock() + var hits []MemoryHit + for _, node := range MemoryBank { + if node.SessionID == excludeSession { + continue + } + score := cosineSimilarity(vector, node.Vector) + // 0.5 is a safe semantic threshold for normalized nomic vectors + if score > 0.5 { + hits = append(hits, MemoryHit{Node: node, Score: score}) + } + } + memoryLock.RUnlock() + + // Sort hits descending by best mathematical match + sort.Slice(hits, func(i, j int) bool { + return hits[i].Score > hits[j].Score + }) + + // Take Top 3 hits + limit := 3 + if len(hits) < limit { + limit = len(hits) + } + topHits := hits[:limit] + + if len(topHits) == 0 { + return "" + } + + var builder strings.Builder + builder.WriteString("\n") + builder.WriteString("Here are distinct chronological memories from past sessions that have strong semantic similarity to the recent user prompt context. Factor them in when providing your answer:\n\n") + + for i, hit := range topHits { + builder.WriteString(fmt.Sprintf("-- Recalled Episode %d (Similarity Score: %.2f) --\n", i+1, hit.Score)) + + // Grab robust timeline: (-2 to +2 messages) around the hit inside this session linearly + rows, err := db.DB.Query("SELECT role, content FROM chat_messages WHERE session_id = ? AND id >= ? AND id <= ? ORDER BY id ASC", hit.Node.SessionID, hit.Node.ID-2, hit.Node.ID+2) + if err == nil { + for rows.Next() { + var role, content string + rows.Scan(&role, &content) + + // Extract raw textual payload safely + plainText := content + if len(plainText) > 0 && plainText[0] == '[' { + var contentArr []map[string]interface{} + if err := json.Unmarshal([]byte(plainText), &contentArr); err == nil { + for _, item := range contentArr { + if t, ok := item["type"].(string); ok && t == "text" { + if textStr, ok := item["text"].(string); ok { + plainText = textStr + break + } + } + } + } + } + + builder.WriteString(fmt.Sprintf("[%s]: %s\n\n", strings.ToUpper(role), plainText)) + } + rows.Close() + } + } + + builder.WriteString("\n") + return builder.String() +} diff --git a/dash/backend/handlers/ping.go b/dash/backend/handlers/ping.go new file mode 100644 index 0000000..ff06e15 --- /dev/null +++ b/dash/backend/handlers/ping.go @@ -0,0 +1,142 @@ +package handlers + +import ( + "encoding/json" + "io" + "net/http" + "sync" + "time" + + "github.com/danilrybalkin/apollo-dash/db" + "github.com/go-ping/ping" +) + +type VpsHost struct { + ID int `json:"id"` + Name string `json:"name"` + IP string `json:"ip"` +} + +type PingResult struct { + Host string `json:"host"` + Name string `json:"name"` + Online bool `json:"online"` + Latency int64 `json:"latencyMs"` +} + +func getHosts() ([]VpsHost, error) { + rows, err := db.DB.Query("SELECT id, name, ip FROM vps_hosts") + if err != nil { + return nil, err + } + defer rows.Close() + + var hosts []VpsHost + for rows.Next() { + var h VpsHost + if err := rows.Scan(&h.ID, &h.Name, &h.IP); err != nil { + return nil, err + } + hosts = append(hosts, h) + } + return hosts, nil +} + +func PingHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + hosts, err := getHosts() + if err != nil { + http.Error(w, "Failed to load hosts", http.StatusInternalServerError) + return + } + + if len(hosts) == 0 { + json.NewEncoder(w).Encode([]PingResult{}) + return + } + + results := make([]PingResult, len(hosts)) + var wg sync.WaitGroup + + for i, host := range hosts { + wg.Add(1) + go func(index int, h VpsHost) { + defer wg.Done() + pinger, err := ping.NewPinger(h.IP) + if err != nil { + results[index] = PingResult{Host: h.IP, Name: h.Name, Online: false} + return + } + pinger.Count = 1 + pinger.Timeout = 2 * time.Second + pinger.SetPrivileged(true) + + err = pinger.Run() + if err != nil || pinger.Statistics().PacketsRecv == 0 { + results[index] = PingResult{Host: h.IP, Name: h.Name, Online: false} + } else { + results[index] = PingResult{ + Host: h.IP, + Name: h.Name, + Online: true, + Latency: pinger.Statistics().AvgRtt.Milliseconds(), + } + } + }(i, host) + } + + wg.Wait() + json.NewEncoder(w).Encode(results) +} + +func VpsManagerHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodPost { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Failed to read body", http.StatusBadRequest) + return + } + + var newHost VpsHost + if err := json.Unmarshal(body, &newHost); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + + if newHost.IP == "" || newHost.Name == "" { + http.Error(w, "Name and IP are required", http.StatusBadRequest) + return + } + + _, err = db.DB.Exec("INSERT INTO vps_hosts (name, ip) VALUES (?, ?)", newHost.Name, newHost.IP) + if err != nil { + http.Error(w, "Failed to save host (maybe IP already exists?)", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + return + } + + if r.Method == http.MethodDelete { + ip := r.URL.Query().Get("ip") + if ip == "" { + http.Error(w, "IP query parameter required", http.StatusBadRequest) + return + } + + _, err := db.DB.Exec("DELETE FROM vps_hosts WHERE ip = ?", ip) + if err != nil { + http.Error(w, "Internal error deleting host", http.StatusInternalServerError) + return + } + + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} diff --git a/dash/backend/handlers/seed.go b/dash/backend/handlers/seed.go new file mode 100644 index 0000000..f4bdfd2 --- /dev/null +++ b/dash/backend/handlers/seed.go @@ -0,0 +1,95 @@ +package handlers + +import ( + "log" + + "github.com/danilrybalkin/apollo-dash/db" + "github.com/google/uuid" +) + +// SeedDefaultModelProfiles creates the two starter model profiles if none exist yet. +// Called once at startup. Models can be changed later in Settings. +func SeedDefaultModelProfiles() { + var count int + if err := db.DB.QueryRow(`SELECT COUNT(*) FROM agent_model_profiles`).Scan(&count); err != nil { + return + } + if count > 0 { + return + } + + profiles := []struct { + label, provider, model string + }{ + // Ultra-cheap trial model (free tier on OpenRouter) + {"Trial Model (Free)", "openrouter", "meta-llama/llama-3.2-1b-instruct:free"}, + // Capable low-cost model for paying users + {"Standard Model", "openrouter", "openai/gpt-4o-mini"}, + } + + for _, p := range profiles { + id := uuid.New().String() + _, err := db.DB.Exec( + `INSERT INTO agent_model_profiles (id, provider, model, settings_json, fallback_chain_json) + VALUES (?, ?, ?, '{}', '[]')`, + id, p.provider, p.model, + ) + if err != nil { + log.Printf("Seed: failed to create model profile %q: %v", p.label, err) + } + } + log.Println("Seed: created 2 default model profiles") +} + +// SeedDemoCompany creates a sample company, department, and agent for a new user +// so they understand the org hierarchy immediately on first login. +func SeedDemoCompany(userID string) { + var count int + if err := db.DB.QueryRow(`SELECT COUNT(*) FROM companies WHERE owner_user_id = ?`, userID).Scan(&count); err != nil { + return + } + if count > 0 { + return // user already has a company + } + + companyID := uuid.New().String() + deptID := uuid.New().String() + agentID := uuid.New().String() + + _, err := db.DB.Exec( + `INSERT INTO companies (id, owner_user_id, name, slug, description, status) + VALUES (?, ?, 'My First Company', ?, 'Your first AI company. Edit or replace this example.', 'active')`, + companyID, userID, "company-"+companyID[:8], + ) + if err != nil { + log.Printf("Seed: failed to create demo company: %v", err) + return + } + + _, err = db.DB.Exec( + `INSERT INTO departments (id, company_id, name, type, description, status) + VALUES (?, ?, 'Product', 'general', 'Product and engineering work', 'active')`, + deptID, companyID, + ) + if err != nil { + log.Printf("Seed: failed to create demo department: %v", err) + return + } + + identityPrompt := `You are Alex, an AI product worker at My First Company. +You help with product research, writing specifications, drafting content, and planning tasks. +Be concise, practical, and always ask for clarification when a task is ambiguous. +You can be given tasks via the chat interface or scheduled to run automatically.` + + _, err = db.DB.Exec( + `INSERT INTO agents (id, company_id, department_id, name, role_type, identity_prompt, status, is_active) + VALUES (?, ?, ?, 'Alex', 'worker', ?, 'idle', 1)`, + agentID, companyID, deptID, identityPrompt, + ) + if err != nil { + log.Printf("Seed: failed to create demo agent: %v", err) + return + } + + log.Printf("Seed: created demo company/dept/agent for user %s", userID) +} diff --git a/dash/backend/handlers/settings.go b/dash/backend/handlers/settings.go new file mode 100644 index 0000000..2433dfa --- /dev/null +++ b/dash/backend/handlers/settings.go @@ -0,0 +1,215 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + "os" + "strings" + + "github.com/danilrybalkin/apollo-dash/db" +) + +// ProjectConfig represents a managed project workspace +type ProjectConfig struct { + Name string `json:"name"` + Path string `json:"path"` + DeployCommand string `json:"deploy_command"` +} + +type SettingsPayload struct { + BearerToken string `json:"bearer_token"` + FeaturedModels []string `json:"featured_models"` + DefaultModel string `json:"default_model"` + EmbeddingApiUrl string `json:"embedding_api_url"` + EmbeddingModel string `json:"embedding_model"` + SystemPrompt string `json:"-"` + ManagedProjects []ProjectConfig `json:"-"` + AutoCompactTokens int `json:"auto_compact_tokens"` + AgentOSEnabled bool `json:"agentos_enabled"` + AgentOSPolicyEnforcement string `json:"agentos_policy_enforcement"` + AgentOSKillSwitch bool `json:"agentos_kill_switch"` +} + +// ── Global settings helpers ─────────────────────────────────────────────────── + +func getSettingString(key string, fallback string) string { + var val string + err := db.DB.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&val) + if err == sql.ErrNoRows { + return fallback + } + return val +} + +func setSettingString(key string, value string) { + db.DB.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", key, value) +} + +func getSettingInt(key string, fallback int) int { + strVal := getSettingString(key, "") + if strVal == "" { + return fallback + } + var intVal int + _, err := fmt.Sscanf(strVal, "%d", &intVal) + if err != nil { + return fallback + } + return intVal +} + +func setSettingInt(key string, value int) { + setSettingString(key, fmt.Sprintf("%d", value)) +} + +func getSettingBool(key string, fallback bool) bool { + raw := strings.ToLower(strings.TrimSpace(getSettingString(key, ""))) + if raw == "" { + return fallback + } + switch raw { + case "1", "true", "yes", "on": + return true + case "0", "false", "no", "off": + return false + default: + return fallback + } +} + +// ── Per-user settings helpers ───────────────────────────────────────────────── + +// getUserSettingString reads from user_settings; falls back to global settings. +func getUserSettingString(userID, key, fallback string) string { + if userID != "" && userID != "admin" { + var val string + err := db.DB.QueryRow("SELECT value FROM user_settings WHERE user_id = ? AND key = ?", userID, key).Scan(&val) + if err == nil && strings.TrimSpace(val) != "" { + return val + } + } + return getSettingString(key, fallback) +} + +func setUserSettingString(userID, key, value string) { + if userID == "" || userID == "admin" { + setSettingString(key, value) + return + } + db.DB.Exec("INSERT OR REPLACE INTO user_settings (user_id, key, value) VALUES (?, ?, ?)", userID, key, value) +} + +func getUserSettingInt(userID, key string, fallback int) int { + strVal := getUserSettingString(userID, key, "") + if strVal == "" { + return fallback + } + var intVal int + _, err := fmt.Sscanf(strVal, "%d", &intVal) + if err != nil { + return fallback + } + return intVal +} + +func setUserSettingInt(userID, key string, value int) { + setUserSettingString(userID, key, fmt.Sprintf("%d", value)) +} + +// ResolveOpenRouterKey returns the user's personal OpenRouter key if set, +// otherwise falls back to the server-wide OPENROUTER_API_KEY env var. +func ResolveOpenRouterKey(userID string) string { + if userID != "" && userID != "admin" { + if key := db.GetUserOpenrouterKey(userID); strings.TrimSpace(key) != "" { + return strings.TrimSpace(key) + } + } + return os.Getenv("OPENROUTER_API_KEY") +} + +// ── Settings payload ────────────────────────────────────────────────────────── + +// GetCurrentSettings returns settings for a given user. Pass "" or "admin" for global/admin view. +// User-specific keys (model, prompt, tokens) come from user_settings with global fallback. +// Platform keys (embedding, agentos) always come from global settings. +func GetCurrentSettings(userID string) SettingsPayload { + // Per-user keys + var featuredModels []string + featuredBytes := getUserSettingString(userID, "featured_models", "[]") + json.Unmarshal([]byte(featuredBytes), &featuredModels) + + defaultModel := getUserSettingString(userID, "default_model", "") + systemPrompt := getUserSettingString(userID, "system_prompt", "You are Apollo, a core intelligent system. You are helpful, direct, and concise.") + autoCompactTokens := getUserSettingInt(userID, "auto_compact_tokens", 80000) + + // Global/platform keys + embeddingApiUrl := getSettingString("embedding_api_url", "http://localhost:11434") + embeddingModel := getSettingString("embedding_model", "nomic-embed-text") + + var managedProjects []ProjectConfig + managedProjectsBytes := getSettingString("managed_projects", "[]") + json.Unmarshal([]byte(managedProjectsBytes), &managedProjects) + + agentOSEnabled := getSettingBool("agentos_enabled", true) + agentOSPolicyEnforcement := getSettingString("agentos_policy_enforcement", "deny_default") + agentOSKillSwitch := getSettingBool("agentos_kill_switch", false) + + return SettingsPayload{ + BearerToken: "", + FeaturedModels: featuredModels, + DefaultModel: defaultModel, + EmbeddingApiUrl: embeddingApiUrl, + EmbeddingModel: embeddingModel, + SystemPrompt: systemPrompt, + ManagedProjects: managedProjects, + AutoCompactTokens: autoCompactTokens, + AgentOSEnabled: agentOSEnabled, + AgentOSPolicyEnforcement: agentOSPolicyEnforcement, + AgentOSKillSwitch: agentOSKillSwitch, + } +} + +// Handler for fetching and updating app settings +func SettingsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + userID := CurrentUserID(r) + + if r.Method == http.MethodGet { + payload := GetCurrentSettings(userID) + json.NewEncoder(w).Encode(payload) + return + } + + if r.Method == http.MethodPost { + payload := GetCurrentSettings(userID) + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + // Per-user settings — any authenticated user can set these + if payload.FeaturedModels != nil { + bytes, _ := json.Marshal(payload.FeaturedModels) + setUserSettingString(userID, "featured_models", string(bytes)) + } + setUserSettingString(userID, "default_model", payload.DefaultModel) + setUserSettingString(userID, "system_prompt", payload.SystemPrompt) + setUserSettingInt(userID, "auto_compact_tokens", payload.AutoCompactTokens) + + // Platform settings — admin only + if userID == "admin" { + setSettingString("embedding_api_url", payload.EmbeddingApiUrl) + setSettingString("embedding_model", payload.EmbeddingModel) + setSettingString("agentos_enabled", fmt.Sprintf("%t", payload.AgentOSEnabled)) + setSettingString("agentos_policy_enforcement", payload.AgentOSPolicyEnforcement) + setSettingString("agentos_kill_switch", fmt.Sprintf("%t", payload.AgentOSKillSwitch)) + } + + json.NewEncoder(w).Encode(map[string]string{"status": "success"}) + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} diff --git a/dash/backend/handlers/subagents.go b/dash/backend/handlers/subagents.go new file mode 100644 index 0000000..770a40c --- /dev/null +++ b/dash/backend/handlers/subagents.go @@ -0,0 +1,208 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" + + "github.com/danilrybalkin/apollo-dash/db" + "github.com/danilrybalkin/apollo-dash/tools" + "github.com/google/uuid" +) + +// SubagentRecord represents a background subagent record in the DB +type SubagentRecord struct { + ID string `json:"id"` + SessionID string `json:"session_id"` + Name string `json:"name"` + Task string `json:"task"` + Status string `json:"status"` // running | done | error + Output string `json:"output"` + CreatedAt string `json:"created_at"` +} + +// SubagentsHandler: GET /api/subagents → list all +// +// DELETE /api/subagents?id= → cancel/remove one +func SubagentsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodGet { + rows, err := db.DB.Query("SELECT id, IFNULL(session_id,''), name, task, status, IFNULL(output,''), created_at FROM subagents ORDER BY created_at DESC") + if err != nil { + http.Error(w, "DB error", http.StatusInternalServerError) + return + } + defer rows.Close() + var agents []SubagentRecord + for rows.Next() { + var a SubagentRecord + rows.Scan(&a.ID, &a.SessionID, &a.Name, &a.Task, &a.Status, &a.Output, &a.CreatedAt) + agents = append(agents, a) + } + if agents == nil { + agents = []SubagentRecord{} + } + json.NewEncoder(w).Encode(agents) + return + } + + if r.Method == http.MethodDelete { + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, "id is required", http.StatusBadRequest) + return + } + db.DB.Exec("DELETE FROM subagents WHERE id = ?", id) + json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}) + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +// SpawnSubagent creates a DB record and launches a background goroutine that runs +// an independent LLM+tool loop for the given task. +func SpawnSubagent(sessionID, name, task string) string { + id := uuid.New().String() + db.DB.Exec( + "INSERT INTO subagents (id, session_id, name, task, status, output) VALUES (?, ?, ?, ?, 'running', '')", + id, sessionID, name, task, + ) + log.Printf("Subagent [%s] spawned: %s", name, task) + + go runSubagent(id, name, task, sessionID) + return id +} + +// GetSubagentStatus returns the current status and output of a subagent +func GetSubagentStatus(id string) (status, output string) { + db.DB.QueryRow("SELECT status, IFNULL(output,'') FROM subagents WHERE id = ?", id).Scan(&status, &output) + return +} + +// runSubagent is the background goroutine that executes the subagent's LLM+tool loop +func runSubagent(id, name, task, sessionID string) { + defer func() { + if r := recover(); r != nil { + db.DB.Exec("UPDATE subagents SET status='error', output=? WHERE id=?", fmt.Sprintf("Panic: %v", r), id) + } + }() + + apiKey := os.Getenv("OPENROUTER_API_KEY") + ollamaUrl := os.Getenv("OLLAMA_API_URL") + + var targetUrl, reqApiKey, model string + + settings := GetCurrentSettings("") + if settings.DefaultModel != "" { + model = settings.DefaultModel + } else { + model = "meta-llama/llama-3-8b-instruct:free" + } + + if ollamaUrl != "" { + targetUrl = ollamaUrl + "/v1/chat/completions" + reqApiKey = "Bearer local" + } else { + targetUrl = "https://openrouter.ai/api/v1/chat/completions" + reqApiKey = "Bearer " + apiKey + } + + sysPrompt := fmt.Sprintf(`You are a background subagent named "%s". Your sole task is: + +%s + +Work autonomously using your tools. When you are done, produce a comprehensive report of your findings, actions taken, or results. Be thorough.`, name, task) + + messages := []map[string]interface{}{ + {"role": "system", "content": sysPrompt}, + {"role": "user", "content": "Begin working on your assigned task."}, + } + + var outputLog string + var projectPath string + if sessionID != "" { + db.DB.QueryRow("SELECT IFNULL(project_path, '') FROM chat_sessions WHERE id = ?", sessionID).Scan(&projectPath) + } + + // Tool loop — up to 8 turns to complete the task + client := &http.Client{Timeout: 300 * time.Second} + for attempt := 0; attempt < 8; attempt++ { + payload, _ := json.Marshal(map[string]interface{}{ + "model": model, + "messages": messages, + "stream": false, + "tools": tools.GetAvailableTools(), + }) + req, _ := http.NewRequest("POST", targetUrl, bytes.NewBuffer(payload)) + req.Header.Set("Authorization", reqApiKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil || resp.StatusCode != 200 { + db.DB.Exec("UPDATE subagents SET status='error', output=? WHERE id=?", "LLM request failed: "+err.Error(), id) + return + } + + var result struct { + Choices []struct { + Message struct { + Content string `json:"content"` + ToolCalls []map[string]interface{} `json:"tool_calls"` + } `json:"message"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + } + json.NewDecoder(resp.Body).Decode(&result) + resp.Body.Close() + + if len(result.Choices) == 0 { + break + } + + msg := result.Choices[0].Message + if msg.Content != "" { + outputLog += msg.Content + "\n\n" + } + + // Execute tool calls if any + if len(msg.ToolCalls) > 0 { + messages = append(messages, map[string]interface{}{ + "role": "assistant", + "content": msg.Content, + "tool_calls": msg.ToolCalls, + }) + for _, tc := range msg.ToolCalls { + id2, _ := tc["id"].(string) + funcObj, _ := tc["function"].(map[string]interface{}) + tName, _ := funcObj["name"].(string) + tArgs, _ := funcObj["arguments"].(string) + result := tools.ExecuteTool(tName, tArgs, projectPath) + outputLog += fmt.Sprintf("[Tool: %s] %s\n\n", tName, result) + messages = append(messages, map[string]interface{}{ + "role": "tool", "tool_call_id": id2, "name": tName, "content": result, + }) + } + continue + } + + // No tool calls → the agent is done + if result.Choices[0].FinishReason == "stop" || msg.Content != "" { + break + } + } + + // Store final output, notify session + db.DB.Exec("UPDATE subagents SET status='done', output=? WHERE id=?", outputLog, id) + if sessionID != "" { + // Inject a system notification into the session's next context via a DB message + notification := fmt.Sprintf("\nSubagent **%s** has finished its task.\n\n**Summary:**\n%s\n", name, outputLog) + db.DB.Exec("INSERT INTO chat_messages (session_id, role, content) VALUES (?, 'system', ?)", sessionID, notification) + } + log.Printf("Subagent [%s] completed.", name) +} diff --git a/dash/backend/handlers/sys.go b/dash/backend/handlers/sys.go new file mode 100644 index 0000000..631de91 --- /dev/null +++ b/dash/backend/handlers/sys.go @@ -0,0 +1,108 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "os/exec" + + "github.com/danilrybalkin/apollo-dash/tools" + "github.com/shirou/gopsutil/v3/cpu" + "github.com/shirou/gopsutil/v3/disk" + "github.com/shirou/gopsutil/v3/host" + "github.com/shirou/gopsutil/v3/mem" + "github.com/shirou/gopsutil/v3/net" +) + +type SysMetrics struct { + CPUPercent []float64 `json:"cpuPercent"` + MemTotal uint64 `json:"memTotal"` + MemUsed uint64 `json:"memUsed"` + MemPercent float64 `json:"memPercent"` + DiskTotal uint64 `json:"diskTotal"` + DiskUsed uint64 `json:"diskUsed"` + DiskPercent float64 `json:"diskPercent"` + NetSent uint64 `json:"netSent"` + NetRecv uint64 `json:"netRecv"` + HostOS string `json:"hostOs"` + Uptime uint64 `json:"uptime"` + Temps []host.TemperatureStat `json:"temps"` +} + +func SysStatsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + metrics := SysMetrics{} + + // CPU + cpuPercents, err := cpu.Percent(0, false) + if err == nil { + metrics.CPUPercent = cpuPercents + } + + // Memory + vMem, err := mem.VirtualMemory() + if err == nil { + metrics.MemTotal = vMem.Total + metrics.MemUsed = vMem.Used + metrics.MemPercent = vMem.UsedPercent + } + + // Disk (root) + d, err := disk.Usage("/") + if err == nil { + metrics.DiskTotal = d.Total + metrics.DiskUsed = d.Used + metrics.DiskPercent = d.UsedPercent + } + + // Network + nv, err := net.IOCounters(false) + if err == nil && len(nv) > 0 { + metrics.NetSent = nv[0].BytesSent + metrics.NetRecv = nv[0].BytesRecv + } + + // Host + hInfo, err := host.Info() + if err == nil { + metrics.HostOS = hInfo.OS + metrics.Uptime = hInfo.Uptime + } + + // Temps + // Note: gopsutil temps might return empty depending on OS (macOS is notorious for missing SMC sensors without extra privileges) + tStats, err := host.SensorsTemperatures() + if err == nil { + metrics.Temps = tStats + } + + json.NewEncoder(w).Encode(metrics) +} + +func SandboxRestoreHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var payload struct { + Hash string `json:"hash"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil || payload.Hash == "" { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + cmd := exec.Command("git", "reset", "--hard", payload.Hash) + cmd.Dir = tools.WorkspaceDir + out, err := cmd.CombinedOutput() + + w.Header().Set("Content-Type", "application/json") + if err != nil { + json.NewEncoder(w).Encode(map[string]string{"error": fmt.Sprintf("Error: %v, Output: %s", err, string(out))}) + return + } + + json.NewEncoder(w).Encode(map[string]string{"success": "true"}) +} diff --git a/dash/backend/handlers/terminal.go b/dash/backend/handlers/terminal.go new file mode 100644 index 0000000..fef19af --- /dev/null +++ b/dash/backend/handlers/terminal.go @@ -0,0 +1,102 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + "os" + "os/exec" + + "github.com/creack/pty" + "github.com/gorilla/websocket" +) + +var wsUpgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + // The whole app is behind BasicAuth so we trust same-origin connections + return true + }, + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +// TerminalHandler: GET /ws/terminal +// Upgrades to a WebSocket connection, spawns a bash PTY, and proxies data bidirectionally. +func TerminalHandler(w http.ResponseWriter, r *http.Request) { + conn, err := wsUpgrader.Upgrade(w, r, nil) + if err != nil { + log.Println("WebSocket upgrade failed:", err) + return + } + defer conn.Close() + + // Determine starting directory + startDir := os.Getenv("HOME") + if startDir == "" { + startDir = "/" + } + if projectPath := r.URL.Query().Get("path"); projectPath != "" { + if _, err := os.Stat(projectPath); err == nil { + startDir = projectPath + } + } + + // Spawn a shell inside a PTY + shell := os.Getenv("SHELL") + if shell == "" { + shell = "/bin/bash" + } + cmd := exec.Command(shell) + cmd.Env = append(os.Environ(), "TERM=xterm-256color") + cmd.Dir = startDir + + ptmx, err := pty.Start(cmd) + if err != nil { + log.Println("PTY start failed:", err) + conn.WriteMessage(websocket.TextMessage, []byte("Failed to start terminal: "+err.Error())) + return + } + defer func() { + ptmx.Close() + cmd.Process.Kill() + }() + + // PTY → WebSocket: forward all terminal output to the browser + go func() { + buf := make([]byte, 1024) + for { + n, err := ptmx.Read(buf) + if n > 0 { + if err2 := conn.WriteMessage(websocket.BinaryMessage, buf[:n]); err2 != nil { + return + } + } + if err != nil { + return + } + } + }() + + // WebSocket → PTY: forward all keyboard input from browser to the shell + for { + msgType, data, err := conn.ReadMessage() + if err != nil { + break + } + if msgType == websocket.TextMessage || msgType == websocket.BinaryMessage { + // Check for a resize message: JSON {"type":"resize","cols":N,"rows":N} + if len(data) > 0 && data[0] == '{' { + var msg struct { + Type string `json:"type"` + Cols uint16 `json:"cols"` + Rows uint16 `json:"rows"` + } + if err := json.Unmarshal(data, &msg); err == nil && msg.Type == "resize" && msg.Cols > 0 && msg.Rows > 0 { + pty.Setsize(ptmx, &pty.Winsize{Cols: msg.Cols, Rows: msg.Rows}) + continue + } + } + ptmx.Write(data) + } + } +} diff --git a/dash/backend/handlers/user_auth.go b/dash/backend/handlers/user_auth.go new file mode 100644 index 0000000..f340544 --- /dev/null +++ b/dash/backend/handlers/user_auth.go @@ -0,0 +1,299 @@ +package handlers + +import ( + "context" + "encoding/json" + "net/http" + "strings" + + "github.com/danilrybalkin/apollo-dash/db" + "github.com/google/uuid" + "golang.org/x/crypto/bcrypt" +) + +func extractBearerToken(r *http.Request) string { + auth := r.Header.Get("Authorization") + if strings.HasPrefix(auth, "Bearer ") { + return strings.TrimPrefix(auth, "Bearer ") + } + return "" +} + +func RegisterHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var body struct { + Email string `json:"email"` + Password string `json:"password"` + Name string `json:"name"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if body.Email == "" || body.Password == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "email and password required"}) + return + } + if len(body.Password) < 8 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "password_too_short"}) + return + } + if db.EmailExists(body.Email) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusConflict) + json.NewEncoder(w).Encode(map[string]string{"error": "email_taken"}) + return + } + hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), bcrypt.DefaultCost) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "internal_error"}) + return + } + userID := uuid.New().String() + user, err := db.CreateUser(userID, body.Email, string(hash), body.Name) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "internal_error"}) + return + } + token := uuid.New().String() + if err := db.CreateUserToken(token, userID); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "internal_error"}) + return + } + // Seed demo data and send welcome email after token is committed + go SeedDemoCompany(userID) + go SendWelcomeEmail(context.Background(), body.Email, body.Name) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "user": user, + "token": token, + }) +} + +func LoginHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + var body struct { + Email string `json:"email"` + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + id, _, hash, _, isBlocked, err := db.GetUserByEmail(body.Email) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid_credentials"}) + return + } + if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(body.Password)); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid_credentials"}) + return + } + if isBlocked == 1 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusForbidden) + json.NewEncoder(w).Encode(map[string]string{"error": "account_blocked"}) + return + } + token := uuid.New().String() + if err := db.CreateUserToken(token, id); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + user, err := db.GetUserByID(id) + if err != nil || user == nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "user": user, + "token": token, + }) +} + +func MeHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet && r.Method != http.MethodPatch { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + token := extractBearerToken(r) + if token == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + user, err := db.GetUserByToken(token) + if err != nil || user == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + w.Header().Set("Content-Type", "application/json") + if r.Method == http.MethodPatch { + var body struct { + Name string `json:"name"` + OpenrouterAPIKey string `json:"openrouter_api_key"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if body.Name != "" { + if err := db.UpdateUserName(user.ID, body.Name); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + user.Name = body.Name + } + if body.OpenrouterAPIKey != "" { + if err := db.UpdateUserOpenrouterKey(user.ID, body.OpenrouterAPIKey); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + user.OpenrouterAPIKey = body.OpenrouterAPIKey + } + } + json.NewEncoder(w).Encode(user) +} + +func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + token := extractBearerToken(r) + if token == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + user, err := db.GetUserByToken(token) + if err != nil || user == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + var body struct { + CurrentPassword string `json:"current_password"` + NewPassword string `json:"new_password"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + if len(body.NewPassword) < 8 { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "password_too_short"}) + return + } + currentHash, err := db.GetUserPasswordHash(user.ID) + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(body.CurrentPassword)); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid_current_password"}) + return + } + newHash, err := bcrypt.GenerateFromPassword([]byte(body.NewPassword), bcrypt.DefaultCost) + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + if err := db.UpdateUserPassword(user.ID, string(newHash)); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} + +func DeleteAccountHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + token := extractBearerToken(r) + if token == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + user, err := db.GetUserByToken(token) + if err != nil || user == nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "unauthorized"}) + return + } + // Require password confirmation + var body struct { + Password string `json:"password"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Password == "" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(map[string]string{"error": "password required to delete account"}) + return + } + currentHash, err := db.GetUserPasswordHash(user.ID) + if err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + if err := bcrypt.CompareHashAndPassword([]byte(currentHash), []byte(body.Password)); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusUnauthorized) + json.NewEncoder(w).Encode(map[string]string{"error": "invalid_password"}) + return + } + if err := db.DeleteUser(user.ID); err != nil { + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "deleted"}) +} + +func LogoutHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + token := extractBearerToken(r) + if token != "" { + db.DeleteUserToken(token) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) +} diff --git a/dash/backend/handlers/workspace.go b/dash/backend/handlers/workspace.go new file mode 100644 index 0000000..8aa868c --- /dev/null +++ b/dash/backend/handlers/workspace.go @@ -0,0 +1,314 @@ +package handlers + +import ( + "encoding/json" + "io" + "net/http" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + + "github.com/danilrybalkin/apollo-dash/db" +) + +// WorkspaceTreeNode represents a file or directory in the tree +type WorkspaceTreeNode struct { + Name string `json:"name"` + Path string `json:"path"` + IsDir bool `json:"isDir"` + Children []*WorkspaceTreeNode `json:"children,omitempty"` +} + +func withinRoot(path string, root string) bool { + rel, err := filepath.Rel(root, path) + if err != nil { + return false + } + if rel == "." { + return true + } + return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) +} + +// getProjectRoots returns mapped workspace roots for one user from companies. +func getProjectRoots(ownerUserID string) []ProjectConfig { + rows, err := db.DB.Query(` + SELECT IFNULL(name,''), IFNULL(workspace_path,''), IFNULL(deploy_command,'') + FROM companies + WHERE owner_user_id = ? AND TRIM(IFNULL(workspace_path,'')) <> '' + ORDER BY created_at ASC + `, ownerUserID) + if err == nil { + defer rows.Close() + var out []ProjectConfig + for rows.Next() { + var p ProjectConfig + if scanErr := rows.Scan(&p.Name, &p.Path, &p.DeployCommand); scanErr != nil { + continue + } + p.Path = strings.TrimSpace(p.Path) + if p.Path == "" { + continue + } + out = append(out, p) + } + if len(out) > 0 { + return out + } + } + + return []ProjectConfig{} +} + +// securePath validates that the requested path is within one of the allowed project roots +func secureWorkspacePath(ownerUserID string, requestedPath string) (string, bool) { + abs, err := filepath.Abs(requestedPath) + if err != nil { + return "", false + } + roots := getProjectRoots(ownerUserID) + for _, p := range roots { + rootAbs, err := filepath.Abs(p.Path) + if err != nil { + continue + } + if withinRoot(abs, rootAbs) { + return abs, true + } + } + return "", false +} + +// buildTree recursively builds a WorkspaceTreeNode tree up to maxDepth levels +func buildTree(root string, displayPath string, depth int) *WorkspaceTreeNode { + info, err := os.Stat(root) + if err != nil { + return nil + } + node := &WorkspaceTreeNode{ + Name: info.Name(), + Path: displayPath, + IsDir: info.IsDir(), + } + if !info.IsDir() || depth <= 0 { + return node + } + entries, err := os.ReadDir(root) + if err != nil { + return node + } + // Sort: dirs first, then files + sort.Slice(entries, func(i, j int) bool { + if entries[i].IsDir() != entries[j].IsDir() { + return entries[i].IsDir() + } + return entries[i].Name() < entries[j].Name() + }) + for _, entry := range entries { + name := entry.Name() + // Skip hidden files and common noise + if strings.HasPrefix(name, ".") || name == "node_modules" || name == "__pycache__" || name == "venv" || name == "dist" { + continue + } + childPath := filepath.Join(root, name) + childDisplay := filepath.Join(displayPath, name) + child := buildTree(childPath, childDisplay, depth-1) + if child != nil { + node.Children = append(node.Children, child) + } + } + return node +} + +// WorkspaceProjectsHandler: GET /api/workspace/projects +// Returns the list of company-mapped workspace roots. +func WorkspaceProjectsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + projects := getProjectRoots(CurrentUserID(r)) + if projects == nil { + projects = []ProjectConfig{} + } + json.NewEncoder(w).Encode(projects) +} + +// WorkspaceTreeHandler: GET /api/workspace/tree?path= +// Returns the file tree for the given directory path (must be within a mapped company workspace). +func WorkspaceTreeHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + requestedPath := r.URL.Query().Get("path") + if requestedPath == "" { + // Return all project roots as a virtual tree + roots := getProjectRoots(CurrentUserID(r)) + var nodes []*WorkspaceTreeNode + for _, p := range roots { + node := buildTree(p.Path, p.Path, 20) + if node != nil { + node.Name = p.Name // Use friendly name as tree root label + nodes = append(nodes, node) + } + } + if nodes == nil { + nodes = []*WorkspaceTreeNode{} + } + json.NewEncoder(w).Encode(nodes) + return + } + safePath, ok := secureWorkspacePath(CurrentUserID(r), requestedPath) + if !ok { + http.Error(w, "Path not within a managed project", http.StatusForbidden) + return + } + node := buildTree(safePath, safePath, 20) + if node == nil { + http.Error(w, "Path not found", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(node) +} + +// WorkspaceFileHandler handles GET/POST /api/workspace/file +func WorkspaceFileHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + if r.Method == http.MethodGet { + // Read file contents + requestedPath := r.URL.Query().Get("path") + if requestedPath == "" { + http.Error(w, "path is required", http.StatusBadRequest) + return + } + safePath, ok := secureWorkspacePath(CurrentUserID(r), requestedPath) + if !ok { + http.Error(w, "Path not within a managed project", http.StatusForbidden) + return + } + content, err := os.ReadFile(safePath) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(map[string]string{ + "path": safePath, + "content": string(content), + }) + return + } + + if r.Method == http.MethodPost { + // Write file contents + var payload struct { + Path string `json:"path"` + Content string `json:"content"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + safePath, ok := secureWorkspacePath(CurrentUserID(r), payload.Path) + if !ok { + http.Error(w, "Path not within a managed project", http.StatusForbidden) + return + } + // Ensure parent directory exists + if err := os.MkdirAll(filepath.Dir(safePath), 0755); err != nil { + http.Error(w, "Failed to create directory", http.StatusInternalServerError) + return + } + if err := os.WriteFile(safePath, []byte(payload.Content), 0644); err != nil { + http.Error(w, "Failed to write file", http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(map[string]string{"status": "saved"}) + return + } + + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) +} + +// WorkspaceDeployHandler: POST /api/workspace/deploy +// Runs the configured deploy command for a project and streams stdout +func WorkspaceDeployHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + var payload struct { + Project string `json:"project"` + } + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + var target *ProjectConfig + for _, p := range getProjectRoots(CurrentUserID(r)) { + if p.Name == payload.Project { + pc := p + target = &pc + break + } + } + if target == nil { + http.Error(w, "Project not found", http.StatusNotFound) + return + } + + // Stream deploy output as SSE + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + flusher, ok := w.(http.Flusher) + + cmd := exec.Command("bash", "-c", target.DeployCommand) + cmd.Dir = target.Path + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + http.Error(w, "Failed to create stdout pipe", http.StatusInternalServerError) + return + } + cmd.Stderr = cmd.Stdout // Merge stderr into stdout + + if err := cmd.Start(); err != nil { + http.Error(w, "Failed to start deploy command: "+err.Error(), http.StatusInternalServerError) + return + } + + buf := make([]byte, 512) + for { + n, err := stdoutPipe.Read(buf) + if n > 0 { + line := string(buf[:n]) + data, _ := json.Marshal(map[string]string{"output": line}) + w.Write([]byte("data: ")) + w.Write(data) + w.Write([]byte("\n\n")) + if ok { + flusher.Flush() + } + } + if err == io.EOF { + break + } + if err != nil { + break + } + } + + cmd.Wait() + w.Write([]byte("data: {\"output\":\"\\n[Deploy complete]\\n\",\"done\":true}\n\n")) + if ok { + flusher.Flush() + } +} diff --git a/dash/backend/main.go b/dash/backend/main.go new file mode 100644 index 0000000..d72fe4f --- /dev/null +++ b/dash/backend/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/danilrybalkin/apollo-dash/agentos" + "github.com/danilrybalkin/apollo-dash/db" + "github.com/danilrybalkin/apollo-dash/handlers" + "github.com/danilrybalkin/apollo-dash/tools" + "github.com/joho/godotenv" +) + +func main() { + // Load .env + err := godotenv.Load() + if err != nil { + log.Println("No .env file found or error loading it. Using environment variables.") + } + + db.InitDB() + handlers.SeedDefaultModelProfiles() + handlers.InitEmailer() + tools.InitWorkspace() + tools.SyncBundledSkills() + tools.SetSubagentFuncs(handlers.SpawnSubagent, handlers.GetSubagentStatus) + + workspaceRoot := os.Getenv("AGENTHQ_WORKSPACE_ROOT") + if workspaceRoot == "" { + workspaceRoot = tools.WorkspaceDir + } + agentSvc := agentos.NewService(db.DB, workspaceRoot) + handlers.SetAgentOSService(agentSvc) + go agentSvc.Start(context.Background()) + handlers.StartEmailNotifier(context.Background()) + + // Initialize RAG Memory Engine + handlers.LoadMemoryBank() + go handlers.StartMemoryIndexer() + + // Initialize MCP servers (reconnect persisted servers) + go handlers.InitMCPServers() + + port := os.Getenv("PORT") + if port == "" { + port = "4000" + } + + mux := http.NewServeMux() + + // Auth routes (public — no auth required) + mux.HandleFunc("/api/auth/register", handlers.RegisterHandler) + mux.HandleFunc("/api/auth/login", handlers.LoginHandler) + mux.HandleFunc("/api/auth/me", handlers.MeHandler) + mux.HandleFunc("/api/auth/change-password", handlers.ChangePasswordHandler) + mux.HandleFunc("/api/auth/delete-account", handlers.DeleteAccountHandler) + mux.HandleFunc("/api/auth/logout", handlers.LogoutHandler) + + // Admin routes (require DASHBOARD_PASSWORD bearer token) + mux.HandleFunc("/api/admin/stats", handlers.AdminStatsHandler) + mux.HandleFunc("/api/admin/users", handlers.AdminUsersHandler) + mux.HandleFunc("/api/admin/users/", handlers.AdminUserByIDHandler) + + // Billing (Vulta payments + webhook) + mux.HandleFunc("/api/billing/checkout", handlers.BillingCheckoutHandler) + mux.HandleFunc("/api/billing/webhook", handlers.BillingWebhookHandler) + + // API Routes under /api/* + mux.HandleFunc("/api/sys", handlers.SysStatsHandler) + mux.HandleFunc("/api/ping", handlers.PingHandler) + mux.HandleFunc("/api/vps", handlers.VpsManagerHandler) + mux.HandleFunc("/api/chat", handlers.AiChatHandler) + mux.HandleFunc("/api/models", handlers.AiModelsHandler) + mux.HandleFunc("/api/sessions", handlers.AiSessionsHandler) + mux.HandleFunc("/api/messages", handlers.AiMessagesHandler) + mux.HandleFunc("/api/settings", handlers.SettingsHandler) + mux.HandleFunc("/api/sandbox/restore", handlers.SandboxRestoreHandler) + + // Workspace IDE routes + mux.HandleFunc("/api/workspace/projects", handlers.WorkspaceProjectsHandler) + mux.HandleFunc("/api/workspace/tree", handlers.WorkspaceTreeHandler) + mux.HandleFunc("/api/workspace/file", handlers.WorkspaceFileHandler) + mux.HandleFunc("/api/workspace/deploy", handlers.WorkspaceDeployHandler) + + // Background Subagents + mux.HandleFunc("/api/subagents", handlers.SubagentsHandler) + mux.HandleFunc("/api/companies", handlers.AgentOSCompaniesHandler) + mux.HandleFunc("/api/companies/", handlers.AgentOSCompanyByIDHandler) + // Compatibility aliases for reverse proxies that strip the /api prefix. + mux.HandleFunc("/companies", handlers.AgentOSCompaniesHandler) + mux.HandleFunc("/companies/", handlers.AgentOSCompanyByIDHandler) + mux.HandleFunc("/api/departments", handlers.AgentOSDepartmentsHandler) + mux.HandleFunc("/api/departments/", handlers.AgentOSDepartmentByIDHandler) + mux.HandleFunc("/api/agents", handlers.AgentOSAgentsHandler) + mux.HandleFunc("/api/agents/", handlers.AgentOSAgentByIDHandler) + mux.HandleFunc("/api/model-profiles", handlers.AgentOSModelProfilesHandler) + mux.HandleFunc("/api/threads", handlers.AgentOSThreadsHandler) + mux.HandleFunc("/api/threads/", handlers.AgentOSThreadMessagesHandler) + mux.HandleFunc("/api/tasks", handlers.AgentOSTasksHandler) + mux.HandleFunc("/api/tasks/", handlers.AgentOSTaskByIDHandler) + mux.HandleFunc("/api/consensus/rounds", handlers.AgentOSConsensusRoundsHandler) + mux.HandleFunc("/api/consensus/rounds/", handlers.AgentOSConsensusRoundByIDHandler) + mux.HandleFunc("/api/memory/query", handlers.AgentOSMemoryQueryHandler) + mux.HandleFunc("/api/memory/write", handlers.AgentOSMemoryWriteHandler) + mux.HandleFunc("/api/memory/timeline", handlers.AgentOSMemoryTimelineHandler) + mux.HandleFunc("/api/schedules", handlers.AgentOSSchedulesHandler) + mux.HandleFunc("/api/schedules/", handlers.AgentOSScheduleByIDHandler) + mux.HandleFunc("/api/events", handlers.AgentOSEventsHandler) + mux.HandleFunc("/api/events/stream", handlers.AgentOSEventsStreamHandler) + mux.HandleFunc("/api/policies", handlers.AgentOSPoliciesHandler) + mux.HandleFunc("/api/policies/test", handlers.AgentOSPolicyTestHandler) + mux.HandleFunc("/api/approvals", handlers.AgentOSApprovalsHandler) + mux.HandleFunc("/api/approvals/resolve", handlers.AgentOSApprovalsResolveHandler) + mux.HandleFunc("/api/audit", handlers.AgentOSAuditHandler) + mux.HandleFunc("/api/audit/verify", handlers.AgentOSAuditVerifyHandler) + mux.HandleFunc("/api/health/agentos", handlers.AgentOSHealthHandler) + mux.HandleFunc("/api/topology", handlers.AgentOSTopologyHandler) + mux.HandleFunc("/api/inter-agent", handlers.AgentInboxHandler) + + // Execution Mode Confirmations + mux.HandleFunc("/api/confirm", handlers.ConfirmHandler) + + // MCP (Model Context Protocol) server management + mux.HandleFunc("/api/mcp", handlers.MCPHandler) + + // WebSocket Terminal (PTY) + mux.HandleFunc("/ws/terminal", handlers.TerminalHandler) + + // Third-party API Endpoints (OpenAI format wrapper) + mux.HandleFunc("/api/ext/chat/completions", handlers.AiExternalChatHandler) + mux.HandleFunc("/api/ext/models", handlers.AiModelsHandler) + + // Combine API routes and wrap them in auth (if desired to protect API) + // Or we can protect everything (Static + API) + // Let's protect EVERYTHING so the dashboard is a true Auth Wall. + + // Serve React Frontend + // The frontend/dist folder will be created by Vite build + frontendDir := filepath.Join("..", "frontend", "dist") + fs := http.FileServer(http.Dir(frontendDir)) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + cleanPath := strings.TrimPrefix(filepath.Clean(r.URL.Path), string(filepath.Separator)) + requestedPath := filepath.Join(frontendDir, cleanPath) + if info, err := os.Stat(requestedPath); err == nil && !info.IsDir() { + fs.ServeHTTP(w, r) + return + } + http.ServeFile(w, r, filepath.Join(frontendDir, "index.html")) + }) + + // Wrap entire mux with Auth + protectedMux := handlers.BasicAuthMiddleware(mux) + + fmt.Printf("AgentHQ Backend running on port %s\n", port) + fmt.Printf("Serving static files from %s\n", frontendDir) + log.Fatal(http.ListenAndServe(":"+port, protectedMux)) +} diff --git a/dash/backend/skills/deployment.md b/dash/backend/skills/deployment.md new file mode 100644 index 0000000..41a6e0b --- /dev/null +++ b/dash/backend/skills/deployment.md @@ -0,0 +1,124 @@ +--- +description: Docker and deployment — Dockerfile best practices, docker-compose, systemd services, nginx reverse proxy, zero-downtime deploys +--- + +# Deployment Skill + +## Dockerfile Best Practices + +### Multi-stage Go build +```dockerfile +FROM golang:1.24-alpine AS builder +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download # cache dependency layer separately +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server . + +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates tzdata +WORKDIR /app +COPY --from=builder /app/server . +COPY --from=builder /app/skills ./skills +EXPOSE 4000 +CMD ["./server"] +``` + +### Layer caching rules +- Copy `go.mod` + `go.sum` BEFORE source code — `go mod download` cache only invalidates when dependencies change +- Put rarely-changing layers (apt installs) before frequently-changing ones (source code) +- Use `.dockerignore` to exclude `node_modules`, `.git`, `dist` + +## Docker Compose +```yaml +services: + agenthq: + build: ./dash/backend + ports: ["4000:4000"] + environment: + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + - AGENTHQ_WORKSPACE_ROOT=/app/data/workspaces + volumes: + - agenthq-data:/app/data + restart: unless-stopped + + frontend: + build: ./dash/frontend + depends_on: [agenthq] + +volumes: + agenthq-data: +``` + +## systemd Service +```ini +# /etc/systemd/system/apollo.service +[Unit] +Description=Apollo Dashboard +After=network.target + +[Service] +Type=simple +User=apollo +WorkingDirectory=/opt/apollo +ExecStart=/opt/apollo/server +Restart=on-failure +RestartSec=5s +Environment=PORT=4000 +EnvironmentFile=/opt/apollo/.env + +[Install] +WantedBy=multi-user.target +``` +```bash +systemctl daemon-reload && systemctl enable --now apollo +journalctl -u apollo -f # follow logs +``` + +## nginx Reverse Proxy +```nginx +server { + listen 443 ssl; + server_name apollo.yourdomain.com; + + ssl_certificate /etc/letsencrypt/live/apollo.yourdomain.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/apollo.yourdomain.com/privkey.pem; + + location / { + proxy_pass http://localhost:4000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; # WebSocket support + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + # SSE: disable buffering + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 300s; + } +} +``` + +## Zero-Downtime Deploy +```bash +# Build new binary +go build -o server.new . + +# Swap atomically +mv server server.old && mv server.new server + +# Graceful restart (sends SIGTERM → process drains → exits) +kill -SIGTERM $(pidof server) +# Or with systemd: +systemctl reload apollo # if ExecReload is configured +systemctl restart apollo # hard restart + +# Keep old binary for quick rollback +``` + +## SSL Certificate (Let's Encrypt) +```bash +certbot --nginx -d apollo.yourdomain.com +# Auto-renewal (already installed by certbot): +systemctl status certbot.timer +``` diff --git a/dash/backend/skills/git_workflow.md b/dash/backend/skills/git_workflow.md new file mode 100644 index 0000000..60ddaac --- /dev/null +++ b/dash/backend/skills/git_workflow.md @@ -0,0 +1,93 @@ +--- +description: Git workflows — branching strategy, interactive rebase, cherry-pick, bisect, stash, undo recipes +--- + +# Git Workflow Skill + +## Branch Strategy +``` +main — always deployable +dev — integration branch +feat/NAME — feature branches (from dev) +fix/NAME — bug fixes +hotfix/NAME — emergency patches directly from main +``` + +## Interactive Rebase (squash, reorder, edit) +```bash +git rebase -i HEAD~5 # rebase last 5 commits +# In editor: pick → squash (s) to merge, reword (r) to edit message +git rebase -i main # squash entire feature branch for clean merge +``` + +## Cherry-Pick +```bash +git cherry-pick abc1234 # apply single commit +git cherry-pick abc1234..def5678 # apply range of commits +git cherry-pick -n abc1234 # stage without committing (--no-commit) +``` + +## Bisect (find the commit that introduced a bug) +```bash +git bisect start +git bisect bad # current commit is broken +git bisect good v2.1.0 # last known good tag +# git does binary search — test each checkout: +git bisect good # or: git bisect bad +git bisect reset # when done +``` + +## Stash +```bash +git stash # stash all tracked changes +git stash -u # include untracked files +git stash push -m "WIP: auth" # named stash +git stash list +git stash pop # apply + remove latest +git stash apply stash@{2} # apply specific, keep in list +``` + +## Undo Recipes +```bash +# Undo last commit, keep changes staged +git reset --soft HEAD~1 + +# Undo last commit, unstage changes (default) +git reset HEAD~1 + +# Undo last commit, DISCARD changes (dangerous) +git reset --hard HEAD~1 + +# Revert a specific commit (safe, creates new commit) +git revert abc1234 + +# Unstage a file +git restore --staged file.go + +# Discard working copy changes in a file +git restore file.go +``` + +## Rebase vs Merge +| Situation | Use | +|---|---| +| Feature branch → main (clean history) | `rebase` then fast-forward merge | +| Hotfix with exact timestamp preservation | `merge --no-ff` | +| Public branch (others have checked out) | Always `merge` — never rebase public history | + +## Useful Aliases +```bash +git config --global alias.lg "log --oneline --graph --decorate --all" +git config --global alias.st "status -sb" +git config --global alias.undo "reset HEAD~1 --mixed" +``` + +## Conventional Commits +``` +feat: add user authentication +fix: resolve null pointer in order processing +docs: update API documentation +refactor: extract payment service +perf: optimize database query caching +chore: upgrade Go to 1.24 +``` diff --git a/dash/backend/skills/go_debugging.md b/dash/backend/skills/go_debugging.md new file mode 100644 index 0000000..2b13e3b --- /dev/null +++ b/dash/backend/skills/go_debugging.md @@ -0,0 +1,86 @@ +--- +description: Debugging Go services — race conditions, goroutine leaks, profiling, pprof, structured logging +--- + +# Go Debugging Skill + +## Race Conditions +Always run tests with: `go test -race ./...` +Common causes: shared maps without mutex, goroutines reading/writing same variable. + +```go +// Safe map access pattern +var mu sync.RWMutex +mu.RLock() +val := myMap[key] +mu.RUnlock() +``` + +## Goroutine Leak Detection +```go +// Import in test file +import "github.com/uber-go/goleak" + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) +} +``` + +## pprof Profiling +Add to main.go for CPU + mem profiling endpoints: +```go +import _ "net/http/pprof" +// Then: go tool pprof http://localhost:6060/debug/pprof/heap +``` + +Common pprof commands: +``` +go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine +go tool pprof -http=:8080 http://localhost:6060/debug/pprof/heap +go tool pprof -http=:8080 http://localhost:6060/debug/pprof/cpu?seconds=30 +``` + +## Structured Logging (slog, Go 1.21+) +```go +import "log/slog" +slog.Info("request received", "method", r.Method, "path", r.URL.Path, "latency_ms", ms) +slog.Error("database error", "err", err, "query", q) +``` + +## JSON Decode Gotchas +- `json.Decoder` reads lazily — always close body with `defer r.Body.Close()` +- Unknown fields are silently ignored unless you use `decoder.DisallowUnknownFields()` +- `time.Time` marshals as RFC3339 by default + +## HTTP Timeout Pattern +```go +client := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + }, +} +``` + +## Error Wrapping +```go +if err != nil { + return fmt.Errorf("getting user %d: %w", id, err) +} +// Unwrap: errors.Is(err, ErrNotFound), errors.As(err, &myErr) +``` + +## go vet Checks +`go vet ./...` catches: printf format mismatches, unreachable code, suspicious composite literals, incorrect mutex copies. + +## Benchmark Pattern +```go +func BenchmarkMyFunc(b *testing.B) { + for b.Loop() { // Go 1.24+ — or: for i := 0; i < b.N; i++ + MyFunc() + } +} +// Run: go test -bench=. -benchmem ./... +``` diff --git a/dash/backend/skills/llm_integration.md b/dash/backend/skills/llm_integration.md new file mode 100644 index 0000000..dd1a9ec --- /dev/null +++ b/dash/backend/skills/llm_integration.md @@ -0,0 +1,128 @@ +--- +description: LLM API integration — streaming SSE, tool/function calling, prompt engineering, token management, OpenAI-compatible APIs +--- + +# LLM Integration Skill + +## Streaming SSE (Server-Sent Events) Pattern + +### Backend (Go) +```go +w.Header().Set("Content-Type", "text/event-stream") +w.Header().Set("Cache-Control", "no-cache") +w.Header().Set("Connection", "keep-alive") +flusher, _ := w.(http.Flusher) + +// Stream each chunk +w.Write([]byte("data: " + jsonChunk + "\n\n")) +flusher.Flush() +// Terminate: +w.Write([]byte("data: [DONE]\n\n")) +``` + +### Frontend (React) +```js +const response = await fetch('/api/chat', { method: 'POST', signal: controller.signal, body: ... }); +const reader = response.body.getReader(); +const decoder = new TextDecoder(); +let buffer = ''; +while (true) { + const { value, done } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + for (const line of buffer.split('\n')) { + if (line.startsWith('data: ') && line !== 'data: [DONE]') { + const chunk = JSON.parse(line.slice(6)); + const delta = chunk.choices[0]?.delta?.content ?? ''; + setOutput(prev => prev + delta); + } + } + buffer = buffer.split('\n').pop(); // keep incomplete line +} +``` + +## Tool/Function Calling + +### Tool Definition Schema +```json +{ + "type": "function", + "function": { + "name": "read_file", + "description": "Reads the content of a file. Use this to understand existing code before making changes.", + "parameters": { + "type": "object", + "properties": { + "path": { "type": "string", "description": "File path relative to workspace root" } + }, + "required": ["path"] + } + } +} +``` + +### Tool Loop Pattern (Go) +```go +for attempt := 0; attempt < maxAttempts; attempt++ { + resp := callLLM(messages, tools) + if resp.ToolCalls == nil { break } // No tools called → final response + + messages = append(messages, resp.AsAssistantMessage()) + for _, tc := range resp.ToolCalls { + result := executeTool(tc.Name, tc.Arguments) + messages = append(messages, toolResultMessage(tc.ID, tc.Name, result)) + } +} +``` + +## Token Management + +### Rough estimates +- 1 token ≈ 4 characters (English) +- 1 token ≈ 0.75 words +- A typical code file of 100 lines ≈ 800–1200 tokens +- GPT-4o context: 128k tokens; Claude 3.7: 200k tokens + +### Context compression trigger +``` +if estimatedTokens > 80_000: + summarize middle messages → replace with summary + keep: system prompt + first 2 turns + last 4 turns +``` + +## Prompt Engineering Principles + +### Chain of Thought +Add to system prompt: *"Before answering, think step by step. Write your reasoning, then your final answer."* + +### Few-Shot Examples +``` +User: Convert 5 miles to km +Assistant: 5 × 1.60934 = 8.0467 km + +User: Convert 10 miles to km +Assistant: +``` + +### Structured Output +Request JSON and validate: +``` +Respond only with valid JSON matching this schema: {"action": "string", "confidence": 0-1, "reasoning": "string"} +Do not include any text outside the JSON object. +``` + +### Persona + Constraints +``` +You are a senior Go engineer. You MUST: +- Always run go vet after editing +- Never use global variables +- Return errors — never panic +- Write table-driven tests +``` + +## OpenRouter-Specific Notes +- `X-Title` header for dashboard tracking +- Model IDs: `anthropic/claude-3.7-sonnet`, `google/gemini-2.0-flash`, `openai/gpt-4o` +- Streaming: same as OpenAI (`stream: true` in payload) +- Tool calling: same schema as OpenAI function calling spec +- Some models don't support tools — check `supported_parameters` from `/api/v1/models` diff --git a/dash/backend/skills/react_patterns.md b/dash/backend/skills/react_patterns.md new file mode 100644 index 0000000..73ccd84 --- /dev/null +++ b/dash/backend/skills/react_patterns.md @@ -0,0 +1,85 @@ +--- +description: React performance — memo, useMemo, useCallback, virtualization, ref patterns, common pitfalls +--- + +# React Patterns Skill + +## Memoization Decision Tree +1. Is the component re-rendering too often? → `React.memo(Component)` +2. Is an expensive calculation running every render? → `useMemo` +3. Is a callback being re-created every render and passed to a child? → `useCallback` +4. Do you need to access a DOM node or preserve a value without re-rendering? → `useRef` + +## React.memo +```jsx +const ExpensiveList = React.memo(({ items, onSelect }) => { + return items.map(item => ); +}); +// Only re-renders when items or onSelect reference changes +``` + +## useMemo — expensive transforms +```jsx +const sorted = useMemo(() => + [...items].sort((a, b) => a.name.localeCompare(b.name)), + [items] // recalculate only when items changes +); +``` + +## useCallback — stable handlers +```jsx +const handleClick = useCallback((id) => { + setSelected(id); +}, []); // stable if no dependencies +``` + +## useRef Patterns +```jsx +// DOM access +const inputRef = useRef(null); +useEffect(() => { inputRef.current?.focus(); }, []); + +// Mutable value without triggering re-render (e.g. abort controller, timer) +const abortRef = useRef(null); +abortRef.current = new AbortController(); +``` + +## Virtualization (large lists) +Use `react-window` or `@tanstack/react-virtual` for lists > 200 items: +```jsx +import { FixedSizeList } from 'react-window'; + + {({ index, style }) =>
{items[index].name}
} +
+``` + +## Context Performance +Context re-renders ALL consumers when value changes. +Split contexts: `` + `` rather than one giant ``. +Use `useMemo` for context values: +```jsx +const value = useMemo(() => ({ user, setUser }), [user]); + +``` + +## State Update Pitfalls +```jsx +// WRONG: stale closure +setCount(count + 1); setCount(count + 1); // only increments by 1 + +// CORRECT: updater function +setCount(c => c + 1); setCount(c => c + 1); // increments by 2 +``` + +## Cleanup Pattern +```jsx +useEffect(() => { + const controller = new AbortController(); + fetch('/api/data', { signal: controller.signal }).then(setData); + return () => controller.abort(); // cleanup on unmount +}, []); +``` + +## Key Prop Rules +- Always use stable unique IDs — never array index for dynamic/reorderable lists +- Changing `key` forces full remount (useful to reset a component's state intentionally) diff --git a/dash/backend/skills/sql_tuning.md b/dash/backend/skills/sql_tuning.md new file mode 100644 index 0000000..3cb2f96 --- /dev/null +++ b/dash/backend/skills/sql_tuning.md @@ -0,0 +1,112 @@ +--- +description: SQLite and SQL tuning — indexing, EXPLAIN QUERY PLAN, query optimization, WAL mode, common patterns +--- + +# SQL Tuning Skill + +## SQLite Performance Essentials + +### WAL Mode (critical for concurrent reads) +```sql +PRAGMA journal_mode=WAL; -- survives restart +PRAGMA synchronous=NORMAL; -- safe + faster than FULL +PRAGMA cache_size=-32000; -- 32MB page cache +PRAGMA temp_store=MEMORY; +``` + +### EXPLAIN QUERY PLAN +```sql +EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'a@b.com'; +-- Look for: "SCAN TABLE users" (bad) vs "SEARCH TABLE users USING INDEX" (good) +``` + +## Indexing Strategy + +### When to add an index +- Column appears in WHERE, JOIN ON, ORDER BY, GROUP BY +- Column has high cardinality (many distinct values) +- Table has > 10k rows and query runs frequently + +### Common index patterns +```sql +-- Simple index +CREATE INDEX idx_users_email ON users(email); + +-- Composite index (order matters! put equality first, then range) +CREATE INDEX idx_orders_user_date ON orders(user_id, created_at DESC); + +-- Covering index (includes all columns the query needs) +CREATE INDEX idx_orders_cover ON orders(user_id, status) INCLUDE (total, created_at); + +-- Partial index (for boolean or enum filters) +CREATE INDEX idx_active_users ON users(email) WHERE active = 1; +``` + +### Don't index +- Columns you always scan fully (e.g. boolean with 2 values on a small table) +- Rarely-queried columns on write-heavy tables (indexes slow down INSERT/UPDATE) + +## Query Optimization Patterns + +### N+1 Problem Fix +```sql +-- Bad: SELECT user for each order +-- Good: JOIN them once +SELECT o.id, o.total, u.name +FROM orders o +JOIN users u ON u.id = o.user_id +WHERE o.status = 'pending'; +``` + +### Pagination (use keyset, not OFFSET for large tables) +```sql +-- Bad (slow on large tables): LIMIT 20 OFFSET 10000 +-- Good (keyset pagination): +SELECT * FROM orders +WHERE created_at < ? AND id < ? +ORDER BY created_at DESC, id DESC +LIMIT 20; +``` + +### Batch Inserts +```sql +-- Instead of looping INSERT: +INSERT INTO events (type, payload) VALUES + ('click', '{}'), + ('view', '{}'), + ('submit', '{}'); +-- Or use: BEGIN TRANSACTION + multiple inserts + COMMIT +``` + +### UPSERT +```sql +INSERT INTO settings (key, value) +VALUES ('theme', 'dark') +ON CONFLICT(key) DO UPDATE SET value = excluded.value; +``` + +## SQLite in Go (database/sql) +```go +// Always use prepared statements for repeated queries +stmt, err := db.Prepare("INSERT INTO logs (msg) VALUES (?)") +defer stmt.Close() +stmt.Exec("hello") + +// Transactions for batch operations +tx, _ := db.Begin() +for _, item := range items { + tx.Exec("INSERT INTO ...", item) +} +tx.Commit() + +// Row scanning +row := db.QueryRow("SELECT id, name FROM users WHERE id = ?", id) +var u User +row.Scan(&u.ID, &u.Name) +``` + +## Table Design Checklist +- Every table has an integer PRIMARY KEY (rowid alias in SQLite) +- Timestamps as `INTEGER` (Unix epoch) — faster comparisons than TEXT +- Use `NOT NULL DEFAULT` where possible — avoids NULL handling complexity +- Foreign keys: `PRAGMA foreign_keys = ON;` (off by default in SQLite!) diff --git a/dash/backend/tools/contextplus_native.go b/dash/backend/tools/contextplus_native.go new file mode 100644 index 0000000..e92b2fe --- /dev/null +++ b/dash/backend/tools/contextplus_native.go @@ -0,0 +1,2841 @@ +package tools + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "math" + "net/http" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/danilrybalkin/apollo-dash/db" + sitter "github.com/smacker/go-tree-sitter" + sitterbash "github.com/smacker/go-tree-sitter/bash" + sitterc "github.com/smacker/go-tree-sitter/c" + sittercpp "github.com/smacker/go-tree-sitter/cpp" + sittercsharp "github.com/smacker/go-tree-sitter/csharp" + sittergolang "github.com/smacker/go-tree-sitter/golang" + sitterjava "github.com/smacker/go-tree-sitter/java" + sitterjavascript "github.com/smacker/go-tree-sitter/javascript" + sitterkotlin "github.com/smacker/go-tree-sitter/kotlin" + sitterphp "github.com/smacker/go-tree-sitter/php" + sitterpython "github.com/smacker/go-tree-sitter/python" + sitterruby "github.com/smacker/go-tree-sitter/ruby" + sitterrust "github.com/smacker/go-tree-sitter/rust" + sitterscala "github.com/smacker/go-tree-sitter/scala" + sittersql "github.com/smacker/go-tree-sitter/sql" + sitterswift "github.com/smacker/go-tree-sitter/swift" + sittertsx "github.com/smacker/go-tree-sitter/typescript/tsx" + sittertypescript "github.com/smacker/go-tree-sitter/typescript/typescript" + sitteryaml "github.com/smacker/go-tree-sitter/yaml" + "gonum.org/v1/gonum/mat" +) + +var contextCodeExt = map[string]bool{ + ".go": true, ".ts": true, ".tsx": true, ".js": true, ".jsx": true, + ".py": true, ".rs": true, ".java": true, ".kt": true, ".swift": true, + ".c": true, ".h": true, ".cpp": true, ".hpp": true, ".cs": true, + ".php": true, ".rb": true, ".scala": true, ".m": true, ".mm": true, + ".sql": true, ".sh": true, ".yaml": true, ".yml": true, ".json": true, + ".toml": true, +} + +var contextSkipDirs = map[string]bool{ + ".git": true, ".svn": true, ".hg": true, + "node_modules": true, "vendor": true, "dist": true, "build": true, + ".next": true, ".turbo": true, ".idea": true, ".vscode": true, + ".apollo_contextplus": true, ".mcp_data": true, +} + +type contextSymbol struct { + Name string + Kind string + Line int + Signature string +} + +type contextFileEntry struct { + RelPath string + AbsPath string + Hash string + Header string + Symbols []contextSymbol + Content string +} + +type contextSearchCandidate struct { + Entry contextFileEntry + KeywordScore float64 + Semantic float64 + Combined float64 +} + +type contextIdentifierEntry struct { + File contextFileEntry + Symbol contextSymbol + Doc string + Keyword float64 + Semantic float64 + Combined float64 + CallSites []string +} + +type contextRestorePointFile struct { + Path string `json:"path"` + Existed bool `json:"existed"` +} + +type contextRestorePoint struct { + ID string `json:"id"` + Timestamp int64 `json:"timestamp"` + Message string `json:"message"` + Files []contextRestorePointFile `json:"files"` +} + +type contextHub struct { + RelPath string + AbsPath string + Links []string +} + +type contextIndexedFile struct { + RelPath string `json:"rel_path"` + Ext string `json:"ext"` + Lang string `json:"lang"` + Header string `json:"header"` + Snippet string `json:"snippet"` + Symbols []contextSymbol `json:"symbols"` + MTime int64 `json:"mtime"` + Size int64 `json:"size"` + Hash string `json:"hash"` +} + +type contextProjectIndex struct { + Version int `json:"version"` + Root string `json:"root"` + UpdatedAt int64 `json:"updated_at"` + Files map[string]contextIndexedFile `json:"files"` +} + +type contextSemanticNode struct { + ID string + Depth int + FileIndices []int + Children []*contextSemanticNode + Locked bool +} + +var contextLanguageByExt = map[string]string{ + ".go": "go", ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "javascript", + ".py": "python", ".rs": "rust", ".java": "java", ".kt": "kotlin", ".swift": "swift", + ".c": "c", ".h": "c", ".cpp": "cpp", ".hpp": "cpp", ".cs": "csharp", + ".php": "php", ".rb": "ruby", ".scala": "scala", ".sql": "sql", ".sh": "bash", + ".yaml": "yaml", ".yml": "yaml", +} + +var ( + contextEmbedMu sync.Mutex + contextEmbedCache = map[string]map[string][]float64{} + contextEmbedLoaded = map[string]bool{} + contextEmbedDirty = map[string]bool{} + contextEmbedSaving = map[string]bool{} + contextEmbedModel = map[string]string{} + contextEmbedBaseURL = map[string]string{} + + contextIndexMu sync.Mutex + contextIndexes = map[string]*contextProjectIndex{} + contextTrackerActive = map[string]bool{} + contextPendingWarm = map[string]map[string]bool{} +) + +func executeGetContextTree(rawArgs string, projectRoot string) string { + var args struct { + TargetPath string `json:"target_path"` + DepthLimit int `json:"depth_limit"` + IncludeSymbols *bool `json:"include_symbols"` + MaxTokens int `json:"max_tokens"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: invalid arguments." + } + + root := contextRoot(projectRoot) + target := root + if strings.TrimSpace(args.TargetPath) != "" { + safePath, err := securePath(args.TargetPath, projectRoot) + if err != nil { + return err.Error() + } + target = safePath + } + if args.MaxTokens <= 0 { + args.MaxTokens = 20000 + } + _ = contextEnsureIndex(root) + + includeSymbols := true + if args.IncludeSymbols != nil { + includeSymbols = *args.IncludeSymbols + } + + levels := []int{2, 1, 0} + if !includeSymbols { + levels = []int{1, 0} + } + + var rendered string + for _, level := range levels { + rendered = contextRenderTree(target, root, args.DepthLimit, level) + if contextEstimateTokens(rendered) <= args.MaxTokens { + break + } + } + + if strings.TrimSpace(rendered) == "" { + return "No files found for context tree." + } + + if len(rendered) > 18000 { + rendered = rendered[:18000] + "\n\n... [CONTEXT TREE TRUNCATED]" + } + return rendered +} + +func executeGetFileSkeleton(rawArgs string, projectRoot string) string { + var args struct { + FilePath string `json:"file_path"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || strings.TrimSpace(args.FilePath) == "" { + return "Error: 'file_path' is required." + } + + safePath, err := securePath(args.FilePath, projectRoot) + if err != nil { + return err.Error() + } + + data, err := os.ReadFile(safePath) + if err != nil { + return fmt.Sprintf("Error reading file: %v", err) + } + + symbols := contextParseSymbolsByExt(strings.ToLower(filepath.Ext(safePath)), string(data)) + if len(symbols) == 0 { + return fmt.Sprintf("No symbols found in %s", args.FilePath) + } + + rel := contextRelativePath(contextRoot(projectRoot), safePath) + var b strings.Builder + b.WriteString(fmt.Sprintf("File Skeleton: %s\n\n", rel)) + for _, s := range symbols { + b.WriteString(fmt.Sprintf("- [%s] %s (line %d)\n %s\n", s.Kind, s.Name, s.Line, s.Signature)) + } + + out := b.String() + if len(out) > 18000 { + out = out[:18000] + "\n\n... [SKELETON TRUNCATED]" + } + return out +} + +func executeGetBlastRadius(rawArgs string, projectRoot string) string { + var args struct { + SymbolName string `json:"symbol_name"` + FileContext string `json:"file_context"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || strings.TrimSpace(args.SymbolName) == "" { + return "Error: 'symbol_name' is required." + } + + entries, err := contextCollectFiles(contextRoot(projectRoot), contextRoot(projectRoot)) + if err != nil { + return fmt.Sprintf("Error indexing files: %v", err) + } + + wordRe := regexp.MustCompile(`\\b` + regexp.QuoteMeta(args.SymbolName) + `\\b`) + exclude := strings.TrimSpace(args.FileContext) + if exclude != "" { + exclude = filepath.Clean(strings.TrimPrefix(exclude, "/")) + } + + var hits []string + for _, e := range entries { + content, err := os.ReadFile(e.AbsPath) + if err != nil { + continue + } + lines := strings.Split(string(content), "\n") + for i, line := range lines { + if !wordRe.MatchString(line) { + continue + } + if exclude != "" && filepath.Clean(e.RelPath) == exclude { + symbols := e.Symbols + isDefinition := false + for _, s := range symbols { + if s.Name == args.SymbolName && s.Line == i+1 { + isDefinition = true + break + } + } + if isDefinition { + continue + } + } + hits = append(hits, fmt.Sprintf("%s:%d: %s", e.RelPath, i+1, strings.TrimSpace(line))) + if len(hits) >= 400 { + hits = append(hits, "... [ADDITIONAL REFERENCES TRUNCATED]") + break + } + } + if len(hits) >= 401 { + break + } + } + + if len(hits) == 0 { + return fmt.Sprintf("No references found for symbol '%s'.", args.SymbolName) + } + + return fmt.Sprintf("Blast Radius for '%s' (%d hits):\n\n%s", args.SymbolName, len(hits), strings.Join(hits, "\n")) +} + +func executeRunStaticAnalysisNative(rawArgs string, projectRoot string) string { + var args struct { + TargetPath string `json:"target_path"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: invalid arguments." + } + + root := contextRoot(projectRoot) + target := root + if strings.TrimSpace(args.TargetPath) != "" { + safePath, err := securePath(args.TargetPath, projectRoot) + if err != nil { + return err.Error() + } + target = safePath + } + + langs := contextDetectLanguages(target) + if len(langs) == 0 { + return "No supported source files found for static analysis." + } + + var reports []string + if langs["go"] && contextPathExists(filepath.Join(root, "go.mod")) { + reports = append(reports, contextRunCheckCommand(root, "go vet", "go", "vet", "./...")) + } + if (langs["typescript"] || langs["javascript"]) && contextPathExists(filepath.Join(root, "package.json")) { + if langs["typescript"] && contextPathExists(filepath.Join(root, "tsconfig.json")) { + reports = append(reports, contextRunCheckCommand(root, "tsc --noEmit", "npx", "tsc", "--noEmit", "--pretty", "false")) + } + if contextPathExists(filepath.Join(root, "eslint.config.js")) || contextPathExists(filepath.Join(root, ".eslintrc")) || contextPathExists(filepath.Join(root, ".eslintrc.js")) { + reports = append(reports, contextRunCheckCommand(root, "eslint", "npx", "eslint", ".", "--format", "compact")) + } + } + if langs["python"] { + pyFiles := contextFindFilesByExt(target, ".py", 80) + if len(pyFiles) > 0 { + args := []string{"-m", "py_compile"} + args = append(args, pyFiles...) + reports = append(reports, contextRunCheckCommand(root, "python -m py_compile", "python3", args...)) + } + } + if langs["rust"] && contextPathExists(filepath.Join(root, "Cargo.toml")) { + reports = append(reports, contextRunCheckCommand(root, "cargo check", "cargo", "check", "--message-format=short")) + } + + if len(reports) == 0 { + return "No runnable static analyzers detected for this project path." + } + + out := strings.Join(reports, "\n\n") + if len(out) > 18000 { + out = out[:18000] + "\n\n... [STATIC ANALYSIS OUTPUT TRUNCATED]" + } + return out +} + +func executeSemanticCodeSearch(rawArgs string, projectRoot string) string { + var args struct { + Query string `json:"query"` + TopK int `json:"top_k"` + SemanticWeight *float64 `json:"semantic_weight"` + KeywordWeight *float64 `json:"keyword_weight"` + MinSemanticScore *float64 `json:"min_semantic_score"` + MinKeywordScore *float64 `json:"min_keyword_score"` + MinCombinedScore *float64 `json:"min_combined_score"` + RequireKeywordMatch *bool `json:"require_keyword_match"` + RequireSemanticMatch *bool `json:"require_semantic_match"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || strings.TrimSpace(args.Query) == "" { + return "Error: 'query' is required." + } + + if args.TopK <= 0 { + args.TopK = 5 + } + semW := 0.72 + kwW := 0.28 + if args.SemanticWeight != nil { + semW = *args.SemanticWeight + } + if args.KeywordWeight != nil { + kwW = *args.KeywordWeight + } + if semW < 0 { + semW = 0 + } + if kwW < 0 { + kwW = 0 + } + if semW == 0 && kwW == 0 { + semW = 0.72 + kwW = 0.28 + } + + root := contextRoot(projectRoot) + entries, err := contextCollectFiles(root, root) + if err != nil { + return fmt.Sprintf("Error indexing project: %v", err) + } + if len(entries) == 0 { + return "No code files found for semantic search." + } + + queryWords := contextSplitWords(args.Query) + candidates := make([]contextSearchCandidate, 0, len(entries)) + for _, e := range entries { + doc := contextFileDocForSearch(e) + kwScore := contextKeywordScore(queryWords, doc) + candidates = append(candidates, contextSearchCandidate{Entry: e, KeywordScore: kwScore}) + } + + sort.Slice(candidates, func(i, j int) bool { + if candidates[i].KeywordScore == candidates[j].KeywordScore { + return candidates[i].Entry.RelPath < candidates[j].Entry.RelPath + } + return candidates[i].KeywordScore > candidates[j].KeywordScore + }) + + poolSize := 80 + if len(candidates) < poolSize { + poolSize = len(candidates) + } + pool := candidates[:poolSize] + + queryVec, queryErr := contextEmbedding(root, "query:"+args.Query) + for i := range pool { + doc := contextFileDocForSearch(pool[i].Entry) + if queryErr == nil && len(queryVec) > 0 { + docVec, err := contextEmbedding(root, fmt.Sprintf("file:%s:%s:%s", pool[i].Entry.RelPath, pool[i].Entry.Hash, doc)) + if err == nil { + pool[i].Semantic = contextCosine(queryVec, docVec) + } + } + pool[i].Combined = semW*pool[i].Semantic + kwW*pool[i].KeywordScore + } + + minSemantic := contextNormalizeThreshold(args.MinSemanticScore) + minKeyword := contextNormalizeThreshold(args.MinKeywordScore) + minCombined := contextNormalizeThreshold(args.MinCombinedScore) + requireKeyword := args.RequireKeywordMatch != nil && *args.RequireKeywordMatch + requireSemantic := args.RequireSemanticMatch != nil && *args.RequireSemanticMatch + + filtered := make([]contextSearchCandidate, 0, len(pool)) + for _, c := range pool { + if c.Semantic < minSemantic { + continue + } + if c.KeywordScore < minKeyword { + continue + } + if c.Combined < minCombined { + continue + } + if requireKeyword && c.KeywordScore <= 0 { + continue + } + if requireSemantic && c.Semantic <= 0 { + continue + } + filtered = append(filtered, c) + } + + sort.Slice(filtered, func(i, j int) bool { + if filtered[i].Combined == filtered[j].Combined { + return filtered[i].Entry.RelPath < filtered[j].Entry.RelPath + } + return filtered[i].Combined > filtered[j].Combined + }) + + if len(filtered) == 0 { + return "No semantic matches found." + } + if len(filtered) > args.TopK { + filtered = filtered[:args.TopK] + } + + var b strings.Builder + b.WriteString(fmt.Sprintf("Semantic Code Search: %s\n\n", args.Query)) + for i, c := range filtered { + b.WriteString(fmt.Sprintf("%d. %s\n", i+1, c.Entry.RelPath)) + b.WriteString(fmt.Sprintf(" combined=%.3f semantic=%.3f keyword=%.3f\n", c.Combined, c.Semantic, c.KeywordScore)) + if c.Entry.Header != "" { + b.WriteString(fmt.Sprintf(" header: %s\n", c.Entry.Header)) + } + if len(c.Entry.Symbols) > 0 { + maxSymbols := 4 + if len(c.Entry.Symbols) < maxSymbols { + maxSymbols = len(c.Entry.Symbols) + } + for si := 0; si < maxSymbols; si++ { + s := c.Entry.Symbols[si] + b.WriteString(fmt.Sprintf(" symbol: %s (%s) line %d\n", s.Name, s.Kind, s.Line)) + } + } + b.WriteString("\n") + } + + out := b.String() + if len(out) > 18000 { + out = out[:18000] + "\n\n... [SEARCH OUTPUT TRUNCATED]" + } + return out +} + +func executeSemanticIdentifierSearch(rawArgs string, projectRoot string) string { + var args struct { + Query string `json:"query"` + TopK int `json:"top_k"` + TopCallsPerIdentifier int `json:"top_calls_per_identifier"` + IncludeKinds []string `json:"include_kinds"` + SemanticWeight *float64 `json:"semantic_weight"` + KeywordWeight *float64 `json:"keyword_weight"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || strings.TrimSpace(args.Query) == "" { + return "Error: 'query' is required." + } + if args.TopK <= 0 { + args.TopK = 5 + } + if args.TopCallsPerIdentifier <= 0 { + args.TopCallsPerIdentifier = 10 + } + + semW := 0.78 + kwW := 0.22 + if args.SemanticWeight != nil { + semW = *args.SemanticWeight + } + if args.KeywordWeight != nil { + kwW = *args.KeywordWeight + } + + allowedKinds := map[string]bool{} + for _, k := range args.IncludeKinds { + allowedKinds[strings.ToLower(strings.TrimSpace(k))] = true + } + + root := contextRoot(projectRoot) + entries, err := contextCollectFiles(root, root) + if err != nil { + return fmt.Sprintf("Error indexing project: %v", err) + } + + var identifiers []contextIdentifierEntry + queryWords := contextSplitWords(args.Query) + for _, file := range entries { + for _, sym := range file.Symbols { + if len(allowedKinds) > 0 && !allowedKinds[strings.ToLower(sym.Kind)] { + continue + } + doc := fmt.Sprintf("%s %s %s %s %s", sym.Name, sym.Kind, sym.Signature, file.Header, file.RelPath) + kw := contextKeywordScore(queryWords, doc) + identifiers = append(identifiers, contextIdentifierEntry{ + File: file, + Symbol: sym, + Doc: doc, + Keyword: kw, + Combined: kwW * kw, + }) + } + } + + if len(identifiers) == 0 { + return "No identifiers found for semantic search." + } + + sort.Slice(identifiers, func(i, j int) bool { + if identifiers[i].Keyword == identifiers[j].Keyword { + return identifiers[i].Symbol.Name < identifiers[j].Symbol.Name + } + return identifiers[i].Keyword > identifiers[j].Keyword + }) + + poolSize := 120 + if len(identifiers) < poolSize { + poolSize = len(identifiers) + } + pool := identifiers[:poolSize] + + queryVec, queryErr := contextEmbedding(root, "identifier-query:"+args.Query) + for i := range pool { + if queryErr == nil && len(queryVec) > 0 { + v, err := contextEmbedding( + root, + fmt.Sprintf("identifier:%s:%d:%s:%s", pool[i].File.RelPath, pool[i].Symbol.Line, pool[i].File.Hash, pool[i].Doc), + ) + if err == nil { + pool[i].Semantic = contextCosine(queryVec, v) + } + } + pool[i].Combined = semW*pool[i].Semantic + kwW*pool[i].Keyword + } + + sort.Slice(pool, func(i, j int) bool { + if pool[i].Combined == pool[j].Combined { + return pool[i].File.RelPath < pool[j].File.RelPath + } + return pool[i].Combined > pool[j].Combined + }) + + if len(pool) > args.TopK { + pool = pool[:args.TopK] + } + + allFiles := entries + for i := range pool { + pool[i].CallSites = contextFindCallSites(allFiles, pool[i].Symbol.Name, pool[i].File.RelPath, pool[i].Symbol.Line, args.TopCallsPerIdentifier) + } + + var b strings.Builder + b.WriteString(fmt.Sprintf("Semantic Identifier Search: %s\n\n", args.Query)) + for i, r := range pool { + b.WriteString(fmt.Sprintf("%d. %s (%s)\n", i+1, r.Symbol.Name, r.Symbol.Kind)) + b.WriteString(fmt.Sprintf(" file: %s:%d\n", r.File.RelPath, r.Symbol.Line)) + b.WriteString(fmt.Sprintf(" score: combined=%.3f semantic=%.3f keyword=%.3f\n", r.Combined, r.Semantic, r.Keyword)) + b.WriteString(fmt.Sprintf(" signature: %s\n", r.Symbol.Signature)) + if len(r.CallSites) > 0 { + b.WriteString(" call sites:\n") + for _, cs := range r.CallSites { + b.WriteString(" - " + cs + "\n") + } + } + b.WriteString("\n") + } + return b.String() +} + +func executeSemanticNavigate(rawArgs string, projectRoot string) string { + var args struct { + MaxDepth int `json:"max_depth"` + MaxClusters int `json:"max_clusters"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: invalid arguments." + } + if args.MaxDepth <= 0 { + args.MaxDepth = 3 + } + if args.MaxClusters <= 0 { + args.MaxClusters = 20 + } + + root := contextRoot(projectRoot) + entries, err := contextCollectFiles(root, root) + if err != nil { + return fmt.Sprintf("Error indexing project: %v", err) + } + if len(entries) == 0 { + return "No files found for semantic navigation." + } + + vectors := contextBuildSemanticVectors(root, entries) + if len(vectors) != len(entries) || len(vectors) == 0 { + return "Unable to build semantic vectors for navigation." + } + + const maxSpectralFiles = 220 + coreIndices := make([]int, 0, len(entries)) + for i := range entries { + coreIndices = append(coreIndices, i) + } + sampled := false + if len(coreIndices) > maxSpectralFiles { + coreIndices = contextSelectDiverseIndices(vectors, maxSpectralFiles) + sampled = true + } + + tree := &contextSemanticNode{ + ID: "1", + Depth: 1, + FileIndices: append([]int(nil), coreIndices...), + } + leafCount := 1 + for leafCount < args.MaxClusters { + candidate := contextSelectSplitCandidate(tree, args.MaxDepth) + if candidate == nil { + break + } + left, right, ok := contextSpectralBisect(vectors, candidate.FileIndices) + if !ok { + candidate.Locked = true + continue + } + candidate.Children = []*contextSemanticNode{ + {ID: candidate.ID + ".1", Depth: candidate.Depth + 1, FileIndices: left}, + {ID: candidate.ID + ".2", Depth: candidate.Depth + 1, FileIndices: right}, + } + leafCount++ + } + + if sampled { + coreSet := map[int]bool{} + for _, idx := range coreIndices { + coreSet[idx] = true + } + leaves := contextCollectLeafNodes(tree) + centroids := make([][]float64, len(leaves)) + for i, leaf := range leaves { + centroids[i] = contextCentroidForIndices(vectors, leaf.FileIndices) + } + for idx := range entries { + if coreSet[idx] { + continue + } + bestLeaf := 0 + bestScore := -2.0 + for li := range leaves { + score := contextCosine(vectors[idx], centroids[li]) + if score > bestScore { + bestScore = score + bestLeaf = li + } + } + leaves[bestLeaf].FileIndices = append(leaves[bestLeaf].FileIndices, idx) + } + } + + for _, leaf := range contextCollectLeafNodes(tree) { + sort.Ints(leaf.FileIndices) + } + + var b strings.Builder + b.WriteString("Semantic Navigate (spectral clustering)\n\n") + b.WriteString(fmt.Sprintf("files: %d", len(entries))) + if sampled { + b.WriteString(fmt.Sprintf(", spectral-core: %d", len(coreIndices))) + } + b.WriteString(fmt.Sprintf(", depth<=%d, max_clusters=%d\n\n", args.MaxDepth, args.MaxClusters)) + contextRenderSemanticNode(&b, tree, entries, 0) + + out := b.String() + if len(out) > 18000 { + out = out[:18000] + "\n\n... [SEMANTIC NAVIGATION TRUNCATED]" + } + return out +} + +func executeGetFeatureHub(rawArgs string, projectRoot string) string { + var args struct { + HubPath string `json:"hub_path"` + Feature string `json:"feature_name"` + ShowOrphan *bool `json:"show_orphans"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: invalid arguments." + } + + root := contextRoot(projectRoot) + hubs, err := contextCollectHubs(root) + if err != nil { + return fmt.Sprintf("Error loading hubs: %v", err) + } + + if args.ShowOrphan != nil && *args.ShowOrphan { + orphans := contextFindHubOrphans(root, hubs) + if len(orphans) == 0 { + return "No orphaned source files found." + } + if len(orphans) > 300 { + orphans = append(orphans[:300], "... [ADDITIONAL ORPHANS TRUNCATED]") + } + return "Orphan Source Files:\n\n" + strings.Join(orphans, "\n") + } + + if strings.TrimSpace(args.HubPath) == "" && strings.TrimSpace(args.Feature) == "" { + if len(hubs) == 0 { + return "No feature hubs found (.md files with [[wikilinks]])." + } + var lines []string + for _, h := range hubs { + lines = append(lines, fmt.Sprintf("- %s (%d links)", h.RelPath, len(h.Links))) + } + sort.Strings(lines) + return "Feature Hubs:\n\n" + strings.Join(lines, "\n") + } + + selected := contextSelectHub(root, hubs, strings.TrimSpace(args.HubPath), strings.TrimSpace(args.Feature)) + if selected == nil { + return "No matching feature hub found." + } + + var b strings.Builder + b.WriteString(fmt.Sprintf("Feature Hub: %s\n\n", selected.RelPath)) + if len(selected.Links) == 0 { + b.WriteString("No wikilinks found in this hub.") + return b.String() + } + for _, l := range selected.Links { + resolved := contextResolveHubLink(root, selected.AbsPath, l) + if resolved == "" { + b.WriteString(fmt.Sprintf("- %s (unresolved)\n", l)) + continue + } + rel := contextRelativePath(root, resolved) + b.WriteString(fmt.Sprintf("- %s\n", rel)) + if data, err := os.ReadFile(resolved); err == nil { + syms := contextParseSymbolsByExt(strings.ToLower(filepath.Ext(resolved)), string(data)) + if len(syms) > 0 { + maxSymbols := 4 + if len(syms) < maxSymbols { + maxSymbols = len(syms) + } + for i := 0; i < maxSymbols; i++ { + s := syms[i] + b.WriteString(fmt.Sprintf(" - %s (%s) line %d\n", s.Name, s.Kind, s.Line)) + } + } + } + } + + out := b.String() + if len(out) > 18000 { + out = out[:18000] + "\n\n... [FEATURE HUB OUTPUT TRUNCATED]" + } + return out +} + +func executeProposeCommitNative(rawArgs string, projectRoot string) string { + var args struct { + FilePath string `json:"file_path"` + NewContent string `json:"new_content"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || strings.TrimSpace(args.FilePath) == "" { + return "Error: 'file_path' and 'new_content' are required." + } + if strings.TrimSpace(args.NewContent) == "" { + return "Error: new content is empty." + } + + lineCount := strings.Count(args.NewContent, "\n") + 1 + if lineCount > 5000 { + return fmt.Sprintf("Error: file has %d lines; exceeds safety limit of 5000.", lineCount) + } + + root := contextRoot(projectRoot) + safePath, err := securePath(args.FilePath, projectRoot) + if err != nil { + return err.Error() + } + rel := contextRelativePath(root, safePath) + + rpID, err := contextCreateRestorePoint(root, []string{rel}, "propose_commit before write") + if err != nil { + return fmt.Sprintf("Error creating restore point: %v", err) + } + + if err := os.MkdirAll(filepath.Dir(safePath), 0o755); err != nil { + return fmt.Sprintf("Error preparing directory: %v", err) + } + if err := os.WriteFile(safePath, []byte(args.NewContent), 0o644); err != nil { + return fmt.Sprintf("Error writing file: %v", err) + } + _ = contextEnsureIndex(root) + + warnings := contextValidateCommitContent(args.NewContent) + if warnings != "" { + return fmt.Sprintf("Saved %s\nRestore point: %s\n\nValidation warnings:\n%s", rel, rpID, warnings) + } + return fmt.Sprintf("Saved %s\nRestore point: %s", rel, rpID) +} + +func executeListRestorePointsNative(rawArgs string, projectRoot string) string { + root := contextRoot(projectRoot) + points, err := contextListRestorePoints(root) + if err != nil { + return fmt.Sprintf("Error listing restore points: %v", err) + } + if len(points) == 0 { + return "No restore points found." + } + + var lines []string + for _, p := range points { + var fileNames []string + for _, f := range p.Files { + fileNames = append(fileNames, f.Path) + } + lines = append(lines, fmt.Sprintf("%s | %s | %s | %s", p.ID, time.UnixMilli(p.Timestamp).Format(time.RFC3339), strings.Join(fileNames, ", "), p.Message)) + } + return fmt.Sprintf("Restore Points (%d):\n\n%s", len(lines), strings.Join(lines, "\n")) +} + +func executeUndoChangeNative(rawArgs string, projectRoot string) string { + var args struct { + PointID string `json:"point_id"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || strings.TrimSpace(args.PointID) == "" { + return "Error: 'point_id' is required." + } + + root := contextRoot(projectRoot) + restored, err := contextRestorePointByID(root, args.PointID) + if err != nil { + return fmt.Sprintf("Error restoring point: %v", err) + } + _ = contextEnsureIndex(root) + if len(restored) == 0 { + return "No files restored." + } + return fmt.Sprintf("Restored %d file(s):\n%s", len(restored), strings.Join(restored, "\n")) +} + +func contextRoot(projectRoot string) string { + if strings.TrimSpace(projectRoot) != "" { + return projectRoot + } + return WorkspaceDir +} + +func contextEstimateTokens(s string) int { + if s == "" { + return 0 + } + return (len(s) / 4) + 1 +} + +func contextRenderTree(target string, root string, depthLimit int, level int) string { + info, err := os.Stat(target) + if err != nil { + return "" + } + + var b strings.Builder + if info.IsDir() { + rel := contextRelativePath(root, target) + if rel == "." || rel == "" { + rel = "/" + } + b.WriteString(fmt.Sprintf("Context Tree (%s)\n\n", rel)) + contextRenderTreeDir(&b, target, root, 0, depthLimit, level) + } else { + rel := contextRelativePath(root, target) + b.WriteString(fmt.Sprintf("Context Tree (%s)\n\n", rel)) + contextRenderTreeFile(&b, target, rel, root, level, "") + } + return b.String() +} + +func contextRenderTreeDir(b *strings.Builder, dir string, root string, depth int, depthLimit int, level int) { + if depthLimit > 0 && depth > depthLimit { + return + } + entries, err := os.ReadDir(dir) + if err != nil { + return + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) + + for _, e := range entries { + name := e.Name() + if strings.HasPrefix(name, ".") && name != ".env" && name != ".github" { + if contextSkipDirs[name] { + continue + } + } + if e.IsDir() { + if contextSkipDirs[name] { + continue + } + indent := strings.Repeat(" ", depth) + b.WriteString(fmt.Sprintf("%s- %s/\n", indent, name)) + contextRenderTreeDir(b, filepath.Join(dir, name), root, depth+1, depthLimit, level) + continue + } + + abs := filepath.Join(dir, name) + ext := strings.ToLower(filepath.Ext(abs)) + if !contextCodeExt[ext] && ext != ".md" { + continue + } + rel := contextRelativePath(root, abs) + indent := strings.Repeat(" ", depth) + contextRenderTreeFile(b, abs, rel, root, level, indent) + } +} + +func contextRenderTreeFile(b *strings.Builder, abs string, rel string, root string, level int, indent string) { + if level <= 0 { + b.WriteString(fmt.Sprintf("%s- %s\n", indent, rel)) + return + } + + header := "" + var symbols []contextSymbol + if idxFile, ok := contextGetIndexedFile(root, rel); ok { + header = idxFile.Header + symbols = idxFile.Symbols + } + if header == "" || (level > 1 && len(symbols) == 0) { + data, err := os.ReadFile(abs) + if err == nil { + content := string(data) + header = contextFileHeader(content) + if level > 1 { + symbols = contextParseSymbolsByExt(strings.ToLower(filepath.Ext(abs)), content) + } + } + } + if header != "" { + b.WriteString(fmt.Sprintf("%s- %s :: %s\n", indent, rel, header)) + } else { + b.WriteString(fmt.Sprintf("%s- %s\n", indent, rel)) + } + if level <= 1 { + return + } + + maxSymbols := 8 + if len(symbols) < maxSymbols { + maxSymbols = len(symbols) + } + for i := 0; i < maxSymbols; i++ { + s := symbols[i] + b.WriteString(fmt.Sprintf("%s - %s %s:%d\n", indent, s.Kind, s.Name, s.Line)) + } +} + +func contextCollectFiles(root string, target string) ([]contextFileEntry, error) { + if err := contextEnsureIndex(root); err != nil { + return nil, err + } + + targetAbs := filepath.Clean(target) + if targetAbs == "" { + targetAbs = root + } + + contextIndexMu.Lock() + idx := contextIndexes[root] + if idx == nil { + contextIndexMu.Unlock() + return []contextFileEntry{}, nil + } + entries := make([]contextFileEntry, 0, len(idx.Files)) + for rel, f := range idx.Files { + abs := filepath.Join(root, rel) + if !contextPathWithinTarget(abs, targetAbs) { + continue + } + entries = append(entries, contextFileEntry{ + RelPath: rel, + AbsPath: abs, + Hash: f.Hash, + Header: f.Header, + Symbols: f.Symbols, + Content: f.Snippet, + }) + } + contextIndexMu.Unlock() + + sort.Slice(entries, func(i, j int) bool { + return entries[i].RelPath < entries[j].RelPath + }) + return entries, nil +} + +func contextLooksText(content string) bool { + if content == "" { + return true + } + if strings.IndexByte(content, 0) >= 0 { + return false + } + return true +} + +func contextFileHeader(content string) string { + lines := strings.Split(content, "\n") + parts := make([]string, 0, 2) + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts = append(parts, line) + if len(parts) == 2 { + break + } + } + if len(parts) == 0 { + return "" + } + header := strings.Join(parts, " | ") + if len(header) > 160 { + header = header[:160] + } + return header +} + +func contextParseSymbolsByExt(ext string, content string) []contextSymbol { + tsSymbols, ok := contextParseSymbolsByTreeSitter(ext, content) + regexSymbols := contextParseSymbolsRegexByExt(ext, content) + if ok && len(tsSymbols) > 0 { + return contextMergeSymbols(tsSymbols, regexSymbols) + } + return regexSymbols +} + +func contextParseSymbolsRegexByExt(ext string, content string) []contextSymbol { + lines := strings.Split(content, "\n") + var out []contextSymbol + + appendMatch := func(line string, idx int, kind string, re *regexp.Regexp) { + m := re.FindStringSubmatch(line) + if len(m) < 2 { + return + } + sig := strings.TrimSpace(line) + if len(sig) > 200 { + sig = sig[:200] + } + out = append(out, contextSymbol{Name: m[1], Kind: kind, Line: idx + 1, Signature: sig}) + } + + for i, raw := range lines { + line := strings.TrimSpace(raw) + if line == "" { + continue + } + + switch ext { + case ".go": + appendMatch(line, i, "function", regexp.MustCompile(`^func\\s+(?:\\([^)]*\\)\\s*)?([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`)) + appendMatch(line, i, "type", regexp.MustCompile(`^type\\s+([A-Za-z_][A-Za-z0-9_]*)\\s+(?:struct|interface)\\b`)) + appendMatch(line, i, "const", regexp.MustCompile(`^const\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + appendMatch(line, i, "variable", regexp.MustCompile(`^var\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + case ".ts", ".tsx", ".js", ".jsx": + appendMatch(line, i, "function", regexp.MustCompile(`^function\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`)) + appendMatch(line, i, "function", regexp.MustCompile(`^(?:export\\s+)?const\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(?:async\\s*)?\\(`)) + appendMatch(line, i, "class", regexp.MustCompile(`^class\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + appendMatch(line, i, "class", regexp.MustCompile(`^export\\s+class\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + appendMatch(line, i, "interface", regexp.MustCompile(`^interface\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + appendMatch(line, i, "type", regexp.MustCompile(`^type\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*=`)) + case ".py": + appendMatch(line, i, "function", regexp.MustCompile(`^def\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`)) + appendMatch(line, i, "class", regexp.MustCompile(`^class\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + case ".rs": + appendMatch(line, i, "function", regexp.MustCompile(`^fn\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`)) + appendMatch(line, i, "struct", regexp.MustCompile(`^struct\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + appendMatch(line, i, "enum", regexp.MustCompile(`^enum\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + appendMatch(line, i, "impl", regexp.MustCompile(`^impl\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + default: + appendMatch(line, i, "function", regexp.MustCompile(`^(?:public|private|protected|static|async|final|export)?\\s*([A-Za-z_][A-Za-z0-9_]*)\\s*\\(`)) + appendMatch(line, i, "class", regexp.MustCompile(`^class\\s+([A-Za-z_][A-Za-z0-9_]*)\\b`)) + } + } + + if len(out) > 400 { + out = out[:400] + } + return out +} + +func contextParseSymbolsByTreeSitter(ext string, content string) ([]contextSymbol, bool) { + lang := contextTreeSitterLanguage(ext) + if lang == nil { + return nil, false + } + source := []byte(content) + parser := sitter.NewParser() + defer parser.Close() + parser.SetLanguage(lang) + + tree, err := parser.ParseCtx(context.Background(), nil, source) + if err != nil || tree == nil { + return nil, false + } + defer tree.Close() + + root := tree.RootNode() + if root == nil { + return nil, true + } + + var symbols []contextSymbol + contextCollectTreeSitterSymbols(root, source, ext, &symbols) + if len(symbols) > 400 { + symbols = symbols[:400] + } + return symbols, true +} + +func contextCollectTreeSitterSymbols(node *sitter.Node, source []byte, ext string, out *[]contextSymbol) { + if node == nil { + return + } + kind, name := contextExtractTreeSitterSymbol(node, source, ext) + if name != "" { + lineText := contextLineByNumber(string(source), int(node.StartPoint().Row)+1) + if len(lineText) > 220 { + lineText = lineText[:220] + } + *out = append(*out, contextSymbol{ + Name: name, + Kind: kind, + Line: int(node.StartPoint().Row) + 1, + Signature: strings.TrimSpace(lineText), + }) + } + for i := uint32(0); i < node.NamedChildCount(); i++ { + contextCollectTreeSitterSymbols(node.NamedChild(int(i)), source, ext, out) + } +} + +func contextExtractTreeSitterSymbol(node *sitter.Node, source []byte, ext string) (string, string) { + nodeType := node.Type() + switch ext { + case ".go": + switch nodeType { + case "function_declaration", "method_declaration": + if n := node.ChildByFieldName("name"); n != nil { + return "function", strings.TrimSpace(n.Content(source)) + } + case "type_spec": + if n := node.ChildByFieldName("name"); n != nil { + return "type", strings.TrimSpace(n.Content(source)) + } + case "const_spec": + if n := node.ChildByFieldName("name"); n != nil { + return "const", strings.TrimSpace(n.Content(source)) + } + case "var_spec": + if n := node.ChildByFieldName("name"); n != nil { + return "variable", strings.TrimSpace(n.Content(source)) + } + } + case ".ts", ".tsx", ".js", ".jsx": + switch nodeType { + case "function_declaration", "generator_function_declaration": + if n := node.ChildByFieldName("name"); n != nil { + return "function", strings.TrimSpace(n.Content(source)) + } + case "method_definition": + if n := node.ChildByFieldName("name"); n != nil { + return "method", strings.TrimSpace(n.Content(source)) + } + case "class_declaration": + if n := node.ChildByFieldName("name"); n != nil { + return "class", strings.TrimSpace(n.Content(source)) + } + case "interface_declaration": + if n := node.ChildByFieldName("name"); n != nil { + return "interface", strings.TrimSpace(n.Content(source)) + } + case "type_alias_declaration": + if n := node.ChildByFieldName("name"); n != nil { + return "type", strings.TrimSpace(n.Content(source)) + } + case "variable_declarator": + n := node.ChildByFieldName("name") + v := node.ChildByFieldName("value") + if n != nil && v != nil { + vt := v.Type() + if vt == "arrow_function" || vt == "function_expression" || vt == "generator_function" || vt == "method_definition" { + return "function", strings.TrimSpace(n.Content(source)) + } + } + } + case ".py": + switch nodeType { + case "function_definition": + if n := node.ChildByFieldName("name"); n != nil { + return "function", strings.TrimSpace(n.Content(source)) + } + case "class_definition": + if n := node.ChildByFieldName("name"); n != nil { + return "class", strings.TrimSpace(n.Content(source)) + } + } + case ".rs": + switch nodeType { + case "function_item": + if n := node.ChildByFieldName("name"); n != nil { + return "function", strings.TrimSpace(n.Content(source)) + } + case "struct_item": + if n := node.ChildByFieldName("name"); n != nil { + return "struct", strings.TrimSpace(n.Content(source)) + } + case "enum_item": + if n := node.ChildByFieldName("name"); n != nil { + return "enum", strings.TrimSpace(n.Content(source)) + } + case "trait_item": + if n := node.ChildByFieldName("name"); n != nil { + return "trait", strings.TrimSpace(n.Content(source)) + } + } + default: + switch nodeType { + case "function_definition", "function_declaration", "method_definition", "class_declaration": + if n := node.ChildByFieldName("name"); n != nil { + kind := "function" + if nodeType == "class_declaration" { + kind = "class" + } else if nodeType == "method_definition" { + kind = "method" + } + return kind, strings.TrimSpace(n.Content(source)) + } + } + } + return "", "" +} + +func contextTreeSitterLanguage(ext string) *sitter.Language { + switch ext { + case ".go": + return sittergolang.GetLanguage() + case ".ts": + return sittertypescript.GetLanguage() + case ".tsx": + return sittertsx.GetLanguage() + case ".js", ".jsx": + return sitterjavascript.GetLanguage() + case ".py": + return sitterpython.GetLanguage() + case ".rs": + return sitterrust.GetLanguage() + case ".java": + return sitterjava.GetLanguage() + case ".kt": + return sitterkotlin.GetLanguage() + case ".swift": + return sitterswift.GetLanguage() + case ".c", ".h": + return sitterc.GetLanguage() + case ".cpp", ".hpp": + return sittercpp.GetLanguage() + case ".cs": + return sittercsharp.GetLanguage() + case ".php": + return sitterphp.GetLanguage() + case ".rb": + return sitterruby.GetLanguage() + case ".scala": + return sitterscala.GetLanguage() + case ".sql": + return sittersql.GetLanguage() + case ".sh": + return sitterbash.GetLanguage() + case ".yaml", ".yml": + return sitteryaml.GetLanguage() + default: + return nil + } +} + +func contextMergeSymbols(primary []contextSymbol, fallback []contextSymbol) []contextSymbol { + seen := map[string]bool{} + out := make([]contextSymbol, 0, len(primary)+len(fallback)) + for _, s := range primary { + key := strings.ToLower(fmt.Sprintf("%s|%s|%d", s.Kind, s.Name, s.Line)) + if seen[key] { + continue + } + seen[key] = true + out = append(out, s) + } + for _, s := range fallback { + key := strings.ToLower(fmt.Sprintf("%s|%s|%d", s.Kind, s.Name, s.Line)) + if seen[key] { + continue + } + seen[key] = true + out = append(out, s) + } + sort.Slice(out, func(i, j int) bool { + if out[i].Line == out[j].Line { + return out[i].Name < out[j].Name + } + return out[i].Line < out[j].Line + }) + if len(out) > 400 { + out = out[:400] + } + return out +} + +func contextFileDocForSearch(e contextFileEntry) string { + var symbolNames []string + for i, s := range e.Symbols { + if i >= 16 { + break + } + symbolNames = append(symbolNames, s.Name) + } + prefix := e.Content + if len(prefix) > 1200 { + prefix = prefix[:1200] + } + return strings.ToLower(strings.Join([]string{e.RelPath, e.Header, strings.Join(symbolNames, " "), prefix}, "\n")) +} + +func contextSplitWords(s string) []string { + s = strings.ToLower(s) + re := regexp.MustCompile(`[a-z0-9_]+`) + words := re.FindAllString(s, -1) + seen := map[string]bool{} + var out []string + for _, w := range words { + if len(w) < 2 || seen[w] { + continue + } + seen[w] = true + out = append(out, w) + } + return out +} + +func contextKeywordScore(queryWords []string, doc string) float64 { + if len(queryWords) == 0 || doc == "" { + return 0 + } + doc = strings.ToLower(doc) + hits := 0.0 + for _, w := range queryWords { + if strings.Contains(doc, w) { + hits += 1.0 + } + } + return hits / float64(len(queryWords)) +} + +func contextNormalizeThreshold(v *float64) float64 { + if v == nil { + return 0 + } + value := *v + if value > 1 { + value = value / 100.0 + } + if value < 0 { + return 0 + } + if value > 1 { + return 1 + } + return value +} + +func contextCosine(a []float64, b []float64) float64 { + if len(a) == 0 || len(b) == 0 || len(a) != len(b) { + return 0 + } + dot := 0.0 + normA := 0.0 + normB := 0.0 + for i := 0; i < len(a); i++ { + dot += a[i] * b[i] + normA += a[i] * a[i] + normB += b[i] * b[i] + } + if normA == 0 || normB == 0 { + return 0 + } + return dot / (math.Sqrt(normA) * math.Sqrt(normB)) +} + +func contextEmbedding(root string, text string) ([]float64, error) { + if strings.TrimSpace(text) == "" { + return nil, fmt.Errorf("empty text") + } + + baseURL := contextGetSetting("embedding_api_url", "http://localhost:11434") + model := contextGetSetting("embedding_model", "nomic-embed-text") + cacheKey := contextEmbeddingKey(baseURL, model, text) + + contextEmbedMu.Lock() + contextEmbedModel[root] = model + contextEmbedBaseURL[root] = baseURL + if !contextEmbedLoaded[root] { + contextEmbedCache[root] = contextLoadEmbeddingCache(root) + contextEmbedLoaded[root] = true + } + rootCache := contextEmbedCache[root] + if v, ok := rootCache[cacheKey]; ok { + contextEmbedMu.Unlock() + return v, nil + } + contextEmbedMu.Unlock() + + isOpenAICompatible := strings.Contains(baseURL, "/v1") || strings.Contains(baseURL, "openrouter.ai") + endpoint := strings.TrimRight(baseURL, "/") + "/api/embeddings" + payload := map[string]interface{}{"model": model, "prompt": text} + if isOpenAICompatible { + endpoint = strings.TrimRight(baseURL, "/") + "/embeddings" + payload = map[string]interface{}{"model": model, "input": text} + } + b, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", endpoint, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + if isOpenAICompatible { + apiKey := strings.TrimSpace(os.Getenv("OPENROUTER_API_KEY")) + if apiKey == "" || apiKey == "your_openrouter_api_key_here" { + apiKey = strings.TrimSpace(os.Getenv("OPENAI_API_KEY")) + } + if apiKey != "" { + req.Header.Set("Authorization", "Bearer "+apiKey) + } + } + client := &http.Client{Timeout: 45 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode/100 != 2 { + return nil, fmt.Errorf("embedding API status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) + } + + var ollamaOut struct { + Embedding []float64 `json:"embedding"` + } + var embedding []float64 + if err := json.Unmarshal(body, &ollamaOut); err == nil && len(ollamaOut.Embedding) > 0 { + embedding = ollamaOut.Embedding + } else { + var openAIOut struct { + Data []struct { + Embedding []float64 `json:"embedding"` + } `json:"data"` + } + if err := json.Unmarshal(body, &openAIOut); err != nil { + return nil, err + } + if len(openAIOut.Data) > 0 { + embedding = openAIOut.Data[0].Embedding + } + } + if len(embedding) == 0 { + return nil, fmt.Errorf("empty embedding vector") + } + + contextEmbedMu.Lock() + if _, ok := contextEmbedCache[root]; !ok { + contextEmbedCache[root] = map[string][]float64{} + } + contextEmbedCache[root][cacheKey] = embedding + contextEmbedDirty[root] = true + shouldSave := !contextEmbedSaving[root] + if shouldSave { + contextEmbedSaving[root] = true + } + contextEmbedMu.Unlock() + + if shouldSave { + go contextPersistEmbeddingCache(root) + } + + return embedding, nil +} + +func contextEmbeddingKey(baseURL string, model string, text string) string { + h := sha1.Sum([]byte(baseURL + "|" + model + "|" + text)) + return hex.EncodeToString(h[:]) +} + +func contextLoadEmbeddingCache(root string) map[string][]float64 { + cachePath := filepath.Join(root, ".apollo_contextplus", "embeddings_cache.json") + data, err := os.ReadFile(cachePath) + if err != nil { + return map[string][]float64{} + } + var m map[string][]float64 + if json.Unmarshal(data, &m) != nil { + return map[string][]float64{} + } + if m == nil { + return map[string][]float64{} + } + return m +} + +func contextPersistEmbeddingCache(root string) { + defer func() { + contextEmbedMu.Lock() + contextEmbedSaving[root] = false + if contextEmbedDirty[root] { + contextEmbedDirty[root] = false + contextEmbedSaving[root] = true + contextEmbedMu.Unlock() + go contextPersistEmbeddingCache(root) + return + } + contextEmbedMu.Unlock() + }() + + time.Sleep(300 * time.Millisecond) + + contextEmbedMu.Lock() + if !contextEmbedDirty[root] { + contextEmbedMu.Unlock() + return + } + cacheCopy := make(map[string][]float64, len(contextEmbedCache)) + rootCache, ok := contextEmbedCache[root] + if !ok { + contextEmbedMu.Unlock() + return + } + cacheCopy = make(map[string][]float64, len(rootCache)) + for k, v := range rootCache { + cacheCopy[k] = v + } + contextEmbedDirty[root] = false + contextEmbedMu.Unlock() + + data, err := json.Marshal(cacheCopy) + if err != nil { + return + } + baseDir := filepath.Join(root, ".apollo_contextplus") + _ = os.MkdirAll(baseDir, 0o755) + _ = os.WriteFile(filepath.Join(baseDir, "embeddings_cache.json"), data, 0o644) +} + +func contextGetSetting(key string, def string) string { + var val string + err := db.DB.QueryRow("SELECT value FROM settings WHERE key = ?", key).Scan(&val) + if err != nil || strings.TrimSpace(val) == "" { + return def + } + return strings.TrimSpace(val) +} + +func contextFindCallSites(entries []contextFileEntry, symbol string, defFile string, defLine int, limit int) []string { + if limit <= 0 { + limit = 10 + } + re := regexp.MustCompile(`\\b` + regexp.QuoteMeta(symbol) + `\\b`) + var sites []string + for _, e := range entries { + content, err := os.ReadFile(e.AbsPath) + if err != nil { + continue + } + lines := strings.Split(string(content), "\n") + for i, line := range lines { + if !re.MatchString(line) { + continue + } + if e.RelPath == defFile && i+1 == defLine { + continue + } + sites = append(sites, fmt.Sprintf("%s:%d: %s", e.RelPath, i+1, strings.TrimSpace(line))) + if len(sites) >= limit { + return sites + } + } + } + return sites +} + +func contextTopTerms(files []contextFileEntry, max int) []string { + stop := map[string]bool{ + "the": true, "and": true, "for": true, "with": true, "from": true, + "const": true, "func": true, "function": true, "class": true, + "type": true, "import": true, "export": true, "return": true, + } + counts := map[string]int{} + for _, f := range files { + tokens := contextSplitWords(f.RelPath + " " + f.Header) + for _, t := range tokens { + if len(t) < 3 || stop[t] { + continue + } + counts[t]++ + } + } + type kv struct { + K string + V int + } + var list []kv + for k, v := range counts { + list = append(list, kv{k, v}) + } + sort.Slice(list, func(i, j int) bool { return list[i].V > list[j].V }) + if len(list) > max { + list = list[:max] + } + var out []string + for _, item := range list { + out = append(out, item.K) + } + return out +} + +func contextBuildSemanticVectors(root string, entries []contextFileEntry) [][]float64 { + raw := make([][]float64, len(entries)) + docs := make([]string, len(entries)) + targetDim := 0 + + for i, e := range entries { + doc := contextFileDocForSearch(e) + docs[i] = doc + vec, err := contextEmbedding(root, fmt.Sprintf("file:%s:%s:%s", e.RelPath, e.Hash, doc)) + if err == nil && len(vec) > 0 { + raw[i] = vec + if targetDim == 0 { + targetDim = len(vec) + } + } + } + + if targetDim == 0 { + targetDim = 96 + } + + out := make([][]float64, len(entries)) + for i := range entries { + if len(raw[i]) == targetDim { + out[i] = contextNormalizeVector(raw[i]) + continue + } + if len(raw[i]) > 0 { + out[i] = contextNormalizeVector(contextResizeVector(raw[i], targetDim)) + continue + } + out[i] = contextLexicalVector(docs[i], targetDim) + } + return out +} + +func contextResizeVector(vec []float64, size int) []float64 { + if size <= 0 { + return []float64{} + } + out := make([]float64, size) + if len(vec) == 0 { + return out + } + if len(vec) >= size { + copy(out, vec[:size]) + return out + } + copy(out, vec) + return out +} + +func contextLexicalVector(doc string, dim int) []float64 { + vec := make([]float64, dim) + if dim <= 0 { + return vec + } + words := contextSplitWords(doc) + if len(words) == 0 { + return vec + } + for _, w := range words { + h := sha1.Sum([]byte(w)) + i1 := (int(h[0])<<8 | int(h[1])) % dim + i2 := (int(h[2])<<8 | int(h[3])) % dim + s1 := 1.0 + s2 := 1.0 + if h[4]%2 == 0 { + s1 = -1 + } + if h[5]%2 == 0 { + s2 = -1 + } + vec[i1] += s1 + vec[i2] += s2 + } + return contextNormalizeVector(vec) +} + +func contextNormalizeVector(vec []float64) []float64 { + out := make([]float64, len(vec)) + copy(out, vec) + norm := 0.0 + for _, v := range out { + norm += v * v + } + if norm == 0 { + return out + } + norm = math.Sqrt(norm) + for i := range out { + out[i] /= norm + } + return out +} + +func contextSelectDiverseIndices(vectors [][]float64, limit int) []int { + n := len(vectors) + if n == 0 || limit <= 0 { + return nil + } + if n <= limit { + out := make([]int, n) + for i := range vectors { + out[i] = i + } + return out + } + + start := 0 + bestNorm := -1.0 + for i, v := range vectors { + norm := 0.0 + for _, x := range v { + norm += x * x + } + if norm > bestNorm { + bestNorm = norm + start = i + } + } + + selected := []int{start} + selectedSet := map[int]bool{start: true} + minDist := make([]float64, n) + for i := range minDist { + minDist[i] = math.Inf(1) + } + + for len(selected) < limit { + last := selected[len(selected)-1] + for i := 0; i < n; i++ { + if selectedSet[i] { + continue + } + dist := 1.0 - contextCosine(vectors[i], vectors[last]) + if dist < minDist[i] { + minDist[i] = dist + } + } + + next := -1 + nextDist := -1.0 + for i := 0; i < n; i++ { + if selectedSet[i] { + continue + } + if minDist[i] > nextDist { + nextDist = minDist[i] + next = i + } + } + if next < 0 { + break + } + selected = append(selected, next) + selectedSet[next] = true + } + + sort.Ints(selected) + return selected +} + +func contextSelectSplitCandidate(root *contextSemanticNode, maxDepth int) *contextSemanticNode { + leaves := contextCollectLeafNodes(root) + var best *contextSemanticNode + for _, leaf := range leaves { + if leaf.Locked { + continue + } + if leaf.Depth >= maxDepth { + continue + } + if len(leaf.FileIndices) < 6 { + continue + } + if best == nil || len(leaf.FileIndices) > len(best.FileIndices) { + best = leaf + } + } + return best +} + +func contextCollectLeafNodes(root *contextSemanticNode) []*contextSemanticNode { + if root == nil { + return nil + } + if len(root.Children) == 0 { + return []*contextSemanticNode{root} + } + var out []*contextSemanticNode + for _, child := range root.Children { + out = append(out, contextCollectLeafNodes(child)...) + } + return out +} + +func contextSpectralBisect(vectors [][]float64, indices []int) ([]int, []int, bool) { + n := len(indices) + if n < 4 { + return nil, nil, false + } + + sim := make([]float64, n*n) + for i := 0; i < n; i++ { + sim[i*n+i] = 1 + for j := i + 1; j < n; j++ { + s := contextCosine(vectors[indices[i]], vectors[indices[j]]) + if s < 0 { + s = 0 + } + sim[i*n+j] = s + sim[j*n+i] = s + } + } + + useSparse := n > 30 + neighbors := 12 + if n-1 < neighbors { + neighbors = n - 1 + } + adj := make([]bool, n*n) + if useSparse { + for i := 0; i < n; i++ { + type pair struct { + j int + s float64 + } + var scores []pair + for j := 0; j < n; j++ { + if i == j { + continue + } + if sim[i*n+j] <= 0 { + continue + } + scores = append(scores, pair{j: j, s: sim[i*n+j]}) + } + sort.Slice(scores, func(a, b int) bool { return scores[a].s > scores[b].s }) + if len(scores) > neighbors { + scores = scores[:neighbors] + } + for _, p := range scores { + adj[i*n+p.j] = true + adj[p.j*n+i] = true + } + } + } else { + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + if sim[i*n+j] > 0 { + adj[i*n+j] = true + adj[j*n+i] = true + } + } + } + } + + w := make([]float64, n*n) + degree := make([]float64, n) + for i := 0; i < n; i++ { + for j := i + 1; j < n; j++ { + if !adj[i*n+j] { + continue + } + v := sim[i*n+j] + w[i*n+j] = v + w[j*n+i] = v + degree[i] += v + degree[j] += v + } + } + for i := 0; i < n; i++ { + if degree[i] == 0 { + degree[i] = 1e-9 + } + } + + lData := make([]float64, n*n) + for i := 0; i < n; i++ { + lData[i*n+i] = 1 + for j := i + 1; j < n; j++ { + if w[i*n+j] <= 0 { + continue + } + v := -w[i*n+j] / math.Sqrt(degree[i]*degree[j]) + lData[i*n+j] = v + lData[j*n+i] = v + } + } + lap := mat.NewSymDense(n, lData) + var eig mat.EigenSym + if ok := eig.Factorize(lap, true); !ok { + return nil, nil, false + } + values := eig.Values(nil) + if len(values) < 2 { + return nil, nil, false + } + order := make([]int, len(values)) + for i := range values { + order[i] = i + } + sort.Slice(order, func(i, j int) bool { return values[order[i]] < values[order[j]] }) + fiedlerCol := order[1] + + vecs := mat.NewDense(n, n, nil) + eig.VectorsTo(vecs) + fiedler := make([]float64, n) + for i := 0; i < n; i++ { + fiedler[i] = vecs.At(i, fiedlerCol) + } + threshold := contextMedianFloat64(fiedler) + + var left, right []int + for i := 0; i < n; i++ { + if fiedler[i] <= threshold { + left = append(left, indices[i]) + } else { + right = append(right, indices[i]) + } + } + if len(left) < 2 || len(right) < 2 { + left = nil + right = nil + for i := 0; i < n; i++ { + if fiedler[i] <= 0 { + left = append(left, indices[i]) + } else { + right = append(right, indices[i]) + } + } + } + if len(left) < 2 || len(right) < 2 { + return nil, nil, false + } + return left, right, true +} + +func contextMedianFloat64(values []float64) float64 { + if len(values) == 0 { + return 0 + } + cp := append([]float64(nil), values...) + sort.Float64s(cp) + mid := len(cp) / 2 + if len(cp)%2 == 1 { + return cp[mid] + } + return (cp[mid-1] + cp[mid]) / 2 +} + +func contextCentroidForIndices(vectors [][]float64, indices []int) []float64 { + if len(indices) == 0 { + return nil + } + dim := len(vectors[indices[0]]) + centroid := make([]float64, dim) + for _, idx := range indices { + v := vectors[idx] + limit := dim + if len(v) < limit { + limit = len(v) + } + for i := 0; i < limit; i++ { + centroid[i] += v[i] + } + } + for i := range centroid { + centroid[i] /= float64(len(indices)) + } + return contextNormalizeVector(centroid) +} + +func contextRenderSemanticNode(b *strings.Builder, node *contextSemanticNode, entries []contextFileEntry, depth int) { + if node == nil { + return + } + indent := strings.Repeat(" ", depth) + files := make([]contextFileEntry, 0, len(node.FileIndices)) + for _, idx := range node.FileIndices { + if idx >= 0 && idx < len(entries) { + files = append(files, entries[idx]) + } + } + terms := contextTopTerms(files, 5) + + label := node.ID + if len(terms) > 0 { + show := 3 + if len(terms) < show { + show = len(terms) + } + label = fmt.Sprintf("%s [%s]", node.ID, strings.Join(terms[:show], ", ")) + } + + b.WriteString(fmt.Sprintf("%s- Cluster %s (%d files)\n", indent, label, len(files))) + if len(node.Children) == 0 { + show := 6 + if len(files) < show { + show = len(files) + } + for i := 0; i < show; i++ { + b.WriteString(fmt.Sprintf("%s - %s\n", indent, files[i].RelPath)) + } + if len(files) > show { + b.WriteString(fmt.Sprintf("%s - ...\n", indent)) + } + return + } + for _, child := range node.Children { + contextRenderSemanticNode(b, child, entries, depth+1) + } +} + +func contextCollectHubs(root string) ([]contextHub, error) { + var hubs []contextHub + re := regexp.MustCompile(`\\[\\[([^\\]|#]+)[^\\]]*\\]\\]`) + err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return nil + } + if d.IsDir() { + if contextSkipDirs[d.Name()] { + return filepath.SkipDir + } + if strings.HasPrefix(d.Name(), ".") && d.Name() != ".github" { + return filepath.SkipDir + } + return nil + } + if strings.ToLower(filepath.Ext(path)) != ".md" { + return nil + } + data, readErr := os.ReadFile(path) + if readErr != nil { + return nil + } + matches := re.FindAllStringSubmatch(string(data), -1) + if len(matches) == 0 { + return nil + } + links := make([]string, 0, len(matches)) + for _, m := range matches { + if len(m) < 2 { + continue + } + links = append(links, strings.TrimSpace(m[1])) + } + hubs = append(hubs, contextHub{RelPath: contextRelativePath(root, path), AbsPath: path, Links: links}) + return nil + }) + return hubs, err +} + +func contextFindHubOrphans(root string, hubs []contextHub) []string { + linked := map[string]bool{} + for _, h := range hubs { + for _, l := range h.Links { + resolved := contextResolveHubLink(root, h.AbsPath, l) + if resolved == "" { + continue + } + linked[contextRelativePath(root, resolved)] = true + } + } + + entries, _ := contextCollectFiles(root, root) + var orphans []string + for _, e := range entries { + if !linked[e.RelPath] { + orphans = append(orphans, e.RelPath) + } + } + sort.Strings(orphans) + return orphans +} + +func contextSelectHub(root string, hubs []contextHub, hubPath string, feature string) *contextHub { + if strings.TrimSpace(hubPath) != "" { + clean := filepath.Clean(strings.TrimPrefix(hubPath, "/")) + for i := range hubs { + if filepath.Clean(hubs[i].RelPath) == clean { + return &hubs[i] + } + } + } + if strings.TrimSpace(feature) != "" { + needle := strings.ToLower(feature) + for i := range hubs { + if strings.Contains(strings.ToLower(hubs[i].RelPath), needle) { + return &hubs[i] + } + } + } + return nil +} + +func contextResolveHubLink(root string, hubAbs string, link string) string { + link = strings.TrimSpace(link) + if link == "" { + return "" + } + candidate := link + if filepath.Ext(candidate) == "" { + for _, ext := range []string{".ts", ".tsx", ".js", ".jsx", ".go", ".py", ".rs", ".md"} { + if contextPathExists(filepath.Join(root, candidate+ext)) { + candidate = candidate + ext + break + } + } + } + + if strings.HasPrefix(candidate, "/") { + abs := filepath.Join(root, strings.TrimPrefix(candidate, "/")) + if contextPathExists(abs) { + return abs + } + } + + hubDir := filepath.Dir(hubAbs) + local := filepath.Clean(filepath.Join(hubDir, candidate)) + if strings.HasPrefix(local, root) && contextPathExists(local) { + return local + } + + global := filepath.Clean(filepath.Join(root, candidate)) + if strings.HasPrefix(global, root) && contextPathExists(global) { + return global + } + return "" +} + +func contextPathExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func contextValidateCommitContent(content string) string { + var warnings []string + lines := strings.Split(content, "\n") + if len(lines) < 2 { + warnings = append(warnings, "- Missing 2-line file header comment.") + } else { + first := strings.TrimSpace(lines[0]) + second := strings.TrimSpace(lines[1]) + if !contextIsCommentLine(first) || !contextIsCommentLine(second) { + warnings = append(warnings, "- First two lines are not comments; Context+ style headers are recommended.") + } + } + if len(lines) > 1000 { + warnings = append(warnings, "- File exceeds 1000 lines; split into smaller modules.") + } + return strings.Join(warnings, "\n") +} + +func contextIsCommentLine(line string) bool { + return strings.HasPrefix(line, "//") || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "/*") || strings.HasPrefix(line, "--") +} + +func contextIndexPath(root string) string { + return filepath.Join(contextDataDir(root), "index.json") +} + +func contextEnsureIndex(root string) error { + contextEnsureTracker(root) + + contextIndexMu.Lock() + defer contextIndexMu.Unlock() + + idx, ok := contextIndexes[root] + if !ok || idx == nil { + idx = contextLoadIndex(root) + contextIndexes[root] = idx + } + + seen := map[string]bool{} + var changed []string + removed := false + + err := filepath.WalkDir(root, func(path string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + return nil + } + if d.IsDir() { + name := d.Name() + if contextSkipDirs[name] { + return filepath.SkipDir + } + if strings.HasPrefix(name, ".") && name != ".github" { + return filepath.SkipDir + } + return nil + } + + ext := strings.ToLower(filepath.Ext(path)) + if !contextCodeExt[ext] { + return nil + } + + info, infoErr := d.Info() + if infoErr != nil { + return nil + } + + rel := contextRelativePath(root, path) + seen[rel] = true + + existing, exists := idx.Files[rel] + mtime := info.ModTime().UnixMilli() + size := info.Size() + if exists && existing.MTime == mtime && existing.Size == size { + return nil + } + + data, readErr := os.ReadFile(path) + if readErr != nil { + return nil + } + content := string(data) + if !contextLooksText(content) { + return nil + } + + snippet := content + if len(snippet) > 2000 { + snippet = snippet[:2000] + } + fileHash := contextHashString(content) + idx.Files[rel] = contextIndexedFile{ + RelPath: rel, + Ext: ext, + Lang: contextLanguageByExt[ext], + Header: contextFileHeader(content), + Snippet: snippet, + Symbols: contextParseSymbolsByExt(ext, content), + MTime: mtime, + Size: size, + Hash: fileHash, + } + changed = append(changed, rel) + return nil + }) + if err != nil { + return err + } + + for rel := range idx.Files { + if seen[rel] { + continue + } + delete(idx.Files, rel) + removed = true + } + + if len(changed) > 0 || removed { + idx.UpdatedAt = time.Now().UnixMilli() + if saveErr := contextSaveIndex(root, idx); saveErr != nil { + return saveErr + } + } + + if len(changed) > 0 { + contextQueuePendingWarmLocked(root, changed) + } + + return nil +} + +func contextLoadIndex(root string) *contextProjectIndex { + idx := &contextProjectIndex{ + Version: 1, + Root: root, + UpdatedAt: time.Now().UnixMilli(), + Files: map[string]contextIndexedFile{}, + } + data, err := os.ReadFile(contextIndexPath(root)) + if err != nil { + return idx + } + var loaded contextProjectIndex + if json.Unmarshal(data, &loaded) != nil { + return idx + } + if loaded.Files == nil { + loaded.Files = map[string]contextIndexedFile{} + } + loaded.Root = root + if loaded.Version <= 0 { + loaded.Version = 1 + } + return &loaded +} + +func contextSaveIndex(root string, idx *contextProjectIndex) error { + if idx == nil { + return nil + } + if err := os.MkdirAll(contextDataDir(root), 0o755); err != nil { + return err + } + b, err := json.Marshal(idx) + if err != nil { + return err + } + return os.WriteFile(contextIndexPath(root), b, 0o644) +} + +func contextEnsureTracker(root string) { + if !contextEmbedTrackerEnabled() { + return + } + contextIndexMu.Lock() + if contextTrackerActive[root] { + contextIndexMu.Unlock() + return + } + contextTrackerActive[root] = true + contextIndexMu.Unlock() + go contextTrackerLoop(root) +} + +func contextTrackerLoop(root string) { + debounce := contextEnvInt("CONTEXTPLUS_EMBED_TRACKER_DEBOUNCE_MS", 700, 200, 10000) + maxFiles := contextEnvInt("CONTEXTPLUS_EMBED_TRACKER_MAX_FILES", 8, 1, 20) + ticker := time.NewTicker(time.Duration(debounce) * time.Millisecond) + defer ticker.Stop() + for range ticker.C { + _ = contextEnsureIndex(root) + files := contextPopPendingWarm(root, maxFiles) + if len(files) == 0 { + continue + } + + contextIndexMu.Lock() + idx := contextIndexes[root] + indexedFiles := make([]contextIndexedFile, 0, len(files)) + for _, rel := range files { + if f, ok := idx.Files[rel]; ok { + indexedFiles = append(indexedFiles, f) + } + } + contextIndexMu.Unlock() + + for _, f := range indexedFiles { + contextWarmEmbeddingsForFile(root, f) + } + } +} + +func contextQueuePendingWarmLocked(root string, relPaths []string) { + set, ok := contextPendingWarm[root] + if !ok { + set = map[string]bool{} + contextPendingWarm[root] = set + } + for _, rel := range relPaths { + set[rel] = true + } +} + +func contextPopPendingWarm(root string, max int) []string { + contextIndexMu.Lock() + defer contextIndexMu.Unlock() + set, ok := contextPendingWarm[root] + if !ok || len(set) == 0 { + return nil + } + keys := make([]string, 0, len(set)) + for k := range set { + keys = append(keys, k) + } + sort.Strings(keys) + if len(keys) > max { + keys = keys[:max] + } + for _, k := range keys { + delete(set, k) + } + return keys +} + +func contextWarmEmbeddingsForFile(root string, file contextIndexedFile) { + fileDoc := strings.ToLower(strings.Join([]string{ + file.RelPath, + file.Header, + contextSymbolsToText(file.Symbols, 16), + file.Snippet, + }, "\n")) + if strings.TrimSpace(fileDoc) != "" { + _, _ = contextEmbedding(root, fmt.Sprintf("file:%s:%s:%s", file.RelPath, file.Hash, fileDoc)) + } + + maxIdentifiers := 16 + if len(file.Symbols) < maxIdentifiers { + maxIdentifiers = len(file.Symbols) + } + for i := 0; i < maxIdentifiers; i++ { + s := file.Symbols[i] + idDoc := strings.ToLower(fmt.Sprintf("%s %s %s %s %s", s.Name, s.Kind, s.Signature, file.RelPath, file.Header)) + _, _ = contextEmbedding(root, fmt.Sprintf("identifier:%s:%d:%s:%s", file.RelPath, s.Line, file.Hash, idDoc)) + } +} + +func contextSymbolsToText(symbols []contextSymbol, limit int) string { + if len(symbols) == 0 || limit <= 0 { + return "" + } + if len(symbols) < limit { + limit = len(symbols) + } + parts := make([]string, 0, limit) + for i := 0; i < limit; i++ { + parts = append(parts, symbols[i].Name) + } + return strings.Join(parts, " ") +} + +func contextEmbedTrackerEnabled() bool { + raw := strings.ToLower(strings.TrimSpace(os.Getenv("CONTEXTPLUS_EMBED_TRACKER"))) + if raw == "" { + return true + } + return raw != "0" && raw != "false" && raw != "no" && raw != "off" +} + +func contextEnvInt(name string, def int, min int, max int) int { + raw := strings.TrimSpace(os.Getenv(name)) + if raw == "" { + return def + } + n, err := strconv.Atoi(raw) + if err != nil { + return def + } + if n < min { + return min + } + if n > max { + return max + } + return n +} + +func contextPathWithinTarget(path string, target string) bool { + cleanPath := filepath.Clean(path) + cleanTarget := filepath.Clean(target) + if cleanPath == cleanTarget { + return true + } + return strings.HasPrefix(cleanPath, cleanTarget+string(os.PathSeparator)) +} + +func contextGetIndexedFile(root string, relPath string) (contextIndexedFile, bool) { + contextIndexMu.Lock() + defer contextIndexMu.Unlock() + idx, ok := contextIndexes[root] + if !ok || idx == nil { + return contextIndexedFile{}, false + } + f, ok := idx.Files[filepath.Clean(relPath)] + return f, ok +} + +func contextHashString(content string) string { + sum := sha1.Sum([]byte(content)) + return hex.EncodeToString(sum[:]) +} + +func contextLineByNumber(content string, lineNumber int) string { + if lineNumber <= 0 { + return "" + } + lines := strings.Split(content, "\n") + if lineNumber > len(lines) { + return "" + } + return strings.TrimSpace(lines[lineNumber-1]) +} + +func contextDataDir(root string) string { + return filepath.Join(root, ".apollo_contextplus") +} + +func contextRestoreBaseDir(root string) string { + return filepath.Join(contextDataDir(root), "restore_points") +} + +func contextCreateRestorePoint(root string, relPaths []string, message string) (string, error) { + if err := os.MkdirAll(contextRestoreBaseDir(root), 0o755); err != nil { + return "", err + } + + random := make([]byte, 3) + _, _ = rand.Read(random) + rpID := fmt.Sprintf("rp-%d-%s", time.Now().UnixMilli(), hex.EncodeToString(random)) + rpDir := filepath.Join(contextRestoreBaseDir(root), rpID) + filesDir := filepath.Join(rpDir, "files") + if err := os.MkdirAll(filesDir, 0o755); err != nil { + return "", err + } + + point := contextRestorePoint{ + ID: rpID, + Timestamp: time.Now().UnixMilli(), + Message: message, + Files: make([]contextRestorePointFile, 0, len(relPaths)), + } + + for _, rel := range relPaths { + rel = filepath.Clean(strings.TrimPrefix(rel, "/")) + abs := filepath.Join(root, rel) + fileEntry := contextRestorePointFile{Path: rel} + if contextPathExists(abs) { + fileEntry.Existed = true + backupPath := filepath.Join(filesDir, rel) + if err := os.MkdirAll(filepath.Dir(backupPath), 0o755); err != nil { + return "", err + } + src, err := os.Open(abs) + if err != nil { + return "", err + } + dst, err := os.Create(backupPath) + if err != nil { + src.Close() + return "", err + } + _, copyErr := io.Copy(dst, src) + src.Close() + dst.Close() + if copyErr != nil { + return "", copyErr + } + } + point.Files = append(point.Files, fileEntry) + } + + metaBytes, _ := json.MarshalIndent(point, "", " ") + if err := os.WriteFile(filepath.Join(rpDir, "meta.json"), metaBytes, 0o644); err != nil { + return "", err + } + return rpID, nil +} + +func contextListRestorePoints(root string) ([]contextRestorePoint, error) { + base := contextRestoreBaseDir(root) + if !contextPathExists(base) { + return []contextRestorePoint{}, nil + } + dirs, err := os.ReadDir(base) + if err != nil { + return nil, err + } + var points []contextRestorePoint + for _, d := range dirs { + if !d.IsDir() { + continue + } + metaPath := filepath.Join(base, d.Name(), "meta.json") + data, err := os.ReadFile(metaPath) + if err != nil { + continue + } + var p contextRestorePoint + if json.Unmarshal(data, &p) != nil { + continue + } + points = append(points, p) + } + sort.Slice(points, func(i, j int) bool { return points[i].Timestamp > points[j].Timestamp }) + return points, nil +} + +func contextRestorePointByID(root string, pointID string) ([]string, error) { + metaPath := filepath.Join(contextRestoreBaseDir(root), pointID, "meta.json") + data, err := os.ReadFile(metaPath) + if err != nil { + return nil, fmt.Errorf("restore point not found") + } + var p contextRestorePoint + if err := json.Unmarshal(data, &p); err != nil { + return nil, err + } + + filesDir := filepath.Join(contextRestoreBaseDir(root), pointID, "files") + var restored []string + for _, f := range p.Files { + abs := filepath.Join(root, f.Path) + if !strings.HasPrefix(abs, root) { + continue + } + if f.Existed { + backup := filepath.Join(filesDir, f.Path) + if !contextPathExists(backup) { + continue + } + if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil { + return restored, err + } + src, err := os.Open(backup) + if err != nil { + return restored, err + } + dst, err := os.Create(abs) + if err != nil { + src.Close() + return restored, err + } + _, copyErr := io.Copy(dst, src) + src.Close() + dst.Close() + if copyErr != nil { + return restored, copyErr + } + restored = append(restored, f.Path) + } else { + _ = os.Remove(abs) + restored = append(restored, f.Path) + } + } + return restored, nil +} + +func contextRelativePath(root string, path string) string { + rel, err := filepath.Rel(root, path) + if err != nil { + return path + } + if rel == "." { + return rel + } + return filepath.Clean(rel) +} + +func contextDetectLanguages(target string) map[string]bool { + langs := map[string]bool{} + _ = filepath.WalkDir(target, func(path string, d os.DirEntry, err error) error { + if err != nil { + return nil + } + if d.IsDir() { + if contextSkipDirs[d.Name()] { + return filepath.SkipDir + } + if strings.HasPrefix(d.Name(), ".") && d.Name() != ".github" { + return filepath.SkipDir + } + return nil + } + switch strings.ToLower(filepath.Ext(path)) { + case ".go": + langs["go"] = true + case ".ts", ".tsx": + langs["typescript"] = true + case ".js", ".jsx": + langs["javascript"] = true + case ".py": + langs["python"] = true + case ".rs": + langs["rust"] = true + } + return nil + }) + return langs +} + +func contextFindFilesByExt(root string, ext string, limit int) []string { + var out []string + _ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { + if err != nil { + return nil + } + if d.IsDir() { + if contextSkipDirs[d.Name()] { + return filepath.SkipDir + } + if strings.HasPrefix(d.Name(), ".") && d.Name() != ".github" { + return filepath.SkipDir + } + return nil + } + if strings.EqualFold(filepath.Ext(path), ext) { + out = append(out, path) + if len(out) >= limit { + return io.EOF + } + } + return nil + }) + return out +} + +func contextRunCheckCommand(dir string, title string, name string, args ...string) string { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + cmd := exec.CommandContext(ctx, name, args...) + cmd.Dir = dir + out, err := cmd.CombinedOutput() + output := strings.TrimSpace(string(out)) + if ctx.Err() == context.DeadlineExceeded { + return fmt.Sprintf("[%s]\nTimed out after 2m", title) + } + if err != nil { + if output == "" { + return fmt.Sprintf("[%s]\nFailed: %v", title, err) + } + return fmt.Sprintf("[%s]\n%s", title, output) + } + if output == "" { + return fmt.Sprintf("[%s]\n✓ No issues", title) + } + return fmt.Sprintf("[%s]\n%s", title, output) +} + +func contextFindIntArg(raw map[string]interface{}, key string, def int) int { + v, ok := raw[key] + if !ok { + return def + } + switch t := v.(type) { + case float64: + return int(t) + case int: + return t + case string: + if n, err := strconv.Atoi(t); err == nil { + return n + } + } + return def +} diff --git a/dash/backend/tools/exec.go b/dash/backend/tools/exec.go new file mode 100644 index 0000000..cbd0b96 --- /dev/null +++ b/dash/backend/tools/exec.go @@ -0,0 +1,54 @@ +package tools + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os/exec" + "time" +) + +// Executes a shell command strictly jailed inside the Workspace sandbox +func executeCommand(rawArgs string, projectRoot string) string { + var args struct { + Command string `json:"command"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Command == "" { + return "Error: Invalid arguments. 'command' is required." + } + + // 15 Second Hard Timeout to prevent AI locking the server with frozen processes + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "sh", "-c", args.Command) + + // Jail execution working directory to the project root if available, otherwise global sandbox + workDir := WorkspaceDir + if projectRoot != "" { + workDir = projectRoot + } + cmd.Dir = workDir + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + _ = cmd.Run() + + if ctx.Err() == context.DeadlineExceeded { + return fmt.Sprintf("Error: Command timed out after 15 seconds.\nStdout: %s\nStderr: %s", stdout.String(), stderr.String()) + } + + result := fmt.Sprintf("Exit Code: %v\n\n--- Stdout ---\n%s\n--- Stderr ---\n%s", + cmd.ProcessState.ExitCode(), + stdout.String(), + stderr.String(), + ) + + if len(result) > 15000 { + return result[:15000] + "\n\n... [TERMINAL TRUNCATED FOR LENGTH]" + } + return result +} diff --git a/dash/backend/tools/fs.go b/dash/backend/tools/fs.go new file mode 100644 index 0000000..178c4a5 --- /dev/null +++ b/dash/backend/tools/fs.go @@ -0,0 +1,525 @@ +package tools + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/danilrybalkin/apollo-dash/db" +) + +var WorkspaceDir = resolveWorkspaceDir() + +func resolveWorkspaceDir() string { + for _, key := range []string{"AGENTHQ_WORKSPACE_ROOT", "APOLLO_WORKSPACE_ROOT"} { + if value := strings.TrimSpace(os.Getenv(key)); value != "" { + if abs, err := filepath.Abs(value); err == nil { + return filepath.Clean(abs) + } + return filepath.Clean(value) + } + } + if abs, err := filepath.Abs(filepath.Join("data", "workspaces")); err == nil { + return filepath.Clean(abs) + } + return filepath.Clean(filepath.Join("data", "workspaces")) +} + +func withinBase(path string, base string) bool { + rel, err := filepath.Rel(base, path) + if err != nil { + return false + } + return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))) +} + +// Ensures the workspace directory exists on boot +func InitWorkspace() { + if _, err := os.Stat(WorkspaceDir); os.IsNotExist(err) { + err := os.MkdirAll(WorkspaceDir, 0755) + if err != nil { + fmt.Printf("Startup Error: Failed to create AI Workspace at %s: %v\n", WorkspaceDir, err) + } else { + fmt.Printf("Startup Success: Created AI Sandbox Workspace at %s\n", WorkspaceDir) + } + } + + // Ensure the workspace is a Git repository to enable the Time Machine / Checkpointing Checkpoint System + cmd := exec.Command("git", "init") + cmd.Dir = WorkspaceDir + _ = cmd.Run() +} + +// Security function to prevent path traversal (e.g. reading /etc/shadow) +func securePath(requestedPath string, projectRoot string) (string, error) { + // If the LLM passes an absolute path inside the workspace, or a relative one like file.txt + cleanRequested := filepath.Clean(requestedPath) + + // Determine the effective root (either the specific project or the global workspace) + effectiveRoot := WorkspaceDir + if projectRoot != "" { + effectiveRoot = projectRoot + } + + // If it doesn't already start with the effective root, prepend it + if !strings.HasPrefix(cleanRequested, effectiveRoot) { + cleanRequested = filepath.Join(effectiveRoot, cleanRequested) + } + + // Final evaluation to ensure it didn't use ../../../ to escape + cleanFinal := filepath.Clean(cleanRequested) + + // Must be within WorkspaceDir ALWAYS + if !withinBase(cleanFinal, WorkspaceDir) { + return "", fmt.Errorf("security violation: path traversal detected (%s)", requestedPath) + } + + // If projectRoot is set, it MUST be within that too + if projectRoot != "" && !withinBase(cleanFinal, projectRoot) { + return "", fmt.Errorf("security violation: path outside assigned project root (%s)", requestedPath) + } + + return cleanFinal, nil +} + +// ---------------------------------------------------- +// TIME MACHINE +// ---------------------------------------------------- + +func CreateCheckpoint(message string) string { + // Add all changes to git stagings + addCmd := exec.Command("git", "add", "-A") + addCmd.Dir = WorkspaceDir + _ = addCmd.Run() + + // Commit with the automated message + commitCmd := exec.Command("git", "commit", "-m", "[AgentHQ Auto-Checkpoint] "+message) + commitCmd.Dir = WorkspaceDir + commitCmd.Run() + + revCmd := exec.Command("git", "rev-parse", "HEAD") + revCmd.Dir = WorkspaceDir + out, err := revCmd.Output() + if err == nil { + return strings.TrimSpace(string(out)) + } + return "" +} + +// ---------------------------------------------------- +// IMPLEMENTATIONS +// ---------------------------------------------------- + +func executeListFiles(rawArgs string, projectRoot string) string { + var args struct { + Path string `json:"path"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: Invalid arguments." + } + + targetPath := args.Path + if targetPath == "" { + targetPath = projectRoot + if targetPath == "" { + targetPath = WorkspaceDir + } + } + + safePath, err := securePath(targetPath, projectRoot) + if err != nil { + return err.Error() + } + + files, err := ioutil.ReadDir(safePath) + if err != nil { + return fmt.Sprintf("Error reading directory: %v", err) + } + + if len(files) == 0 { + return "Directory is empty." + } + + var out strings.Builder + for _, f := range files { + kind := "FILE" + if f.IsDir() { + kind = "DIR " + } + out.WriteString(fmt.Sprintf("[%s] %s (%d bytes)\n", kind, f.Name(), f.Size())) + } + return out.String() +} + +func executeReadFile(rawArgs string, projectRoot string) string { + var args struct { + Path string `json:"path"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Path == "" { + return "Error: Invalid arguments. 'path' is required." + } + + safePath, err := securePath(args.Path, projectRoot) + if err != nil { + return err.Error() + } + + data, err := ioutil.ReadFile(safePath) + if err != nil { + return fmt.Sprintf("Error reading file: %v", err) + } + + // Truncate to prevent massive tokens + content := string(data) + if len(content) > 15000 { + content = content[:15000] + "\n\n... [FILE TRUNCATED FOR LENGTH LIMITS]" + } + + return content +} + +func executeWriteFile(rawArgs string, projectRoot string) string { + var args struct { + Path string `json:"path"` + Content string `json:"content"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Path == "" { + return "Error: Invalid arguments. 'path' and 'content' are required." + } + + safePath, err := securePath(args.Path, projectRoot) + if err != nil { + return err.Error() + } + + // Ensure parent dir exists + parent := filepath.Dir(safePath) + os.MkdirAll(parent, 0755) + + err = ioutil.WriteFile(safePath, []byte(args.Content), 0644) + if err != nil { + return fmt.Sprintf("Error writing file: %v", err) + } + + return fmt.Sprintf("Success: Wrote %d bytes to %s", len(args.Content), args.Path) +} + +func executeEditFile(rawArgs string, projectRoot string) string { + var args struct { + Path string `json:"path"` + Search string `json:"search"` + Replace string `json:"replace"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Path == "" || args.Search == "" { + return "Error: Invalid arguments. 'path', 'search', and 'replace' are required." + } + + safePath, err := securePath(args.Path, projectRoot) + if err != nil { + return err.Error() + } + + data, err := ioutil.ReadFile(safePath) + if err != nil { + return fmt.Sprintf("Error reading file: %v", err) + } + + content := string(data) + + occurrences := strings.Count(content, args.Search) + if occurrences == 0 { + return "Error: The exact search string was not found in the file." + } else if occurrences > 1 { + return "Error: The search string appears multiple times. Please provide a larger unique search block to ensure the correct code is replaced." + } + + newContent := strings.Replace(content, args.Search, args.Replace, 1) + + if err := os.WriteFile(safePath, []byte(newContent), 0644); err != nil { + return fmt.Sprintf("Error writing modified file: %v", err) + } + + return fmt.Sprintf("Success: Replaced block in %s.", safePath) +} + +func executeAddWorkspaceProject(rawArgs string, projectRoot string) string { + var args struct { + Name string `json:"name"` + Path string `json:"path"` + DeployCommand string `json:"deploy_command"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: Invalid arguments." + } + + if args.Name == "" || args.Path == "" { + return "Error: name and path are required." + } + + safePath, err := securePath(args.Path, projectRoot) + if err != nil { + return err.Error() + } + + // Make sure the directory exists natively + if _, err := os.Stat(safePath); os.IsNotExist(err) { + if err := os.MkdirAll(safePath, 0755); err != nil { + return fmt.Sprintf("Error creating project directory: %v", err) + } + } + + // Link to internal ManagedProjects settings DB so it appears in the File Explorer + var val string + err = db.DB.QueryRow("SELECT value FROM settings WHERE key = 'managed_projects'").Scan(&val) + if err != nil { + val = "[]" + } + + var projects []map[string]interface{} + json.Unmarshal([]byte(val), &projects) + + // Check if already mounted + for _, p := range projects { + if p["path"] == safePath { + return fmt.Sprintf("Project at %s is already managed under name '%s'.", safePath, p["name"]) + } + } + + deployCmd := args.DeployCommand + if deployCmd == "" { + deployCmd = "echo 'No deploy command configured'" + } + + projects = append(projects, map[string]interface{}{ + "name": args.Name, + "path": safePath, + "deploy_command": deployCmd, + }) + + bytes, _ := json.Marshal(projects) + db.DB.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES ('managed_projects', ?)", string(bytes)) + + return fmt.Sprintf("Success: Project '%s' mapped to workspace %s and is now visible in the File Explorer.", args.Name, safePath) +} + +func executeGrepSearch(rawArgs string, projectRoot string) string { + var args struct { + Pattern string `json:"pattern"` + Path string `json:"path"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Pattern == "" { + return "Error: Invalid arguments. 'pattern' is required." + } + + targetPath := args.Path + if targetPath == "" { + targetPath = projectRoot + if targetPath == "" { + targetPath = WorkspaceDir // Default to workspace root if empty + } + } + + safePath, err := securePath(targetPath, projectRoot) + if err != nil { + return err.Error() + } + + re, err := regexp.Compile(args.Pattern) + if err != nil { + return fmt.Sprintf("Error: Invalid regex pattern: %v", err) + } + + var results []string + matches := 0 + MAX_MATCHES := 100 + + filepath.Walk(safePath, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + + // Skip hidden files to save time + if strings.HasPrefix(filepath.Base(path), ".") { + return nil + } + + data, err := ioutil.ReadFile(path) + if err == nil { + lines := strings.Split(string(data), "\n") + for i, line := range lines { + if re.MatchString(line) { + relPath := strings.TrimPrefix(path, WorkspaceDir) + if !strings.HasPrefix(relPath, "/") { + relPath = "/" + relPath + } + results = append(results, fmt.Sprintf("%s:%d: %s", relPath, i+1, strings.TrimSpace(line))) + matches++ + if matches >= MAX_MATCHES { + return fmt.Errorf("max_matches") + } + } + } + } + return nil + }) + + if len(results) == 0 { + return "No matches found." + } + + out := strings.Join(results, "\n") + if matches >= MAX_MATCHES { + out += "\n\n... [ADDITIONAL MATCHES TRUNCATED]" + } + + return out +} + +// executeFindFiles searches for files by glob pattern within the workspace +func executeFindFiles(rawArgs string, projectRoot string) string { + var args struct { + Pattern string `json:"pattern"` + Path string `json:"path"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Pattern == "" { + return "Error: 'pattern' is required (e.g. '*.go', 'test_*.py')." + } + + searchPath := args.Path + if searchPath == "" { + searchPath = projectRoot + if searchPath == "" { + searchPath = WorkspaceDir + } + } + safePath, err := securePath(searchPath, projectRoot) + if err != nil { + return err.Error() + } + + cmd := exec.Command("find", safePath, "-name", args.Pattern, "-not", "-path", "*/.*", "-not", "-path", "*/node_modules/*", "-not", "-path", "*/vendor/*") + out, err := cmd.Output() + if err != nil { + return fmt.Sprintf("Error running find: %v", err) + } + result := strings.TrimSpace(string(out)) + if result == "" { + return fmt.Sprintf("No files matching '%s' found in %s", args.Pattern, searchPath) + } + // Strip the workspace prefix for readability + lines := strings.Split(result, "\n") + for i, l := range lines { + lines[i] = strings.TrimPrefix(l, WorkspaceDir+"/") + } + return strings.Join(lines, "\n") +} + +// executeRenameFile renames or moves a file/directory within the workspace +func executeRenameFile(rawArgs string, projectRoot string) string { + var args struct { + From string `json:"from"` + To string `json:"to"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.From == "" || args.To == "" { + return "Error: 'from' and 'to' paths are required." + } + + fromPath, err := securePath(args.From, projectRoot) + if err != nil { + return fmt.Sprintf("Error (from): %v", err) + } + toPath, err := securePath(args.To, projectRoot) + if err != nil { + return fmt.Sprintf("Error (to): %v", err) + } + + if _, err := os.Stat(fromPath); os.IsNotExist(err) { + return fmt.Sprintf("Error: Source path '%s' does not exist.", args.From) + } + + // Ensure destination parent dir exists + os.MkdirAll(filepath.Dir(toPath), 0755) + + if err := os.Rename(fromPath, toPath); err != nil { + return fmt.Sprintf("Error renaming: %v", err) + } + return fmt.Sprintf("Success: Renamed '%s' → '%s'", args.From, args.To) +} + +// executeCheckCode runs language-appropriate linting/type-checking on a file or directory +func executeCheckCode(rawArgs string, projectRoot string) string { + var args struct { + Path string `json:"path"` + Language string `json:"language"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Path == "" { + return "Error: 'path' is required." + } + + safePath, err := securePath(args.Path, projectRoot) + if err != nil { + return err.Error() + } + + lang := strings.ToLower(args.Language) + // Auto-detect from extension if not specified + if lang == "" { + ext := strings.ToLower(filepath.Ext(args.Path)) + switch ext { + case ".go": + lang = "go" + case ".ts", ".tsx": + lang = "typescript" + case ".js", ".jsx": + lang = "javascript" + case ".py": + lang = "python" + case ".rs": + lang = "rust" + } + } + + var cmd *exec.Cmd + switch lang { + case "go": + // Run go vet on the directory containing the file + dir := safePath + if info, _ := os.Stat(safePath); info != nil && !info.IsDir() { + dir = filepath.Dir(safePath) + } + cmd = exec.Command("go", "vet", "./...") + cmd.Dir = dir + case "typescript": + cmd = exec.Command("npx", "tsc", "--noEmit", "--pretty") + cmd.Dir = WorkspaceDir + case "javascript": + cmd = exec.Command("npx", "eslint", safePath, "--format", "compact") + cmd.Dir = WorkspaceDir + case "python": + cmd = exec.Command("python3", "-m", "flake8", safePath, "--max-line-length", "120") + case "rust": + cmd = exec.Command("cargo", "check", "--message-format=short") + cmd.Dir = WorkspaceDir + default: + return fmt.Sprintf("Error: Cannot automatically detect language for '%s'. Pass language='go'|'typescript'|'javascript'|'python'|'rust'.", args.Path) + } + + cmd.Env = os.Environ() + out, err := cmd.CombinedOutput() + result := strings.TrimSpace(string(out)) + if err == nil { + if result == "" { + return fmt.Sprintf("✓ No issues found in '%s'", args.Path) + } + return result + } + if result == "" { + return fmt.Sprintf("Check failed: %v", err) + } + return result +} diff --git a/dash/backend/tools/mcp.go b/dash/backend/tools/mcp.go new file mode 100644 index 0000000..003a609 --- /dev/null +++ b/dash/backend/tools/mcp.go @@ -0,0 +1,447 @@ +package tools + +import ( + "bufio" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os/exec" + "strings" + "sync" + "time" + + "github.com/danilrybalkin/apollo-dash/db" +) + +// MCPServerConfig defines a single MCP server connection +type MCPServerConfig struct { + Name string `json:"name"` + Command string `json:"command"` // e.g. "npx @modelcontextprotocol/server-postgres postgresql://..." + Transport string `json:"transport"` // "stdio" (default) or "http" + URL string `json:"url"` // for HTTP transport +} + +// MCPTool represents a tool discovered from an MCP server +type MCPTool struct { + Server string `json:"server"` + Name string `json:"name"` + Description string `json:"description"` + Schema map[string]interface{} `json:"schema"` +} + +var ( + mcpMu sync.RWMutex + mcpTools []MCPTool + mcpProcs = map[string]*mcpProcess{} +) + +type mcpProcess struct { + cfg MCPServerConfig + cmd *exec.Cmd + stdin io.WriteCloser + stdout *bufio.Scanner + mu sync.Mutex + status string // "running", "crashed", "starting", "retrying" + lastError string + stopped bool +} + +// mcpRPCRequest sends a JSON-RPC message to the MCP server process (stdio or http) +func (p *mcpProcess) call(method string, params interface{}) (map[string]interface{}, error) { + if p.cfg.Transport == "http" { + return p.callHttp(method, params) + } + return p.callStdio(method, params) +} + +func (p *mcpProcess) callHttp(method string, params interface{}) (map[string]interface{}, error) { + reqID := time.Now().UnixNano() + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": reqID, + "method": method, + "params": params, + } + b, _ := json.Marshal(payload) + + req, _ := http.NewRequest("POST", p.cfg.URL, strings.NewReader(string(b))) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var r map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + return nil, err + } + if errObj, ok := r["error"]; ok { + return nil, fmt.Errorf("MCP HTTP error: %v", errObj) + } + if result, ok := r["result"].(map[string]interface{}); ok { + return result, nil + } + return nil, fmt.Errorf("invalid MCP HTTP response") +} + +func (p *mcpProcess) callStdio(method string, params interface{}) (map[string]interface{}, error) { + p.mu.Lock() + defer p.mu.Unlock() + + reqID := time.Now().UnixNano() + req := map[string]interface{}{ + "jsonrpc": "2.0", + "id": reqID, + "method": method, + "params": params, + } + b, _ := json.Marshal(req) + _, err := fmt.Fprintf(p.stdin, "%s\n", string(b)) + if err != nil { + return nil, fmt.Errorf("write to MCP process failed: %w", err) + } + + // Read next line as response (MCP uses newline-delimited JSON-RPC) + if p.stdout.Scan() { + var resp map[string]interface{} + if err := json.Unmarshal([]byte(p.stdout.Text()), &resp); err != nil { + return nil, fmt.Errorf("invalid MCP response: %w", err) + } + if errObj, ok := resp["error"]; ok { + return nil, fmt.Errorf("MCP error: %v", errObj) + } + if result, ok := resp["result"].(map[string]interface{}); ok { + return result, nil + } + } + return nil, fmt.Errorf("no response from MCP server") +} + +// StartMCPServer launches an MCP server process and discovers its tools +func StartMCPServer(cfg MCPServerConfig) { + if cfg.Transport == "http" { + startHttpMCPServer(cfg) + return + } + startStdioMCPServer(cfg) +} + +func startHttpMCPServer(cfg MCPServerConfig) { + proc := &mcpProcess{ + cfg: cfg, + status: "running", + } + mcpMu.Lock() + mcpProcs[cfg.Name] = proc + mcpMu.Unlock() + + // Initialize + _, err := proc.call("initialize", map[string]interface{}{ + "protocolVersion": "2024-11-05", + "clientInfo": map[string]string{"name": "AgentHQ", "version": "1.0"}, + }) + if err != nil { + proc.status = "crashed" + proc.lastError = err.Error() + return + } + discoverTools(cfg.Name, proc) +} + +func startStdioMCPServer(cfg MCPServerConfig) { + go func() { + backoff := 1 * time.Second + for { + mcpMu.RLock() + p, ok := mcpProcs[cfg.Name] + mcpMu.RUnlock() + if ok && p.stopped { + return + } + + log.Printf("MCP: Starting stdio server '%s'...", cfg.Name) + proc, err := launchStdioProcess(cfg) + if err != nil { + log.Printf("MCP: Failed to launch '%s': %v", cfg.Name, err) + mcpMu.Lock() + mcpProcs[cfg.Name] = &mcpProcess{cfg: cfg, status: "crashed", lastError: err.Error()} + mcpMu.Unlock() + time.Sleep(backoff) + if backoff < 60*time.Second { + backoff *= 2 + } + continue + } + + mcpMu.Lock() + mcpProcs[cfg.Name] = proc + mcpMu.Unlock() + + // Initialize + _, err = proc.call("initialize", map[string]interface{}{ + "protocolVersion": "2024-11-05", + "clientInfo": map[string]string{"name": "AgentHQ", "version": "1.0"}, + }) + if err != nil { + log.Printf("MCP: Initialize failed for '%s': %v", cfg.Name, err) + proc.status = "crashed" + proc.lastError = err.Error() + proc.cmd.Process.Kill() + time.Sleep(backoff) + continue + } + + discoverTools(cfg.Name, proc) + backoff = 1 * time.Second // Reset backoff on success + + // Watchdog: wait for process to exit + err = proc.cmd.Wait() + log.Printf("MCP: Server '%s' exited: %v", cfg.Name, err) + + mcpMu.RLock() + isStopped := mcpProcs[cfg.Name].stopped + mcpMu.RUnlock() + if isStopped { + return + } + + mcpMu.Lock() + proc.status = "retrying" + if err != nil { + proc.lastError = err.Error() + } + mcpMu.Unlock() + + time.Sleep(backoff) + if backoff < 60*time.Second { + backoff *= 2 + } + } + }() +} + +func launchStdioProcess(cfg MCPServerConfig) (*mcpProcess, error) { + parts := strings.Fields(cfg.Command) + if len(parts) == 0 { + return nil, fmt.Errorf("empty command") + } + cmd := exec.Command(parts[0], parts[1:]...) + stdin, err := cmd.StdinPipe() + if err != nil { + return nil, err + } + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return nil, err + } + + if err := cmd.Start(); err != nil { + return nil, err + } + + return &mcpProcess{ + cfg: cfg, + cmd: cmd, + stdin: stdin, + stdout: bufio.NewScanner(stdoutPipe), + status: "running", + }, nil +} + +func discoverTools(serverName string, proc *mcpProcess) { + toolsResult, err := proc.call("tools/list", map[string]interface{}{}) + if err != nil { + log.Printf("MCP: tools/list failed for '%s': %v", serverName, err) + return + } + + toolsList, _ := toolsResult["tools"].([]interface{}) + mcpMu.Lock() + // Clear old tools for this server + var filtered []MCPTool + for _, t := range mcpTools { + if t.Server != serverName { + filtered = append(filtered, t) + } + } + mcpTools = filtered + + for _, t := range toolsList { + toolMap, ok := t.(map[string]interface{}) + if !ok { + continue + } + name, _ := toolMap["name"].(string) + desc, _ := toolMap["description"].(string) + schema, _ := toolMap["inputSchema"].(map[string]interface{}) + mcpTools = append(mcpTools, MCPTool{ + Server: serverName, + Name: fmt.Sprintf("mcp_%s_%s", serverName, name), + Description: fmt.Sprintf("[MCP:%s] %s", serverName, desc), + Schema: schema, + }) + } + mcpMu.Unlock() +} + +// GetMCPTools returns discovered MCP tools (used by tools/registry.go via MCP integration) +func GetMCPTools() []MCPTool { + mcpMu.RLock() + defer mcpMu.RUnlock() + result := make([]MCPTool, len(mcpTools)) + copy(result, mcpTools) + return result +} + +// CallMCPTool executes an MCP tool by its registry name. +func CallMCPTool(toolName, rawArgs string) string { + // toolName format: mcp_{server}_{toolName} + parts := strings.SplitN(strings.TrimPrefix(toolName, "mcp_"), "_", 2) + if len(parts) != 2 { + return fmt.Sprintf("Error: invalid MCP tool name '%s'", toolName) + } + serverName, toolName := parts[0], parts[1] + + mcpMu.RLock() + proc, ok := mcpProcs[serverName] + mcpMu.RUnlock() + if !ok { + return fmt.Sprintf("Error: MCP server '%s' is not running.", serverName) + } + + var argsMap map[string]interface{} + json.Unmarshal([]byte(rawArgs), &argsMap) + + result, err := proc.call("tools/call", map[string]interface{}{ + "name": toolName, + "arguments": argsMap, + }) + if err != nil { + return fmt.Sprintf("MCP tool error: %v", err) + } + + // Extract text content from MCP response + if content, ok := result["content"].([]interface{}); ok { + var parts []string + for _, c := range content { + if m, ok := c.(map[string]interface{}); ok { + if text, ok := m["text"].(string); ok { + parts = append(parts, text) + } + } + } + return strings.Join(parts, "\n") + } + b, _ := json.Marshal(result) + return string(b) +} + +// MCPHandler: GET /api/mcp → list MCP tools, POST → configure new servers +func MCPHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + switch r.Method { + case http.MethodGet: + mcpMu.RLock() + tools := make([]MCPTool, len(mcpTools)) + copy(tools, mcpTools) + + serverSummaries := []map[string]interface{}{} + configs := getMCPServerConfigs() + for _, cfg := range configs { + summary := map[string]interface{}{ + "name": cfg.Name, + "transport": cfg.Transport, + "command": cfg.Command, + "url": cfg.URL, + "status": "offline", + "lastError": "", + } + if p, ok := mcpProcs[cfg.Name]; ok { + summary["status"] = p.status + summary["lastError"] = p.lastError + } + serverSummaries = append(serverSummaries, summary) + } + mcpMu.RUnlock() + + json.NewEncoder(w).Encode(map[string]interface{}{ + "tools": tools, + "servers": serverSummaries, + }) + + case http.MethodPost: + var cfg MCPServerConfig + if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil { + http.Error(w, "Invalid JSON", http.StatusBadRequest) + return + } + // Persist to DB + b, _ := json.Marshal(cfg) + db.DB.Exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", + fmt.Sprintf("mcp_server_%s", cfg.Name), string(b)) + // Start the server process + go StartMCPServer(cfg) + json.NewEncoder(w).Encode(map[string]string{"status": "starting", "name": cfg.Name}) + + case http.MethodDelete: + name := r.URL.Query().Get("name") + if name == "" { + http.Error(w, "name required", http.StatusBadRequest) + return + } + db.DB.Exec("DELETE FROM settings WHERE key = ?", fmt.Sprintf("mcp_server_%s", name)) + mcpMu.Lock() + if proc, ok := mcpProcs[name]; ok { + proc.stopped = true + if proc.cmd != nil && proc.cmd.Process != nil { + proc.cmd.Process.Kill() + } + delete(mcpProcs, name) + } + // Remove tools from this server + var filtered []MCPTool + for _, t := range mcpTools { + if t.Server != name { + filtered = append(filtered, t) + } + } + mcpTools = filtered + mcpMu.Unlock() + json.NewEncoder(w).Encode(map[string]string{"status": "removed"}) + + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func getMCPServerConfigs() []MCPServerConfig { + rows, err := db.DB.Query("SELECT value FROM settings WHERE key LIKE 'mcp_server_%'") + if err != nil { + return nil + } + defer rows.Close() + var cfgs []MCPServerConfig + for rows.Next() { + var val string + if err := rows.Scan(&val); err == nil { + var cfg MCPServerConfig + if json.Unmarshal([]byte(val), &cfg) == nil { + cfgs = append(cfgs, cfg) + } + } + } + return cfgs +} + +// InitMCPServers loads and starts all persisted MCP servers at startup +func InitMCPServers() { + cfgs := getMCPServerConfigs() + for _, cfg := range cfgs { + go StartMCPServer(cfg) + } +} diff --git a/dash/backend/tools/registry.go b/dash/backend/tools/registry.go new file mode 100644 index 0000000..884e0f6 --- /dev/null +++ b/dash/backend/tools/registry.go @@ -0,0 +1,511 @@ +package tools + +import ( + "encoding/json" + "fmt" + "log" + "os/exec" + "strings" +) + +type ToolDefinition struct { + Type string `json:"type"` + Function struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters struct { + Type string `json:"type"` + Properties map[string]interface{} `json:"properties"` + Required []string `json:"required,omitempty"` + } `json:"parameters"` + } `json:"function"` +} + +// Generate the array of JSON schemas attached to the stream payload +func GetAvailableTools() []ToolDefinition { + var registry []ToolDefinition + + // ====================================== + // 1. Sandboxed FS Tools + // ====================================== + fsList := ToolDefinition{Type: "function"} + fsList.Function.Name = "list_files" + fsList.Function.Description = "Lists files and directories in the AI Sandbox." + fsList.Function.Parameters.Type = "object" + fsList.Function.Parameters.Properties = map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Relative or absolute path. If empty, lists the Sandbox root.", + }, + } + registry = append(registry, fsList) + + fsRead := ToolDefinition{Type: "function"} + fsRead.Function.Name = "read_file" + fsRead.Function.Description = "Reads the textual contents of a file inside the AI Sandbox." + fsRead.Function.Parameters.Type = "object" + fsRead.Function.Parameters.Properties = map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Target file path.", + }, + } + fsRead.Function.Parameters.Required = []string{"path"} + registry = append(registry, fsRead) + + fsWrite := ToolDefinition{Type: "function"} + fsWrite.Function.Name = "write_file" + fsWrite.Function.Description = "Writes or overwrites a text file inside the AI Sandbox." + fsWrite.Function.Parameters.Type = "object" + fsWrite.Function.Parameters.Properties = map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Target file path.", + }, + "content": map[string]interface{}{ + "type": "string", + "description": "Full file content to write.", + }, + } + fsWrite.Function.Parameters.Required = []string{"path", "content"} + registry = append(registry, fsWrite) + + fsEdit := ToolDefinition{Type: "function"} + fsEdit.Function.Name = "edit_file" + fsEdit.Function.Description = "Modifies an existing file by replacing a unique search block with new content inside the Sandbox." + fsEdit.Function.Parameters.Type = "object" + fsEdit.Function.Parameters.Properties = map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Target file path.", + }, + "search": map[string]interface{}{ + "type": "string", + "description": "The exact block of code or text to find. It must be completely unique within the file.", + }, + "replace": map[string]interface{}{ + "type": "string", + "description": "The new content to replace the search block with.", + }, + } + fsEdit.Function.Parameters.Required = []string{"path", "search", "replace"} + registry = append(registry, fsEdit) + + fsGrep := ToolDefinition{Type: "function"} + fsGrep.Function.Name = "grep_search" + fsGrep.Function.Description = "Recursively searches all files in the Sandbox for a specific Regex pattern." + fsGrep.Function.Parameters.Type = "object" + fsGrep.Function.Parameters.Properties = map[string]interface{}{ + "pattern": map[string]interface{}{ + "type": "string", + "description": "Regex or string pattern to search for.", + }, + "path": map[string]interface{}{ + "type": "string", + "description": "Optional subdirectory to restrict search. Defaults to Sandbox root.", + }, + } + fsGrep.Function.Parameters.Required = []string{"pattern"} + registry = append(registry, fsGrep) + + addProject := ToolDefinition{Type: "function"} + addProject.Function.Name = "add_workspace_project" + addProject.Function.Description = "Creates a new project directory and permanently mounts it into the UI Workspace File Explorer." + addProject.Function.Parameters.Type = "object" + addProject.Function.Parameters.Properties = map[string]interface{}{ + "name": map[string]interface{}{ + "type": "string", + "description": "The user-facing display name of the project.", + }, + "path": map[string]interface{}{ + "type": "string", + "description": "The folder path to create and mount.", + }, + "deploy_command": map[string]interface{}{ + "type": "string", + "description": "Optional terminal command to run when 'Deploy' is clicked.", + }, + } + addProject.Function.Parameters.Required = []string{"name", "path"} + registry = append(registry, addProject) + + // ====================================== + // User Interaction Toolkit + // ====================================== + askQ := ToolDefinition{Type: "function"} + askQ.Function.Name = "ask_user_question" + askQ.Function.Description = "Pauses AI execution to ask the user a specific question. Use this to gather architectural preferences, clarify requirements, or request a decision before drafting plans or writing code." + askQ.Function.Parameters.Type = "object" + askQ.Function.Parameters.Properties = map[string]interface{}{ + "question": map[string]interface{}{ + "type": "string", + "description": "The precise question to ask the user.", + }, + "options": map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{"type": "string"}, + "description": "An array of 2 to 5 distinct options for the user to choose from (e.g. ['React', 'Vue', 'Vanilla JS']).", + }, + } + askQ.Function.Parameters.Required = []string{"question", "options"} + registry = append(registry, askQ) + + // ====================================== + // 3. Sandboxed Terminal Execution + // ====================================== + termExec := ToolDefinition{Type: "function"} + termExec.Function.Name = "execute_command" + termExec.Function.Description = "Executes a raw bash terminal command strictly jailed within the AI Sandbox directory." + termExec.Function.Parameters.Type = "object" + termExec.Function.Parameters.Properties = map[string]interface{}{ + "command": map[string]interface{}{ + "type": "string", + "description": "The exact shell command string (e.g. 'npm install react'). Avoid interactive commands like 'nano' or 'vim'.", + }, + } + termExec.Function.Parameters.Required = []string{"command"} + registry = append(registry, termExec) + + // ====================================== + // 4. Web Toolkit + // ====================================== + webScrape := ToolDefinition{Type: "function"} + webScrape.Function.Name = "web_scrape" + webScrape.Function.Description = "Downloads a specific webpage and extracts all plaintext paragraphs, stripping HTML junk." + webScrape.Function.Parameters.Type = "object" + webScrape.Function.Parameters.Properties = map[string]interface{}{ + "url": map[string]interface{}{ + "type": "string", + "description": "The exact 'https://' URL to scrape.", + }, + } + webScrape.Function.Parameters.Required = []string{"url"} + registry = append(registry, webScrape) + + // ====================================== + // 5. Version Control / Checkpointing + // ====================================== + undoOp := ToolDefinition{Type: "function"} + undoOp.Function.Name = "undo_checkpoint" + undoOp.Function.Description = "Physically reverts the entire Sandbox workspace back to its state prior to the last file modification or terminal command execution." + undoOp.Function.Parameters.Type = "object" + undoOp.Function.Parameters.Properties = map[string]interface{}{} + registry = append(registry, undoOp) + + // ====================================== + // 5b. Extended File Operations + // ====================================== + findFiles := ToolDefinition{Type: "function"} + findFiles.Function.Name = "find_files" + findFiles.Function.Description = "Finds files by name glob pattern within the workspace (e.g. '*.go', 'test_*', 'README.md'). Skips hidden dirs, node_modules, and vendor." + findFiles.Function.Parameters.Type = "object" + findFiles.Function.Parameters.Properties = map[string]interface{}{ + "pattern": map[string]interface{}{"type": "string", "description": "Glob pattern for file names, e.g. '*.go', 'test_*'."}, + "path": map[string]interface{}{"type": "string", "description": "Optional subdirectory to search within."}, + } + findFiles.Function.Parameters.Required = []string{"pattern"} + registry = append(registry, findFiles) + + renameFile := ToolDefinition{Type: "function"} + renameFile.Function.Name = "rename_file" + renameFile.Function.Description = "Renames or moves a file or directory within the workspace." + renameFile.Function.Parameters.Type = "object" + renameFile.Function.Parameters.Properties = map[string]interface{}{ + "from": map[string]interface{}{"type": "string", "description": "Source path (relative to workspace)."}, + "to": map[string]interface{}{"type": "string", "description": "Destination path (relative to workspace)."}, + } + renameFile.Function.Parameters.Required = []string{"from", "to"} + registry = append(registry, renameFile) + + checkCode := ToolDefinition{Type: "function"} + checkCode.Function.Name = "check_code" + checkCode.Function.Description = "Runs language-appropriate static analysis on a file or directory: go vet (Go), tsc --noEmit (TypeScript), eslint (JavaScript), flake8 (Python), cargo check (Rust). Auto-detects language from file extension." + checkCode.Function.Parameters.Type = "object" + checkCode.Function.Parameters.Properties = map[string]interface{}{ + "path": map[string]interface{}{"type": "string", "description": "File or directory path to analyze."}, + "language": map[string]interface{}{"type": "string", "description": "Optional: 'go'|'typescript'|'javascript'|'python'|'rust'. Auto-detected if omitted."}, + } + checkCode.Function.Parameters.Required = []string{"path"} + registry = append(registry, checkCode) + + contextTree := ToolDefinition{Type: "function"} + contextTree.Function.Name = "get_context_tree" + contextTree.Function.Description = "Native structural context tree of the current project. Returns folders/files, file headers, and optional symbols with line ranges." + contextTree.Function.Parameters.Type = "object" + contextTree.Function.Parameters.Properties = map[string]interface{}{ + "target_path": map[string]interface{}{"type": "string", "description": "Optional subpath to analyze (relative to project root)."}, + "depth_limit": map[string]interface{}{"type": "number", "description": "Optional directory depth limit."}, + "include_symbols": map[string]interface{}{"type": "boolean", "description": "Include symbol-level details (default true)."}, + "max_tokens": map[string]interface{}{"type": "number", "description": "Approximate output token cap. Tool auto-prunes if exceeded."}, + } + registry = append(registry, contextTree) + + fileSkeleton := ToolDefinition{Type: "function"} + fileSkeleton.Function.Name = "get_file_skeleton" + fileSkeleton.Function.Description = "Native file skeleton view. Returns signatures for functions/classes/types and line ranges without dumping full file bodies." + fileSkeleton.Function.Parameters.Type = "object" + fileSkeleton.Function.Parameters.Properties = map[string]interface{}{ + "file_path": map[string]interface{}{"type": "string", "description": "Target file path (relative to project root)."}, + } + fileSkeleton.Function.Parameters.Required = []string{"file_path"} + registry = append(registry, fileSkeleton) + + semanticSearch := ToolDefinition{Type: "function"} + semanticSearch.Function.Name = "semantic_code_search" + semanticSearch.Function.Description = "Native semantic code search with hybrid embedding + keyword ranking over project files." + semanticSearch.Function.Parameters.Type = "object" + semanticSearch.Function.Parameters.Properties = map[string]interface{}{ + "query": map[string]interface{}{"type": "string", "description": "Natural language search query."}, + "top_k": map[string]interface{}{"type": "number", "description": "Max number of matches to return."}, + "semantic_weight": map[string]interface{}{"type": "number", "description": "Weight for embedding similarity."}, + "keyword_weight": map[string]interface{}{"type": "number", "description": "Weight for keyword overlap score."}, + "min_semantic_score": map[string]interface{}{"type": "number", "description": "Minimum semantic score threshold (0-1 or 0-100)."}, + "min_keyword_score": map[string]interface{}{"type": "number", "description": "Minimum keyword score threshold (0-1 or 0-100)."}, + "min_combined_score": map[string]interface{}{"type": "number", "description": "Minimum combined score threshold (0-1 or 0-100)."}, + "require_keyword_match": map[string]interface{}{"type": "boolean", "description": "When true, discard results with no keyword overlap."}, + "require_semantic_match": map[string]interface{}{"type": "boolean", "description": "When true, discard results with no semantic match."}, + } + semanticSearch.Function.Parameters.Required = []string{"query"} + registry = append(registry, semanticSearch) + + semanticIdentifiers := ToolDefinition{Type: "function"} + semanticIdentifiers.Function.Name = "semantic_identifier_search" + semanticIdentifiers.Function.Description = "Native identifier-level semantic search for functions/classes/variables with ranked call sites." + semanticIdentifiers.Function.Parameters.Type = "object" + semanticIdentifiers.Function.Parameters.Properties = map[string]interface{}{ + "query": map[string]interface{}{"type": "string", "description": "Natural language query for identifier intent."}, + "top_k": map[string]interface{}{"type": "number", "description": "Max identifiers to return."}, + "top_calls_per_identifier": map[string]interface{}{"type": "number", "description": "Max call sites per identifier."}, + "include_kinds": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}, "description": "Optional kind filter list (e.g. function,class,variable)."}, + "semantic_weight": map[string]interface{}{"type": "number", "description": "Weight for embedding similarity."}, + "keyword_weight": map[string]interface{}{"type": "number", "description": "Weight for keyword score."}, + } + semanticIdentifiers.Function.Parameters.Required = []string{"query"} + registry = append(registry, semanticIdentifiers) + + blastRadius := ToolDefinition{Type: "function"} + blastRadius.Function.Name = "get_blast_radius" + blastRadius.Function.Description = "Native blast-radius analysis. Finds references/import-usage lines for a symbol across the project." + blastRadius.Function.Parameters.Type = "object" + blastRadius.Function.Parameters.Properties = map[string]interface{}{ + "symbol_name": map[string]interface{}{"type": "string", "description": "Symbol name to trace."}, + "file_context": map[string]interface{}{"type": "string", "description": "Optional defining file path to avoid counting definition line."}, + } + blastRadius.Function.Parameters.Required = []string{"symbol_name"} + registry = append(registry, blastRadius) + + staticAnalysis := ToolDefinition{Type: "function"} + staticAnalysis.Function.Name = "run_static_analysis" + staticAnalysis.Function.Description = "Native multi-language static analysis runner (go vet, tsc/eslint, py_compile, cargo check where applicable)." + staticAnalysis.Function.Parameters.Type = "object" + staticAnalysis.Function.Parameters.Properties = map[string]interface{}{ + "target_path": map[string]interface{}{"type": "string", "description": "Optional file/directory to scope analysis."}, + } + registry = append(registry, staticAnalysis) + + semanticNavigate := ToolDefinition{Type: "function"} + semanticNavigate.Function.Name = "semantic_navigate" + semanticNavigate.Function.Description = "Native semantic navigator that clusters project files into topic groups for high-level exploration." + semanticNavigate.Function.Parameters.Type = "object" + semanticNavigate.Function.Parameters.Properties = map[string]interface{}{ + "max_depth": map[string]interface{}{"type": "number", "description": "Optional cluster depth hint."}, + "max_clusters": map[string]interface{}{"type": "number", "description": "Maximum clusters to return."}, + } + registry = append(registry, semanticNavigate) + + featureHub := ToolDefinition{Type: "function"} + featureHub.Function.Name = "get_feature_hub" + featureHub.Function.Description = "Native feature-hub graph over markdown wikilinks. List hubs, inspect a hub, or detect orphaned code files." + featureHub.Function.Parameters.Type = "object" + featureHub.Function.Parameters.Properties = map[string]interface{}{ + "hub_path": map[string]interface{}{"type": "string", "description": "Optional explicit hub markdown path."}, + "feature_name": map[string]interface{}{"type": "string", "description": "Optional feature name to resolve to a hub."}, + "show_orphans": map[string]interface{}{"type": "boolean", "description": "If true, list source files not linked from hubs."}, + } + registry = append(registry, featureHub) + + proposeCommit := ToolDefinition{Type: "function"} + proposeCommit.Function.Name = "propose_commit" + proposeCommit.Function.Description = "Native guarded write operation. Creates a restore point, writes file content, and returns validation warnings." + proposeCommit.Function.Parameters.Type = "object" + proposeCommit.Function.Parameters.Properties = map[string]interface{}{ + "file_path": map[string]interface{}{"type": "string", "description": "File path to write (relative to project root)."}, + "new_content": map[string]interface{}{"type": "string", "description": "Full new file content."}, + } + proposeCommit.Function.Parameters.Required = []string{"file_path", "new_content"} + registry = append(registry, proposeCommit) + + listRestorePoints := ToolDefinition{Type: "function"} + listRestorePoints.Function.Name = "list_restore_points" + listRestorePoints.Function.Description = "List native restore points created before propose_commit writes." + listRestorePoints.Function.Parameters.Type = "object" + listRestorePoints.Function.Parameters.Properties = map[string]interface{}{} + registry = append(registry, listRestorePoints) + + undoChange := ToolDefinition{Type: "function"} + undoChange.Function.Name = "undo_change" + undoChange.Function.Description = "Restore files to the state captured by a native restore point ID." + undoChange.Function.Parameters.Type = "object" + undoChange.Function.Parameters.Properties = map[string]interface{}{ + "point_id": map[string]interface{}{"type": "string", "description": "Restore point ID from list_restore_points."}, + } + undoChange.Function.Parameters.Required = []string{"point_id"} + registry = append(registry, undoChange) + + webSearch := ToolDefinition{Type: "function"} + webSearch.Function.Name = "web_search" + webSearch.Function.Description = "Searches the internet using DuckDuckGo. Use to find documentation, look up error messages, research libraries, or find solutions to problems." + webSearch.Function.Parameters.Type = "object" + webSearch.Function.Parameters.Properties = map[string]interface{}{ + "query": map[string]interface{}{"type": "string", "description": "The search query string."}, + } + webSearch.Function.Parameters.Required = []string{"query"} + registry = append(registry, webSearch) + + // ====================================== + // 5c. Skills System + // ====================================== + loadSkill := ToolDefinition{Type: "function"} + loadSkill.Function.Name = "load_skill" + loadSkill.Function.Description = "Loads the full content of a named skill file. Skills provide deep specialized knowledge (debugging patterns, architectural recipes, language idioms) loaded on demand." + loadSkill.Function.Parameters.Type = "object" + loadSkill.Function.Parameters.Properties = map[string]interface{}{ + "name": map[string]interface{}{"type": "string", "description": "The skill name (e.g. 'go_debugging', 'react_patterns', 'git_workflow')."}, + } + loadSkill.Function.Parameters.Required = []string{"name"} + registry = append(registry, loadSkill) + + // ====================================== + // 6. Background Subagents + // ====================================== + spawnAgent := ToolDefinition{Type: "function"} + spawnAgent.Function.Name = "spawn_subagent" + spawnAgent.Function.Description = "Spawns a background AI subagent to handle a long-running task independently (research, code review, testing, etc.) without blocking the main conversation. Returns a subagent ID." + spawnAgent.Function.Parameters.Type = "object" + spawnAgent.Function.Parameters.Properties = map[string]interface{}{ + "name": map[string]interface{}{"type": "string", "description": "A short, descriptive name for this subagent (e.g., 'Researcher-1', 'CodeReviewer')."}, + "task": map[string]interface{}{"type": "string", "description": "A precise, self-contained task description for the subagent."}, + "session_id": map[string]interface{}{"type": "string", "description": "The current session ID so the subagent can notify the parent conversation when done."}, + } + spawnAgent.Function.Parameters.Required = []string{"name", "task"} + registry = append(registry, spawnAgent) + + getAgentStatus := ToolDefinition{Type: "function"} + getAgentStatus.Function.Name = "get_subagent_status" + getAgentStatus.Function.Description = "Checks the status and output of a spawned background subagent by its ID." + getAgentStatus.Function.Parameters.Type = "object" + getAgentStatus.Function.Parameters.Properties = map[string]interface{}{ + "id": map[string]interface{}{"type": "string", "description": "The subagent ID returned by spawn_subagent."}, + } + getAgentStatus.Function.Parameters.Required = []string{"id"} + registry = append(registry, getAgentStatus) + + // ====================================== + // 8. Dynamic MCP Tools + // ====================================== + mcpTools := GetMCPTools() + for _, mt := range mcpTools { + mTool := ToolDefinition{Type: "function"} + mTool.Function.Name = mt.Name + mTool.Function.Description = mt.Description + mTool.Function.Parameters.Type = "object" + mTool.Function.Parameters.Properties = map[string]interface{}{} + if propsRaw, ok := mt.Schema["properties"]; ok { + if props, ok := propsRaw.(map[string]interface{}); ok { + mTool.Function.Parameters.Properties = props + } + } + if req, ok := mt.Schema["required"].([]interface{}); ok { + for _, r := range req { + if rs, ok := r.(string); ok { + mTool.Function.Parameters.Required = append(mTool.Function.Parameters.Required, rs) + } + } + } + registry = append(registry, mTool) + } + + return registry +} + +func executeUndoCheckpoint(rawArgs string) string { + cmd := exec.Command("git", "reset", "--hard", "HEAD~1") + cmd.Dir = WorkspaceDir + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Sprintf("Error reverting checkpoint: %v\nOutput: %s", err, string(out)) + } + return fmt.Sprintf("Success: Sandbox reverted to previous checkpoint.\n%s", string(out)) +} + +// Routes tool call arguments to the strict Go function implementations +func ExecuteTool(name string, rawArgs string, projectRoot string) string { + var args map[string]interface{} + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return fmt.Sprintf("Error: Invalid JSON arguments passed to %s", name) + } + + log.Printf("Tool Dispatcher: Executing '%s'", name) + + switch name { + case "list_files": + return executeListFiles(rawArgs, projectRoot) + case "read_file": + return executeReadFile(rawArgs, projectRoot) + case "write_file": + return executeWriteFile(rawArgs, projectRoot) + case "edit_file": + return executeEditFile(rawArgs, projectRoot) + case "add_workspace_project": + return executeAddWorkspaceProject(rawArgs, projectRoot) + case "grep_search": + return executeGrepSearch(rawArgs, projectRoot) + case "execute_command": + return executeCommand(rawArgs, projectRoot) + case "undo_checkpoint": + return executeUndoCheckpoint(rawArgs) + case "web_scrape": + return executeWebScrape(rawArgs) + case "web_search": + return executeWebSearch(rawArgs) + case "find_files": + return executeFindFiles(rawArgs, projectRoot) + case "rename_file": + return executeRenameFile(rawArgs, projectRoot) + case "check_code": + return executeCheckCode(rawArgs, projectRoot) + case "get_context_tree": + return executeGetContextTree(rawArgs, projectRoot) + case "get_file_skeleton": + return executeGetFileSkeleton(rawArgs, projectRoot) + case "semantic_code_search": + return executeSemanticCodeSearch(rawArgs, projectRoot) + case "semantic_identifier_search": + return executeSemanticIdentifierSearch(rawArgs, projectRoot) + case "get_blast_radius": + return executeGetBlastRadius(rawArgs, projectRoot) + case "run_static_analysis": + return executeRunStaticAnalysisNative(rawArgs, projectRoot) + case "semantic_navigate": + return executeSemanticNavigate(rawArgs, projectRoot) + case "get_feature_hub": + return executeGetFeatureHub(rawArgs, projectRoot) + case "propose_commit": + return executeProposeCommitNative(rawArgs, projectRoot) + case "list_restore_points": + return executeListRestorePointsNative(rawArgs, projectRoot) + case "undo_change": + return executeUndoChangeNative(rawArgs, projectRoot) + case "load_skill": + return executeLoadSkill(rawArgs) + case "spawn_subagent": + return executeSpawnSubagent(rawArgs) + case "get_subagent_status": + return executeGetSubagentStatus(rawArgs) + default: + if strings.HasPrefix(name, "mcp_") { + return CallMCPTool(name, rawArgs) + } + return fmt.Sprintf("Error: Tool '%s' is not registered.", name) + } +} diff --git a/dash/backend/tools/skills.go b/dash/backend/tools/skills.go new file mode 100644 index 0000000..bf64bb9 --- /dev/null +++ b/dash/backend/tools/skills.go @@ -0,0 +1,169 @@ +package tools + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +var SkillsDir = filepath.Join(WorkspaceDir, ".skills") + +func resolveBundledSkillsDir() string { + candidates := []string{ + "skills", // run from backend/ + "backend/skills", // run from repo root + "dash/backend/skills", // run from workspace root containing dash/ + } + + if exePath, err := os.Executable(); err == nil && exePath != "" { + exeDir := filepath.Dir(exePath) + candidates = append(candidates, + filepath.Join(exeDir, "skills"), + filepath.Join(exeDir, "..", "skills"), + filepath.Join(exeDir, "..", "backend", "skills"), + ) + } + + for _, d := range candidates { + if info, err := os.Stat(d); err == nil && info.IsDir() { + return d + } + } + return "" +} + +func copySkillFile(srcPath string, dstPath string) error { + src, err := os.Open(srcPath) + if err != nil { + return err + } + defer src.Close() + + dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer dst.Close() + + _, err = io.Copy(dst, src) + return err +} + +// SyncBundledSkills ensures bundled skills are present in the runtime workspace library. +// Existing runtime skill files are preserved and not overwritten. +func SyncBundledSkills() { + _ = os.MkdirAll(SkillsDir, 0755) + + srcDir := resolveBundledSkillsDir() + if srcDir == "" { + return + } + if filepath.Clean(srcDir) == filepath.Clean(SkillsDir) { + return + } + + entries, err := os.ReadDir(srcDir) + if err != nil { + return + } + + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(strings.ToLower(e.Name()), ".md") { + continue + } + dstPath := filepath.Join(SkillsDir, e.Name()) + if info, err := os.Stat(dstPath); err == nil && !info.IsDir() { + continue // keep user-updated runtime copy + } + _ = copySkillFile(filepath.Join(srcDir, e.Name()), dstPath) + } +} + +// resolveSkillsDir returns the first skills directory that actually exists. +// Priority: runtime workspace dir (auto-seeded from bundled skills) → local fallback. +func resolveSkillsDir() string { + SyncBundledSkills() + + candidates := []string{ + SkillsDir, + "skills", // relative to cwd — used when binary runs from backend/ + } + for _, d := range candidates { + if info, err := os.Stat(d); err == nil && info.IsDir() { + return d + } + } + return SkillsDir // fallback — will return empty manifest +} + +// GetSkillsManifest returns a compact list of skill names and descriptions for the system prompt +func GetSkillsManifest() string { + dir := resolveSkillsDir() + entries, err := os.ReadDir(dir) + if err != nil { + return "" // Skills dir doesn't exist yet — silently skip + } + var lines []string + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { + continue + } + data, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + continue + } + // Extract description from frontmatter (line starting with "description:") + desc := "" + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "description:") { + desc = strings.TrimSpace(strings.TrimPrefix(line, "description:")) + break + } + } + name := strings.TrimSuffix(e.Name(), ".md") + if desc != "" { + lines = append(lines, fmt.Sprintf("- %s: %s", name, desc)) + } else { + lines = append(lines, fmt.Sprintf("- %s", name)) + } + } + if len(lines) == 0 { + return "" + } + return "AVAILABLE SKILLS (call load_skill(name) to get full content when needed):\n" + strings.Join(lines, "\n") +} + +// executeLoadSkill reads the full content of a skill file by name +func executeLoadSkill(rawArgs string) string { + var args struct { + Name string `json:"name"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Name == "" { + return "Error: 'name' is required for load_skill." + } + + name := strings.TrimSuffix(strings.TrimSpace(args.Name), ".md") + dir := resolveSkillsDir() + path := filepath.Join(dir, name+".md") + + data, err := os.ReadFile(path) + if err != nil { + entries, _ := os.ReadDir(dir) + var available []string + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".md") { + available = append(available, strings.TrimSuffix(e.Name(), ".md")) + } + } + if len(available) > 0 { + return fmt.Sprintf("Skill '%s' not found. Available skills: %s", name, strings.Join(available, ", ")) + } + return fmt.Sprintf("Skill '%s' not found. No skills are installed yet.", name) + } + + return fmt.Sprintf("=== SKILL: %s ===\n\n%s", name, string(data)) +} diff --git a/dash/backend/tools/subagents.go b/dash/backend/tools/subagents.go new file mode 100644 index 0000000..a571653 --- /dev/null +++ b/dash/backend/tools/subagents.go @@ -0,0 +1,56 @@ +package tools + +import ( + "encoding/json" + "fmt" +) + +// SubagentSpawner is a function variable injected from handlers to avoid circular imports +// Set via tools.SetSubagentFuncs() in main.go +var SubagentSpawner func(sessionID, name, task string) string +var SubagentStatusGetter func(id string) (status, output string) + +// SetSubagentFuncs injects the handler-level subagent functions into the tools package +func SetSubagentFuncs(spawner func(string, string, string) string, getter func(string) (string, string)) { + SubagentSpawner = spawner + SubagentStatusGetter = getter +} + +func executeSpawnSubagent(rawArgs string) string { + var args map[string]interface{} + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: Invalid arguments for spawn_subagent" + } + name, _ := args["name"].(string) + task, _ := args["task"].(string) + sessionID, _ := args["session_id"].(string) + + if name == "" || task == "" { + return "Error: 'name' and 'task' are required for spawn_subagent" + } + if SubagentSpawner == nil { + return "Error: Subagent system is not initialized" + } + id := SubagentSpawner(sessionID, name, task) + return fmt.Sprintf(`{"id": "%s", "status": "running", "message": "Subagent '%s' spawned successfully. Use get_subagent_status('%s') to check progress."}`, id, name, id) +} + +func executeGetSubagentStatus(rawArgs string) string { + var args map[string]interface{} + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil { + return "Error: Invalid arguments for get_subagent_status" + } + id, _ := args["id"].(string) + if id == "" { + return "Error: 'id' is required for get_subagent_status" + } + if SubagentStatusGetter == nil { + return "Error: Subagent system is not initialized" + } + status, output := SubagentStatusGetter(id) + if status == "" { + return fmt.Sprintf(`{"error": "Subagent '%s' not found"}`, id) + } + result, _ := json.Marshal(map[string]string{"id": id, "status": status, "output": output}) + return string(result) +} diff --git a/dash/backend/tools/web.go b/dash/backend/tools/web.go new file mode 100644 index 0000000..e0eeb21 --- /dev/null +++ b/dash/backend/tools/web.go @@ -0,0 +1,110 @@ +package tools + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "time" + + "github.com/microcosm-cc/bluemonday" +) + +// Extracts paragraphs of raw text from an arbitrary URL and strips HTML tags +func executeWebScrape(rawArgs string) string { + var args struct { + URL string `json:"url"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.URL == "" { + return "Error: Invalid arguments. 'url' is required." + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(func() *http.Request { + req, _ := http.NewRequest("GET", args.URL, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AgentHQ/1.0 Agentic Proxy") + return req + }()) + + if err != nil { + return fmt.Sprintf("Error fetching URL: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return fmt.Sprintf("Error: Website returned HTTP %d", resp.StatusCode) + } + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "Error reading response body" + } + + // Use Bluemonday strict policy to strip EVERYTHING except raw text + p := bluemonday.StrictPolicy() + cleanHtml := p.Sanitize(string(bodyBytes)) + + // Some basic whitespace cleanup since stripping tags leaves massive blank gaps + lines := strings.Split(cleanHtml, "\n") + var out []string + for _, l := range lines { + trimmed := strings.TrimSpace(l) + if trimmed != "" { + out = append(out, trimmed) + } + } + + finalText := strings.Join(out, "\n") + if len(finalText) > 15000 { + return finalText[:15000] + "\n\n... [WEBPAGE TRUNCATED FOR LENGTH LIMITS]" + } + + return finalText +} + +// executeWebSearch queries DuckDuckGo Lite for search results (no API key required) +func executeWebSearch(rawArgs string) string { + var args struct { + Query string `json:"query"` + } + if err := json.Unmarshal([]byte(rawArgs), &args); err != nil || args.Query == "" { + return "Error: 'query' is required." + } + + client := &http.Client{Timeout: 15 * time.Second} + searchURL := "https://lite.duckduckgo.com/lite/?q=" + strings.ReplaceAll(strings.TrimSpace(args.Query), " ", "+") + + req, _ := http.NewRequest("GET", searchURL, nil) + req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; AgentHQ/1.0)") + req.Header.Set("Accept", "text/html") + + resp, err := client.Do(req) + if err != nil { + return fmt.Sprintf("Error performing search: %v", err) + } + defer resp.Body.Close() + + bodyBytes, _ := ioutil.ReadAll(resp.Body) + + p := bluemonday.StrictPolicy() + clean := p.Sanitize(string(bodyBytes)) + + // Collect non-empty lines, limit to top results + lines := strings.Split(clean, "\n") + var results []string + for _, l := range lines { + t := strings.TrimSpace(l) + if len(t) > 20 { + results = append(results, t) + } + if len(results) >= 40 { + break + } + } + + if len(results) == 0 { + return "No search results found." + } + return fmt.Sprintf("Search results for: %s\n\n%s", args.Query, strings.Join(results, "\n")) +} diff --git a/dash/deploy.sh b/dash/deploy.sh new file mode 100755 index 0000000..80ab956 --- /dev/null +++ b/dash/deploy.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +echo "=== AgentHQ Deploy ===" + +echo "→ Building frontend..." +cd "$(dirname "$0")/frontend" +npm run build + +echo "→ Syncing frontend..." +rsync -az --delete frontend/dist/ prod:/home/deploy/agenthq/frontend/dist/ + +echo "→ Syncing backend source..." +rsync -az --delete \ + --exclude='*.db' \ + --exclude='agenthq-server' \ + --exclude='.env' \ + --exclude='workspace/' \ + backend/ \ + prod:/home/deploy/agenthq/backend/ + +echo "→ Building binary on server..." +ssh prod "cd /home/deploy/agenthq/backend && CGO_ENABLED=1 /usr/local/go/bin/go build -o agenthq-server ." + +echo "→ Restarting service..." +ssh prod "sudo systemctl restart agenthq && sleep 2 && sudo systemctl status agenthq --no-pager | head -8" + +echo "=== Done: https://agenthq.one ===" diff --git a/dash/frontend/.gitignore b/dash/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/dash/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/dash/frontend/README.md b/dash/frontend/README.md new file mode 100644 index 0000000..18bc70e --- /dev/null +++ b/dash/frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/dash/frontend/eslint.config.js b/dash/frontend/eslint.config.js new file mode 100644 index 0000000..fc20f56 --- /dev/null +++ b/dash/frontend/eslint.config.js @@ -0,0 +1,32 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{js,jsx}'], + extends: [ + js.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: { ...globals.browser, ...globals.node }, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + rules: { + 'no-unused-vars': ['error', { + argsIgnorePattern: '^[A-Z_]', + varsIgnorePattern: '^[A-Z_]', + }], + }, + }, +]) diff --git a/dash/frontend/index.html b/dash/frontend/index.html new file mode 100644 index 0000000..59f70e5 --- /dev/null +++ b/dash/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + Apollo + + +
+ + + diff --git a/dash/frontend/package-lock.json b/dash/frontend/package-lock.json new file mode 100644 index 0000000..f7e4b7b --- /dev/null +++ b/dash/frontend/package-lock.json @@ -0,0 +1,4574 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "d3-ease": "^3.0.1", + "d3-path": "^3.1.0", + "lucide-react": "^0.511.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.0.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.3", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", + "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.14.tgz", + "integrity": "sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.407", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", + "integrity": "sha512-4R8XgQOdfxexCd/u63lRm6wCHjECwI45MV9wxAs2ggtfWe2hwlo1ql97jKsju2IcJ+jFSTwBssyYoiWhh7mauQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.3", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.511.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz", + "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/monaco-editor": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.4.8", + "marked": "14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/dash/frontend/package.json b/dash/frontend/package.json new file mode 100644 index 0000000..3838c10 --- /dev/null +++ b/dash/frontend/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "overrides": { + "dompurify": "3.4.13" + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@monaco-editor/react": "^4.7.0", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", + "d3-ease": "^3.0.1", + "d3-path": "^3.1.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "lucide-react": "^0.511.0", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.0.0", + "remark-gfm": "^4.0.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "vite": "^7.3.1" + } +} diff --git a/dash/frontend/public/android-chrome-192x192.png b/dash/frontend/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..4ae0a87e32e4ceb68912bdc0ad62e8cae1e8b1dd GIT binary patch literal 47091 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

#dla%;(M+3aYNF0V`tRYgVXJpG3WNw6iEcqEZ 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 literal 0 HcmV?d00001 diff --git a/dash/frontend/public/android-chrome-512x512.png b/dash/frontend/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..0486bb5e1809d228fb591e18127dd9dd399f85af GIT binary patch literal 284873 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%0ARuN*V!ofQauWy+=6OQ7;UmEk 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 literal 0 HcmV?d00001 diff --git a/dash/frontend/public/apple-touch-icon.png b/dash/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..695689bac1e5d5ee6fdb65fa891bc40cd46c6999 GIT binary patch literal 42326 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 Q_W%F@07*qoM6N<$f|i%FlmGw# literal 0 HcmV?d00001 diff --git a/dash/frontend/public/favicon-16x16.png b/dash/frontend/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..2f264fd786ca444a2dc12fe43eb3344345bd09c0 GIT binary patch literal 746 zcmVP z)O9^TKzR>+eOJ-j+e=%OArWU<+E2wgOF;x#GD}YhxKW{%lwdH3nVCSz0w^1y>zaNu zl}bI~1~Z$@F1@Hf%W-EWctLlvM9T|NG6jQ z9vQ~y=os#ejbUVX1j=^>ilUThV&%S!o)=_^GP=6E5DJA54u=s41Q2}}#Y!|PN{Ub~ z78e)ca5{-DhQaFCb5u2ZzIfL_C4LAA3ULi8$<+?C|+~cpO>~ zed0caeBq1eN=r-2tfA|hv$Jz(Z@-8S8yg~sIEN;u!--fdhPJjg%*{Wdn>RK!s}>|k zG!f}^`lPwJ`BpZYeO)LPv9|Ue$GKyU!=N{p%VB+e9a>RCHj{b7xj4ihP7oPz<>=@r zPk-S9^-%5edZ%7d%8JY7`b~1uVYB`8csy@aRlP=>`<%;txcmbE0RR75fr>%^000I_ cL_t&o0ChG7WufPgvj6}907*qoM6N<$f)cw@VE_OC literal 0 HcmV?d00001 diff --git a/dash/frontend/public/favicon-32x32.png b/dash/frontend/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..d335362f549b3433b13c323d633aaa142b7a77a7 GIT binary patch literal 2121 zcmV-P2)6f$P)Hr97zt4>-Q zbVNz2)2K}!(^2_Dr!f!zsL^P229-cU$4W_{0)h;??DFmAckez}$P)ebo_o*zopaAU z_q*qw@2-JId?rquXm>aq^X&GxT^6go+Axe;q`R7|)>~G)wVJZs)cL8>eMGZ(r~|2~ zsUvL;$G+Oy+W&dI-g7=*&#(NZuP6`*OdyTYWSSF9pIJoN=ANFO^R-{pHqqPPC8VW| zc$g!DI-pj;Rc&c+Z?Et5de?KJw3x;Mfe1_;hzJ7yK&sE{-Qa0!tGCUBGOOJF$q4OZ|C(JH!jAfq>Sn(40ZZO zhk(Sy#0h+jpPIeqj1Z(7k^aUNICt((FlWvj#YTveM))w}H_e&ej*d%7N%pKT7H<1F z5EmCW#_e{0KqC*`J7r+qFV|rmN%0(`nwX?ye!qWYx7++zN=iz4v{J-@v17-^_4fAu zlZro31C&y*?Z3wgQ%F|UcoY^CV17{%#*NF=d#}cbFa{VGT90iQQ?1|hkL>8|_(x`D zrbG1=MI31QrfHk$_fJ!xNYIFgdhLo73O$L4Kme>=yB0ixBtcpEDk2Xr$|*XL+t}Fn zMp#=rFl5M(ANjn#ml5(<2}RU2iu*1lCnuw%`gaGUcP^3In?LZ3St5%gFDajQPyONTyY}v9%e;}jADGKA{9o?Sp z*HqtNP`G+c^LaX;`kF+7p6X6iB_gm|ttcxiB^s?GShHp|ELIEgP+_2OqEH7AiPjH* zM|_Ez+vsw4J;PU@fM_`ee~OD2V)W>-R0xE8=1k-=GBU7W!2;r-IbyUY5* z{62nW{!Gl8^<%22H2?=IDo{a!GROd)J$p8Cb93>?j~~+vp8NEmxrYyXiAOJ}CAizz zh*N)gAMd?)0?pri4TwMhzJ)ccLxqN;;~<1a8g62`It4@2zz8}BlRhRL3yX`jR2i*2 zTnV4o3#M&UR#g$_Y^)b8UW^eVLy44zMOYCJ7d1A_vwciC5FE+gpoKO{OIO0Cqg>#2 zdvN&hVdy1OcCg|Q+@2nQfWvM>8ILJh#Ck+HKnd6ggjob=SN-p!goFf?m6lN@7}dv* zA4gkz8^fIt-EV1b#tCi(Lz{qsQtI-HO1`fQSh*fc?7j-ZJAG=gxssCS@z`U@;GUtO z;XgQV-~jl|r!r?}t;$=!e+yqVG=N5tmX?a=e!)yBN)jZIs7?@qh}=feXvJT@iEu)AIioZM63GExgEdd;0WgWM^l? z<#I72jz)TVy5{q8^Kky$dA#z<%eZ;722V_ygtpe!0oMAuoU=PIKJlDE&mUN9mOuB2 z`Z$Zlf-sYD_7IiDG@>gYRqQi#B6vts)6 zX;}R1Vk~-g5vER?3iXi^5%5xjLkABbKR+J}ezpK_SG*00x;N@M1U{?7_^sl#1574g z+HLkfh={^8m9Bh&x1ISYEG$C(mtSctE#m(Uap}?}q>oP5N4B>13x1~8 z=xs}ICQx6On&Cr-ZqXd8hyzVcP2Gu3=R)p#Yv+SbEM2-3XU?4E8y8+al5qCS8BCfu zi9HsI$}tJGr{>3 zZGVaZ&vzV9@~*C~8eST+8SU?K*1(2WHzG509JX%#H9o&~O*^F0veM<(L8)l$+UM7( zvkl`PACFBNUsHI1HjcTHTsg|7(jQ1M9Z*XAPuSt}`j+$0$}GFXalWOcC9r$4PF=Ix*JAC-?Pde^*)-fU7tEsu!+1%Xxwab-sWyGiv?=wSgOh`_iUx z^>{oN!*&L1zXbpQ|Nr97c>n+a00v1!K~w_(+o}~g`)K1_00000NkvXXu0mjf>kktQ literal 0 HcmV?d00001 diff --git a/dash/frontend/public/favicon.ico b/dash/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..abcd629009c006b48b40c26fe16b4a5d71905dab GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/dash/frontend/public/site.webmanifest b/dash/frontend/public/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/dash/frontend/public/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/dash/frontend/public/vite.svg b/dash/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/dash/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dash/frontend/src/App.jsx b/dash/frontend/src/App.jsx new file mode 100644 index 0000000..774b72a --- /dev/null +++ b/dash/frontend/src/App.jsx @@ -0,0 +1,41 @@ +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' +import { AuthProvider } from './lib/auth' +import { useAuth } from './lib/useAuth' +import Landing from './pages/Landing' +import Auth from './pages/Auth' +import Account from './pages/Account' +import Dashboard from './pages/Dashboard' +import Admin from './pages/Admin' + +function ProtectedRoute({ children }) { + const { token } = useAuth() + if (!token) return + return children +} + +function AppRoutes() { + return ( + + } /> + } /> + + } /> + + } /> + } /> + } /> + + ) +} + +export default function App() { + return ( + + + + + + ) +} diff --git a/dash/frontend/src/components/AgentOSShell.jsx b/dash/frontend/src/components/AgentOSShell.jsx new file mode 100644 index 0000000..679451f --- /dev/null +++ b/dash/frontend/src/components/AgentOSShell.jsx @@ -0,0 +1,1549 @@ +import { useEffect, useMemo, useState } from 'react' +import { Activity, Bot, Brain, CalendarDays, CheckCircle2, MessageSquare, Plus, ShieldCheck, Sparkles } from 'lucide-react' + +function isNotFoundError(err) { + const message = String(err?.message || '') + return message.startsWith('404') || message.toLowerCase().includes('404') +} + +async function api(path, options = {}) { + const token = localStorage.getItem('agenthq_token') + const headers = { + ...(options.headers || {}), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + } + const res = await fetch(path, { ...options, headers }) + if (!res.ok) { + const text = await res.text() + throw new Error(`${res.status} ${text || res.statusText || `HTTP ${res.status}`}`.trim()) + } + return res.json() +} + +async function apiOrDefault(path, fallbackValue) { + try { + return await api(path) + } catch (err) { + if (isNotFoundError(err)) { + return fallbackValue + } + throw new Error(`${path}: ${err.message}`) + } +} + +function EmptyAction({ title, body, action, onClick, disabled = false }) { + return ( +

+
+
+ {title} + {body} +
+ {action && ( + + )} +
+ ) +} + +function OverviewMetric({ icon: Icon, label, value, tone = 'neutral' }) { + return ( +
+
+ {label} + {value} +
+ ) +} + +export default function AgentOSShell({ activeTab, setActiveTab }) { + const [companies, setCompanies] = useState([]) + const [departments, setDepartments] = useState([]) + const [agents, setAgents] = useState([]) + const [tasks, setTasks] = useState([]) + + const [threads, setThreads] = useState([]) + const [messages, setMessages] = useState([]) + const [schedules, setSchedules] = useState([]) + const [events, setEvents] = useState([]) + const [memoryTimeline, setMemoryTimeline] = useState([]) + const [memoryHits, setMemoryHits] = useState([]) + const [policies, setPolicies] = useState([]) + const [approvals, setApprovals] = useState([]) + const [auditVerify, setAuditVerify] = useState(null) + + const [modelProfiles, setModelProfiles] = useState([]) + const [selectedProfileId, setSelectedProfileId] = useState('') + + const [selectedCompanyId, setSelectedCompanyId] = useState('') + const [selectedDepartmentId, setSelectedDepartmentId] = useState('') + const [selectedAgentId, setSelectedAgentId] = useState('') + const [selectedThreadId, setSelectedThreadId] = useState('') + + const [chatInput, setChatInput] = useState('') + const [newThreadTitle, setNewThreadTitle] = useState('') + const [memoryQuery, setMemoryQuery] = useState('') + const [scheduleExpr, setScheduleExpr] = useState('*/30 * * * *') +const [scheduleMode, setScheduleMode] = useState('cron') +const [scheduleOnce, setScheduleOnce] = useState('') +const [scheduleMessage, setScheduleMessage] = useState('') +const [interAgentInbox, setInterAgentInbox] = useState([]) +const [interAgentTo, setInterAgentTo] = useState('') +const [interAgentContent, setInterAgentContent] = useState('') + const [statusMessage, setStatusMessage] = useState('') + const [lastEventId, setLastEventId] = useState(0) + + const [companyWorkspacePath, setCompanyWorkspacePath] = useState('') + const [companyDeployCommand, setCompanyDeployCommand] = useState('') + + const [modalType, setModalType] = useState('') + const [modalCompanyId, setModalCompanyId] = useState('') + const [modalDepartmentId, setModalDepartmentId] = useState('') + + const [newCompanyName, setNewCompanyName] = useState('') + const [newCompanyPath, setNewCompanyPath] = useState('') + const [newCompanyDeployCommand, setNewCompanyDeployCommand] = useState('') + const [newDepartmentName, setNewDepartmentName] = useState('') + const [newAgentName, setNewAgentName] = useState('') + const [newAgentRole, setNewAgentRole] = useState('worker') + + const companyByID = useMemo(() => { + const map = {} + for (const company of companies) { + map[company.id] = company + } + return map + }, [companies]) + + const departmentByID = useMemo(() => { + const map = {} + for (const department of departments) { + map[department.id] = department + } + return map + }, [departments]) + + const agentByID = useMemo(() => { + const map = {} + for (const agent of agents) { + map[agent.id] = agent + } + return map + }, [agents]) + + const selectedCompany = useMemo( + () => companies.find((c) => c.id === selectedCompanyId) || null, + [companies, selectedCompanyId], + ) + + const selectedAgent = useMemo( + () => agents.find((a) => a.id === selectedAgentId) || null, + [agents, selectedAgentId], + ) + + const agentsByDepartment = useMemo(() => { + const grouped = {} + for (const agent of agents) { + if (!grouped[agent.department_id]) { + grouped[agent.department_id] = [] + } + grouped[agent.department_id].push(agent) + } + return grouped + }, [agents]) + + const departmentsByCompany = useMemo(() => { + const grouped = {} + for (const department of departments) { + if (!grouped[department.company_id]) { + grouped[department.company_id] = [] + } + grouped[department.company_id].push(department) + } + return grouped + }, [departments]) + + const agentsByCompany = useMemo(() => { + const grouped = {} + for (const agent of agents) { + if (!grouped[agent.company_id]) { + grouped[agent.company_id] = [] + } + grouped[agent.company_id].push(agent) + } + return grouped + }, [agents]) + + const tasksByCompany = useMemo(() => { + const grouped = {} + for (const task of tasks) { + const companyID = task.company_id || agentByID[task.agent_id]?.company_id || '' + if (!companyID) continue + if (!grouped[companyID]) { + grouped[companyID] = [] + } + grouped[companyID].push(task) + } + return grouped + }, [tasks, agentByID]) + + const tasksByDepartment = useMemo(() => { + const grouped = {} + for (const task of tasks) { + const departmentID = task.department_id || agentByID[task.agent_id]?.department_id || '' + if (!departmentID) continue + if (!grouped[departmentID]) { + grouped[departmentID] = [] + } + grouped[departmentID].push(task) + } + return grouped + }, [tasks, agentByID]) + + const scheduleEvents = useMemo( + () => events.filter((e) => String(e?.event_type || '').toLowerCase().includes('schedule')).slice(-16).reverse(), + [events], + ) + + const runningTasks = useMemo(() => tasks.filter((task) => task.status === 'running'), [tasks]) + const activeSchedules = useMemo(() => schedules.filter((schedule) => schedule.is_active), [schedules]) + const departmentsInModalCompany = useMemo( + () => departments.filter((department) => department.company_id === modalCompanyId), + [departments, modalCompanyId], + ) + + const loadCompanies = async () => { + try { + const data = await api('/api/companies') + setCompanies(data || []) + } catch (err) { + if (!isNotFoundError(err)) { + throw err + } + setCompanies([]) + } + } + + const loadDirectoryData = async () => { + const [deps, ags, tks] = await Promise.all([ + apiOrDefault('/api/departments', []), + apiOrDefault('/api/agents', []), + apiOrDefault('/api/tasks?limit=300', []), + ]) + setDepartments(deps || []) + setAgents(ags || []) + setTasks(tks || []) + } + + const loadModelProfiles = async () => { + try { + const data = await api('/api/model-profiles') + setModelProfiles(data || []) + if (data?.length && !selectedProfileId) { + setSelectedProfileId(data[0].id) + } + } catch (err) { + if (!isNotFoundError(err)) { + throw err + } + setModelProfiles([]) + } + } + + const loadCompanyRuntimeData = async (companyID) => { + if (!companyID) { + setSchedules([]) + setMemoryTimeline([]) + setEvents([]) + setLastEventId(0) + return + } + const [sch, mem, evs] = await Promise.all([ + api(`/api/schedules?company_id=${encodeURIComponent(companyID)}`), + api(`/api/memory/timeline?company_id=${encodeURIComponent(companyID)}&limit=80`), + api(`/api/events?company_id=${encodeURIComponent(companyID)}&since_id=0&limit=120`), + ]) + setSchedules(sch || []) + setMemoryTimeline(mem || []) + setEvents(evs || []) + const maxID = (evs || []).reduce((maxValue, item) => Math.max(maxValue, item?.id || 0), 0) + setLastEventId(maxID) + } + + const loadThreads = async (agentID) => { + if (!agentID) { + setThreads([]) + setSelectedThreadId('') + setMessages([]) + return + } + const data = await api(`/api/threads?agent_id=${encodeURIComponent(agentID)}`) + setThreads(data || []) + if (data?.length) { + setSelectedThreadId((previousID) => (previousID && data.find((item) => item.id === previousID) ? previousID : data[0].id)) + } else { + setSelectedThreadId('') + setMessages([]) + } + } + + const loadMessages = async (threadID) => { + if (!threadID) { + setMessages([]) + return + } + const data = await api(`/api/threads/${encodeURIComponent(threadID)}/messages`) + setMessages(data || []) + } + + const loadGovernance = async () => { + if (!selectedCompanyId) { + setPolicies([]) + setApprovals([]) + setAuditVerify(null) + return + } + const [ps, aps, verify] = await Promise.allSettled([ + api(`/api/policies?company_id=${encodeURIComponent(selectedCompanyId)}`), + api(`/api/approvals?company_id=${encodeURIComponent(selectedCompanyId)}&status=pending`), + api('/api/audit/verify'), + ]) + setPolicies(ps.status === 'fulfilled' ? (ps.value || []) : []) + setApprovals(aps.status === 'fulfilled' ? (aps.value || []) : []) + setAuditVerify(verify.status === 'fulfilled' ? (verify.value || null) : null) + } + + async function loadAgentInbox(agentId, companyId) { + if (!agentId || !companyId) return + const msgs = await api(`/api/inter-agent?agent_id=${encodeURIComponent(agentId)}&company_id=${encodeURIComponent(companyId)}`) + setInterAgentInbox(msgs || []) + } + + useEffect(() => { + Promise.allSettled([loadCompanies(), loadDirectoryData(), loadModelProfiles()]).then((results) => { + const failures = results + .filter((result) => result.status === 'rejected') + .map((result) => result.reason) + if (failures.length === 0) { + return + } + const non404 = failures.filter((err) => !isNotFoundError(err)) + if (non404.length === 0) { + setStatusMessage('Some Agent OS endpoints are not available yet. Rebuild/restart backend to enable full features.') + return + } + setStatusMessage(`Failed to load bootstrap data: ${non404[0]?.message || non404[0]}`) + }) + // Bootstrap helpers intentionally run once on mount. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (companies.length === 0) { + if (selectedCompanyId) setSelectedCompanyId('') + if (selectedDepartmentId) setSelectedDepartmentId('') + if (selectedAgentId) setSelectedAgentId('') + return + } + + if (!companies.some((company) => company.id === selectedCompanyId)) { + setSelectedCompanyId(companies[0].id) + return + } + + const scopedDepartments = departments.filter((department) => department.company_id === selectedCompanyId) + if (scopedDepartments.length === 0) { + if (selectedDepartmentId) setSelectedDepartmentId('') + if (selectedAgentId) setSelectedAgentId('') + return + } + + if (!scopedDepartments.some((department) => department.id === selectedDepartmentId)) { + setSelectedDepartmentId(scopedDepartments[0].id) + return + } + + const scopedAgents = agents.filter((agent) => agent.department_id === selectedDepartmentId) + if (scopedAgents.length === 0) { + if (selectedAgentId) setSelectedAgentId('') + return + } + + if (!scopedAgents.some((agent) => agent.id === selectedAgentId)) { + setSelectedAgentId(scopedAgents[0].id) + } + }, [companies, departments, agents, selectedCompanyId, selectedDepartmentId, selectedAgentId]) + + useEffect(() => { + loadCompanyRuntimeData(selectedCompanyId).catch((err) => setStatusMessage(`Failed to load company data: ${err.message}`)) + }, [selectedCompanyId]) + + useEffect(() => { + setCompanyWorkspacePath(selectedCompany?.workspace_path || '') + setCompanyDeployCommand(selectedCompany?.deploy_command || '') + }, [selectedCompany?.id, selectedCompany?.workspace_path, selectedCompany?.deploy_command]) + + useEffect(() => { + loadThreads(selectedAgentId).catch((err) => setStatusMessage(`Failed to load threads: ${err.message}`)) + loadAgentInbox(selectedAgentId, selectedCompanyId).catch(() => {}) + }, [selectedAgentId, selectedCompanyId]) + + useEffect(() => { + loadMessages(selectedThreadId).catch((err) => setStatusMessage(`Failed to load messages: ${err.message}`)) + }, [selectedThreadId]) + + useEffect(() => { + if (activeTab !== 'governance') return + loadGovernance().catch((err) => setStatusMessage(`Failed to load governance data: ${err.message}`)) + // selected inputs control refresh; helper is recreated on render. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeTab, selectedCompanyId]) + + useEffect(() => { + if (!selectedThreadId) return + const timer = setInterval(() => { + loadMessages(selectedThreadId).catch(() => {}) + }, 1800) + return () => clearInterval(timer) + }, [selectedThreadId]) + + useEffect(() => { + if (!selectedCompanyId) return + + const source = new EventSource(`/api/events/stream?company_id=${encodeURIComponent(selectedCompanyId)}`) + + source.addEventListener('event', (event) => { + try { + const payload = JSON.parse(event.data) + setEvents((previous) => [...previous, payload].slice(-240)) + setLastEventId((previous) => Math.max(previous, payload?.id || 0)) + + const eventType = String(payload?.event_type || '').toLowerCase() + if (eventType.includes('company_') || eventType.includes('department_') || eventType.includes('agent_')) { + loadCompanies().catch(() => {}) + loadDirectoryData().catch(() => {}) + } + if (payload?.task_id) { + api('/api/tasks?limit=300').then((items) => setTasks(items || [])).catch(() => {}) + } + if (payload?.thread_id) { + loadMessages(payload.thread_id).catch(() => {}) + } + } catch { + // ignore parse errors + } + }) + + source.addEventListener('error', () => { + source.close() + setTimeout(() => { + api(`/api/events?company_id=${encodeURIComponent(selectedCompanyId)}&since_id=${lastEventId}&limit=100`) + .then((items) => { + if (!Array.isArray(items) || items.length === 0) return + setEvents((previous) => [...previous, ...items].slice(-240)) + const maxID = items.reduce((maxValue, item) => Math.max(maxValue, item?.id || 0), 0) + setLastEventId((previous) => Math.max(previous, maxID)) + }) + .catch(() => {}) + }, 2000) + }) + + return () => source.close() + }, [selectedCompanyId, lastEventId]) + + useEffect(() => { + if (modalType === 'agent' && departmentsInModalCompany.length > 0) { + if (!departmentsInModalCompany.some((department) => department.id === modalDepartmentId)) { + setModalDepartmentId(departmentsInModalCompany[0].id) + } + } + }, [modalType, departmentsInModalCompany, modalDepartmentId]) + + const closeModal = () => { + setModalType('') + setModalCompanyId('') + setModalDepartmentId('') + } + + const openCompanyModal = () => { + setNewCompanyName('') + setNewCompanyPath('') + setNewCompanyDeployCommand('') + setModalType('company') + } + + const openDepartmentModal = (companyID = '') => { + if (companies.length === 0) return + const targetCompanyID = companyID || selectedCompanyId || companies[0]?.id || '' + if (!targetCompanyID) return + setNewDepartmentName('') + setModalCompanyId(targetCompanyID) + setModalType('department') + } + + const openAgentModal = (departmentID = '') => { + if (departments.length === 0) return + const targetDepartmentID = departmentID || selectedDepartmentId || departments[0]?.id || '' + const targetDepartment = departments.find((department) => department.id === targetDepartmentID) + if (!targetDepartment) return + setNewAgentName('') + setNewAgentRole('worker') + setModalCompanyId(targetDepartment.company_id) + setModalDepartmentId(targetDepartmentID) + setModalType('agent') + } + + const createCompany = async () => { + const name = newCompanyName.trim() + if (!name) return + await api('/api/companies', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name, + description: 'Agent OS company', + workspace_path: newCompanyPath.trim(), + deploy_command: newCompanyDeployCommand.trim(), + }), + }) + await loadCompanies() + await loadDirectoryData() + closeModal() + } + + const createDepartment = async () => { + const companyID = modalCompanyId || selectedCompanyId + const name = newDepartmentName.trim() + if (!companyID || !name) return + await api('/api/departments', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ company_id: companyID, name, type: 'general' }), + }) + await loadDirectoryData() + if (selectedCompanyId === companyID) { + await loadCompanyRuntimeData(companyID) + } + closeModal() + } + + const createAgent = async () => { + const companyID = modalCompanyId || selectedCompanyId + const departmentID = modalDepartmentId || selectedDepartmentId + const name = newAgentName.trim() + if (!companyID || !departmentID || !name) return + await api('/api/agents', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + company_id: companyID, + department_id: departmentID, + name, + role_type: newAgentRole, + identity_prompt: `You are ${name}, operating as part of Agent OS.`, + }), + }) + await loadDirectoryData() + closeModal() + } + + const updateCompanyWorkspace = async () => { + if (!selectedCompanyId) return + await api(`/api/companies/${encodeURIComponent(selectedCompanyId)}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workspace_path: companyWorkspacePath.trim(), + deploy_command: companyDeployCommand.trim(), + }), + }) + await loadCompanies() + setStatusMessage('Company workspace mapping updated.') + } + + const bindAgentModel = async () => { + if (!selectedAgentId || !selectedProfileId) return + await api(`/api/agents/${encodeURIComponent(selectedAgentId)}/model-bind`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + primary_profile_id: selectedProfileId, + temperature: 0.2, + max_tokens: 1400, + reasoning_effort: 'standard', + }), + }) + setStatusMessage('Model profile was bound to selected agent.') + } + + const createThread = async () => { + if (!selectedCompanyId || !selectedAgentId) return + const created = await api('/api/threads', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + company_id: selectedCompanyId, + department_id: selectedDepartmentId, + agent_id: selectedAgentId, + title: newThreadTitle.trim() || 'New Agent Thread', + }), + }) + setNewThreadTitle('') + await loadThreads(selectedAgentId) + if (created?.id) { + setSelectedThreadId(created.id) + await loadMessages(created.id) + } + } + + const sendChatMessage = async () => { + const value = chatInput.trim() + if (!value || !selectedAgentId || !selectedCompanyId) return + + let targetThreadID = selectedThreadId + if (!targetThreadID) { + const created = await api('/api/threads', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + company_id: selectedCompanyId, + department_id: selectedDepartmentId, + agent_id: selectedAgentId, + title: 'Autocreated Thread', + }), + }) + targetThreadID = created.id + setSelectedThreadId(created.id) + await loadThreads(selectedAgentId) + } + + setChatInput('') + await api(`/api/threads/${encodeURIComponent(targetThreadID)}/messages`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ role: 'user', content: value }), + }) + await loadMessages(targetThreadID) + } + + const createSchedule = async () => { + if (!selectedCompanyId || !selectedAgentId) return + const payload = { prompt: scheduleMessage || 'Scheduled task' } + if (scheduleMode === 'cron') { + if (!scheduleExpr.trim()) return + await api('/api/schedules', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + company_id: selectedCompanyId, + department_id: selectedDepartmentId || '', + target_agent_id: selectedAgentId, + schedule_type: 'cron', + cron_expr: scheduleExpr.trim(), + timezone: 'UTC', + payload_json: JSON.stringify(payload), + is_active: true, + }), + }) + } else { + if (!scheduleOnce) return + const dt = new Date(scheduleOnce).toISOString() + await api('/api/schedules', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + company_id: selectedCompanyId, + department_id: selectedDepartmentId || '', + target_agent_id: selectedAgentId, + schedule_type: 'once', + start_at: dt, + timezone: 'UTC', + payload_json: JSON.stringify(payload), + is_active: true, + }), + }) + } + await loadCompanyRuntimeData(selectedCompanyId) + } + + const sendInterAgentMessage = async () => { + if (!interAgentTo || !interAgentContent.trim() || !selectedAgentId || !selectedCompanyId) return + await api('/api/inter-agent', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + from_agent_id: selectedAgentId, + to_agent_id: interAgentTo, + content: interAgentContent, + company_id: selectedCompanyId, + }), + }) + setInterAgentContent('') + await loadAgentInbox(selectedAgentId, selectedCompanyId) + } + + const queryMemory = async () => { + if (!selectedCompanyId) return + const items = await api(`/api/memory/query?company_id=${encodeURIComponent(selectedCompanyId)}&department_id=${encodeURIComponent(selectedDepartmentId || '')}&agent_id=${encodeURIComponent(selectedAgentId || '')}&query=${encodeURIComponent(memoryQuery)}&limit=40`) + setMemoryHits(items || []) + } + + const resolveApproval = async (approvalID, decision) => { + await api('/api/approvals/resolve', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ approval_id: approvalID, decision, actor: 'owner' }), + }) + await loadGovernance() + } + + const onboardingSteps = [ + { label: 'Company', done: companies.length > 0, action: openCompanyModal }, + { label: 'Department', done: departments.length > 0, action: () => openDepartmentModal() }, + { label: 'Agent', done: agents.length > 0, action: () => openAgentModal() }, + { label: 'Model', done: modelProfiles.length > 0, action: null }, + ] + + const renderOrganizationTree = () => { + if (companies.length === 0) { + return ( +
+ +
+ ) + } + + return ( + <> +
+

Organization

+ +
+ +
+ {companies.map((company) => { + const isCompanyActive = selectedCompanyId === company.id + const scopedDepartments = departmentsByCompany[company.id] || [] + return ( +
+ + + {isCompanyActive && ( +
+ {scopedDepartments.length === 0 && ( +
+
No departments
+ +
+ )} + + {scopedDepartments.map((department) => { + const scopedAgents = agentsByDepartment[department.id] || [] + const isDepartmentActive = selectedDepartmentId === department.id + return ( +
+ + +
+ {scopedAgents.length === 0 && ( +
+
No agents
+ +
+ )} + + {scopedAgents.map((agent) => ( + + ))} +
+
+ ) + })} +
+ )} +
+ ) + })} +
+ + ) + } + + return ( +
+ + +
+
+
+

{selectedCompany?.name || 'No Company Selected'}

+ + {selectedAgent + ? `${selectedAgent.name} · ${selectedAgent.role_type}` + : 'Select an agent in the left tree to open chat'} + +
+
+ + {statusMessage &&
{statusMessage}
} + + {activeTab === 'companies' && ( +
+
+
+

AgentHQ Command Center

+

Operate companies, agents, schedules, memory, and governance from one workspace.

+
+ +
+ +
+
+
+
+

Today at a glance

+ {selectedCompany?.name || 'No company selected'} +
+ +
+ +
+ Schedules + Tasks + Agents +
+
+ +
+
+
+

Setup path

+ First useful agent company +
+ +
+
+ {onboardingSteps.map((step) => ( +
+
+ {onboardingSteps.map((step) => ( + + ))} +
+
+
+ +
+ + + + +
+ + {companies.length === 0 && ( + + )} + +
+ + + + + + + + + + + + {companies.length === 0 && ( + + + + )} + {companies.map((company) => { + const companyTasks = tasksByCompany[company.id] || [] + return ( + + + + + + + + ) + })} + +
NameDepartmentsAgentsRunning TasksWorkspace
No companies yet.
+ + {(departmentsByCompany[company.id] || []).length}{(agentsByCompany[company.id] || []).length}{companyTasks.filter((task) => task.status === 'running').length}{company.workspace_path || '-'}
+
+
+ )} + + {activeTab === 'departments' && ( +
+
+

Departments

+ +
+ {companies.length === 0 &&
Create a company first.
} + +
+ + + + + + + + + + + + + {departments.length === 0 && ( + + + + )} + {departments.map((department) => { + const departmentAgentsList = agentsByDepartment[department.id] || [] + const departmentTasks = tasksByDepartment[department.id] || [] + return ( + + + + + + + + + ) + })} + +
DepartmentCompanyAgentsManagersRunning TasksTotal Tasks
No departments yet.
+ + {companyByID[department.company_id]?.name || '-'}{departmentAgentsList.length}{departmentAgentsList.filter((agent) => agent.role_type === 'manager').length}{departmentTasks.filter((task) => task.status === 'running').length}{departmentTasks.length}
+
+
+ )} + + {activeTab === 'agents' && ( +
+
+

Agents

+
+ + + +
+
+ {departments.length === 0 && ( + openDepartmentModal()} + disabled={companies.length === 0} + /> + )} + +
+ + + + + + + + + + + + + + {agents.length === 0 && ( + + + + )} + {agents.map((agent) => { + const agentTasks = tasks.filter((task) => task.agent_id === agent.id) + return ( + + + + + + + + + + ) + })} + +
AgentRoleDepartmentCompanyStatusTasksSelect
No agents yet. Add a manager first, then workers.
{agent.name}{agent.role_type}{departmentByID[agent.department_id]?.name || '-'}{companyByID[agent.company_id]?.name || '-'} + + {agent.status || 'idle'} + + {agentTasks.length} + +
+
+ + {selectedAgentId && ( +
+

Inter-agent Messages

+
+ +
+
+