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 +24 -77
- package/docs/README.en.md +24 -77
- package/docs/README.es.md +23 -76
- package/docs/README.fr.md +24 -78
- package/docs/diagrams/flow1.en.mmd +47 -0
- package/docs/diagrams/flow1.es.mmd +47 -0
- package/docs/diagrams/flow1.fr.mmd +47 -0
- package/docs/diagrams/flow1.pt.mmd +47 -0
- package/docs/diagrams/flow2.en.mmd +14 -0
- package/docs/diagrams/flow2.es.mmd +14 -0
- package/docs/diagrams/flow2.fr.mmd +14 -0
- package/docs/diagrams/flow2.pt.mmd +14 -0
- package/omni_ai/chat.py +21 -3
- package/omni_ai/cli.py +95 -3
- package/omni_ai/openrouter.py +84 -6
- package/omni_ai/ui.py +10 -0
- package/package.json +5 -1
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
|
-
](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
|
+
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
|
+
[](https://docs.npmjs.com/trusted-publishers)
|
|
4
10
|
[](LICENSE)
|
|
5
11
|

|
|
6
12
|

|
|
@@ -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
|
-
|
|
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
|
+

|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
564
|
-
|
|
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
|
-
](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
|
+
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
|
+
[](https://docs.npmjs.com/trusted-publishers)
|
|
4
10
|
[](../LICENSE)
|
|
5
11
|

|
|
6
12
|

|
|
@@ -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
|
-
|
|
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
|
+

|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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
|
-
](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
|
+
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
|
+
[](https://docs.npmjs.com/trusted-publishers)
|
|
4
10
|
[](../LICENSE)
|
|
5
11
|

|
|
6
12
|

|
|
@@ -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
|
-
|
|
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
|
+

|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
+

|
|
561
505
|
|
|
562
|
-
|
|
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. **
|
|
565
|
-
|
|
566
|
-
2. **Settings → Branches**: proteger `main` (y `next`/`beta` si se usan) exigiendo
|
|
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
|
-
](https://www.npmjs.com/package/omni-ai-cli)
|
|
8
|
+
[](https://www.npmjs.com/package/omni-ai-cli)
|
|
9
|
+
[](https://docs.npmjs.com/trusted-publishers)
|
|
4
10
|
[](../LICENSE)
|
|
5
11
|

|
|
6
12
|

|
|
@@ -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
|
-
|
|
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
|
+

|
|
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:])
|
package/omni_ai/openrouter.py
CHANGED
|
@@ -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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
},
|