feat(billing): /credits command — balance + portal top-up handoff (#44776)
* feat(billing): /usage → portal top-up browser handoff
Add the terminal side of the billing slice (phase 2a): start a top-up by
throwing the user to the portal billing page with the top-up modal open. The
terminal does not confirm, poll, or track payment — checkout completes in the
browser and the next /usage shows the new balance.
- nous_account.py: parse organisation.slug/name from /api/oauth/account into
NousPortalAccountInfo; add nous_portal_topup_url() building the org-pinned
{base}/orgs/{slug}/billing?topup=open with a null-slug fallback to the legacy
{base}/billing?topup=open (never /orgs/None/...).
- portal_cli.py: 'hermes portal topup' — fresh account fetch, identity line
(Topping up as <email> / org <name>), browser open with printed-URL fallback,
no-wait closing copy. No polling/confirmation (deferred to 2b).
- account_usage.py: the shared /usage credits block now links the org-pinned
top-up URL (auto-opens the modal) + points to the command.
Depends on NAS #409 (organisation.slug/name + ?topup=open). Do not merge until
that is live on the target env; until then /api/oauth/account returns
organisation: { id } only and the URL falls back to legacy.
* feat(billing): /credits command for balance + top-up handoff
Replace the standalone `hermes portal topup` subcommand with an in-session
/credits slash command — a focused money surface (balance in, top-up out) that
works in the CLI, TUI, and every messaging platform from one registry entry.
- commands.py: register /credits (Info category). Slack is at its 50-slash cap,
so /credits is routed via /hermes credits on Slack only (new
_SLACK_VIA_HERMES_ONLY set) to avoid clamping a canonical command off the
native list and breaking Telegram parity; native everywhere else.
- account_usage.py: build_credits_view() — one portal fetch → balance lines +
identity line + org-pinned top-up URL + depleted flag, consumed by all
surfaces. Reuses the same snapshot/URL builder as /usage so numbers match.
- cli.py: _show_credits() — balance block + identity line + 3-button panel
(Open top-up / Copy link / Cancel) via the existing prompt_toolkit modal.
ASK, never auto-launch; headless falls back to printing the URL.
- gateway/slash_commands.py: _handle_credits_command() — renders the block +
tappable top-up URL + no-wait copy; works on button and plain-text platforms.
- /usage credits line now points to /credits.
- Retire `hermes portal topup` (portal_cli.py back to baseline); the engine
(slug/name parse + nous_portal_topup_url) stays as the shared core.
No polling, no payment confirmation (billing phase 2a). Depends on NAS #409.
* fix(credits): /credits works in the TUI slash-worker (non-interactive)
In the TUI, /credits runs in the slash-worker subprocess where there is no
live prompt_toolkit app and stdin is the JSON-RPC pipe. _show_credits called
the 3-button modal unconditionally, which fell back to reading stdin →
exception → slash.exec rejected → the command produced no output (only the
pre-existing 'Credit access paused' banner showed).
- _show_credits: when self._app is None (TUI worker / piped / non-interactive),
render the text variant — balance block + tappable top-up URL + no-wait line,
same affordance as the messaging surfaces — and skip the modal entirely. The
3-button panel still renders in the interactive CLI.
- Depleted banner copy: 'run /usage for balance' → 'run /credits to top up'
now that /credits is the dedicated money surface (+ tests).
- Regression tests: _show_credits with self._app=None renders text and never
invokes the modal; logged-out path.
* feat(tui): credits.view RPC for the /credits tappable top-up button
Add a credits.view JSON-RPC method returning the structured CreditsView
(logged_in, balance_lines, identity_line, topup_url, depleted) so the TUI can
render a clickable <Link> top-up button instead of plain text. Account-
independent (portal fetch gated on a logged-in Nous account), fail-open to
{logged_in: false} on any hiccup. Mirrors session.usage's credits-block pattern.
Frontend (TUI-local /credits command + Ink component) lands separately.
* feat(tui): /credits command with keyboard-driven top-up confirm
TUI-local /credits: fetches the structured balance via the credits.view RPC,
prints the balance + identity + top-up URL, then arms the EXISTING confirm
overlay (Enter = open top-up in browser via openExternalUrl, Esc = cancel).
Reuses ConfirmReq — no new overlay component/state/input handler. Headless
(openExternalUrl returns false) falls back to printing the URL.
- gatewayTypes.ts: CreditsViewResponse.
- commands/credits.ts: the command (mirrors /status's rpc+guarded pattern).
- registry.ts: register creditsCommands.
- test: balance+overlay armed, headless fallback, no-url, logged-out (4 cases).
Matches the CLI /credits 'Enter to open' affordance. Phase 2a: no polling.
This commit is contained in:
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Gedetailleerde gebruik beskikbaar na die eerste agent-antwoord)_"
|
||||
no_data: "Geen gebruiksdata beskikbaar vir hierdie sessie nie."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Nie by Nous Portal aangemeld nie. Meld aan om jou kredietsaldo te sien en op te laai."
|
||||
|
||||
verbose:
|
||||
not_enabled: "Die `/verbose`-opdrag is nie vir boodskapplatforms geaktiveer nie.\n\nAktiveer dit in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Gereedskap-vordering: **AF** — geen gereedskap-aktiwiteit word vertoon nie."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Detaillierte Nutzung nach der ersten Agentenantwort verfügbar)_"
|
||||
no_data: "Keine Nutzungsdaten für diese Sitzung verfügbar."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Nicht bei Nous Portal angemeldet. Melde dich an, um dein Guthaben zu sehen und aufzuladen."
|
||||
|
||||
verbose:
|
||||
not_enabled: "Der Befehl `/verbose` ist für Messaging-Plattformen nicht aktiviert.\n\nIn `config.yaml` aktivieren:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Tool-Fortschritt: **OFF** — keine Tool-Aktivität angezeigt."
|
||||
|
||||
@@ -346,6 +346,9 @@ gateway:
|
||||
detailed_after_first: "_(Detailed usage available after the first agent response)_"
|
||||
no_data: "No usage data available for this session."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Not logged into Nous Portal. Log in to see your credit balance and top up."
|
||||
|
||||
verbose:
|
||||
not_enabled: "The `/verbose` command is not enabled for messaging platforms.\n\nEnable it in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Tool progress: **OFF** — no tool activity shown."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Uso detallado disponible tras la primera respuesta del agente)_"
|
||||
no_data: "No hay datos de uso disponibles para esta sesión."
|
||||
|
||||
credits:
|
||||
not_logged_in: "No has iniciado sesión en Nous Portal. Inicia sesión para ver tu saldo de créditos y recargar."
|
||||
|
||||
verbose:
|
||||
not_enabled: "El comando `/verbose` no está habilitado para plataformas de mensajería.\n\nHabilítalo en `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Progreso de herramientas: **OFF** — no se muestra actividad de herramientas."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Utilisation détaillée disponible après la première réponse de l'agent)_"
|
||||
no_data: "Aucune donnée d'utilisation disponible pour cette session."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Non connecté à Nous Portal. Connecte-toi pour voir ton solde de crédits et recharger."
|
||||
|
||||
verbose:
|
||||
not_enabled: "La commande `/verbose` n'est pas activée pour les plateformes de messagerie.\n\nActivez-la dans `config.yaml` :\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Progression des outils : **OFF** — aucune activité d'outil affichée."
|
||||
|
||||
@@ -338,6 +338,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Úsáid mhionsonraithe ar fáil tar éis chéad fhreagra an ghníomhaire)_"
|
||||
no_data: "Níl aon sonraí úsáide ar fáil don seisiún seo."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Níl tú logáilte isteach i Nous Portal. Logáil isteach chun d'iarmhéid creidmheasa a fheiceáil agus breis a chur leis."
|
||||
|
||||
verbose:
|
||||
not_enabled: "Níl an t-ordú `/verbose` cumasaithe d'ardáin teachtaireachtaí.\n\nCumasaigh in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Dul chun cinn uirlise: **AS** — gan aon ghníomhaíocht uirlise á thaispeáint."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(A részletes használat az első ügynökválasz után érhető el)_"
|
||||
no_data: "Ehhez a munkamenethez nincsenek elérhető használati adatok."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Nincs bejelentkezve a Nous Portalra. Jelentkezz be a kreditegyenleg megtekintéséhez és feltöltéséhez."
|
||||
|
||||
verbose:
|
||||
not_enabled: "A `/verbose` parancs nincs engedélyezve az üzenetküldő platformokon.\n\nEngedélyezd a `config.yaml` fájlban:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Eszközfolyamat: **OFF** — nem jelenik meg eszközaktivitás."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(L'uso dettagliato sarà disponibile dopo la prima risposta dell'agente)_"
|
||||
no_data: "Nessun dato di utilizzo disponibile per questa sessione."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Non hai effettuato l'accesso a Nous Portal. Accedi per vedere il saldo dei crediti e ricaricare."
|
||||
|
||||
verbose:
|
||||
not_enabled: "Il comando `/verbose` non è abilitato per le piattaforme di messaggistica.\n\nAbilitalo in `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Progresso strumenti: **OFF** — nessuna attività degli strumenti mostrata."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(詳細な使用状況は最初のエージェント応答後に利用可能)_"
|
||||
no_data: "このセッションの使用データはありません。"
|
||||
|
||||
credits:
|
||||
not_logged_in: "Nous Portal にログインしていません。ログインすると残高の確認とチャージができます。"
|
||||
|
||||
verbose:
|
||||
not_enabled: "`/verbose` コマンドはメッセージングプラットフォームで有効になっていません。\n\n`config.yaml` で有効にしてください:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ ツール進捗: **OFF** — ツールの動作は表示されません。"
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(자세한 사용량은 첫 에이전트 응답 이후 확인할 수 있습니다)_"
|
||||
no_data: "이 세션에 사용 가능한 사용량 데이터가 없습니다."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Nous Portal에 로그인되어 있지 않습니다. 로그인하면 크레딧 잔액 확인 및 충전을 할 수 있습니다."
|
||||
|
||||
verbose:
|
||||
not_enabled: "`/verbose` 명령은 메시징 플랫폼에서 활성화되어 있지 않습니다.\n\n`config.yaml`에서 활성화하세요:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ 도구 진행 상황: **OFF** — 도구 활동이 표시되지 않습니다."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Utilização detalhada disponível após a primeira resposta do agente)_"
|
||||
no_data: "Não há dados de utilização disponíveis para esta sessão."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Você não está conectado ao Nous Portal. Faça login para ver seu saldo de créditos e recarregar."
|
||||
|
||||
verbose:
|
||||
not_enabled: "O comando `/verbose` não está ativado para plataformas de mensagens.\n\nAtiva-o em `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Progresso de ferramentas: **OFF** — não é mostrada qualquer atividade de ferramentas."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Подробное использование доступно после первого ответа агента)_"
|
||||
no_data: "Данные об использовании для этого сеанса отсутствуют."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Вы не вошли в Nous Portal. Войдите, чтобы увидеть баланс кредитов и пополнить его."
|
||||
|
||||
verbose:
|
||||
not_enabled: "Команда `/verbose` не включена для платформ обмена сообщениями.\n\nВключите в `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Прогресс инструментов: **OFF** — активность инструментов не показывается."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Ayrıntılı kullanım, ilk ajan yanıtından sonra kullanılabilir)_"
|
||||
no_data: "Bu oturum için kullanım verisi yok."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Nous Portal'a giriş yapılmadı. Bakiyenizi görmek ve yükleme yapmak için giriş yapın."
|
||||
|
||||
verbose:
|
||||
not_enabled: "`/verbose` komutu mesajlaşma platformlarında etkin değil.\n\n`config.yaml` içinde etkinleştirin:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Araç ilerlemesi: **OFF** — araç etkinliği gösterilmez."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(Детальне використання доступне після першої відповіді агента)_"
|
||||
no_data: "Дані про використання для цього сеансу відсутні."
|
||||
|
||||
credits:
|
||||
not_logged_in: "Ви не ввійшли в Nous Portal. Увійдіть, щоб переглянути баланс кредитів і поповнити його."
|
||||
|
||||
verbose:
|
||||
not_enabled: "Команду `/verbose` не ввімкнено для платформ обміну повідомленнями.\n\nУвімкніть у `config.yaml`:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ Прогрес інструментів: **OFF** — активність інструментів не показується."
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(首次代理回應後可檢視詳細使用情況)_"
|
||||
no_data: "此工作階段沒有可用的使用資料。"
|
||||
|
||||
credits:
|
||||
not_logged_in: "未登入 Nous Portal。登入後即可查看額度餘額並儲值。"
|
||||
|
||||
verbose:
|
||||
not_enabled: "`/verbose` 指令未在訊息平台上啟用。\n\n請在 `config.yaml` 中啟用:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ 工具進度:**OFF** — 不顯示任何工具活動。"
|
||||
|
||||
@@ -334,6 +334,9 @@ Future messages in this room will use that transcript until `/reset` or another
|
||||
detailed_after_first: "_(首次代理响应后可查看详细使用情况)_"
|
||||
no_data: "此会话暂无使用数据。"
|
||||
|
||||
credits:
|
||||
not_logged_in: "未登录 Nous Portal。登录后即可查看额度余额并充值。"
|
||||
|
||||
verbose:
|
||||
not_enabled: "`/verbose` 命令未在消息平台启用。\n\n请在 `config.yaml` 中启用:\n```yaml\ndisplay:\n tool_progress_command: true\n```"
|
||||
mode_off: "⚙️ 工具进度:**OFF** — 不显示任何工具活动。"
|
||||
|
||||
Reference in New Issue
Block a user