diff --git a/.claude/skills/fin-daily-pulse/SKILL.md b/.claude/skills/fin-daily-pulse/SKILL.md index 2f2a74af..1c98eba8 100644 --- a/.claude/skills/fin-daily-pulse/SKILL.md +++ b/.claude/skills/fin-daily-pulse/SKILL.md @@ -157,9 +157,9 @@ workspace/finance/reports/daily/[C] YYYY-MM-DD-financial-pulse.html Create the directory `workspace/finance/reports/daily/` if it does not exist. -## Step 8 — Confirm and notify (ONE Telegram message only) +## Step 8 — Confirm -Output the completion summary, then send **exactly one** Telegram message. Do NOT call `reply` more than once per run. +Output the completion summary in the terminal: ``` ## Financial Pulse generated @@ -171,6 +171,4 @@ Output the completion summary, then send **exactly one** Telegram message. Do NO **Alerts:** {N} attention points ``` -Call `reply` **once** with a short summary (do not send the full markdown above — send a compact version): - -- Format: `[emoji] Financial Pulse [date] | MRR: R$ X,XXX | Receita: R$ X,XXX | Churn: X% | [N] alertas` +Do NOT send a Telegram message here — the caller handles notifications. diff --git a/.claude/skills/fin-monthly-close-kickoff/SKILL.md b/.claude/skills/fin-monthly-close-kickoff/SKILL.md index a62548f3..83a97202 100644 --- a/.claude/skills/fin-monthly-close-kickoff/SKILL.md +++ b/.claude/skills/fin-monthly-close-kickoff/SKILL.md @@ -182,9 +182,3 @@ Create the directory `workspace/finance/reports/monthly/` if it does not exist. **Checklist:** X/10 completed **Finance team pending items:** {N} items ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + "Monthly Close" + month's result + pending items (2-3 lines) diff --git a/.claude/skills/fin-weekly-report/SKILL.md b/.claude/skills/fin-weekly-report/SKILL.md index ef3c85f2..4c344aa2 100644 --- a/.claude/skills/fin-weekly-report/SKILL.md +++ b/.claude/skills/fin-weekly-report/SKILL.md @@ -164,9 +164,3 @@ Create the directory `workspace/finance/reports/weekly/` if it does not exist. **MRR total:** R$ X,XXX (Stripe: R$ X,XXX | Evo Academy: R$ X,XXX) | **Projected 30d balance:** R$ XX,XXX **Alerts:** {N} overdue accounts | {N} pending invoices ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + "Financial Weekly" + revenue vs expenses + MRR + alerts (2-3 lines) diff --git a/.claude/skills/gog-email-triage/SKILL.md b/.claude/skills/gog-email-triage/SKILL.md index 44cc3d45..3bda6b0f 100644 --- a/.claude/skills/gog-email-triage/SKILL.md +++ b/.claude/skills/gog-email-triage/SKILL.md @@ -354,11 +354,3 @@ See `skills/gog/_shared/references/testing.md` for complete test plan. - If email subject/sender contains obvious credentials or secrets, redact in output - For recurring newsletters, suggest creating a filter/rule rather than manual archiving - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/int-github-review/SKILL.md b/.claude/skills/int-github-review/SKILL.md index d9ac780a..f997e810 100644 --- a/.claude/skills/int-github-review/SKILL.md +++ b/.claude/skills/int-github-review/SKILL.md @@ -116,9 +116,7 @@ Create directory if it does not exist. - **Focus on action** — what needs the responsible person's attention, not just numbers -### Notify via Telegram +### Notification line -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" +Write as the last line of your output: +TELEGRAM_MSG: 🐙 GitHub Review [date] | [main result in 1 line] diff --git a/.claude/skills/int-linear-review/SKILL.md b/.claude/skills/int-linear-review/SKILL.md index 1deb55e7..94b14f70 100644 --- a/.claude/skills/int-linear-review/SKILL.md +++ b/.claude/skills/int-linear-review/SKILL.md @@ -95,9 +95,7 @@ Create the directory `workspace/projects/linear-reviews/` if it does not exist. - **Be direct** — numbers, not narrative -### Notify via Telegram +### Notification line -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" +Write as the last line of your output: +TELEGRAM_MSG: 📋 Linear Review [date] | [main result in 1 line] diff --git a/.claude/skills/int-sync-meetings/SKILL.md b/.claude/skills/int-sync-meetings/SKILL.md index c4b22f22..b4e3f284 100644 --- a/.claude/skills/int-sync-meetings/SKILL.md +++ b/.claude/skills/int-sync-meetings/SKILL.md @@ -184,14 +184,13 @@ When finished, present a short summary: Without listing tasks one by one — just counts. If the user wants details, they ask. -### Step 9 — Notify via Telegram +### Step 9 — Notification line -Send the Step 8 summary via Telegram to the user using the `/int-telegram` skill: -- Chat ID: `YOUR_CHAT_ID` -- Use `reply(chat_id="YOUR_CHAT_ID", text="...")` via MCP -- Short format: emoji + title + meeting and task count +Only if at least one new meeting was processed, write this as the last line of your output: -If there are no new meetings (stopped at Step 2), do **NOT** send any Telegram message — stay silent. Only notify when at least one new meeting was processed. +TELEGRAM_MSG: 🎙️ Sync Fathom — N reunião(ões) processada(s) | N tarefas criadas + +If no new meetings were processed (stopped at Step 2), do NOT write a TELEGRAM_MSG line. ## Notes diff --git a/.claude/skills/prod-dashboard/SKILL.md b/.claude/skills/prod-dashboard/SKILL.md index 56c3c84a..56da10f4 100644 --- a/.claude/skills/prod-dashboard/SKILL.md +++ b/.claude/skills/prod-dashboard/SKILL.md @@ -142,10 +142,3 @@ Present a short summary: **Health:** Product {status} | Community {status} | Financial {status} | Routines {status} **Alerts:** {N} attention points ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + health status of each area (1-3 lines) -- If there were no updates, send anyway with "no updates" diff --git a/.claude/skills/prod-review-todoist/SKILL.md b/.claude/skills/prod-review-todoist/SKILL.md index 2180aab2..34a78c1d 100644 --- a/.claude/skills/prod-review-todoist/SKILL.md +++ b/.claude/skills/prod-review-todoist/SKILL.md @@ -124,10 +124,3 @@ If the user wants to see details of what changed, they ask. - **If unsure about the category**, use `[Operations]` as fallback - **Execute first, report after** — no intermediate report - -### Notify via Telegram - -Upon completion, send **exactly ONE** Telegram message — do NOT call `reply()` more than once per run: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines, all combined in a single message) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/prod-trends/SKILL.md b/.claude/skills/prod-trends/SKILL.md index 051ce18d..062f5270 100644 --- a/.claude/skills/prod-trends/SKILL.md +++ b/.claude/skills/prod-trends/SKILL.md @@ -163,11 +163,3 @@ Create `memory/trends/` if it does not exist. - **If a source has no data, skip** — do not block due to a missing report - **Focus on action** — each insight should lead to a concrete recommendation - **Do not alarm without evidence** — red only when the metric truly indicates risk - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/pulse-monthly/SKILL.md b/.claude/skills/pulse-monthly/SKILL.md index c84a2439..dbe8cfb1 100644 --- a/.claude/skills/pulse-monthly/SKILL.md +++ b/.claude/skills/pulse-monthly/SKILL.md @@ -178,9 +178,3 @@ Create the directory `workspace/community/reports/monthly/` if it does not exist **Sentiment:** {trend} | **Resolution:** {X}% **Highlights:** {N} features, {N} bugs, {N} docs gaps ``` - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + "Community Monthly" + MAM + sentiment + highlights (2-3 lines) diff --git a/.claude/skills/pulse-weekly/SKILL.md b/.claude/skills/pulse-weekly/SKILL.md index ff41cc33..10d584fb 100644 --- a/.claude/skills/pulse-weekly/SKILL.md +++ b/.claude/skills/pulse-weekly/SKILL.md @@ -111,11 +111,3 @@ Report saved to workspace/community/reports/weekly/ - **Docs gap is gold** — each question without docs becomes a backlog item - **Comparison is fundamental** — always show trend vs previous week - **Product insights** — the most valuable section, handle with care - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/sage-strategy-digest/SKILL.md b/.claude/skills/sage-strategy-digest/SKILL.md index ff47d1c3..f4f8135e 100644 --- a/.claude/skills/sage-strategy-digest/SKILL.md +++ b/.claude/skills/sage-strategy-digest/SKILL.md @@ -96,11 +96,3 @@ Present a short and direct version. - **Opinions flagged** — when it is opinion vs data, make it clear - **One recommendation** — do not give 10 suggestions, give 1 clear one - **Connect the dots** — the value of the digest is crossing areas, not repeating individual reports - - -### Notify via Telegram - -Upon completion, send a short summary via Telegram to the user: -- Use the Telegram MCP: `reply(chat_id="YOUR_CHAT_ID", text="...")` -- Format: emoji + routine name + main result (1-3 lines) -- If the routine had no updates, send anyway with "no updates" diff --git a/.claude/skills/social-analytics-report/SKILL.md b/.claude/skills/social-analytics-report/SKILL.md index ce8aec24..fdc2c2c9 100644 --- a/.claude/skills/social-analytics-report/SKILL.md +++ b/.claude/skills/social-analytics-report/SKILL.md @@ -137,16 +137,3 @@ workspace/social/reports/consolidated/[C] YYYY-MM-DD-social-analytics.html ``` Create directory if it does not exist. - -### Step 9 — Telegram - -Notify: `reply(chat_id="YOUR_CHAT_ID", text="...")` -Format: -``` -📊 Social Analytics — {period} -👥 Total followers: {N} ({delta}) -📹 YouTube: {subs} sub | {eng}% eng -📸 Instagram: {followers} fol | {eng}% eng -💼 LinkedIn: profile connected -🏆 Top: "{best content}" ({platform}, {eng}%) -``` diff --git a/.claude/skills/social-youtube-report/SKILL.md b/.claude/skills/social-youtube-report/SKILL.md index 63d2ace6..8d5c42bf 100644 --- a/.claude/skills/social-youtube-report/SKILL.md +++ b/.claude/skills/social-youtube-report/SKILL.md @@ -64,8 +64,3 @@ workspace/social/reports/youtube/[C] YYYY-MM-DD-youtube-{period}.html ``` Criar diretório if it does not exist. - -### Step 7 — Telegram - -Notify: `reply(chat_id="946857210", text="...")` -Format: emoji + canal + inscritos + delta + melhor vídeo diff --git a/ADWs/runner.py b/ADWs/runner.py index c7b75fbe..5a2cda4b 100644 --- a/ADWs/runner.py +++ b/ADWs/runner.py @@ -303,27 +303,47 @@ def run_skill( Args: notify_telegram: Controls post-skill Telegram notification. - False (default) — no notification (skill must NOT call reply() either). - True — appends notification instruction; reads chat_id from - TELEGRAM_CHAT_ID env var. + False (default) — no notification. + True — Python sends ONE Telegram after the skill; reads + chat_id from TELEGRAM_CHAT_ID env var. "" — same as True but overrides the chat_id. + + The agent is asked to output a line "TELEGRAM_MSG: " in its stdout. + Python reads that line and calls send_telegram() exactly once. + The agent NEVER calls the Telegram MCP tool directly. """ - prompt = f"Execute the skill /{skill_name} {args}".strip() + chat_id = None if notify_telegram: chat_id = ( notify_telegram if isinstance(notify_telegram, str) else os.environ.get("TELEGRAM_CHAT_ID", "") + ) or None + + prompt = f"Execute the skill /{skill_name} {args}".strip() + if chat_id: + prompt += ( + f"\n\n---\n" + f"Ao finalizar, escreva na última linha do output:\n" + f"TELEGRAM_MSG: [emoji] [nome da rotina] [data] | [resultado 1] | [resultado 2]\n" + f"Apenas UMA linha TELEGRAM_MSG:. NÃO use a ferramenta Telegram/reply — " + f"o sistema Python lê essa linha e envia a notificação automaticamente.\n" + f"---" ) - if chat_id: - prompt += ( - f"\n\nAo concluir TODOS os passos acima, envie UMA única mensagem Telegram via:" - f'\nreply(chat_id="{chat_id}", text="...")' - f"\nFormato: emoji + nome da rotina + principais resultados em 2-3 linhas." - f"\nCRÍTICO: chame reply() EXATAMENTE UMA VEZ, somente aqui no final." - f" Não envie mensagens intermediárias nem de progresso." - ) - return run_claude(prompt, log_name or skill_name, timeout, agent=agent) + + result = run_claude(prompt, log_name or skill_name, timeout, agent=agent) + + if chat_id and result.get("returncode", -1) == 0: + stdout = result.get("stdout", "") + for line in reversed(stdout.splitlines()): + line = line.strip() + if line.startswith("TELEGRAM_MSG:"): + msg = line[len("TELEGRAM_MSG:"):].strip() + if msg: + send_telegram(msg, chat_id=chat_id) + break # only ever send one message + + return result def run_script(func, log_name: str = "unnamed", timeout: int = 120) -> dict: diff --git a/dashboard/backend/app.py b/dashboard/backend/app.py index 2dccb597..7b28a5e1 100644 --- a/dashboard/backend/app.py +++ b/dashboard/backend/app.py @@ -433,7 +433,7 @@ def _cors_allowed_origins(): ); CREATE TABLE IF NOT EXISTS plugin_audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, - plugin_id TEXT NOT NULL, + slug TEXT NOT NULL, action TEXT NOT NULL, payload TEXT, success INTEGER NOT NULL DEFAULT 1, @@ -450,8 +450,9 @@ def _cors_allowed_origins(): PRIMARY KEY (plugin_slug, handler_path) ); CREATE INDEX IF NOT EXISTS idx_plugins_status ON plugins_installed(status); - CREATE INDEX IF NOT EXISTS idx_plugin_audit_plugin ON plugin_audit_log(plugin_id, created_at); + CREATE INDEX IF NOT EXISTS idx_plugin_audit_slug ON plugin_audit_log(slug); CREATE INDEX IF NOT EXISTS idx_hook_cb_disabled ON plugin_hook_circuit_state(disabled_until); + """) _conn.commit() # --- End plugins migration --- diff --git a/dashboard/backend/brain_repo/restore.py b/dashboard/backend/brain_repo/restore.py index dc328483..9ca596cb 100644 --- a/dashboard/backend/brain_repo/restore.py +++ b/dashboard/backend/brain_repo/restore.py @@ -16,7 +16,14 @@ def _event(step: str, progress: int, message: str, error: bool = False) -> dict: - return {"step": step, "progress": progress, "message": message, "error": error} + event_type = "error" if error else "progress" + return { + "type": event_type, + "step": step, + "progress": progress, + "message": message, + "error": error + } def _cleanup_staging(staging: Path) -> None: @@ -47,6 +54,9 @@ def execute_restore( """ from brain_repo import git_ops, manifest as manifest_mod, migrations, secrets_scanner # type: ignore[import] + # ✅ Cria o diretório pai primeiro + STAGING_DIR.mkdir(parents=True, exist_ok=True) + staging = STAGING_DIR / f"restore-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}" staging.mkdir(parents=True, exist_ok=True) @@ -57,10 +67,16 @@ def execute_restore( # If ref is not HEAD/default branch, checkout that specific ref if ref and ref not in ("HEAD", "main", "master"): ref_staging = staging / "_ref_extract" + ref_staging.mkdir(parents=True, exist_ok=True) # ✅ CREATE THE DIRECTORY FIRST git_ops.checkout_ref(staging, ref, ref_staging) # Use extracted ref content instead of full clone - shutil.rmtree(staging, ignore_errors=True) - ref_staging.rename(staging) + # ✅ Move o conteúdo de ref_staging para staging + for item in ref_staging.iterdir(): + dest = staging / item.name + if dest.exists(): + shutil.rmtree(dest, ignore_errors=True) + shutil.move(str(item), str(dest)) + shutil.rmtree(ref_staging, ignore_errors=True) except Exception as exc: yield _event("clone", 5, f"Clone failed: {exc}", error=True) _cleanup_staging(staging) @@ -194,4 +210,4 @@ def execute_restore( # ---------------------------------------------------------------- 10. complete _cleanup_staging(staging) - yield _event("complete", 100, "Restore complete") + yield {"type": "complete", "step": "complete", "progress": 100, "message": "Restore complete", "error": False} diff --git a/dashboard/backend/routes/brain_repo.py b/dashboard/backend/routes/brain_repo.py index 5bd8dd03..32a1f4ca 100644 --- a/dashboard/backend/routes/brain_repo.py +++ b/dashboard/backend/routes/brain_repo.py @@ -325,7 +325,7 @@ def connect(): create_repo - Name of a new private repo to create (mutually exclusive with repo_url) """ data = request.get_json() or {} - token = data.get("token", "").strip() + token = data.get("token", "").strip() or request.args.get("token", "").strip() repo_url = data.get("repo_url", "").strip() create_repo = data.get("create_repo", "").strip() @@ -527,6 +527,9 @@ def snapshots(): if not token: abort(400, description="Could not decrypt stored token") + if not config.repo_owner or not config.repo_name: + abort(400, description="Repository info not fully configured — please reconnect the brain repo") + try: from brain_repo.github_api import list_snapshots result = list_snapshots(token, config.repo_owner, config.repo_name) diff --git a/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx b/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx index 7f9d5f6f..cd816ef8 100644 --- a/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx +++ b/dashboard/frontend/src/pages/onboarding/restore/RestoreFlow.tsx @@ -21,12 +21,14 @@ export default function RestoreFlow({ onComplete, onBack }: RestoreFlowProps) { const [step, setStep] = useState('select-repo') const [repoUrl, setRepoUrl] = useState('') const [snapshot, setSnapshot] = useState(null) + const [token, setToken] = useState('') if (step === 'select-repo') { return ( { + onNext={(url: string, pat: string) => { setRepoUrl(url) + setToken(pat) setStep('select-snapshot') }} onBack={onBack} @@ -38,6 +40,7 @@ export default function RestoreFlow({ onComplete, onBack }: RestoreFlowProps) { return ( { setSnapshot(s) setStep('confirm') diff --git a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx index a7dfa790..4b4feac4 100644 --- a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx +++ b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectRepo.tsx @@ -12,7 +12,7 @@ interface Repo { } interface RestoreSelectRepoProps { - onNext: (repoUrl: string) => void + onNext: (repoUrl: string, token: string) => void onBack: () => void } @@ -65,7 +65,7 @@ export default function RestoreSelectRepo({ onNext, onBack }: RestoreSelectRepoP setError(t('restore.selectRepo.selectRepo')) return } - onNext(selectedRepo.html_url) + onNext(selectedRepo.html_url, token.trim()) } return ( diff --git a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx index 4822c17b..d5bae4b1 100644 --- a/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx +++ b/dashboard/frontend/src/pages/onboarding/restore/RestoreSelectSnapshot.tsx @@ -25,6 +25,7 @@ interface SelectedSnapshot { interface RestoreSelectSnapshotProps { repoUrl: string + token: string onNext: (snapshot: SelectedSnapshot) => void onBack: () => void } @@ -62,7 +63,7 @@ function SnapshotItem({ ) } -export default function RestoreSelectSnapshot({ repoUrl, onNext, onBack }: RestoreSelectSnapshotProps) { +export default function RestoreSelectSnapshot({ repoUrl, token, onNext, onBack }: RestoreSelectSnapshotProps) { const { t } = useTranslation() const [data, setData] = useState(null) const [loading, setLoading] = useState(true) @@ -71,11 +72,27 @@ export default function RestoreSelectSnapshot({ repoUrl, onNext, onBack }: Resto const [error, setError] = useState('') useEffect(() => { - api.get('/brain-repo/snapshots') - .then((d: SnapshotData) => setData(d)) - .catch(() => setError(t('restore.selectSnapshot.failed'))) - .finally(() => setLoading(false)) - }, [repoUrl, t]) + const loadSnapshots = async () => { + try { + setLoading(true) + setError('') + // ✅ Primeiro: conectar o repositório + await api.post('/brain-repo/connect', { token, repo_url: repoUrl }) + + // ✅ Depois: carregar os snapshots + const d = await api.get('/brain-repo/snapshots') as SnapshotData + setData(d) + } catch (err) { + setError(t('restore.selectSnapshot.failed')) + } finally { + setLoading(false) + } + } + + if (token && repoUrl) { + loadSnapshots() + } + }, [repoUrl, token, t]) const handleNext = () => { if (!selected) { diff --git a/dashboard/terminal-server/package-lock.json b/dashboard/terminal-server/package-lock.json index 1a4af597..0611b4de 100644 --- a/dashboard/terminal-server/package-lock.json +++ b/dashboard/terminal-server/package-lock.json @@ -858,7 +858,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -1063,7 +1062,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz", "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -1765,7 +1763,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/dashboard/terminal-server/src/chat-bridge.js b/dashboard/terminal-server/src/chat-bridge.js index 64827bf6..caf138b1 100644 --- a/dashboard/terminal-server/src/chat-bridge.js +++ b/dashboard/terminal-server/src/chat-bridge.js @@ -48,7 +48,10 @@ const NEEDS_APPROVAL = new Set([ * Extracts YAML frontmatter for metadata and the body as the prompt. */ function loadAgentFile(agentName, cwd) { - const agentPath = path.join(cwd, '.claude', 'agents', `${agentName}.md`); + // Agent definitions live at the workspace root — cwd varies per ticket session. + const rootPath = path.join(WORKSPACE_ROOT, '.claude', 'agents', `${agentName}.md`); + const cwdPath = path.join(cwd, '.claude', 'agents', `${agentName}.md`); + const agentPath = fs.existsSync(rootPath) ? rootPath : cwdPath; if (!fs.existsSync(agentPath)) { console.warn(`[chat-bridge] Agent file not found: ${agentPath}`); return null; diff --git a/pyproject.toml b/pyproject.toml index 802fcdc0..66185c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "watchdog>=4.0", "sqlparse>=0.4,<1.0", "jsonschema>=4.21", + "websocket-client>=1.7", ] [project.scripts] diff --git a/uv.lock b/uv.lock index 6d6116d7..69360e19 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'darwin'", @@ -589,7 +589,7 @@ wheels = [ [[package]] name = "evo-nexus" -version = "0.32.3" +version = "0.32.2" source = { virtual = "." } dependencies = [ { name = "alembic" }, @@ -826,18 +826,14 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/bc/e30e1e3d5e8860b0e0ce4d2b16b2681b77fd13542fc0d72f7e3c22d16eff/greenlet-3.4.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d18eae9a7fb0f499efcd146b8c9750a2e1f6e0e93b5a382b3481875354a430e6", size = 284315, upload-time = "2026-04-08T17:02:52.322Z" }, { url = "https://files.pythonhosted.org/packages/5b/cc/e023ae1967d2a26737387cac083e99e47f65f58868bd155c4c80c01ec4e0/greenlet-3.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636d2f95c309e35f650e421c23297d5011716be15d966e6328b367c9fc513a82", size = 601916, upload-time = "2026-04-08T16:24:35.533Z" }, { url = "https://files.pythonhosted.org/packages/67/32/5be1677954b6d8810b33abe94e3eb88726311c58fa777dc97e390f7caf5a/greenlet-3.4.0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:234582c20af9742583c3b2ddfbdbb58a756cfff803763ffaae1ac7990a9fac31", size = 616399, upload-time = "2026-04-08T16:30:54.536Z" }, - { url = "https://files.pythonhosted.org/packages/82/0a/3a4af092b09ea02bcda30f33fd7db397619132fe52c6ece24b9363130d34/greenlet-3.4.0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ac6a5f618be581e1e0713aecec8e54093c235e5fa17d6d8eb7ffc487e2300508", size = 621077, upload-time = "2026-04-08T16:40:34.946Z" }, { url = "https://files.pythonhosted.org/packages/74/bf/2d58d5ea515704f83e34699128c9072a34bea27d2b6a556e102105fe62a5/greenlet-3.4.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:523677e69cd4711b5a014e37bc1fb3a29947c3e3a5bb6a527e1cc50312e5a398", size = 611978, upload-time = "2026-04-08T15:56:31.335Z" }, - { url = "https://files.pythonhosted.org/packages/8c/39/3786520a7d5e33ee87b3da2531f589a3882abf686a42a3773183a41ef010/greenlet-3.4.0-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:d336d46878e486de7d9458653c722875547ac8d36a1cff9ffaf4a74a3c1f62eb", size = 416893, upload-time = "2026-04-08T16:43:02.392Z" }, { url = "https://files.pythonhosted.org/packages/bd/69/6525049b6c179d8a923256304d8387b8bdd4acab1acf0407852463c6d514/greenlet-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b45e45fe47a19051a396abb22e19e7836a59ee6c5a90f3be427343c37908d65b", size = 1571957, upload-time = "2026-04-08T16:26:17.041Z" }, { url = "https://files.pythonhosted.org/packages/4e/6c/bbfb798b05fec736a0d24dc23e81b45bcee87f45a83cfb39db031853bddc/greenlet-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5434271357be07f3ad0936c312645853b7e689e679e29310e2de09a9ea6c3adf", size = 1637223, upload-time = "2026-04-08T15:57:27.556Z" }, { url = "https://files.pythonhosted.org/packages/b7/7d/981fe0e7c07bd9d5e7eb18decb8590a11e3955878291f7a7de2e9c668eb7/greenlet-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:a19093fbad824ed7c0f355b5ff4214bffda5f1a7f35f29b31fcaa240cc0135ab", size = 237902, upload-time = "2026-04-08T17:03:14.16Z" }, { url = "https://files.pythonhosted.org/packages/fb/c6/dba32cab7e3a625b011aa5647486e2d28423a48845a2998c126dd69c85e1/greenlet-3.4.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:805bebb4945094acbab757d34d6e1098be6de8966009ab9ca54f06ff492def58", size = 285504, upload-time = "2026-04-08T15:52:14.071Z" }, { url = "https://files.pythonhosted.org/packages/54/f4/7cb5c2b1feb9a1f50e038be79980dfa969aa91979e5e3a18fdbcfad2c517/greenlet-3.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:439fc2f12b9b512d9dfa681c5afe5f6b3232c708d13e6f02c845e0d9f4c2d8c6", size = 605476, upload-time = "2026-04-08T16:24:37.064Z" }, { url = "https://files.pythonhosted.org/packages/d6/af/b66ab0b2f9a4c5a867c136bf66d9599f34f21a1bcca26a2884a29c450bd9/greenlet-3.4.0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a70ed1cb0295bee1df57b63bf7f46b4e56a5c93709eea769c1fec1bb23a95875", size = 618336, upload-time = "2026-04-08T16:30:56.59Z" }, - { url = "https://files.pythonhosted.org/packages/6d/31/56c43d2b5de476f77d36ceeec436328533bff960a4cba9a07616e93063ab/greenlet-3.4.0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c5696c42e6bb5cfb7c6ff4453789081c66b9b91f061e5e9367fa15792644e76", size = 625045, upload-time = "2026-04-08T16:40:37.111Z" }, { url = "https://files.pythonhosted.org/packages/e5/5c/8c5633ece6ba611d64bf2770219a98dd439921d6424e4e8cf16b0ac74ea5/greenlet-3.4.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c660bce1940a1acae5f51f0a064f1bc785d07ea16efcb4bc708090afc4d69e83", size = 613515, upload-time = "2026-04-08T15:56:32.478Z" }, - { url = "https://files.pythonhosted.org/packages/80/ca/704d4e2c90acb8bdf7ae593f5cbc95f58e82de95cc540fb75631c1054533/greenlet-3.4.0-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:89995ce5ddcd2896d89615116dd39b9703bfa0c07b583b85b89bf1b5d6eddf81", size = 419745, upload-time = "2026-04-08T16:43:04.022Z" }, { url = "https://files.pythonhosted.org/packages/a9/df/950d15bca0d90a0e7395eb777903060504cdb509b7b705631e8fb69ff415/greenlet-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee407d4d1ca9dc632265aee1c8732c4a2d60adff848057cdebfe5fe94eb2c8a2", size = 1574623, upload-time = "2026-04-08T16:26:18.596Z" }, { url = "https://files.pythonhosted.org/packages/1a/e7/0839afab829fcb7333c9ff6d80c040949510055d2d4d63251f0d1c7c804e/greenlet-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:956215d5e355fffa7c021d168728321fd4d31fd730ac609b1653b450f6a4bc71", size = 1639579, upload-time = "2026-04-08T15:57:29.231Z" }, { url = "https://files.pythonhosted.org/packages/d9/2b/b4482401e9bcaf9f5c97f67ead38db89c19520ff6d0d6699979c6efcc200/greenlet-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:5cb614ace7c27571270354e9c9f696554d073f8aa9319079dcba466bbdead711", size = 238233, upload-time = "2026-04-08T17:02:54.286Z" }, @@ -845,9 +841,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, - { url = "https://files.pythonhosted.org/packages/44/57/eae2cac10421feae6c0987e3dc106c6d86262b1cb379e171b017aba893a6/greenlet-3.4.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6f0def07ec9a71d72315cf26c061aceee53b306c36ed38c35caba952ea1b319d", size = 624901, upload-time = "2026-04-08T16:40:38.981Z" }, { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, - { url = "https://files.pythonhosted.org/packages/6a/8a/0e73c9b94f31d1cc257fe79a0eff621674141cdae7d6d00f40de378a1e42/greenlet-3.4.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:0e1254cf0cbaa17b04320c3a78575f29f3c161ef38f59c977108f19ffddaf077", size = 423927, upload-time = "2026-04-08T16:43:05.293Z" }, { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, @@ -855,9 +849,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, - { url = "https://files.pythonhosted.org/packages/e0/93/c8c508d68ba93232784bbc1b5474d92371f2897dfc6bc281b419f2e0d492/greenlet-3.4.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98eedd1803353daf1cd9ef23eef23eda5a4d22f99b1f998d273a8b78b70dd47f", size = 628455, upload-time = "2026-04-08T16:40:40.698Z" }, { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, - { url = "https://files.pythonhosted.org/packages/7f/46/cfaaa0ade435a60550fd83d07dfd5c41f873a01da17ede5c4cade0b9bab8/greenlet-3.4.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:b7857e2202aae67bc5725e0c1f6403c20a8ff46094ece015e7d474f5f7020b55", size = 426238, upload-time = "2026-04-08T16:43:06.865Z" }, { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, @@ -865,9 +857,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, - { url = "https://files.pythonhosted.org/packages/d2/4a/74078d3936712cff6d3c91a930016f476ce4198d84e224fe6d81d3e02880/greenlet-3.4.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06c2d3b89e0c62ba50bd7adf491b14f39da9e7e701647cb7b9ff4c99bee04b19", size = 673405, upload-time = "2026-04-08T16:40:42.527Z" }, { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, - { url = "https://files.pythonhosted.org/packages/79/3e/df8a83ab894751bc31e1106fdfaa80ca9753222f106b04de93faaa55feb7/greenlet-3.4.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:070b8bac2ff3b4d9e0ff36a0d19e42103331d9737e8504747cd1e659f76297bd", size = 471670, upload-time = "2026-04-08T16:43:08.512Z" }, { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, @@ -875,9 +865,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, - { url = "https://files.pythonhosted.org/packages/ef/92/466b0d9afd44b8af623139a3599d651c7564fa4152f25f117e1ee5949ffb/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c4cd56a9eb7a6444edbc19062f7b6fbc8f287c663b946e3171d899693b1c19fa", size = 665872, upload-time = "2026-04-08T16:40:43.912Z" }, { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, - { url = "https://files.pythonhosted.org/packages/0d/14/3395a7ef3e260de0325152ddfe19dffb3e49fe10873b94654352b53ad48e/greenlet-3.4.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:1f85f204c4d54134ae850d401fa435c89cd667d5ce9dc567571776b45941af72", size = 489237, upload-time = "2026-04-08T16:43:09.993Z" }, { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" },