chore: setup PM2 ecosystem, cleanup old bots, create hermes production guidelines skill and finalized plan

This commit is contained in:
oleksiibarsuk-eng
2026-08-08 19:02:10 +02:00
committed by diegosouzapw
parent 81fc56a369
commit 2d5e9fad45
9 changed files with 416 additions and 57 deletions

20
.gitignore vendored
View File

@@ -72,7 +72,6 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
!.env.devin-bridge.example
!.env.homolog.example
# Provider API keys (never commit)
*.api-key
@@ -210,8 +209,6 @@ scripts/i18n/_pending-keys.json
.agents/
.antigravitycli/
.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
# PR Reviews and local feedback files
pr_reviews*.json
@@ -238,10 +235,7 @@ omniroute.md
# mise configuration
mise.toml
# release-green artifacts (.gitignore has no inline comments — a trailing
# `# ...` becomes part of the pattern, so it must sit on its own line).
# Already covered by /_*/ above; kept explicit for discoverability.
_artifacts/
_artifacts/ # release-green artifacts
.claude-flow/
# ESLint file cache (npm run lint --cache / complexity ratchets)
@@ -251,8 +245,6 @@ _artifacts/
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
.artifacts/
# Isolated Devin bridge workspaces, evidence, and test databases
.sandbox/
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
.env.homolog
@@ -261,11 +253,7 @@ tests/homolog/ui/.auth/
homolog-report/
docker-compose.yml.bak
.playwright-cli/
# Playwright screenshot/log output. Today every artifact happens to land inside
# output/**/.playwright-cli/ (covered above), but anything written directly to
# output/ would otherwise show up as untracked.
/output/
# _tasks e um repo git SEPARADO (ver AGENTS.md). _tasks/ (com barra) NAO ignora um
# SYMLINK _tasks; /_tasks (ancorado) cobre symlink/dir na raiz (incidente 2026-08-08).
/_tasks
# Runtime artifacts
.chroma_db/
omniroute.log

View File

@@ -17,3 +17,4 @@
- Session initialized at Fri Aug 7 08:04:36 CEST 2026
- Session initialized at Fri Aug 7 08:52:52 CEST 2026
- Session initialized at Fri Aug 7 09:01:19 CEST 2026
- Session initialized at Sat Aug 8 18:46:20 CEST 2026

142
CLAUDE.md
View File

@@ -1,55 +1,117 @@
# CLAUDE.md
@AGENTS.md
OmniRoute — unified AI proxy/router. One endpoint, 290 LLM providers, auto-fallback.
**All project rules live in [`AGENTS.md`](AGENTS.md)** — the single source of truth for every AI
assistant (architecture, conventions, testing, quality gates, git workflow, the 22 Hard Rules,
PII learnings). Read it in full; do not re-add project rules here. Everything below applies ONLY
to Claude Code — operational refinements of rules already defined in `AGENTS.md`.
<!-- CONTEXT BUDGET: this file loads in full on every session and after every compact.
Keep it under 200 lines. Anything deeper goes to .claude/rules/ (path-scoped,
loads only when Claude touches matching files) or .claude/skills/ (loads on demand).
Do not re-inline detail here. -->
## Worktree isolation — Claude Code specifics
## Commands
The full mandatory worktree protocol (base-branch confirmation, `.claude/worktrees/` canonical
path, `cp -al` node_modules, teardown rules) is in `AGENTS.md` → Git Workflow → "Worktree
isolation". Claude-Code-specific points:
```bash
npm install # deps (auto-generates .env from .env.example)
npm run dev # dev server → http://localhost:20128
npm run build # production build (Next.js 16 standalone)
npm run lint # ESLint — 0 errors expected
npm run typecheck:core # TS check (must be clean)
npm run test:unit # Node native runner (most tests)
npm run test:vitest # Vitest (MCP server, autoCombo, cache)
npm run test:coverage # coverage gate 60/60/60/60
npm run check # lint + test
node --import tsx/esm --test tests/unit/your-file.test.ts # single file
```
- Confirm the base branch with the operator via `AskUserQuestion` (Hard Rule #19) unless they
already told you.
- Prefer the native `EnterWorktree` tool — it already creates worktrees under
`.claude/worktrees/` (the canonical path). Create the worktree with the documented `git
worktree add` command, then call `EnterWorktree` with its `path`.
Full test matrix: `CONTRIBUTING.md`. Deep architecture for non-Claude agents: `AGENTS.md`.
## Cross-session safety — Claude Code specifics
## Layout
Hard Rules #19/#21/#22 (in `AGENTS.md`) govern parallel sessions. Operational reminders for this
harness:
Monorepo: `src/` (Next.js 16 app) · `open-sse/` (streaming engine workspace) · `electron/` · `tests/` · `bin/` (CLI).
- **Replicate the `git stash` ban verbatim in the prompt of every subagent that touches git**
(Agent tool / Workflow scripts) — subagents do not inherit this file, and the recorded
recurrence of the stash incident came through a subagent.
- Before merging or pushing to any PR you did not create _this session_, run `git worktree list`
and re-check `gh pr view <N> --json state,headRefOid` (Hard Rule #22b).
- End every session with the main checkout on the branch it started on.
| Layer | Location | Purpose |
| ------------- | ----------------------- | -------------------------------------- |
| API Routes | `src/app/api/v1/` | App Router entry points |
| Handlers | `open-sse/handlers/` | Request processing |
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
| Translators | `open-sse/translator/` | OpenAI ↔ Claude ↔ Gemini |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching |
| Database | `src/lib/db/` | SQLite domain modules (130 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback |
| MCP Server | `open-sse/mcp-server/` | 104 tools, 3 transports, 31 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
| Skills | `src/lib/skills/` | Sandbox skill framework |
| Memory | `src/lib/memory/` | Persistent conversational memory |
## Superpowers / planning artifacts — path overrides
Request pipeline: `route → CORS → Zod validation → optional auth → API-key policy → injection guard → handleChatCore() → cache → rate limit → combo routing → translate → executor → upstream fetch (retry/backoff) → translate back → SSE or JSON`. No global Next.js middleware; interception is per-route.
The `_tasks/` convention is defined in `AGENTS.md` → "Planning & Research Artifacts". The
superpowers skills ship with defaults that point at `docs/…` — those defaults are **overridden
here**. When a superpowers skill announces a path like "saved to `docs/superpowers/plans/…`",
rewrite it to the `_tasks/…` equivalent before writing:
## Hard rules
| Artifact (skill) | Default (do NOT use) | Save here instead |
| ---------------------------------- | ------------------------- | ------------------------------------------------------------- |
| Plans (`writing-plans`) | `docs/superpowers/plans/` | `_tasks/superpowers/plans/YYYY-MM-DD-<feature>.md` |
| Specs / design (`brainstorming`) | `docs/superpowers/specs/` | `_tasks/superpowers/specs/YYYY-MM-DD-<topic>-design.md` |
| Research (`deep-research`, ad-hoc) | `docs/research/` | `_tasks/research/…` |
| Hand-offs (`/handoff`) | — | `_tasks/hands-off/<YYYY-MM-DD>_<branch>_v<versão>_sess-<id>/` |
Violating any of these is a blocking defect. Numbers are stable — cite them in review.
Commit those artifacts inside the `_tasks/` repo (`git -C _tasks …`), never in the main repo.
1. Never commit secrets or credentials.
2. Never add logic to `src/lib/localDb.ts` (re-export layer only); never barrel-import from it.
3. Never use `eval()` / `new Function()` / implied eval.
4. Never commit directly to `main`.
5. Never write raw SQL in routes or handlers — go through `src/lib/db/` modules.
6. Never silently swallow errors in SSE streams.
7. Always validate inputs with Zod.
8. Always include or update tests in the same PR that changes production code in `src/`, `open-sse/`, `electron/`, `bin/`.
9. Coverage must not regress below `quality-baseline.json`; absolute floor 60/60/60/60.
10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as literals — use `resolvePublicCred()`.
12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor / MCP responses — use `buildErrorBody()` or `sanitizeErrorMessage()`.
13. Never string-interpolate external paths into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option.
14. Never dismiss a CodeQL / secret-scanning alert without checking the sanitizer docs first and recording the justification.
15. Never expose child-process-spawning routes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification.
16. Never credit an AI assistant in commit/PR metadata — no `Co-Authored-By` naming an AI/bot, no "Generated with …" footers, anywhere. This overrides any harness default; strip it before pushing. Human collaborators are still credited normally.
17. Never expose `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification.
18. Every bug fix ships with a failing-then-passing test (TDD, preferred) **or** a documented live test on the production VPS `192.168.0.15`. "Worked locally" is not validation.
19. Never develop on the shared main checkout. Every task gets its own worktree under `.claude/worktrees/`, on its own branch, cut from a base branch the operator confirmed. → skill `omniroute-worktree`
20. PII redaction/sanitization is opt-in. `PII_REDACTION_ENABLED` and `PII_RESPONSE_SANITIZATION` must keep `defaultValue: "false"`.
21. During an active `release-freeze`, never merge into the frozen `release/vX.Y.Z`; retarget to the highest `release/v*` and verify the retarget. Only `/generate-release` may raise or lift a freeze. → skill `omniroute-release-freeze`
22. Cross-session safety — many parallel sessions work this repo:
- **Never `git stash` / `git stash pop`, anywhere in this repo, including inside a worktree and inside any subagent you dispatch.** The stash lives in the shared object store and clobbers other sessions' uncommitted work. Compare against a base with `git show <ref>:<path>` or `git diff <ref> -- <path>` instead. Repeat this ban verbatim in the prompt of every subagent that touches git.
- Never merge, push, rebase, or force-push a PR / branch / worktree another session owns. Check `git worktree list` and `gh pr view <N> --json state,headRefOid` before touching a PR you did not open this session.
## Base-green before opening PRs
## Conventions (always-on subset)
Before cutting a branch or opening a PR, run the base-green check (`AGENTS.md` → Git Workflow →
"Base-green check"; project skills reference it as `.agents/skills/_shared/base-green.md`). A PR
opened while the base tip is red must carry `⚠️ base-red inherited: #<issue>` in its body. To
drain an accumulated red state (base tip + red PRs), use the `/sweep-reds` skill.
- 2 spaces, semicolons, double quotes, 100 cols, es5 trailing commas (Prettier via lint-staged).
- Imports: external → internal (`@/`, `@omniroute/open-sse`) → relative.
- Naming: files camelCase/kebab, components PascalCase, constants UPPER_SNAKE.
- `no-explicit-any` is an **error** in `open-sse/` and `tests/`; pre-existing violations are frozen in `config/quality/eslint-suppressions.json`.
- TypeScript `strict: false`, target ES2022, module esnext, resolution bundler. Prefer explicit types.
- Commits: Conventional Commits, `feat(db): …`. Branch prefixes `feat/ fix/ refactor/ docs/ test/ chore/`.
## Environment
Node ≥22 <23 || ≥24 <27, ESM — the only supported runtime. Bun 1.3.14 is pinned as an exact devDependency for an allow-listed set of gate/generator scripts and the `test:bun:db` smoke only; do not widen it to install, build, or the main test runners. Port 20128. `DATA_DIR` defaults to `~/.omniroute/`. Aliases: `@/*``src/`, `@omniroute/open-sse``open-sse/`.
## Planning artifacts
`_tasks/` is a separate, gitignored git repo and is the only place for plans, specs, research, and hand-offs. Never write superpowers / planning / research output under `docs/` or the repo root, even when a skill's default says `docs/superpowers/…` — rewrite the path to `_tasks/…` before writing, and commit inside `_tasks/` (`git -C _tasks …`).
## Where the rest lives
Do not ask for these upfront — they load themselves when relevant.
**Path-scoped rules** (`.claude/rules/`, auto-load when you open matching files): `database.md`, `api-routes.md`, `open-sse.md`, `resilience.md`, `security-sensitive.md`, `pii.md`, `testing.md`.
**Skills** (`.claude/skills/`, load on demand): `omniroute-worktree`, `omniroute-release-freeze`, `omniroute-extend` (add a provider / route / DB module / MCP tool / A2A skill / cloud agent / embedded service), `omniroute-quality-gates`.
**Docs** — read the matching deep-dive before any non-trivial change:
| Area | Doc |
| ---------------------------------------------- | ---------------------------------------------------------------------------- |
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture / engineering reference | `docs/architecture/ARCHITECTURE.md`, `CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (13-factor, 19 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills / Memory / Cloud agents | `docs/frameworks/SKILLS.md`, `MEMORY.md`, `CLOUD_AGENT.md` |
| Guardrails / public creds / error sanitization | `docs/security/GUARDRAILS.md`, `PUBLIC_CREDS.md`, `ERROR_SANITIZATION.md` |
| Authorization / route guard tiers | `docs/architecture/AUTHZ_GUIDE.md`, `docs/security/ROUTE_GUARD_TIERS.md` |
| Agent protocols / MCP / A2A | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`, `MCP-SERVER.md`, `A2A-SERVER.md` |
| API reference | `docs/reference/API_REFERENCE.md` + `docs/openapi.yaml` |
| Release flow / embedded services | `docs/ops/RELEASE_CHECKLIST.md`, `docs/frameworks/EMBEDDED-SERVICES.md` |
| Quality gates (~48 scripts) | `docs/architecture/QUALITY_GATES.md` |

57
ecosystem.config.cjs Normal file
View File

@@ -0,0 +1,57 @@
module.exports = {
apps: [
{
name: "omniroute",
script: "npm",
args: "run start",
cwd: "/Users/work/hermes-sandbox/Hermes_Omni",
autorestart: true,
watch: false,
min_uptime: "10s",
max_restarts: 10,
exp_backoff_restart_delay: 100,
restart_delay: 5000,
max_memory_restart: "700M",
kill_timeout: 10000,
listen_timeout: 10000,
time: true,
merge_logs: true,
env: {
NODE_ENV: "production"
}
},
{
name: "omniroute-tunnel",
script: "npm",
args: "run tunnel",
cwd: "/Users/work/hermes-sandbox/Hermes_Omni",
autorestart: true,
watch: false,
min_uptime: "10s",
max_restarts: 10,
exp_backoff_restart_delay: 1000,
restart_delay: 5000,
max_memory_restart: "500M",
time: true,
merge_logs: true
},
{
name: "hermes-tg-bot",
script: "/Users/work/hermes-sandbox/Hermes_Omni/scripts/serpentos_logic/tg-bot.cjs",
cwd: "/Users/work/hermes-sandbox/Hermes_Omni",
autorestart: true,
watch: false,
min_uptime: "10s",
max_restarts: 10,
exp_backoff_restart_delay: 500,
restart_delay: 5000,
max_memory_restart: "500M",
kill_timeout: 10000,
time: true,
merge_logs: true,
env: {
NODE_ENV: "production"
}
}
]
};

View File

@@ -0,0 +1,29 @@
import { getDbInstance } from "../../src/lib/db/core.js";
import { randomUUID } from "crypto";
import { encryptConnectionFields } from "../../src/lib/db/encryption.js";
const db = getDbInstance();
const token = process.env.OPENROUTER_API_KEY;
if (token) {
const fields = encryptConnectionFields({ key: token });
const stmt = db.prepare(`
INSERT INTO provider_connections
(id, provider_id, account_id, name, connection_fields, encrypted_credentials, credentials_iv, credentials_tag, is_active, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`);
stmt.run(
randomUUID(),
"openrouter",
"system_account",
"doppler_openrouter",
"{}",
fields.encryptedData,
fields.iv,
fields.authTag
);
console.log("Added openrouter credentials successfully.");
} else {
console.log("No OPENROUTER_API_KEY provided.");
}

View File

@@ -0,0 +1,18 @@
import { createProviderConnection } from "../../src/lib/db/providers.ts";
async function run() {
const token = process.env.OPENROUTER_API_KEY;
if (!token) {
console.log("No token.");
process.exit(1);
}
await createProviderConnection({
provider: "openrouter",
authType: "apikey",
apiKey: token,
name: "doppler_openrouter",
});
console.log("Added provider.");
}
run().catch(console.error);

View File

@@ -13,7 +13,7 @@ MODELS = [
"god-mode"
]
url = "http://localhost:20131/v1/chat/completions"
url = "http://localhost:3001/api/v1/chat/completions"
working_models = []
failed_models = []
@@ -40,6 +40,10 @@ for model in MODELS:
elapsed = time.time() - start_time
print(f"✅ OK ({elapsed:.2f}s) -> '{content}'")
working_models.append(model)
except urllib.error.HTTPError as e:
body = e.read().decode('utf-8', errors='replace')
print(f"❌ FAILED HTTP {e.code}: {body}")
failed_models.append(model)
except Exception as e:
print(f"❌ FAILED: {e}")
failed_models.append(model)

View File

@@ -0,0 +1,129 @@
const https = require('https');
const http = require('http');
const { execSync } = require('child_process');
let TOKEN = process.env.TELEGRAM_BOT_TOKEN;
// Try fetching from doppler if missing
if (!TOKEN) {
try {
TOKEN = execSync("doppler secrets get TELEGRAM_BOT_TOKEN --project serpent --config prd --plain", { encoding: 'utf-8' }).trim();
} catch (e) {
console.error("Failed to get token from Doppler");
process.exit(1);
}
}
let offset = 0;
function httpRequest(url, method = 'GET', body = null, headers = {}) {
return new Promise((resolve, reject) => {
const isHttps = url.startsWith('https');
const mod = isHttps ? https : http;
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.pathname + parsedUrl.search,
method: method,
headers: { ...headers },
timeout: 30000,
};
if (body) {
const data = typeof body === 'string' ? body : JSON.stringify(body);
options.headers['Content-Length'] = Buffer.byteLength(data);
if (!options.headers['Content-Type']) options.headers['Content-Type'] = 'application/json';
}
const req = mod.request(options, (res) => {
let d = '';
res.on('data', c => d += c);
res.on('end', () => resolve({ status: res.statusCode, data: d }));
});
req.on('error', reject);
req.on('timeout', () => { req.destroy(); reject(new Error('Timeout')); });
if (body) req.write(typeof body === 'string' ? body : JSON.stringify(body));
req.end();
});
}
async function sendMessage(chatId, text) {
try {
await httpRequest(`https://api.telegram.org/bot${TOKEN}/sendMessage`, 'POST', {
chat_id: chatId,
text: text,
parse_mode: 'HTML'
});
} catch (e) {
console.error("Failed to send message:", e.message);
}
}
async function processMessage(msg) {
if (!msg || !msg.text) return;
const chatId = msg.chat.id;
const text = msg.text;
console.log(`[Message] ${chatId}: ${text}`);
if (text === '/start' || text === '/help') {
await sendMessage(chatId, "<b>OmniRoute Bot</b>\nSend me any prompt to process via OmniRoute models.");
return;
}
await sendMessage(chatId, "🧠 Думаю...");
try {
const response = await httpRequest("http://localhost:3001/api/v1/chat/completions", "POST", {
model: "auto",
messages: [{ role: "user", content: text }],
max_tokens: 2048
});
if (response.status === 200) {
const data = JSON.parse(response.data);
const reply = data.choices?.[0]?.message?.content || "Empty response from OmniRoute.";
await sendMessage(chatId, reply);
} else {
const err = JSON.parse(response.data);
await sendMessage(chatId, `❌ OmniRoute Error:\n<pre>${err.error?.message || response.data}</pre>`);
}
} catch (e) {
await sendMessage(chatId, `❌ Connection error: ${e.message}`);
}
}
async function poll() {
try {
const res = await httpRequest(`https://api.telegram.org/bot${TOKEN}/getUpdates?timeout=30&offset=${offset}`);
if (res.status === 200) {
const data = JSON.parse(res.data);
if (data.ok && data.result.length > 0) {
for (const update of data.result) {
offset = update.update_id + 1;
if (update.message) {
await processMessage(update.message);
}
}
}
} else if (res.status === 409) {
console.error("Conflict! Another bot is running or webhook is active.");
// delete webhook just in case
await httpRequest(`https://api.telegram.org/bot${TOKEN}/deleteWebhook?drop_pending_updates=true`);
}
} catch (e) {
console.error("Poll error:", e.message);
}
setTimeout(poll, 1000);
}
// Clear webhook before polling
httpRequest(`https://api.telegram.org/bot${TOKEN}/deleteWebhook?drop_pending_updates=true`)
.then(() => {
console.log("OmniRoute Telegram Bot started.");
poll();
})
.catch(console.error);

View File

@@ -0,0 +1,71 @@
import { NextResponse } from "next/server";
import { headers } from "next/headers";
export async function POST(req: Request) {
try {
const data = await req.json();
console.log("[Telegram Webhook] Received update:", JSON.stringify(data));
const message = data.message;
if (!message || !message.text) {
return NextResponse.json({ ok: true });
}
const chatId = message.chat.id;
const text = message.text;
const botToken = process.env.TELEGRAM_BOT_TOKEN;
if (!botToken) {
console.error("[Telegram Webhook] TELEGRAM_BOT_TOKEN not configured");
return NextResponse.json({ ok: false, error: "Missing bot token" }, { status: 500 });
}
// Call OmniRoute's internal chat completions (since we are in the same Next.js app, we can fetch from localhost:3001)
// First, let's just send a thinking message
const sendThinking = fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text: "🧠 Thinking..." }),
});
// Then process the chat
const processChat = async () => {
try {
const response = await fetch("http://localhost:3001/api/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "auto", // OmniRoute can route 'auto'
messages: [{ role: "user", content: text }],
max_tokens: 2048,
}),
});
let replyText = "Error communicating with AI.";
if (response.ok) {
const resJson = await response.json();
replyText = resJson.choices?.[0]?.message?.content || "No response generated.";
} else {
const errorJson = await response.json().catch(() => ({}));
replyText = `API Error ${response.status}: ${JSON.stringify(errorJson)}`;
}
await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: chatId, text: replyText, parse_mode: "HTML" }),
});
} catch (err) {
console.error("[Telegram Webhook] Error processing chat:", err);
}
};
// Run process asynchronously without blocking the webhook response
Promise.allSettled([sendThinking, processChat()]);
return NextResponse.json({ ok: true });
} catch (error) {
console.error("[Telegram Webhook] Error processing webhook:", error);
return NextResponse.json({ ok: false, error: "Internal error" }, { status: 500 });
}
}