Compare commits

...

30 Commits

Author SHA1 Message Date
Frederico Castro
4a6fe8606a Merge remote-tracking branch 'nitro/main' 2026-02-28 12:22:58 -03:00
Frederico Castro
2e14223dd4 Corrigir 5 vulnerabilidades XSS no frontend
Sanitizar valores dinâmicos com escapeHtml em pontos que estavam
sem proteção: tags no modal de agente, campo model no card,
mensagens de toast, prompt do modal e expressão cron nos agendamentos.
2026-02-28 12:22:19 -03:00
Frederico Castro
b9681b6746 Remover redirect da página inicial para agen.nitro-cloud 2026-02-28 11:59:55 -03:00
Frederico Castro
b7ebb8b855 Redirecionar página inicial para agen.nitro-cloud.duckdns.org 2026-02-28 11:59:55 -03:00
Agents Orchestrator
1efffdb92f AO acessar a página principal do orchestator tirar o redireconamento para o https://agen.nitro-cloud
Executado por: Backend Developer
2026-02-28 14:58:19 +00:00
Frederico Castro
0230265ca1 Persistir output do terminal no servidor e corrigir cursor do flow editor
- Buffer server-side no executor para manter até 1000 linhas por execução ativa
- Terminal restaura output do servidor ao recarregar a página (F5)
- Fechar overlay do flow editor ao navegar para outra seção
- Garantir SHELL e HOME no ambiente dos processos filhos
2026-02-28 11:02:46 -03:00
Frederico Castro
356411d388 Botão Commit & Push nos projetos e correção do resume de sessão
- Adicionar botão de commit & push para cada projeto na página de arquivos
- Criar rota POST /api/files/commit-push com git add, commit e push
- Adicionar Modal.prompt reutilizável para inputs com valor padrão
- Corrigir detecção de erro no executor (is_error/errors do CLI)
- Fallback automático para nova execução quando sessão expira no resume
2026-02-28 08:55:39 -03:00
Frederico Castro
87062c288e Incluir pasta data no repositório
Adiciona os arquivos JSON de dados (agentes, pipelines, tarefas,
agendamentos, execuções, webhooks, configurações) ao versionamento.
Mantém uploads e reports no gitignore.
2026-02-28 08:35:54 -03:00
Frederico Castro
a2f7c5f466 Adicionar timer e melhorar espaçamento da toolbar do terminal
- Contador de tempo (mm:ss ou hh:mm:ss) que inicia ao processar e
  persiste entre reloads via sessionStorage
- Espaçamento maior entre elementos da toolbar principal e action bar
- Timer para ao completar execução/pipeline ou ao limpar terminal
2026-02-28 08:34:27 -03:00
Frederico Castro
83b078b9ae Filtrar arquivo .git de worktrees no upload de projetos
Worktrees têm .git como arquivo (não pasta), que passava pelo filtro.
Adicionado à lista de exclusão no frontend e no backend.
2026-02-28 06:04:17 -03:00
Frederico Castro
d78fe02411 Tratar diretório existente inválido (worktree órfão) no import
Se o diretório em /home/projetos/ já existe mas não é um repo git
válido (ex: worktree órfão), remove e clona do zero ao invés de
falhar com erro.
2026-02-28 06:00:21 -03:00
Frederico Castro
7f8bf5e3a9 Corrigir limite de upload e error handling no import de projetos
Aumenta fileSize do multer para 500MB e adiciona tratamento de erro
do multer que retorna 413 com mensagem clara ao invés de crash.
2026-02-28 05:58:06 -03:00
Frederico Castro
e3103d27e7 Importar projetos da máquina local via upload de pasta
Substitui o navegador de diretórios do servidor por upload de pasta
local usando webkitdirectory. Filtra automaticamente .git,
node_modules e padrões do .gitignore antes do envio. Cria o repo
no Gitea, faz push e clona em /home/projetos/ para uso com agentes.
2026-02-28 05:46:03 -03:00
Frederico Castro
8a9a3d7988 Adicionar painel de importação de projetos para o Gitea
Novo menu "Importar" que permite selecionar um diretório do servidor,
navegar pela árvore de pastas, criar um repositório no Gitea e copiar
os arquivos respeitando .gitignore, sem alterar o projeto original.
2026-02-28 05:16:09 -03:00
Frederico Castro
884e8802bd Corrigir publicação: usar caddy reload ao invés de recriar container
Remove a modificação do docker-compose.yml (volumes individuais por
projeto não são mais necessários) e substitui docker compose up por
docker exec caddy caddy reload, que funciona corretamente de dentro
do container.
2026-02-28 04:58:17 -03:00
Frederico Castro
1606efa09f Reescrever README com layout profissional e documentação completa 2026-02-28 04:42:00 -03:00
Frederico Castro
633b19f80d Integrar repositórios Git na execução de agentes e pipelines
- Módulo git-integration: clone/pull, commit/push automático, listagem de repos
- Seletor de repositório nos modais de execução (agente e pipeline)
- Seletor de branch carregado dinamicamente ao escolher repo
- Campo de diretório escondido quando repositório selecionado
- Auto-commit e push ao final da execução com mensagem descritiva
- Instrução injetada para agentes não fazerem operações git
- Rotas API: GET /repos, GET /repos/:name/branches
- Pipeline: commit automático ao final de todos os steps
2026-02-28 04:24:47 -03:00
Frederico Castro
2fae816162 Corrigir truncamento no terminal usando CSS grid 2026-02-28 04:05:07 -03:00
Frederico Castro
2201ac8699 Usar force-recreate ao reiniciar Caddy na publicação 2026-02-28 03:30:21 -03:00
Frederico Castro
a6bbe33e4b Adicionar grupo docker ao user node no Dockerfile 2026-02-28 03:18:52 -03:00
Frederico Castro
4c197eef91 Adicionar publicação automática de projetos
- Botão publicar (rocket) nas pastas raiz do explorador
- Cria repositório no Gitea, faz git init + push
- Atualiza Caddyfile com subdomínio e file_server
- Adiciona volume ao docker-compose e reinicia Caddy
- Botões lado a lado (download, publicar, excluir) no file explorer
- Dockerfile: adiciona git e docker-cli
2026-02-28 03:17:56 -03:00
Frederico Castro
e9f65c2845 Corrigir ícones e adicionar exclusão no explorador de arquivos
- Trocar ícone archive (lixeira) por download em todos os botões
- Adicionar botão de excluir com ícone trash-2 em cada entrada
- Rota DELETE /api/files com proteção contra exclusão da raiz
- Confirmação via modal antes de excluir
2026-02-28 03:00:45 -03:00
Frederico Castro
2fccaaac40 Adicionar botão de download da pasta raiz no explorador de arquivos 2026-02-28 02:42:57 -03:00
Frederico Castro
3178366e0e Corrigir truncamento de linhas longas no terminal
- Adicionar overflow-wrap: anywhere e min-width: 0 no .content
- Adicionar min-width: 0 no .terminal-line para flex shrink correto
- Definir overflow-x: hidden no .terminal-body
2026-02-28 02:27:27 -03:00
Frederico Castro
fa47538a8f Adicionar campo diretório de trabalho no modal de execução
- Campo execute-workdir no modal com valor pré-preenchido do agente
- Frontend envia workingDirectory na API de execução
- Backend aceita e aplica override de workingDirectory via metadata
- Pré-preenche com config do agente selecionado ao abrir modal
- Adiciona stopAll ao scheduler e limpa README
2026-02-28 02:20:09 -03:00
Frederico Castro
7a4ab2279d Corrigir classes CSS dos modais para convenção BEM (modal--lg) 2026-02-28 02:13:27 -03:00
Frederico Castro
7cbfcb2d0d Desabilitar cache de arquivos estáticos para evitar JS desatualizado 2026-02-28 02:10:41 -03:00
Frederico Castro
46a6ebc9dd Pré-preencher diretório de trabalho dos agentes com /home/projetos/ 2026-02-28 02:06:40 -03:00
Frederico Castro
f6bf7ce0ed Adicionar delegação automática entre agentes coordenadores 2026-02-28 01:59:38 -03:00
Frederico Castro
96733b55cd Adicionar file explorer para projetos criados pelos agentes 2026-02-28 01:38:26 -03:00
34 changed files with 7686 additions and 365 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
node_modules/ node_modules/
data/ data/uploads/
data/reports/
.env .env
*.log *.log
.DS_Store .DS_Store

View File

@@ -1,10 +1,12 @@
FROM node:22-alpine FROM node:22-alpine
RUN apk add --no-cache git docker-cli
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci --omit=dev RUN npm ci --omit=dev
RUN npm install -g @anthropic-ai/claude-code RUN npm install -g @anthropic-ai/claude-code
COPY . . COPY . .
RUN mkdir -p data && chown -R node:node /app RUN mkdir -p data && chown -R node:node /app
RUN addgroup -g 984 docker 2>/dev/null; addgroup node docker 2>/dev/null || true
USER node USER node
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV PORT=3000 ENV PORT=3000

568
README.md
View File

@@ -1,363 +1,373 @@
# Agents Orchestrator <p align="center">
<img src="docs/logo.svg" alt="Agents Orchestrator" width="80" />
</p>
Painel administrativo web para orquestração de agentes [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Crie, configure e execute múltiplos agentes com diferentes personalidades, modelos e diretórios de trabalho — tudo a partir de uma interface visual profissional. <h1 align="center">Agents Orchestrator</h1>
![Dashboard do Agents Orchestrator — visão geral com métricas, gráficos de execuções e custos, distribuição de status, ranking de agentes e taxa de sucesso](docs/dashboard.png) <p align="center">
<strong>Plataforma de orquestração de agentes IA com interface visual, pipelines automatizados e integração Git nativa.</strong>
</p>
## Acesso <p align="center">
<a href="https://agents.nitro-cloud.duckdns.org"><img src="https://img.shields.io/badge/demo-live-00d4aa?style=flat-square" alt="Live Demo" /></a>
<a href="https://git.nitro-cloud.duckdns.org/fred/agents-orchestrator"><img src="https://img.shields.io/badge/gitea-repo-6c40cc?style=flat-square" alt="Gitea" /></a>
<img src="https://img.shields.io/badge/node-%3E%3D22-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js" />
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License" />
</p>
| Recurso | URL | <p align="center">
|---------|-----| <a href="#visao-geral">Visao Geral</a> &bull;
| **Aplicação** | https://agents.nitro-cloud.duckdns.org | <a href="#funcionalidades">Funcionalidades</a> &bull;
| **Repositório** | https://git.nitro-cloud.duckdns.org/fred/agents-orchestrator | <a href="#quick-start">Quick Start</a> &bull;
| **Portal Nitro Cloud** | https://nitro-cloud.duckdns.org | <a href="#arquitetura">Arquitetura</a> &bull;
<a href="#api">API</a> &bull;
<a href="#deploy">Deploy</a>
</p>
---
## Visao Geral
Agents Orchestrator e uma plataforma web para criar, configurar e executar agentes [Claude Code](https://docs.anthropic.com/en/docs/claude-code) de forma visual. Projetada para equipes de desenvolvimento e profissionais que precisam orquestrar multiplos agentes IA com diferentes especialidades, executar pipelines de trabalho automatizados e integrar com repositorios Git — tudo a partir de um painel administrativo elegante.
### Por que usar?
| Problema | Solucao |
|----------|---------|
| Gerenciar multiplos agentes via CLI e tedioso | Interface visual com cards, filtros e execucao com 1 clique |
| Saida do agente nao e visivel em tempo real | Terminal com streaming WebSocket chunk-a-chunk |
| Automatizar fluxos sequenciais e complexo | Pipelines visuais com aprovacao humana entre passos |
| Agentes nao tem acesso a repositorios remotos | Integracao Git nativa com clone, commit e push automatico |
| Deploy manual e propenso a erros | `git deploy` — um comando faz tudo |
---
## Funcionalidades ## Funcionalidades
### Gerenciamento de Agentes ### Agentes
- Crie agentes com nome, system prompt, modelo (Sonnet/Opus/Haiku), diretório de trabalho, ferramentas permitidas, modo de permissão e tags
- Ative, desative, edite, **duplique** ou exclua a qualquer momento
- Exporte/importe configurações completas em JSON
### Catálogo de Tarefas - Criacao com system prompt, modelo (Sonnet/Opus/Haiku), diretorio de trabalho, ferramentas permitidas e modo de permissao
- Crie e gerencie tarefas reutilizáveis com nome, categoria e descrição detalhada - Tags para organizacao e filtragem
- Categorias: Code Review, Segurança, Refatoração, Testes, Documentação, Performance - Duplicacao, importacao/exportacao JSON
- Filtro por texto e categoria - Delegacao automatica entre agentes (Tech Lead → PO)
- Execute qualquer tarefa diretamente no agente escolhido - Agentes coordenadores recebem lista de agentes disponiveis injetada no prompt
- Cards com truncamento inteligente e tooltip com descrição completa
### Execução de Tarefas ### Execucao
- Execute tarefas sob demanda em qualquer agente ativo
- Templates rápidos incluídos (detecção de bugs, revisão OWASP, refatoração, testes, documentação, performance)
- **Reexecute** tarefas que falharam ou foram canceladas com um clique
- Continuação de conversa (resume session) no terminal
### Terminal em Tempo Real - Modal de execucao com seletor de agente, tarefa, instrucoes adicionais e arquivos de contexto
- Streaming chunk-a-chunk via WebSocket com indicador de conexão - **Seletor de repositorio Git** — escolha um repo do Gitea e o branch; o sistema clona/atualiza, executa e faz commit/push automatico
- **Botão Interromper** para cancelar todas as execuções ativas - Templates rapidos: deteccao de bugs, revisao OWASP, refatoracao, testes, documentacao, performance
- **Busca** no output do terminal com navegação entre ocorrências - Retry automatico configuravel por agente
- **Download** da saída completa como `.txt` - Continuacao de conversa (resume session)
- **Copiar** saída para a área de transferência - Cancelamento individual ou em massa
- **Toggle de auto-scroll** para controle manual da rolagem
- Filtro por execução
### Dashboard com Gráficos
- Métricas em tempo real (agentes, execuções, agendamentos, custo, webhooks)
- **Gráfico de execuções** por dia (barras empilhadas sucesso/erro)
- **Gráfico de custo** por dia (linha com área preenchida)
- **Distribuição de status** (doughnut chart)
- **Top 5 agentes** mais executados (barras horizontais)
- **Taxa de sucesso** geral (gauge com percentual)
- Seletor de período: 7, 14 ou 30 dias
### Agendamento Cron
- Agende tarefas recorrentes com expressões cron
- Presets incluídos (horário, diário, semanal, mensal)
- Histórico de execuções por agendamento com duração e custo
### Pipelines ### Pipelines
- Encadeie múltiplos agentes em fluxos sequenciais
- Saída de cada passo alimenta o próximo via template `{{input}}` - Encadeamento de multiplos agentes em fluxos sequenciais
- **Diretório de trabalho** configurável por pipeline (pré-preenchido com base path) - Saida de cada passo alimenta o proximo via `{{input}}`
- Portões de aprovação humana entre passos (human-in-the-loop) - **Seletor de repositorio** — todos os passos trabalham no mesmo repo com commit automatico ao final
- **Retomar pipelines falhas** a partir do passo onde pararam - Portoes de aprovacao humana (human-in-the-loop)
- Ideal para fluxos como "analisar → corrigir → testar" - Retomada de pipelines falhos a partir do passo onde pararam
- Editor de fluxo visual com drag para reordenar passos
### Terminal
- Streaming em tempo real via WebSocket
- Botao Interromper para cancelar execucoes ativas
- Busca no output com navegacao entre ocorrencias
- Download como `.txt` e copia para clipboard
- Auto-scroll toggleavel
### Integração Git
- Listagem automatica de repositorios do Gitea
- Seletor de branch dinamico
- Clone/pull automatico antes da execucao
- **Commit e push automatico** ao final com mensagem descritiva
- Instrucao injetada para agentes nao fazerem operacoes git
- Publicacao de projetos: cria repo, configura subdominio, deploy com 1 clique
### Explorador de Arquivos
- Navegacao em `/home/projetos/` com breadcrumb
- Download de arquivos individuais ou pastas completas (.tar.gz)
- Exclusao com confirmacao
- Botao publicar em projetos — cria repo no Gitea, configura Caddy e faz deploy automatico em `projeto.nitro-cloud.duckdns.org`
### Dashboard
- Metricas em tempo real: agentes, execucoes, agendamentos, custo, webhooks
- Graficos: execucoes por dia, custo diario, distribuicao de status, top 5 agentes, taxa de sucesso
- Seletor de periodo: 7, 14 ou 30 dias
### Catalogo de Tarefas
- Tarefas reutilizaveis com nome, categoria e descricao
- Categorias: Code Review, Seguranca, Refatoracao, Testes, Documentacao, Performance
- Filtro por texto e categoria
- Execucao direta a partir do catalogo
### Agendamento Cron
- Expressoes cron com presets (horario, diario, semanal, mensal)
- Historico de execucoes por agendamento
- Retry automatico em caso de limite de slots
### Webhooks ### Webhooks
- Dispare execuções de agentes ou pipelines via HTTP externo
- **Edite** webhooks existentes (nome, alvo, status)
- **Teste** webhooks com um clique para verificar configuração
- Snippet cURL pronto para copiar
- Assinatura HMAC-SHA256 para validação de origem
### Notificações - Disparo de execucoes via HTTP externo
- **Centro de notificações** no header com badge de contagem - Edicao, teste com 1 clique e snippet cURL
- Notificações automáticas para execuções concluídas e com erro - Assinatura HMAC-SHA256
- **Notificações nativas do navegador** (Browser Notification API)
- Marcar como lidas / limpar todas
- Polling automático a cada 15 segundos
### Tema Claro/Escuro ### Notificacoes
- Toggle de tema no header com transições suaves
- Persistência da preferência em localStorage
- Terminal mantém fundo escuro em ambos os temas
### Exportação de Dados - Centro de notificacoes com badge de contagem
- **Exportar histórico** de execuções como CSV (UTF-8 com BOM) - Notificacoes nativas do navegador
- Exportar configuração de agentes em JSON - Polling automatico a cada 15 segundos
### Atalhos de Teclado ### Tema e UX
| Tecla | Ação |
|-------|------|
| `1``9` | Navegar entre seções |
| `N` | Novo agente |
| `Esc` | Fechar modal |
## Deploy - Tema claro/escuro com transicao suave
- Atalhos de teclado (`1`-`9` navegacao, `N` novo agente, `Esc` fechar modal)
- Exportacao de historico como CSV
A aplicação roda em container Docker na infraestrutura Nitro Cloud com HTTPS automático via Caddy + Let's Encrypt. ---
### Deploy automático (recomendado) ## Quick Start
Um único comando faz push e deploy completo: ### Requisitos
- Node.js >= 22
- [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) instalado e autenticado
### Execucao local
```bash ```bash
git deploy git clone https://github.com/fredac100/agents-orchestrator.git
cd agents-orchestrator
npm install
npm start
``` ```
O script `scripts/deploy.sh` executa automaticamente: Acesse `http://localhost:3000`.
1. Push para GitHub (origin) e Gitea (nitro) ### Com Docker
2. Backup dos dados no VPS (`data-backup-YYYYMMDD-HHMMSS`)
3. Sincronização via rsync (exclui `data/`, `.git`, `node_modules`)
4. Correção de permissões do diretório de dados
5. Rebuild do container Docker
6. Verificação do container e integridade dos dados
7. Limpeza de backups antigos (mantém os 3 mais recentes)
Opções:
```bash ```bash
git deploy # Push + deploy completo docker build -t agents-orchestrator .
bash scripts/deploy.sh --skip-push # Apenas deploy, sem push docker run -p 3000:3000 \
-v $(pwd)/data:/app/data \
-v ~/.claude:/home/node/.claude \
agents-orchestrator
``` ```
### Verificar status ---
```bash ## Arquitetura
ssh -p 2222 fred@192.168.1.151 "docker logs agents-orchestrator --tail 20"
```
### Reiniciar
```bash
ssh -p 2222 fred@192.168.1.151 "cd ~/vps && docker compose restart agents-orchestrator"
```
## Variáveis de Ambiente
| Variável | Descrição | Padrão |
|----------|-----------|--------|
| `PORT` | Porta do servidor | `3000` |
| `HOST` | Endereço de bind | `0.0.0.0` |
| `AUTH_TOKEN` | Token Bearer para autenticação da API | _(desabilitado)_ |
| `ALLOWED_ORIGIN` | Origin permitida para CORS | `https://agents.nitro-cloud.duckdns.org` |
| `ALLOWED_DIRECTORIES` | Diretórios permitidos para working directory (CSV) | _(todos)_ |
| `WEBHOOK_SECRET` | Segredo HMAC para assinatura de webhooks | _(desabilitado)_ |
| `CLAUDE_BIN` | Caminho para o binário do Claude CLI | _(auto-detectado)_ |
| `REDIS_URL` | URL do Redis para cache L2 (opcional) | _(somente memória)_ |
## Como Funciona
### Criando um agente
1. Acesse https://agents.nitro-cloud.duckdns.org
2. Clique em **Novo Agente** no header ou na seção Agentes
3. Configure nome, system prompt, modelo e diretório de trabalho
4. Salve — o agente aparecerá como card na listagem
### Executando uma tarefa
1. No card do agente, clique em **Executar**
2. Descreva a tarefa ou use um template rápido
3. Opcionalmente adicione instruções extras
4. A execução inicia e o terminal abre automaticamente com streaming da saída
### Criando um pipeline
1. Vá em **Pipelines****Novo Pipeline**
2. Adicione pelo menos 2 passos, selecionando um agente para cada
3. Opcionalmente defina um template de input usando `{{input}}` para referenciar a saída do passo anterior
4. Marque passos que requerem aprovação humana antes de prosseguir
5. Execute o pipeline fornecendo o input inicial
### Agendando uma tarefa
1. Vá em **Agendamentos****Novo Agendamento**
2. Selecione o agente, descreva a tarefa e defina a expressão cron
3. A tarefa será executada automaticamente nos horários configurados
## Infraestrutura
``` ```
Cliente (navegador) HTTPS (443)
|
▼ HTTPS (porta 443) [Caddy] ─── SSL automatico via DuckDNS
Caddy (reverse proxy + SSL automático) |
*.nitro-cloud.duckdns.org
▼ agents.nitro-cloud.duckdns.org |
Container Docker (agents-orchestrator) ┌──────────────┼──────────────┐
| | |
├── Express (HTTP API + arquivos estáticos) [agents.*] [git.*] [projeto.*]
└── WebSocket (streaming em tempo real) | | |
┌──────┴──────┐ [Gitea] [Caddy file_server]
| |
[Express] [WebSocket]
| |
├── API REST (40+ endpoints)
├── Manager (CRUD + orquestracao)
├── Executor (spawn claude CLI)
├── Pipeline (sequencial + aprovacao)
├── Scheduler (cron jobs)
├── Git Integration (clone/pull/commit/push)
└── Store (JSON com escrita atomica)
``` ```
### Arquitetura Interna ### Estrutura do Projeto
``` ```
server.js Express + WebSocket + rate limiting + auth server.js HTTP + WebSocket + rate limiting + auth
src/ src/
routes/api.js API REST (/api/*)30+ endpoints routes/api.js API REST — 40+ endpoints
agents/ agents/
manager.js CRUD + orquestração + notificações manager.js CRUD + orquestracao + delegacao
executor.js Spawna o CLI claude como child_process executor.js Spawna o CLI claude como child_process
scheduler.js Agendamento cron (in-memory + persistido) scheduler.js Agendamento cron
pipeline.js Execução sequencial com aprovação humana pipeline.js Execucao sequencial + aprovacao humana
store/db.js Persistência em JSON com escrita atômica git-integration.js Clone, pull, commit, push automatico
cache/index.js Cache em 2 níveis (memória + Redis opcional) store/db.js Persistencia JSON com escrita atomica
cache/index.js Cache L1 (memoria) + L2 (Redis opcional)
reports/generator.js Geracao de relatorios de execucao
public/ public/
index.html SPA single-page com hash routing app.html SPA com hash routing
css/styles.css Design system (dark/light themes) css/styles.css Design system (dark/light)
js/ js/
app.js Controlador principal + WebSocket + tema + routing app.js Controlador principal + WebSocket
api.js Client HTTP para a API api.js Client HTTP para a API
components/ UI por seção (15 módulos) components/ 16 modulos UI independentes
data/ scripts/
agents.json Agentes cadastrados deploy.sh Deploy automatizado via rsync + Docker
tasks.json Templates de tarefas data/ Persistencia em JSON (8 stores)
pipelines.json Pipelines configurados
schedules.json Agendamentos persistidos
executions.json Histórico de execuções (max 5000)
webhooks.json Configuração de webhooks
notifications.json Notificações do sistema
settings.json Configurações globais
``` ```
O executor invoca o binário `claude` com `--output-format stream-json`, parseia o stdout linha a linha e transmite os chunks via WebSocket para o frontend em tempo real. ---
## API REST ## API
### Agentes ### Agentes
| Método | Endpoint | Descrição | | Metodo | Endpoint | Descricao |
|--------|----------|-----------| |--------|----------|-----------|
| `GET` | `/api/agents` | Listar agentes | | `GET` | `/api/agents` | Listar agentes |
| `POST` | `/api/agents` | Criar agente | | `POST` | `/api/agents` | Criar agente |
| `GET` | `/api/agents/:id` | Obter agente | | `GET` | `/api/agents/:id` | Obter agente |
| `PUT` | `/api/agents/:id` | Atualizar agente | | `PUT` | `/api/agents/:id` | Atualizar agente |
| `DELETE` | `/api/agents/:id` | Excluir agente | | `DELETE` | `/api/agents/:id` | Excluir agente |
| `POST` | `/api/agents/:id/execute` | Executar tarefa no agente | | `POST` | `/api/agents/:id/execute` | Executar tarefa (aceita `repoName` e `repoBranch`) |
| `POST` | `/api/agents/:id/continue` | Continuar conversa (resume) | | `POST` | `/api/agents/:id/continue` | Continuar conversa |
| `POST` | `/api/agents/:id/cancel/:execId` | Cancelar execução | | `POST` | `/api/agents/:id/cancel/:execId` | Cancelar execucao |
| `GET` | `/api/agents/:id/export` | Exportar agente (JSON) | | `GET` | `/api/agents/:id/export` | Exportar agente |
| `POST` | `/api/agents/:id/duplicate` | Duplicar agente | | `POST` | `/api/agents/:id/duplicate` | Duplicar agente |
### Tarefas
| Método | Endpoint | Descrição |
|--------|----------|-----------|
| `GET` | `/api/tasks` | Listar tarefas |
| `POST` | `/api/tasks` | Criar tarefa |
| `PUT` | `/api/tasks/:id` | Atualizar tarefa |
| `DELETE` | `/api/tasks/:id` | Excluir tarefa |
### Agendamentos
| Método | Endpoint | Descrição |
|--------|----------|-----------|
| `GET` | `/api/schedules` | Listar agendamentos |
| `POST` | `/api/schedules` | Criar agendamento |
| `PUT` | `/api/schedules/:taskId` | Atualizar agendamento |
| `DELETE` | `/api/schedules/:taskId` | Remover agendamento |
| `GET` | `/api/schedules/history` | Histórico de execuções agendadas |
### Pipelines ### Pipelines
| Método | Endpoint | Descrição | | Metodo | Endpoint | Descricao |
|--------|----------|-----------| |--------|----------|-----------|
| `GET` | `/api/pipelines` | Listar pipelines | | `GET` | `/api/pipelines` | Listar pipelines |
| `POST` | `/api/pipelines` | Criar pipeline | | `POST` | `/api/pipelines` | Criar pipeline |
| `GET` | `/api/pipelines/:id` | Obter pipeline | | `POST` | `/api/pipelines/:id/execute` | Executar (aceita `repoName` e `repoBranch`) |
| `PUT` | `/api/pipelines/:id` | Atualizar pipeline |
| `DELETE` | `/api/pipelines/:id` | Excluir pipeline |
| `POST` | `/api/pipelines/:id/execute` | Executar pipeline |
| `POST` | `/api/pipelines/:id/cancel` | Cancelar pipeline |
| `POST` | `/api/pipelines/:id/approve` | Aprovar passo pendente | | `POST` | `/api/pipelines/:id/approve` | Aprovar passo pendente |
| `POST` | `/api/pipelines/:id/reject` | Rejeitar passo pendente | | `POST` | `/api/pipelines/:id/reject` | Rejeitar passo |
| `POST` | `/api/pipelines/resume/:execId` | Retomar pipeline falha | | `POST` | `/api/pipelines/resume/:execId` | Retomar pipeline falho |
### Webhooks ### Repositorios
| Método | Endpoint | Descrição | | Metodo | Endpoint | Descricao |
|--------|----------|-----------| |--------|----------|-----------|
| `GET` | `/api/webhooks` | Listar webhooks | | `GET` | `/api/repos` | Listar repositorios do Gitea |
| `POST` | `/api/webhooks` | Criar webhook | | `GET` | `/api/repos/:name/branches` | Listar branches de um repo |
| `PUT` | `/api/webhooks/:id` | Atualizar webhook |
| `DELETE` | `/api/webhooks/:id` | Excluir webhook |
| `POST` | `/api/webhooks/:id/test` | Testar webhook |
### Execuções e Histórico ### Arquivos e Publicacao
| Método | Endpoint | Descrição | | Metodo | Endpoint | Descricao |
|--------|----------|-----------| |--------|----------|-----------|
| `GET` | `/api/executions/active` | Execuções em andamento | | `GET` | `/api/files` | Listar diretorio |
| `GET` | `/api/executions/history` | Histórico paginado com filtros | | `GET` | `/api/files/download` | Download de arquivo |
| `GET` | `/api/executions/recent` | Execuções recentes | | `GET` | `/api/files/download-folder` | Download de pasta (.tar.gz) |
| `GET` | `/api/executions/export` | Exportar histórico como CSV | | `DELETE` | `/api/files` | Excluir arquivo ou pasta |
| `GET` | `/api/executions/:id` | Detalhes de uma execução | | `POST` | `/api/files/publish` | Publicar projeto (repo + deploy + subdominio) |
| `DELETE` | `/api/executions/:id` | Excluir execução do histórico |
| `POST` | `/api/executions/:id/retry` | Reexecutar execução falha |
| `POST` | `/api/executions/cancel-all` | Cancelar todas as execuções ativas |
| `DELETE` | `/api/executions` | Limpar histórico |
### Notificações
| Método | Endpoint | Descrição |
|--------|----------|-----------|
| `GET` | `/api/notifications` | Listar notificações |
| `POST` | `/api/notifications/:id/read` | Marcar como lida |
| `POST` | `/api/notifications/read-all` | Marcar todas como lidas |
| `DELETE` | `/api/notifications` | Limpar notificações |
### Sistema ### Sistema
| Método | Endpoint | Descrição | | Metodo | Endpoint | Descricao |
|--------|----------|-----------| |--------|----------|-----------|
| `GET` | `/api/health` | Health check (sem auth) | | `GET` | `/api/health` | Health check |
| `GET` | `/api/system/status` | Status geral do sistema | | `GET` | `/api/system/status` | Status geral |
| `GET` | `/api/system/info` | Informações do servidor | | `GET` | `/api/stats/costs` | Estatisticas de custo |
| `GET` | `/api/stats/costs` | Estatísticas de custo | | `GET` | `/api/stats/charts` | Dados para graficos |
| `GET` | `/api/stats/charts` | Dados para gráficos do dashboard |
| `GET/PUT` | `/api/settings` | Configurações globais | ---
## Deploy
### Deploy automatico
```bash
git deploy
```
O alias executa `scripts/deploy.sh` que automaticamente:
1. Push para GitHub e Gitea
2. Backup dos dados no VPS
3. Sincronizacao via rsync
4. Correcao de permissoes
5. Rebuild do container Docker
6. Verificacao de integridade
7. Limpeza de backups antigos (mantem 3)
```bash
# Apenas deploy sem push
bash scripts/deploy.sh --skip-push
```
### Variaveis de Ambiente
| Variavel | Descricao | Padrao |
|----------|-----------|--------|
| `PORT` | Porta do servidor | `3000` |
| `HOST` | Endereco de bind | `0.0.0.0` |
| `AUTH_TOKEN` | Bearer token para auth da API | _(desabilitado)_ |
| `ALLOWED_ORIGIN` | Origin para CORS | `http://localhost:3000` |
| `WEBHOOK_SECRET` | Segredo HMAC para webhooks | _(desabilitado)_ |
| `GITEA_URL` | URL interna do Gitea | `http://gitea:3000` |
| `GITEA_USER` | Usuario do Gitea | `fred` |
| `GITEA_PASS` | Senha do Gitea | _(obrigatorio para Git)_ |
| `DOMAIN` | Dominio base para subdominios | `nitro-cloud.duckdns.org` |
| `CLAUDE_BIN` | Caminho do CLI Claude | _(auto-detectado)_ |
| `REDIS_URL` | Redis para cache L2 | _(somente memoria)_ |
---
## Seguranca
- HTTPS via Caddy com certificado wildcard Let's Encrypt
- Autenticacao Bearer token com timing-safe comparison
- Rate limiting: 100 req/min (API), 30 req/min (webhooks)
- CORS restrito a origin configurada
- Correlation IDs em todas as requisicoes
- Escrita atomica em disco (temp + rename)
- Sanitizacao de prompts (NUL, controle, limite 50K chars)
- HMAC-SHA256 para webhooks recebidos
- Protecao contra path traversal no file explorer
---
## Eventos WebSocket ## Eventos WebSocket
O servidor envia eventos tipados via WebSocket que o frontend renderiza no terminal: | Evento | Descricao |
| Evento | Descrição |
|--------|-----------| |--------|-----------|
| `execution_output` | Chunk de texto da saída do agente | | `execution_output` | Chunk de saida do agente |
| `execution_complete` | Execução finalizada com resultado | | `execution_complete` | Execucao finalizada |
| `execution_error` | Erro durante execução | | `execution_error` | Erro durante execucao |
| `pipeline_step_start` | Início de um passo do pipeline | | `execution_retry` | Tentativa de retry |
| `pipeline_step_complete` | Passo do pipeline concluído | | `pipeline_step_start` | Inicio de passo |
| `pipeline_step_complete` | Passo concluido |
| `pipeline_complete` | Pipeline finalizado | | `pipeline_complete` | Pipeline finalizado |
| `pipeline_error` | Erro em um passo do pipeline | | `pipeline_error` | Erro no pipeline |
| `pipeline_approval_required` | Passo aguardando aprovação humana | | `pipeline_approval_required` | Aguardando aprovacao humana |
| `report_generated` | Relatorio gerado |
## Segurança ---
- **HTTPS** via Caddy com certificado Let's Encrypt automático
- **Autenticação** via Bearer token (variável `AUTH_TOKEN`)
- **Rate limiting** — 100 requisições por minuto por IP
- **CORS** restrito à origin configurada
- **Timing-safe comparison** para tokens de autenticação e webhooks
- **Correlation IDs** em todas as requisições para rastreabilidade
- **Escrita atômica** em disco (temp + rename) para integridade de dados
- **Sanitização** de prompts (NUL, caracteres de controle, limite de 50.000 chars)
- **Assinatura HMAC-SHA256** para webhooks recebidos
## Stack ## Stack
- **Backend**: Node.js, Express, WebSocket (ws), node-cron, uuid, express-rate-limit | Camada | Tecnologias |
- **Frontend**: HTML, CSS, JavaScript vanilla (sem framework, sem bundler) |--------|-------------|
- **Gráficos**: Chart.js 4.x | **Backend** | Node.js 22, Express, WebSocket (ws), node-cron, uuid |
- **Ícones**: Lucide | **Frontend** | HTML, CSS, JavaScript vanilla — sem framework, sem bundler |
- **Fontes**: Inter (UI), JetBrains Mono (código/terminal) | **Graficos** | Chart.js 4.x |
- **Persistência**: Arquivos JSON em disco com escrita atômica | **Icones** | Lucide |
- **Cache**: In-memory com suporte opcional a Redis (ioredis) | **Fontes** | Inter (UI), JetBrains Mono (terminal) |
- **Infraestrutura**: Docker + Caddy + DuckDNS + Let's Encrypt | **Persistencia** | JSON em disco com escrita atomica |
| **Cache** | In-memory + Redis opcional (ioredis) |
| **Infra** | Docker, Caddy, DuckDNS, Let's Encrypt |
| **Git** | Gitea (self-hosted) |
## Licença ---
## Licenca
MIT MIT
---
<p align="center">
<sub>Desenvolvido por <a href="https://nitro-cloud.duckdns.org">Nitro Cloud</a></sub>
</p>

5
data/agent-settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"env": {
"CLAUDE_CODE_MAX_OUTPUT_TOKENS": "128000"
}
}

1656
data/agent_versions.json Normal file

File diff suppressed because one or more lines are too long

1266
data/agents.json Normal file

File diff suppressed because one or more lines are too long

1266
data/executions.json Normal file

File diff suppressed because one or more lines are too long

296
data/notifications.json Normal file
View File

@@ -0,0 +1,296 @@
[
{
"id": "9f80ba16-4325-4129-9d35-9716ac68164a",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Arquiteto de Software\" finalizou a tarefa",
"metadata": {
"agentId": "f356e42a-73db-4b04-bca2-1fb022f373b7",
"executionId": "4863452b-259e-48ec-abf1-f41f0e4bf0c4"
},
"read": false,
"createdAt": "2026-02-26T23:56:33.927Z",
"created_at": "2026-02-26T23:56:33.927Z",
"updated_at": "2026-02-26T23:56:33.927Z"
},
{
"id": "ded8580c-f49c-4a9a-8855-43163afd68b9",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Arquiteto de Software\" finalizou a tarefa",
"metadata": {
"agentId": "f356e42a-73db-4b04-bca2-1fb022f373b7",
"executionId": "a4e88ba3-7d72-4069-848c-c98dbed801b1"
},
"read": false,
"createdAt": "2026-02-27T00:00:56.197Z",
"created_at": "2026-02-27T00:00:56.197Z",
"updated_at": "2026-02-27T00:00:56.197Z"
},
{
"id": "008a6514-5ae4-4c62-8c61-2024520395af",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "4bffa6ef-1d64-4788-aec1-d5a596ea287f"
},
"read": false,
"createdAt": "2026-02-27T01:34:32.561Z",
"created_at": "2026-02-27T01:34:32.561Z",
"updated_at": "2026-02-27T01:34:32.561Z"
},
{
"id": "a9b4e347-9bb2-416b-aa86-11b59b6e2346",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "ddd17e8a-9a37-4179-9499-f28df598d4b1"
},
"read": false,
"createdAt": "2026-02-27T01:34:37.970Z",
"created_at": "2026-02-27T01:34:37.970Z",
"updated_at": "2026-02-27T01:34:37.970Z"
},
{
"id": "b4fe795a-09ce-4978-901f-e0d2609c0adf",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "124e658b-a947-4c41-bac9-790c093c9754"
},
"read": false,
"createdAt": "2026-02-27T01:35:06.418Z",
"created_at": "2026-02-27T01:35:06.418Z",
"updated_at": "2026-02-27T01:35:06.418Z"
},
{
"id": "940de39e-e62b-4580-821c-9ae2a613c570",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "526e060f-5ea2-4302-a7d0-d2107959e395"
},
"read": false,
"createdAt": "2026-02-27T01:36:23.692Z",
"created_at": "2026-02-27T01:36:23.692Z",
"updated_at": "2026-02-27T01:36:23.692Z"
},
{
"id": "8db380ef-6329-46c8-a740-aad07c001b8d",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "53b75799-e74e-41e8-a8aa-f0af83a82693"
},
"read": false,
"createdAt": "2026-02-27T01:42:15.698Z",
"created_at": "2026-02-27T01:42:15.698Z",
"updated_at": "2026-02-27T01:42:15.698Z"
},
{
"id": "931fa597-5af3-47c3-8f71-2d56a2c9bf0f",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "91454df6-acc7-44d3-b3dc-4418fa7a093a"
},
"read": false,
"createdAt": "2026-02-27T01:52:49.615Z",
"created_at": "2026-02-27T01:52:49.615Z",
"updated_at": "2026-02-27T01:52:49.615Z"
},
{
"id": "69426884-a8f8-4efe-a37d-80d8a524133d",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Arquiteto de Software\" finalizou a tarefa",
"metadata": {
"agentId": "f356e42a-73db-4b04-bca2-1fb022f373b7",
"executionId": "97e3f81a-f04d-48dd-9f9b-23b19f91fd00"
},
"read": false,
"createdAt": "2026-02-27T05:00:32.024Z",
"created_at": "2026-02-27T05:00:32.024Z",
"updated_at": "2026-02-27T05:00:32.024Z"
},
{
"id": "c4419676-b592-4a29-8590-2b70b60dc04c",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "ec7c2602-56d2-40fc-8b0f-cb434aa6910e"
},
"read": false,
"createdAt": "2026-02-27T06:24:58.009Z",
"created_at": "2026-02-27T06:24:58.009Z",
"updated_at": "2026-02-27T06:24:58.009Z"
},
{
"id": "f5f04515-85e8-4e3b-9bd5-2197eb7aa719",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analisador de Armazenamento\" finalizou a tarefa",
"metadata": {
"agentId": "07cfd2ae-e6a4-4254-891b-0989ff472381",
"executionId": "9e80e8d2-43e1-4430-9f89-91a370dd22e1"
},
"read": false,
"createdAt": "2026-02-27T06:37:17.326Z",
"created_at": "2026-02-27T06:37:17.326Z",
"updated_at": "2026-02-27T06:37:17.326Z"
},
{
"id": "84ae7db5-9617-43f7-a441-2d5783ebc826",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analisador de Armazenamento\" finalizou a tarefa",
"metadata": {
"agentId": "07cfd2ae-e6a4-4254-891b-0989ff472381",
"executionId": "ec5e8f78-fc81-4537-b26b-f923e0ab142c"
},
"read": false,
"createdAt": "2026-02-27T06:43:46.776Z",
"created_at": "2026-02-27T06:43:46.776Z",
"updated_at": "2026-02-27T06:43:46.776Z"
},
{
"id": "1a7ab6a0-252c-453e-a1aa-2bc39bd284ef",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "1d15c272-e953-4087-aea3-a46c8e12aa17"
},
"read": false,
"createdAt": "2026-02-27T16:05:54.704Z",
"created_at": "2026-02-27T16:05:54.704Z",
"updated_at": "2026-02-27T16:05:54.704Z"
},
{
"id": "16198936-ad6f-4af4-bc06-0f2e40e4e4ca",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analisador de Armazenamento\" finalizou a tarefa",
"metadata": {
"agentId": "07cfd2ae-e6a4-4254-891b-0989ff472381",
"executionId": "46f2afb4-5827-4db8-a1c4-db67ac9dd538"
},
"read": false,
"createdAt": "2026-02-27T16:23:06.763Z",
"created_at": "2026-02-27T16:23:06.763Z",
"updated_at": "2026-02-27T16:23:06.763Z"
},
{
"id": "b62a6ae6-7f6b-4475-bdae-48557269573b",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Analista de Segurança do Sistema\" finalizou a tarefa",
"metadata": {
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"executionId": "23edd584-0ddc-41a7-b135-b8880685fecb"
},
"read": false,
"createdAt": "2026-02-27T16:30:33.854Z",
"created_at": "2026-02-27T16:30:33.854Z",
"updated_at": "2026-02-27T16:30:33.854Z"
},
{
"id": "f4fadf97-a587-4844-ba46-c7ea5362e100",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Monitor SAE\" finalizou a tarefa",
"metadata": {
"agentId": "a53e89a1-2f82-4188-91b8-142705f94f47",
"executionId": "2c502232-7486-42ed-975c-8a7ef1628914"
},
"read": false,
"createdAt": "2026-02-27T17:04:57.155Z",
"created_at": "2026-02-27T17:04:57.155Z",
"updated_at": "2026-02-27T17:04:57.155Z"
},
{
"id": "54a03f66-494a-4572-98b7-77919aa133ea",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Monitor CONSPRE\" finalizou a tarefa",
"metadata": {
"agentId": "c954e0cd-48ab-4c6c-bc38-2d360bf417ae",
"executionId": "2fb1397d-ad04-42d3-bc9d-324500a8c11f"
},
"read": false,
"createdAt": "2026-02-27T17:20:03.623Z",
"created_at": "2026-02-27T17:20:03.623Z",
"updated_at": "2026-02-27T17:20:03.623Z"
},
{
"id": "a8d697a2-344c-4015-aa00-65532f5dce3b",
"type": "error",
"title": "Execução falhou",
"message": "Agente \"Monitor AtuaCAPES\" encontrou um erro",
"metadata": {
"agentId": "1568189e-92c5-4139-8f18-035eca8e3753",
"executionId": "261418b2-04ae-4889-a12b-e37b2e11adcc"
},
"read": false,
"createdAt": "2026-02-27T17:40:05.390Z",
"created_at": "2026-02-27T17:40:05.390Z",
"updated_at": "2026-02-27T17:40:05.390Z"
},
{
"id": "989ebce7-5805-48e0-baf8-092bffe8d8ea",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Monitor Consolidado CAPES\" finalizou a tarefa",
"metadata": {
"agentId": "d5ca1c47-872c-47cc-8d80-5ae5491f8207",
"executionId": "c2b3c2c2-061a-4e86-a214-0ec1211864df"
},
"read": false,
"createdAt": "2026-02-27T18:03:21.985Z",
"created_at": "2026-02-27T18:03:21.985Z",
"updated_at": "2026-02-27T18:03:21.985Z"
},
{
"id": "a71c0471-c452-449b-80ac-4fb2882b57ec",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Tech Lead\" finalizou a tarefa",
"metadata": {
"agentId": "ea485e4f-b4b7-47ab-bb9b-e9faebcb3921",
"executionId": "1b95b14c-092a-406e-906b-acadbc4e391a"
},
"read": false,
"createdAt": "2026-02-27T18:07:52.369Z",
"created_at": "2026-02-27T18:07:52.369Z",
"updated_at": "2026-02-27T18:07:52.369Z"
},
{
"id": "082b2e45-24c3-4de3-8020-0a34d6a35288",
"type": "success",
"title": "Execução concluída",
"message": "Agente \"Code Reviewer\" finalizou a tarefa",
"metadata": {
"agentId": "3f1a5442-ffe9-461c-a8e1-2f7239a8f025",
"executionId": "51908dd9-1759-4cd1-856d-7650321e4f7f"
},
"read": false,
"createdAt": "2026-02-28T07:42:13.938Z",
"created_at": "2026-02-28T07:42:13.938Z",
"updated_at": "2026-02-28T07:42:13.938Z"
}
]

355
data/pipelines.json Normal file
View File

@@ -0,0 +1,355 @@
[
{
"id": "652065ba-a996-44e9-8c14-75b6ad76d280",
"name": "Pipeline de Desenvolvimento de Feature",
"description": "Pipeline completo para desenvolvimento de uma nova feature: desde a análise arquitetural, passando pelo desenvolvimento backend e frontend, testes, revisão de segurança e preparação para deploy.",
"steps": [
{
"id": "7efde1d7-b844-4a48-8d5f-3ca58fe32159",
"agentId": "f356e42a-73db-4b04-bca2-1fb022f373b7",
"order": 0,
"inputTemplate": "Analise os requisitos da feature a seguir e crie um design arquitetural detalhado. Defina as entidades, endpoints de API, estrutura de banco de dados e componentes de frontend necessários. Considere escalabilidade, segurança e manutenibilidade. Feature: {{input}}",
"description": "",
"requiresApproval": false
},
{
"id": "b4d5030c-a67f-4fac-a464-d3b3bba17b52",
"agentId": "4212b6b0-f519-41dc-891d-0c8b4a8f6843",
"order": 1,
"inputTemplate": "Com base no design arquitetural abaixo, implemente os endpoints de API, models, services e repositories necessários. Use TypeScript com NestJS, Prisma ORM e PostgreSQL. Inclua validações, tratamento de erros e testes unitários. Design: {{input}}",
"description": "",
"requiresApproval": false
},
{
"id": "e4662771-ed2f-48e5-9121-aff85e2433a7",
"agentId": "a763246a-f411-4895-aa0d-8324af490d2e",
"order": 2,
"inputTemplate": "Com base na implementação backend abaixo, desenvolva os componentes React/TypeScript necessários para a interface. Crie componentes reutilizáveis, hooks customizados, integração com a API usando React Query, formulários com validação e testes com Testing Library. Implementação Backend: {{input}}",
"description": "",
"requiresApproval": false
},
{
"id": "940b975e-7109-4d16-ab9b-df4e9f4f585f",
"agentId": "aacbde57-6952-407d-8388-a62230b06c0b",
"order": 3,
"inputTemplate": "Revise todo o código implementado (backend e frontend) abaixo. Crie testes E2E com Playwright cobrindo os fluxos críticos, verifique a cobertura de testes unitários, identifique bugs potenciais e sugira melhorias de qualidade. Código para revisão: {{input}}",
"description": "",
"requiresApproval": false
},
{
"id": "a4adea8c-e1a2-4996-902a-2538cfe92db3",
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"order": 4,
"inputTemplate": "Realize uma auditoria de segurança completa no código e testes abaixo. Verifique vulnerabilidades OWASP Top 10, validação de entrada, autenticação/autorização, exposição de dados sensíveis e compliance. Forneça um relatório de segurança com severidade e correções. Código e testes: {{input}}",
"description": "",
"requiresApproval": false
}
],
"status": "active",
"created_at": "2026-02-26T03:31:18.048Z",
"updated_at": "2026-02-26T22:33:45.869Z"
},
{
"id": "21d4ad2c-6337-4389-b766-e0806ecf696e",
"name": "Pipeline de Code Review Completo",
"description": "Pipeline para revisão completa de código: análise arquitetural, revisão de qualidade, verificação de segurança e validação de performance. Garante que todo código atenda aos padrões da equipe.",
"steps": [
{
"id": "dcda608f-da88-48d6-8ab5-ff9410de6c68",
"agentId": "f356e42a-73db-4b04-bca2-1fb022f373b7",
"order": 0,
"inputTemplate": "Revise a arquitetura e padrões de design do código abaixo. Verifique aderência a Clean Architecture, SOLID, e identifique acoplamentos indesejados ou violações de camadas. Código: {{input}}",
"description": ""
},
{
"id": "1d8d819c-474b-4bd0-823b-ff14a6cb4b25",
"agentId": "4212b6b0-f519-41dc-891d-0c8b4a8f6843",
"order": 1,
"inputTemplate": "Com base na revisão arquitetural, analise a qualidade do código backend: legibilidade, tratamento de erros, tipagem, testes, performance de queries e boas práticas. Sugira refatorações específicas. Revisão anterior e código: {{input}}",
"description": ""
},
{
"id": "7b0fe629-8c92-4d09-bc18-bd65a1289acc",
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"order": 2,
"inputTemplate": "Realize análise de segurança no código revisado. Verifique: SQL injection, XSS, CSRF, exposição de dados, autenticação/autorização, validação de input e dependências vulneráveis. Código e revisões: {{input}}",
"description": ""
},
{
"id": "3c8da665-0f70-4374-8507-bd9e74081d7a",
"agentId": "aacbde57-6952-407d-8388-a62230b06c0b",
"order": 3,
"inputTemplate": "Com base em todas as revisões anteriores, compile um relatório final de qualidade. Liste: bugs encontrados, melhorias sugeridas, cobertura de testes necessária e um checklist de aprovação para merge. Revisões: {{input}}",
"description": ""
}
],
"status": "active",
"created_at": "2026-02-26T03:32:06.846Z",
"updated_at": "2026-02-26T03:32:06.846Z"
},
{
"id": "675a2293-a77b-4953-9cf7-168d8671afb2",
"name": "Pipeline de Correção de Bug",
"description": "Pipeline para análise, correção e validação de bugs reportados. Inclui diagnóstico, implementação da correção, testes de regressão e verificação de segurança.",
"steps": [
{
"id": "49f3f0ba-230b-4349-9e6d-b0845f8737d2",
"agentId": "4212b6b0-f519-41dc-891d-0c8b4a8f6843",
"order": 0,
"inputTemplate": "Analise o bug reportado abaixo. Identifique a causa raiz, o impacto no sistema e proponha uma correção com código. Inclua análise de possíveis efeitos colaterais da correção. Bug: {{input}}",
"description": ""
},
{
"id": "d65f086f-d5de-4b9c-abe1-834ce96fd1d2",
"agentId": "aacbde57-6952-407d-8388-a62230b06c0b",
"order": 1,
"inputTemplate": "Com base na correção proposta abaixo, crie testes de regressão que cubram o cenário do bug e cenários relacionados. Verifique se a correção não introduz novos problemas. Correção: {{input}}",
"description": ""
},
{
"id": "44fc4194-fc4b-481e-8b22-38ec9bbb9ecf",
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"order": 2,
"inputTemplate": "Verifique se a correção de bug e os testes abaixo não introduzem vulnerabilidades de segurança. Analise se o bug original tinha implicações de segurança. Correção e testes: {{input}}",
"description": ""
}
],
"status": "active",
"created_at": "2026-02-26T03:32:06.849Z",
"updated_at": "2026-02-26T03:32:06.849Z"
},
{
"id": "ce04edc0-64e7-460c-964e-9a06e3954d59",
"name": "Pipeline de Otimização de Performance",
"description": "Pipeline para identificar e resolver problemas de performance: análise de banco de dados, otimização de código backend e frontend, caching e monitoramento.",
"steps": [
{
"id": "107b4ac9-c600-4dbf-aeda-530c78dac864",
"agentId": "7a9b05ab-3f87-4e70-9394-14fda4136d59",
"order": 0,
"inputTemplate": "Analise as queries e o schema do banco de dados relacionados ao problema de performance abaixo. Identifique queries lentas, índices faltantes, N+1 problems e sugira otimizações com EXPLAIN ANALYZE. Problema: {{input}}",
"description": ""
},
{
"id": "01c0a13b-8c2f-4b16-adb3-71f2d53ef497",
"agentId": "4212b6b0-f519-41dc-891d-0c8b4a8f6843",
"order": 1,
"inputTemplate": "Com base nas otimizações de banco sugeridas, otimize o código backend: implemente caching com Redis, otimize algoritmos, reduza chamadas de rede e melhore o uso de recursos. Otimizações DB: {{input}}",
"description": ""
},
{
"id": "89459a69-6b09-4bef-b422-647472b42dbb",
"agentId": "a763246a-f411-4895-aa0d-8324af490d2e",
"order": 2,
"inputTemplate": "Otimize o frontend para melhor performance: implemente lazy loading, code splitting, memoização, virtualização de listas longas e otimize re-renders. Analise o bundle size. Contexto: {{input}}",
"description": ""
},
{
"id": "7e65b80f-e652-4ad3-a1fd-0971950c64a3",
"agentId": "7075eeef-1f11-441b-9a18-e5a715f28099",
"order": 3,
"inputTemplate": "Configure monitoramento de performance para as otimizações implementadas: dashboards no Grafana, alertas de latência, métricas de throughput e configuração de auto-scaling. Otimizações realizadas: {{input}}",
"description": ""
}
],
"status": "active",
"created_at": "2026-02-26T03:32:06.852Z",
"updated_at": "2026-02-26T03:32:06.852Z"
},
{
"id": "6e698339-f089-4b5d-aedb-8bb2e4f6bb21",
"name": "Pipeline de Deploy para Produção",
"description": "Pipeline completo de preparação e execução de deploy: checklist de pré-deploy, configuração de infraestrutura, estratégia de rollback e monitoramento pós-deploy.",
"steps": [
{
"id": "5d218cf9-507a-42b9-9d0f-4dfe4a2d1b71",
"agentId": "aacbde57-6952-407d-8388-a62230b06c0b",
"order": 0,
"inputTemplate": "Execute o checklist de pré-deploy: verifique se todos os testes passam, valide a cobertura mínima, execute smoke tests e confirme que não há regressões. Gere relatório de go/no-go. Release: {{input}}",
"description": ""
},
{
"id": "7894833e-8272-4259-9f12-8f0103306bb3",
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"order": 1,
"inputTemplate": "Realize verificação de segurança pré-deploy: scan de vulnerabilidades em dependências, verificação de secrets, análise de configurações de produção e checklist OWASP. Relatório QA: {{input}}",
"description": ""
},
{
"id": "0640c41a-61e1-4d80-bd16-64f4947955a8",
"agentId": "7075eeef-1f11-441b-9a18-e5a715f28099",
"order": 2,
"inputTemplate": "Prepare a infraestrutura para o deploy: configure blue-green deployment, prepare scripts de rollback, atualize configurações de produção e configure health checks. Relatórios anteriores: {{input}}",
"description": ""
},
{
"id": "f98508bd-476b-4339-b114-4ae812ce0bd0",
"agentId": "7075eeef-1f11-441b-9a18-e5a715f28099",
"order": 3,
"inputTemplate": "Configure monitoramento pós-deploy: alertas de error rate, latência p99, dashboards de KPIs, log aggregation e runbook de incidentes. Defina critérios de rollback automático. Deploy info: {{input}}",
"description": ""
}
],
"status": "active",
"created_at": "2026-02-26T03:32:06.855Z",
"updated_at": "2026-02-26T03:32:06.855Z"
},
{
"id": "18308362-793a-4fdd-b85c-a16bb41cf0c1",
"name": "Auto-Evolução do Sistema",
"description": "Pipeline completa de auto-melhoria: análise → planejamento → implementação backend → implementação frontend → validação. Os agentes trabalham em sequência, cada um recebendo o output do anterior.",
"steps": [
{
"id": "9fc6ac16-ad6e-4a3a-a1ea-a87bfe496b7c",
"agentId": "51202705-ce9d-4d96-acb5-00c6fe9d6b9e",
"order": 0,
"inputTemplate": "Analise o codebase do projeto em: {{input}}\n\nNavegue até o diretório, leia TODOS os arquivos principais e gere o relatório de análise conforme seu system prompt. Limite a saída a no máximo 3000 palavras. Inclua a seção META com caminho e nota.",
"description": "Análise de Código",
"requiresApproval": false
},
{
"id": "ec14ac5e-a82a-41df-9250-cead5600a8fc",
"agentId": "5f0be2a6-e549-44f7-8cc1-2a0634500321",
"order": 1,
"inputTemplate": "Relatório de análise:\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META. Navegue ao projeto, valide os achados e projete o plano de melhorias conforme seu system prompt.\n\nREGRAS CRÍTICAS:\n- Máximo 10 itens no plano\n- Cada item DEVE listar arquivos exatos a modificar\n- Cada item DEVE ter backend E frontend se aplicável\n- NUNCA proponha mudar defaults ou adicionar requisitos obrigatórios\n- Priorize: bugs > segurança > performance > UX > features novas",
"description": "Planejamento de Features",
"requiresApproval": true
},
{
"id": "472ac286-eee1-454e-9e5a-42678e91137b",
"agentId": "db116f20-f663-4d98-ab04-5ddeb09e2c0d",
"order": 2,
"inputTemplate": "Plano de implementação:\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META. Implemente APENAS os itens de BACKEND do plano.\n\nREGRAS INVIOLÁVEIS:\n- SÓ modifique arquivos de backend (server.js, src/**/*.js, package.json)\n- NUNCA toque no frontend (public/**)\n- NUNCA mude permissionMode, AUTH_TOKEN, ou outros defaults\n- NUNCA adicione dependências obrigatórias que quebrem o sistema se ausentes\n- Execute `node --check` em CADA arquivo modificado\n- Ao finalizar: inicie o servidor, teste com curl /api/health e pelo menos 3 endpoints afetados\n- Retorne o resumo conforme seu system prompt",
"description": "Implementação Backend",
"requiresApproval": true
},
{
"id": "5bcc2eab-6ba6-4586-b073-b7d2cc96f8e6",
"agentId": "7aad9f1c-fcbe-4c36-8cf4-26e8efed09c0",
"order": 3,
"inputTemplate": "Resumo do backend implementado + plano original:\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META. Implemente APENAS os itens de FRONTEND do plano.\n\nREGRAS INVIOLÁVEIS:\n- SÓ modifique arquivos do frontend (public/**)\n- NUNCA toque no backend (server.js, src/**)\n- Para cada rota criada pelo backend, implemente o client em api.js\n- SEMPRE chame Utils.refreshIcons() após inserir HTML dinâmico\n- SEMPRE escape conteúdo com Utils.escapeHtml()\n- Execute `node --check` em CADA arquivo modificado\n- Retorne o resumo conforme seu system prompt",
"description": "Implementação Frontend",
"requiresApproval": true
},
{
"id": "33daf19f-6ae1-410b-934b-c21e2d1c0c86",
"agentId": "e1e81038-d4cc-4772-a1cf-f1469e65351f",
"order": 4,
"inputTemplate": "Resumo das implementações:\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META. Execute a validação completa conforme seu system prompt:\n1. node --check em TODOS os .js\n2. Validar imports com node -e\n3. Iniciar servidor e testar health\n4. Testar TODOS os endpoints\n5. Verificar integração frontend-backend\n6. CORRIGIR qualquer problema encontrado\n7. Re-validar após correções\n\nRetorne o relatório de validação conforme seu system prompt.",
"description": "Validação e Integração",
"requiresApproval": true
}
],
"status": "active",
"created_at": "2026-02-26T23:50:38.506Z",
"updated_at": "2026-02-27T04:56:19.540Z"
},
{
"id": "3a1cddd2-adc9-48da-b0dc-103a6b96af15",
"name": "Avaliação Completa de Sistema",
"description": "Pipeline completa de avaliação com 6 especialistas: Arquitetura → Segurança → Performance → Qualidade → Infraestrutura → Consolidação. Recebe o caminho do projeto como input. Nenhum arquivo é modificado.",
"steps": [
{
"id": "d455cc61-a4e3-492f-aa73-bd24c5cd4c51",
"agentId": "0f089f30-2776-48ec-a20b-e70b97e946a6",
"order": 0,
"inputTemplate": "Analise a ARQUITETURA do projeto localizado em: {{input}}\n\nNavegue até o diretório e faça sua análise completa. Siga o formato de saída do seu system prompt. Mantenha o relatório com no máximo 3000 palavras.",
"description": "",
"requiresApproval": false
},
{
"id": "d886d627-2e1e-4a71-b1fc-b1d02cf4da76",
"agentId": "0b2b35c5-42c2-4ad5-8a4e-65370680e6f7",
"order": 1,
"inputTemplate": "Relatório anterior (Arquitetura):\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META acima. Analise a SEGURANÇA desse mesmo projeto. Use sudo se necessário. Siga o formato de saída do seu system prompt. Mantenha o relatório com no máximo 3000 palavras. Inclua a seção META com caminho e nota.",
"description": "",
"requiresApproval": false
},
{
"id": "32d5acf9-6475-4b03-a6b4-f60024165ce7",
"agentId": "8c157f2d-fb99-4ccc-9feb-f8777d00ea10",
"order": 2,
"inputTemplate": "Relatórios anteriores:\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META acima. Analise a PERFORMANCE desse mesmo projeto. Siga o formato de saída do seu system prompt. Mantenha o relatório com no máximo 3000 palavras. Inclua a seção META com caminho e nota.",
"description": "",
"requiresApproval": false
},
{
"id": "880a3c64-5897-4f63-8d7b-39f537390c7c",
"agentId": "5cbf86aa-c342-4d3e-8a73-3b4a700876d7",
"order": 3,
"inputTemplate": "Relatórios anteriores:\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META acima. Analise a QUALIDADE DO CÓDIGO desse mesmo projeto. Siga o formato de saída do seu system prompt. Mantenha o relatório com no máximo 3000 palavras. Inclua a seção META com caminho e nota.",
"description": "",
"requiresApproval": false
},
{
"id": "c8b22cf4-04b5-4411-be7d-ba1e8b15053a",
"agentId": "a6540e40-1973-4c06-8214-047e353f7202",
"order": 4,
"inputTemplate": "Relatórios anteriores:\n---\n{{input}}\n---\n\nExtraia o caminho do projeto da seção META acima. Analise a INFRAESTRUTURA desse mesmo projeto. Use sudo para inspecionar Docker, systemd, portas, etc. Siga o formato de saída do seu system prompt. Mantenha o relatório com no máximo 3000 palavras. Inclua a seção META com caminho e nota.",
"description": "",
"requiresApproval": false
},
{
"id": "00d165b1-0c07-4417-846e-c88f9d4f4386",
"agentId": "dafa71f1-4ad8-49d1-9e1d-ef8e591f537d",
"order": 5,
"inputTemplate": "Consolide TODOS os relatórios abaixo em um parecer técnico unificado:\n---\n{{input}}\n---\n\nExtraia as notas de cada seção META, calcule a nota ponderada, e gere o parecer final conforme o formato do seu system prompt.",
"description": "",
"requiresApproval": false
}
],
"status": "active",
"created_at": "2026-02-27T02:34:57.004Z",
"updated_at": "2026-02-27T16:24:21.116Z"
},
{
"id": "8119e34c-3586-4ad9-b1cd-7ac47fc32b37",
"name": "Pipeline de Desenvolvimento",
"description": "Fluxo completo: Tech Lead → PO → Desenvolvedor → QA → Code Review → DevOps",
"steps": [
{
"id": "c5bf6a17-53c0-40dc-8673-d5de044d40a7",
"agentId": "927f157c-e005-4973-ae6a-21f29fd11a0f",
"order": 0,
"inputTemplate": "Analise a demanda a seguir e produza uma ESPECIFICAÇÃO TÉCNICA detalhada para o desenvolvedor.\n\n<demanda>\n{{input}}\n</demanda>\n\n<regras>\n- Você é o PO. Seu papel é PLANEJAR, não implementar.\n- NÃO escreva código. NÃO crie arquivos. NÃO use Write, Bash ou WebSearch.\n- Apenas LEIA o codebase existente (se houver) para entender o contexto.\n</regras>\n\n<formato_obrigatorio>\n## Resumo da Demanda\n[2-3 linhas descrevendo o que foi solicitado]\n\n## Diretório do Projeto\n[caminho completo do projeto, ex: /home/fred/projetos/meu-projeto]\n\n## User Stories\n- Como [persona], quero [funcionalidade] para [benefício]\n\n## Critérios de Aceite\n- [ ] [critério específico e testável]\n\n## Requisitos Técnicos\n- [stack, padrões, restrições]\n\n## Escopo\n### Incluído\n- [o que DEVE ser feito]\n### Excluído\n- [o que NÃO deve ser feito]\n\n## Pontos de Atenção\n- [riscos, dependências, edge cases]\n</formato_obrigatorio>",
"description": "",
"requiresApproval": false
},
{
"id": "f6b9f9b4-cce6-4739-a8cd-be9420937a7d",
"agentId": "5c82ae64-4fcc-4299-ad5a-b8d55db1e951",
"order": 1,
"inputTemplate": "Implemente o código com base na especificação do Product Owner abaixo.\n\n<especificacao_po>\n{{input}}\n</especificacao_po>\n\n<instrucoes>\n- Extraia o diretório do projeto da especificação e trabalhe nele.\n- Siga os requisitos técnicos e critérios de aceite definidos pelo PO.\n- Implemente APENAS o que está no escopo.\n- Ao finalizar, produza o relatório abaixo (NÃO repita a especificação do PO).\n</instrucoes>\n\n<formato_obrigatorio>\n## Relatório de Implementação\n\n### Diretório do Projeto\n[caminho completo]\n\n### Arquivos Criados/Modificados\n| Arquivo | Ação | Descrição |\n|---------|------|-----------|\n| caminho/arquivo | criado/modificado | o que foi feito |\n\n### Decisões Técnicas\n- [decisão tomada e justificativa]\n\n### Como Testar\n- [passos para validar a implementação]\n\n### Pontos de Atenção para QA\n- [áreas que merecem teste especial]\n</formato_obrigatorio>",
"description": "",
"requiresApproval": false
},
{
"id": "a47c9037-8460-4f8f-b04f-5c5f2cc91dc0",
"agentId": "61718d7b-f118-403c-8ba0-094b5c8ba733",
"order": 2,
"inputTemplate": "Realize testes de qualidade com base no relatório de implementação do Desenvolvedor.\n\n<relatorio_desenvolvedor>\n{{input}}\n</relatorio_desenvolvedor>\n\n<instrucoes>\n- Extraia o diretório do projeto do relatório.\n- Navegue até o projeto e LEIA os arquivos mencionados.\n- Valide cada critério de aceite e cada ponto de atenção.\n- Execute testes se possível (abrir no browser, rodar scripts, etc).\n- NÃO corrija bugs — apenas DOCUMENTE-OS.\n</instrucoes>\n\n<formato_obrigatorio>\n## Relatório de QA\n\n### Diretório do Projeto\n[caminho]\n\n### Resultado Geral\n[APROVADO / APROVADO COM RESSALVAS / REPROVADO]\n\n### Bugs Encontrados\n| # | Severidade | Descrição | Arquivo | Linha |\n|---|-----------|-----------|---------|-------|\n\n### Critérios de Aceite Validados\n- [x] ou [ ] [cada critério]\n\n### Observações e Recomendações\n- [notas adicionais]\n</formato_obrigatorio>",
"description": "",
"requiresApproval": false
},
{
"id": "20c496b4-dc71-4258-994f-8aaf134dcd83",
"agentId": "3f1a5442-ffe9-461c-a8e1-2f7239a8f025",
"order": 3,
"inputTemplate": "Realize code review com base no relatório de QA e na implementação.\n\n<relatorio_qa>\n{{input}}\n</relatorio_qa>\n\n<instrucoes>\n- Extraia o diretório do projeto.\n- Navegue até o projeto e LEIA TODO o código dos arquivos mencionados.\n- Analise segurança, performance, qualidade e padrões.\n- NÃO corrija nada — apenas DOCUMENTE os problemas.\n</instrucoes>\n\n<formato_obrigatorio>\n## Code Review\n\n### Diretório do Projeto\n[caminho]\n\n### Veredicto\n[APROVADO / APROVADO COM RESSALVAS / REPROVADO]\n\n### Issues Encontradas\n| # | Severidade | Tipo | Arquivo | Descrição | Correção Sugerida |\n|---|-----------|------|---------|-----------|-------------------|\n\n### Aprovações\n- [o que está bem feito]\n\n### Correções Obrigatórias (para o Desenvolvedor)\n1. [correção específica com arquivo e o que fazer]\n2. [...]\n</formato_obrigatorio>",
"description": "",
"requiresApproval": false
},
{
"id": "c9e01b94-37db-42ad-b9f1-0205e97f7eab",
"agentId": "5c82ae64-4fcc-4299-ad5a-b8d55db1e951",
"order": 4,
"inputTemplate": "Aplique as correções apontadas pelo Code Reviewer abaixo.\n\n<code_review>\n{{input}}\n</code_review>\n\n<instrucoes>\n- Extraia o diretório do projeto e as correções obrigatórias do code review.\n- Aplique CADA correção listada na seção \"Correções Obrigatórias\".\n- Se o veredicto foi APROVADO, apenas confirme que não há pendências.\n- NÃO adicione features novas. Apenas corrija o que foi apontado.\n</instrucoes>\n\n<formato_obrigatorio>\n## Relatório de Correções\n\n### Correções Aplicadas\n| # | Issue | Arquivo | Status |\n|---|-------|---------|--------|\n| 1 | [descrição] | [arquivo] | Corrigido |\n\n### Verificação\n- [resultado dos testes após correções]\n\n### Status Final\n[Todas as correções aplicadas / Pendências restantes]\n</formato_obrigatorio>",
"description": "",
"requiresApproval": false
}
],
"status": "active",
"created_at": "2026-02-27T17:19:39.329Z",
"updated_at": "2026-02-27T20:08:42.379Z"
}
]

62
data/schedules.json Normal file
View File

@@ -0,0 +1,62 @@
[
{
"id": "8e84dadd-ef88-4188-bb6e-8bf9fee9d2af",
"agentId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"agentName": "Analista de Segurança do Sistema",
"taskDescription": "Execute uma auditoria completa de segurança do sistema operacional. Siga estas etapas:\n\n1. **Identificação do Sistema**: Colete informações sobre o kernel, distribuição, hostname e uptime.\n\n2. **Atualizações de Segurança**: Verifique pacotes desatualizados com `apt list --upgradable 2>/dev/null || dnf check-update 2>/dev/null`. Identifique quais possuem patches de segurança pendentes.\n\n3. **Portas e Serviços Expostos**: Execute `ss -tlnp` para listar todas as portas TCP em escuta. Avalie se cada serviço exposto é necessário e se está configurado de forma segura.\n\n4. **Análise de Usuários e Acessos**:\n - Verifique contas com UID 0 além do root\n - Identifique usuários sem senha ou com shells interativos desnecessários\n - Analise o histórico de logins com `last` e tentativas falhadas com `lastb` ou logs em /var/log/auth.log\n\n5. **Permissões Perigosas**:\n - Busque binários SUID/SGID com `find / -perm -4000 -type f 2>/dev/null`\n - Identifique arquivos world-writable em diretórios críticos\n - Verifique permissões de /etc/passwd, /etc/shadow, /etc/sudoers\n\n6. **Firewall**: Verifique as regras ativas com `iptables -L -n` ou `ufw status verbose`. Reporte se o firewall está inativo.\n\n7. **Configuração SSH**: Analise /etc/ssh/sshd_config verificando: PermitRootLogin, PasswordAuthentication, PermitEmptyPasswords, MaxAuthTries, AllowUsers/AllowGroups.\n\n8. **Processos e Cron Jobs**: Liste processos ativos com `ps aux` e tarefas agendadas com `crontab -l` e `ls -la /etc/cron.*`. Identifique qualquer processo ou tarefa suspeita.\n\n9. **Uso de Disco e Logs**: Verifique espaço em disco com `df -h` e o crescimento dos logs. Alerte se alguma partição estiver acima de 85%.\n\n10. **Comparação com Auditoria Anterior**: Se existirem relatórios anteriores em /home/fred/security-reports/, compare os resultados e destaque novos achados ou problemas que persistem.\n\nSalve o relatório completo em /home/fred/security-reports/audit-$(date +%Y%m%d-%H%M).md com resumo executivo, nota de segurança (0-10), achados por severidade e as 5 correções mais urgentes.\n\nIMPORTANTE: Apenas diagnostique e reporte. NÃO faça nenhuma alteração no sistema. Crie o diretório /home/fred/security-reports/ se ele não existir.",
"cronExpression": "0 13 * * *",
"active": true,
"created_at": "2026-02-26T04:05:08.137Z",
"updated_at": "2026-02-27T07:10:14.897Z"
},
{
"id": "ee6ebdb2-8fc5-4fa8-b55b-f0a2167ce5c8",
"agentId": "07cfd2ae-e6a4-4254-891b-0989ff472381",
"agentName": "Analisador de Armazenamento",
"taskDescription": "Faça uma análise completa do armazenamento do sistema e gere o relatório detalhado conforme suas instruções.",
"cronExpression": "20 13 * * *",
"active": true,
"created_at": "2026-02-26T06:34:31.668Z",
"updated_at": "2026-02-27T07:10:37.766Z"
},
{
"id": "7686b73f-61b9-4971-b1f1-8fede183165e",
"agentId": "a53e89a1-2f82-4188-91b8-142705f94f47",
"agentName": "Monitor SAE",
"taskDescription": "Executar análise diária do sistema SAE no AMBIENTE REMOTO DHT (NÃO na máquina local).\n\nREGRAS DO AMBIENTE:\n- O DHT é um ambiente remoto de homologação da CAPES. NÃO roda em Docker/containers.\n- NÃO faça nenhuma modificação. Apenas leitura e diagnóstico.\n- Use o MCP (tools de consulta Oracle via unified-db ou consulta-refactor) para inspecionar os bancos de dados.\n- Alternativamente, use SQLPlus com as credenciais disponíveis no .env do projeto.\n- Os schemas Oracle relevantes para o SAE incluem: SAE, CORPORATIVO, FINANCEIRO, SEGURANCA, PARAMETRO.\n\nO QUE INSPECIONAR:\n1. Verificar namespaces e schemas dos bancos Oracle — listar tabelas principais do schema SAE e verificar se estão acessíveis.\n2. Consultar logs de erro das últimas 24h via Graylog (streams do SAE).\n3. Verificar execução dos cron jobs CADIN — consultar as tabelas de CADIN no schema SAE para ver se houve processamento recente.\n4. Verificar estatísticas de registros nas tabelas principais (PESSOA_EVENTO, EVENTO, etc.) — comparar contagens com dias anteriores se possível.\n5. Consultar health check geral via MCP (health_check()).\n\nGerar relatório diagnóstico em Markdown. Salvar em /home/fred/agent_reports/.",
"cronExpression": "0 14 * * *",
"active": true,
"created_at": "2026-02-27T06:58:51.495Z",
"updated_at": "2026-02-27T07:08:47.070Z"
},
{
"id": "6406456d-837a-46da-b5d8-a3cce648a5e9",
"agentId": "c954e0cd-48ab-4c6c-bc38-2d360bf417ae",
"agentName": "Monitor CONSPRE",
"taskDescription": "Executar análise diária do sistema CONSPRE no AMBIENTE REMOTO DHT (NÃO na máquina local).\n\nREGRAS DO AMBIENTE:\n- O DHT é um ambiente remoto de homologação da CAPES. NÃO roda em Docker/containers.\n- NÃO faça nenhuma modificação. Apenas leitura e diagnóstico.\n- Use o MCP (tools de consulta Oracle via unified-db ou consulta-refactor) para inspecionar os bancos de dados.\n- Alternativamente, use SQLPlus com as credenciais disponíveis no .env do projeto.\n- O schema Oracle relevante é CONSPRE. As coleções SODA estão em SODA_CONSPRE (CERTIFICADOS, INSCRICOES, INSCRICOES_2, ORCID).\n\nO QUE INSPECIONAR:\n1. PRIORIDADE: Verificar se a carga diária SODA (03:15) executou com sucesso — consultar as coleções SODA_CONSPRE e verificar registros recentes (últimas 24h).\n2. Verificar namespaces e schemas dos bancos Oracle — listar tabelas do schema CONSPRE e verificar acessibilidade.\n3. Consultar logs de erro das últimas 24h via Graylog (streams do CONSPRE).\n4. Verificar contagens de registros nas coleções SODA e tabelas principais — detectar anomalias.\n5. Consultar health check geral via MCP (health_check()).\n\nGerar relatório diagnóstico em Markdown. Salvar em /home/fred/agent_reports/.",
"cronExpression": "20 14 * * *",
"active": true,
"created_at": "2026-02-27T06:58:51.561Z",
"updated_at": "2026-02-27T07:09:07.156Z"
},
{
"id": "7287ee31-9bb2-4f3a-8da4-b08085c9e9b6",
"agentId": "1568189e-92c5-4139-8f18-035eca8e3753",
"agentName": "Monitor AtuaCAPES",
"taskDescription": "Executar análise diária do sistema AtuaCAPES no AMBIENTE REMOTO DHT (NÃO na máquina local).\n\nREGRAS DO AMBIENTE:\n- O DHT é um ambiente remoto de homologação da CAPES. NÃO roda em Docker/containers.\n- NÃO faça nenhuma modificação. Apenas leitura e diagnóstico.\n- Use o MCP (tools de consulta Elasticsearch via unified-db ou consulta-refactor) para inspecionar o cluster.\n- Alternativamente, use SQLPlus com as credenciais disponíveis no .env do projeto para consultas Oracle auxiliares.\n- O Elasticsearch é o data store principal do AtuaCAPES (índice ATUACAPES com campos nested: atuacoes, bolsas).\n\nO QUE INSPECIONAR:\n1. PRIORIDADE: Verificar saúde do cluster Elasticsearch — usar get_es_mapping() e search_elasticsearch com query de health.\n2. Verificar distribuição de tipos de atuação (Projeto, Docência, Inscrição Prêmio, Emprego, Evento, Consultor) — comparar volumes com referência.\n3. Consultar logs de erro das últimas 24h via Graylog (streams do AtuaCAPES).\n4. Verificar se existem schemas Oracle auxiliares acessíveis — usar health_check().\n5. Verificar contagem total de documentos no índice e detectar anomalias de volume.\n\nGerar relatório diagnóstico em Markdown. Salvar em /home/fred/agent_reports/.",
"cronExpression": "40 14 * * *",
"active": true,
"created_at": "2026-02-27T06:58:51.660Z",
"updated_at": "2026-02-27T07:09:30.438Z"
},
{
"id": "2d92f8d0-0f82-4c29-9b48-9799f42e64d8",
"agentId": "d5ca1c47-872c-47cc-8d80-5ae5491f8207",
"agentName": "Monitor Consolidado CAPES",
"taskDescription": "Executar análise CONSOLIDADA de todos os 3 sistemas CAPES no AMBIENTE REMOTO DHT (NÃO na máquina local).\n\nREGRAS DO AMBIENTE:\n- O DHT é um ambiente remoto de homologação da CAPES. NÃO roda em Docker/containers.\n- NÃO faça nenhuma modificação. Apenas leitura e diagnóstico.\n- Use o MCP (tools de consulta Oracle/Graylog/Elasticsearch via unified-db ou consulta-refactor).\n- Alternativamente, use SQLPlus com as credenciais disponíveis no .env.\n- Comece com health_check() para visão geral de todos os serviços.\n\nSISTEMAS PARA INSPECIONAR:\n1. SAE: schemas Oracle (SAE, CORPORATIVO, FINANCEIRO, SEGURANCA, PARAMETRO). Verificar tabelas principais, cron CADIN, logs Graylog.\n2. CONSPRE: schema Oracle CONSPRE + coleções SODA (SODA_CONSPRE). Verificar carga diária SODA, registros recentes, logs Graylog.\n3. AtuaCAPES: cluster Elasticsearch (índice ATUACAPES, campos nested). Verificar saúde do cluster, volumes, logs Graylog.\n\nPARA CADA SISTEMA:\n- Verificar namespaces e acessibilidade dos schemas/índices\n- Consultar logs de erro das últimas 24h via Graylog\n- Verificar contagens e detectar anomalias de volume\n\nComparar com relatórios anteriores em /home/fred/agent_reports/ se existirem. Gerar relatório executivo unificado em Markdown com nota de saúde (0-10) para cada sistema. Salvar em /home/fred/agent_reports/.",
"cronExpression": "0 15 * * *",
"active": true,
"created_at": "2026-02-27T06:58:51.758Z",
"updated_at": "2026-02-27T07:09:45.715Z"
}
]

1
data/secrets.json Normal file
View File

@@ -0,0 +1 @@
[]

5
data/settings.json Normal file
View File

@@ -0,0 +1,5 @@
{
"defaultModel": "claude-sonnet-4-6",
"defaultWorkdir": "/home/fred/projetos",
"maxConcurrent": 5
}

218
data/tasks.json Normal file
View File

@@ -0,0 +1,218 @@
[
{
"id": "55e69e93-0923-4131-8216-fff07b48116d",
"name": "Análise de Arquitetura do Sistema",
"category": "code-review",
"description": "Analisar a arquitetura atual do sistema, identificar pontos de melhoria, gargalos de escalabilidade e propor refatorações arquiteturais. Gerar relatório com diagramas C4 e recomendações priorizadas.",
"created_at": "2026-02-26T03:28:01.818Z",
"updated_at": "2026-02-26T03:28:01.818Z"
},
{
"id": "da1372c4-8420-4377-8981-b3a113da09d4",
"name": "Criar API REST de Autenticação",
"category": "code-review",
"description": "Desenvolver endpoints de autenticação completos: registro, login, refresh token, logout, recuperação de senha. Implementar JWT com refresh tokens, rate limiting e validação de entrada.",
"created_at": "2026-02-26T03:28:01.821Z",
"updated_at": "2026-02-26T03:28:01.821Z"
},
{
"id": "dd4bcce5-4948-459b-ba2c-9a7cec2dd5df",
"name": "Desenvolver Dashboard de Monitoramento",
"category": "code-review",
"description": "Criar interface de dashboard responsiva com gráficos em tempo real, KPIs principais, filtros dinâmicos e exportação de dados. Utilizar React, TypeScript e bibliotecas de gráficos como Recharts.",
"created_at": "2026-02-26T03:28:01.828Z",
"updated_at": "2026-02-26T03:28:01.828Z"
},
{
"id": "596361ac-78f1-436a-9270-bf8302f6a4a6",
"name": "Criar Plano de Testes E2E",
"category": "tests",
"description": "Elaborar plano completo de testes end-to-end cobrindo todos os fluxos críticos do sistema. Implementar testes automatizados com Playwright, incluindo testes de regressão, smoke tests e testes de performance.",
"created_at": "2026-02-26T03:28:01.832Z",
"updated_at": "2026-02-26T03:28:01.832Z"
},
{
"id": "b98993fb-a4ab-4333-aa1a-3e2273074685",
"name": "Configurar Pipeline CI/CD",
"category": "code-review",
"description": "Configurar pipeline completo de CI/CD com GitHub Actions: build, lint, testes unitários, testes de integração, análise estática, build de Docker, deploy em staging e produção com aprovação manual.",
"created_at": "2026-02-26T03:28:01.834Z",
"updated_at": "2026-02-26T03:28:01.834Z"
},
{
"id": "bb45f1af-965c-49ba-8d0f-fc3da89e8b36",
"name": "Auditoria de Segurança do Código",
"category": "security",
"description": "Realizar auditoria completa de segurança no código-fonte: análise de vulnerabilidades OWASP Top 10, verificação de dependências (SCA), análise de secrets expostos, revisão de autenticação/autorização e compliance LGPD.",
"created_at": "2026-02-26T03:28:01.837Z",
"updated_at": "2026-02-26T03:28:01.837Z"
},
{
"id": "29f4e223-2e82-415a-8487-6a7cde118298",
"name": "Otimização de Queries SQL",
"category": "performance",
"description": "Analisar e otimizar queries SQL lentas identificadas no monitoramento. Usar EXPLAIN ANALYZE, criar índices estratégicos, reescrever queries N+1 e implementar estratégias de caching com Redis.",
"created_at": "2026-02-26T03:28:01.839Z",
"updated_at": "2026-02-26T03:28:01.839Z"
},
{
"id": "14535b01-855e-4f2a-92b6-84bef154f8cb",
"name": "Refatorar Módulo de Pagamentos",
"category": "refactor",
"description": "Refatorar o módulo de pagamentos aplicando Clean Architecture, separando camadas de domínio, aplicação e infraestrutura. Implementar Strategy Pattern para múltiplos gateways e adicionar circuit breaker.",
"created_at": "2026-02-26T03:28:01.841Z",
"updated_at": "2026-02-26T03:28:01.841Z"
},
{
"id": "c324a8dc-cf81-4e31-9b96-5e2c17af2047",
"name": "Documentar API com OpenAPI/Swagger",
"category": "docs",
"description": "Criar documentação completa da API usando especificação OpenAPI 3.0. Incluir descrições detalhadas de endpoints, schemas de request/response, exemplos, autenticação e códigos de erro.",
"created_at": "2026-02-26T03:28:01.845Z",
"updated_at": "2026-02-26T03:28:01.845Z"
},
{
"id": "020ac1a7-11db-44e6-99d5-6892cd7ee4c7",
"name": "Testes de Carga e Performance",
"category": "performance",
"description": "Executar testes de carga com K6 simulando cenários realistas: carga normal, pico, stress test e soak test. Gerar relatório com métricas de latência (p50, p95, p99), throughput e identificar bottlenecks.",
"created_at": "2026-02-26T03:28:01.847Z",
"updated_at": "2026-02-26T03:28:01.847Z"
},
{
"id": "4cfdbae6-84bd-40a8-afd6-cb660c3bc450",
"name": "Implementar Sistema de Cache",
"category": "performance",
"description": "Projetar e implementar estratégia de caching em múltiplas camadas: cache de aplicação (in-memory), Redis para cache distribuído, CDN para assets estáticos. Definir políticas de invalidação e TTL.",
"created_at": "2026-02-26T03:28:01.849Z",
"updated_at": "2026-02-26T03:28:01.849Z"
},
{
"id": "6daefc78-6d0b-40e0-997a-6f9f5e02690c",
"name": "Criar Componentes de Design System",
"category": "code-review",
"description": "Desenvolver biblioteca de componentes reutilizáveis seguindo Design System: Button, Input, Modal, Table, Card, Toast, Dropdown. Com variantes, acessibilidade WCAG AA, testes e documentação Storybook.",
"created_at": "2026-02-26T03:28:01.852Z",
"updated_at": "2026-02-26T03:28:01.852Z"
},
{
"id": "a66caa2f-d8d3-4846-ab76-2bf74c199340",
"name": "Migração de Banco de Dados",
"category": "refactor",
"description": "Planejar e executar migração de banco de dados com zero downtime. Criar scripts de migração, rollback, validação de dados e estratégia de blue-green deployment para o banco.",
"created_at": "2026-02-26T03:28:01.855Z",
"updated_at": "2026-02-26T03:28:01.855Z"
},
{
"id": "8d4b625a-4b3b-4b3d-8347-26cff931ee5a",
"name": "Implementar Observabilidade",
"category": "code-review",
"description": "Configurar stack completa de observabilidade: logs estruturados (Winston/Pino), métricas (Prometheus), tracing distribuído (OpenTelemetry), dashboards (Grafana) e alertas inteligentes.",
"created_at": "2026-02-26T03:28:01.858Z",
"updated_at": "2026-02-26T03:28:01.858Z"
},
{
"id": "58b015e9-65d9-4fe9-848d-5bc888ef6c3d",
"name": "Code Review de Pull Request",
"category": "code-review",
"description": "Realizar revisão detalhada de código em pull requests: verificar qualidade, padrões, segurança, performance, testes, documentação e aderência à arquitetura definida. Fornecer feedback construtivo.",
"created_at": "2026-02-26T03:28:01.860Z",
"updated_at": "2026-02-26T03:28:01.860Z"
},
{
"id": "fa012b57-55a8-487f-962d-2cfc6fd9ce9d",
"name": "Geração de CRUD Completo a partir de Schema de Entidade",
"category": "automation",
"description": "Receber um schema de entidade (JSON ou YAML) e gerar automaticamente a stack completa: model com validações, repository com queries otimizadas, service com regras de negócio, controller REST com tratamento de erros, rotas com middleware de autenticação, validação de entrada com Zod/Joi, testes unitários e de integração para cada camada, e migration de banco de dados. O output deve seguir rigorosamente a arquitetura e convenções do projeto-alvo, incluindo nomenclatura de arquivos, padrão de imports e estrutura de diretórios.",
"created_at": "2026-02-28T03:38:20.972Z",
"updated_at": "2026-02-28T03:38:20.972Z"
},
{
"id": "c327e65b-f217-492a-801c-efa1991ecf54",
"name": "Análise de Débito Técnico e Backlog Priorizado",
"category": "code-review",
"description": "Varrer o codebase completo utilizando métricas objetivas para identificar: code smells (funções longas, parâmetros excessivos, acoplamento alto), complexidade ciclomática acima do threshold, código duplicado com percentual de similaridade, dependências desatualizadas com CVEs conhecidos, TODO/FIXME/HACK abandonados há mais de 30 dias, módulos sem cobertura de testes, e violações de padrões arquiteturais. Gerar relatório estruturado com score de saúde do projeto (0-100), heatmap de áreas problemáticas, e backlog priorizado por matriz impacto × esforço com estimativas em story points.",
"created_at": "2026-02-28T03:38:21.032Z",
"updated_at": "2026-02-28T03:38:21.032Z"
},
{
"id": "6b90b793-7666-4d8d-b81f-d21e1a2ab25f",
"name": "Geração de Testes para Módulos Sem Cobertura",
"category": "tests",
"description": "Identificar automaticamente todos os módulos com cobertura de testes abaixo de 80%, analisar o código-fonte para compreender comportamento esperado, e gerar suíte de testes abrangente cobrindo: cenários de sucesso (happy path), edge cases e valores limítrofes, tratamento de erros e exceções, mocks/stubs de dependências externas (APIs, banco, filesystem), e testes de contrato para interfaces públicas. Usar o framework de testes do projeto (Jest/Vitest), seguir padrão AAA (Arrange-Act-Assert), e garantir que cada teste seja independente e determinístico. Output inclui os arquivos de teste e relatório de cobertura antes/depois.",
"created_at": "2026-02-28T03:38:21.092Z",
"updated_at": "2026-02-28T03:38:21.093Z"
},
{
"id": "35222595-5e4d-4c6e-9ed0-12a577987480",
"name": "Scaffolding de Microsserviço Production-Ready",
"category": "automation",
"description": "Gerar estrutura completa de um novo microsserviço pronto para produção, incluindo: Dockerfile multi-stage otimizado (build + runtime), docker-compose com dependências (banco, cache, message broker), endpoint /health com readiness e liveness checks, graceful shutdown com drain de conexões, logging estruturado em JSON (correlationId, requestId, userId), middleware global de tratamento de erros com códigos padronizados, autenticação via JWT com validação de roles, configuração 12-factor via variáveis de ambiente com validação no startup, Makefile com comandos de desenvolvimento, e README com instruções detalhadas de setup local, testes e deploy. O serviço deve seguir o template e stack tecnológica padrão da organização.",
"created_at": "2026-02-28T03:38:21.150Z",
"updated_at": "2026-02-28T03:38:21.150Z"
},
{
"id": "0e076cef-b19e-42a1-9c82-592fa496ac11",
"name": "Migração Incremental de JavaScript para TypeScript",
"category": "migration",
"description": "Executar migração incremental de módulos JavaScript para TypeScript com zero breaking changes: analisar o codebase para mapear dependências entre módulos, inferir tipos a partir de uso real e JSDoc existente, criar interfaces e types para DTOs e payloads de API, tipar parâmetros de funções e retornos com generics onde apropriado, configurar tsconfig.json com strict mode progressivo (começar permissivo e apertar gradualmente), resolver todos os erros de compilação, configurar path aliases, e atualizar scripts de build. Priorizar módulos core e compartilhados primeiro (utils, models, services), depois controllers e rotas. Cada módulo migrado deve compilar sem erros e manter 100% de compatibilidade com os consumidores existentes.",
"created_at": "2026-02-28T03:38:21.211Z",
"updated_at": "2026-02-28T03:38:21.211Z"
},
{
"id": "4f1ca1ff-098c-400f-8989-797ef7e6a73c",
"name": "Relatório de Compliance LGPD no Código-Fonte",
"category": "compliance",
"description": "Realizar varredura completa do codebase para mapear o ciclo de vida de dados pessoais: identificar campos PII (CPF, email, telefone, endereço, dados biométricos) em models, DTOs e schemas de banco; rastrear pontos de coleta (formulários, APIs, webhooks); verificar mecanismos de consentimento e base legal para cada tratamento; auditar logs e rastreamentos que possam conter dados sensíveis; verificar criptografia em trânsito (TLS) e em repouso; identificar compartilhamento com terceiros (APIs externas, analytics, SDKs); validar implementação do direito de exclusão e portabilidade. Gerar relatório RIPD (Relatório de Impacto à Proteção de Dados) com gaps identificados, classificação de risco (alto/médio/baixo), e plano de remediação priorizado com prazos sugeridos.",
"created_at": "2026-02-28T03:38:21.272Z",
"updated_at": "2026-02-28T03:38:21.272Z"
},
{
"id": "297e370a-9b96-471f-8d4a-082e2d795305",
"name": "Documentação de Onboarding para Novos Desenvolvedores",
"category": "docs",
"description": "Analisar o projeto completo e gerar documentação abrangente de onboarding: guia passo-a-passo de setup do ambiente local (dependências, variáveis de ambiente, banco de dados, serviços externos), mapa da arquitetura com diagramas C4 (contexto, container, componente), glossário de termos de domínio/negócio com exemplos, walkthrough comentado dos 5 fluxos mais críticos do sistema (da requisição HTTP até a persistência), catálogo de padrões e convenções do projeto (nomenclatura, estrutura de pastas, patterns utilizados), guia de troubleshooting com os 10 problemas mais comuns e suas soluções, e checklist de primeira semana com tarefas progressivas para o novo desenvolvedor ganhar confiança no codebase.",
"created_at": "2026-02-28T03:38:21.343Z",
"updated_at": "2026-02-28T03:38:21.343Z"
},
{
"id": "7813b9df-a1e7-4d60-b2c3-61a224d4629c",
"name": "Changelog Automatizado e Release Notes para Stakeholders",
"category": "automation",
"description": "Analisar todos os commits e PRs mergeados desde a última tag de release, classificar automaticamente por tipo (feature, bugfix, refactor, performance, docs, breaking change) usando Conventional Commits, extrair descrições significativas de cada mudança, identificar breaking changes com guia de migração, listar contribuidores e PRs associados. Gerar dois outputs: (1) CHANGELOG.md técnico seguindo o padrão Keep a Changelog com seções Added/Changed/Fixed/Removed/Security, e (2) Release Notes em linguagem acessível para stakeholders não-técnicos, destacando novas funcionalidades visíveis ao usuário, melhorias de performance com métricas quando disponíveis, e bugs corrigidos que impactavam a experiência do usuário. Incluir resumo executivo de 3-5 linhas no topo.",
"created_at": "2026-02-28T03:38:21.405Z",
"updated_at": "2026-02-28T03:38:21.405Z"
},
{
"id": "82b9aec1-9853-4819-ae2a-3a8bbf97032c",
"name": "Auditoria de Dependências e Plano de Atualização Segura",
"category": "maintenance",
"description": "Executar auditoria completa de todas as dependências diretas e transitivas do projeto: identificar vulnerabilidades conhecidas (CVEs) com severidade CVSS, pacotes deprecated ou abandonados (sem commits há 12+ meses), licenças incompatíveis com uso comercial, dependências com major versions defasadas (2+ majors atrás), pacotes duplicados em versões diferentes na árvore de dependências, e dependências com alternativas mais leves/mantidas. Gerar relatório com: inventário completo de dependências com versão atual vs mais recente, matriz de risco (probabilidade × impacto), breaking changes documentados entre versões, e plano de atualização faseado ordenado por criticidade — atualizações de segurança primeiro, depois minor/patch seguras, e por último majors com breaking changes. Cada atualização deve incluir o changelog relevante e testes de regressão sugeridos.",
"created_at": "2026-02-28T03:38:21.467Z",
"updated_at": "2026-02-28T03:38:21.467Z"
},
{
"id": "483e22eb-aa2b-4d08-8377-af50ec02037a",
"name": "Implementar API de Webhooks com Retry e Monitoramento",
"category": "code-generation",
"description": "Projetar e implementar sistema completo de webhooks outbound: endpoint de registro de webhooks com validação de URL (SSRF prevention) e secret para assinatura HMAC-SHA256, sistema de dispatch assíncrono com fila (Bull/BullMQ ou similar), política de retry com backoff exponencial (1s, 5s, 30s, 2min, 15min) e max 5 tentativas, circuit breaker por destino para evitar sobrecarga em endpoints instáveis, payload padronizado com envelope (event_type, timestamp, delivery_id, data), verificação de assinatura documentada para consumidores, endpoint de logs de entrega com status (pending, delivered, failed, exhausted) e latência, dashboard de monitoramento com taxa de sucesso por webhook e alertas para falhas recorrentes. Incluir testes de integração simulando cenários de falha (timeout, 5xx, rede) e documentação da API para integradores externos.",
"created_at": "2026-02-28T03:38:21.530Z",
"updated_at": "2026-02-28T03:38:21.530Z"
},
{
"id": "b98c3d99-1e05-4c3e-b392-49e047b1789a",
"name": "Implementar Feature Flags com Rollout Gradual",
"category": "automation",
"description": "Projetar e implementar sistema de feature flags completo para deploys seguros: criar módulo server-side com avaliação de flags por contexto (usuário, ambiente, percentual, segmento), SDK client-side leve (<5KB) com cache local e fallback offline, estratégias de rollout configuráveis (canary 1%→5%→25%→100%, por tenant, por região, por role), toggle kill-switch para desabilitar features instantaneamente sem redeploy, persistência de configurações em banco com versionamento e audit log de alterações, API REST para gestão das flags com controle de acesso, e integração com stack de observabilidade para correlacionar métricas de negócio com ativação de flags. Incluir testes para cada estratégia de rollout e documentação com exemplos de uso para a equipe.",
"created_at": "2026-02-28T03:38:40.215Z",
"updated_at": "2026-02-28T03:38:40.215Z"
},
{
"id": "92fafb45-d568-47e8-85da-f7f14a930a9c",
"name": "Revisão e Hardening de Configurações de Infraestrutura",
"category": "security",
"description": "Auditar e fortalecer todas as configurações de infraestrutura como código (IaC) do projeto: analisar Dockerfiles verificando imagem base (não usar latest, preferir Alpine/distroless), usuário não-root, multi-stage build, .dockerignore adequado, e scan de vulnerabilidades na imagem; revisar docker-compose validando rede isolada, limites de recursos (CPU/memória), volumes com permissões mínimas, e secrets não hardcoded; auditar CI/CD pipelines verificando permissões de tokens (princípio do menor privilégio), secrets management, pinning de actions/versões, e SAST/DAST integrados; verificar variáveis de ambiente sensíveis (senhas, tokens, chaves de API) garantindo que não estejam em código, logs ou imagens; e validar configurações de CORS, CSP, HSTS e demais headers de segurança. Gerar relatório com findings categorizados por severidade (crítico/alto/médio/baixo), evidência do problema, impacto potencial, e fix sugerido com código.",
"created_at": "2026-02-28T03:38:40.288Z",
"updated_at": "2026-02-28T03:38:40.288Z"
}
]

14
data/webhooks.json Normal file
View File

@@ -0,0 +1,14 @@
[
{
"id": "b5b42ba4-e811-4f34-b399-a47267b60b31",
"name": "Web Hook de Segurança",
"targetType": "agent",
"targetId": "d026be9f-0b16-492e-9e3c-833f839f6d72",
"token": "069e296d1b756b99003d69e0cc9a48ec051f7f0f2b901e4d",
"active": true,
"lastTriggeredAt": "2026-02-27T01:48:07.754Z",
"triggerCount": 2,
"created_at": "2026-02-27T01:29:21.901Z",
"updated_at": "2026-02-27T01:48:07.754Z"
}
]

View File

@@ -72,6 +72,18 @@
<span>Histórico</span> <span>Histórico</span>
</a> </a>
</li> </li>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="import">
<i data-lucide="upload-cloud"></i>
<span>Importar</span>
</a>
</li>
<li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="files">
<i data-lucide="folder-open"></i>
<span>Projetos</span>
</a>
</li>
<li class="sidebar-nav-item"> <li class="sidebar-nav-item">
<a href="#" class="sidebar-nav-link" data-section="settings"> <a href="#" class="sidebar-nav-link" data-section="settings">
<i data-lucide="settings"></i> <i data-lucide="settings"></i>
@@ -474,6 +486,10 @@
<span class="terminal-title">Output de Execução</span> <span class="terminal-title">Output de Execução</span>
</div> </div>
<div class="terminal-toolbar-right"> <div class="terminal-toolbar-right">
<div class="terminal-timer" id="terminal-timer" hidden>
<i data-lucide="clock" style="width:14px;height:14px"></i>
<span id="terminal-timer-value">00:00</span>
</div>
<select class="select select--sm" id="terminal-execution-select" aria-label="Selecionar execução"> <select class="select select--sm" id="terminal-execution-select" aria-label="Selecionar execução">
<option value="">Selecionar execução...</option> <option value="">Selecionar execução...</option>
</select> </select>
@@ -578,6 +594,14 @@
<div id="history-pagination"></div> <div id="history-pagination"></div>
</section> </section>
<section id="import" class="section" aria-label="Importar Projeto" hidden>
<div id="import-container"></div>
</section>
<section id="files" class="section" aria-label="Projetos" hidden>
<div id="files-container"></div>
</section>
<section id="settings" class="section" aria-label="Configurações" hidden> <section id="settings" class="section" aria-label="Configurações" hidden>
<div class="settings-grid"> <div class="settings-grid">
<div class="card"> <div class="card">
@@ -798,7 +822,7 @@
class="input" class="input"
id="agent-workdir" id="agent-workdir"
name="workdir" name="workdir"
placeholder="/home/fred/projetos" value="/home/projetos/"
autocomplete="off" autocomplete="off"
/> />
</div> </div>
@@ -842,6 +866,16 @@
</div> </div>
</div> </div>
<div class="form-row">
<div class="form-group">
<label class="form-label" for="agent-delegate-to">Delegar para (auto)</label>
<select class="select" id="agent-delegate-to" name="delegateTo">
<option value="">Nenhum</option>
</select>
<p class="form-hint">Ao concluir, delega automaticamente o resultado para este agente.</p>
</div>
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label class="form-label">Retry em caso de falha</label> <label class="form-label">Retry em caso de falha</label>
@@ -978,6 +1012,30 @@
></textarea> ></textarea>
</div> </div>
<div class="form-group">
<label class="form-label" for="execute-repo">Repositório Git</label>
<div class="repo-selector">
<select class="select" id="execute-repo">
<option value="">Nenhum (usar diretório manual)</option>
</select>
<select class="select" id="execute-repo-branch" style="display:none">
<option value="">Branch padrão</option>
</select>
</div>
<p class="form-hint">Se selecionado, o agente trabalha no repositório e faz commit/push automático ao finalizar.</p>
</div>
<div class="form-group" id="execute-workdir-group">
<label class="form-label" for="execute-workdir">Diretório de Trabalho</label>
<input
type="text"
class="input"
id="execute-workdir"
value="/home/projetos/"
autocomplete="off"
/>
</div>
<div class="form-group"> <div class="form-group">
<label class="form-label">Arquivos de Contexto</label> <label class="form-label">Arquivos de Contexto</label>
<div class="dropzone" id="execute-dropzone"> <div class="dropzone" id="execute-dropzone">
@@ -1201,6 +1259,19 @@
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="form-label" for="pipeline-execute-repo">Repositório Git</label>
<div class="repo-selector">
<select class="select" id="pipeline-execute-repo">
<option value="">Nenhum (usar diretório manual)</option>
</select>
<select class="select" id="pipeline-execute-repo-branch" style="display:none">
<option value="">Branch padrão</option>
</select>
</div>
<p class="form-hint">Se selecionado, todos os agentes trabalham no repositório e o commit/push é automático ao final.</p>
</div>
<div class="form-group" id="pipeline-execute-workdir-group">
<label class="form-label" for="pipeline-execute-workdir">Diretório de Trabalho (opcional)</label> <label class="form-label" for="pipeline-execute-workdir">Diretório de Trabalho (opcional)</label>
<input <input
type="text" type="text"
@@ -1256,6 +1327,25 @@
</div> </div>
</div> </div>
<div class="modal-overlay" id="prompt-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="prompt-modal-title" hidden>
<div class="modal modal--sm">
<div class="modal-header">
<h2 class="modal-title" id="prompt-modal-title">Prompt</h2>
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="prompt-modal-overlay">
<i data-lucide="x"></i>
</button>
</div>
<div class="modal-body">
<p id="prompt-modal-message"></p>
<input type="text" class="form-input" id="prompt-modal-input" autocomplete="off">
</div>
<div class="modal-footer">
<button class="btn btn--ghost" type="button" id="prompt-modal-cancel-btn">Cancelar</button>
<button class="btn btn--primary" type="button" id="prompt-modal-confirm-btn">Confirmar</button>
</div>
</div>
</div>
<div class="modal-overlay" id="export-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="export-modal-title" hidden> <div class="modal-overlay" id="export-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="export-modal-title" hidden>
<div class="modal modal--md"> <div class="modal modal--md">
<div class="modal-header"> <div class="modal-header">
@@ -1377,6 +1467,8 @@
<script src="js/components/history.js"></script> <script src="js/components/history.js"></script>
<script src="js/components/webhooks.js"></script> <script src="js/components/webhooks.js"></script>
<script src="js/components/notifications.js"></script> <script src="js/components/notifications.js"></script>
<script src="js/components/files.js"></script>
<script src="js/components/import.js"></script>
<script src="js/app.js"></script> <script src="js/app.js"></script>
<script> <script>
Utils.refreshIcons(); Utils.refreshIcons();

View File

@@ -966,15 +966,22 @@ textarea {
transform: scale(1) translateY(0); transform: scale(1) translateY(0);
} }
.modal-sm { .modal-sm,
.modal--sm {
max-width: 400px; max-width: 400px;
} }
.modal-lg { .modal--md {
max-width: 700px;
}
.modal-lg,
.modal--lg {
max-width: 800px; max-width: 800px;
} }
.modal-xl { .modal-xl,
.modal--xl {
max-width: 1000px; max-width: 1000px;
} }
@@ -1088,29 +1095,32 @@ textarea {
flex: 1; flex: 1;
padding: 16px; padding: 16px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden;
font-family: 'JetBrains Mono', monospace; font-family: 'JetBrains Mono', monospace;
font-size: 13px; font-size: 13px;
line-height: 1.7; line-height: 1.7;
min-width: 0;
} }
.terminal-line { .terminal-line {
padding: 2px 0; padding: 2px 0;
display: flex; display: grid;
align-items: flex-start; grid-template-columns: auto 1fr;
gap: 12px;
} }
.terminal-line .timestamp { .terminal-line .timestamp {
color: var(--text-muted); color: var(--text-muted);
margin-right: 12px;
flex-shrink: 0;
font-size: 12px; font-size: 12px;
padding-top: 1px; padding-top: 1px;
white-space: nowrap;
} }
.terminal-line .content { .terminal-line .content {
color: #c8c8d8; color: #c8c8d8;
word-break: break-all; word-break: break-word;
flex: 1; overflow-wrap: anywhere;
white-space: pre-wrap;
} }
.terminal-line.error .content { .terminal-line.error .content {
@@ -2671,25 +2681,39 @@ tbody tr:hover td {
.terminal-toolbar { .terminal-toolbar {
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
padding: 10px 16px; padding: 12px 16px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-bottom: 1px solid var(--border-primary); border-bottom: 1px solid var(--border-primary);
flex-shrink: 0; flex-shrink: 0;
gap: 12px; gap: 16px;
} }
.terminal-toolbar-left { .terminal-toolbar-left {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 10px;
} }
.terminal-toolbar-right { .terminal-toolbar-right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 12px;
}
.terminal-timer {
display: flex;
align-items: center;
gap: 6px;
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
font-weight: 500;
color: var(--success);
background: rgba(34, 197, 94, 0.1);
padding: 4px 10px;
border-radius: 6px;
border: 1px solid rgba(34, 197, 94, 0.2);
} }
.terminal-dot--red { .terminal-dot--red {
@@ -4479,13 +4503,15 @@ body, .sidebar, .header, .card, .modal-content, .input, .select, textarea, .metr
.terminal-action-toolbar { .terminal-action-toolbar {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
padding: 0.5rem 0.75rem; padding: 0.5rem 1rem;
background: var(--bg-tertiary); background: var(--bg-tertiary);
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-bottom: none; border-bottom: none;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
gap: 12px;
} }
.terminal-toolbar-left, .terminal-toolbar-right { display: flex; align-items: center; gap: 0.25rem; } .terminal-action-toolbar .terminal-toolbar-left,
.terminal-action-toolbar .terminal-toolbar-right { display: flex; align-items: center; gap: 0.5rem; }
.terminal-toggle-label { .terminal-toggle-label {
display: flex; align-items: center; gap: 0.375rem; display: flex; align-items: center; gap: 0.375rem;
font-size: 0.75rem; color: var(--text-secondary); cursor: pointer; user-select: none; font-size: 0.75rem; color: var(--text-secondary); cursor: pointer; user-select: none;
@@ -5166,3 +5192,319 @@ body, .sidebar, .header, .card, .modal-content, .input, .select, textarea, .metr
padding-top: 16px; padding-top: 16px;
border-top: 1px solid var(--border-primary); border-top: 1px solid var(--border-primary);
} }
/* File Explorer */
#files-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.files-breadcrumb {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
padding: 12px 16px;
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 8px;
font-size: 13px;
}
.files-breadcrumb-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--accent);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.files-breadcrumb-link:hover {
color: var(--accent-hover);
}
.files-breadcrumb-link:last-child {
color: var(--text-primary);
pointer-events: none;
}
.files-breadcrumb-sep {
color: var(--text-muted);
user-select: none;
}
.files-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
}
.files-count {
font-size: 13px;
color: var(--text-muted);
}
.files-table-wrapper {
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 8px;
overflow: hidden;
}
.files-table {
width: 100%;
border-collapse: collapse;
}
.files-table thead {
background: var(--bg-tertiary);
}
.files-table th {
text-align: left;
padding: 10px 16px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
border-bottom: 1px solid var(--border-primary);
}
.files-table td {
padding: 10px 16px;
font-size: 13px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-primary);
}
.files-row:last-child td {
border-bottom: none;
}
.files-row:hover {
background: var(--bg-card-hover);
}
.files-td-name {
color: var(--text-primary);
}
.files-th-size,
.files-td-size {
width: 100px;
text-align: right;
}
.files-th-date,
.files-td-date {
width: 160px;
}
.files-th-actions,
.files-td-actions {
width: 120px;
text-align: right;
}
.files-td-actions {
display: flex;
gap: 4px;
justify-content: flex-end;
align-items: center;
}
.repo-selector { display: flex; gap: 8px; }
.repo-selector .select { flex: 1; }
.btn-commit-push { color: var(--warning); }
.btn-commit-push:hover { background: rgba(245, 158, 11, 0.1); }
.btn-publish { color: var(--success); }
.btn-publish:hover { background: rgba(16, 185, 129, 0.1); }
.publish-result { display: flex; flex-direction: column; gap: 12px; padding: 8px 0; }
.publish-result-item { font-size: 14px; }
.publish-result-item a { color: var(--accent); text-decoration: underline; }
.files-entry-link {
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
color: inherit;
}
.files-entry-dir {
color: var(--text-primary);
font-weight: 500;
cursor: pointer;
transition: color 0.2s;
}
.files-entry-dir:hover {
color: var(--accent);
}
.files-entry-file {
color: var(--text-secondary);
}
.files-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 80px 20px;
background: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: 8px;
color: var(--text-muted);
font-size: 14px;
}
@media (max-width: 768px) {
.files-th-date,
.files-td-date {
display: none;
}
.files-th-size,
.files-td-size {
width: 70px;
}
}
.import-layout {
display: flex;
flex-direction: column;
gap: 24px;
}
.import-card .card-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.import-desc {
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
}
.import-desc code {
background: var(--bg-tertiary);
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
.import-path-row {
display: flex;
gap: 8px;
align-items: center;
}
.import-folder-display {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--bg-input);
border: 1px solid var(--border-primary);
border-radius: 6px;
font-size: 13px;
min-height: 40px;
}
.import-preview {
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 12px 16px;
}
.import-preview-stats {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.import-stat {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-primary);
}
.import-stat--muted {
color: var(--text-muted);
font-size: 12px;
}
.import-repos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.import-repo-card {
background: var(--bg-primary);
border: 1px solid var(--border-primary);
border-radius: 8px;
padding: 14px;
transition: border-color 0.2s, background 0.2s;
}
.import-repo-card:hover {
border-color: var(--accent);
background: var(--bg-card-hover);
}
.import-repo-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.import-repo-name {
color: var(--accent);
text-decoration: none;
font-weight: 600;
font-size: 14px;
}
.import-repo-name:hover {
text-decoration: underline;
}
.import-repo-desc {
color: var(--text-secondary);
font-size: 12px;
margin-bottom: 8px;
line-height: 1.4;
}
.import-repo-meta {
display: flex;
gap: 12px;
color: var(--text-muted);
font-size: 11px;
}
.import-repo-meta span {
display: flex;
align-items: center;
gap: 4px;
}
.spin {
animation: spin 1s linear infinite;
}

View File

@@ -38,8 +38,10 @@ const API = {
create(data) { return API.request('POST', '/agents', data); }, create(data) { return API.request('POST', '/agents', data); },
update(id, data) { return API.request('PUT', `/agents/${id}`, data); }, update(id, data) { return API.request('PUT', `/agents/${id}`, data); },
delete(id) { return API.request('DELETE', `/agents/${id}`); }, delete(id) { return API.request('DELETE', `/agents/${id}`); },
execute(id, task, instructions, contextFiles) { execute(id, task, instructions, contextFiles, workingDirectory, repoName, repoBranch) {
const body = { task, instructions }; const body = { task, instructions };
if (repoName) { body.repoName = repoName; if (repoBranch) body.repoBranch = repoBranch; }
else if (workingDirectory) body.workingDirectory = workingDirectory;
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles; if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
return API.request('POST', `/agents/${id}/execute`, body); return API.request('POST', `/agents/${id}/execute`, body);
}, },
@@ -82,9 +84,10 @@ const API = {
create(data) { return API.request('POST', '/pipelines', data); }, create(data) { return API.request('POST', '/pipelines', data); },
update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); }, update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); },
delete(id) { return API.request('DELETE', `/pipelines/${id}`); }, delete(id) { return API.request('DELETE', `/pipelines/${id}`); },
execute(id, input, workingDirectory, contextFiles) { execute(id, input, workingDirectory, contextFiles, repoName, repoBranch) {
const body = { input }; const body = { input };
if (workingDirectory) body.workingDirectory = workingDirectory; if (repoName) { body.repoName = repoName; if (repoBranch) body.repoBranch = repoBranch; }
else if (workingDirectory) body.workingDirectory = workingDirectory;
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles; if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
return API.request('POST', `/pipelines/${id}/execute`, body); return API.request('POST', `/pipelines/${id}/execute`, body);
}, },
@@ -141,6 +144,37 @@ const API = {
}, },
}, },
repos: {
list() { return API.request('GET', '/repos'); },
branches(name) { return API.request('GET', `/repos/${encodeURIComponent(name)}/branches`); },
},
projects: {
browse(path) { return API.request('GET', `/browse?path=${encodeURIComponent(path || '/home')}`); },
importLocal(sourcePath, repoName) { return API.request('POST', '/projects/import', { sourcePath, repoName }); },
async upload(files, paths, repoName) {
const form = new FormData();
form.append('repoName', repoName);
form.append('paths', JSON.stringify(paths));
for (const f of files) form.append('files', f);
const response = await fetch('/api/projects/upload', {
method: 'POST',
headers: { 'X-Client-Id': API.clientId },
body: form,
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'Erro no upload');
return data;
},
},
files: {
list(path) { return API.request('GET', `/files${path ? '?path=' + encodeURIComponent(path) : ''}`); },
delete(path) { return API.request('DELETE', `/files?path=${encodeURIComponent(path)}`); },
publish(path) { return API.request('POST', '/files/publish', { path }); },
commitPush(path, message) { return API.request('POST', '/files/commit-push', { path, message }); },
},
reports: { reports: {
list() { return API.request('GET', '/reports'); }, list() { return API.request('GET', '/reports'); },
get(filename) { return API.request('GET', `/reports/${encodeURIComponent(filename)}`); }, get(filename) { return API.request('GET', `/reports/${encodeURIComponent(filename)}`); },

View File

@@ -17,10 +17,12 @@ const App = {
webhooks: 'Webhooks', webhooks: 'Webhooks',
terminal: 'Terminal', terminal: 'Terminal',
history: 'Histórico', history: 'Histórico',
import: 'Importar Projeto',
files: 'Projetos',
settings: 'Configurações', settings: 'Configurações',
}, },
sections: ['dashboard', 'agents', 'tasks', 'schedules', 'pipelines', 'webhooks', 'terminal', 'history', 'settings'], sections: ['dashboard', 'agents', 'tasks', 'schedules', 'pipelines', 'webhooks', 'terminal', 'history', 'import', 'files', 'settings'],
init() { init() {
if (App._initialized) return; if (App._initialized) return;
@@ -36,6 +38,9 @@ const App = {
App._executeDropzone = Utils.initDropzone('execute-dropzone', 'execute-files', 'execute-file-list'); App._executeDropzone = Utils.initDropzone('execute-dropzone', 'execute-files', 'execute-file-list');
App._pipelineDropzone = Utils.initDropzone('pipeline-execute-dropzone', 'pipeline-execute-files', 'pipeline-execute-file-list'); App._pipelineDropzone = Utils.initDropzone('pipeline-execute-dropzone', 'pipeline-execute-files', 'pipeline-execute-file-list');
App._initRepoSelectors();
Terminal.restoreIfActive();
const initialSection = location.hash.replace('#', '') || 'dashboard'; const initialSection = location.hash.replace('#', '') || 'dashboard';
App.navigateTo(App.sections.includes(initialSection) ? initialSection : 'dashboard'); App.navigateTo(App.sections.includes(initialSection) ? initialSection : 'dashboard');
@@ -85,6 +90,8 @@ const App = {
history.pushState(null, '', `#${section}`); history.pushState(null, '', `#${section}`);
} }
if (typeof FlowEditor !== 'undefined') FlowEditor._teardown();
document.querySelectorAll('.section').forEach((el) => { document.querySelectorAll('.section').forEach((el) => {
const isActive = el.id === section; const isActive = el.id === section;
el.classList.toggle('active', isActive); el.classList.toggle('active', isActive);
@@ -113,6 +120,8 @@ const App = {
case 'pipelines': await PipelinesUI.load(); break; case 'pipelines': await PipelinesUI.load(); break;
case 'webhooks': await WebhooksUI.load(); break; case 'webhooks': await WebhooksUI.load(); break;
case 'history': await HistoryUI.load(); break; case 'history': await HistoryUI.load(); break;
case 'import': await ImportUI.load(); break;
case 'files': await FilesUI.load(); break;
case 'settings': await SettingsUI.load(); break; case 'settings': await SettingsUI.load(); break;
} }
} catch (err) { } catch (err) {
@@ -230,6 +239,7 @@ const App = {
Toast.success('Execução concluída'); Toast.success('Execução concluída');
App.refreshCurrentSection(); App.refreshCurrentSection();
App._updateActiveBadge(); App._updateActiveBadge();
App._checkStopTimer();
break; break;
} }
@@ -244,6 +254,7 @@ const App = {
Toast.error(`Erro na execução: ${data.data?.error || 'desconhecido'}`); Toast.error(`Erro na execução: ${data.data?.error || 'desconhecido'}`);
App._updateActiveBadge(); App._updateActiveBadge();
App._checkStopTimer();
break; break;
case 'execution_retry': case 'execution_retry':
@@ -289,6 +300,7 @@ const App = {
case 'pipeline_complete': case 'pipeline_complete':
Terminal.stopProcessing(); Terminal.stopProcessing();
Terminal._hideTimer();
Terminal.addLine('Pipeline concluído com sucesso.', 'success'); Terminal.addLine('Pipeline concluído com sucesso.', 'success');
if (data.lastSessionId && data.lastAgentId) { if (data.lastSessionId && data.lastAgentId) {
Terminal.enableChat(data.lastAgentId, data.lastAgentName || 'Agente', data.lastSessionId); Terminal.enableChat(data.lastAgentId, data.lastAgentName || 'Agente', data.lastSessionId);
@@ -299,6 +311,7 @@ const App = {
case 'pipeline_error': case 'pipeline_error':
Terminal.stopProcessing(); Terminal.stopProcessing();
Terminal._hideTimer();
Terminal.addLine(`Erro no passo ${data.stepIndex + 1}: ${data.error}`, 'error'); Terminal.addLine(`Erro no passo ${data.stepIndex + 1}: ${data.error}`, 'error');
Toast.error('Erro no pipeline'); Toast.error('Erro no pipeline');
break; break;
@@ -763,6 +776,21 @@ const App = {
} }
}); });
document.getElementById('files-container')?.addEventListener('click', (e) => {
const el = e.target.closest('[data-action]');
if (!el) return;
e.preventDefault();
const { action, path } = el.dataset;
switch (action) {
case 'navigate-files': FilesUI.navigate(path || ''); break;
case 'download-file': FilesUI.downloadFile(path); break;
case 'download-folder': FilesUI.downloadFolder(path); break;
case 'commit-push': FilesUI.commitPush(path); break;
case 'publish-project': FilesUI.publishProject(path); break;
case 'delete-entry': FilesUI.deleteEntry(path, el.dataset.entryType); break;
}
});
document.getElementById('pipeline-steps-container')?.addEventListener('click', (e) => { document.getElementById('pipeline-steps-container')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-step-action]'); const btn = e.target.closest('[data-step-action]');
if (!btn) return; if (!btn) return;
@@ -844,6 +872,61 @@ const App = {
}); });
}, },
_reposCache: null,
async _loadRepos(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
try {
if (!App._reposCache) App._reposCache = await API.repos.list();
const current = select.value;
select.innerHTML = '<option value="">Nenhum (usar diretório manual)</option>';
App._reposCache.forEach(r => {
select.insertAdjacentHTML('beforeend',
`<option value="${Utils.escapeHtml(r.name)}">${Utils.escapeHtml(r.name)}${r.description ? ' — ' + Utils.escapeHtml(r.description.slice(0, 40)) : ''}</option>`
);
});
if (current) select.value = current;
} catch { }
},
_initRepoSelectors() {
const pairs = [
['execute-repo', 'execute-repo-branch', 'execute-workdir-group'],
['pipeline-execute-repo', 'pipeline-execute-repo-branch', 'pipeline-execute-workdir-group'],
];
pairs.forEach(([repoId, branchId, workdirGroupId]) => {
const repoSelect = document.getElementById(repoId);
const branchSelect = document.getElementById(branchId);
const workdirGroup = document.getElementById(workdirGroupId);
if (!repoSelect) return;
repoSelect.addEventListener('change', async () => {
const repoName = repoSelect.value;
if (repoName) {
if (workdirGroup) workdirGroup.style.display = 'none';
if (branchSelect) {
branchSelect.style.display = '';
branchSelect.innerHTML = '<option value="">Branch padrão</option>';
try {
const branches = await API.repos.branches(repoName);
branches.forEach(b => {
branchSelect.insertAdjacentHTML('beforeend', `<option value="${Utils.escapeHtml(b)}">${Utils.escapeHtml(b)}</option>`);
});
} catch { }
}
} else {
if (workdirGroup) workdirGroup.style.display = '';
if (branchSelect) branchSelect.style.display = 'none';
}
});
repoSelect.addEventListener('focus', () => {
if (repoSelect.options.length <= 1) App._loadRepos(repoId);
});
});
},
async _handleExecute() { async _handleExecute() {
const agentId = document.getElementById('execute-agent-select')?.value const agentId = document.getElementById('execute-agent-select')?.value
|| document.getElementById('execute-agent-id')?.value; || document.getElementById('execute-agent-id')?.value;
@@ -860,6 +943,9 @@ const App = {
} }
const instructions = document.getElementById('execute-instructions')?.value.trim() || ''; const instructions = document.getElementById('execute-instructions')?.value.trim() || '';
const workingDirectory = document.getElementById('execute-workdir')?.value.trim() || '';
const repoName = document.getElementById('execute-repo')?.value || '';
const repoBranch = document.getElementById('execute-repo-branch')?.value || '';
try { try {
const selectEl = document.getElementById('execute-agent-select'); const selectEl = document.getElementById('execute-agent-select');
@@ -876,7 +962,7 @@ const App = {
Terminal.disableChat(); Terminal.disableChat();
App._lastAgentName = agentName; App._lastAgentName = agentName;
await API.agents.execute(agentId, task, instructions, contextFiles); await API.agents.execute(agentId, task, instructions, contextFiles, workingDirectory, repoName, repoBranch);
if (dropzone) dropzone.reset(); if (dropzone) dropzone.reset();
Modal.close('execute-modal-overlay'); Modal.close('execute-modal-overlay');
@@ -997,6 +1083,15 @@ const App = {
} }
}, },
async _checkStopTimer() {
try {
const active = await API.system.activeExecutions();
if (!Array.isArray(active) || active.length === 0) {
Terminal._hideTimer();
}
} catch {}
},
startPeriodicRefresh() { startPeriodicRefresh() {
setInterval(async () => { setInterval(async () => {
await App._updateActiveBadge(); await App._updateActiveBadge();

View File

@@ -119,7 +119,7 @@ const AgentsUI = {
<div class="agent-meta"> <div class="agent-meta">
<span class="agent-meta-item"> <span class="agent-meta-item">
<i data-lucide="cpu"></i> <i data-lucide="cpu"></i>
${model} ${Utils.escapeHtml(model)}
</span> </span>
<span class="agent-meta-item"> <span class="agent-meta-item">
<i data-lucide="clock"></i> <i data-lucide="clock"></i>
@@ -180,6 +180,9 @@ const AgentsUI = {
const maxTurns = document.getElementById('agent-max-turns'); const maxTurns = document.getElementById('agent-max-turns');
if (maxTurns) maxTurns.value = '0'; if (maxTurns) maxTurns.value = '0';
const workdir = document.getElementById('agent-workdir');
if (workdir) workdir.value = '/home/projetos/';
const permissionMode = document.getElementById('agent-permission-mode'); const permissionMode = document.getElementById('agent-permission-mode');
if (permissionMode) permissionMode.value = ''; if (permissionMode) permissionMode.value = '';
@@ -192,6 +195,8 @@ const AgentsUI = {
const retryMax = document.getElementById('agent-retry-max'); const retryMax = document.getElementById('agent-retry-max');
if (retryMax) retryMax.value = '3'; if (retryMax) retryMax.value = '3';
AgentsUI._populateDelegateSelect('');
const secretsSection = document.getElementById('agent-secrets-section'); const secretsSection = document.getElementById('agent-secrets-section');
if (secretsSection) secretsSection.hidden = true; if (secretsSection) secretsSection.hidden = true;
@@ -236,7 +241,7 @@ const AgentsUI = {
const tagsChips = document.getElementById('agent-tags-chips'); const tagsChips = document.getElementById('agent-tags-chips');
if (tagsChips) { if (tagsChips) {
tagsChips.innerHTML = tags.map((t) => tagsChips.innerHTML = tags.map((t) =>
`<span class="tag-chip">${t}<button type="button" data-tag="${t}" class="tag-remove" aria-label="Remover tag ${t}">×</button></span>` `<span class="tag-chip">${Utils.escapeHtml(t)}<button type="button" data-tag="${Utils.escapeHtml(t)}" class="tag-remove" aria-label="Remover tag ${Utils.escapeHtml(t)}">×</button></span>`
).join(''); ).join('');
} }
@@ -250,6 +255,8 @@ const AgentsUI = {
const retryMax = document.getElementById('agent-retry-max'); const retryMax = document.getElementById('agent-retry-max');
if (retryMax) retryMax.value = (agent.config && agent.config.maxRetries) || '3'; if (retryMax) retryMax.value = (agent.config && agent.config.maxRetries) || '3';
AgentsUI._populateDelegateSelect(agent.config?.delegateTo || '', agent.id);
const secretsSection = document.getElementById('agent-secrets-section'); const secretsSection = document.getElementById('agent-secrets-section');
if (secretsSection) secretsSection.hidden = false; if (secretsSection) secretsSection.hidden = false;
@@ -296,6 +303,7 @@ const AgentsUI = {
permissionMode: document.getElementById('agent-permission-mode')?.value || '', permissionMode: document.getElementById('agent-permission-mode')?.value || '',
retryOnFailure: !!document.getElementById('agent-retry-toggle')?.checked, retryOnFailure: !!document.getElementById('agent-retry-toggle')?.checked,
maxRetries: parseInt(document.getElementById('agent-retry-max')?.value) || 3, maxRetries: parseInt(document.getElementById('agent-retry-max')?.value) || 3,
delegateTo: document.getElementById('agent-delegate-to')?.value || '',
}, },
}; };
@@ -358,8 +366,19 @@ const AgentsUI = {
if (App._executeDropzone) App._executeDropzone.reset(); if (App._executeDropzone) App._executeDropzone.reset();
const selectedAgent = allAgents.find(a => a.id === agentId);
const workdirEl = document.getElementById('execute-workdir');
if (workdirEl) {
workdirEl.value = (selectedAgent?.config?.workingDirectory) || '/home/projetos/';
}
AgentsUI._loadSavedTasks(); AgentsUI._loadSavedTasks();
const repoSelect = document.getElementById('execute-repo');
if (repoSelect) { repoSelect.value = ''; repoSelect.dispatchEvent(new Event('change')); }
App._reposCache = null;
App._loadRepos('execute-repo');
Modal.open('execute-modal-overlay'); Modal.open('execute-modal-overlay');
} catch (err) { } catch (err) {
Toast.error(`Erro ao abrir modal de execução: ${err.message}`); Toast.error(`Erro ao abrir modal de execução: ${err.message}`);
@@ -468,6 +487,14 @@ const AgentsUI = {
}); });
}, },
_populateDelegateSelect(currentValue, excludeId) {
const select = document.getElementById('agent-delegate-to');
if (!select) return;
const activeAgents = AgentsUI.agents.filter(a => a.status === 'active' && a.id !== excludeId);
select.innerHTML = '<option value="">Nenhum</option>' +
activeAgents.map(a => `<option value="${a.id}" ${a.id === currentValue ? 'selected' : ''}>${Utils.escapeHtml(a.agent_name || a.name)}</option>`).join('');
},
_setupModalListeners() { _setupModalListeners() {
const retryToggle = document.getElementById('agent-retry-toggle'); const retryToggle = document.getElementById('agent-retry-toggle');
const retryMaxGroup = document.getElementById('agent-retry-max-group'); const retryMaxGroup = document.getElementById('agent-retry-max-group');

View File

@@ -0,0 +1,236 @@
const FilesUI = {
currentPath: '',
async load() {
await FilesUI.navigate('');
},
async navigate(path) {
try {
const data = await API.files.list(path);
FilesUI.currentPath = data.path || '';
FilesUI.render(data);
} catch (err) {
Toast.error(`Erro ao carregar arquivos: ${err.message}`);
}
},
render(data) {
const container = document.getElementById('files-container');
if (!container) return;
const breadcrumb = FilesUI._renderBreadcrumb(data.path);
const entries = data.entries || [];
if (entries.length === 0) {
container.innerHTML = `
${breadcrumb}
<div class="files-empty">
<i data-lucide="folder-open" style="width:48px;height:48px;color:var(--text-muted)"></i>
<p>Nenhum arquivo encontrado neste diretório</p>
</div>
`;
Utils.refreshIcons(container);
return;
}
const rows = entries.map(entry => FilesUI._renderRow(entry, data.path)).join('');
container.innerHTML = `
${breadcrumb}
<div class="files-toolbar">
<span class="files-count">${entries.length} ${entries.length === 1 ? 'item' : 'itens'}</span>
<button class="btn btn--ghost btn--sm" data-action="download-folder" data-path="${Utils.escapeHtml(data.path || '')}" title="Baixar pasta como .tar.gz"><i data-lucide="download" style="width:14px;height:14px"></i> Baixar tudo</button>
</div>
<div class="files-table-wrapper">
<table class="files-table">
<thead>
<tr>
<th class="files-th-name">Nome</th>
<th class="files-th-size">Tamanho</th>
<th class="files-th-date">Modificado</th>
<th class="files-th-actions"></th>
</tr>
</thead>
<tbody>
${rows}
</tbody>
</table>
</div>
`;
Utils.refreshIcons(container);
},
_renderBreadcrumb(currentPath) {
const parts = currentPath ? currentPath.split('/').filter(Boolean) : [];
let html = `<nav class="files-breadcrumb"><a href="#" data-action="navigate-files" data-path="" class="files-breadcrumb-link"><i data-lucide="home" style="width:14px;height:14px"></i> projetos</a>`;
let accumulated = '';
for (const part of parts) {
accumulated += (accumulated ? '/' : '') + part;
html += ` <span class="files-breadcrumb-sep">/</span> <a href="#" data-action="navigate-files" data-path="${Utils.escapeHtml(accumulated)}" class="files-breadcrumb-link">${Utils.escapeHtml(part)}</a>`;
}
html += '</nav>';
return html;
},
_renderRow(entry, currentPath) {
const fullPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
const icon = entry.type === 'directory' ? 'folder' : FilesUI._fileIcon(entry.extension);
const iconColor = entry.type === 'directory' ? 'var(--warning)' : 'var(--text-muted)';
const size = entry.type === 'directory' ? '—' : FilesUI._formatSize(entry.size);
const date = FilesUI._formatDate(entry.modified);
const nameCell = entry.type === 'directory'
? `<a href="#" class="files-entry-link files-entry-dir" data-action="navigate-files" data-path="${Utils.escapeHtml(fullPath)}"><i data-lucide="${icon}" style="width:16px;height:16px;color:${iconColor};flex-shrink:0"></i> ${Utils.escapeHtml(entry.name)}</a>`
: `<span class="files-entry-link files-entry-file"><i data-lucide="${icon}" style="width:16px;height:16px;color:${iconColor};flex-shrink:0"></i> ${Utils.escapeHtml(entry.name)}</span>`;
const downloadBtn = entry.type === 'directory'
? `<button class="btn btn--ghost btn--sm" data-action="download-folder" data-path="${Utils.escapeHtml(fullPath)}" title="Baixar pasta"><i data-lucide="download" style="width:14px;height:14px"></i></button>`
: `<button class="btn btn--ghost btn--sm" data-action="download-file" data-path="${Utils.escapeHtml(fullPath)}" title="Baixar arquivo"><i data-lucide="download" style="width:14px;height:14px"></i></button>`;
const isRootDir = entry.type === 'directory' && !currentPath;
const commitPushBtn = isRootDir
? `<button class="btn btn--ghost btn--sm btn-commit-push" data-action="commit-push" data-path="${Utils.escapeHtml(fullPath)}" title="Commit & Push para o Gitea"><i data-lucide="git-commit-horizontal" style="width:14px;height:14px"></i></button>`
: '';
const publishBtn = isRootDir
? `<button class="btn btn--ghost btn--sm btn-publish" data-action="publish-project" data-path="${Utils.escapeHtml(fullPath)}" title="Publicar projeto"><i data-lucide="rocket" style="width:14px;height:14px"></i></button>`
: '';
const deleteBtn = `<button class="btn btn--ghost btn--sm btn-danger" data-action="delete-entry" data-path="${Utils.escapeHtml(fullPath)}" data-entry-type="${entry.type}" title="Excluir"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button>`;
const actions = `${downloadBtn}${commitPushBtn}${publishBtn}${deleteBtn}`;
return `
<tr class="files-row">
<td class="files-td-name">${nameCell}</td>
<td class="files-td-size">${size}</td>
<td class="files-td-date">${date}</td>
<td class="files-td-actions">${actions}</td>
</tr>
`;
},
_fileIcon(ext) {
const map = {
js: 'file-code-2', ts: 'file-code-2', jsx: 'file-code-2', tsx: 'file-code-2',
py: 'file-code-2', rb: 'file-code-2', go: 'file-code-2', rs: 'file-code-2',
java: 'file-code-2', c: 'file-code-2', cpp: 'file-code-2', h: 'file-code-2',
html: 'file-code-2', css: 'file-code-2', scss: 'file-code-2', vue: 'file-code-2',
json: 'file-json', xml: 'file-json', yaml: 'file-json', yml: 'file-json',
md: 'file-text', txt: 'file-text', log: 'file-text', csv: 'file-text',
pdf: 'file-text',
png: 'file-image', jpg: 'file-image', jpeg: 'file-image', gif: 'file-image',
svg: 'file-image', webp: 'file-image', ico: 'file-image',
zip: 'file-archive', tar: 'file-archive', gz: 'file-archive', rar: 'file-archive',
sh: 'file-terminal', bash: 'file-terminal',
sql: 'database',
env: 'file-lock',
};
return map[ext] || 'file';
},
_formatSize(bytes) {
if (bytes == null) return '—';
if (bytes === 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i];
},
_formatDate(isoString) {
if (!isoString) return '—';
const d = new Date(isoString);
return d.toLocaleDateString('pt-BR') + ' ' + d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
},
downloadFile(path) {
const a = document.createElement('a');
a.href = `/api/files/download?path=${encodeURIComponent(path)}`;
a.download = '';
a.click();
},
downloadFolder(path) {
const a = document.createElement('a');
a.href = `/api/files/download-folder?path=${encodeURIComponent(path)}`;
a.download = '';
a.click();
},
async publishProject(path) {
const name = path.split('/').pop();
const confirmed = await Modal.confirm(
'Publicar projeto',
`Isso irá criar o repositório "${name}" no Gitea, fazer push dos arquivos e publicar em <strong>${name}.nitro-cloud.duckdns.org</strong>. Continuar?`
);
if (!confirmed) return;
try {
Toast.info('Publicando projeto... isso pode levar alguns segundos');
const result = await API.files.publish(path);
Toast.success(`Projeto publicado com sucesso!`);
const modal = document.getElementById('execution-detail-modal-overlay');
const title = document.getElementById('execution-detail-title');
const content = document.getElementById('execution-detail-content');
if (modal && title && content) {
title.textContent = 'Projeto Publicado';
content.innerHTML = `
<div class="publish-result">
<div class="publish-result-item"><strong>Repositório:</strong> <a href="${Utils.escapeHtml(result.repoUrl)}" target="_blank">${Utils.escapeHtml(result.repoUrl)}</a></div>
<div class="publish-result-item"><strong>Site:</strong> <a href="${Utils.escapeHtml(result.siteUrl)}" target="_blank">${Utils.escapeHtml(result.siteUrl)}</a></div>
<div class="publish-result-item"><strong>Status:</strong> <span class="badge badge-active">${Utils.escapeHtml(result.status)}</span></div>
${result.message ? `<div class="publish-result-item"><em>${Utils.escapeHtml(result.message)}</em></div>` : ''}
</div>`;
Modal.open('execution-detail-modal-overlay');
}
await FilesUI.navigate(FilesUI.currentPath);
} catch (err) {
Toast.error(`Erro ao publicar: ${err.message}`);
}
},
async commitPush(path) {
const name = path.split('/').pop();
const message = await Modal.prompt(
'Commit & Push',
`Mensagem do commit para <strong>${name}</strong>:`,
`Atualização - ${new Date().toLocaleDateString('pt-BR')} ${new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}`
);
if (message === null) return;
try {
Toast.info('Realizando commit e push...');
const result = await API.files.commitPush(path, message || undefined);
if (result.status === 'clean') {
Toast.info(result.message);
} else {
Toast.success(`${result.changes} arquivo(s) enviados ao Gitea`);
}
} catch (err) {
Toast.error(`Erro no commit/push: ${err.message}`);
}
},
async deleteEntry(path, entryType) {
const label = entryType === 'directory' ? 'pasta' : 'arquivo';
const name = path.split('/').pop();
const confirmed = await Modal.confirm(
`Excluir ${label}`,
`Tem certeza que deseja excluir "${name}"? Esta ação não pode ser desfeita.`
);
if (!confirmed) return;
try {
await API.files.delete(path);
Toast.success(`${label.charAt(0).toUpperCase() + label.slice(1)} excluído`);
await FilesUI.navigate(FilesUI.currentPath);
} catch (err) {
Toast.error(`Erro ao excluir: ${err.message}`);
}
},
};
window.FilesUI = FilesUI;

View File

@@ -737,8 +737,12 @@ const FlowEditor = {
if (!leave) return; if (!leave) return;
} }
FlowEditor._teardown();
},
_teardown() {
const overlay = FlowEditor._overlay; const overlay = FlowEditor._overlay;
if (!overlay) return; if (!overlay || overlay.hidden) return;
overlay.classList.remove('active'); overlay.classList.remove('active');
setTimeout(() => { overlay.hidden = true; }, 200); setTimeout(() => { overlay.hidden = true; }, 200);
@@ -755,6 +759,7 @@ const FlowEditor = {
FlowEditor._selectedNode = null; FlowEditor._selectedNode = null;
FlowEditor._dragState = null; FlowEditor._dragState = null;
FlowEditor._panStart = null; FlowEditor._panStart = null;
FlowEditor._dirty = false;
}, },
}; };

View File

@@ -0,0 +1,309 @@
const ImportUI = {
_selectedFiles: [],
_selectedPaths: [],
_folderName: '',
_importing: false,
_excludedDirs: ['.git', 'node_modules', '__pycache__', '.next', '.nuxt', 'venv', '.venv', '.cache', '.parcel-cache', 'dist', 'build', '.output', '.svelte-kit', 'vendor', 'target', '.gradle', '.idea', '.vs', 'coverage', '.nyc_output'],
_excludedFiles: ['.git', '.DS_Store', 'Thumbs.db', 'desktop.ini', '*.pyc', '*.pyo', '*.class', '*.o', '*.so', '*.dll'],
async load() {
const container = document.getElementById('import-container');
if (!container) return;
let repos = [];
try { repos = await API.repos.list(); } catch {}
container.innerHTML = `
<div class="import-layout">
<div class="card import-card">
<div class="card-header">
<h2 class="card-title"><i data-lucide="upload-cloud" style="width:20px;height:20px"></i> Importar Projeto</h2>
</div>
<div class="card-body">
<p class="import-desc">Selecione uma pasta do seu computador para enviar ao Gitea. Arquivos ignorados pelo <code>.gitignore</code> e pastas como <code>node_modules</code> serão filtrados automaticamente.</p>
<input type="file" id="import-folder-input" webkitdirectory directory multiple hidden />
<div class="form-group">
<label class="form-label">Pasta do projeto</label>
<div class="import-path-row">
<div class="import-folder-display" id="import-folder-display">
<i data-lucide="folder-open" style="width:18px;height:18px;color:var(--text-muted)"></i>
<span class="text-muted">Nenhuma pasta selecionada</span>
</div>
<button class="btn btn--primary btn--sm" id="import-select-btn" type="button">
<i data-lucide="folder-search" style="width:16px;height:16px"></i> Selecionar Pasta
</button>
</div>
</div>
<div id="import-preview" class="import-preview" hidden></div>
<div class="form-group">
<label class="form-label">Nome do repositório no Gitea</label>
<input type="text" class="form-input" id="import-repo-name" placeholder="meu-projeto" />
<span class="form-hint">Letras minúsculas, números e hífens</span>
</div>
<button class="btn btn--primary" id="import-submit-btn" type="button" disabled>
<i data-lucide="upload-cloud" style="width:16px;height:16px"></i> Importar para o Gitea
</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title"><i data-lucide="git-branch" style="width:20px;height:20px"></i> Repositórios no Gitea</h2>
<span class="badge badge--accent">${repos.length}</span>
</div>
<div class="card-body">
${repos.length === 0 ? '<p class="text-muted">Nenhum repositório encontrado</p>' : ''}
<div class="import-repos-grid">
${repos.map(r => ImportUI._renderRepoCard(r)).join('')}
</div>
</div>
</div>
</div>
`;
Utils.refreshIcons(container);
ImportUI._bindEvents();
},
_renderRepoCard(repo) {
const domain = 'nitro-cloud.duckdns.org';
const repoUrl = `https://git.${domain}/${repo.full_name || repo.name}`;
const updated = repo.updated_at ? new Date(repo.updated_at).toLocaleDateString('pt-BR') : '';
const size = repo.size ? ImportUI._fmtSize(repo.size * 1024) : '';
return `
<div class="import-repo-card">
<div class="import-repo-header">
<i data-lucide="git-branch" style="width:16px;height:16px;color:var(--accent)"></i>
<a href="${Utils.escapeHtml(repoUrl)}" target="_blank" class="import-repo-name">${Utils.escapeHtml(repo.name)}</a>
</div>
${repo.description ? `<p class="import-repo-desc">${Utils.escapeHtml(repo.description)}</p>` : ''}
<div class="import-repo-meta">
${updated ? `<span><i data-lucide="calendar" style="width:12px;height:12px"></i> ${updated}</span>` : ''}
${size ? `<span><i data-lucide="hard-drive" style="width:12px;height:12px"></i> ${size}</span>` : ''}
${repo.default_branch ? `<span><i data-lucide="git-commit" style="width:12px;height:12px"></i> ${Utils.escapeHtml(repo.default_branch)}</span>` : ''}
</div>
</div>
`;
},
_fmtSize(bytes) {
if (!bytes) return '';
const units = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i];
},
_bindEvents() {
const selectBtn = document.getElementById('import-select-btn');
const folderInput = document.getElementById('import-folder-input');
const submitBtn = document.getElementById('import-submit-btn');
if (selectBtn && folderInput) {
selectBtn.addEventListener('click', () => folderInput.click());
folderInput.addEventListener('change', () => ImportUI._onFolderSelected(folderInput.files));
}
if (submitBtn) {
submitBtn.addEventListener('click', () => ImportUI._doUpload());
}
},
_shouldExclude(relativePath) {
const parts = relativePath.split('/');
for (const part of parts.slice(0, -1)) {
if (ImportUI._excludedDirs.includes(part)) return true;
}
const fileName = parts[parts.length - 1];
for (const pattern of ImportUI._excludedFiles) {
if (pattern.startsWith('*.')) {
if (fileName.endsWith(pattern.slice(1))) return true;
} else {
if (fileName === pattern) return true;
}
}
return false;
},
_parseGitignore(content) {
const patterns = [];
for (const raw of content.split('\n')) {
const line = raw.trim();
if (!line || line.startsWith('#') || line.startsWith('!')) continue;
patterns.push(line.replace(/\/$/, ''));
}
return patterns;
},
_matchesGitignore(relativePath, patterns) {
const parts = relativePath.split('/');
for (const pattern of patterns) {
if (pattern.includes('/')) {
if (relativePath.startsWith(pattern + '/') || relativePath === pattern) return true;
} else if (pattern.startsWith('*.')) {
const ext = pattern.slice(1);
if (relativePath.endsWith(ext)) return true;
} else {
for (const part of parts) {
if (part === pattern) return true;
}
}
}
return false;
},
_onFolderSelected(fileList) {
if (!fileList || fileList.length === 0) return;
const allFiles = Array.from(fileList);
const firstPath = allFiles[0].webkitRelativePath || '';
ImportUI._folderName = firstPath.split('/')[0] || 'projeto';
let gitignorePatterns = [];
const gitignoreFile = allFiles.find(f => {
const rel = f.webkitRelativePath || '';
const parts = rel.split('/');
return parts.length === 2 && parts[1] === '.gitignore';
});
if (gitignoreFile) {
const reader = new FileReader();
reader.onload = (e) => {
gitignorePatterns = ImportUI._parseGitignore(e.target.result);
ImportUI._applyFilter(allFiles, gitignorePatterns);
};
reader.readAsText(gitignoreFile);
} else {
ImportUI._applyFilter(allFiles, []);
}
},
_applyFilter(allFiles, gitignorePatterns) {
const filtered = [];
const paths = [];
let totalSize = 0;
let excluded = 0;
for (const file of allFiles) {
const fullRel = file.webkitRelativePath || file.name;
const relWithoutRoot = fullRel.split('/').slice(1).join('/');
if (!relWithoutRoot) continue;
if (ImportUI._shouldExclude(relWithoutRoot)) { excluded++; continue; }
if (gitignorePatterns.length > 0 && ImportUI._matchesGitignore(relWithoutRoot, gitignorePatterns)) { excluded++; continue; }
filtered.push(file);
paths.push(fullRel);
totalSize += file.size;
}
ImportUI._selectedFiles = filtered;
ImportUI._selectedPaths = paths;
const display = document.getElementById('import-folder-display');
if (display) {
display.innerHTML = `
<i data-lucide="folder" style="width:18px;height:18px;color:var(--warning)"></i>
<strong>${Utils.escapeHtml(ImportUI._folderName)}</strong>
`;
Utils.refreshIcons(display);
}
const preview = document.getElementById('import-preview');
if (preview) {
preview.hidden = false;
preview.innerHTML = `
<div class="import-preview-stats">
<div class="import-stat">
<i data-lucide="file" style="width:16px;height:16px"></i>
<span><strong>${filtered.length}</strong> arquivos selecionados</span>
</div>
<div class="import-stat">
<i data-lucide="hard-drive" style="width:16px;height:16px"></i>
<span><strong>${ImportUI._fmtSize(totalSize)}</strong> total</span>
</div>
${excluded > 0 ? `<div class="import-stat import-stat--muted">
<i data-lucide="eye-off" style="width:16px;height:16px"></i>
<span>${excluded} arquivos ignorados (.gitignore / node_modules / etc.)</span>
</div>` : ''}
</div>
`;
Utils.refreshIcons(preview);
}
const nameInput = document.getElementById('import-repo-name');
if (nameInput && !nameInput.value.trim()) {
nameInput.value = ImportUI._folderName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
}
const submitBtn = document.getElementById('import-submit-btn');
if (submitBtn) submitBtn.disabled = filtered.length === 0;
},
async _doUpload() {
if (ImportUI._importing) return;
if (ImportUI._selectedFiles.length === 0) { Toast.warning('Selecione uma pasta primeiro'); return; }
const nameInput = document.getElementById('import-repo-name');
const submitBtn = document.getElementById('import-submit-btn');
const repoName = (nameInput?.value || '').trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
if (!repoName) { Toast.warning('Informe o nome do repositório'); return; }
ImportUI._importing = true;
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px" class="spin"></i> Enviando...';
Utils.refreshIcons(submitBtn);
}
try {
Toast.info(`Enviando ${ImportUI._selectedFiles.length} arquivos...`);
const result = await API.projects.upload(ImportUI._selectedFiles, ImportUI._selectedPaths, repoName);
Toast.success('Projeto importado com sucesso!');
const modal = document.getElementById('execution-detail-modal-overlay');
const title = document.getElementById('execution-detail-title');
const content = document.getElementById('execution-detail-content');
if (modal && title && content) {
title.textContent = 'Projeto Importado';
content.innerHTML = `
<div class="publish-result">
<div class="publish-result-item"><strong>Repositório:</strong> <a href="${Utils.escapeHtml(result.repoUrl)}" target="_blank">${Utils.escapeHtml(result.repoUrl)}</a></div>
<div class="publish-result-item"><strong>Diretório:</strong> <code>${Utils.escapeHtml(result.projectDir)}</code></div>
<div class="publish-result-item"><strong>Status:</strong> <span class="badge badge-active">${Utils.escapeHtml(result.status)}</span></div>
${result.message ? `<div class="publish-result-item"><em>${Utils.escapeHtml(result.message)}</em></div>` : ''}
<div class="publish-result-steps">
<strong>Passos:</strong>
<ul>${(result.steps || []).map(s => `<li>${Utils.escapeHtml(s)}</li>`).join('')}</ul>
</div>
</div>`;
Modal.open('execution-detail-modal-overlay');
}
ImportUI._selectedFiles = [];
ImportUI._selectedPaths = [];
ImportUI._folderName = '';
if (nameInput) nameInput.value = '';
App._reposCache = null;
await ImportUI.load();
} catch (err) {
Toast.error(`Erro ao importar: ${err.message}`);
} finally {
ImportUI._importing = false;
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = '<i data-lucide="upload-cloud" style="width:16px;height:16px"></i> Importar para o Gitea';
Utils.refreshIcons(submitBtn);
}
}
},
};
window.ImportUI = ImportUI;

View File

@@ -58,6 +58,33 @@ const Modal = {
} }
}, },
_promptResolve: null,
prompt(title, message, defaultValue = '') {
return new Promise((resolve) => {
Modal._promptResolve = resolve;
const titleEl = document.getElementById('prompt-modal-title');
const messageEl = document.getElementById('prompt-modal-message');
const inputEl = document.getElementById('prompt-modal-input');
if (titleEl) titleEl.textContent = title;
if (messageEl) messageEl.textContent = message;
if (inputEl) inputEl.value = defaultValue;
Modal.open('prompt-modal-overlay');
});
},
_resolvePrompt(result) {
const inputEl = document.getElementById('prompt-modal-input');
Modal.close('prompt-modal-overlay');
if (Modal._promptResolve) {
Modal._promptResolve(result ? (inputEl?.value || '') : null);
Modal._promptResolve = null;
}
},
_setupListeners() { _setupListeners() {
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (e.target.classList.contains('modal-overlay')) { if (e.target.classList.contains('modal-overlay')) {
@@ -65,6 +92,8 @@ const Modal = {
if (modalId === 'confirm-modal-overlay') { if (modalId === 'confirm-modal-overlay') {
Modal._resolveConfirm(false); Modal._resolveConfirm(false);
} else if (modalId === 'prompt-modal-overlay') {
Modal._resolvePrompt(false);
} else { } else {
Modal.close(modalId); Modal.close(modalId);
} }
@@ -77,6 +106,8 @@ const Modal = {
if (targetId === 'confirm-modal-overlay') { if (targetId === 'confirm-modal-overlay') {
Modal._resolveConfirm(false); Modal._resolveConfirm(false);
} else if (targetId === 'prompt-modal-overlay') {
Modal._resolvePrompt(false);
} else { } else {
Modal.close(targetId); Modal.close(targetId);
} }
@@ -91,6 +122,8 @@ const Modal = {
if (activeModal.id === 'confirm-modal-overlay') { if (activeModal.id === 'confirm-modal-overlay') {
Modal._resolveConfirm(false); Modal._resolveConfirm(false);
} else if (activeModal.id === 'prompt-modal-overlay') {
Modal._resolvePrompt(false);
} else { } else {
Modal.close(activeModal.id); Modal.close(activeModal.id);
} }
@@ -98,6 +131,17 @@ const Modal = {
const confirmBtn = document.getElementById('confirm-modal-confirm-btn'); const confirmBtn = document.getElementById('confirm-modal-confirm-btn');
if (confirmBtn) confirmBtn.addEventListener('click', () => Modal._resolveConfirm(true)); if (confirmBtn) confirmBtn.addEventListener('click', () => Modal._resolveConfirm(true));
const promptConfirmBtn = document.getElementById('prompt-modal-confirm-btn');
if (promptConfirmBtn) promptConfirmBtn.addEventListener('click', () => Modal._resolvePrompt(true));
const promptCancelBtn = document.getElementById('prompt-modal-cancel-btn');
if (promptCancelBtn) promptCancelBtn.addEventListener('click', () => Modal._resolvePrompt(false));
const promptInput = document.getElementById('prompt-modal-input');
if (promptInput) promptInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') Modal._resolvePrompt(true);
});
}, },
}; };

View File

@@ -476,6 +476,11 @@ const PipelinesUI = {
if (App._pipelineDropzone) App._pipelineDropzone.reset(); if (App._pipelineDropzone) App._pipelineDropzone.reset();
const repoSelect = document.getElementById('pipeline-execute-repo');
if (repoSelect) { repoSelect.value = ''; repoSelect.dispatchEvent(new Event('change')); }
App._reposCache = null;
App._loadRepos('pipeline-execute-repo');
Modal.open('pipeline-execute-modal-overlay'); Modal.open('pipeline-execute-modal-overlay');
}, },
@@ -503,7 +508,9 @@ const PipelinesUI = {
contextFiles = uploadResult.files; contextFiles = uploadResult.files;
} }
await API.pipelines.execute(pipelineId, input, workingDirectory, contextFiles); const repoName = document.getElementById('pipeline-execute-repo')?.value || '';
const repoBranch = document.getElementById('pipeline-execute-repo-branch')?.value || '';
await API.pipelines.execute(pipelineId, input, workingDirectory, contextFiles, repoName, repoBranch);
if (dropzone) dropzone.reset(); if (dropzone) dropzone.reset();
Modal.close('pipeline-execute-modal-overlay'); Modal.close('pipeline-execute-modal-overlay');
App.navigateTo('terminal'); App.navigateTo('terminal');

View File

@@ -47,7 +47,7 @@ const SchedulesUI = {
<td>${Utils.escapeHtml(schedule.agentName || '—')}</td> <td>${Utils.escapeHtml(schedule.agentName || '—')}</td>
<td class="schedule-task-cell" title="${Utils.escapeHtml(schedule.taskDescription || '')}">${Utils.escapeHtml(schedule.taskDescription || '—')}</td> <td class="schedule-task-cell" title="${Utils.escapeHtml(schedule.taskDescription || '')}">${Utils.escapeHtml(schedule.taskDescription || '—')}</td>
<td> <td>
<code class="font-mono">${cronExpr}</code> <code class="font-mono">${Utils.escapeHtml(cronExpr)}</code>
</td> </td>
<td>${nextRun}</td> <td>${nextRun}</td>
<td><span class="badge ${statusClass}">${statusLabel}</span></td> <td><span class="badge ${statusClass}">${statusLabel}</span></td>

View File

@@ -8,9 +8,79 @@ const Terminal = {
searchMatches: [], searchMatches: [],
searchIndex: -1, searchIndex: -1,
_toolbarInitialized: false, _toolbarInitialized: false,
_storageKey: 'terminal_lines',
_chatStorageKey: 'terminal_chat',
_timerInterval: null,
_timerStart: null,
_timerStorageKey: 'terminal_timer_start',
_saveToStorage() {
try {
const data = JSON.stringify(Terminal.lines.slice(-Terminal.maxLines));
sessionStorage.setItem(Terminal._storageKey, data);
} catch {}
},
_restoreFromStorage() {
try {
const data = sessionStorage.getItem(Terminal._storageKey);
if (data) {
Terminal.lines = JSON.parse(data);
return true;
}
} catch {}
return false;
},
_clearStorage() {
try {
sessionStorage.removeItem(Terminal._storageKey);
sessionStorage.removeItem(Terminal._chatStorageKey);
} catch {}
},
async restoreIfActive() {
try {
const active = await API.system.activeExecutions();
const hasActive = Array.isArray(active) && active.length > 0;
if (hasActive) {
const exec = active[0];
const serverBuffer = Array.isArray(exec.outputBuffer) ? exec.outputBuffer : [];
if (serverBuffer.length > 0) {
Terminal.lines = serverBuffer.map((item) => {
const time = new Date();
return {
content: item.content || '',
type: item.type || 'default',
timestamp: time.toTimeString().slice(0, 8),
executionId: exec.executionId,
};
});
Terminal._saveToStorage();
} else {
Terminal._restoreFromStorage();
}
Terminal.render();
const startedAt = exec.startedAt ? new Date(exec.startedAt).getTime() : null;
const savedStart = sessionStorage.getItem(Terminal._timerStorageKey);
Terminal._startTimer(savedStart ? Number(savedStart) : startedAt);
Terminal.startProcessing(exec.agentConfig?.agent_name || 'Agente');
try {
const chatData = sessionStorage.getItem(Terminal._chatStorageKey);
if (chatData) Terminal._chatSession = JSON.parse(chatData);
} catch {}
} else {
Terminal._clearStorage();
Terminal._hideTimer();
}
} catch {}
},
enableChat(agentId, agentName, sessionId) { enableChat(agentId, agentName, sessionId) {
Terminal._chatSession = { agentId, agentName, sessionId }; Terminal._chatSession = { agentId, agentName, sessionId };
try { sessionStorage.setItem(Terminal._chatStorageKey, JSON.stringify(Terminal._chatSession)); } catch {}
const bar = document.getElementById('terminal-input-bar'); const bar = document.getElementById('terminal-input-bar');
const ctx = document.getElementById('terminal-input-context'); const ctx = document.getElementById('terminal-input-context');
const input = document.getElementById('terminal-input'); const input = document.getElementById('terminal-input');
@@ -21,6 +91,7 @@ const Terminal = {
disableChat() { disableChat() {
Terminal._chatSession = null; Terminal._chatSession = null;
try { sessionStorage.removeItem(Terminal._chatStorageKey); } catch {}
const bar = document.getElementById('terminal-input-bar'); const bar = document.getElementById('terminal-input-bar');
if (bar) bar.hidden = true; if (bar) bar.hidden = true;
}, },
@@ -43,13 +114,55 @@ const Terminal = {
Terminal.lines.shift(); Terminal.lines.shift();
} }
Terminal._saveToStorage();
Terminal.render(); Terminal.render();
}, },
_startTimer(fromTimestamp) {
Terminal._stopTimer();
Terminal._timerStart = fromTimestamp || Date.now();
try { sessionStorage.setItem(Terminal._timerStorageKey, String(Terminal._timerStart)); } catch {}
const timerEl = document.getElementById('terminal-timer');
const valueEl = document.getElementById('terminal-timer-value');
if (timerEl) timerEl.hidden = false;
const tick = () => {
if (!valueEl) return;
const elapsed = Math.floor((Date.now() - Terminal._timerStart) / 1000);
const h = Math.floor(elapsed / 3600);
const m = Math.floor((elapsed % 3600) / 60);
const s = elapsed % 60;
valueEl.textContent = h > 0
? `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
: `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
};
tick();
Terminal._timerInterval = setInterval(tick, 1000);
},
_stopTimer() {
if (Terminal._timerInterval) {
clearInterval(Terminal._timerInterval);
Terminal._timerInterval = null;
}
try { sessionStorage.removeItem(Terminal._timerStorageKey); } catch {}
},
_hideTimer() {
Terminal._stopTimer();
const timerEl = document.getElementById('terminal-timer');
if (timerEl) timerEl.hidden = true;
},
startProcessing(agentName) { startProcessing(agentName) {
Terminal.stopProcessing(); Terminal.stopProcessing();
Terminal.addLine(`Agente "${agentName}" processando tarefa...`, 'system'); Terminal.addLine(`Agente "${agentName}" processando tarefa...`, 'system');
if (!Terminal._timerInterval) {
Terminal._startTimer();
}
let dots = 0; let dots = 0;
Terminal._processingInterval = setInterval(() => { Terminal._processingInterval = setInterval(() => {
dots = (dots + 1) % 4; dots = (dots + 1) % 4;
@@ -71,8 +184,10 @@ const Terminal = {
clear() { clear() {
Terminal.stopProcessing(); Terminal.stopProcessing();
Terminal._hideTimer();
Terminal.lines = []; Terminal.lines = [];
Terminal.executionFilter = null; Terminal.executionFilter = null;
Terminal._clearStorage();
Terminal.render(); Terminal.render();
}, },

View File

@@ -24,7 +24,7 @@ const Toast = {
toast.innerHTML = ` toast.innerHTML = `
<span class="toast-icon" data-lucide="${iconName}"></span> <span class="toast-icon" data-lucide="${iconName}"></span>
<span class="toast-message">${message}</span> <span class="toast-message">${Utils.escapeHtml(message)}</span>
<button class="toast-close" aria-label="Fechar notificação"> <button class="toast-close" aria-label="Fechar notificação">
<i data-lucide="x"></i> <i data-lucide="x"></i>
</button> </button>

View File

@@ -12,6 +12,7 @@ import apiRouter, { setWsBroadcast, setWsBroadcastTo, hookRouter } from './src/r
import * as manager from './src/agents/manager.js'; import * as manager from './src/agents/manager.js';
import { setGlobalBroadcast } from './src/agents/manager.js'; import { setGlobalBroadcast } from './src/agents/manager.js';
import { cancelAllExecutions } from './src/agents/executor.js'; import { cancelAllExecutions } from './src/agents/executor.js';
import { stopAll as stopAllSchedules } from './src/agents/scheduler.js';
import { flushAllStores } from './src/store/db.js'; import { flushAllStores } from './src/store/db.js';
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -111,16 +112,18 @@ app.use(express.json({
verify: (req, res, buf) => { req.rawBody = buf || Buffer.alloc(0); }, verify: (req, res, buf) => { req.rawBody = buf || Buffer.alloc(0); },
})); }));
app.use('/hook', hookLimiter, verifyWebhookSignature, hookRouter); app.use('/hook', hookLimiter, verifyWebhookSignature, hookRouter);
// Serve o app diretamente na raiz — sem landing page intermediária
app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'public', 'app.html'));
});
app.use(express.static(join(__dirname, 'public'), { app.use(express.static(join(__dirname, 'public'), {
etag: true, etag: true,
setHeaders(res, filePath) { setHeaders(res, filePath) {
if (filePath.endsWith('.html')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache');
res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0');
res.setHeader('Expires', '0');
} else {
res.setHeader('Cache-Control', 'public, max-age=3600');
}
}, },
})); }));
app.use('/api', apiRouter); app.use('/api', apiRouter);
@@ -177,6 +180,9 @@ setGlobalBroadcast(broadcast);
function gracefulShutdown(signal) { function gracefulShutdown(signal) {
console.log(`\nSinal ${signal} recebido. Encerrando servidor...`); console.log(`\nSinal ${signal} recebido. Encerrando servidor...`);
stopAllSchedules();
console.log('Agendamentos parados.');
cancelAllExecutions(); cancelAllExecutions();
console.log('Execuções ativas canceladas.'); console.log('Execuções ativas canceladas.');
@@ -185,15 +191,23 @@ function gracefulShutdown(signal) {
clearInterval(wsHeartbeat); clearInterval(wsHeartbeat);
httpServer.close(() => { for (const client of wss.clients) {
console.log('Servidor HTTP encerrado.'); client.close(1001, 'Servidor encerrando');
process.exit(0); }
connectedClients.clear();
wss.close(() => {
console.log('WebSocket server encerrado.');
httpServer.close(() => {
console.log('Servidor HTTP encerrado.');
process.exit(0);
});
}); });
setTimeout(() => { setTimeout(() => {
console.error('Forçando encerramento após timeout.'); console.error('Forçando encerramento após timeout.');
process.exit(1); process.exit(1);
}, 10000); }, 10000).unref();
} }
process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));

View File

@@ -9,8 +9,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const AGENT_SETTINGS = path.resolve(__dirname, '..', '..', 'data', 'agent-settings.json'); const AGENT_SETTINGS = path.resolve(__dirname, '..', '..', 'data', 'agent-settings.json');
const CLAUDE_BIN = resolveBin(); const CLAUDE_BIN = resolveBin();
const activeExecutions = new Map(); const activeExecutions = new Map();
const executionOutputBuffers = new Map();
const MAX_OUTPUT_SIZE = 512 * 1024; const MAX_OUTPUT_SIZE = 512 * 1024;
const MAX_ERROR_SIZE = 100 * 1024; const MAX_ERROR_SIZE = 100 * 1024;
const MAX_BUFFER_LINES = 1000;
const ALLOWED_DIRECTORIES = (process.env.ALLOWED_DIRECTORIES || '').split(',').map(d => d.trim()).filter(Boolean); const ALLOWED_DIRECTORIES = (process.env.ALLOWED_DIRECTORIES || '').split(',').map(d => d.trim()).filter(Boolean);
let maxConcurrent = settingsStore.get().maxConcurrent || 5; let maxConcurrent = settingsStore.get().maxConcurrent || 5;
@@ -52,6 +54,8 @@ function cleanEnv(agentSecrets) {
delete env.CLAUDECODE; delete env.CLAUDECODE;
delete env.ANTHROPIC_API_KEY; delete env.ANTHROPIC_API_KEY;
env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '128000'; env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '128000';
if (!env.SHELL) env.SHELL = '/bin/bash';
if (!env.HOME) env.HOME = process.env.HOME || '/root';
if (agentSecrets && typeof agentSecrets === 'object') { if (agentSecrets && typeof agentSecrets === 'object') {
Object.assign(env, agentSecrets); Object.assign(env, agentSecrets);
} }
@@ -179,6 +183,16 @@ function extractSystemInfo(event) {
return null; return null;
} }
function bufferLine(executionId, data) {
let buf = executionOutputBuffers.get(executionId);
if (!buf) {
buf = [];
executionOutputBuffers.set(executionId, buf);
}
buf.push(data);
if (buf.length > MAX_BUFFER_LINES) buf.shift();
}
function processChildOutput(child, executionId, callbacks, options = {}) { function processChildOutput(child, executionId, callbacks, options = {}) {
const { onData, onError, onComplete } = callbacks; const { onData, onError, onComplete } = callbacks;
const timeoutMs = options.timeout || 1800000; const timeoutMs = options.timeout || 1800000;
@@ -202,7 +216,9 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
if (tools) { if (tools) {
for (const t of tools) { for (const t of tools) {
const msg = t.detail ? `${t.name}: ${t.detail}` : t.name; const msg = t.detail ? `${t.name}: ${t.detail}` : t.name;
if (onData) onData({ type: 'tool', content: msg, toolName: t.name }, executionId); const data = { type: 'tool', content: msg, toolName: t.name };
bufferLine(executionId, data);
if (onData) onData(data, executionId);
} }
} }
@@ -211,17 +227,23 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
if (fullText.length < MAX_OUTPUT_SIZE) { if (fullText.length < MAX_OUTPUT_SIZE) {
fullText += text; fullText += text;
} }
if (onData) onData({ type: 'chunk', content: text }, executionId); const data = { type: 'chunk', content: text };
bufferLine(executionId, data);
if (onData) onData(data, executionId);
} }
const sysInfo = extractSystemInfo(parsed); const sysInfo = extractSystemInfo(parsed);
if (sysInfo) { if (sysInfo) {
if (onData) onData({ type: 'system', content: sysInfo }, executionId); const data = { type: 'system', content: sysInfo };
bufferLine(executionId, data);
if (onData) onData(data, executionId);
} }
if (parsed.type === 'assistant') { if (parsed.type === 'assistant') {
turnCount++; turnCount++;
if (onData) onData({ type: 'turn', content: `Turno ${turnCount}`, turn: turnCount }, executionId); const data = { type: 'turn', content: `Turno ${turnCount}`, turn: turnCount };
bufferLine(executionId, data);
if (onData) onData(data, executionId);
} }
if (parsed.type === 'result') { if (parsed.type === 'result') {
@@ -232,6 +254,8 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
durationApiMs: parsed.duration_api_ms || 0, durationApiMs: parsed.duration_api_ms || 0,
numTurns: parsed.num_turns || 0, numTurns: parsed.num_turns || 0,
sessionId: parsed.session_id || sessionIdOverride || '', sessionId: parsed.session_id || sessionIdOverride || '',
isError: parsed.is_error || false,
errors: parsed.errors || [],
}; };
} }
} }
@@ -249,7 +273,9 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
} }
const lines = str.split('\n').filter(l => l.trim()); const lines = str.split('\n').filter(l => l.trim());
for (const line of lines) { for (const line of lines) {
if (onData) onData({ type: 'stderr', content: line.trim() }, executionId); const data = { type: 'stderr', content: line.trim() };
bufferLine(executionId, data);
if (onData) onData(data, executionId);
} }
}); });
@@ -258,6 +284,7 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
console.log(`[executor][error] ${err.message}`); console.log(`[executor][error] ${err.message}`);
hadError = true; hadError = true;
activeExecutions.delete(executionId); activeExecutions.delete(executionId);
executionOutputBuffers.delete(executionId);
if (onError) onError(err, executionId); if (onError) onError(err, executionId);
}); });
@@ -265,8 +292,16 @@ function processChildOutput(child, executionId, callbacks, options = {}) {
clearTimeout(timeout); clearTimeout(timeout);
const wasCanceled = activeExecutions.get(executionId)?.canceled || false; const wasCanceled = activeExecutions.get(executionId)?.canceled || false;
activeExecutions.delete(executionId); activeExecutions.delete(executionId);
executionOutputBuffers.delete(executionId);
if (hadError) return; if (hadError) return;
if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer)); if (outputBuffer.trim()) processEvent(parseStreamLine(outputBuffer));
if (resultMeta?.isError && resultMeta.errors?.length > 0) {
const errorMsg = resultMeta.errors.join('; ');
if (onError) onError(new Error(errorMsg), executionId);
return;
}
if (onComplete) { if (onComplete) {
onComplete({ onComplete({
executionId, executionId,
@@ -417,6 +452,7 @@ export function cancel(executionId) {
export function cancelAllExecutions() { export function cancelAllExecutions() {
for (const [, exec] of activeExecutions) exec.process.kill('SIGTERM'); for (const [, exec] of activeExecutions) exec.process.kill('SIGTERM');
activeExecutions.clear(); activeExecutions.clear();
executionOutputBuffers.clear();
} }
export function getActiveExecutions() { export function getActiveExecutions() {
@@ -424,6 +460,7 @@ export function getActiveExecutions() {
executionId: exec.executionId, executionId: exec.executionId,
startedAt: exec.startedAt, startedAt: exec.startedAt,
agentConfig: exec.agentConfig, agentConfig: exec.agentConfig,
outputBuffer: executionOutputBuffers.get(exec.executionId) || [],
})); }));
} }

View File

@@ -0,0 +1,117 @@
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { join, basename } from 'path';
const PROJECTS_DIR = '/home/projetos';
const GITEA_URL = () => process.env.GITEA_URL || 'http://gitea:3000';
const GITEA_USER = () => process.env.GITEA_USER || 'fred';
const GITEA_PASS = () => process.env.GITEA_PASS || '';
const DOMAIN = () => process.env.DOMAIN || 'nitro-cloud.duckdns.org';
function exec(cmd, cwd) {
return new Promise((resolve, reject) => {
const proc = spawn('sh', ['-c', cmd], {
cwd,
env: { ...process.env, HOME: '/tmp', GIT_TERMINAL_PROMPT: '0' },
});
let stdout = '', stderr = '';
proc.stdout.on('data', d => stdout += d);
proc.stderr.on('data', d => stderr += d);
proc.on('close', code =>
code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `exit ${code}`))
);
});
}
function authHeader() {
return 'Basic ' + Buffer.from(`${GITEA_USER()}:${GITEA_PASS()}`).toString('base64');
}
function repoCloneUrl(repoName) {
return `${GITEA_URL().replace('://', `://${GITEA_USER()}:${GITEA_PASS()}@`)}/${GITEA_USER()}/${repoName}.git`;
}
export async function listRepos() {
const url = `${GITEA_URL()}/api/v1/user/repos?limit=50&sort=updated`;
const res = await fetch(url, { headers: { Authorization: authHeader() } });
if (!res.ok) throw new Error('Erro ao listar repositórios');
const repos = await res.json();
return repos.map(r => ({
name: r.name,
fullName: r.full_name,
description: r.description || '',
defaultBranch: r.default_branch || 'main',
updatedAt: r.updated_at,
htmlUrl: r.html_url,
cloneUrl: r.clone_url,
empty: r.empty,
}));
}
export async function listBranches(repoName) {
const url = `${GITEA_URL()}/api/v1/repos/${GITEA_USER()}/${repoName}/branches?limit=50`;
const res = await fetch(url, { headers: { Authorization: authHeader() } });
if (!res.ok) return [];
const branches = await res.json();
return branches.map(b => b.name);
}
export async function cloneOrPull(repoName, branch) {
const targetDir = join(PROJECTS_DIR, repoName);
const cloneUrl = repoCloneUrl(repoName);
if (existsSync(join(targetDir, '.git'))) {
await exec(`git remote set-url origin "${cloneUrl}"`, targetDir);
await exec('git fetch origin', targetDir);
if (branch) {
try {
await exec(`git checkout ${branch}`, targetDir);
} catch {
await exec(`git checkout -b ${branch} origin/${branch}`, targetDir);
}
await exec(`git reset --hard origin/${branch}`, targetDir);
} else {
const currentBranch = await exec('git rev-parse --abbrev-ref HEAD', targetDir);
await exec(`git reset --hard origin/${currentBranch}`, targetDir);
}
return { dir: targetDir, action: 'pull' };
}
const branchArg = branch ? `-b ${branch}` : '';
await exec(`git clone ${branchArg} "${cloneUrl}" "${targetDir}"`);
return { dir: targetDir, action: 'clone' };
}
export async function commitAndPush(repoDir, agentName, taskSummary) {
try {
const status = await exec('git status --porcelain', repoDir);
if (!status) return { changed: false };
await exec('git add -A', repoDir);
const summary = taskSummary
? taskSummary.slice(0, 100).replace(/"/g, '\\"')
: 'Alterações automáticas';
const message = `${summary}\n\nExecutado por: ${agentName}`;
await exec(
`git -c user.name="Agents Orchestrator" -c user.email="agents@${DOMAIN()}" commit -m "${message}"`,
repoDir
);
await exec('git push origin HEAD', repoDir);
const commitHash = await exec('git rev-parse --short HEAD', repoDir);
const branch = await exec('git rev-parse --abbrev-ref HEAD', repoDir);
const repoName = basename(repoDir);
const commitUrl = `https://git.${DOMAIN()}/${GITEA_USER()}/${repoName}/commit/${commitHash}`;
return { changed: true, commitHash, branch, commitUrl, filesChanged: status.split('\n').length };
} catch (err) {
return { changed: false, error: err.message };
}
}
export function getProjectDir(repoName) {
return join(PROJECTS_DIR, repoName);
}

View File

@@ -4,6 +4,7 @@ import { agentsStore, schedulesStore, executionsStore, notificationsStore, secre
import * as executor from './executor.js'; import * as executor from './executor.js';
import * as scheduler from './scheduler.js'; import * as scheduler from './scheduler.js';
import { generateAgentReport } from '../reports/generator.js'; import { generateAgentReport } from '../reports/generator.js';
import * as gitIntegration from './git-integration.js';
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
model: 'claude-sonnet-4-6', model: 'claude-sonnet-4-6',
@@ -172,9 +173,26 @@ export function executeTask(agentId, task, instructions, wsCallback, metadata =
const agentSecrets = loadAgentSecrets(agentId); const agentSecrets = loadAgentSecrets(agentId);
let effectiveInstructions = instructions || '';
const tags = agent.tags || [];
if (tags.includes('coordinator')) {
const allAgents = agentsStore.getAll().filter(a => a.id !== agentId && a.status === 'active');
const agentList = allAgents.map(a => `- **${a.agent_name}**: ${a.description || 'Sem descrição'}`).join('\n');
effectiveInstructions += `\n\n<agentes_disponiveis>\n${agentList}\n</agentes_disponiveis>`;
}
if (metadata.repoName) {
effectiveInstructions += `\n\n<git_repository>\nVocê está trabalhando no repositório "${metadata.repoName}". NÃO faça git init, git commit, git push ou qualquer operação git. O sistema fará commit e push automaticamente ao final da execução. Foque apenas no código.\n</git_repository>`;
}
const effectiveConfig = { ...agent.config };
if (metadata.workingDirectoryOverride) {
effectiveConfig.workingDirectory = metadata.workingDirectoryOverride;
}
const executionId = executor.execute( const executionId = executor.execute(
agent.config, effectiveConfig,
{ description: task, instructions }, { description: task, instructions: effectiveInstructions },
{ {
onData: (parsed, execId) => { onData: (parsed, execId) => {
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: parsed }); if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: parsed });
@@ -233,7 +251,50 @@ export function executeTask(agentId, task, instructions, wsCallback, metadata =
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename }); if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
} }
} catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); } } catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); }
if (metadata.repoName && result.result) {
const repoDir = gitIntegration.getProjectDir(metadata.repoName);
gitIntegration.commitAndPush(repoDir, agent.agent_name, taskText.slice(0, 100))
.then(gitResult => {
if (gitResult.changed) {
console.log(`[manager] Auto-commit: ${gitResult.commitHash} em ${metadata.repoName}`);
if (cb) cb({
type: 'execution_output', executionId: execId, agentId,
data: { type: 'success', content: `Git: commit ${gitResult.commitHash} pushed para ${metadata.repoName} (${gitResult.filesChanged} arquivos) → ${gitResult.commitUrl}` },
});
} else if (gitResult.error) {
console.error(`[manager] Erro no auto-commit:`, gitResult.error);
}
})
.catch(err => console.error(`[manager] Erro no auto-commit:`, err.message));
}
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result }); if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
const isPipelineStep = !!metadata.pipelineExecutionId;
const delegateTo = agent.config?.delegateTo;
if (!isPipelineStep && delegateTo && result.result) {
const delegateAgent = agentsStore.getById(delegateTo);
if (delegateAgent && delegateAgent.status === 'active') {
console.log(`[manager] Auto-delegando de "${agent.agent_name}" para "${delegateAgent.agent_name}"`);
if (cb) cb({
type: 'execution_output',
executionId: execId,
agentId,
data: { type: 'system', content: `Delegando para ${delegateAgent.agent_name}...` },
});
setTimeout(() => {
try {
executeTask(delegateTo, result.result, null, wsCallback, {
delegatedFrom: agent.agent_name,
originalTask: taskText,
});
} catch (delegateErr) {
console.error(`[manager] Erro ao delegar para "${delegateAgent.agent_name}":`, delegateErr.message);
if (cb) cb({ type: 'execution_error', executionId: execId, agentId: delegateTo, data: { error: delegateErr.message } });
}
}, 3000);
}
}
}, },
}, },
agentSecrets agentSecrets
@@ -359,42 +420,70 @@ export function continueConversation(agentId, sessionId, message, wsCallback) {
parentSessionId: sessionId, parentSessionId: sessionId,
}); });
const onData = (parsed, execId) => {
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: parsed });
};
const onComplete = (result, execId) => {
const endedAt = new Date().toISOString();
executionsStore.update(historyRecord.id, {
status: 'completed',
result: result.result || '',
exitCode: result.exitCode,
endedAt,
costUsd: result.costUsd || 0,
totalCostUsd: result.totalCostUsd || 0,
durationMs: result.durationMs || 0,
numTurns: result.numTurns || 0,
sessionId: result.sessionId || sessionId,
});
try {
const updated = executionsStore.getById(historyRecord.id);
if (updated) {
const report = generateAgentReport(updated);
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
}
} catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); }
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, agentName: agent.agent_name, data: result });
};
const onError = (err, execId) => {
const isSessionLost = err.message.includes('No conversation found') || err.message.includes('not a valid');
if (isSessionLost) {
console.log(`[manager] Sessão perdida (${sessionId}), iniciando nova execução para agente ${agentId}`);
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: { type: 'system', content: 'Sessão anterior expirou. Iniciando nova execução...' } });
const secrets = secretsStore.getByAgent(agentId);
const newExecId = executor.execute(
agent.config,
{ description: message },
{ onData, onError: onErrorFinal, onComplete },
Object.keys(secrets).length > 0 ? secrets : null,
);
if (newExecId) {
executionsStore.update(historyRecord.id, { executionId: newExecId, parentSessionId: null });
} else {
onErrorFinal(new Error('Falha ao iniciar nova execução'), execId);
}
return;
}
onErrorFinal(err, execId);
};
const onErrorFinal = (err, execId) => {
const endedAt = new Date().toISOString();
executionsStore.update(historyRecord.id, { status: 'error', error: err.message, endedAt });
if (cb) cb({ type: 'execution_error', executionId: execId, agentId, data: { error: err.message } });
};
const executionId = executor.resume( const executionId = executor.resume(
agent.config, agent.config,
sessionId, sessionId,
message, message,
{ { onData, onError, onComplete }
onData: (parsed, execId) => {
if (cb) cb({ type: 'execution_output', executionId: execId, agentId, data: parsed });
},
onError: (err, execId) => {
const endedAt = new Date().toISOString();
executionsStore.update(historyRecord.id, { status: 'error', error: err.message, endedAt });
if (cb) cb({ type: 'execution_error', executionId: execId, agentId, data: { error: err.message } });
},
onComplete: (result, execId) => {
const endedAt = new Date().toISOString();
executionsStore.update(historyRecord.id, {
status: 'completed',
result: result.result || '',
exitCode: result.exitCode,
endedAt,
costUsd: result.costUsd || 0,
totalCostUsd: result.totalCostUsd || 0,
durationMs: result.durationMs || 0,
numTurns: result.numTurns || 0,
sessionId: result.sessionId || sessionId,
});
try {
const updated = executionsStore.getById(historyRecord.id);
if (updated) {
const report = generateAgentReport(updated);
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
}
} catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); }
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
},
}
); );
if (!executionId) { if (!executionId) {

View File

@@ -1,6 +1,7 @@
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { pipelinesStore, agentsStore, executionsStore } from '../store/db.js'; import { pipelinesStore, agentsStore, executionsStore } from '../store/db.js';
import * as executor from './executor.js'; import * as executor from './executor.js';
import * as gitIntegration from './git-integration.js';
import { mem } from '../cache/index.js'; import { mem } from '../cache/index.js';
import { generatePipelineReport } from '../reports/generator.js'; import { generatePipelineReport } from '../reports/generator.js';
@@ -293,6 +294,19 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
}); });
if (!pipelineState.canceled) { if (!pipelineState.canceled) {
if (options.repoName) {
try {
const repoDir = gitIntegration.getProjectDir(options.repoName);
const gitResult = await gitIntegration.commitAndPush(repoDir, pl.name, `Pipeline: ${pl.name}`);
if (gitResult.changed && wsCallback) {
wsCallback({
type: 'pipeline_step_output', pipelineId, stepIndex: steps.length - 1,
data: { type: 'success', content: `Git: commit ${gitResult.commitHash} pushed para ${options.repoName} (${gitResult.filesChanged} arquivos) → ${gitResult.commitUrl}` },
});
}
} catch (e) { console.error('[pipeline] Erro no auto-commit:', e.message); }
}
try { try {
const updated = executionsStore.getById(historyRecord.id); const updated = executionsStore.getById(historyRecord.id);
if (updated) { if (updated) {

View File

@@ -160,6 +160,13 @@ export function restoreSchedules(executeFn) {
if (restored > 0) console.log(`[scheduler] ${restored} agendamento(s) restaurado(s)`); if (restored > 0) console.log(`[scheduler] ${restored} agendamento(s) restaurado(s)`);
} }
export function stopAll() {
for (const [, entry] of schedules) {
entry.task.stop();
}
schedules.clear();
}
export function on(event, listener) { export function on(event, listener) {
emitter.on(event, listener); emitter.on(event, listener);
} }

View File

@@ -1,5 +1,5 @@
import { Router } from 'express'; import { Router } from 'express';
import { execFile } from 'child_process'; import { execFile, spawn as spawnProcess } from 'child_process';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import crypto from 'crypto'; import crypto from 'crypto';
import os from 'os'; import os from 'os';
@@ -8,11 +8,14 @@ import * as manager from '../agents/manager.js';
import { tasksStore, settingsStore, executionsStore, webhooksStore, notificationsStore, secretsStore, agentVersionsStore } from '../store/db.js'; import { tasksStore, settingsStore, executionsStore, webhooksStore, notificationsStore, secretsStore, agentVersionsStore } from '../store/db.js';
import * as scheduler from '../agents/scheduler.js'; import * as scheduler from '../agents/scheduler.js';
import * as pipeline from '../agents/pipeline.js'; import * as pipeline from '../agents/pipeline.js';
import * as gitIntegration from '../agents/git-integration.js';
import { getBinPath, updateMaxConcurrent, cancelAllExecutions, getActiveExecutions } from '../agents/executor.js'; import { getBinPath, updateMaxConcurrent, cancelAllExecutions, getActiveExecutions } from '../agents/executor.js';
import { invalidateAgentMapCache } from '../agents/pipeline.js'; import { invalidateAgentMapCache } from '../agents/pipeline.js';
import { cached } from '../cache/index.js'; import { cached } from '../cache/index.js';
import { readdirSync, readFileSync, unlinkSync, existsSync, mkdirSync } from 'fs'; import { readdirSync, readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, statSync, createReadStream, rmSync } from 'fs';
import { join, dirname, resolve as pathResolve, extname } from 'path'; import { join, dirname, resolve as pathResolve, extname, basename, relative } from 'path';
import { createGzip } from 'zlib';
import { Readable } from 'stream';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
const __apiDirname = dirname(fileURLToPath(import.meta.url)); const __apiDirname = dirname(fileURLToPath(import.meta.url));
@@ -36,6 +39,11 @@ const upload = multer({
limits: { fileSize: 10 * 1024 * 1024, files: 20 }, limits: { fileSize: 10 * 1024 * 1024, files: 20 },
}); });
const importUpload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 500 * 1024 * 1024, files: 50000 },
});
const router = Router(); const router = Router();
export const hookRouter = Router(); export const hookRouter = Router();
@@ -163,14 +171,25 @@ function buildContextFilesPrompt(contextFiles) {
return `\n\nArquivos de contexto anexados (leia cada um deles antes de iniciar):\n${lines.join('\n')}`; return `\n\nArquivos de contexto anexados (leia cada um deles antes de iniciar):\n${lines.join('\n')}`;
} }
router.post('/agents/:id/execute', (req, res) => { router.post('/agents/:id/execute', async (req, res) => {
try { try {
const { task, instructions, contextFiles } = req.body; const { task, instructions, contextFiles, workingDirectory, repoName, repoBranch } = req.body;
if (!task) return res.status(400).json({ error: 'task é obrigatório' }); if (!task) return res.status(400).json({ error: 'task é obrigatório' });
const clientId = req.headers['x-client-id'] || null; const clientId = req.headers['x-client-id'] || null;
const filesPrompt = buildContextFilesPrompt(contextFiles); const filesPrompt = buildContextFilesPrompt(contextFiles);
const fullTask = task + filesPrompt; const fullTask = task + filesPrompt;
const executionId = manager.executeTask(req.params.id, fullTask, instructions, (msg) => wsCallback(msg, clientId)); const metadata = {};
if (repoName) {
const syncResult = await gitIntegration.cloneOrPull(repoName, repoBranch || null);
metadata.workingDirectoryOverride = syncResult.dir;
metadata.repoName = repoName;
metadata.repoBranch = repoBranch || null;
} else if (workingDirectory) {
metadata.workingDirectoryOverride = workingDirectory;
}
const executionId = manager.executeTask(req.params.id, fullTask, instructions, (msg) => wsCallback(msg, clientId), metadata);
res.status(202).json({ executionId, status: 'started' }); res.status(202).json({ executionId, status: 'started' });
} catch (err) { } catch (err) {
const status = err.message.includes('não encontrado') ? 404 : 400; const status = err.message.includes('não encontrado') ? 404 : 400;
@@ -460,11 +479,20 @@ router.delete('/pipelines/:id', (req, res) => {
router.post('/pipelines/:id/execute', async (req, res) => { router.post('/pipelines/:id/execute', async (req, res) => {
try { try {
const { input, workingDirectory, contextFiles } = req.body; const { input, workingDirectory, contextFiles, repoName, repoBranch } = req.body;
if (!input) return res.status(400).json({ error: 'input é obrigatório' }); if (!input) return res.status(400).json({ error: 'input é obrigatório' });
const clientId = req.headers['x-client-id'] || null; const clientId = req.headers['x-client-id'] || null;
const options = {}; const options = {};
if (workingDirectory) options.workingDirectory = workingDirectory;
if (repoName) {
const syncResult = await gitIntegration.cloneOrPull(repoName, repoBranch || null);
options.workingDirectory = syncResult.dir;
options.repoName = repoName;
options.repoBranch = repoBranch || null;
} else if (workingDirectory) {
options.workingDirectory = workingDirectory;
}
const filesPrompt = buildContextFilesPrompt(contextFiles); const filesPrompt = buildContextFilesPrompt(contextFiles);
const fullInput = input + filesPrompt; const fullInput = input + filesPrompt;
const result = pipeline.executePipeline(req.params.id, fullInput, (msg) => wsCallback(msg, clientId), options); const result = pipeline.executePipeline(req.params.id, fullInput, (msg) => wsCallback(msg, clientId), options);
@@ -1037,4 +1065,556 @@ router.delete('/reports/:filename', (req, res) => {
} }
}); });
const PROJECTS_DIR = '/home/projetos';
function resolveProjectPath(requestedPath) {
const decoded = decodeURIComponent(requestedPath || '');
const resolved = pathResolve(PROJECTS_DIR, decoded);
if (!resolved.startsWith(PROJECTS_DIR)) return null;
return resolved;
}
router.get('/files', (req, res) => {
try {
const targetPath = resolveProjectPath(req.query.path || '');
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Diretório não encontrado' });
const stat = statSync(targetPath);
if (!stat.isDirectory()) return res.status(400).json({ error: 'Caminho não é um diretório' });
const entries = readdirSync(targetPath, { withFileTypes: true })
.filter(e => !e.name.startsWith('.'))
.map(entry => {
const fullPath = join(targetPath, entry.name);
try {
const s = statSync(fullPath);
return {
name: entry.name,
type: entry.isDirectory() ? 'directory' : 'file',
size: entry.isDirectory() ? null : s.size,
modified: s.mtime.toISOString(),
extension: entry.isDirectory() ? null : extname(entry.name).slice(1).toLowerCase(),
};
} catch {
return null;
}
})
.filter(Boolean)
.sort((a, b) => {
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
return a.name.localeCompare(b.name);
});
const relativePath = relative(PROJECTS_DIR, targetPath) || '';
res.json({
path: relativePath,
parent: relativePath ? dirname(relativePath) : null,
entries,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/files/download', (req, res) => {
try {
const targetPath = resolveProjectPath(req.query.path || '');
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Arquivo não encontrado' });
const stat = statSync(targetPath);
if (!stat.isFile()) return res.status(400).json({ error: 'Caminho não é um arquivo' });
const filename = basename(targetPath);
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
res.setHeader('Content-Length', stat.size);
createReadStream(targetPath).pipe(res);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/files/download-folder', (req, res) => {
try {
const targetPath = resolveProjectPath(req.query.path || '');
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Pasta não encontrada' });
const stat = statSync(targetPath);
if (!stat.isDirectory()) return res.status(400).json({ error: 'Caminho não é uma pasta' });
const folderName = basename(targetPath) || 'projetos';
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(folderName)}.tar.gz"`);
res.setHeader('Content-Type', 'application/gzip');
const parentDir = dirname(targetPath);
const dirName = basename(targetPath);
const tar = spawnProcess('tar', ['-czf', '-', '-C', parentDir, dirName]);
tar.stdout.pipe(res);
tar.stderr.on('data', () => {});
tar.on('error', (err) => {
if (!res.headersSent) res.status(500).json({ error: err.message });
});
req.on('close', () => { try { tar.kill(); } catch {} });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.delete('/files', (req, res) => {
try {
const targetPath = resolveProjectPath(req.query.path || '');
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
if (targetPath === PROJECTS_DIR) return res.status(400).json({ error: 'Não é permitido excluir o diretório raiz' });
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Arquivo ou pasta não encontrado' });
const stat = statSync(targetPath);
if (stat.isDirectory()) {
rmSync(targetPath, { recursive: true, force: true });
} else {
unlinkSync(targetPath);
}
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/repos', async (req, res) => {
try {
const repos = await gitIntegration.listRepos();
res.json(repos);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/repos/:name/branches', async (req, res) => {
try {
const branches = await gitIntegration.listBranches(req.params.name);
res.json(branches);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/files/commit-push', async (req, res) => {
const { path: projectPath, message } = req.body;
if (!projectPath) return res.status(400).json({ error: 'path é obrigatório' });
const targetPath = resolveProjectPath(projectPath);
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Projeto não encontrado' });
if (!statSync(targetPath).isDirectory()) return res.status(400).json({ error: 'Caminho não é uma pasta' });
const gitDir = `${targetPath}/.git`;
if (!existsSync(gitDir)) return res.status(400).json({ error: 'Projeto não possui repositório git inicializado' });
const exec = (cmd, opts = {}) => new Promise((resolve, reject) => {
const proc = spawnProcess('sh', ['-c', cmd], { cwd: opts.cwd || targetPath, env: { ...process.env, HOME: '/tmp', GIT_TERMINAL_PROMPT: '0' } });
let stdout = '', stderr = '';
proc.stdout.on('data', d => stdout += d);
proc.stderr.on('data', d => stderr += d);
proc.on('close', code => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `exit ${code}`)));
});
try {
const status = await exec('git status --porcelain');
if (!status) return res.json({ status: 'clean', message: 'Nenhuma alteração para commitar', changes: 0 });
const changes = status.split('\n').filter(l => l.trim()).length;
await exec('git add -A');
const commitMsg = message || `Atualização automática - ${new Date().toLocaleDateString('pt-BR')} ${new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}`;
await exec(`git -c user.name="Agents Orchestrator" -c user.email="agents@nitro-cloud" commit -m "${commitMsg.replace(/"/g, '\\"')}"`);
await exec('git push origin HEAD:main');
const log = await exec('git log -1 --format="%h %s"');
res.json({ status: 'pushed', message: `Commit e push realizados: ${log}`, changes, commit: log });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/files/publish', async (req, res) => {
const { path: projectPath } = req.body;
if (!projectPath) return res.status(400).json({ error: 'path é obrigatório' });
const targetPath = resolveProjectPath(projectPath);
if (!targetPath) return res.status(400).json({ error: 'Caminho inválido' });
if (!existsSync(targetPath)) return res.status(404).json({ error: 'Projeto não encontrado' });
if (!statSync(targetPath).isDirectory()) return res.status(400).json({ error: 'Caminho não é uma pasta' });
const projectName = basename(targetPath).toLowerCase().replace(/[^a-z0-9-]/g, '-');
const GITEA_URL = process.env.GITEA_URL || 'http://gitea:3000';
const GITEA_USER = process.env.GITEA_USER || 'fred';
const GITEA_PASS = process.env.GITEA_PASS || '';
const DOMAIN = process.env.DOMAIN || 'nitro-cloud.duckdns.org';
const VPS_COMPOSE_DIR = process.env.VPS_COMPOSE_DIR || '/vps';
if (!GITEA_PASS) return res.status(500).json({ error: 'GITEA_PASS não configurado no servidor' });
const exec = (cmd, opts = {}) => new Promise((resolve, reject) => {
const proc = spawnProcess('sh', ['-c', cmd], { cwd: opts.cwd || targetPath, env: { ...process.env, HOME: '/tmp', GIT_TERMINAL_PROMPT: '0' } });
let stdout = '', stderr = '';
proc.stdout.on('data', d => stdout += d);
proc.stderr.on('data', d => stderr += d);
proc.on('close', code => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `exit ${code}`)));
});
const steps = [];
try {
const authUrl = `${GITEA_URL.replace('://', `://${GITEA_USER}:${GITEA_PASS}@`)}`;
const repoApiUrl = `${GITEA_URL}/api/v1/repos/${GITEA_USER}/${projectName}`;
const createUrl = `${GITEA_URL}/api/v1/user/repos`;
const authHeader = 'Basic ' + Buffer.from(`${GITEA_USER}:${GITEA_PASS}`).toString('base64');
let repoExists = false;
try {
const check = await fetch(repoApiUrl, { headers: { Authorization: authHeader } });
repoExists = check.ok;
} catch {}
if (!repoExists) {
const createRes = await fetch(createUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
body: JSON.stringify({ name: projectName, auto_init: false, private: false }),
});
if (!createRes.ok) {
const err = await createRes.json().catch(() => ({}));
throw new Error(`Erro ao criar repositório: ${err.message || createRes.statusText}`);
}
steps.push('Repositório criado no Gitea');
} else {
steps.push('Repositório já existe no Gitea');
}
const repoUrl = `${authUrl}/${GITEA_USER}/${projectName}.git`;
const gitDir = `${targetPath}/.git`;
if (!existsSync(gitDir)) {
await exec('git init');
await exec(`git remote add origin "${repoUrl}"`);
steps.push('Git inicializado');
} else {
try {
await exec('git remote get-url origin');
await exec(`git remote set-url origin "${repoUrl}"`);
} catch {
await exec(`git remote add origin "${repoUrl}"`);
}
steps.push('Remote atualizado');
}
await exec('git add -A');
try {
await exec('git -c user.name="Agents Orchestrator" -c user.email="agents@nitro-cloud" commit -m "Publicação automática"');
steps.push('Commit criado');
} catch {
steps.push('Sem alterações para commit');
}
await exec('git push -u origin HEAD:main --force');
steps.push('Push realizado');
const caddyFile = `${VPS_COMPOSE_DIR}/caddy/Caddyfile`;
if (existsSync(caddyFile)) {
const caddyContent = readFileSync(caddyFile, 'utf-8');
const marker = `@${projectName} host ${projectName}.${DOMAIN}`;
if (!caddyContent.includes(marker)) {
const block = `\n @${projectName} host ${projectName}.${DOMAIN}\n handle @${projectName} {\n root * /srv/${projectName}\n file_server\n try_files {path} /index.html\n }\n`;
const updated = caddyContent.replace(
/(\n? {4}handle \{[\s\S]*?respond.*?200[\s\S]*?\})/,
block + '$1'
);
writeFileSync(caddyFile, updated);
steps.push('Caddyfile atualizado');
} else {
steps.push('Caddyfile já configurado');
}
}
try {
await exec('docker exec caddy caddy reload --config /etc/caddy/Caddyfile');
steps.push('Caddy recarregado');
} catch (e) {
steps.push(`Caddy: reload necessário (${e.message})`);
}
const siteUrl = `https://${projectName}.${DOMAIN}`;
const repoWebUrl = `https://git.${DOMAIN}/${GITEA_USER}/${projectName}`;
res.json({
status: 'Publicado',
siteUrl,
repoUrl: repoWebUrl,
projectName,
steps,
message: `Acesse ${siteUrl} em alguns segundos`,
});
} catch (err) {
res.status(500).json({ error: err.message, steps });
}
});
router.get('/browse', (req, res) => {
const requestedPath = req.query.path || '/home';
const resolved = pathResolve(requestedPath);
const blocked = ['/proc', '/sys', '/dev', '/boot', '/run'];
if (blocked.some(b => resolved.startsWith(b))) {
return res.status(403).json({ error: 'Acesso negado a este diretório' });
}
try {
const entries = readdirSync(resolved, { withFileTypes: true });
const dirs = entries
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
.map(e => ({ name: e.name, path: join(resolved, e.name) }))
.sort((a, b) => a.name.localeCompare(b.name));
res.json({
currentPath: resolved,
parentPath: dirname(resolved),
directories: dirs,
});
} catch (err) {
res.status(400).json({ error: `Erro ao listar: ${err.message}` });
}
});
router.post('/projects/import', async (req, res) => {
const { sourcePath, repoName } = req.body;
if (!sourcePath || !repoName) return res.status(400).json({ error: 'sourcePath e repoName são obrigatórios' });
const resolvedSource = pathResolve(sourcePath);
if (!existsSync(resolvedSource)) return res.status(400).json({ error: 'Diretório não encontrado' });
if (!statSync(resolvedSource).isDirectory()) return res.status(400).json({ error: 'Caminho não é um diretório' });
const sanitizedName = repoName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
const GITEA_URL = process.env.GITEA_URL || 'http://gitea:3000';
const GITEA_USER = process.env.GITEA_USER || 'fred';
const GITEA_PASS = process.env.GITEA_PASS || '';
const DOMAIN = process.env.DOMAIN || 'nitro-cloud.duckdns.org';
if (!GITEA_PASS) return res.status(500).json({ error: 'GITEA_PASS não configurado' });
const steps = [];
const tmpDir = join(os.tmpdir(), `import-${Date.now()}`);
const exec = (cmd, cwd) => new Promise((resolve, reject) => {
const proc = spawnProcess('sh', ['-c', cmd], { cwd: cwd || tmpDir, env: { ...process.env, HOME: '/tmp', GIT_TERMINAL_PROMPT: '0' } });
let stdout = '', stderr = '';
proc.stdout.on('data', d => stdout += d);
proc.stderr.on('data', d => stderr += d);
proc.on('close', code => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `exit ${code}`)));
});
try {
const authHeader = 'Basic ' + Buffer.from(`${GITEA_USER}:${GITEA_PASS}`).toString('base64');
const repoApiUrl = `${GITEA_URL}/api/v1/repos/${GITEA_USER}/${sanitizedName}`;
let repoExists = false;
try {
const check = await fetch(repoApiUrl, { headers: { Authorization: authHeader } });
repoExists = check.ok;
} catch {}
if (!repoExists) {
const createRes = await fetch(`${GITEA_URL}/api/v1/user/repos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
body: JSON.stringify({ name: sanitizedName, auto_init: false, private: false }),
});
if (!createRes.ok) {
const err = await createRes.json().catch(() => ({}));
throw new Error(`Erro ao criar repositório: ${err.message || createRes.statusText}`);
}
steps.push('Repositório criado no Gitea');
} else {
steps.push('Repositório já existe no Gitea');
}
mkdirSync(tmpDir, { recursive: true });
const hasGit = existsSync(join(resolvedSource, '.git'));
if (hasGit) {
try {
await exec(`git -C "${resolvedSource}" archive HEAD | tar -x -C "${tmpDir}"`, resolvedSource);
steps.push('Arquivos exportados via git archive (respeitando .gitignore)');
} catch {
await exec(`rsync -a --exclude='.git' --exclude='node_modules' --exclude='__pycache__' --exclude='.env' "${resolvedSource}/" "${tmpDir}/"`);
steps.push('Arquivos copiados via rsync (git archive falhou, possivelmente sem commits)');
}
} else {
const hasGitignore = existsSync(join(resolvedSource, '.gitignore'));
let rsyncCmd = `rsync -a --exclude='.git' --exclude='node_modules' --exclude='__pycache__' --exclude='.env'`;
if (hasGitignore) rsyncCmd += ` --filter=':- .gitignore'`;
rsyncCmd += ` "${resolvedSource}/" "${tmpDir}/"`;
await exec(rsyncCmd, resolvedSource);
steps.push(hasGitignore ? 'Arquivos copiados respeitando .gitignore' : 'Arquivos copiados (sem .gitignore encontrado)');
}
const repoUrl = `${GITEA_URL.replace('://', `://${GITEA_USER}:${GITEA_PASS}@`)}/${GITEA_USER}/${sanitizedName}.git`;
await exec('git init');
await exec('git add -A');
await exec(`git -c user.name="Agents Orchestrator" -c user.email="agents@${DOMAIN}" commit -m "Import do projeto ${sanitizedName}"`);
await exec(`git remote add origin "${repoUrl}"`);
await exec('git push -u origin HEAD:main --force');
steps.push('Push realizado para o Gitea');
const projectsDir = '/home/projetos';
const targetDir = join(projectsDir, sanitizedName);
if (existsSync(targetDir)) {
try {
await exec('git rev-parse --git-dir', targetDir);
await exec(`git remote set-url origin "${repoUrl}"`, targetDir);
await exec('git fetch origin', targetDir);
await exec('git reset --hard origin/main', targetDir);
steps.push('Projeto atualizado em /home/projetos/');
} catch {
rmSync(targetDir, { recursive: true, force: true });
await exec(`git clone "${repoUrl}" "${targetDir}"`, projectsDir);
steps.push('Diretório anterior removido e projeto clonado em /home/projetos/');
}
} else {
await exec(`git clone "${repoUrl}" "${targetDir}"`, projectsDir);
steps.push('Projeto clonado em /home/projetos/');
}
rmSync(tmpDir, { recursive: true, force: true });
res.json({
status: 'Importado',
repoName: sanitizedName,
repoUrl: `https://git.${DOMAIN}/${GITEA_USER}/${sanitizedName}`,
projectDir: targetDir,
steps,
message: 'Projeto disponível no Gitea e pronto para uso com agentes',
});
} catch (err) {
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
res.status(500).json({ error: err.message, steps });
}
});
router.post('/projects/upload', (req, res, next) => {
importUpload.array('files', 50000)(req, res, (err) => {
if (err) return res.status(413).json({ error: `Upload falhou: ${err.message}` });
next();
});
}, async (req, res) => {
const repoName = (req.body.repoName || '').toLowerCase().replace(/[^a-z0-9-]/g, '-');
if (!repoName) return res.status(400).json({ error: 'repoName é obrigatório' });
let paths;
try { paths = JSON.parse(req.body.paths || '[]'); } catch { return res.status(400).json({ error: 'paths inválido' }); }
const files = req.files || [];
if (files.length === 0) return res.status(400).json({ error: 'Nenhum arquivo enviado' });
if (files.length !== paths.length) return res.status(400).json({ error: 'Quantidade de files e paths diverge' });
const GITEA_URL = process.env.GITEA_URL || 'http://gitea:3000';
const GITEA_USER = process.env.GITEA_USER || 'fred';
const GITEA_PASS = process.env.GITEA_PASS || '';
const DOMAIN = process.env.DOMAIN || 'nitro-cloud.duckdns.org';
if (!GITEA_PASS) return res.status(500).json({ error: 'GITEA_PASS não configurado' });
const steps = [];
const tmpDir = join(os.tmpdir(), `upload-${Date.now()}`);
const exec = (cmd, cwd) => new Promise((resolve, reject) => {
const proc = spawnProcess('sh', ['-c', cmd], { cwd: cwd || tmpDir, env: { ...process.env, HOME: '/tmp', GIT_TERMINAL_PROMPT: '0' } });
let stdout = '', stderr = '';
proc.stdout.on('data', d => stdout += d);
proc.stderr.on('data', d => stderr += d);
proc.on('close', code => code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `exit ${code}`)));
});
try {
mkdirSync(tmpDir, { recursive: true });
for (let i = 0; i < files.length; i++) {
const relativePath = paths[i].split('/').slice(1).join('/');
if (!relativePath || relativePath.includes('..')) continue;
if (relativePath === '.git' || relativePath.startsWith('.git/')) continue;
const dest = join(tmpDir, relativePath);
mkdirSync(dirname(dest), { recursive: true });
writeFileSync(dest, files[i].buffer);
}
steps.push(`${files.length} arquivos recebidos`);
const authHeader = 'Basic ' + Buffer.from(`${GITEA_USER}:${GITEA_PASS}`).toString('base64');
let repoExists = false;
try {
const check = await fetch(`${GITEA_URL}/api/v1/repos/${GITEA_USER}/${repoName}`, { headers: { Authorization: authHeader } });
repoExists = check.ok;
} catch {}
if (!repoExists) {
const createRes = await fetch(`${GITEA_URL}/api/v1/user/repos`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: authHeader },
body: JSON.stringify({ name: repoName, auto_init: false, private: false }),
});
if (!createRes.ok) throw new Error('Erro ao criar repositório no Gitea');
steps.push('Repositório criado no Gitea');
} else {
steps.push('Repositório já existe no Gitea');
}
const repoUrl = `${GITEA_URL.replace('://', `://${GITEA_USER}:${GITEA_PASS}@`)}/${GITEA_USER}/${repoName}.git`;
await exec('git init');
await exec('git add -A');
await exec(`git -c user.name="Agents Orchestrator" -c user.email="agents@${DOMAIN}" commit -m "Import do projeto ${repoName}"`);
await exec(`git remote add origin "${repoUrl}"`);
await exec('git push -u origin HEAD:main --force');
steps.push('Push realizado para o Gitea');
const projectsDir = '/home/projetos';
const targetDir = join(projectsDir, repoName);
if (existsSync(targetDir)) {
try {
await exec('git rev-parse --git-dir', targetDir);
await exec(`git remote set-url origin "${repoUrl}"`, targetDir);
await exec('git fetch origin', targetDir);
await exec('git reset --hard origin/main', targetDir);
steps.push('Projeto atualizado em /home/projetos/');
} catch {
rmSync(targetDir, { recursive: true, force: true });
await exec(`git clone "${repoUrl}" "${targetDir}"`, projectsDir);
steps.push('Diretório anterior removido e projeto clonado em /home/projetos/');
}
} else {
await exec(`git clone "${repoUrl}" "${targetDir}"`, projectsDir);
steps.push('Projeto clonado em /home/projetos/');
}
rmSync(tmpDir, { recursive: true, force: true });
res.json({
status: 'Importado',
repoName,
repoUrl: `https://git.${DOMAIN}/${GITEA_USER}/${repoName}`,
projectDir: targetDir,
steps,
message: 'Projeto disponível no Gitea e pronto para uso com agentes',
});
} catch (err) {
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
res.status(500).json({ error: err.message, steps });
}
});
export default router; export default router;