omni-ai-cli 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/assets/omni-ai-cli-logo.png" alt="Omni AI CLI" width="440">
3
+ </p>
4
+
1
5
  # Omni AI CLI
2
6
 
3
- ![npm](https://img.shields.io/badge/npm-em%20breve-1A1714)
7
+ [![npm](https://img.shields.io/npm/v/omni-ai-cli?color=C4552F)](https://www.npmjs.com/package/omni-ai-cli)
8
+ [![downloads](https://img.shields.io/npm/dm/omni-ai-cli?color=1A1714)](https://www.npmjs.com/package/omni-ai-cli)
9
+ [![provenance](https://img.shields.io/badge/npm-OIDC%20trusted%20publishing-C4552F)](https://docs.npmjs.com/trusted-publishers)
4
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-C4552F.svg)](LICENSE)
5
11
  ![Plataformas](https://img.shields.io/badge/macOS%20·%20Linux%20·%20Windows-1A1714)
6
12
  ![Idiomas](https://img.shields.io/badge/pt--BR%20·%20en%20·%20es%20·%20fr-C4552F)
@@ -49,55 +55,7 @@ Da linha digitada até a resposta salva. O chat (`chat.py`) não chama nenhuma I
49
55
  direto: ele dispara o roteador (`cli.py`) como subprocesso e lê os eventos que ele
50
56
  emite, linha a linha, para mostrar quem está atendendo e o que está fazendo.
51
57
 
52
- ```mermaid
53
- flowchart TD
54
- A["Você digita e tecla Enter<br/>(a caixa nunca trava)"] --> B{"Começa com / ?"}
55
- B -- sim --> C["Comando interno<br/>/model · /account · /key · /lang · /versions"]
56
- C --> C1["config.py grava em<br/>models.json, profiles.json ou keys/"]
57
- B -- não --> FQ["Entra na fila<br/>com a ferramenta do momento do envio"]
58
-
59
- subgraph CHAT ["chat.py — chat"]
60
- FQ --> FW{"Esperou na fila e a resposta<br/>anterior já resolve o pedido?"}
61
- FW -- "sim (classificador grátis)" --> FS["Pulado, avisa na tela"]
62
- FW -- não --> D["Monta o contexto: últimas mensagens<br/>da sessão até 48 mil caracteres + idioma"]
63
- D --> E["Worker em thread dispara o roteador<br/>em grupo de processos próprio"]
64
- E -. "eventos JSON linha a linha" .-> F["Mostra provedor, comandos e resposta"]
65
- X["Ctrl-C"] -. "esvazia a fila; SIGTERM no grupo,<br/>após 5 s, SIGKILL" .-> E
66
- end
67
-
68
- subgraph ROUTER ["cli.py — roteador"]
69
- E --> G["Carrega modelos:<br/>env > models.json > padrão"]
70
- G --> H{"Ferramenta fixada<br/>com /tool?"}
71
- H -- sim --> H1["Só a ferramenta escolhida"]
72
- H -- auto --> I{"Classifica: Groq → Cerebras →<br/>OpenRouter grátis → Grok;<br/>sem resposta, palavras-chave"}
73
- I -- simples --> J["OpenRouter: gratuitos, ou pagos<br/>se autorizados · dados pessoais mascarados"]
74
- J -- "sem chave ou falhou" --> K["Codex: assinatura,<br/>depois chave de API"]
75
- K -- falhou --> L["Antigravity: assinatura,<br/>depois chave de API"]
76
- L -- falhou --> LP["Pagos só se listados em fallback_order:<br/>Cursor, Grok, DeepSeek"]
77
- LP -- falhou --> M
78
- I -- complexa --> M["Claude: próxima conta ativa<br/>por prioridade"]
79
- M --> N{"Rate limit?"}
80
- N -- sim --> O["Conta em cooldown de 60 min"]
81
- O --> P{"Outra conta<br/>disponível?"}
82
- P -- sim --> M
83
- P -- não --> PK{"Chave de API<br/>da Anthropic?"}
84
- PK -- sim --> PK1["Claude com ANTHROPIC_API_KEY"]
85
- PK1 -- ok --> R
86
- PK1 -- falhou --> Q
87
- PK -- não --> Q["Último recurso:<br/>Codex, depois Antigravity"]
88
- end
89
-
90
- N -- não --> R["Resposta"]
91
- J -- ok --> R
92
- K -- ok --> R
93
- L -- ok --> R
94
- LP -- ok --> R
95
- Q --> R
96
- H1 --> R
97
- R --> F
98
- F --> S[("Sessão salva em disco<br/>com escrita atômica")]
99
- S --> FQ
100
- ```
58
+ ![Fluxo do pedido até a resposta](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow1.pt.png)
101
59
 
102
60
  Três detalhes que o diagrama torna explícitos:
103
61
 
@@ -142,7 +100,7 @@ curl -fsSL https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.sh |
142
100
  irm https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.ps1 | iex
143
101
  ```
144
102
 
145
- **Em breve no npm, yarn, pnpm e bun.** O pacote `omni-ai-cli` está pronto e sai assim que a publicação for liberada:
103
+ **No npm, yarn, pnpm ou bun:**
146
104
 
147
105
  ```bash
148
106
  npm install -g omni-ai-cli
@@ -151,7 +109,7 @@ pnpm add -g omni-ai-cli
151
109
  bun add -g omni-ai-cli
152
110
  ```
153
111
 
154
- Por enquanto, use o comando de macOS/Linux ou Windows acima.
112
+ Qualquer um dos comandos acima (script ou gerenciador de pacotes) instala o mesmo `ai`.
155
113
 
156
114
  Para atualizar depois: `ai --update` (ou `/update` no chat).
157
115
 
@@ -429,6 +387,7 @@ ai --deepseek "explicar CAP theorem" # força DeepSeek
429
387
  ai --install # instala as ferramentas que faltam (pergunta antes)
430
388
  ai --classify "prompt" # mostra como seria classificado
431
389
  ai --log # últimas chamadas
390
+ ai doctor # diagnóstico de instalação, contas e chaves
432
391
  ```
433
392
 
434
393
  ---
@@ -490,7 +449,7 @@ python3 -m unittest discover -s tests -v
490
449
  bash -n scripts/install.sh
491
450
  ```
492
451
 
493
- **38 testes**: persistência e retomada de sessão, limite de contexto, permissão dos
452
+ **106 testes**: persistência e retomada de sessão, limite de contexto, permissão dos
494
453
  arquivos, sessão aberta em dois terminais, leitura de eventos, cancelamento da árvore de
495
454
  processos, paralelismo real, sandbox somente leitura, mascaramento de dados pessoais,
496
455
  fallback OpenRouter → Codex, validação de modelos, armazenamento das chaves e o ciclo de vida das contas. Todos os
@@ -541,31 +500,20 @@ O `/versions` mostra o título de cada `feat`/`fix` como a "visão simples" e o
541
500
  - Um commit por mudança perceptível. Refatoração e testes vão em `refactor:`/`test:`,
542
501
  que não aparecem em `/versions`.
543
502
 
544
- ```mermaid
545
- flowchart TD
546
- A[PR aberto] --> B[CI: testes Python 3.11-3.13 em Linux e macOS<br/>+ npm pack + launcher]
547
- A --> C[PR title: Conventional Commits?]
548
- B -->|falhou| X[Merge bloqueado]
549
- C -->|fora do padrão| X
550
- B -->|ok| D[Squash merge em main / next / beta]
551
- C -->|ok| D
552
- D --> E[Release: CI roda de novo]
553
- E -->|falhou| Y[Nada é publicado]
554
- E -->|ok| F{semantic-release:<br/>há feat/fix/breaking<br/>desde a última tag?}
555
- F -->|não| Z[Sem release]
556
- F -->|sim| G[Calcula versão e cria tag vX.Y.Z]
557
- G --> H[npm publish com provenance<br/>no dist-tag da branch]
558
- G --> I[GitHub Release com notas]
559
- ```
503
+ ![Fluxo de release e publicação](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow2.pt.png)
504
+
505
+ A publicação usa **OIDC / trusted publishing**: o GitHub Actions se autentica no npm sem
506
+ token, e o pacote sai com *provenance*. Configuração única:
560
507
 
561
- Configuração única no GitHub:
508
+ 1. **npm → página do pacote → Settings → Trusted Publisher**: adicionar *GitHub Actions*
509
+ com `samuelrms/omni-ai-cli` e o workflow `release.yml`.
510
+ 2. **GitHub → Settings → Branches**: proteger `main` (e `next`/`beta`, se usar) exigindo PR
511
+ e os checks `Tests (...)`, `npm package` e `conventional`.
512
+ 3. **GitHub → Settings → General → Pull Requests**: deixar só *Allow squash merging* e
513
+ marcar *Default to pull request title*.
562
514
 
563
- 1. **Settings → Secrets and variables → Actions**: criar `NPM_TOKEN` (token *Automation*
564
- ou *Granular* com permissão de publish no npmjs.com).
565
- 2. **Settings → Branches**: proteger `main` (e `next`/`beta`, se usar) exigindo PR e os
566
- checks `Tests (...)`, `npm package` e `conventional`.
567
- 3. **Settings → General → Pull Requests**: deixar só *Allow squash merging* e marcar
568
- *Default to pull request title*.
515
+ O workflow já pede `id-token: write` e `NPM_CONFIG_PROVENANCE`; não há `NPM_TOKEN` para
516
+ guardar nem rotacionar.
569
517
 
570
518
  ---
571
519
 
@@ -595,7 +543,6 @@ Configuração única no GitHub:
595
543
  arquivos. Os agentes não editam o projeto ao mesmo tempo.
596
544
  - A classificação simples/complexa é por palavras-chave e tamanho, não por um modelo.
597
545
  - Trocar de modelo não renova a cota da conta.
598
- - O OpenRouter não tem streaming nesta versão: a resposta aparece inteira no final.
599
546
  - No Windows, o código usa os equivalentes nativos (grupo de processos e `taskkill`, `msvcrt`, junções de pasta), mas foi testado de verdade só no macOS.
600
547
 
601
548
  ---
package/docs/README.en.md CHANGED
@@ -1,6 +1,12 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/assets/omni-ai-cli-logo.png" alt="Omni AI CLI" width="440">
3
+ </p>
4
+
1
5
  # Omni AI CLI
2
6
 
3
- ![npm](https://img.shields.io/badge/npm-coming%20soon-1A1714)
7
+ [![npm](https://img.shields.io/npm/v/omni-ai-cli?color=C4552F)](https://www.npmjs.com/package/omni-ai-cli)
8
+ [![downloads](https://img.shields.io/npm/dm/omni-ai-cli?color=1A1714)](https://www.npmjs.com/package/omni-ai-cli)
9
+ [![provenance](https://img.shields.io/badge/npm-OIDC%20trusted%20publishing-C4552F)](https://docs.npmjs.com/trusted-publishers)
4
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-C4552F.svg)](../LICENSE)
5
11
  ![Platforms](https://img.shields.io/badge/macOS%20·%20Linux%20·%20Windows-1A1714)
6
12
  ![Languages](https://img.shields.io/badge/pt--BR%20·%20en%20·%20es%20·%20fr-C4552F)
@@ -49,55 +55,7 @@ From the typed line to the saved answer. The chat (`chat.py`) does not call any
49
55
  directly: it launches the router (`cli.py`) as a subprocess and reads the events it
50
56
  emits, line by line, to show who is answering and what they are doing.
51
57
 
52
- ```mermaid
53
- flowchart TD
54
- A["You type and press Enter<br/>(the box never locks)"] --> B{"Starts with / ?"}
55
- B -- yes --> C["Internal command<br/>/model · /account · /key · /lang · /versions"]
56
- C --> C1["config.py writes to<br/>models.json, profiles.json or keys/"]
57
- B -- no --> FQ["Goes into the queue<br/>with the tool selected at send time"]
58
-
59
- subgraph CHAT ["chat.py — chat"]
60
- FQ --> FW{"Waited in the queue and the previous<br/>answer already covers the request?"}
61
- FW -- "yes (free classifier)" --> FS["Skipped, shown on screen"]
62
- FW -- no --> D["Builds the context: latest messages<br/>of the session up to 48 thousand characters + language"]
63
- D --> E["Thread worker launches the router<br/>in its own process group"]
64
- E -. "JSON events line by line" .-> F["Shows provider, commands and answer"]
65
- X["Ctrl-C"] -. "empties the queue; SIGTERM to the group,<br/>after 5 s, SIGKILL" .-> E
66
- end
67
-
68
- subgraph ROUTER ["cli.py — router"]
69
- E --> G["Loads models:<br/>env > models.json > default"]
70
- G --> H{"Tool pinned<br/>with /tool?"}
71
- H -- yes --> H1["Only the chosen tool"]
72
- H -- auto --> I{"Classifies: Groq → Cerebras →<br/>free OpenRouter → Grok;<br/>no answer, keywords"}
73
- I -- simple --> J["OpenRouter: free, or paid<br/>if authorized · personal data masked"]
74
- J -- "no key or failed" --> K["Codex: subscription,<br/>then API key"]
75
- K -- failed --> L["Antigravity: subscription,<br/>then API key"]
76
- L -- failed --> LP["Paid ones only if listed in fallback_order:<br/>Cursor, Grok, DeepSeek"]
77
- LP -- failed --> M
78
- I -- complex --> M["Claude: next active account<br/>by priority"]
79
- M --> N{"Rate limit?"}
80
- N -- yes --> O["Account on 60 min cooldown"]
81
- O --> P{"Another account<br/>available?"}
82
- P -- yes --> M
83
- P -- no --> PK{"Anthropic<br/>API key?"}
84
- PK -- yes --> PK1["Claude with ANTHROPIC_API_KEY"]
85
- PK1 -- ok --> R
86
- PK1 -- failed --> Q
87
- PK -- no --> Q["Last resort:<br/>Codex, then Antigravity"]
88
- end
89
-
90
- N -- no --> R["Answer"]
91
- J -- ok --> R
92
- K -- ok --> R
93
- L -- ok --> R
94
- LP -- ok --> R
95
- Q --> R
96
- H1 --> R
97
- R --> F
98
- F --> S[("Session saved to disk<br/>with atomic write")]
99
- S --> FQ
100
- ```
58
+ ![Request-to-answer flow](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow1.en.png)
101
59
 
102
60
  Three details the diagram makes explicit:
103
61
 
@@ -142,7 +100,7 @@ curl -fsSL https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.sh |
142
100
  irm https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.ps1 | iex
143
101
  ```
144
102
 
145
- **Coming soon to npm, yarn, pnpm and bun.** The `omni-ai-cli` package is ready and ships as soon as publishing is unlocked:
103
+ **On npm, yarn, pnpm or bun:**
146
104
 
147
105
  ```bash
148
106
  npm install -g omni-ai-cli
@@ -151,7 +109,7 @@ pnpm add -g omni-ai-cli
151
109
  bun add -g omni-ai-cli
152
110
  ```
153
111
 
154
- For now, use the macOS/Linux or Windows command above.
112
+ Any of the commands above (script or package manager) installs the same `ai`.
155
113
 
156
114
  To update later: `ai --update` (or `/update` in the chat).
157
115
 
@@ -428,6 +386,7 @@ ai --deepseek "explicar CAP theorem" # forces DeepSeek
428
386
  ai --install # installs the missing tools (asks first)
429
387
  ai --classify "prompt" # shows how it would be classified
430
388
  ai --log # latest calls
389
+ ai doctor # diagnose install, accounts and keys
431
390
  ```
432
391
 
433
392
  ---
@@ -489,7 +448,7 @@ python3 -m unittest discover -s tests -v
489
448
  bash -n scripts/install.sh
490
449
  ```
491
450
 
492
- **38 tests**: session persistence and resume, context limit, file permissions, session
451
+ **106 tests**: session persistence and resume, context limit, file permissions, session
493
452
  open in two terminals, event reading, process tree cancellation, real parallelism,
494
453
  read-only sandbox, personal data masking, OpenRouter → Codex fallback, model
495
454
  validation, key storage and the account lifecycle. All the
@@ -540,31 +499,20 @@ titles that don't follow the convention.
540
499
  - One commit per noticeable change. Refactoring and tests go in `refactor:`/`test:`,
541
500
  which don't show up in `/versions`.
542
501
 
543
- ```mermaid
544
- flowchart TD
545
- A[PR opened] --> B[CI: Python 3.11-3.13 tests on Linux and macOS<br/>+ npm pack + launcher]
546
- A --> C[PR title: Conventional Commits?]
547
- B -->|failed| X[Merge blocked]
548
- C -->|invalid| X
549
- B -->|ok| D[Squash merge into main / next / beta]
550
- C -->|ok| D
551
- D --> E[Release: CI runs again]
552
- E -->|failed| Y[Nothing is published]
553
- E -->|ok| F{semantic-release:<br/>any feat/fix/breaking<br/>since last tag?}
554
- F -->|no| Z[No release]
555
- F -->|yes| G[Compute version and tag vX.Y.Z]
556
- G --> H[npm publish with provenance<br/>on the branch dist-tag]
557
- G --> I[GitHub Release with notes]
558
- ```
502
+ ![Release and publish flow](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow2.en.png)
503
+
504
+ Publishing uses **OIDC / trusted publishing**: GitHub Actions authenticates to npm with no
505
+ token and the package ships with *provenance*. One-time setup:
559
506
 
560
- One-time GitHub setup:
507
+ 1. **npm → package page → Settings → Trusted Publisher**: add *GitHub Actions* with
508
+ `samuelrms/omni-ai-cli` and the `release.yml` workflow.
509
+ 2. **GitHub → Settings → Branches**: protect `main` (and `next`/`beta` if used), requiring a
510
+ PR and the `Tests (...)`, `npm package` and `conventional` checks.
511
+ 3. **GitHub → Settings → General → Pull Requests**: allow only *squash merging* and tick
512
+ *Default to pull request title*.
561
513
 
562
- 1. **Settings → Secrets and variables → Actions**: add `NPM_TOKEN` (an *Automation* or
563
- *Granular* npmjs.com token with publish permission).
564
- 2. **Settings → Branches**: protect `main` (and `next`/`beta` if used), requiring a PR and
565
- the `Tests (...)`, `npm package` and `conventional` checks.
566
- 3. **Settings → General → Pull Requests**: allow only *squash merging* and tick *Default
567
- to pull request title*.
514
+ The workflow already requests `id-token: write` and `NPM_CONFIG_PROVENANCE`; there is no
515
+ `NPM_TOKEN` to store or rotate.
568
516
 
569
517
  ---
570
518
 
@@ -594,7 +542,6 @@ One-time GitHub setup:
594
542
  access the files. The agents do not edit the project at the same time.
595
543
  - The simple/complex classification is by keywords and length, not by a model.
596
544
  - Switching models does not renew the account's quota.
597
- - OpenRouter has no streaming in this version: the answer appears whole at the end.
598
545
  - On Windows the code uses the native equivalents (process groups and `taskkill`, `msvcrt`, folder junctions), but it has only been tested for real on macOS.
599
546
 
600
547
  ---
package/docs/README.es.md CHANGED
@@ -1,6 +1,12 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/assets/omni-ai-cli-logo.png" alt="Omni AI CLI" width="440">
3
+ </p>
4
+
1
5
  # Omni AI CLI
2
6
 
3
- ![npm](https://img.shields.io/badge/npm-pr%C3%B3ximamente-1A1714)
7
+ [![npm](https://img.shields.io/npm/v/omni-ai-cli?color=C4552F)](https://www.npmjs.com/package/omni-ai-cli)
8
+ [![downloads](https://img.shields.io/npm/dm/omni-ai-cli?color=1A1714)](https://www.npmjs.com/package/omni-ai-cli)
9
+ [![provenance](https://img.shields.io/badge/npm-OIDC%20trusted%20publishing-C4552F)](https://docs.npmjs.com/trusted-publishers)
4
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-C4552F.svg)](../LICENSE)
5
11
  ![Plataformas](https://img.shields.io/badge/macOS%20·%20Linux%20·%20Windows-1A1714)
6
12
  ![Idiomas](https://img.shields.io/badge/pt--BR%20·%20en%20·%20es%20·%20fr-C4552F)
@@ -50,55 +56,7 @@ ninguna IA directamente: lanza el enrutador (`cli.py`) como subproceso y lee los
50
56
  eventos que este emite, línea por línea, para mostrar quién está atendiendo y qué está
51
57
  haciendo.
52
58
 
53
- ```mermaid
54
- flowchart TD
55
- A["Escribes y presionas Enter<br/>(la caja nunca se bloquea)"] --> B{"¿Empieza con / ?"}
56
- B -- sí --> C["Comando interno<br/>/model · /account · /key · /lang · /versions"]
57
- C --> C1["config.py guarda en<br/>models.json, profiles.json o keys/"]
58
- B -- no --> FQ["Entra en la cola<br/>con la herramienta del momento del envío"]
59
-
60
- subgraph CHAT ["chat.py — chat"]
61
- FQ --> FW{"¿Esperó en la cola y la respuesta<br/>anterior ya resuelve el pedido?"}
62
- FW -- "sí (clasificador gratis)" --> FS["Omitido, avisa en pantalla"]
63
- FW -- no --> D["Arma el contexto: últimos mensajes<br/>de la sesión hasta 48 mil caracteres + idioma"]
64
- D --> E["Worker en hilo lanza el enrutador<br/>en un grupo de procesos propio"]
65
- E -. "eventos JSON línea por línea" .-> F["Muestra proveedor, comandos y respuesta"]
66
- X["Ctrl-C"] -. "vacía la cola; SIGTERM al grupo,<br/>tras 5 s, SIGKILL" .-> E
67
- end
68
-
69
- subgraph ROUTER ["cli.py — enrutador"]
70
- E --> G["Carga modelos:<br/>env > models.json > predeterminado"]
71
- G --> H{"¿Herramienta fijada<br/>con /tool?"}
72
- H -- sí --> H1["Solo la herramienta elegida"]
73
- H -- auto --> I{"Clasifica: Groq → Cerebras →<br/>OpenRouter gratis → Grok;<br/>sin respuesta, palabras clave"}
74
- I -- simple --> J["OpenRouter: gratuitos, o pagos<br/>si se autorizan · datos personales enmascarados"]
75
- J -- "sin clave o falló" --> K["Codex: suscripción,<br/>luego clave de API"]
76
- K -- falló --> L["Antigravity: suscripción,<br/>luego clave de API"]
77
- L -- falló --> LP["De pago solo si están en fallback_order:<br/>Cursor, Grok, DeepSeek"]
78
- LP -- falló --> M
79
- I -- compleja --> M["Claude: siguiente cuenta activa<br/>por prioridad"]
80
- M --> N{"¿Rate limit?"}
81
- N -- sí --> O["Cuenta en cooldown de 60 min"]
82
- O --> P{"¿Otra cuenta<br/>disponible?"}
83
- P -- sí --> M
84
- P -- no --> PK{"¿Clave de API<br/>de Anthropic?"}
85
- PK -- sí --> PK1["Claude con ANTHROPIC_API_KEY"]
86
- PK1 -- ok --> R
87
- PK1 -- falló --> Q
88
- PK -- no --> Q["Último recurso:<br/>Codex, luego Antigravity"]
89
- end
90
-
91
- N -- no --> R["Respuesta"]
92
- J -- ok --> R
93
- K -- ok --> R
94
- L -- ok --> R
95
- LP -- ok --> R
96
- Q --> R
97
- H1 --> R
98
- R --> F
99
- F --> S[("Sesión guardada en disco<br/>con escritura atómica")]
100
- S --> FQ
101
- ```
59
+ ![Flujo del pedido a la respuesta](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow1.es.png)
102
60
 
103
61
  Tres detalles que el diagrama deja explícitos:
104
62
 
@@ -143,7 +101,7 @@ curl -fsSL https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.sh |
143
101
  irm https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.ps1 | iex
144
102
  ```
145
103
 
146
- **Pronto en npm, yarn, pnpm y bun.** El paquete `omni-ai-cli` está listo y sale en cuanto se libere la publicación:
104
+ **En npm, yarn, pnpm o bun:**
147
105
 
148
106
  ```bash
149
107
  npm install -g omni-ai-cli
@@ -152,7 +110,7 @@ pnpm add -g omni-ai-cli
152
110
  bun add -g omni-ai-cli
153
111
  ```
154
112
 
155
- Por ahora, usa el comando de macOS/Linux o Windows de arriba.
113
+ Cualquiera de los comandos de arriba (script o gestor de paquetes) instala el mismo `ai`.
156
114
 
157
115
  Para actualizar después: `ai --update` (o `/update` en el chat).
158
116
 
@@ -429,6 +387,7 @@ ai --deepseek "explicar CAP theorem" # fuerza DeepSeek
429
387
  ai --install # instala las herramientas que faltan (pregunta antes)
430
388
  ai --classify "prompt" # muestra cómo se clasificaría
431
389
  ai --log # últimas llamadas
390
+ ai doctor # diagnóstico de instalación, cuentas y claves
432
391
  ```
433
392
 
434
393
  ---
@@ -490,7 +449,7 @@ python3 -m unittest discover -s tests -v
490
449
  bash -n scripts/install.sh
491
450
  ```
492
451
 
493
- **38 pruebas**: persistencia y reanudación de sesión, límite de contexto, permisos de
452
+ **106 pruebas**: persistencia y reanudación de sesión, límite de contexto, permisos de
494
453
  los archivos, sesión abierta en dos terminales, lectura de eventos, cancelación del árbol
495
454
  de procesos, paralelismo real, sandbox de solo lectura, enmascaramiento de datos
496
455
  personales, fallback OpenRouter → Codex, validación de modelos, almacenamiento de las
@@ -542,32 +501,21 @@ títulos fuera del estándar.
542
501
  - Un commit por cambio perceptible. Refactorizaciones y tests van en `refactor:`/`test:`,
543
502
  que no aparecen en `/versions`.
544
503
 
545
- ```mermaid
546
- flowchart TD
547
- A[PR abierto] --> B[CI: tests Python 3.11-3.13 en Linux y macOS<br/>+ npm pack + launcher]
548
- A --> C[PR title: ¿Conventional Commits?]
549
- B -->|falló| X[Merge bloqueado]
550
- C -->|inválido| X
551
- B -->|ok| D[Squash merge en main / next / beta]
552
- C -->|ok| D
553
- D --> E[Release: CI corre de nuevo]
554
- E -->|falló| Y[No se publica nada]
555
- E -->|ok| F{semantic-release:<br/>¿hay feat/fix/breaking<br/>desde la última tag?}
556
- F -->|no| Z[Sin release]
557
- F -->|sí| G[Calcula versión y crea tag vX.Y.Z]
558
- G --> H[npm publish con provenance<br/>en el dist-tag de la rama]
559
- G --> I[GitHub Release con notas]
560
- ```
504
+ ![Flujo de release y publicación](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow2.es.png)
561
505
 
562
- Configuración única en GitHub:
506
+ La publicación usa **OIDC / trusted publishing**: GitHub Actions se autentica en npm sin
507
+ token y el paquete sale con *provenance*. Configuración única:
563
508
 
564
- 1. **Settings → Secrets and variables → Actions**: crear `NPM_TOKEN` (token *Automation*
565
- o *Granular* de npmjs.com con permiso de publish).
566
- 2. **Settings → Branches**: proteger `main` (y `next`/`beta` si se usan) exigiendo PR y
567
- los checks `Tests (...)`, `npm package` y `conventional`.
568
- 3. **Settings → General → Pull Requests**: permitir solo *squash merging* y marcar
509
+ 1. **npm → página del paquete → Settings → Trusted Publisher**: añadir *GitHub Actions* con
510
+ `samuelrms/omni-ai-cli` y el workflow `release.yml`.
511
+ 2. **GitHub → Settings → Branches**: proteger `main` (y `next`/`beta` si se usan) exigiendo
512
+ PR y los checks `Tests (...)`, `npm package` y `conventional`.
513
+ 3. **GitHub → Settings → General → Pull Requests**: permitir solo *squash merging* y marcar
569
514
  *Default to pull request title*.
570
515
 
516
+ El workflow ya pide `id-token: write` y `NPM_CONFIG_PROVENANCE`; no hay `NPM_TOKEN` que
517
+ guardar ni rotar.
518
+
571
519
  ---
572
520
 
573
521
  ## Troubleshooting
@@ -597,7 +545,6 @@ Configuración única en GitHub:
597
545
  a los archivos. Los agentes no editan el proyecto al mismo tiempo.
598
546
  - La clasificación simple/compleja se hace por palabras clave y tamaño, no con un modelo.
599
547
  - Cambiar de modelo no renueva la cuota de la cuenta.
600
- - OpenRouter no tiene streaming en esta versión: la respuesta aparece completa al final.
601
548
  - En Windows el código usa los equivalentes nativos (grupos de procesos y `taskkill`, `msvcrt`, uniones de carpeta), pero solo se probó de verdad en macOS.
602
549
 
603
550
  ---
package/docs/README.fr.md CHANGED
@@ -1,6 +1,12 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/assets/omni-ai-cli-logo.png" alt="Omni AI CLI" width="440">
3
+ </p>
4
+
1
5
  # Omni AI CLI
2
6
 
3
- ![npm](https://img.shields.io/badge/npm-bient%C3%B4t-1A1714)
7
+ [![npm](https://img.shields.io/npm/v/omni-ai-cli?color=C4552F)](https://www.npmjs.com/package/omni-ai-cli)
8
+ [![downloads](https://img.shields.io/npm/dm/omni-ai-cli?color=1A1714)](https://www.npmjs.com/package/omni-ai-cli)
9
+ [![provenance](https://img.shields.io/badge/npm-OIDC%20trusted%20publishing-C4552F)](https://docs.npmjs.com/trusted-publishers)
4
10
  [![License: MIT](https://img.shields.io/badge/license-MIT-C4552F.svg)](../LICENSE)
5
11
  ![Plateformes](https://img.shields.io/badge/macOS%20·%20Linux%20·%20Windows-1A1714)
6
12
  ![Langues](https://img.shields.io/badge/pt--BR%20·%20en%20·%20es%20·%20fr-C4552F)
@@ -50,55 +56,7 @@ De la ligne saisie jusqu'à la réponse sauvegardée. Le chat (`chat.py`) n'appe
50
56
  aucune IA directement : il lance le routeur (`cli.py`) comme sous-processus et lit
51
57
  les événements qu'il émet, ligne par ligne, pour afficher qui répond et ce qu'il fait.
52
58
 
53
- ```mermaid
54
- flowchart TD
55
- A["Vous saisissez et appuyez sur Entrée<br/>(la zone ne se bloque jamais)"] --> B{"Commence par / ?"}
56
- B -- oui --> C["Commande interne<br/>/model · /account · /key · /lang · /versions"]
57
- C --> C1["config.py écrit dans<br/>models.json, profiles.json ou keys/"]
58
- B -- non --> FQ["Entre dans la file<br/>avec l'outil choisi au moment de l'envoi"]
59
-
60
- subgraph CHAT ["chat.py — chat"]
61
- FQ --> FW{"A attendu dans la file et la réponse<br/>précédente règle déjà la demande ?"}
62
- FW -- "oui (classifieur gratuit)" --> FS["Ignorée, signalé à l'écran"]
63
- FW -- non --> D["Construit le contexte : derniers messages<br/>de la session jusqu'à 48 mille caractères + langue"]
64
- D --> E["Worker en thread lance le routeur<br/>dans son propre groupe de processus"]
65
- E -. "événements JSON ligne par ligne" .-> F["Affiche le fournisseur, les commandes et la réponse"]
66
- X["Ctrl-C"] -. "vide la file ; SIGTERM sur le groupe,<br/>après 5 s, SIGKILL" .-> E
67
- end
68
-
69
- subgraph ROUTER ["cli.py — routeur"]
70
- E --> G["Charge les modèles :<br/>env > models.json > défaut"]
71
- G --> H{"Outil fixé<br/>avec /tool ?"}
72
- H -- oui --> H1["Seulement l'outil choisi"]
73
- H -- auto --> I{"Classe : Groq → Cerebras →<br/>OpenRouter gratuit → Grok ;<br/>sans réponse, mots-clés"}
74
- I -- simple --> J["OpenRouter : gratuits, ou payants<br/>si autorisés · données personnelles masquées"]
75
- J -- "sans clé ou échec" --> K["Codex : abonnement,<br/>puis clé d'API"]
76
- K -- échec --> L["Antigravity : abonnement,<br/>puis clé d'API"]
77
- L -- échec --> LP["Payants seulement si listés dans fallback_order :<br/>Cursor, Grok, DeepSeek"]
78
- LP -- échec --> M
79
- I -- complexe --> M["Claude : prochain compte actif<br/>par priorité"]
80
- M --> N{"Rate limit ?"}
81
- N -- oui --> O["Compte en cooldown de 60 min"]
82
- O --> P{"Un autre compte<br/>disponible ?"}
83
- P -- oui --> M
84
- P -- non --> PK{"Clé d'API<br/>Anthropic ?"}
85
- PK -- oui --> PK1["Claude avec ANTHROPIC_API_KEY"]
86
- PK1 -- ok --> R
87
- PK1 -- échec --> Q
88
- PK -- non --> Q["Dernier recours :<br/>Codex, puis Antigravity"]
89
- end
90
-
91
- N -- non --> R["Réponse"]
92
- J -- ok --> R
93
- K -- ok --> R
94
- L -- ok --> R
95
- LP -- ok --> R
96
- Q --> R
97
- H1 --> R
98
- R --> F
99
- F --> S[("Session sauvegardée sur disque<br/>avec écriture atomique")]
100
- S --> FQ
101
- ```
59
+ ![Flux de la demande à la réponse](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow1.fr.png)
102
60
 
103
61
  Trois détails que le diagramme rend explicites :
104
62
 
@@ -145,7 +103,7 @@ curl -fsSL https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.sh |
145
103
  irm https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/get.ps1 | iex
146
104
  ```
147
105
 
148
- **Bientôt sur npm, yarn, pnpm et bun.** Le paquet `omni-ai-cli` est prêt et sera publié dès que possible :
106
+ **Sur npm, yarn, pnpm ou bun :**
149
107
 
150
108
  ```bash
151
109
  npm install -g omni-ai-cli
@@ -154,7 +112,7 @@ pnpm add -g omni-ai-cli
154
112
  bun add -g omni-ai-cli
155
113
  ```
156
114
 
157
- En attendant, utilisez la commande macOS/Linux ou Windows ci-dessus.
115
+ N'importe laquelle des commandes ci-dessus (script ou gestionnaire de paquets) installe le même `ai`.
158
116
 
159
117
  Pour mettre à jour ensuite : `ai --update` (ou `/update` dans le chat).
160
118
 
@@ -438,6 +396,7 @@ ai --deepseek "explicar CAP theorem" # force DeepSeek
438
396
  ai --install # installe les outils manquants (demande avant)
439
397
  ai --classify "prompt" # montre la classification
440
398
  ai --log # derniers appels
399
+ ai doctor # diagnostic installation, comptes et clés
441
400
  ```
442
401
 
443
402
  ---
@@ -499,7 +458,7 @@ python3 -m unittest discover -s tests -v
499
458
  bash -n scripts/install.sh
500
459
  ```
501
460
 
502
- **38 tests** : persistance et reprise de session, limite de contexte, permissions des
461
+ **106 tests** : persistance et reprise de session, limite de contexte, permissions des
503
462
  fichiers, session ouverte dans deux terminaux, lecture des événements, annulation de
504
463
  l'arbre de processus, parallélisme réel, sandbox en lecture seule, masquage des données
505
464
  personnelles, fallback OpenRouter → Codex, validation des modèles, stockage des clés et
@@ -551,31 +510,20 @@ bloque les titres hors convention.
551
510
  - Un commit par changement perceptible. Refactorisations et tests vont dans
552
511
  `refactor:`/`test:`, qui n'apparaissent pas dans `/versions`.
553
512
 
554
- ```mermaid
555
- flowchart TD
556
- A[PR ouverte] --> B[CI : tests Python 3.11-3.13 sur Linux et macOS<br/>+ npm pack + launcher]
557
- A --> C[PR title : Conventional Commits ?]
558
- B -->|échec| X[Merge bloqué]
559
- C -->|invalide| X
560
- B -->|ok| D[Squash merge dans main / next / beta]
561
- C -->|ok| D
562
- D --> E[Release : la CI tourne à nouveau]
563
- E -->|échec| Y[Rien n'est publié]
564
- E -->|ok| F{semantic-release :<br/>feat/fix/breaking<br/>depuis le dernier tag ?}
565
- F -->|non| Z[Pas de release]
566
- F -->|oui| G[Calcule la version et crée le tag vX.Y.Z]
567
- G --> H[npm publish avec provenance<br/>sur le dist-tag de la branche]
568
- G --> I[GitHub Release avec notes]
569
- ```
513
+ ![Flux de release et publication](https://raw.githubusercontent.com/samuelrms/omni-ai-cli/main/docs/diagrams/flow2.fr.png)
514
+
515
+ La publication utilise **OIDC / trusted publishing** : GitHub Actions s'authentifie auprès
516
+ de npm sans jeton et le paquet est publié avec *provenance*. Configuration unique :
570
517
 
571
- Configuration unique sur GitHub :
518
+ 1. **npm → page du paquet → Settings → Trusted Publisher** : ajouter *GitHub Actions* avec
519
+ `samuelrms/omni-ai-cli` et le workflow `release.yml`.
520
+ 2. **GitHub → Settings → Branches** : protéger `main` (et `next`/`beta` si utilisées) en
521
+ exigeant une PR et les checks `Tests (...)`, `npm package` et `conventional`.
522
+ 3. **GitHub → Settings → General → Pull Requests** : n'autoriser que le *squash merging* et
523
+ cocher *Default to pull request title*.
572
524
 
573
- 1. **Settings → Secrets and variables → Actions** : créer `NPM_TOKEN` (jeton *Automation*
574
- ou *Granular* npmjs.com avec droit de publish).
575
- 2. **Settings → Branches** : protéger `main` (et `next`/`beta` si utilisées) en exigeant
576
- une PR et les checks `Tests (...)`, `npm package` et `conventional`.
577
- 3. **Settings → General → Pull Requests** : n'autoriser que le *squash merging* et cocher
578
- *Default to pull request title*.
525
+ Le workflow demande déjà `id-token: write` et `NPM_CONFIG_PROVENANCE` ; il n'y a pas de
526
+ `NPM_TOKEN` à stocker ni à renouveler.
579
527
 
580
528
  ---
581
529
 
@@ -607,8 +555,6 @@ Configuration unique sur GitHub :
607
555
  - La classification simple/complexe repose sur des mots-clés et la taille, pas sur un
608
556
  modèle.
609
557
  - Changer de modèle ne renouvelle pas le quota du compte.
610
- - OpenRouter n'a pas de streaming dans cette version : la réponse apparaît en entier à
611
- la fin.
612
558
  - Sous Windows le code utilise les équivalents natifs (groupes de processus et `taskkill`, `msvcrt`, jonctions de dossier), mais il n'a été testé pour de vrai que sous macOS.
613
559
 
614
560
  ---
@@ -0,0 +1,47 @@
1
+ flowchart TD
2
+ A["You type and press Enter<br/>(the box never locks)"] --> B{"Starts with / ?"}
3
+ B -- yes --> C["Internal command<br/>/model · /account · /key · /lang · /versions"]
4
+ C --> C1["config.py writes to<br/>models.json, profiles.json or keys/"]
5
+ B -- no --> FQ["Goes into the queue<br/>with the tool selected at send time"]
6
+
7
+ subgraph CHAT ["chat.py — chat"]
8
+ FQ --> FW{"Waited in the queue and the previous<br/>answer already covers the request?"}
9
+ FW -- "yes (free classifier)" --> FS["Skipped, shown on screen"]
10
+ FW -- no --> D["Builds the context: latest messages<br/>of the session up to 48 thousand characters + language"]
11
+ D --> E["Thread worker launches the router<br/>in its own process group"]
12
+ E -. "JSON events line by line" .-> F["Shows provider, commands and answer"]
13
+ X["Ctrl-C"] -. "empties the queue; SIGTERM to the group,<br/>after 5 s, SIGKILL" .-> E
14
+ end
15
+
16
+ subgraph ROUTER ["cli.py — router"]
17
+ E --> G["Loads models:<br/>env > models.json > default"]
18
+ G --> H{"Tool pinned<br/>with /tool?"}
19
+ H -- yes --> H1["Only the chosen tool"]
20
+ H -- auto --> I{"Classifies: Groq → Cerebras →<br/>free OpenRouter → Grok;<br/>no answer, keywords"}
21
+ I -- simple --> J["OpenRouter: free, or paid<br/>if authorized · personal data masked"]
22
+ J -- "no key or failed" --> K["Codex: subscription,<br/>then API key"]
23
+ K -- failed --> L["Antigravity: subscription,<br/>then API key"]
24
+ L -- failed --> LP["Paid ones only if listed in fallback_order:<br/>Cursor, Grok, DeepSeek"]
25
+ LP -- failed --> M
26
+ I -- complex --> M["Claude: next active account<br/>by priority"]
27
+ M --> N{"Rate limit?"}
28
+ N -- yes --> O["Account on 60 min cooldown"]
29
+ O --> P{"Another account<br/>available?"}
30
+ P -- yes --> M
31
+ P -- no --> PK{"Anthropic<br/>API key?"}
32
+ PK -- yes --> PK1["Claude with ANTHROPIC_API_KEY"]
33
+ PK1 -- ok --> R
34
+ PK1 -- failed --> Q
35
+ PK -- no --> Q["Last resort:<br/>Codex, then Antigravity"]
36
+ end
37
+
38
+ N -- no --> R["Answer"]
39
+ J -- ok --> R
40
+ K -- ok --> R
41
+ L -- ok --> R
42
+ LP -- ok --> R
43
+ Q --> R
44
+ H1 --> R
45
+ R --> F
46
+ F --> S[("Session saved to disk<br/>with atomic write")]
47
+ S --> FQ
@@ -0,0 +1,47 @@
1
+ flowchart TD
2
+ A["Escribes y presionas Enter<br/>(la caja nunca se bloquea)"] --> B{"¿Empieza con / ?"}
3
+ B -- sí --> C["Comando interno<br/>/model · /account · /key · /lang · /versions"]
4
+ C --> C1["config.py guarda en<br/>models.json, profiles.json o keys/"]
5
+ B -- no --> FQ["Entra en la cola<br/>con la herramienta del momento del envío"]
6
+
7
+ subgraph CHAT ["chat.py — chat"]
8
+ FQ --> FW{"¿Esperó en la cola y la respuesta<br/>anterior ya resuelve el pedido?"}
9
+ FW -- "sí (clasificador gratis)" --> FS["Omitido, avisa en pantalla"]
10
+ FW -- no --> D["Arma el contexto: últimos mensajes<br/>de la sesión hasta 48 mil caracteres + idioma"]
11
+ D --> E["Worker en hilo lanza el enrutador<br/>en un grupo de procesos propio"]
12
+ E -. "eventos JSON línea por línea" .-> F["Muestra proveedor, comandos y respuesta"]
13
+ X["Ctrl-C"] -. "vacía la cola; SIGTERM al grupo,<br/>tras 5 s, SIGKILL" .-> E
14
+ end
15
+
16
+ subgraph ROUTER ["cli.py — enrutador"]
17
+ E --> G["Carga modelos:<br/>env > models.json > predeterminado"]
18
+ G --> H{"¿Herramienta fijada<br/>con /tool?"}
19
+ H -- sí --> H1["Solo la herramienta elegida"]
20
+ H -- auto --> I{"Clasifica: Groq → Cerebras →<br/>OpenRouter gratis → Grok;<br/>sin respuesta, palabras clave"}
21
+ I -- simple --> J["OpenRouter: gratuitos, o pagos<br/>si se autorizan · datos personales enmascarados"]
22
+ J -- "sin clave o falló" --> K["Codex: suscripción,<br/>luego clave de API"]
23
+ K -- falló --> L["Antigravity: suscripción,<br/>luego clave de API"]
24
+ L -- falló --> LP["De pago solo si están en fallback_order:<br/>Cursor, Grok, DeepSeek"]
25
+ LP -- falló --> M
26
+ I -- compleja --> M["Claude: siguiente cuenta activa<br/>por prioridad"]
27
+ M --> N{"¿Rate limit?"}
28
+ N -- sí --> O["Cuenta en cooldown de 60 min"]
29
+ O --> P{"¿Otra cuenta<br/>disponible?"}
30
+ P -- sí --> M
31
+ P -- no --> PK{"¿Clave de API<br/>de Anthropic?"}
32
+ PK -- sí --> PK1["Claude con ANTHROPIC_API_KEY"]
33
+ PK1 -- ok --> R
34
+ PK1 -- falló --> Q
35
+ PK -- no --> Q["Último recurso:<br/>Codex, luego Antigravity"]
36
+ end
37
+
38
+ N -- no --> R["Respuesta"]
39
+ J -- ok --> R
40
+ K -- ok --> R
41
+ L -- ok --> R
42
+ LP -- ok --> R
43
+ Q --> R
44
+ H1 --> R
45
+ R --> F
46
+ F --> S[("Sesión guardada en disco<br/>con escritura atómica")]
47
+ S --> FQ
@@ -0,0 +1,47 @@
1
+ flowchart TD
2
+ A["Vous saisissez et appuyez sur Entrée<br/>(la zone ne se bloque jamais)"] --> B{"Commence par / ?"}
3
+ B -- oui --> C["Commande interne<br/>/model · /account · /key · /lang · /versions"]
4
+ C --> C1["config.py écrit dans<br/>models.json, profiles.json ou keys/"]
5
+ B -- non --> FQ["Entre dans la file<br/>avec l'outil choisi au moment de l'envoi"]
6
+
7
+ subgraph CHAT ["chat.py — chat"]
8
+ FQ --> FW{"A attendu dans la file et la réponse<br/>précédente règle déjà la demande ?"}
9
+ FW -- "oui (classifieur gratuit)" --> FS["Ignorée, signalé à l'écran"]
10
+ FW -- non --> D["Construit le contexte : derniers messages<br/>de la session jusqu'à 48 mille caractères + langue"]
11
+ D --> E["Worker en thread lance le routeur<br/>dans son propre groupe de processus"]
12
+ E -. "événements JSON ligne par ligne" .-> F["Affiche le fournisseur, les commandes et la réponse"]
13
+ X["Ctrl-C"] -. "vide la file ; SIGTERM sur le groupe,<br/>après 5 s, SIGKILL" .-> E
14
+ end
15
+
16
+ subgraph ROUTER ["cli.py — routeur"]
17
+ E --> G["Charge les modèles :<br/>env > models.json > défaut"]
18
+ G --> H{"Outil fixé<br/>avec /tool ?"}
19
+ H -- oui --> H1["Seulement l'outil choisi"]
20
+ H -- auto --> I{"Classe : Groq → Cerebras →<br/>OpenRouter gratuit → Grok ;<br/>sans réponse, mots-clés"}
21
+ I -- simple --> J["OpenRouter : gratuits, ou payants<br/>si autorisés · données personnelles masquées"]
22
+ J -- "sans clé ou échec" --> K["Codex : abonnement,<br/>puis clé d'API"]
23
+ K -- échec --> L["Antigravity : abonnement,<br/>puis clé d'API"]
24
+ L -- échec --> LP["Payants seulement si listés dans fallback_order :<br/>Cursor, Grok, DeepSeek"]
25
+ LP -- échec --> M
26
+ I -- complexe --> M["Claude : prochain compte actif<br/>par priorité"]
27
+ M --> N{"Rate limit ?"}
28
+ N -- oui --> O["Compte en cooldown de 60 min"]
29
+ O --> P{"Un autre compte<br/>disponible ?"}
30
+ P -- oui --> M
31
+ P -- non --> PK{"Clé d'API<br/>Anthropic ?"}
32
+ PK -- oui --> PK1["Claude avec ANTHROPIC_API_KEY"]
33
+ PK1 -- ok --> R
34
+ PK1 -- échec --> Q
35
+ PK -- non --> Q["Dernier recours :<br/>Codex, puis Antigravity"]
36
+ end
37
+
38
+ N -- non --> R["Réponse"]
39
+ J -- ok --> R
40
+ K -- ok --> R
41
+ L -- ok --> R
42
+ LP -- ok --> R
43
+ Q --> R
44
+ H1 --> R
45
+ R --> F
46
+ F --> S[("Session sauvegardée sur disque<br/>avec écriture atomique")]
47
+ S --> FQ
@@ -0,0 +1,47 @@
1
+ flowchart TD
2
+ A["Você digita e tecla Enter<br/>(a caixa nunca trava)"] --> B{"Começa com / ?"}
3
+ B -- sim --> C["Comando interno<br/>/model · /account · /key · /lang · /versions"]
4
+ C --> C1["config.py grava em<br/>models.json, profiles.json ou keys/"]
5
+ B -- não --> FQ["Entra na fila<br/>com a ferramenta do momento do envio"]
6
+
7
+ subgraph CHAT ["chat.py — chat"]
8
+ FQ --> FW{"Esperou na fila e a resposta<br/>anterior já resolve o pedido?"}
9
+ FW -- "sim (classificador grátis)" --> FS["Pulado, avisa na tela"]
10
+ FW -- não --> D["Monta o contexto: últimas mensagens<br/>da sessão até 48 mil caracteres + idioma"]
11
+ D --> E["Worker em thread dispara o roteador<br/>em grupo de processos próprio"]
12
+ E -. "eventos JSON linha a linha" .-> F["Mostra provedor, comandos e resposta"]
13
+ X["Ctrl-C"] -. "esvazia a fila; SIGTERM no grupo,<br/>após 5 s, SIGKILL" .-> E
14
+ end
15
+
16
+ subgraph ROUTER ["cli.py — roteador"]
17
+ E --> G["Carrega modelos:<br/>env > models.json > padrão"]
18
+ G --> H{"Ferramenta fixada<br/>com /tool?"}
19
+ H -- sim --> H1["Só a ferramenta escolhida"]
20
+ H -- auto --> I{"Classifica: Groq → Cerebras →<br/>OpenRouter grátis → Grok;<br/>sem resposta, palavras-chave"}
21
+ I -- simples --> J["OpenRouter: gratuitos, ou pagos<br/>se autorizados · dados pessoais mascarados"]
22
+ J -- "sem chave ou falhou" --> K["Codex: assinatura,<br/>depois chave de API"]
23
+ K -- falhou --> L["Antigravity: assinatura,<br/>depois chave de API"]
24
+ L -- falhou --> LP["Pagos só se listados em fallback_order:<br/>Cursor, Grok, DeepSeek"]
25
+ LP -- falhou --> M
26
+ I -- complexa --> M["Claude: próxima conta ativa<br/>por prioridade"]
27
+ M --> N{"Rate limit?"}
28
+ N -- sim --> O["Conta em cooldown de 60 min"]
29
+ O --> P{"Outra conta<br/>disponível?"}
30
+ P -- sim --> M
31
+ P -- não --> PK{"Chave de API<br/>da Anthropic?"}
32
+ PK -- sim --> PK1["Claude com ANTHROPIC_API_KEY"]
33
+ PK1 -- ok --> R
34
+ PK1 -- falhou --> Q
35
+ PK -- não --> Q["Último recurso:<br/>Codex, depois Antigravity"]
36
+ end
37
+
38
+ N -- não --> R["Resposta"]
39
+ J -- ok --> R
40
+ K -- ok --> R
41
+ L -- ok --> R
42
+ LP -- ok --> R
43
+ Q --> R
44
+ H1 --> R
45
+ R --> F
46
+ F --> S[("Sessão salva em disco<br/>com escrita atômica")]
47
+ S --> FQ
@@ -0,0 +1,14 @@
1
+ flowchart TD
2
+ A[PR opened] --> B[CI: Python 3.11-3.13 tests on Linux and macOS<br/>+ npm pack + launcher]
3
+ A --> C[PR title: Conventional Commits?]
4
+ B -->|failed| X[Merge blocked]
5
+ C -->|invalid| X
6
+ B -->|ok| D[Squash merge into main / next / beta]
7
+ C -->|ok| D
8
+ D --> E[Release: CI runs again]
9
+ E -->|failed| Y[Nothing is published]
10
+ E -->|ok| F{semantic-release:<br/>any feat/fix/breaking<br/>since last tag?}
11
+ F -->|no| Z[No release]
12
+ F -->|yes| G[Compute version and tag vX.Y.Z]
13
+ G --> H[npm publish via OIDC / trusted publishing<br/>with provenance, on the branch dist-tag]
14
+ G --> I[GitHub Release with notes]
@@ -0,0 +1,14 @@
1
+ flowchart TD
2
+ A[PR abierto] --> B[CI: tests Python 3.11-3.13 en Linux y macOS<br/>+ npm pack + launcher]
3
+ A --> C[PR title: ¿Conventional Commits?]
4
+ B -->|falló| X[Merge bloqueado]
5
+ C -->|inválido| X
6
+ B -->|ok| D[Squash merge en main / next / beta]
7
+ C -->|ok| D
8
+ D --> E[Release: CI corre de nuevo]
9
+ E -->|falló| Y[No se publica nada]
10
+ E -->|ok| F{semantic-release:<br/>¿hay feat/fix/breaking<br/>desde la última tag?}
11
+ F -->|no| Z[Sin release]
12
+ F -->|sí| G[Calcula versión y crea tag vX.Y.Z]
13
+ G --> H[npm publish vía OIDC / trusted publishing<br/>con provenance, en el dist-tag de la rama]
14
+ G --> I[GitHub Release con notas]
@@ -0,0 +1,14 @@
1
+ flowchart TD
2
+ A[PR ouverte] --> B[CI : tests Python 3.11-3.13 sur Linux et macOS<br/>+ npm pack + launcher]
3
+ A --> C[PR title : Conventional Commits ?]
4
+ B -->|échec| X[Merge bloqué]
5
+ C -->|invalide| X
6
+ B -->|ok| D[Squash merge dans main / next / beta]
7
+ C -->|ok| D
8
+ D --> E[Release : la CI tourne à nouveau]
9
+ E -->|échec| Y[Rien n'est publié]
10
+ E -->|ok| F{semantic-release :<br/>feat/fix/breaking<br/>depuis le dernier tag ?}
11
+ F -->|non| Z[Pas de release]
12
+ F -->|oui| G[Calcule la version et crée le tag vX.Y.Z]
13
+ G --> H[npm publish via OIDC / trusted publishing<br/>avec provenance, sur le dist-tag de la branche]
14
+ G --> I[GitHub Release avec notes]
@@ -0,0 +1,14 @@
1
+ flowchart TD
2
+ A[PR aberto] --> B[CI: testes Python 3.11-3.13 em Linux e macOS<br/>+ npm pack + launcher]
3
+ A --> C[PR title: Conventional Commits?]
4
+ B -->|falhou| X[Merge bloqueado]
5
+ C -->|fora do padrão| X
6
+ B -->|ok| D[Squash merge em main / next / beta]
7
+ C -->|ok| D
8
+ D --> E[Release: CI roda de novo]
9
+ E -->|falhou| Y[Nada é publicado]
10
+ E -->|ok| F{semantic-release:<br/>há feat/fix/breaking<br/>desde a última tag?}
11
+ F -->|não| Z[Sem release]
12
+ F -->|sim| G[Calcula versão e cria tag vX.Y.Z]
13
+ G --> H[npm publish via OIDC / trusted publishing<br/>com provenance, no dist-tag da branch]
14
+ G --> I[GitHub Release com notas]
package/omni_ai/chat.py CHANGED
@@ -154,6 +154,7 @@ T = {
154
154
  /account [list|add|login|remove] [codex|agy|grok] NOME Contas (Claude por padrão; quantas quiser)
155
155
  /key [add|remove] [{keys}] [NOME] Chaves de API (várias por ferramenta, usadas em ordem)
156
156
  /status /reset Status das contas / zerar cooldowns
157
+ /doctor Diagnóstico de instalação, contas e chaves
157
158
  /sync [--dry-run] Compartilhar MCPs e skills entre Claude, Codex e agy
158
159
  /mode default|plan|edits|auto|yolo Modo de permissão (vale para Claude, Codex e agy)
159
160
  /setup [install] Guia de primeiro acesso; install instala as ferramentas que faltam
@@ -177,6 +178,7 @@ T = {
177
178
  /account [list|add|login|remove] [codex|agy|grok] NAME Accounts (Claude by default; as many as you want)
178
179
  /key [add|remove] [{keys}] [NAME] API keys (several per tool, tried in order)
179
180
  /status /reset Account status / clear cooldowns
181
+ /doctor Diagnose install, accounts and keys
180
182
  /sync [--dry-run] Share MCPs and skills across Claude, Codex and agy
181
183
  /mode default|plan|edits|auto|yolo Permission mode (applies to Claude, Codex and agy)
182
184
  /setup [install] First-run guide; install installs the missing tools
@@ -200,6 +202,7 @@ T = {
200
202
  /account [list|add|login|remove] [codex|agy|grok] NOMBRE Cuentas (Claude por defecto; las que quieras)
201
203
  /key [add|remove] [{keys}] [NOMBRE] Claves de API (varias por herramienta, en orden)
202
204
  /status /reset Estado de cuentas / limpiar cooldowns
205
+ /doctor Diagnóstico de instalación, cuentas y claves
203
206
  /sync [--dry-run] Compartir MCPs y skills entre Claude, Codex y agy
204
207
  /mode default|plan|edits|auto|yolo Modo de permisos (Claude, Codex y agy)
205
208
  /setup [install] Guía de primer acceso; install instala las herramientas que faltan
@@ -223,6 +226,7 @@ T = {
223
226
  /account [list|add|login|remove] [codex|agy|grok] NOM Comptes (Claude par défaut ; autant que voulu)
224
227
  /key [add|remove] [{keys}] [NOM] Clés API (plusieurs par outil, essayées dans l'ordre)
225
228
  /status /reset État des comptes / réinitialiser les cooldowns
229
+ /doctor Diagnostic installation, comptes et clés
226
230
  /sync [--dry-run] Partager MCP et skills entre Claude, Codex et agy
227
231
  /mode default|plan|edits|auto|yolo Mode de permissions (Claude, Codex et agy)
228
232
  /setup [install] Guide de démarrage ; install installe les outils manquants
@@ -477,10 +481,24 @@ class Runner:
477
481
  if job.get('dispatches'):
478
482
  job['dispatches'][-1]['sent'] = event['data']
479
483
  view.note('dados pessoais mascarados antes do envio · /sent para ver')
484
+ elif kind == 'smart.delta':
485
+ # Stream the answer line by line as the tokens arrive (OpenRouter).
486
+ job['stream_buf'] = job.get('stream_buf', '') + event.get('text', '')
487
+ job['streamed'] = True
488
+ while '\n' in job['stream_buf']:
489
+ line, job['stream_buf'] = job['stream_buf'].split('\n', 1)
490
+ view.stream_line(line)
480
491
  elif kind == 'smart.answer':
481
492
  job['model'] = event.get('model')
482
493
  spend(job, event.get('usage') or {})
483
- self.answer(job, event.get('content', ''))
494
+ if job.get('streamed'):
495
+ if job.get('stream_buf'):
496
+ view.stream_line(job['stream_buf'])
497
+ job['stream_buf'] = ''
498
+ # the streamed lines were display-only; keep the full, unmasked answer for the session
499
+ job['answers'] = [event.get('content', '')]
500
+ else:
501
+ self.answer(job, event.get('content', ''))
484
502
  view.note('modelo: ' + str(event.get('model', '?')))
485
503
  elif kind in ('item.started', 'item.completed'):
486
504
  item = event.get('item', {})
@@ -1016,7 +1034,7 @@ def compact(session):
1016
1034
 
1017
1035
 
1018
1036
  TOOL_COMMANDS = {'/usage': ['--usage'], '/limits': ['--usage'], '/model': ['--model'], '/account': ['--account'], '/key': ['--key'],
1019
- '/status': ['--status'], '/sync': ['--sync'], '/update': ['--update'], '/reset': ['--reset']}
1037
+ '/status': ['--status'], '/doctor': ['--doctor'], '/sync': ['--sync'], '/update': ['--update'], '/reset': ['--reset']}
1020
1038
 
1021
1039
 
1022
1040
  def run_tool(args):
@@ -1027,7 +1045,7 @@ def run_tool(args):
1027
1045
  pass
1028
1046
 
1029
1047
 
1030
- BUILTIN = ['/usage', '/clear', '/compact', '/queue', '/versions', '/setup', '/terminal-setup', '/tool', '/model', '/account', '/key', '/status', '/reset', '/sync', '/update', '/mode', '/lang',
1048
+ BUILTIN = ['/usage', '/clear', '/compact', '/queue', '/versions', '/setup', '/terminal-setup', '/tool', '/model', '/account', '/key', '/status', '/doctor', '/reset', '/sync', '/update', '/mode', '/lang',
1031
1049
  '/skills', '/history', '/sent', '/trace', '/sessions', '/resume', '/new', '/parallel', '/help', '/exit']
1032
1050
 
1033
1051
 
package/omni_ai/cli.py CHANGED
@@ -7,7 +7,7 @@
7
7
  ai --mode plan|edits|auto|yolo "question"
8
8
  ai --usage Uso e limites de todas as ferramentas e contas
9
9
  ai --setup First-run guide: logins and API keys
10
- ai --status | --reset | --log | --classify "prompt"
10
+ ai --status | --doctor | --reset | --log | --classify "prompt"
11
11
  ai --model ... | --account ... | --key ... | --sync ...
12
12
  """
13
13
  import json
@@ -683,6 +683,91 @@ def show_status():
683
683
  say()
684
684
 
685
685
 
686
+ def ai_version():
687
+ try:
688
+ return json.loads((REPO / 'package.json').read_text()).get('version', '?')
689
+ except (OSError, ValueError):
690
+ return '?'
691
+
692
+
693
+ def doctor():
694
+ """Health check: what is installed, logged in and configured, and what is missing. Returns 1 when
695
+ nothing can answer a request (no Claude login and no API key), 0 otherwise."""
696
+ import platform
697
+ ok, warn, bad = f'{GREEN}✓{NC}', f'{YELLOW}!{NC}', f'{RED}✗{NC}'
698
+ problems = []
699
+
700
+ say(f'{BOLD}{CYAN} 🩺 Omni AI CLI — diagnóstico{NC}\n')
701
+
702
+ say(f'{BOLD} Sistema{NC}')
703
+ say(f' {ok} Omni AI CLI {ai_version()}')
704
+ say(f' {ok} Python {platform.python_version()} · {platform.system()} {platform.machine()}')
705
+ local_bin = str(Path('~/.local/bin').expanduser())
706
+ if local_bin in os.environ.get('PATH', '').split(os.pathsep):
707
+ say(f' {ok} ~/.local/bin no PATH')
708
+ else:
709
+ say(f' {warn} ~/.local/bin fora do PATH (adicione ao shell para o comando `ai`)')
710
+ problems.append('~/.local/bin não está no PATH')
711
+ say()
712
+
713
+ say(f'{BOLD} Configuração{NC}')
714
+ cfg = config_tool.CONFIG_DIR
715
+ say(f' {ok if cfg.is_dir() else warn} {cfg}')
716
+ say()
717
+
718
+ say(f'{BOLD} Contas Claude{NC}')
719
+ logged = 0
720
+ profiles = config_tool.ordered(config_tool.profiles())
721
+ for name, conf in profiles:
722
+ if not conf.get('enabled', True):
723
+ say(f' {warn} {name}: desabilitado')
724
+ continue
725
+ has_login = bool(conf.get('config_dir')) or (
726
+ conf.get('credentials_file') and Path(conf['credentials_file']).expanduser().is_file())
727
+ left = cooldown_left(name)
728
+ if has_login and left:
729
+ say(f' {warn} {name}: em cooldown ({left // 60}m{left % 60}s)')
730
+ logged += 1
731
+ elif has_login:
732
+ say(f' {ok} {name}: logado')
733
+ logged += 1
734
+ else:
735
+ say(f' {bad} {name}: sem login (ai --account login {name})')
736
+ if not logged:
737
+ say(f' {warn} nenhuma conta Claude logada')
738
+ say()
739
+
740
+ say(f'{BOLD} Chaves de API{NC}')
741
+ keys = [p for p in config_tool.KEYS if config_tool.stored_key(p)[0]]
742
+ say(f" {ok if keys else warn} {', '.join(keys) if keys else 'nenhuma configurada (ai --key)'}")
743
+ say()
744
+
745
+ say(f'{BOLD} Ferramentas{NC}')
746
+ for name in CLIS:
747
+ label = TOOLS[name].label
748
+ say(f' {ok} {label}: {version(name)}' if installed(name) else f' {warn} {label}: não instalado')
749
+ say()
750
+
751
+ classifiers = [p for p in ('groq', 'cerebras') if config_tool.stored_key(p)[0]]
752
+ if classifiers:
753
+ say(f' {ok} Classificador: {", ".join(classifiers)} (rápido e grátis)')
754
+ else:
755
+ say(f' {warn} Sem chave de classificador (Groq/Cerebras); usa OpenRouter grátis ou palavras-chave')
756
+ say()
757
+
758
+ if not logged and not keys:
759
+ problems.append('nenhuma conta Claude logada e nenhuma chave de API: o roteador não tem como responder')
760
+
761
+ if problems:
762
+ say(f'{BOLD}{YELLOW} Pendências:{NC}')
763
+ for p in problems:
764
+ say(f' {warn} {p}')
765
+ say(f'\n{DIM} Rode `ai --setup` para um guia de primeiro acesso.{NC}\n')
766
+ else:
767
+ say(f'{GREEN} Tudo pronto.{NC}\n')
768
+ return 1 if not logged and not keys else 0
769
+
770
+
686
771
  def ago(seconds):
687
772
  seconds = int(seconds)
688
773
  return f'{seconds // 3600}h{seconds % 3600 // 60:02d}m' if seconds >= 3600 else f'{seconds // 60}m'
@@ -867,7 +952,12 @@ def show_log():
867
952
  def update():
868
953
  """Pull the latest version into the install folder; settings live in ~/.config and are kept."""
869
954
  if (REPO / '.git').is_dir() and resolve_bin('git'):
870
- code = subprocess.call(['git', '-C', str(REPO), 'pull', '--ff-only'])
955
+ # Pull the current branch from origin explicitly, so it works even when the branch has no
956
+ # upstream tracking set (a plain `git pull` fails there). A detached HEAD updates from main.
957
+ branch = subprocess.run(['git', '-C', str(REPO), 'rev-parse', '--abbrev-ref', 'HEAD'],
958
+ capture_output=True, text=True).stdout.strip()
959
+ ref = branch if branch and branch != 'HEAD' else 'main'
960
+ code = subprocess.call(['git', '-C', str(REPO), 'pull', '--ff-only', 'origin', ref])
871
961
  say(f'{GREEN}✓ Omni AI atualizado.{NC}' if code == 0 else f'{RED}✗ Não consegui atualizar; veja a mensagem acima.{NC}')
872
962
  return code
873
963
  if os.environ.get('SMART_AI_INSTALLER') == 'npm':
@@ -895,7 +985,7 @@ USAGE = f'''{BOLD}Uso:{NC}
895
985
  ai --usage Uso e limites de todas as ferramentas e contas
896
986
  ai --setup Guia de primeiro acesso: logins e chaves
897
987
  ai --install Instala as ferramentas que faltam (pergunta antes)
898
- ai --status | --reset | --log | --classify "prompt"
988
+ ai --status | --doctor | --reset | --log | --classify "prompt"
899
989
  ai --update Atualizar para a versão mais nova
900
990
  '''
901
991
 
@@ -945,6 +1035,8 @@ def main(argv):
945
1035
  elif arg in ('--status', '-s'):
946
1036
  show_status()
947
1037
  return 0
1038
+ elif arg in ('--doctor', 'doctor'):
1039
+ return doctor()
948
1040
  elif arg in ('--setup', '--install'):
949
1041
  from . import onboarding
950
1042
  onboarding.main(install_missing=arg == '--install' or 'install' in argv[i + 1:])
@@ -105,6 +105,55 @@ class OpenRouterClient:
105
105
  except urllib.error.URLError as e:
106
106
  raise RuntimeError("Falha de rede ao acessar OpenRouter") from None
107
107
 
108
+ def _stream_request(self, data: dict):
109
+ """POST /chat/completions with stream=True, yielding ('delta', text) per token and a final
110
+ ('final', {content, model, usage}). Raises RuntimeError on any failure, like _request."""
111
+ if os.environ.get('SMART_AI_EVENTS') == '1':
112
+ print(json.dumps({'type': 'smart.request', 'data': data}, ensure_ascii=False), file=sys.stderr, flush=True)
113
+ headers = {
114
+ "Content-Type": "application/json",
115
+ "HTTP-Referer": "https://github.com/samuelrms/omni-ai-cli",
116
+ "X-Title": "Omni AI CLI",
117
+ "Accept": "text/event-stream",
118
+ }
119
+ if self.api_key:
120
+ headers["Authorization"] = f"Bearer {self.api_key}"
121
+ body = json.dumps(dict(data, stream=True, stream_options={"include_usage": True})).encode()
122
+ req = urllib.request.Request(f"{OPENROUTER_BASE}/chat/completions", data=body, headers=headers, method="POST")
123
+ content, model, usage = [], None, {}
124
+ try:
125
+ with urllib.request.urlopen(req, timeout=60) as resp:
126
+ for raw in resp:
127
+ line = raw.decode("utf-8", "replace").strip()
128
+ if not line.startswith("data:"):
129
+ continue
130
+ payload = line[5:].strip()
131
+ if payload == "[DONE]":
132
+ break
133
+ try:
134
+ chunk = json.loads(payload)
135
+ except ValueError:
136
+ continue
137
+ model = chunk.get("model") or model
138
+ usage = chunk.get("usage") or usage
139
+ for choice in chunk.get("choices", []):
140
+ piece = (choice.get("delta") or {}).get("content") or ""
141
+ if piece:
142
+ content.append(piece)
143
+ yield ("delta", piece)
144
+ except urllib.error.HTTPError as e:
145
+ try:
146
+ reason = json.loads(e.read().decode())["error"]["message"][:160]
147
+ except (ValueError, KeyError, TypeError, OSError):
148
+ reason = ""
149
+ e.close()
150
+ raise RuntimeError(f"OpenRouter HTTP {e.code}" + (f": {reason}" if reason else "")) from None
151
+ except (ValueError, TimeoutError):
152
+ raise RuntimeError("Resposta inválida ou timeout do OpenRouter") from None
153
+ except urllib.error.URLError:
154
+ raise RuntimeError("Falha de rede ao acessar OpenRouter") from None
155
+ yield ("final", {"content": "".join(content), "model": model, "usage": usage})
156
+
108
157
  def list_free_models(self) -> list:
109
158
  """Lista todos os modelos gratuitos disponíveis."""
110
159
  resp = self._request("/models")
@@ -122,6 +171,7 @@ class OpenRouterClient:
122
171
  system_prompt: str = None,
123
172
  temperature: float = 0.7,
124
173
  max_tokens: int = 4096,
174
+ on_delta=None,
125
175
  ) -> dict:
126
176
  """
127
177
  Envia um prompt e retorna a resposta.
@@ -185,10 +235,26 @@ class OpenRouterClient:
185
235
  if not self.allow_paid:
186
236
  data["provider"] = {"max_price": {"prompt": 0, "completion": 0}}
187
237
  try:
188
- resp = self._request("/chat/completions", data=data, method="POST")
189
- if resp.get("choices"):
190
- break
191
- last_error = RuntimeError("OpenRouter não retornou uma resposta válida")
238
+ if on_delta is not None:
239
+ # Stream the tokens as they arrive; a chunk may still be a masked placeholder, so
240
+ # unmask each piece best-effort and unmask the whole answer again at the end.
241
+ final = None
242
+ for kind, payload in self._stream_request(data):
243
+ if kind == "delta":
244
+ piece = self.masker.unmask(payload, self._pii_map) if (self.masker and self._pii_map) else payload
245
+ on_delta(piece)
246
+ else:
247
+ final = payload
248
+ if final and final["content"].strip():
249
+ resp = {"choices": [{"message": {"content": final["content"]}}],
250
+ "model": final["model"], "usage": final["usage"]}
251
+ break
252
+ last_error = RuntimeError("OpenRouter não retornou uma resposta válida")
253
+ else:
254
+ resp = self._request("/chat/completions", data=data, method="POST")
255
+ if resp.get("choices"):
256
+ break
257
+ last_error = RuntimeError("OpenRouter não retornou uma resposta válida")
192
258
  except RuntimeError as error:
193
259
  last_error = error
194
260
  # Bad key or no credit: another model won't help
@@ -310,19 +376,31 @@ def main():
310
376
  parser.print_help()
311
377
  sys.exit(1)
312
378
 
379
+ events = os.environ.get('SMART_AI_EVENTS') == '1'
380
+ # Stream tokens for the chat (events) and for a human at a terminal; keep --json buffered.
381
+ if events:
382
+ on_delta = lambda piece: print(json.dumps({'type': 'smart.delta', 'text': piece}, ensure_ascii=False), flush=True)
383
+ elif not args.json:
384
+ on_delta = lambda piece: (sys.stdout.write(piece), sys.stdout.flush())
385
+ else:
386
+ on_delta = None
387
+
313
388
  try:
314
389
  result = client.chat(
315
390
  prompt=prompt,
316
391
  system_prompt=args.system,
317
392
  temperature=args.temperature,
393
+ on_delta=on_delta,
318
394
  )
319
395
 
320
- if os.environ.get('SMART_AI_EVENTS') == '1':
396
+ if events:
321
397
  print(json.dumps({'type': 'smart.answer', **result}, ensure_ascii=False), flush=True)
322
398
  elif args.json:
323
399
  print(json.dumps(result, indent=2, ensure_ascii=False))
324
- else:
400
+ elif on_delta is None:
325
401
  print(result["content"])
402
+ else:
403
+ print() # end the streamed line
326
404
 
327
405
  except RuntimeError as e:
328
406
  print(f"❌ Erro: {e}", file=sys.stderr)
package/omni_ai/ui.py CHANGED
@@ -214,10 +214,20 @@ class View:
214
214
  def __init__(self, prefix=''):
215
215
  self.prefix = prefix
216
216
  self.pending = None # last action, so its result can be attached under it
217
+ self._streamed = False # whether a streaming answer has started printing
217
218
 
218
219
  def _p(self, text):
219
220
  out('\n'.join(self.prefix + line for line in text.split('\n')))
220
221
 
222
+ def stream_line(self, line):
223
+ """One completed line of a streaming answer, printed as it arrives (line-granular keeps the
224
+ chat's input box intact, unlike raw partial writes). The first line carries the answer mark."""
225
+ if not self._streamed:
226
+ self._p('\n' + gradient(MARK) + ' ' + line)
227
+ self._streamed = True
228
+ else:
229
+ self._p(' ' + line)
230
+
221
231
  def provider(self, tool, detail):
222
232
  self._p(dim(' ') + rgb(TOOL_RGB.get(tool, (6, 182, 212)), '● ' + tool) + (dim(' · ' + detail) if detail else ''))
223
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-ai-cli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "One chat for Claude Code, Codex, Antigravity and OpenRouter, with automatic routing, account fallback, API keys, permission modes and MCP/skill sync.",
5
5
  "bin": {
6
6
  "ai": "bin/ai.js",
@@ -12,9 +12,13 @@
12
12
  "omni_ai/",
13
13
  "!omni_ai/**/__pycache__",
14
14
  "docs/",
15
+ "!docs/**/*.png",
15
16
  "README.md",
16
17
  "LICENSE"
17
18
  ],
19
+ "scripts": {
20
+ "diagrams": "bash scripts/diagrams.sh"
21
+ },
18
22
  "optionalDependencies": {
19
23
  "@anthropic-ai/claude-code": "*"
20
24
  },