diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index acf1bb7..c2bd60e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,3 +26,4 @@ jobs:
- run: python scripts/check_bundle.py
- run: python scripts/check_dashboard_pty.py
- run: python scripts/check_workspace_pty.py
+ - run: python scripts/check_claude_native_pty.py
diff --git a/Cargo.lock b/Cargo.lock
index a4bff90..5084f02 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -32,6 +32,12 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "arrayvec"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
+
[[package]]
name = "atomic"
version = "0.6.1"
@@ -122,6 +128,12 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
[[package]]
name = "cfg_aliases"
version = "0.2.2"
@@ -309,6 +321,12 @@ dependencies = [
"litrs",
]
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "either"
version = "1.18.0"
@@ -616,7 +634,7 @@ version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
dependencies = [
- "nix",
+ "nix 0.29.0",
"winapi",
]
@@ -647,11 +665,14 @@ version = "0.1.0"
dependencies = [
"chrono",
"crossterm",
+ "portable-pty",
"ratatui",
"regex",
"serde",
"serde_json",
"sha2",
+ "uuid",
+ "vt100",
]
[[package]]
@@ -672,6 +693,18 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.13.2",
+ "cfg-if",
+ "cfg_aliases 0.1.1",
+ "libc",
+]
+
[[package]]
name = "nix"
version = "0.29.0"
@@ -680,7 +713,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.13.2",
"cfg-if",
- "cfg_aliases",
+ "cfg_aliases 0.2.2",
"libc",
"memoffset",
]
@@ -907,6 +940,27 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
+[[package]]
+name = "portable-pty"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
+dependencies = [
+ "anyhow",
+ "bitflags 1.3.2",
+ "downcast-rs",
+ "filedescriptor",
+ "lazy_static",
+ "libc",
+ "log",
+ "nix 0.28.0",
+ "serial2",
+ "shared_library",
+ "shell-words",
+ "winapi",
+ "winreg",
+]
+
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -1186,6 +1240,17 @@ dependencies = [
"zmij",
]
+[[package]]
+name = "serial2"
+version = "0.2.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b16809bc35793b19ce4e0c53924bc0dce3937f15487997cfdaed936004180730"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-sys",
+]
+
[[package]]
name = "sha2"
version = "0.10.9"
@@ -1197,6 +1262,22 @@ dependencies = [
"digest",
]
+[[package]]
+name = "shared_library"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
+dependencies = [
+ "lazy_static",
+ "libc",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
[[package]]
name = "signal-hook"
version = "0.3.18"
@@ -1364,7 +1445,7 @@ dependencies = [
"libc",
"log",
"memmem",
- "nix",
+ "nix 0.29.0",
"num-derive",
"num-traits",
"ordered-float",
@@ -1505,6 +1586,7 @@ dependencies = [
"atomic",
"getrandom 0.4.3",
"js-sys",
+ "serde_core",
"wasm-bindgen",
]
@@ -1514,6 +1596,27 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "vt100"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "054ff75fb8fa83e609e685106df4faeffdf3a735d3c74ebce97ec557d5d36fd9"
+dependencies = [
+ "itoa",
+ "unicode-width",
+ "vte",
+]
+
+[[package]]
+name = "vte"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
+dependencies = [
+ "arrayvec",
+ "memchr",
+]
+
[[package]]
name = "vtparse"
version = "0.6.2"
@@ -1692,6 +1795,15 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "wit-bindgen"
version = "0.57.1"
diff --git a/Cargo.toml b/Cargo.toml
index 40abdf4..5456831 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,3 +14,6 @@ sha2 = "0.10"
regex = "1"
ratatui = "0.30.2"
crossterm = "0.29"
+portable-pty = "0.9.0"
+vt100 = "0.16.2"
+uuid = { version = "1", features = ["serde", "v4"] }
diff --git a/README.md b/README.md
index f886184..5629503 100644
--- a/README.md
+++ b/README.md
@@ -72,10 +72,22 @@ cargo run --locked -- workspace --demo --project . --state /tmp/bee-demo-state
cargo run --locked -- workspace --demo --project . --once --no-color
```
-Contrato e teclas em [workspace](docs/specs/workspace.md). A direção aprovada usa
-os logins dos CLIs oficiais como processos locais, com lançamento real conjunto
-de Claude e Codex; integração real e isolamento continuam pendentes
-([ADR 0018](docs/decisions/0018-unified-workspace.md)).
+O primeiro recorte real abre o **Claude Code original** em um PTY dentro da TUI.
+Faça login pelo próprio `claude` antes; a Memory Bee não solicita nem armazena
+credenciais. Use um projeto e uma pasta de estado explícitos:
+
+```sh
+cargo run --locked -- workspace --claude --project . --state /tmp/bee-claude-state
+cargo run --locked -- workspace --claude --project . --state /tmp/bee-claude-state --resume
+```
+
+`/exit` encerra o Claude; `Ctrl+G` abre ajuda da Memory Bee e `Ctrl+C` vai ao Claude.
+A pasta privada guarda apenas IDs de sessão e códigos de saída, enquanto o Claude
+mantém a conversa nativa. Este recorte ainda mostra a interface do Claude dentro
+da moldura Bee; memória/exportação integrada, perfis isolados e interface de
+conversa própria continuam pendentes. Contrato e teclas em
+[workspace](docs/specs/workspace.md). O lançamento da experiência completa com
+Claude e Codex segue condicionado aos aceites do [ADR 0018](docs/decisions/0018-unified-workspace.md).
O dashboard anterior permanece disponível.
## Desenvolvimento
diff --git a/ROADMAP.md b/ROADMAP.md
index 1b578da..041bb8e 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -34,9 +34,9 @@ por trás, assinaturas existentes e lançamento conjunto. Isso amplia as etapas
1. Protótipo funcional com os dois agentes simulados, menus, streaming,
permissões, erro, persistência e exportação: implementado como `workspace --demo`.
-2. Prova real por agente/assinatura, incluindo dois perfis isolados: pendente;
- hospedar o binário Claude Code original com login do usuário é um caminho
- documentado, mas o controle da conversa pela nossa TUI ainda não foi provado.
+2. Prova real por agente/assinatura, incluindo dois perfis isolados: em andamento.
+ `workspace --claude` hospeda o binário Claude Code original em PTY e retoma
+ seu ID nativo; conversa, permissões e perfis reais ainda exigem ensaio humano.
3. Experiência integrada aos dois harnesses, histórico e exportação: pendente.
4. Troca real entre agentes/contas com revisão e preservação da origem: pendente.
5. Lançamento conjunto após todos os aceites, sem substituir o requisito por
@@ -52,6 +52,13 @@ uso da assinatura em um produto de terceiros. A prova deve preservar permissões
reais, interrupção, retomada e exportação. Se a interface estruturada Claude não
atender a esse contrato, registrar a lacuna antes do lançamento conjunto.
+O mantenedor priorizou a implementação nativa **Claude primeiro**. O PTY com
+sessão nativa é a primeira entrega desse caminho, disponível como modo experimental;
+não altera o aceite do lançamento conjunto da experiência completa. O próximo
+recorte deve ligar histórico/exportação revisável e ações à sessão Claude, além
+de validar o fluxo real de conversa e permissões. O HTML ainda representa o plano
+visual anterior; este Markdown e o ADR 0018 prevalecem para a sequência.
+
Aceite real: conversa, edição, execução, permitir/negar, interrupção e retomada
com cada harness; isolamento de dois perfis; exportar após falha/limite; recuperar
histórico sem reenvio; teclado/colagem/resize/sem cor e restauração. Demo e testes
diff --git a/docs/EXECUTION.md b/docs/EXECUTION.md
index a1fc6db..ab9f7f3 100644
--- a/docs/EXECUTION.md
+++ b/docs/EXECUTION.md
@@ -5,7 +5,7 @@
## Situação atual
- Existe documentação de produto, workflow e um HTML de planejamento.
-- CLI de inspeção, descoberta, seleção e exportação somente contexto implementadas; referência Git opcional implementada; código selecionado v2 implementado; verify/apply explícito implementados; inspeção/exportação Codex por arquivo e descoberta por projeto sob raiz explícita implementadas, experimentais; preparação de retomada Claude/Codex e lançamento confirmado por token implementados, experimentais; dashboard de terminal (ratatui) com lista, detalhe, exportação de contexto/exclusões, aplicação e prompt/lançamento confirmado implementados; workspace unificado disponível apenas com simulação explícita (ADR 0018); cena animada, exportação de código pela TUI e contas reais pendentes.
+- CLI de inspeção, descoberta, seleção e exportação somente contexto implementadas; referência Git opcional implementada; código selecionado v2 implementado; verify/apply explícito implementados; inspeção/exportação Codex por arquivo e descoberta por projeto sob raiz explícita implementadas, experimentais; preparação de retomada Claude/Codex e lançamento confirmado por token implementados, experimentais; dashboard de terminal (ratatui) com lista, detalhe, exportação de contexto/exclusões, aplicação e prompt/lançamento confirmado implementados; workspace unificado com demo explícita e primeiro modo Claude local sobre CLI oficial/PTY, experimental (ADR 0018); cena animada, exportação nativa pela TUI e perfis reais pendentes.
- Nome decidido: **Memory Bee** (`memory-bee`), com mascote abelha e colmeia dos projetos, conforme o [ADR 0015](decisions/0015-memory-bee-identity.md); crate, binário, pacotes e documentos renomeados para `memory-bee` (registros anteriores mantêm o nome da época). Núcleo em Rust, primeiro alvo macOS arm64 e leitor Claude Code escolhidos. Licença pendente.
- Cargo, toolchain Rust 1.98.1 e checks canônicos definidos no README; CI em macOS/Linux.
- Remoto atual: [lippdev/memory-bee](https://github.com/lippdev/memory-bee), público.
@@ -28,25 +28,19 @@ de patches e consulta ao Git ficam para a etapa 04.
## Próxima tarefa de produto
-**Refinamento visual — próxima entrega local.** A orientação mais recente do
-mantenedor autoriza a frente visual em paralelo à prova dos harnesses. Referência
-HTML e [plano visual](specs/workspace-visual.md) concluídos; próximo recorte:
-renderizador Rust com abelha à direita do campo amarelo e estados por ação,
-seguido da conexão aos eventos da demo. Não iniciar esse porte na entrega
-restrita ao protótipo/plano. Exportação e troca de agente ainda precisam de
-animações próprias.
-
-**TUI unificada — prova de integração com assinaturas.** A orientação de
-2026-09-25 substitui a prioridade anterior da cena animada: interface permanente
-Memory Bee, assinaturas existentes e lançamento real somente com Claude e Codex.
-Primeiro recorte `workspace --demo` implementado; não declarar programação real.
-Próximo: provar a hospedagem do CLI Claude original autenticado pelo próprio
-usuário e fazer prova controlada de ambos os processos (conversa, edição,
-execução, permissões, interrupção, retomada e dois perfis isolados). Protocolo
-Codex experimental existe, sem conexão à TUI nem
-validação real. Não copiar credenciais, não substituir assinatura por API
-paga e não lançar só um provedor. Ver ADR 0018 e contrato `specs/workspace.md`.
-Cena, exportação de código pela TUI e worktree na interface continuam pendentes.
+**Claude nativo primeiro — próxima entrega de produto.** O mantenedor priorizou
+Claude antes de Codex. `workspace --claude` já hospeda a CLI original em PTY,
+com sessão nativa, entrada/saída interativa e retomada por ID; é experimental.
+Próximo: ensaio humano de conversa, edição, execução, permitir/negar, interrupção
+e retomada com assinatura existente; ligar histórico/exportação revisável e ações
+da Memory Bee à sessão Claude. O modo atual guarda apenas IDs, não a memória.
+Perfis reais, interface de conversa própria e Codex integrado permanecem pendentes.
+O lançamento da experiência completa ainda exige os dois agentes (ADR 0018).
+
+**Refinamento visual.** Referência HTML e [plano visual](specs/workspace-visual.md)
+concluídos; renderizador Rust, abelha por ação e animações de exportação/troca
+permanecem pendentes. O HTML apresenta o plano visual anterior à prioridade
+Claude primeiro; este registro e o roadmap Markdown prevalecem.
A renomeação no código está feita (seção "Renomeação — memory-bee"). O GitHub
já informa `lippdev/memory-bee`; a URL local antiga redireciona. Não foi feita
@@ -637,3 +631,25 @@ escopo da entrega atual nem a próxima tarefa aprovada.
Autorrevisão; nenhuma revisão independente ou chamada a modelo nesta revisão.
- Próximo: ensaio controlado com os CLIs oficiais em ambiente explícito, cobrindo
conversa, eventos, permissões e interrupção antes de integrar à tela.
+
+## Claude Code local na TUI — primeiro recorte nativo
+
+- Escopo: `workspace --claude --project
--state ` abre o Claude Code
+ instalado em PTY, usando autenticação e permissões da CLI original. A Memory
+ Bee encaminha entrada/colagem/resize e desenha a saída; `--resume` reutiliza o
+ último ID nativo. Estado separado guarda só IDs e códigos de saída, com pasta
+ privada e trava. Demo e dashboard anteriores continuam disponíveis.
+- Evidências: `cargo fmt --all -- --check`, `cargo clippy --locked --all-targets
+ -- -D warnings`, `cargo test --locked`, `cargo build --locked`,
+ `python3 scripts/check_claude_native_pty.py`, links relativos e
+ `git diff --check` passaram localmente. Teste sintético exercitou CLI falsa,
+ entrada, retomada, privacidade, recusa sem TTY e restauração. Ensaio controlado
+ iniciou e encerrou o Claude instalado com `/exit`, sem enviar prompt/modelo;
+ não comprova fluxo de conversa nem permissões. CI macOS/Linux configurado para
+ repetir o teste sintético.
+- Autorrevisão de escopo, armazenamento e fronteira de autenticação; sem revisão
+ independente. Ensaio manual 51 pendente. Não há ainda exportação integrada,
+ eventos estruturados, UI de conversa própria, perfis isolados nem Codex real.
+- Próximo: executar o roteiro 51 com conversa/permissões reais e vincular a sessão
+ Claude ao histórico/exportação revisável, mantendo origem e omissões explícitas.
+ Lançamento completo com ambos os agentes permanece pendente.
diff --git a/docs/MANUAL_TESTS.md b/docs/MANUAL_TESTS.md
index 31ef354..2ddc5ce 100644
--- a/docs/MANUAL_TESTS.md
+++ b/docs/MANUAL_TESTS.md
@@ -839,3 +839,33 @@ histórico. Após inatividade, digitar acorda. Conferir movimento desligado, sem
cor, tema claro/escuro, larguras 140/100/60/40 e janela estreita. A cena inicial
continua disponível. Nenhum teste, arquivo ou agente real é executado pelo HTML.
Esse roteiro não valida o renderizador Rust, ainda pendente.
+
+## 51. Claude Code nativo dentro da Memory Bee (pendente)
+
+Em um projeto descartável, faça login primeiro pelo Claude Code original e
+confirme que ele funciona fora da Memory Bee. Depois rode:
+
+```sh
+cargo build --locked
+mkdir -p /tmp/bee-claude-project
+target/debug/memory-bee workspace --claude --project /tmp/bee-claude-project --state /tmp/bee-claude-state
+target/debug/memory-bee workspace --claude --project /tmp/bee-claude-project --state /tmp/bee-claude-state --resume
+```
+
+Na primeira sessão, enviar tarefa simples, observar resposta e, só no projeto
+descartável, pedir uma edição/execução que solicite permissão. Ensaiar **Negar** e
+**Permitir** separadamente. Verificar `Ctrl+C` durante resposta, colagem,
+redimensionamento, `Ctrl+G` e `/exit`; conferir restauração do terminal. Na
+segunda chamada, verificar que a conversa nativa retorna sem reenviar o prompt.
+Conferir `claude-native.json` e permissões 0700/0600, sem registrar conta, tokens
+ou conteúdo da conversa no repositório. Estado com outro projeto e chamada sem
+TTY devem ser recusados. Anotar versão do Claude, resultado de cada ação e se
+houve uso da assinatura já autenticada.
+
+O teste automatizado sintético, sem login/modelo, é
+`python3 scripts/check_claude_native_pty.py`; aprovação nele não conclui este
+ensaio. Exportação da memória pelo modo nativo e perfis isolados seguem pendentes.
+
+| Item novo | Estado | Evidência manual |
+|---|---|---|
+| 51: conversa, permissões, interrupção e retomada Claude reais | Pendente | — |
diff --git a/docs/decisions/0018-unified-workspace.md b/docs/decisions/0018-unified-workspace.md
index f13c0f8..1cc1a40 100644
--- a/docs/decisions/0018-unified-workspace.md
+++ b/docs/decisions/0018-unified-workspace.md
@@ -112,3 +112,20 @@ de tokens externos na Memory Bee. Um terminal incorporado pode ser um transporte
ou um escape para interações nativas, mas não substitui sozinho o aceite da
conversa unificada e da exportação. Nenhum login ou modelo foi acionado nesta
revisão documental.
+
+## Recorte Claude primeiro — 2026-09-25
+
+O mantenedor mudou a ordem de implementação: tornar Claude nativo na TUI antes
+de integrar Codex. A experiência completa ainda depende dos dois agentes, mas
+o modo Claude pode ser entregue e ensaiado separadamente, identificado como
+experimental. Isso substitui a restrição anterior de não disponibilizar um
+provedor isoladamente durante o desenvolvimento.
+
+Escolhemos iniciar o binário Claude Code original em PTY, com o login já feito
+na CLI oficial. `--session-id` cria uma referência controlada pela Memory Bee;
+`--resume` retoma esse ID no mesmo projeto. A TUI encaminha entrada e desenha
+a saída; a pasta privada guarda apenas IDs/códigos, sem token ou transcrição.
+O transporte preserva a interface de permissões do próprio Claude e não exige
+API paga separada. É um passo concreto para testar assinatura e conversa nativa,
+mas não comprova uma UI Bee própria para eventos, exportação ou perfis. Essas
+lacunas, o ensaio humano e a integração Codex permanecem no aceite completo.
diff --git a/docs/specs/workspace.md b/docs/specs/workspace.md
index 20a4fbd..25b711d 100644
--- a/docs/specs/workspace.md
+++ b/docs/specs/workspace.md
@@ -1,15 +1,39 @@
-# Workspace unificado — demonstração experimental
+# Workspace unificado — demonstração e Claude local experimental
-Contrato do primeiro recorte do [ADR 0018](../decisions/0018-unified-workspace.md).
-Não é programação real. Claude/Codex, ferramentas, mudanças e permissões são
-simulados, sem processos de provedores, rede ou edição do projeto.
+Contrato dos primeiros recortes do [ADR 0018](../decisions/0018-unified-workspace.md).
+`workspace --demo` não é programação real: Claude/Codex, ferramentas, mudanças e
+permissões são simulados. `workspace --claude` inicia o Claude Code instalado.
Para a versão real, a TUI deverá hospedar os CLIs oficiais como processos locais.
O usuário autentica cada um pelo seu próprio fluxo. A Memory Bee não lê nem
armazena tokens; associa sessões, eventos e exportações ao processo e ao perfil
selecionado. App Server é o transporte candidato para Codex; para Claude, testar
o binário original em PTY e verificar se a CLI permite uma conversa própria com
-permissões fiéis. O transporte Claude ainda não está decidido nem implementado.
+permissões fiéis. O PTY é o primeiro transporte Claude implementado; ainda não
+fornece eventos estruturados nem conversa renderizada pela Memory Bee.
+
+## Claude local experimental
+
+```sh
+memory-bee workspace --claude --project . --state /tmp/bee-claude-state
+memory-bee workspace --claude --project . --state /tmp/bee-claude-state --resume
+```
+
+Exige `claude` no PATH, TTY interativo, projeto existente e pasta de estado
+privada (pai existente). Inicia o binário original com `--session-id ` ou
+`--resume `, sem copiar tokens. O login, a conversa, as ferramentas e as
+decisões de permissão pertencem ao Claude Code. A Memory Bee encaminha teclado,
+colagem e resize ao PTY, desenha sua saída em moldura própria e restaura o terminal
+ao sair. `Ctrl+G` abre ajuda Bee; `Ctrl+C` vai ao Claude; `/exit` encerra a sessão.
+
+`claude-native.json` guarda projeto canônico, IDs e códigos de saída (até 128
+sessões, 64 KiB), sem transcrição. A pasta é 0700 e arquivos são 0600 em Unix;
+`claude-native.lock` impede segundo escritor. Após crash, verificar que não há
+processo ativo antes de remover a trava antiga. `--resume` usa o último ID desta
+pasta; não reenvia prompt. A sessão nativa pode não existir quando se sai antes
+de enviar uma mensagem. Exportação/revisão de memória, isolamento de perfis,
+decisões em UI própria e integração Codex estão pendentes. O modo não comprova
+conversa/permissões reais por si só; ver [roteiro manual](../MANUAL_TESTS.md).
```sh
cargo run --locked -- workspace --demo --project . --state /tmp/bee-demo-state
@@ -20,8 +44,9 @@ O pai de `--state` precisa existir. Pasta nova é criada 0700, arquivos 0600;
pasta existente precisa ser privada. `--once` usa memória, não aceita `--state`,
não grava nem lê sessões existentes. `--width/--height` (1–500) só com `--once`.
`--agent claude|codex` escolhe apenas a primeira sessão de um estado novo.
-`--theme dark|light`, `--no-color` e `NO_COLOR` controlam aparência. Flags repetidas,
-modo real, projeto ausente e uso interativo sem TTY retornam 64; falha de I/O, 1.
+`--theme dark|light`, `--no-color` e `NO_COLOR` controlam aparência. Na demo,
+flags repetidas, projeto ausente e uso interativo sem TTY retornam 64; falha de
+I/O, 1. Modo sem `--demo` nem `--claude` segue recusado.
Projeto é normalizado para caminho absoluto e precisa ser diretório existente.
## Experiência e teclas
@@ -92,5 +117,7 @@ pelo usuário continua sendo dado real local, mesmo numa simulação: não versi
proveniência, exclusões e telas estreitas. `scripts/check_workspace_pty.py` percorre
colagem, decisão negada, exportação/schema/verify, resize, interrupção e restauração
em PTY com projeto sintético. O [roteiro manual](../MANUAL_TESTS.md) mantém ensaios
-humanos separados. Adaptador Codex experimental é apenas protocolo não conectado;
-Claude real, contas, isolamento real, reinício nativo e lançamento conjunto pendentes.
+humanos separados. `scripts/check_claude_native_pty.py` usa CLI falsa para testar
+entrada, ID, retomada, estado privado e restauração do terminal. Adaptador Codex
+experimental é apenas protocolo não conectado; ensaio de conversa Claude real,
+contas, isolamento real, exportação nativa e lançamento conjunto pendentes.
diff --git a/scripts/check_claude_native_pty.py b/scripts/check_claude_native_pty.py
new file mode 100644
index 0000000..15670bc
--- /dev/null
+++ b/scripts/check_claude_native_pty.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python3
+"""Exercise Memory Bee's Claude terminal with a fake CLI; no model calls."""
+import fcntl
+import json
+import os
+from pathlib import Path
+import pty
+import select
+import struct
+import subprocess
+import tempfile
+import termios
+import time
+
+
+def one_run(binary, project, state, env, resume=False):
+ master, slave = pty.openpty()
+ fcntl.ioctl(slave, termios.TIOCSWINSZ, struct.pack('HHHH', 20, 80, 0, 0))
+ before = termios.tcgetattr(slave)
+ argv = [str(binary), 'workspace', '--claude', '--project', str(project), '--state', str(state), '--no-color']
+ if resume:
+ argv.append('--resume')
+ proc = subprocess.Popen(argv, stdin=slave, stdout=slave, stderr=slave, env=env)
+ output = bytearray()
+
+ def drain_until(token, seconds=5):
+ deadline = time.monotonic() + seconds
+ while time.monotonic() < deadline:
+ if select.select([master], [], [], 0.05)[0]:
+ chunk = os.read(master, 65536)
+ output.extend(chunk)
+ if b'\x1b[6n' in chunk:
+ os.write(master, b'\x1b[1;1R')
+ if token in output:
+ return
+ if proc.poll() is not None:
+ break
+ raise AssertionError(f'did not see synthetic {token!r}; exit={proc.poll()}')
+
+ try:
+ drain_until(b'READY')
+ assert (state / 'claude-native.json').exists()
+ os.write(master, b'hello\r')
+ drain_until(b'GOT:')
+ proc.wait(timeout=5)
+ assert proc.returncode == 0
+ assert termios.tcgetattr(slave) == before, 'terminal state not restored'
+ assert not (state / 'claude-native.lock').exists()
+ return json.loads((state / 'claude-native.json').read_text())
+ finally:
+ if proc.poll() is None:
+ proc.kill()
+ proc.wait()
+ os.close(master)
+ os.close(slave)
+
+
+def main():
+ repo = Path(__file__).resolve().parent.parent
+ binary = repo / 'target/debug/memory-bee'
+ with tempfile.TemporaryDirectory(prefix='memory-bee-claude-native-') as tmp:
+ root = Path(tmp)
+ fake_dir = root / 'bin'
+ fake_dir.mkdir()
+ fake = fake_dir / 'claude'
+ fake.write_text('#!/bin/sh\nprintf "%s|%s|%s\\n" "$1" "$2" "$PWD" >> "$BEE_FAKE_ARGS"\nprintf "FAKE CLAUDE READY\\r\\n"\nIFS= read -r line\nprintf "GOT:%s\\r\\n" "$line"\n')
+ fake.chmod(0o700)
+ project = root / 'project'
+ project.mkdir()
+ state = root / 'state'
+ args_file = root / 'claude-args'
+ env = dict(os.environ, PATH=f'{fake_dir}:/usr/bin:/bin', TERM='xterm-256color', BEE_FAKE_ARGS=str(args_file))
+ first = one_run(binary, project, state, env)
+ assert first['version'] == 1
+ assert len(first['sessions']) == 1
+ session_id = first['sessions'][0]['id']
+ assert first['sessions'][0]['exit_code'] == 0
+ second = one_run(binary, project, state, env, resume=True)
+ assert len(second['sessions']) == 1
+ assert second['sessions'][0]['id'] == session_id
+ assert args_file.read_text().splitlines() == [
+ f'--session-id|{session_id}|{project.resolve()}',
+ f'--resume|{session_id}|{project.resolve()}',
+ ], 'Claude must receive the native session ID in the selected project'
+ assert list(project.iterdir()) == [], 'fake CLI modified the project'
+ denied_state = root / 'denied'
+ result = subprocess.run([str(binary), 'workspace', '--claude', '--project', str(project), '--state', str(denied_state)], capture_output=True)
+ assert result.returncode == 64 and not denied_state.exists(), 'non-TTY launch should refuse before writing'
+ print('PASS: native Claude PTY, input, session ID, resume, private state and terminal restoration (fake CLI)')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/main.rs b/src/main.rs
index f185297..bb32e5c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -18,7 +18,7 @@ use std::{
process::ExitCode,
};
-const USAGE: &str = "Usage:\n memory-bee workspace --demo --project (--state | --once [--width ] [--height ]) [--agent claude|codex] [--theme dark|light] [--no-color]\n memory-bee export-codex (--preview | --output ) [--exclude-line ]... [--project ] [--include-path ]...\n memory-bee inspect-codex \n memory-bee inspect [--leaf ]\n memory-bee sessions-codex --root --project \n memory-bee sessions --root --project \n memory-bee export (--preview | --output ) [--leaf ] [--exclude-line ]... [--project ] [--include-path ]...\n memory-bee verify \n memory-bee apply --project (--check | --write)\n memory-bee prepare-resume --target (claude | codex) --project [--worktree ] (--preview | --output [--launch ])\n memory-bee dashboard --project [--claude-root ] [--codex-root ] [--bundle ] [--theme (dark|light)] [--no-color] [--once [--width ] [--height ]]\nOffline; no model calls.\nCLI launch requires --launch and a matching confirmation from --preview.\nDashboard actions require a preview and explicit confirmation.\nExit: 0 success, 2 partial or attention needed, 3 possible secrets, 4 launched agent exited non-zero, 1 I/O or selection error, 64 usage error.";
+const USAGE: &str = "Usage:\n memory-bee workspace --demo --project (--state | --once [--width ] [--height ]) [--agent claude|codex] [--theme dark|light] [--no-color]\n memory-bee workspace --claude --project --state [--resume] [--theme dark|light] [--no-color]\n memory-bee export-codex (--preview | --output ) [--exclude-line ]... [--project ] [--include-path ]...\n memory-bee inspect-codex \n memory-bee inspect [--leaf ]\n memory-bee sessions-codex --root --project \n memory-bee sessions --root --project \n memory-bee export (--preview | --output ) [--leaf ] [--exclude-line ]... [--project ] [--include-path ]...\n memory-bee verify \n memory-bee apply --project (--check | --write)\n memory-bee prepare-resume --target (claude | codex) --project [--worktree ] (--preview | --output [--launch ])\n memory-bee dashboard --project [--claude-root ] [--codex-root ] [--bundle ] [--theme (dark|light)] [--no-color] [--once [--width ] [--height ]]\nInspection and export are offline. workspace --claude starts the installed Claude Code CLI.\nCLI launch requires --launch and a matching confirmation from --preview.\nDashboard actions require a preview and explicit confirmation.\nExit: 0 success, 2 partial or attention needed, 3 possible secrets, 4 launched agent exited non-zero, 1 I/O or selection error, 64 usage error.";
fn output(value: &impl Serialize) -> Result<(), (u8, String)> {
let stdout = io::stdout();
let mut out = stdout.lock();
@@ -528,6 +528,8 @@ fn workspace(args: &[std::ffi::OsString]) -> Result {
let mut project: Option = None;
let mut state: Option = None;
let mut demo = false;
+ let mut claude_live = false;
+ let mut resume = false;
let mut once = false;
let mut agent = Agent::Claude;
let mut mode = tui::theme::Mode::Dark;
@@ -542,6 +544,8 @@ fn workspace(args: &[std::ffi::OsString]) -> Result {
}
match flag {
"--demo" => demo = true,
+ "--claude" => claude_live = true,
+ "--resume" => resume = true,
"--once" => once = true,
"--no-color" => no_color = true,
"--project" | "--state" | "--agent" | "--theme" | "--width" | "--height" => {
@@ -580,7 +584,10 @@ fn workspace(args: &[std::ffi::OsString]) -> Result {
}
i += 1;
}
- if !demo {
+ if (demo && claude_live) || (resume && !claude_live) {
+ return Err((64, USAGE.into()));
+ }
+ if !demo && !claude_live {
memory_bee::workspace::live_gate().map_err(|e| (64, e))?;
}
let project = project
@@ -597,6 +604,24 @@ fn workspace(args: &[std::ffi::OsString]) -> Result {
if once && state.is_some() || !once && (seen.contains("--width") || seen.contains("--height")) {
return Err((64, USAGE.into()));
}
+ if claude_live {
+ if once || seen.contains("--agent") {
+ return Err((64, USAGE.into()));
+ }
+ if !io::stdin().is_terminal() || !io::stdout().is_terminal() {
+ return Err((
+ 64,
+ "workspace --claude precisa de terminal interativo".into(),
+ ));
+ }
+ let state_path =
+ state.ok_or((64, "workspace --claude exige --state ".into()))?;
+ let (id, code) =
+ tui::claude_native::run(Path::new(&project), &state_path, resume, mode, no_color)
+ .map_err(|e| (1, e))?;
+ println!("Claude Code encerrou. Sessão nativa: {id}");
+ return Ok(if code == 0 { 0 } else { 4 });
+ }
if once {
let state = State::new(project, agent);
let mut app = tui::workspace::App::new(state, None, mode, no_color);
diff --git a/src/tui/claude_native.rs b/src/tui/claude_native.rs
new file mode 100644
index 0000000..0b9d3ed
--- /dev/null
+++ b/src/tui/claude_native.rs
@@ -0,0 +1,532 @@
+//! Memory Bee frame around the unmodified, interactive Claude Code CLI.
+use super::theme::{Mode, Palette};
+use crossterm::{
+ cursor::Show,
+ event::{
+ self, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyEvent, KeyEventKind,
+ KeyModifiers,
+ },
+ terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
+};
+use memory_bee::workspace::claude_native::ClaudeStore;
+use portable_pty::{CommandBuilder, MasterPty, NativePtySystem, PtySize, PtySystem};
+use ratatui::{
+ Frame, Terminal,
+ backend::CrosstermBackend,
+ layout::{Constraint, Layout, Rect},
+ style::{Color, Modifier, Style},
+ widgets::{Block, Borders, Paragraph},
+};
+use std::{
+ ffi::OsStr,
+ io::{self, Read, Write},
+ path::Path,
+ sync::mpsc::{self, Receiver},
+ thread,
+ time::{Duration, Instant},
+};
+use uuid::Uuid;
+
+enum Output {
+ Bytes(Vec),
+ Closed,
+}
+
+struct Pty {
+ master: Box,
+ child: Box,
+ writer: Box,
+ output: Receiver