diff --git a/.agents/skills/deploy-vps-akamai/SKILL.md b/.agents/skills/deploy-vps-akamai/SKILL.md new file mode 100644 index 0000000000..a0f0cd25c6 --- /dev/null +++ b/.agents/skills/deploy-vps-akamai/SKILL.md @@ -0,0 +1,45 @@ +--- +name: deploy-vps-akamai-cx +description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35) +--- + +# Deploy to Akamai VPS Workflow + +Deploy OmniRoute to the Akamai VPS using `npm pack + scp` + PM2. + +## Codex Execution Notes + +- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` only for independent commands. Do not parallelize dependent build, copy, install, restart, and verification steps. +- Report each remote result explicitly before finishing. + +**Akamai VPS:** `69.164.221.35` +**Process manager:** PM2 (`omniroute`) +**Port:** `20128` + +## Steps + +### 1. Build + pack locally + +// turbo + +```bash +cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts +``` + +### 2. Copy to Akamai VPS and install + +// turbo-all + +```bash +scp omniroute-*.tgz root@69.164.221.35:/tmp/ +``` + +```bash +ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" +``` + +### 3. Verify the deployment + +```bash +curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/ +``` diff --git a/.agents/skills/deploy-vps-both/SKILL.md b/.agents/skills/deploy-vps-both/SKILL.md new file mode 100644 index 0000000000..867fba8f9a --- /dev/null +++ b/.agents/skills/deploy-vps-both/SKILL.md @@ -0,0 +1,56 @@ +--- +name: deploy-vps-both-cx +description: Deploy the latest OmniRoute code to BOTH the Akamai VPS and the Local VPS +--- + +# Deploy to VPS (Both) Workflow + +Deploy OmniRoute to the production VPSs using `npm pack + scp` + PM2. + +**Akamai VPS:** `69.164.221.35` +**Local VPS:** `192.168.0.15` +**Process manager:** PM2 (`omniroute`) +**Port:** `20128` +**PM2 entry:** `/usr/lib/node_modules/omniroute/app/server.js` + +> [!IMPORTANT] +> The npm registry rejects packages > 100MB, so deployment uses **npm pack + scp**. + +## Codex Execution Notes + +- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` only for independent commands. +- Build/package once first. After the artifact exists, copy/install/verify on Akamai and Local may run in parallel if they do not depend on each other. +- Report each VPS result explicitly before finishing. + +## Steps + +### 1. Build + pack locally + +// turbo + +```bash +cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts +``` + +### 2. Copy to both VPS and install + +// turbo-all + +```bash +scp omniroute-*.tgz root@69.164.221.35:/tmp/ && scp omniroute-*.tgz root@192.168.0.15:/tmp/ +``` + +```bash +ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" +``` + +```bash +ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'" +``` + +### 3. Verify the deployment + +```bash +curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/ +curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/ +``` diff --git a/.agents/skills/implement-features/SKILL.md b/.agents/skills/implement-features/SKILL.md new file mode 100644 index 0000000000..ff88804039 --- /dev/null +++ b/.agents/skills/implement-features/SKILL.md @@ -0,0 +1,713 @@ +--- +name: implement-features-cx +description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors +--- + +# /implement-features — Feature Request Harvest, Research & Implementation Workflow + +## Overview + +A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them. + +## Codex Execution Notes + +- Treat `// turbo` / `// turbo-all` as instructions to use `multi_tool_use.parallel` for independent reads, checks, and GitHub calls. +- Approval gates are hard stops. Present the report/plan in the final response and do not move to implementation phases until the user explicitly approves. +- Keep harvest/research bounded enough to produce the approval report quickly; do not start implementation while still in report phases. + +**Output directory structure:** + +``` +_ideia/ +├── viable/ # Features approved for implementation +│ ├── need_details/ # ❓ Good idea but waiting for author clarification (issues stay OPEN) +│ │ └── 1015-warp-terminal-mitm.md +│ ├── 1046-native-playground.md # ✅ Ready — researched and planned +│ └── 1046-native-playground.requirements.md +├── defer/ # ⏭️ Good ideas deferred for future cycles (issues CLOSED) +│ └── 1041-smart-auto-combos.md +└── notfit/ # ❌ Out of scope / already exists (issues CLOSED) + └── 945-telegram-integration.md + +_tasks/features-vX.Y.Z/ # Implementation plans (per-release) +└── 1046-native-playground.plan.md +``` + +> **LIFECYCLE RULE:** `viable/` files are **DELETED** once the feature is implemented — they are not moved. Only unimplemented features live in `viable/` (or `viable/need_details/`). Files in `defer/` and `notfit/` remain as permanent reference. + +> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5. + +--- + +## Phase 1 — Harvest: Collect & Catalog Feature Ideas + +### 1.1 Identify the Repository + +// turbo + +- Run: `git -C remote get-url origin` to extract owner/repo. + +### 1.2 Ensure Release Branch Exists + +// turbo + +Before doing any work, ensure you are on the current release branch: + +```bash +# Check current branch +git branch --show-current + +# If on main, determine next version and create the release branch +VERSION=$(node -p "require('./package.json').version") +NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)") +git checkout -b release/v$NEXT +npm version patch --no-git-tag-version +npm install +``` + +If already on a `release/vX.Y.Z` branch, continue working there. + +### 1.3 Fetch ALL Open Feature Requests + +// turbo-all + +**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below. + +**Step 1 — Get Issue numbers only** (small output, never truncated): + +```bash +# Fetch issues with feature/enhancement labels +gh issue list --repo / --state open -l "enhancement" --limit 500 --json number --jq '.[].number' + +# Also check for [Feature] in title (common pattern when no labels are set) +gh issue list --repo / --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number' +``` + +- Merge both lists, deduplicate. Count and confirm the total. + +**Step 2 — Fetch full metadata for each Issue** (one call per issue): + +```bash +gh issue view --repo / --json number,title,labels,body,comments,createdAt,author,assignees +``` + +- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images. +- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs. +- **Images**: If the body or comments contain image URLs (`![...](...)` or `https://...png/jpg/gif`), note them — they may contain UI mockups, wireframes, or architecture diagrams that are essential to understanding the request. +- You may batch these into parallel calls (up to 4 at a time). +- Sort by oldest first (FIFO). + +### 1.4 Create Idea Files (initially in `_ideia/` root) + +For each feature request, create a structured idea file in `/_ideia/`: + +**Filename convention**: `-.md` +Example: `1046-native-playground.md`, `1041-smart-auto-combos.md` + +#### 1.4a — If the idea file does NOT exist yet, create it: + +```markdown +# Feature: + +> GitHub Issue: #<NUMBER> — opened by @<author> on <date> +> Status: 📋 Cataloged | Priority: TBD + +## 📝 Original Request + +<Paste the FULL issue body here, preserving all formatting, images, and code blocks> + +## 💬 Community Discussion + +<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised> + +### Participants + +- @<author> — Original requester +- @<commenter1> — <brief role/opinion> +- ... + +### Key Points + +- <bullet list of the most important discussion points> +- <agreements reached> +- <objections raised> + +## 🎯 Refined Feature Description + +<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.> + +### What it solves + +- <problem 1> +- <problem 2> + +### How it should work (high level) + +1. <step 1> +2. <step 2> +3. ... + +### Affected areas + +- <list of codebase areas, modules, files likely affected> + +## 📎 Attachments & References + +- <any image URLs, mockup links, or external references from the issue> + +## 🔗 Related Ideas + +- <links to related \_ideia/ files if any overlap found> +``` + +#### 1.4b — If the idea file ALREADY exists, update it: + +- Append new comments from the issue to the **Community Discussion** section. +- Update the **Refined Feature Description** if new information changes the understanding. +- Add any new **Related Ideas** cross-references found. +- **Do NOT overwrite** existing content — append and enrich it. + +### 1.5 Cross-Reference & Deduplication + +After processing all issues: + +- Scan all `_ideia/*.md` files for overlapping features. +- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both. +- If one is a strict subset of another, note it in the smaller file: `> ℹ️ This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.` + +--- + +## Phase 2 — Research: Find Solutions & Build Requirements + +For each cataloged idea that is **viable** (aligns with the project's goals): + +### 2.1 Viability Pre-Check + +Before investing in research, quickly assess: + +- [ ] Does this feature align with the project's goals and architecture? +- [ ] Is it technically feasible with the current codebase? +- [ ] Does it duplicate existing functionality? +- [ ] Would it introduce breaking changes or security risks? +- [ ] Is there enough detail to understand what's needed? + +**Verdict options:** + +| Verdict | When | Action | +| --------------------- | ------------------------------------- | --------------------------- | +| ✅ **VIABLE** | Good idea, enough context | Proceed to Research | +| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author | +| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research | +| ❌ **NOT FIT** | Doesn't fit the project | Explain why | +| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature | + +### 2.2 Internet Research (for VIABLE features) + +For each viable feature, perform systematic research: + +**Step 1 — Web search for similar implementations:** + +``` +WebSearch("how to implement <feature description> in <tech stack>") +WebSearch("<feature keyword> implementation nextjs typescript 2025 2026") +WebSearch("<feature keyword> open source library npm") +``` + +**Step 2 — Find reference Git repositories:** + +``` +WebSearch("site:github.com <feature keyword> <tech stack> stars:>100") +WebSearch("github <feature keyword> implementation recently updated 2026") +``` + +- Find **up to 10 relevant repositories**, sorted by most recently updated. +- For each repository: + - Note the repo URL, star count, last commit date + - Read its README and relevant source files via `WebFetch` + - Extract the architectural approach, patterns used, and key code snippets + +**Step 3 — Read API docs and standards:** + +If the feature involves an external API, protocol, or standard: + +- Find and read the official documentation +- Note version requirements, authentication patterns, rate limits + +### 2.3 Create Requirements File + +For each researched feature, create a requirements file alongside its idea file: + +**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md` + +```markdown +# Requirements: <Feature Title> + +> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md) +> Research Date: <YYYY-MM-DD> +> Verdict: ✅ VIABLE + +## 🔍 Research Summary + +<Brief summary of what was found during research> + +## 📚 Reference Implementations + +| # | Repository | Stars | Last Updated | Approach | Relevance | +| --- | ---------------- | ----- | ------------ | -------- | ------------ | +| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low | +| 2 | ... | | | | | + +### Key Patterns Found + +- <pattern 1 with code snippet or link> +- <pattern 2> + +## 📐 Proposed Solution Architecture + +### Approach + +<Describe the chosen approach based on research findings> + +### New Files + +| File | Purpose | +| --------------------- | ------------- | +| `path/to/new/file.ts` | <description> | + +### Modified Files + +| File | Changes | +| -------------------------- | -------------- | +| `path/to/existing/file.ts` | <what changes> | + +### Database Changes + +- <migrations needed, if any> + +### API Changes + +- <new/modified endpoints, if any> + +### UI Changes + +- <new/modified pages/components, if any> + +## ⚙️ Implementation Effort + +- **Estimated complexity**: Low / Medium / High / Very High +- **Estimated files changed**: ~N +- **Dependencies needed**: <new npm packages, if any> +- **Breaking changes**: Yes/No — <details> +- **i18n impact**: <number of new translation keys> +- **Test coverage needed**: <brief description> + +## ⚠️ Open Questions + +- <question 1> +- <question 2> + +## 🔗 External References + +- <documentation URLs> +- <API references> +``` + +--- + +## Phase 2.5 — Organize & Respond: Sort Files and Post GitHub Comments + +### 2.5.1 Create Directory Structure + +// turbo + +```bash +mkdir -p <project_root>/_ideia/viable +mkdir -p <project_root>/_ideia/viable/need_details +mkdir -p <project_root>/_ideia/defer +mkdir -p <project_root>/_ideia/notfit +``` + +### 2.5.2 Move Idea Files to Category Subdirectories + +After classification, move EVERY idea file to its correct subdirectory: + +```bash +# ✅ VIABLE — move idea + requirements files +mv _ideia/<NUMBER>-*.md _ideia/viable/ +mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/ + +# ❓ NEEDS DETAIL — viable but waiting for author response +mv _ideia/<NUMBER>-*.md _ideia/viable/need_details/ + +# ⏭️ DEFER — move idea files only +mv _ideia/<NUMBER>-*.md _ideia/defer/ + +# ❌ NOT FIT & 🔁 ALREADY EXISTS — move idea files only +mv _ideia/<NUMBER>-*.md _ideia/notfit/ +``` + +No files should remain in `_ideia/` root after this step (except subdirectories). + +### 2.5.3 Post GitHub Comments by Category + +**Each category has a specific comment template and action:** + +--- + +#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue + +// turbo + +The feature already exists in the system. Explain WHERE it is and HOW to use it. + +```markdown +Hi @<author>! Thanks for the suggestion! 🙏 + +Great news — this functionality **already exists** in OmniRoute: + +**📍 Where to find it:** <exact dashboard path or settings location> + +**🔧 How to use it:** + +1. <step 1> +2. <step 2> +3. <step 3> + +If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help! + +Closing this as the feature is already available. 🎉 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ⏭️ DEFER — Comment + CLOSE issue + +// turbo + +Thank the user, explain the idea was cataloged, and that we'll study it before implementing. + +```markdown +Hi @<author>! Thanks for this thoughtful feature request! 🙏 + +We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog. + +Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions. + +**What happens next:** + +- Your idea is saved in our internal feature backlog +- We'll conduct architecture studies when this area is prioritized +- We'll notify you here when development begins + +Thank you for contributing to OmniRoute's roadmap! Your input helps shape the product. 🚀 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ❌ NOT FIT — Comment + CLOSE issue + +// turbo + +Politely explain why the feature doesn't fit the project scope. + +```markdown +Hi @<author>! Thanks for the suggestion! 🙏 + +After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router. + +**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself."> + +**Alternative:** <suggest an alternative approach if possible> + +We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ❓ NEEDS DETAIL — Comment (keep OPEN) + +// turbo + +Ask for the specific missing details needed. + +```markdown +Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏 + +To move forward, we need a few more details: + +1. <specific question 1> +2. <specific question 2> +3. <specific question 3> + +If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution. + +Looking forward to your response! 🚀 +``` + +--- + +#### For ✅ VIABLE — Comment (keep OPEN) + +// turbo + +Thank the user, confirm we've cataloged their idea, and explain it may be implemented in future versions. + +```markdown +Hi @<author>! Thanks for the great feature suggestion! 🙏 + +We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog. + +**Status:** 📋 Cataloged for future implementation + +This feature may be included in upcoming releases. We'll **respond to this issue and tag you** as soon as implementation begins so you can test it. + +Thank you for helping improve OmniRoute! 🚀 +``` + +**⚠️ Do NOT close viable issues — they remain OPEN for tracking.** + +--- + +## Phase 3 — Report: Present Findings to User + +### 3.1 🛑 MANDATORY STOP — Present Consolidated Report + +After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. Do NOT proceed to implementation. + +Present a structured report containing: + +#### 3.1a — Feature Summary Table + +| # | Issue | Title | Verdict | Location | Action | +| --- | ----- | ----- | --------------- | ----------------------------- | ----------------------------- | +| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Issue OPEN, comment posted | +| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Issue CLOSED with explanation | +| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Issue CLOSED with explanation | +| 4 | #N | Title | 🔁 EXISTS | `_ideia/notfit/` | Issue CLOSED with guidance | +| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/viable/need_details/` | Issue OPEN, questions posted | + +#### 3.1b — Viable Features Detail + +For each VIABLE feature, provide a brief paragraph: + +- What was found during research +- The proposed approach +- Key risks or unknowns +- Which reference repositories were most useful + +#### 3.1c — Issues Requiring Author Feedback + +For features marked ❓ NEEDS DETAIL, list: + +- What specific information is missing +- What examples or repository references would help + +#### 3.1d — Ask for User Confirmation + +End the report with: + +> **Ready to proceed with implementation?** +> +> - Reply **"sim"** or **"yes"** to generate full implementation plans for all VIABLE features. +> - Reply with specific issue numbers to select only certain features. +> - Reply **"não"** or **"no"** to stop here. + +--- + +## Phase 4 — Plan: Generate Implementation Plans (after user says "yes") + +> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.** + +### 4.1 Create Task Directory + +```bash +mkdir -p <project_root>/_tasks/features-vX.Y.Z/ +``` + +### 4.2 Generate One Implementation Plan Per Feature + +For each VIABLE feature approved by the user, create: + +**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md` + +```markdown +# Implementation Plan: <Feature Title> + +> Issue: #<NUMBER> +> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md) +> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md) +> Branch: `release/vX.Y.Z` + +## Overview + +<Brief description of what will be built> + +## Pre-Implementation Checklist + +- [ ] Read all related source files listed below +- [ ] Confirm no conflicts with in-flight PRs +- [ ] Verify database migration numbering + +## Implementation Steps + +### Step 1: <Title> + +**Files:** + +- `path/to/file.ts` — <what to change> + +**Details:** +<Detailed description of the change, including code patterns to follow, function signatures, etc.> + +### Step 2: <Title> + +... + +### Step N: Tests + +**New test files:** + +- `tests/unit/<test-file>.test.mjs` — <what to test> + +**Test cases:** + +- [ ] <test case 1> +- [ ] <test case 2> + +### Step N+1: i18n + +**Translation keys to add:** + +- `<namespace>.<key>` — "<English value>" + +### Step N+2: Documentation + +- [ ] Update CHANGELOG.md +- [ ] Update relevant docs/ files + +## Verification Plan + +1. Run `npm run build` — must pass +2. Run `npm test` — all tests must pass +3. Run `npm run lint` — no new errors +4. <Manual verification steps> + +## Commit Plan +``` + +feat: <description> (#<NUMBER>) + +``` + +``` + +### 4.3 Present Plans for Final Approval + +Present a summary of all generated plans: + +> **Implementation plans generated:** +> +> | # | Feature | Plan File | Steps | Effort | +> | --- | ------- | ---------------------------------------- | ------- | ------ | +> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium | +> +> Reply **"sim"** or **"yes"** to begin implementation of all features. +> Reply with specific issue numbers to implement only certain ones. + +--- + +## Phase 5 — Execute: Implement the Plans (after user says "yes") + +> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.** + +### 5.1 Implement Each Feature + +For each approved plan, execute it step by step: + +1. **Follow the plan** — implement exactly as specified in the `.plan.md` file +2. **Build** — Run `npm run build` after each feature to verify compilation +3. **Test** — Run `npm test` to ensure no regressions +4. **Commit** — Commit with: `feat: <description> (#<NUMBER>)` +5. **Update the plan** — Mark completed steps with `[x]` in the plan file +6. **Continue** — Move to the next feature (do NOT switch branches) + +### 5.2 Respond to Authors (Update Viable Issues) + +For each implemented feature, **close the issue with a final comment**: + +````markdown +✅ **Implemented in `release/vX.Y.Z`!** + +Hi @<author>! Great news — your feature request has been implemented! 🎉 + +**What was done:** + +- <bullet list of what was built> + +**How to try it:** + +```bash +git fetch origin && git checkout release/vX.Y.Z +npm install && npm run dev +``` +```` + +This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀 + +```` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +```` + +Then **DELETE the idea file** — it has served its purpose: + +```bash +# ✅ Implemented files are DELETED (not moved) +rm _ideia/viable/<NUMBER>-<title>.md +rm _ideia/viable/<NUMBER>-<title>.requirements.md # if exists +``` + +> **Why delete?** `viable/` only holds features that still NEED to be done. Once implemented, the commit history and CHANGELOG are the source of truth. Keeping the file would be confusing. + +### 5.3 Finalize & Push + +After implementing all approved features: + +1. **Update CHANGELOG.md** on the release branch with all new feature entries +2. Push the release branch: `git push origin release/vX.Y.Z` +3. Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user) + +### 5.4 Final Summary Report + +Present a final summary report to the user: + +| Issue | Title | Verdict | Action | Commit | +| ----- | ----- | --------------- | -------------------------------------------------- | --------- | +| #N | Title | ✅ Implemented | Issue closed, idea file deleted | `abc1234` | +| #N | Title | ⏭️ Deferred | Issue closed + saved in `_ideia/defer/` | — | +| #N | Title | ❌ Not Fit | Issue closed + saved in `_ideia/notfit/` | — | +| #N | Title | 🔁 Exists | Issue closed + saved in `_ideia/notfit/` | — | +| #N | Title | ❓ Needs Detail | Issue OPEN, moved to `_ideia/viable/need_details/` | — | + +Include: + +- Total features harvested +- Total ideas cataloged (`viable/need_details/` + `defer/` + `notfit/`) +- Total features implemented (idea files deleted, issues closed) +- Total features deferred +- Total issues closed +- Total issues left open (needs detail only — viable are closed after implementation) +- Test results (pass/fail count) diff --git a/.agents/workflows/deploy-vps-akamai-ag.md b/.agents/workflows/deploy-vps-akamai-ag.md new file mode 100644 index 0000000000..b25ae3cd99 --- /dev/null +++ b/.agents/workflows/deploy-vps-akamai-ag.md @@ -0,0 +1,39 @@ +--- +description: Deploy the latest OmniRoute code to the Akamai VPS (69.164.221.35) +--- + +# Deploy to Akamai VPS Workflow + +Deploy OmniRoute to the Akamai VPS using `npm pack + scp` + PM2. + +**Akamai VPS:** `69.164.221.35` +**Process manager:** PM2 (`omniroute`) +**Port:** `20128` + +## Steps + +### 1. Build + pack locally + +// turbo + +```bash +cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts +``` + +### 2. Copy to Akamai VPS and install + +// turbo-all + +```bash +scp omniroute-*.tgz root@69.164.221.35:/tmp/ +``` + +```bash +ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" +``` + +### 3. Verify the deployment + +```bash +curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/ +``` diff --git a/.agents/workflows/deploy-vps-both-ag.md b/.agents/workflows/deploy-vps-both-ag.md new file mode 100644 index 0000000000..e1aa4d1def --- /dev/null +++ b/.agents/workflows/deploy-vps-both-ag.md @@ -0,0 +1,49 @@ +--- +description: Deploy the latest OmniRoute code to BOTH the Akamai VPS and the Local VPS +--- + +# Deploy to VPS (Both) Workflow + +Deploy OmniRoute to the production VPSs using `npm pack + scp` + PM2. + +**Akamai VPS:** `69.164.221.35` +**Local VPS:** `192.168.0.15` +**Process manager:** PM2 (`omniroute`) +**Port:** `20128` +**PM2 entry:** `/usr/lib/node_modules/omniroute/app/server.js` + +> [!IMPORTANT] +> The npm registry rejects packages > 100MB, so deployment uses **npm pack + scp**. + +## Steps + +### 1. Build + pack locally + +// turbo + +```bash +cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts +``` + +### 2. Copy to both VPS and install + +// turbo-all + +```bash +scp omniroute-*.tgz root@69.164.221.35:/tmp/ && scp omniroute-*.tgz root@192.168.0.15:/tmp/ +``` + +```bash +ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'" +``` + +```bash +ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'" +``` + +### 3. Verify the deployment + +```bash +curl -s -o /dev/null -w 'AKAMAI HTTP %{http_code}\n' http://69.164.221.35:20128/ +curl -s -o /dev/null -w 'LOCAL HTTP %{http_code}\n' http://192.168.0.15:20128/ +``` diff --git a/.agents/workflows/implement-features-ag.md b/.agents/workflows/implement-features-ag.md new file mode 100644 index 0000000000..afb8b3ed93 --- /dev/null +++ b/.agents/workflows/implement-features-ag.md @@ -0,0 +1,706 @@ +--- +description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors +--- + +# /implement-features — Feature Request Harvest, Research & Implementation Workflow + +## Overview + +A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them. + +**Output directory structure:** + +``` +_ideia/ +├── viable/ # Features approved for implementation +│ ├── need_details/ # ❓ Good idea but waiting for author clarification (issues stay OPEN) +│ │ └── 1015-warp-terminal-mitm.md +│ ├── 1046-native-playground.md # ✅ Ready — researched and planned +│ └── 1046-native-playground.requirements.md +├── defer/ # ⏭️ Good ideas deferred for future cycles (issues CLOSED) +│ └── 1041-smart-auto-combos.md +└── notfit/ # ❌ Out of scope / already exists (issues CLOSED) + └── 945-telegram-integration.md + +_tasks/features-vX.Y.Z/ # Implementation plans (per-release) +└── 1046-native-playground.plan.md +``` + +> **LIFECYCLE RULE:** `viable/` files are **DELETED** once the feature is implemented — they are not moved. Only unimplemented features live in `viable/` (or `viable/need_details/`). Files in `defer/` and `notfit/` remain as permanent reference. + +> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5. + +--- + +## Phase 1 — Harvest: Collect & Catalog Feature Ideas + +### 1.1 Identify the Repository + +// turbo + +- Run: `git -C <project_root> remote get-url origin` to extract owner/repo. + +### 1.2 Ensure Release Branch Exists + +// turbo + +Before doing any work, ensure you are on the current release branch: + +```bash +# Check current branch +git branch --show-current + +# If on main, determine next version and create the release branch +VERSION=$(node -p "require('./package.json').version") +NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)") +git checkout -b release/v$NEXT +npm version patch --no-git-tag-version +npm install +``` + +If already on a `release/vX.Y.Z` branch, continue working there. + +### 1.3 Fetch ALL Open Feature Requests + +// turbo-all + +**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below. + +**Step 1 — Get Issue numbers only** (small output, never truncated): + +```bash +# Fetch issues with feature/enhancement labels +gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number' + +# Also check for [Feature] in title (common pattern when no labels are set) +gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number' +``` + +- Merge both lists, deduplicate. Count and confirm the total. + +**Step 2 — Fetch full metadata for each Issue** (one call per issue): + +```bash +gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees +``` + +- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images. +- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs. +- **Images**: If the body or comments contain image URLs (`![...](...)` or `https://...png/jpg/gif`), note them — they may contain UI mockups, wireframes, or architecture diagrams that are essential to understanding the request. +- You may batch these into parallel calls (up to 4 at a time). +- Sort by oldest first (FIFO). + +### 1.4 Create Idea Files (initially in `_ideia/` root) + +For each feature request, create a structured idea file in `<project_root>/_ideia/`: + +**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md` +Example: `1046-native-playground.md`, `1041-smart-auto-combos.md` + +#### 1.4a — If the idea file does NOT exist yet, create it: + +```markdown +# Feature: <Title from Issue> + +> GitHub Issue: #<NUMBER> — opened by @<author> on <date> +> Status: 📋 Cataloged | Priority: TBD + +## 📝 Original Request + +<Paste the FULL issue body here, preserving all formatting, images, and code blocks> + +## 💬 Community Discussion + +<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised> + +### Participants + +- @<author> — Original requester +- @<commenter1> — <brief role/opinion> +- ... + +### Key Points + +- <bullet list of the most important discussion points> +- <agreements reached> +- <objections raised> + +## 🎯 Refined Feature Description + +<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.> + +### What it solves + +- <problem 1> +- <problem 2> + +### How it should work (high level) + +1. <step 1> +2. <step 2> +3. ... + +### Affected areas + +- <list of codebase areas, modules, files likely affected> + +## 📎 Attachments & References + +- <any image URLs, mockup links, or external references from the issue> + +## 🔗 Related Ideas + +- <links to related \_ideia/ files if any overlap found> +``` + +#### 1.4b — If the idea file ALREADY exists, update it: + +- Append new comments from the issue to the **Community Discussion** section. +- Update the **Refined Feature Description** if new information changes the understanding. +- Add any new **Related Ideas** cross-references found. +- **Do NOT overwrite** existing content — append and enrich it. + +### 1.5 Cross-Reference & Deduplication + +After processing all issues: + +- Scan all `_ideia/*.md` files for overlapping features. +- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both. +- If one is a strict subset of another, note it in the smaller file: `> ℹ️ This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.` + +--- + +## Phase 2 — Research: Find Solutions & Build Requirements + +For each cataloged idea that is **viable** (aligns with the project's goals): + +### 2.1 Viability Pre-Check + +Before investing in research, quickly assess: + +- [ ] Does this feature align with the project's goals and architecture? +- [ ] Is it technically feasible with the current codebase? +- [ ] Does it duplicate existing functionality? +- [ ] Would it introduce breaking changes or security risks? +- [ ] Is there enough detail to understand what's needed? + +**Verdict options:** + +| Verdict | When | Action | +| --------------------- | ------------------------------------- | --------------------------- | +| ✅ **VIABLE** | Good idea, enough context | Proceed to Research | +| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author | +| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research | +| ❌ **NOT FIT** | Doesn't fit the project | Explain why | +| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature | + +### 2.2 Internet Research (for VIABLE features) + +For each viable feature, perform systematic research: + +**Step 1 — Web search for similar implementations:** + +``` +WebSearch("how to implement <feature description> in <tech stack>") +WebSearch("<feature keyword> implementation nextjs typescript 2025 2026") +WebSearch("<feature keyword> open source library npm") +``` + +**Step 2 — Find reference Git repositories:** + +``` +WebSearch("site:github.com <feature keyword> <tech stack> stars:>100") +WebSearch("github <feature keyword> implementation recently updated 2026") +``` + +- Find **up to 10 relevant repositories**, sorted by most recently updated. +- For each repository: + - Note the repo URL, star count, last commit date + - Read its README and relevant source files via `WebFetch` + - Extract the architectural approach, patterns used, and key code snippets + +**Step 3 — Read API docs and standards:** + +If the feature involves an external API, protocol, or standard: + +- Find and read the official documentation +- Note version requirements, authentication patterns, rate limits + +### 2.3 Create Requirements File + +For each researched feature, create a requirements file alongside its idea file: + +**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md` + +```markdown +# Requirements: <Feature Title> + +> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md) +> Research Date: <YYYY-MM-DD> +> Verdict: ✅ VIABLE + +## 🔍 Research Summary + +<Brief summary of what was found during research> + +## 📚 Reference Implementations + +| # | Repository | Stars | Last Updated | Approach | Relevance | +| --- | ---------------- | ----- | ------------ | -------- | ------------ | +| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low | +| 2 | ... | | | | | + +### Key Patterns Found + +- <pattern 1 with code snippet or link> +- <pattern 2> + +## 📐 Proposed Solution Architecture + +### Approach + +<Describe the chosen approach based on research findings> + +### New Files + +| File | Purpose | +| --------------------- | ------------- | +| `path/to/new/file.ts` | <description> | + +### Modified Files + +| File | Changes | +| -------------------------- | -------------- | +| `path/to/existing/file.ts` | <what changes> | + +### Database Changes + +- <migrations needed, if any> + +### API Changes + +- <new/modified endpoints, if any> + +### UI Changes + +- <new/modified pages/components, if any> + +## ⚙️ Implementation Effort + +- **Estimated complexity**: Low / Medium / High / Very High +- **Estimated files changed**: ~N +- **Dependencies needed**: <new npm packages, if any> +- **Breaking changes**: Yes/No — <details> +- **i18n impact**: <number of new translation keys> +- **Test coverage needed**: <brief description> + +## ⚠️ Open Questions + +- <question 1> +- <question 2> + +## 🔗 External References + +- <documentation URLs> +- <API references> +``` + +--- + +## Phase 2.5 — Organize & Respond: Sort Files and Post GitHub Comments + +### 2.5.1 Create Directory Structure + +// turbo + +```bash +mkdir -p <project_root>/_ideia/viable +mkdir -p <project_root>/_ideia/viable/need_details +mkdir -p <project_root>/_ideia/defer +mkdir -p <project_root>/_ideia/notfit +``` + +### 2.5.2 Move Idea Files to Category Subdirectories + +After classification, move EVERY idea file to its correct subdirectory: + +```bash +# ✅ VIABLE — move idea + requirements files +mv _ideia/<NUMBER>-*.md _ideia/viable/ +mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/ + +# ❓ NEEDS DETAIL — viable but waiting for author response +mv _ideia/<NUMBER>-*.md _ideia/viable/need_details/ + +# ⏭️ DEFER — move idea files only +mv _ideia/<NUMBER>-*.md _ideia/defer/ + +# ❌ NOT FIT & 🔁 ALREADY EXISTS — move idea files only +mv _ideia/<NUMBER>-*.md _ideia/notfit/ +``` + +No files should remain in `_ideia/` root after this step (except subdirectories). + +### 2.5.3 Post GitHub Comments by Category + +**Each category has a specific comment template and action:** + +--- + +#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue + +// turbo + +The feature already exists in the system. Explain WHERE it is and HOW to use it. + +```markdown +Hi @<author>! Thanks for the suggestion! 🙏 + +Great news — this functionality **already exists** in OmniRoute: + +**📍 Where to find it:** <exact dashboard path or settings location> + +**🔧 How to use it:** + +1. <step 1> +2. <step 2> +3. <step 3> + +If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help! + +Closing this as the feature is already available. 🎉 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ⏭️ DEFER — Comment + CLOSE issue + +// turbo + +Thank the user, explain the idea was cataloged, and that we'll study it before implementing. + +```markdown +Hi @<author>! Thanks for this thoughtful feature request! 🙏 + +We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog. + +Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions. + +**What happens next:** + +- Your idea is saved in our internal feature backlog +- We'll conduct architecture studies when this area is prioritized +- We'll notify you here when development begins + +Thank you for contributing to OmniRoute's roadmap! Your input helps shape the product. 🚀 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ❌ NOT FIT — Comment + CLOSE issue + +// turbo + +Politely explain why the feature doesn't fit the project scope. + +```markdown +Hi @<author>! Thanks for the suggestion! 🙏 + +After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router. + +**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself."> + +**Alternative:** <suggest an alternative approach if possible> + +We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ❓ NEEDS DETAIL — Comment (keep OPEN) + +// turbo + +Ask for the specific missing details needed. + +```markdown +Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏 + +To move forward, we need a few more details: + +1. <specific question 1> +2. <specific question 2> +3. <specific question 3> + +If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution. + +Looking forward to your response! 🚀 +``` + +--- + +#### For ✅ VIABLE — Comment (keep OPEN) + +// turbo + +Thank the user, confirm we've cataloged their idea, and explain it may be implemented in future versions. + +```markdown +Hi @<author>! Thanks for the great feature suggestion! 🙏 + +We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog. + +**Status:** 📋 Cataloged for future implementation + +This feature may be included in upcoming releases. We'll **respond to this issue and tag you** as soon as implementation begins so you can test it. + +Thank you for helping improve OmniRoute! 🚀 +``` + +**⚠️ Do NOT close viable issues — they remain OPEN for tracking.** + +--- + +## Phase 3 — Report: Present Findings to User + +### 3.1 🛑 MANDATORY STOP — Present Consolidated Report + +After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. Do NOT proceed to implementation. + +Present a structured report containing: + +#### 3.1a — Feature Summary Table + +| # | Issue | Title | Verdict | Location | Action | +| --- | ----- | ----- | --------------- | ----------------------------- | ----------------------------- | +| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Issue OPEN, comment posted | +| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Issue CLOSED with explanation | +| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Issue CLOSED with explanation | +| 4 | #N | Title | 🔁 EXISTS | `_ideia/notfit/` | Issue CLOSED with guidance | +| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/viable/need_details/` | Issue OPEN, questions posted | + +#### 3.1b — Viable Features Detail + +For each VIABLE feature, provide a brief paragraph: + +- What was found during research +- The proposed approach +- Key risks or unknowns +- Which reference repositories were most useful + +#### 3.1c — Issues Requiring Author Feedback + +For features marked ❓ NEEDS DETAIL, list: + +- What specific information is missing +- What examples or repository references would help + +#### 3.1d — Ask for User Confirmation + +End the report with: + +> **Ready to proceed with implementation?** +> +> - Reply **"sim"** or **"yes"** to generate full implementation plans for all VIABLE features. +> - Reply with specific issue numbers to select only certain features. +> - Reply **"não"** or **"no"** to stop here. + +--- + +## Phase 4 — Plan: Generate Implementation Plans (after user says "yes") + +> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.** + +### 4.1 Create Task Directory + +```bash +mkdir -p <project_root>/_tasks/features-vX.Y.Z/ +``` + +### 4.2 Generate One Implementation Plan Per Feature + +For each VIABLE feature approved by the user, create: + +**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md` + +```markdown +# Implementation Plan: <Feature Title> + +> Issue: #<NUMBER> +> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md) +> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md) +> Branch: `release/vX.Y.Z` + +## Overview + +<Brief description of what will be built> + +## Pre-Implementation Checklist + +- [ ] Read all related source files listed below +- [ ] Confirm no conflicts with in-flight PRs +- [ ] Verify database migration numbering + +## Implementation Steps + +### Step 1: <Title> + +**Files:** + +- `path/to/file.ts` — <what to change> + +**Details:** +<Detailed description of the change, including code patterns to follow, function signatures, etc.> + +### Step 2: <Title> + +... + +### Step N: Tests + +**New test files:** + +- `tests/unit/<test-file>.test.mjs` — <what to test> + +**Test cases:** + +- [ ] <test case 1> +- [ ] <test case 2> + +### Step N+1: i18n + +**Translation keys to add:** + +- `<namespace>.<key>` — "<English value>" + +### Step N+2: Documentation + +- [ ] Update CHANGELOG.md +- [ ] Update relevant docs/ files + +## Verification Plan + +1. Run `npm run build` — must pass +2. Run `npm test` — all tests must pass +3. Run `npm run lint` — no new errors +4. <Manual verification steps> + +## Commit Plan +``` + +feat: <description> (#<NUMBER>) + +``` + +``` + +### 4.3 Present Plans for Final Approval + +Present a summary of all generated plans: + +> **Implementation plans generated:** +> +> | # | Feature | Plan File | Steps | Effort | +> | --- | ------- | ---------------------------------------- | ------- | ------ | +> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium | +> +> Reply **"sim"** or **"yes"** to begin implementation of all features. +> Reply with specific issue numbers to implement only certain ones. + +--- + +## Phase 5 — Execute: Implement the Plans (after user says "yes") + +> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.** + +### 5.1 Implement Each Feature + +For each approved plan, execute it step by step: + +1. **Follow the plan** — implement exactly as specified in the `.plan.md` file +2. **Build** — Run `npm run build` after each feature to verify compilation +3. **Test** — Run `npm test` to ensure no regressions +4. **Commit** — Commit with: `feat: <description> (#<NUMBER>)` +5. **Update the plan** — Mark completed steps with `[x]` in the plan file +6. **Continue** — Move to the next feature (do NOT switch branches) + +### 5.2 Respond to Authors (Update Viable Issues) + +For each implemented feature, **close the issue with a final comment**: + +````markdown +✅ **Implemented in `release/vX.Y.Z`!** + +Hi @<author>! Great news — your feature request has been implemented! 🎉 + +**What was done:** + +- <bullet list of what was built> + +**How to try it:** + +```bash +git fetch origin && git checkout release/vX.Y.Z +npm install && npm run dev +``` +```` + +This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀 + +```` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +```` + +Then **DELETE the idea file** — it has served its purpose: + +```bash +# ✅ Implemented files are DELETED (not moved) +rm _ideia/viable/<NUMBER>-<title>.md +rm _ideia/viable/<NUMBER>-<title>.requirements.md # if exists +``` + +> **Why delete?** `viable/` only holds features that still NEED to be done. Once implemented, the commit history and CHANGELOG are the source of truth. Keeping the file would be confusing. + +### 5.3 Finalize & Push + +After implementing all approved features: + +1. **Update CHANGELOG.md** on the release branch with all new feature entries +2. Push the release branch: `git push origin release/vX.Y.Z` +3. Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user) + +### 5.4 Final Summary Report + +Present a final summary report to the user: + +| Issue | Title | Verdict | Action | Commit | +| ----- | ----- | --------------- | -------------------------------------------------- | --------- | +| #N | Title | ✅ Implemented | Issue closed, idea file deleted | `abc1234` | +| #N | Title | ⏭️ Deferred | Issue closed + saved in `_ideia/defer/` | — | +| #N | Title | ❌ Not Fit | Issue closed + saved in `_ideia/notfit/` | — | +| #N | Title | 🔁 Exists | Issue closed + saved in `_ideia/notfit/` | — | +| #N | Title | ❓ Needs Detail | Issue OPEN, moved to `_ideia/viable/need_details/` | — | + +Include: + +- Total features harvested +- Total ideas cataloged (`viable/need_details/` + `defer/` + `notfit/`) +- Total features implemented (idea files deleted, issues closed) +- Total features deferred +- Total issues closed +- Total issues left open (needs detail only — viable are closed after implementation) +- Test results (pass/fail count) diff --git a/.claude/commands/implement-features-cc.md b/.claude/commands/implement-features-cc.md new file mode 100644 index 0000000000..afb8b3ed93 --- /dev/null +++ b/.claude/commands/implement-features-cc.md @@ -0,0 +1,706 @@ +--- +description: Analyze open feature request issues, implement viable ones on dedicated branches, and respond to authors +--- + +# /implement-features — Feature Request Harvest, Research & Implementation Workflow + +## Overview + +A **5-phase** workflow that systematically harvests feature requests from GitHub issues, creates structured idea files, researches solutions across the internet and Git repositories, presents a consolidated report for user approval, then generates detailed implementation plans and executes them. + +**Output directory structure:** + +``` +_ideia/ +├── viable/ # Features approved for implementation +│ ├── need_details/ # ❓ Good idea but waiting for author clarification (issues stay OPEN) +│ │ └── 1015-warp-terminal-mitm.md +│ ├── 1046-native-playground.md # ✅ Ready — researched and planned +│ └── 1046-native-playground.requirements.md +├── defer/ # ⏭️ Good ideas deferred for future cycles (issues CLOSED) +│ └── 1041-smart-auto-combos.md +└── notfit/ # ❌ Out of scope / already exists (issues CLOSED) + └── 945-telegram-integration.md + +_tasks/features-vX.Y.Z/ # Implementation plans (per-release) +└── 1046-native-playground.plan.md +``` + +> **LIFECYCLE RULE:** `viable/` files are **DELETED** once the feature is implemented — they are not moved. Only unimplemented features live in `viable/` (or `viable/need_details/`). Files in `defer/` and `notfit/` remain as permanent reference. + +> **BRANCH RULE**: All implementation work MUST happen on the current `release/vX.Y.Z` branch. Never create separate `feat/` branches. If no release branch exists yet, create one first using `/generate-release` Phase 1 steps 1–5. + +--- + +## Phase 1 — Harvest: Collect & Catalog Feature Ideas + +### 1.1 Identify the Repository + +// turbo + +- Run: `git -C <project_root> remote get-url origin` to extract owner/repo. + +### 1.2 Ensure Release Branch Exists + +// turbo + +Before doing any work, ensure you are on the current release branch: + +```bash +# Check current branch +git branch --show-current + +# If on main, determine next version and create the release branch +VERSION=$(node -p "require('./package.json').version") +NEXT=$(node -p "const [a,b,c]=('$VERSION').split('.').map(Number); c>=9?a+'.'+(b+1)+'.0':a+'.'+b+'.'+(c+1)") +git checkout -b release/v$NEXT +npm version patch --no-git-tag-version +npm install +``` + +If already on a `release/vX.Y.Z` branch, continue working there. + +### 1.3 Fetch ALL Open Feature Requests + +// turbo-all + +**⚠️ CRITICAL**: The JSON output of `gh issue list` can be truncated by the tool, silently hiding issues. You MUST use the two-step approach below. + +**Step 1 — Get Issue numbers only** (small output, never truncated): + +```bash +# Fetch issues with feature/enhancement labels +gh issue list --repo <owner>/<repo> --state open -l "enhancement" --limit 500 --json number --jq '.[].number' + +# Also check for [Feature] in title (common pattern when no labels are set) +gh issue list --repo <owner>/<repo> --state open --limit 500 --json number,title --jq '.[] | select(.title | test("\\[Feature\\]|\\[feature\\]|feature request"; "i")) | .number' +``` + +- Merge both lists, deduplicate. Count and confirm the total. + +**Step 2 — Fetch full metadata for each Issue** (one call per issue): + +```bash +gh issue view <NUMBER> --repo <owner>/<repo> --json number,title,labels,body,comments,createdAt,author,assignees +``` + +- Read the **entire body** — including description, use cases, screenshots, mockups, and any embedded images. +- Read **ALL comments** — community discussion, agreements, restrictions, owner responses, and linked PRs. +- **Images**: If the body or comments contain image URLs (`![...](...)` or `https://...png/jpg/gif`), note them — they may contain UI mockups, wireframes, or architecture diagrams that are essential to understanding the request. +- You may batch these into parallel calls (up to 4 at a time). +- Sort by oldest first (FIFO). + +### 1.4 Create Idea Files (initially in `_ideia/` root) + +For each feature request, create a structured idea file in `<project_root>/_ideia/`: + +**Filename convention**: `<NUMBER>-<kebab-case-short-title>.md` +Example: `1046-native-playground.md`, `1041-smart-auto-combos.md` + +#### 1.4a — If the idea file does NOT exist yet, create it: + +```markdown +# Feature: <Title from Issue> + +> GitHub Issue: #<NUMBER> — opened by @<author> on <date> +> Status: 📋 Cataloged | Priority: TBD + +## 📝 Original Request + +<Paste the FULL issue body here, preserving all formatting, images, and code blocks> + +## 💬 Community Discussion + +<Summarize ALL comments chronologically, noting who said what and any decisions or objections raised> + +### Participants + +- @<author> — Original requester +- @<commenter1> — <brief role/opinion> +- ... + +### Key Points + +- <bullet list of the most important discussion points> +- <agreements reached> +- <objections raised> + +## 🎯 Refined Feature Description + +<YOUR interpretation and enrichment of the feature request. Expand on what was asked, fill in logical gaps, provide concrete examples of how it would work. This section should be MORE detailed and clearer than the original request.> + +### What it solves + +- <problem 1> +- <problem 2> + +### How it should work (high level) + +1. <step 1> +2. <step 2> +3. ... + +### Affected areas + +- <list of codebase areas, modules, files likely affected> + +## 📎 Attachments & References + +- <any image URLs, mockup links, or external references from the issue> + +## 🔗 Related Ideas + +- <links to related \_ideia/ files if any overlap found> +``` + +#### 1.4b — If the idea file ALREADY exists, update it: + +- Append new comments from the issue to the **Community Discussion** section. +- Update the **Refined Feature Description** if new information changes the understanding. +- Add any new **Related Ideas** cross-references found. +- **Do NOT overwrite** existing content — append and enrich it. + +### 1.5 Cross-Reference & Deduplication + +After processing all issues: + +- Scan all `_ideia/*.md` files for overlapping features. +- If two features are substantially the same, add `🔗 Related Ideas` cross-references to both. +- If one is a strict subset of another, note it in the smaller file: `> ℹ️ This feature is a subset of #<OTHER_NUMBER>. Consider implementing together.` + +--- + +## Phase 2 — Research: Find Solutions & Build Requirements + +For each cataloged idea that is **viable** (aligns with the project's goals): + +### 2.1 Viability Pre-Check + +Before investing in research, quickly assess: + +- [ ] Does this feature align with the project's goals and architecture? +- [ ] Is it technically feasible with the current codebase? +- [ ] Does it duplicate existing functionality? +- [ ] Would it introduce breaking changes or security risks? +- [ ] Is there enough detail to understand what's needed? + +**Verdict options:** + +| Verdict | When | Action | +| --------------------- | ------------------------------------- | --------------------------- | +| ✅ **VIABLE** | Good idea, enough context | Proceed to Research | +| ❓ **NEEDS DETAIL** | Good idea, insufficient spec | Skip research, ask author | +| ⏭️ **DEFER** | Good idea, too complex for this cycle | Catalog only, skip research | +| ❌ **NOT FIT** | Doesn't fit the project | Explain why | +| 🔁 **ALREADY EXISTS** | Feature already implemented | Point to existing feature | + +### 2.2 Internet Research (for VIABLE features) + +For each viable feature, perform systematic research: + +**Step 1 — Web search for similar implementations:** + +``` +WebSearch("how to implement <feature description> in <tech stack>") +WebSearch("<feature keyword> implementation nextjs typescript 2025 2026") +WebSearch("<feature keyword> open source library npm") +``` + +**Step 2 — Find reference Git repositories:** + +``` +WebSearch("site:github.com <feature keyword> <tech stack> stars:>100") +WebSearch("github <feature keyword> implementation recently updated 2026") +``` + +- Find **up to 10 relevant repositories**, sorted by most recently updated. +- For each repository: + - Note the repo URL, star count, last commit date + - Read its README and relevant source files via `WebFetch` + - Extract the architectural approach, patterns used, and key code snippets + +**Step 3 — Read API docs and standards:** + +If the feature involves an external API, protocol, or standard: + +- Find and read the official documentation +- Note version requirements, authentication patterns, rate limits + +### 2.3 Create Requirements File + +For each researched feature, create a requirements file alongside its idea file: + +**Filename**: `<NUMBER>-<kebab-case-short-title>.requirements.md` + +```markdown +# Requirements: <Feature Title> + +> Feature Idea: [#<NUMBER>](./<NUMBER>-<kebab-case-short-title>.md) +> Research Date: <YYYY-MM-DD> +> Verdict: ✅ VIABLE + +## 🔍 Research Summary + +<Brief summary of what was found during research> + +## 📚 Reference Implementations + +| # | Repository | Stars | Last Updated | Approach | Relevance | +| --- | ---------------- | ----- | ------------ | -------- | ------------ | +| 1 | [repo/name](url) | ⭐ N | YYYY-MM-DD | <brief> | High/Med/Low | +| 2 | ... | | | | | + +### Key Patterns Found + +- <pattern 1 with code snippet or link> +- <pattern 2> + +## 📐 Proposed Solution Architecture + +### Approach + +<Describe the chosen approach based on research findings> + +### New Files + +| File | Purpose | +| --------------------- | ------------- | +| `path/to/new/file.ts` | <description> | + +### Modified Files + +| File | Changes | +| -------------------------- | -------------- | +| `path/to/existing/file.ts` | <what changes> | + +### Database Changes + +- <migrations needed, if any> + +### API Changes + +- <new/modified endpoints, if any> + +### UI Changes + +- <new/modified pages/components, if any> + +## ⚙️ Implementation Effort + +- **Estimated complexity**: Low / Medium / High / Very High +- **Estimated files changed**: ~N +- **Dependencies needed**: <new npm packages, if any> +- **Breaking changes**: Yes/No — <details> +- **i18n impact**: <number of new translation keys> +- **Test coverage needed**: <brief description> + +## ⚠️ Open Questions + +- <question 1> +- <question 2> + +## 🔗 External References + +- <documentation URLs> +- <API references> +``` + +--- + +## Phase 2.5 — Organize & Respond: Sort Files and Post GitHub Comments + +### 2.5.1 Create Directory Structure + +// turbo + +```bash +mkdir -p <project_root>/_ideia/viable +mkdir -p <project_root>/_ideia/viable/need_details +mkdir -p <project_root>/_ideia/defer +mkdir -p <project_root>/_ideia/notfit +``` + +### 2.5.2 Move Idea Files to Category Subdirectories + +After classification, move EVERY idea file to its correct subdirectory: + +```bash +# ✅ VIABLE — move idea + requirements files +mv _ideia/<NUMBER>-*.md _ideia/viable/ +mv _ideia/<NUMBER>-*.requirements.md _ideia/viable/ + +# ❓ NEEDS DETAIL — viable but waiting for author response +mv _ideia/<NUMBER>-*.md _ideia/viable/need_details/ + +# ⏭️ DEFER — move idea files only +mv _ideia/<NUMBER>-*.md _ideia/defer/ + +# ❌ NOT FIT & 🔁 ALREADY EXISTS — move idea files only +mv _ideia/<NUMBER>-*.md _ideia/notfit/ +``` + +No files should remain in `_ideia/` root after this step (except subdirectories). + +### 2.5.3 Post GitHub Comments by Category + +**Each category has a specific comment template and action:** + +--- + +#### For 🔁 ALREADY EXISTS — Comment + CLOSE issue + +// turbo + +The feature already exists in the system. Explain WHERE it is and HOW to use it. + +```markdown +Hi @<author>! Thanks for the suggestion! 🙏 + +Great news — this functionality **already exists** in OmniRoute: + +**📍 Where to find it:** <exact dashboard path or settings location> + +**🔧 How to use it:** + +1. <step 1> +2. <step 2> +3. <step 3> + +If you have any trouble finding or using it, feel free to ask in a Discussion. We're always happy to help! + +Closing this as the feature is already available. 🎉 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ⏭️ DEFER — Comment + CLOSE issue + +// turbo + +Thank the user, explain the idea was cataloged, and that we'll study it before implementing. + +```markdown +Hi @<author>! Thanks for this thoughtful feature request! 🙏 + +We really appreciate the detailed proposal. We've **cataloged your idea** and it's now part of our improvement backlog. + +Due to the **significant architectural impact** of this feature, we'll need to conduct thorough use-case studies and architectural analysis before we start development. This ensures we build it right and don't introduce regressions. + +**What happens next:** + +- Your idea is saved in our internal feature backlog +- We'll conduct architecture studies when this area is prioritized +- We'll notify you here when development begins + +Thank you for contributing to OmniRoute's roadmap! Your input helps shape the product. 🚀 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ❌ NOT FIT — Comment + CLOSE issue + +// turbo + +Politely explain why the feature doesn't fit the project scope. + +```markdown +Hi @<author>! Thanks for the suggestion! 🙏 + +After careful analysis, we've determined that this feature **falls outside OmniRoute's core scope** as a proxy/router. + +**Reason:** <explain why — e.g., "Telegram integration belongs in the application/orchestrator layer that consumes OmniRoute's API, not inside the router itself."> + +**Alternative:** <suggest an alternative approach if possible> + +We appreciate you thinking of ways to improve OmniRoute! If you'd like to discuss this further, feel free to open a Discussion. 🙏 +``` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +``` + +--- + +#### For ❓ NEEDS DETAIL — Comment (keep OPEN) + +// turbo + +Ask for the specific missing details needed. + +```markdown +Hi @<author>! Thanks for the feature request — it's an interesting idea and we'd love to explore it further. 🙏 + +To move forward, we need a few more details: + +1. <specific question 1> +2. <specific question 2> +3. <specific question 3> + +If you know of any **open-source projects or repositories** that implement something similar, please share links — it would help us design the best solution. + +Looking forward to your response! 🚀 +``` + +--- + +#### For ✅ VIABLE — Comment (keep OPEN) + +// turbo + +Thank the user, confirm we've cataloged their idea, and explain it may be implemented in future versions. + +```markdown +Hi @<author>! Thanks for the great feature suggestion! 🙏 + +We've analyzed your request and it aligns well with OmniRoute's roadmap. We've **cataloged this feature** and it's in our implementation backlog. + +**Status:** 📋 Cataloged for future implementation + +This feature may be included in upcoming releases. We'll **respond to this issue and tag you** as soon as implementation begins so you can test it. + +Thank you for helping improve OmniRoute! 🚀 +``` + +**⚠️ Do NOT close viable issues — they remain OPEN for tracking.** + +--- + +## Phase 3 — Report: Present Findings to User + +### 3.1 🛑 MANDATORY STOP — Present Consolidated Report + +After completing Phase 1, Phase 2, and Phase 2.5, **STOP and present the following report** in the chat. Do NOT proceed to implementation. + +Present a structured report containing: + +#### 3.1a — Feature Summary Table + +| # | Issue | Title | Verdict | Location | Action | +| --- | ----- | ----- | --------------- | ----------------------------- | ----------------------------- | +| 1 | #N | Title | ✅ VIABLE | `_ideia/viable/` | Issue OPEN, comment posted | +| 2 | #N | Title | ⏭️ DEFER | `_ideia/defer/` | Issue CLOSED with explanation | +| 3 | #N | Title | ❌ NOT FIT | `_ideia/notfit/` | Issue CLOSED with explanation | +| 4 | #N | Title | 🔁 EXISTS | `_ideia/notfit/` | Issue CLOSED with guidance | +| 5 | #N | Title | ❓ NEEDS DETAIL | `_ideia/viable/need_details/` | Issue OPEN, questions posted | + +#### 3.1b — Viable Features Detail + +For each VIABLE feature, provide a brief paragraph: + +- What was found during research +- The proposed approach +- Key risks or unknowns +- Which reference repositories were most useful + +#### 3.1c — Issues Requiring Author Feedback + +For features marked ❓ NEEDS DETAIL, list: + +- What specific information is missing +- What examples or repository references would help + +#### 3.1d — Ask for User Confirmation + +End the report with: + +> **Ready to proceed with implementation?** +> +> - Reply **"sim"** or **"yes"** to generate full implementation plans for all VIABLE features. +> - Reply with specific issue numbers to select only certain features. +> - Reply **"não"** or **"no"** to stop here. + +--- + +## Phase 4 — Plan: Generate Implementation Plans (after user says "yes") + +> **⚠️ Do NOT enter this phase without explicit user approval from Phase 3.** + +### 4.1 Create Task Directory + +```bash +mkdir -p <project_root>/_tasks/features-vX.Y.Z/ +``` + +### 4.2 Generate One Implementation Plan Per Feature + +For each VIABLE feature approved by the user, create: + +**Filename**: `_tasks/features-vX.Y.Z/<NUMBER>-<kebab-case-title>.plan.md` + +```markdown +# Implementation Plan: <Feature Title> + +> Issue: #<NUMBER> +> Idea: [\_ideia/viable/<NUMBER>-title.md](../../_ideia/viable/<NUMBER>-title.md) +> Requirements: [\_ideia/viable/<NUMBER>-title.requirements.md](../../_ideia/viable/<NUMBER>-title.requirements.md) +> Branch: `release/vX.Y.Z` + +## Overview + +<Brief description of what will be built> + +## Pre-Implementation Checklist + +- [ ] Read all related source files listed below +- [ ] Confirm no conflicts with in-flight PRs +- [ ] Verify database migration numbering + +## Implementation Steps + +### Step 1: <Title> + +**Files:** + +- `path/to/file.ts` — <what to change> + +**Details:** +<Detailed description of the change, including code patterns to follow, function signatures, etc.> + +### Step 2: <Title> + +... + +### Step N: Tests + +**New test files:** + +- `tests/unit/<test-file>.test.mjs` — <what to test> + +**Test cases:** + +- [ ] <test case 1> +- [ ] <test case 2> + +### Step N+1: i18n + +**Translation keys to add:** + +- `<namespace>.<key>` — "<English value>" + +### Step N+2: Documentation + +- [ ] Update CHANGELOG.md +- [ ] Update relevant docs/ files + +## Verification Plan + +1. Run `npm run build` — must pass +2. Run `npm test` — all tests must pass +3. Run `npm run lint` — no new errors +4. <Manual verification steps> + +## Commit Plan +``` + +feat: <description> (#<NUMBER>) + +``` + +``` + +### 4.3 Present Plans for Final Approval + +Present a summary of all generated plans: + +> **Implementation plans generated:** +> +> | # | Feature | Plan File | Steps | Effort | +> | --- | ------- | ---------------------------------------- | ------- | ------ | +> | 1 | <title> | `_tasks/features-vX.Y.Z/N-title.plan.md` | N steps | Medium | +> +> Reply **"sim"** or **"yes"** to begin implementation of all features. +> Reply with specific issue numbers to implement only certain ones. + +--- + +## Phase 5 — Execute: Implement the Plans (after user says "yes") + +> **⚠️ Do NOT enter this phase without explicit user approval from Phase 4.** + +### 5.1 Implement Each Feature + +For each approved plan, execute it step by step: + +1. **Follow the plan** — implement exactly as specified in the `.plan.md` file +2. **Build** — Run `npm run build` after each feature to verify compilation +3. **Test** — Run `npm test` to ensure no regressions +4. **Commit** — Commit with: `feat: <description> (#<NUMBER>)` +5. **Update the plan** — Mark completed steps with `[x]` in the plan file +6. **Continue** — Move to the next feature (do NOT switch branches) + +### 5.2 Respond to Authors (Update Viable Issues) + +For each implemented feature, **close the issue with a final comment**: + +````markdown +✅ **Implemented in `release/vX.Y.Z`!** + +Hi @<author>! Great news — your feature request has been implemented! 🎉 + +**What was done:** + +- <bullet list of what was built> + +**How to try it:** + +```bash +git fetch origin && git checkout release/vX.Y.Z +npm install && npm run dev +``` +```` + +This will be included in the upcoming **vX.Y.Z** release. Feel free to reopen if you spot any issues! 🚀 + +```` + +```bash +gh issue close <NUMBER> --repo <owner>/<repo> --comment "<comment above>" +```` + +Then **DELETE the idea file** — it has served its purpose: + +```bash +# ✅ Implemented files are DELETED (not moved) +rm _ideia/viable/<NUMBER>-<title>.md +rm _ideia/viable/<NUMBER>-<title>.requirements.md # if exists +``` + +> **Why delete?** `viable/` only holds features that still NEED to be done. Once implemented, the commit history and CHANGELOG are the source of truth. Keeping the file would be confusing. + +### 5.3 Finalize & Push + +After implementing all approved features: + +1. **Update CHANGELOG.md** on the release branch with all new feature entries +2. Push the release branch: `git push origin release/vX.Y.Z` +3. Run `/generate-release` workflow Phase 1 steps 7–10 (tests → commit → push → open PR to main → wait for user) + +### 5.4 Final Summary Report + +Present a final summary report to the user: + +| Issue | Title | Verdict | Action | Commit | +| ----- | ----- | --------------- | -------------------------------------------------- | --------- | +| #N | Title | ✅ Implemented | Issue closed, idea file deleted | `abc1234` | +| #N | Title | ⏭️ Deferred | Issue closed + saved in `_ideia/defer/` | — | +| #N | Title | ❌ Not Fit | Issue closed + saved in `_ideia/notfit/` | — | +| #N | Title | 🔁 Exists | Issue closed + saved in `_ideia/notfit/` | — | +| #N | Title | ❓ Needs Detail | Issue OPEN, moved to `_ideia/viable/need_details/` | — | + +Include: + +- Total features harvested +- Total ideas cataloged (`viable/need_details/` + `defer/` + `notfit/`) +- Total features implemented (idea files deleted, issues closed) +- Total features deferred +- Total issues closed +- Total issues left open (needs detail only — viable are closed after implementation) +- Test results (pass/fail count) diff --git a/.github/workflows/build-fork.yml b/.github/workflows/build-fork.yml index 46dc5833d6..17badfe096 100644 --- a/.github/workflows/build-fork.yml +++ b/.github/workflows/build-fork.yml @@ -1,23 +1,30 @@ -name: Build Fork Image (ghcr.io) +name: Publish Fork Image to GHCR on: push: branches: [main] + tags: + - "v*" workflow_dispatch: permissions: contents: read packages: write +env: + IMAGE_NAME: ghcr.io/kang-heewon/omniroute + jobs: build: - name: Build and Push to ghcr.io + name: Build and Push Fork Image + if: github.repository == 'kang-heewon/OmniRoute' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - with: - ref: main + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 @@ -29,14 +36,30 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix=sha- + type=ref,event=tag + labels: | + org.opencontainers.image.title=omniroute + org.opencontainers.image.description=Unified AI proxy/router — fork image + org.opencontainers.image.url=https://github.com/kang-heewon/OmniRoute + org.opencontainers.image.source=https://github.com/kang-heewon/OmniRoute + org.opencontainers.image.licenses=MIT + - name: Build and push uses: docker/build-push-action@v7 with: context: . target: runner-base - platforms: linux/amd64 + platforms: linux/amd64,linux/arm64 push: true - tags: | - ghcr.io/gi99lin/omniroute:latest + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/AGENTS.md b/AGENTS.md index 24ce8c533d..5d0be6ac04 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -507,6 +507,24 @@ For any non-trivial change, read the matching deep-dive first: --- +## Fork / Upstream Workflow + +This repository is a fork of `diegosouzapw/OmniRoute`. Keep fork-only operational +changes (for example GHCR image publishing, personal deployment workflows, or local +automation) out of upstream contribution PRs. + +When preparing a PR for upstream, always start the work branch from `upstream/main`, +not from this fork's `main`: + +```bash +git fetch upstream +git switch -c <branch-name> upstream/main +``` + +Only cherry-pick or reapply the changes intended for the upstream PR. + +--- + ## Review Focus - **DB ops** go through `src/lib/db/` modules, never raw SQL in routes diff --git a/open-sse/executors/chatgpt-web.ts b/open-sse/executors/chatgpt-web.ts index 534b02bdf3..d9149ee3c5 100644 --- a/open-sse/executors/chatgpt-web.ts +++ b/open-sse/executors/chatgpt-web.ts @@ -321,6 +321,7 @@ async function exchangeSession( try { data = JSON.parse(response.text || "{}"); } catch { + console.warn("[chatgpt-web] session response JSON parse failed"); /* empty body or non-JSON */ } if (!data.accessToken) { @@ -621,6 +622,7 @@ async function prepareChatRequirements( try { prepData = JSON.parse(prepResp.text || "{}") as ChatRequirements; } catch { + console.warn("[chatgpt-web] chat requirements prep JSON parse failed"); /* keep empty */ } // Stage 2: POST /chat-requirements with the prepare_token in the body. This @@ -650,6 +652,7 @@ async function prepareChatRequirements( // Merge: prepare_token from stage 1, everything else from stage 2. return { ...crData, prepare_token: prepData.prepare_token }; } catch { + console.warn("[chatgpt-web] chat requirements response JSON parse failed"); return prepData; } } @@ -1168,6 +1171,7 @@ async function* readChatGptSseEvents( try { return JSON.parse(trimmed) as ChatGptStreamEvent; } catch { + console.warn("[chatgpt-web] stream event JSON parse failed"); return null; } } @@ -1594,6 +1598,7 @@ function buildStreamingResponse( } catch { // Controller may already be closed if the client disconnected // — just stop firing. + console.warn("[chatgpt-web] heartbeat enqueue failed - controller closed"); clearInterval(timer); } }, intervalMs); @@ -1669,6 +1674,7 @@ function buildStreamingResponse( controller.enqueue(bytes); return true; } catch { + console.warn("[chatgpt-web] controller enqueue failed"); return false; } }; @@ -1868,6 +1874,7 @@ function isLocalBaseUrl(baseUrl: string): boolean { const host = new URL(baseUrl).hostname.toLowerCase(); return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "0.0.0.0"; } catch { + console.warn("[chatgpt-web] URL parse failed, falling back to regex"); return /\b(?:localhost|127\.0\.0\.1|0\.0\.0\.0)\b/i.test(baseUrl); } } @@ -1988,6 +1995,7 @@ async function fetchDownloadUrl(endpoint: string, ctx: ResolverContext): Promise try { parsed = JSON.parse(response.text || "{}"); } catch { + console.warn("[chatgpt-web] image download URL parse failed"); return null; } return parsed.download_url ?? null; @@ -2155,6 +2163,7 @@ async function registerWebSocket(ctx: ResolverContext): Promise<string | null> { return ws; } } catch { + console.warn("[chatgpt-web] WebSocket URL parse failed, falling through"); /* fall through */ } } @@ -2194,6 +2203,7 @@ async function waitForImageViaWebSocket( try { ws.close(); } catch { + console.warn("[chatgpt-web] ws.close failed"); /* ignore */ } resolve({ @@ -2232,6 +2242,7 @@ async function waitForImageViaWebSocket( try { payload = JSON.parse(raw); } catch { + console.warn("[chatgpt-web] WebSocket event JSON parse failed"); return; } // chatgpt.com's celsius WS frames look like: diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index 54128529ad..8ca45d32eb 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -67,6 +67,7 @@ function getCodexWebSocketTransport(): WebsocketFn | null { const mod = _wreqRequire("wreq-js") as { websocket?: WebsocketFn }; _websocketFn = typeof mod.websocket === "function" ? mod.websocket : null; } catch { + console.warn("[codex] wreq-js import failed, websocket disabled"); _websocketFn = null; } return _websocketFn; @@ -847,6 +848,7 @@ export function encodeResponseSseEvent(raw: string): { sse: string; terminal: bo terminal = eventType === "response.completed" || eventType === "response.failed"; } } catch { + console.warn("[codex] SSE payload parse failed, using raw payload"); // Keep message as the generic SSE event for non-JSON upstream payloads. } @@ -952,6 +954,7 @@ export class CodexExecutor extends BaseExecutor { try { ws?.close(1000, reason); } catch { + console.warn("[codex] closeUpstream: socket close race ignored"); // ignore close races } }; @@ -985,12 +988,14 @@ export class CodexExecutor extends BaseExecutor { try { controller.enqueue(encoder.encode("data: [DONE]\n\n")); } catch { + console.warn("[codex] finishStream: failed to enqueue [DONE]"); // The downstream may already have gone away. } } try { controller.close(); } catch { + console.warn("[codex] finishStream: failed to close controller"); // The controller may already be closed. } }; diff --git a/open-sse/executors/commandCode.ts b/open-sse/executors/commandCode.ts index 141982cca8..58b2f0303f 100644 --- a/open-sse/executors/commandCode.ts +++ b/open-sse/executors/commandCode.ts @@ -31,8 +31,11 @@ function recordOrEmpty(value: unknown): JsonRecord { try { const parsed: unknown = JSON.parse(value); if (isRecord(parsed)) return parsed; - } catch { - // Tool argument fragments may be incomplete in streamed deltas. + } catch (error) { + console.warn( + "[commandCode] tool arg parse failed:", + error instanceof Error ? error.message : String(error) + ); } } return {}; @@ -183,7 +186,11 @@ function parseStreamLine(line: string): unknown | undefined { try { return JSON.parse(trimmed); - } catch { + } catch (error) { + console.warn( + "[commandCode] stream line parse failed:", + error instanceof Error ? error.message : String(error) + ); return undefined; } } @@ -398,8 +405,11 @@ function createStreamResponse( signal?.removeEventListener("abort", abort); try { reader.releaseLock(); - } catch { - // Reader may already be released/cancelled. + } catch (error) { + console.warn( + "[commandCode] reader releaseLock failed:", + error instanceof Error ? error.message : String(error) + ); } } }; @@ -457,13 +467,19 @@ async function createJsonResponse( } finally { try { await reader.cancel(); - } catch { - // Reader may already be closed. + } catch (error) { + console.warn( + "[commandCode] reader cancel failed:", + error instanceof Error ? error.message : String(error) + ); } try { reader.releaseLock(); - } catch { - // Reader may already be released. + } catch (error) { + console.warn( + "[commandCode] reader releaseLock failed:", + error instanceof Error ? error.message : String(error) + ); } } @@ -523,7 +539,10 @@ export class CommandCodeExecutor extends BaseExecutor { }); if (!upstream.ok) { - const errorText = await upstream.text().catch(() => ""); + const errorText = await upstream.text().catch(() => { + console.warn("[commandCode] upstream text failed"); + return ""; + }); return { response: new Response(errorText || `Command Code API error ${upstream.status}`, { status: upstream.status, diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index ee192f7cf4..789ede4c29 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -1,4 +1,4 @@ -import { BaseExecutor } from "./base.ts"; +import { BaseExecutor, setUserAgentHeader } from "./base.ts"; import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts"; import { getAccessToken } from "../services/tokenRefresh.ts"; import { @@ -239,7 +239,7 @@ export class DefaultExecutor extends BaseExecutor { } } - buildHeaders(credentials, stream = true) { + buildHeaders(credentials, stream = true, clientHeaders?: Record<string, string> | null) { const headers = { "Content-Type": "application/json", ...this.config.headers }; // Allow per-provider User-Agent override via environment variable. @@ -405,6 +405,30 @@ export class DefaultExecutor extends BaseExecutor { } } + // Forward client request metadata headers (from OpenCode or similar clients) + // Allowlist-based: only specific x-opencode-* headers and User-Agent are forwarded + if (clientHeaders) { + const clientUA = clientHeaders["User-Agent"] || clientHeaders["user-agent"]; + if (clientUA) { + setUserAgentHeader(headers, clientUA); + } + + const opencodeHeaderKeys = [ + "x-opencode-session", + "x-opencode-request", + "x-opencode-project", + "x-opencode-client", + ]; + for (const headerName of opencodeHeaderKeys) { + const value = Object.entries(clientHeaders).find( + ([key]) => key.toLowerCase() === headerName.toLowerCase() + )?.[1]; + if (value) { + headers[headerName] = value; + } + } + } + return headers; } diff --git a/open-sse/executors/opencode.ts b/open-sse/executors/opencode.ts index 9788cd53d0..59681c8b6d 100644 --- a/open-sse/executors/opencode.ts +++ b/open-sse/executors/opencode.ts @@ -1,4 +1,9 @@ -import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts"; +import { + BaseExecutor, + setUserAgentHeader, + type ExecuteInput, + type ProviderCredentials, +} from "./base.ts"; import { PROVIDERS } from "../config/constants.ts"; import { getModelTargetFormat } from "../config/providerModels.ts"; @@ -40,7 +45,12 @@ export class OpencodeExecutor extends BaseExecutor { } } - buildHeaders(credentials: ProviderCredentials | null, stream = true) { + buildHeaders( + credentials: ProviderCredentials | null, + stream = true, + clientHeaders?: Record<string, string> | null, + model?: string + ) { const headers: Record<string, string> = { "Content-Type": "application/json" }; const key = credentials?.apiKey || credentials?.accessToken; @@ -60,6 +70,31 @@ export class OpencodeExecutor extends BaseExecutor { headers["Accept"] = "text/event-stream"; } + if (clientHeaders) { + const clientUA = clientHeaders["User-Agent"] || clientHeaders["user-agent"]; + if (clientUA) { + setUserAgentHeader(headers, clientUA); + } + + // Forward OpenCode request metadata headers from client + const opencodeHeaderKeys = [ + "x-opencode-session", + "x-opencode-request", + "x-opencode-project", + "x-opencode-client", + ]; + for (const headerName of opencodeHeaderKeys) { + const value = Object.entries(clientHeaders).find( + ([key]) => key.toLowerCase() === headerName.toLowerCase() + )?.[1]; + if (value) { + headers[headerName] = value; + } + } + } + + void model; + return headers; } diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index d229ef2f98..c24757d3d6 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -969,7 +969,7 @@ export function createMcpServer(): McpServer { withScopeEnforcement(toolDef.name, async (args) => { try { const parsedArgs = toolDef.inputSchema.parse(args ?? {}); - // @ts-ignore: handler expected specific object + // @ts-expect-error - handler type lost through dynamic Object.values() access const result = await toolDef.handler(parsedArgs); return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; } catch (err) { @@ -992,7 +992,7 @@ export function createMcpServer(): McpServer { withScopeEnforcement(toolDef.name, async (args) => { try { const parsedArgs = toolDef.inputSchema.parse(args ?? {}); - // @ts-ignore: handler expected specific object + // @ts-expect-error - handler type lost through dynamic Object.values() access const result = await toolDef.handler(parsedArgs); return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; } catch (err) { @@ -1015,7 +1015,7 @@ export function createMcpServer(): McpServer { withScopeEnforcement(toolDef.name, async (args) => { try { const parsedArgs = toolDef.inputSchema.parse(args ?? {}); - // @ts-ignore: handler expected specific object + // @ts-expect-error - handler type lost through dynamic Object.values() access const result = await toolDef.handler(parsedArgs); return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; } catch (err) { diff --git a/src/app/(dashboard)/dashboard/settings/components/FeatureFlagCard.tsx b/src/app/(dashboard)/dashboard/settings/components/FeatureFlagCard.tsx index 58ead505cc..f758d4b867 100644 --- a/src/app/(dashboard)/dashboard/settings/components/FeatureFlagCard.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/FeatureFlagCard.tsx @@ -52,7 +52,12 @@ function Spinner() { ); } -export default function FeatureFlagCard({ flag, onToggle, onReset, saving = false }: FeatureFlagCardProps) { +export default function FeatureFlagCard({ + flag, + onToggle, + onReset, + saving = false, +}: FeatureFlagCardProps) { const enabled = flag.type === "boolean" ? isEnabled(flag.effectiveValue) : false; const category = CATEGORY_STYLES[flag.category]; const source = SOURCE_STYLES[flag.source]; diff --git a/src/app/(dashboard)/dashboard/settings/components/FeatureFlagsGrid.tsx b/src/app/(dashboard)/dashboard/settings/components/FeatureFlagsGrid.tsx index e54a0548d5..5325bf68ff 100644 --- a/src/app/(dashboard)/dashboard/settings/components/FeatureFlagsGrid.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/FeatureFlagsGrid.tsx @@ -80,7 +80,7 @@ export default function FeatureFlagsGrid() { (f) => debouncedSearch === "" || f.key.toLowerCase().includes(debouncedSearch.toLowerCase()) || - f.description.toLowerCase().includes(debouncedSearch.toLowerCase()), + f.description.toLowerCase().includes(debouncedSearch.toLowerCase()) ); }, [flags, debouncedSearch, category]); diff --git a/src/app/api/cache/stats/route.ts b/src/app/api/cache/stats/route.ts index b345031bf6..2d5436d83a 100644 --- a/src/app/api/cache/stats/route.ts +++ b/src/app/api/cache/stats/route.ts @@ -9,10 +9,10 @@ export async function GET(req: NextRequest) { try { const cache = getPromptCache(); - const stats = (cache as any).getStats(); + const stats = cache.getStats(); return NextResponse.json(stats); } catch (error) { - return NextResponse.json({ error: (error as any).message }, { status: 500 }); + return NextResponse.json({ error: (error as Error).message }, { status: 500 }); } } @@ -23,9 +23,9 @@ export async function DELETE(req: NextRequest) { try { const cache = getPromptCache(); - (cache as any).clear(); + cache.clear(); return NextResponse.json({ success: true, message: "Cache cleared" }); } catch (error) { - return NextResponse.json({ error: (error as any).message }, { status: 500 }); + return NextResponse.json({ error: (error as Error).message }, { status: 500 }); } } diff --git a/src/app/api/models/route.ts b/src/app/api/models/route.ts index f53c8da3ef..92cf976126 100644 --- a/src/app/api/models/route.ts +++ b/src/app/api/models/route.ts @@ -25,7 +25,7 @@ export async function GET(request: Request) { // Without this, models for aliased providers always appear unconfigured. activeProviders = new Set<string>(); for (const c of active) { - const pId = String((c as any).provider); + const pId = String((c as Record<string, unknown>).provider); activeProviders.add(pId); const alias = PROVIDER_ID_TO_ALIAS[pId]; if (alias) activeProviders.add(alias); diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 7897c4dbad..332c73da1e 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -875,9 +875,15 @@ export async function GET( return localCatalog.map((model) => ({ id: model.id, name: model.name || model.id, - ...((model as any).apiFormat ? { apiFormat: (model as any).apiFormat } : {}), - ...((model as any).supportedEndpoints - ? { supportedEndpoints: (model as any).supportedEndpoints } + ...((model as Record<string, unknown>).apiFormat + ? { apiFormat: (model as Record<string, unknown>).apiFormat as string | undefined } + : {}), + ...((model as Record<string, unknown>).supportedEndpoints + ? { + supportedEndpoints: (model as Record<string, unknown>).supportedEndpoints as + | string[] + | undefined, + } : {}), ...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}), })); @@ -1876,9 +1882,15 @@ export async function GET( models: localCatalog.map((m) => ({ id: m.id, name: m.name || m.id, - ...((m as any).apiFormat ? { apiFormat: (m as any).apiFormat } : {}), - ...((m as any).supportedEndpoints - ? { supportedEndpoints: (m as any).supportedEndpoints } + ...((m as Record<string, unknown>).apiFormat + ? { apiFormat: (m as Record<string, unknown>).apiFormat as string | undefined } + : {}), + ...((m as Record<string, unknown>).supportedEndpoints + ? { + supportedEndpoints: (m as Record<string, unknown>).supportedEndpoints as + | string[] + | undefined, + } : {}), ...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}), })), diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index cef3846634..fc3e5d1d6d 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -221,7 +221,10 @@ function hasQoderToken(connection: any): boolean { if (typeof connection?.apiKey === "string" && connection.apiKey.trim().length > 0) return true; const psd = connection?.providerSpecificData; if (psd && typeof psd === "object") { - const pat = (psd as any).personalAccessToken ?? (psd as any).pat ?? (psd as any).accessToken; + const pat = + (psd as Record<string, unknown>).personalAccessToken ?? + (psd as Record<string, unknown>).pat ?? + (psd as Record<string, unknown>).accessToken; if (typeof pat === "string" && pat.trim().length > 0) return true; } return false; diff --git a/src/app/api/providers/test-batch/route.ts b/src/app/api/providers/test-batch/route.ts index a893875260..f5b97cede3 100644 --- a/src/app/api/providers/test-batch/route.ts +++ b/src/app/api/providers/test-batch/route.ts @@ -133,7 +133,7 @@ export async function POST(request) { const PER_CONNECTION_TIMEOUT = 30_000; // 30s per connection const CONCURRENCY = 5; // max parallel tests - const testOne = async (conn) => { + const testOne = async (conn: Record<string, unknown>) => { try { const result = await Promise.race([ testSingleConnection(conn.id), @@ -144,7 +144,14 @@ export async function POST(request) { ) ), ]); - const data = result as any; + const data = result as { + valid: boolean; + latencyMs?: number; + error?: string | null; + diagnosis?: unknown; + statusCode?: number | null; + testedAt?: string; + }; return { provider: conn.provider, connectionId: conn.id, diff --git a/src/app/api/settings/feature-flags/route.ts b/src/app/api/settings/feature-flags/route.ts index 2399bfd05f..9108340208 100644 --- a/src/app/api/settings/feature-flags/route.ts +++ b/src/app/api/settings/feature-flags/route.ts @@ -55,10 +55,7 @@ export async function GET(request: NextRequest) { summary: { total, active, inactive, overriddenByDb, overriddenByEnv }, }); } catch (error) { - return NextResponse.json( - { error: sanitizeErrorMessage(error) }, - { status: 500 } - ); + return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 }); } } @@ -95,10 +92,7 @@ export async function PUT(request: NextRequest) { // Validate key against known definitions const definition = FEATURE_FLAG_DEFINITIONS.find((d) => d.key === key); if (!definition) { - return NextResponse.json( - { error: `Unknown feature flag key: ${key}` }, - { status: 400 } - ); + return NextResponse.json({ error: `Unknown feature flag key: ${key}` }, { status: 400 }); } // Validate enum values @@ -141,10 +135,7 @@ export async function PUT(request: NextRequest) { requiresRestart: definition.requiresRestart, }); } catch (error) { - return NextResponse.json( - { error: sanitizeErrorMessage(error) }, - { status: 500 } - ); + return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 }); } } @@ -168,9 +159,6 @@ export async function DELETE(request: NextRequest) { message: `Cleared ${count} feature flag override${count !== 1 ? "s" : ""}`, }); } catch (error) { - return NextResponse.json( - { error: sanitizeErrorMessage(error) }, - { status: 500 } - ); + return NextResponse.json({ error: sanitizeErrorMessage(error) }, { status: 500 }); } } diff --git a/src/lib/db/featureFlags.ts b/src/lib/db/featureFlags.ts index 0dc57cf9ab..724543c354 100644 --- a/src/lib/db/featureFlags.ts +++ b/src/lib/db/featureFlags.ts @@ -47,15 +47,21 @@ export function setFeatureFlagOverride(key: string, value: string): void { if (!definition) { throw new Error(`Unknown feature flag key: ${key}`); } - if (definition.type === "enum" && definition.enumValues && !definition.enumValues.includes(value)) { + if ( + definition.type === "enum" && + definition.enumValues && + !definition.enumValues.includes(value) + ) { throw new Error( `Invalid value "${value}" for enum flag ${key}. Allowed: ${definition.enumValues.join(", ")}` ); } const db = getDbInstance(); - db.prepare( - "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)" - ).run(NAMESPACE, key, value); + db.prepare("INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES (?, ?, ?)").run( + NAMESPACE, + key, + value + ); } /** diff --git a/src/shared/utils/featureFlags.ts b/src/shared/utils/featureFlags.ts index ee5471ee7d..2b04a77eb1 100644 --- a/src/shared/utils/featureFlags.ts +++ b/src/shared/utils/featureFlags.ts @@ -1,5 +1,8 @@ import { getFeatureFlagOverride } from "@/lib/db/featureFlags"; -import { FEATURE_FLAG_DEFINITIONS, type FeatureFlagDefinition } from "@/shared/constants/featureFlagDefinitions"; +import { + FEATURE_FLAG_DEFINITIONS, + type FeatureFlagDefinition, +} from "@/shared/constants/featureFlagDefinitions"; /** * Resolve the effective value of a feature flag. @@ -43,7 +46,12 @@ export function resolveAllFeatureFlags(): Array<{ if (envValue !== undefined && envValue !== "") { return { key: definition.key, effectiveValue: envValue, source: "env", definition }; } - return { key: definition.key, effectiveValue: definition.defaultValue, source: "default", definition }; + return { + key: definition.key, + effectiveValue: definition.defaultValue, + source: "default", + definition, + }; }); } diff --git a/tests/unit/feature-flags-settings.test.ts b/tests/unit/feature-flags-settings.test.ts index f5f6a15c63..1bf6cbf951 100644 --- a/tests/unit/feature-flags-settings.test.ts +++ b/tests/unit/feature-flags-settings.test.ts @@ -10,9 +10,8 @@ process.env.DATA_DIR = tmpDir; const core = await import("../../src/lib/db/core.ts"); -const { FEATURE_FLAG_DEFINITIONS } = await import( - "../../src/shared/constants/featureFlagDefinitions.ts" -); +const { FEATURE_FLAG_DEFINITIONS } = + await import("../../src/shared/constants/featureFlagDefinitions.ts"); const { getFeatureFlagOverrides, getFeatureFlagOverride, diff --git a/tests/unit/opencode-executor.test.ts b/tests/unit/opencode-executor.test.ts index 72cf128582..986fc94bbe 100644 --- a/tests/unit/opencode-executor.test.ts +++ b/tests/unit/opencode-executor.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, it } from "node:test"; import assert from "node:assert/strict"; const { OpencodeExecutor } = await import("../../open-sse/executors/opencode.ts"); +const { DefaultExecutor } = await import("../../open-sse/executors/default.ts"); const { PROVIDER_MODELS } = await import("../../open-sse/config/providerModels.ts"); function createMockResponse() { @@ -253,4 +254,163 @@ describe("OpencodeExecutor", () => { assert.deepEqual(fetchCalls[0].options.headers, result.headers); }); }); + + describe("user-agent forwarding", () => { + it("forwards User-Agent from clientHeaders", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "User-Agent": "opencode/1.15.4", + }); + assert.equal(headers["User-Agent"], "opencode/1.15.4"); + }); + + it("omits User-Agent when clientHeaders is null", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, null); + assert.equal(headers["User-Agent"], undefined); + }); + + it("omits User-Agent when clientHeaders has no User-Agent key", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, {}); + assert.equal(headers["User-Agent"], undefined); + }); + + it("forwards User-Agent with claude format headers", () => { + goExecutor._requestFormat = "claude"; + const headers = goExecutor.buildHeaders( + { apiKey: "claude-key" }, + true, + { "User-Agent": "opencode/1.0" }, + "minimax-m2.7" + ); + assert.equal(headers["User-Agent"], "opencode/1.0"); + assert.equal(headers["x-api-key"], "claude-key"); + assert.equal(headers["anthropic-version"], "2023-06-01"); + assert.equal(headers["Content-Type"], "application/json"); + assert.equal(headers["Accept"], "text/event-stream"); + }); + + it("forwards User-Agent without credentials", () => { + const headers = zenExecutor.buildHeaders(null, true, { "User-Agent": "opencode/1.0" }); + assert.equal(headers["User-Agent"], "opencode/1.0"); + assert.equal(headers["Authorization"], undefined); + }); + }); + + describe("opencode request metadata headers", () => { + it("forwards x-opencode-session from clientHeaders", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "x-opencode-session": "sess-123", + }); + assert.equal(headers["x-opencode-session"], "sess-123"); + }); + + it("forwards all four x-opencode-* headers from clientHeaders", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "x-opencode-session": "sess-abc", + "x-opencode-request": "req-xyz", + "x-opencode-project": "proj-5", + "x-opencode-client": "tui", + }); + assert.equal(headers["x-opencode-session"], "sess-abc"); + assert.equal(headers["x-opencode-request"], "req-xyz"); + assert.equal(headers["x-opencode-project"], "proj-5"); + assert.equal(headers["x-opencode-client"], "tui"); + }); + + it("does not add x-opencode-* when clientHeaders is null", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, null); + assert.equal(headers["x-opencode-session"], undefined); + assert.equal(headers["x-opencode-request"], undefined); + assert.equal(headers["x-opencode-project"], undefined); + assert.equal(headers["x-opencode-client"], undefined); + }); + + it("does not add x-opencode-* when clientHeaders has no matching keys", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "some-other-header": "val", + }); + assert.equal(headers["x-opencode-session"], undefined); + assert.equal(headers["x-opencode-request"], undefined); + }); + + it("skips empty string x-opencode-* values", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "x-opencode-session": "", + "x-opencode-request": "req-xyz", + }); + assert.equal(headers["x-opencode-session"], undefined); + assert.equal(headers["x-opencode-request"], "req-xyz"); + }); + + it("handles case-insensitive x-opencode-* key matching", () => { + const headers = zenExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "X-OpenCode-Session": "sess-456", + "X-OPENCODE-REQUEST": "req-789", + }); + assert.equal(headers["x-opencode-session"], "sess-456"); + assert.equal(headers["x-opencode-request"], "req-789"); + }); + + it("opencode-go executor also forwards x-opencode-* headers", () => { + const headers = goExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "x-opencode-session": "sess-go", + }); + assert.equal(headers["x-opencode-session"], "sess-go"); + }); + + it("forwards x-opencode-* headers with claude format", () => { + goExecutor._requestFormat = "claude"; + const headers = goExecutor.buildHeaders( + { apiKey: "claude-key" }, + true, + { + "x-opencode-session": "sess-claude", + "User-Agent": "opencode/1.0", + }, + "minimax-m2.7" + ); + assert.equal(headers["x-opencode-session"], "sess-claude"); + assert.equal(headers["x-api-key"], "claude-key"); + assert.equal(headers["anthropic-version"], "2023-06-01"); + assert.equal(headers["User-Agent"], "opencode/1.0"); + }); + + it("forwards x-opencode-* headers without credentials", () => { + const headers = zenExecutor.buildHeaders(null, true, { + "x-opencode-session": "sess-noauth", + }); + assert.equal(headers["x-opencode-session"], "sess-noauth"); + assert.equal(headers["Authorization"], undefined); + }); + }); +}); + +describe("DefaultExecutor", () => { + let defaultExecutor; + + beforeEach(() => { + defaultExecutor = new DefaultExecutor("openai-compatible-test"); + }); + + describe("buildHeaders", () => { + it("forwards x-opencode-* headers from clientHeaders", () => { + const headers = defaultExecutor.buildHeaders({ apiKey: "test-key" }, true, { + "x-opencode-session": "sess-abc", + "x-opencode-request": "req-xyz", + "x-opencode-project": "proj-5", + "x-opencode-client": "tui", + }); + assert.equal(headers["x-opencode-session"], "sess-abc"); + assert.equal(headers["x-opencode-request"], "req-xyz"); + assert.equal(headers["x-opencode-project"], "proj-5"); + assert.equal(headers["x-opencode-client"], "tui"); + }); + + it("preserves existing behavior when clientHeaders is null", () => { + const headers = defaultExecutor.buildHeaders({ apiKey: "test-key" }, true, null); + assert.equal(headers["x-opencode-session"], undefined); + assert.equal(headers["Content-Type"], "application/json"); + assert.equal(headers["Accept"], "text/event-stream"); + assert.equal(headers["Authorization"], "Bearer test-key"); + }); + }); });