crabcode 0.1.1__tar.gz → 0.1.3__tar.gz
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.
- {crabcode-0.1.1 → crabcode-0.1.3}/.gitignore +2 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/PKG-INFO +16 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/README.md +16 -5
- {crabcode-0.1.1 → crabcode-0.1.3}/README.zh-CN.md +16 -5
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/crabcode_cli/__init__.py +1 -1
- crabcode-0.1.3/packages/cli/crabcode_cli/__main__.py +4 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/crabcode_cli/app.py +29 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/crabcode_cli/pipe.py +50 -3
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/crabcode_cli/repl.py +170 -12
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/pyproject.toml +2 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/__init__.py +1 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/agent_manager.py +1 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/anthropic_adapter.py +8 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/azure_adapter.py +2 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/base.py +2 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/codex_adapter.py +271 -9
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/openai_adapter.py +30 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/events.py +82 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/logging_utils.py +11 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/prompts/system.py +17 -13
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/prompts/templates.py +2 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/query/loop.py +134 -38
- crabcode-0.1.3/packages/core/crabcode_core/schedule/__init__.py +5 -0
- crabcode-0.1.3/packages/core/crabcode_core/schedule/models.py +204 -0
- crabcode-0.1.3/packages/core/crabcode_core/schedule/store.py +349 -0
- crabcode-0.1.3/packages/core/crabcode_core/session/__init__.py +12 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/session/storage.py +19 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/__init__.py +4 -0
- crabcode-0.1.3/packages/core/crabcode_core/tools/checkpoint.py +66 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/file_edit.py +28 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/grep.py +6 -2
- crabcode-0.1.3/packages/core/crabcode_core/tools/revert.py +90 -0
- crabcode-0.1.3/packages/core/crabcode_core/tools/schedule.py +612 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/types/config.py +17 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/types/event.py +19 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/types/message.py +12 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/types/tool.py +2 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/pyproject.toml +1 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/__init__.py +1 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/acp/agent.py +1 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/event_bus.py +15 -6
- {crabcode-0.1.1/packages/gateway/crabcode_gateway/grpc_ → crabcode-0.1.3/packages/gateway/crabcode_gateway/grpc}/server.py +18 -18
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/middleware.py +99 -25
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/routes/config.py +92 -9
- crabcode-0.1.3/packages/gateway/crabcode_gateway/routes/event.py +356 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/routes/permission.py +1 -0
- crabcode-0.1.3/packages/gateway/crabcode_gateway/routes/session.py +371 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/routes/snapshot.py +23 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/schemas.py +40 -1
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/server.py +2 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/pyproject.toml +2 -2
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/crabcode_search/embedder.py +57 -7
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/pyproject.toml +2 -2
- crabcode-0.1.3/packages/vscode/.vscode/launch.json +19 -0
- crabcode-0.1.3/packages/vscode/.vscode/tasks.json +18 -0
- crabcode-0.1.3/packages/vscode/esbuild.js +35 -0
- crabcode-0.1.3/packages/vscode/logo.png +0 -0
- crabcode-0.1.3/packages/vscode/package-lock.json +545 -0
- crabcode-0.1.3/packages/vscode/package.json +219 -0
- crabcode-0.1.3/packages/vscode/resources/icon.svg +17 -0
- crabcode-0.1.3/packages/vscode/scripts/gen-types.py +413 -0
- crabcode-0.1.3/packages/vscode/src/chatPanel.ts +5930 -0
- crabcode-0.1.3/packages/vscode/src/client/protocol.ts +164 -0
- crabcode-0.1.3/packages/vscode/src/client/types.ts +392 -0
- crabcode-0.1.3/packages/vscode/src/connection.ts +410 -0
- crabcode-0.1.3/packages/vscode/src/extension.ts +697 -0
- crabcode-0.1.3/packages/vscode/src/gatewayManager.ts +466 -0
- crabcode-0.1.3/packages/vscode/src/pendingEdits.ts +1890 -0
- crabcode-0.1.3/packages/vscode/tsconfig.json +19 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/pyproject.toml +4 -1
- crabcode-0.1.1/packages/core/crabcode_core/session/__init__.py +0 -4
- crabcode-0.1.1/packages/gateway/crabcode_gateway/routes/event.py +0 -168
- crabcode-0.1.1/packages/gateway/crabcode_gateway/routes/session.py +0 -159
- crabcode-0.1.1/tests/test_acp.py +0 -267
- crabcode-0.1.1/tests/test_browser_tool.py +0 -382
- crabcode-0.1.1/tests/test_hooks.py +0 -170
- crabcode-0.1.1/tests/test_imports.py +0 -14
- crabcode-0.1.1/tests/test_logging.py +0 -88
- crabcode-0.1.1/tests/test_multi_agent.py +0 -415
- crabcode-0.1.1/tests/test_plan_mode.py +0 -738
- crabcode-0.1.1/tests/test_provider_adapters.py +0 -393
- crabcode-0.1.1/tests/test_snapshot.py +0 -293
- crabcode-0.1.1/tests/test_tool_input_helpers.py +0 -108
- crabcode-0.1.1/tests/test_web_search.py +0 -462
- {crabcode-0.1.1 → crabcode-0.1.3}/.github/workflows/python-publish.yml +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/crabcode_cli/banner.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/crabcode_cli/render/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/cli/crabcode_cli/render/messages.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/gemini_adapter.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/model_info.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/ollama_adapter.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/api/registry.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/compact/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/compact/compact.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/config/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/config/claudemd.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/config/manager.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/hooks/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/hooks/manager.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/lsp/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/lsp/client.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/lsp/diagnostics.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/lsp/manager.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/mcp/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/mcp/client.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/mcp/config.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/permissions/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/permissions/manager.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/plan/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/plan/executor.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/plan/types.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/prompts/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/prompts/context.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/prompts/profile.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/query/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/session/export.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/session/meta_db.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/session/title_gen.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/skills/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/skills/loader.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/skills/matcher.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/snapshot/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/snapshot/snapshot.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/snapshot/tracker.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/team/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/team/bridge.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/team/inbox.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/team/manager.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/team/message_bus.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/team/models.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/team/recovery.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/_input_helpers.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/agent.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/ask_user.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/bash.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/browser.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/checklist.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/diff_utils.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/file_read.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/file_write.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/glob.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/lint.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/memory.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/skill.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/switch_mode.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/team.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/tools/web_search.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/types/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/core/crabcode_core/utf8_sanitize.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/acp/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/acp/session.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/acp/transport.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/acp/types.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/adapter.py +0 -0
- {crabcode-0.1.1/packages/gateway/crabcode_gateway/grpc_ → crabcode-0.1.3/packages/gateway/crabcode_gateway/grpc}/__init__.py +0 -0
- {crabcode-0.1.1/packages/gateway/crabcode_gateway/grpc_ → crabcode-0.1.3/packages/gateway/crabcode_gateway/grpc}/proto/crabcode.proto +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/routes/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/routes/agent.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/gateway/crabcode_gateway/routes/health.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/crabcode_search/__init__.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/crabcode_search/background.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/crabcode_search/chunker.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/crabcode_search/indexer.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/crabcode_search/store.py +0 -0
- {crabcode-0.1.1 → crabcode-0.1.3}/packages/search/crabcode_search/tool.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crabcode
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: CrabCode - AI coding assistant in the terminal
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: anthropic>=0.40.0
|
|
@@ -12,6 +12,21 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
12
12
|
Requires-Dist: rich>=13.0.0
|
|
13
13
|
Requires-Dist: tiktoken>=0.7.0
|
|
14
14
|
Requires-Dist: typer>=0.12.0
|
|
15
|
+
Provides-Extra: all
|
|
16
|
+
Requires-Dist: agent-client-protocol>=0.9.0; extra == 'all'
|
|
17
|
+
Requires-Dist: anthropic[bedrock]>=0.40.0; extra == 'all'
|
|
18
|
+
Requires-Dist: anthropic[vertex]>=0.40.0; extra == 'all'
|
|
19
|
+
Requires-Dist: fastapi>=0.115.0; extra == 'all'
|
|
20
|
+
Requires-Dist: grpcio-health-checking>=1.65.0; extra == 'all'
|
|
21
|
+
Requires-Dist: grpcio>=1.65.0; extra == 'all'
|
|
22
|
+
Requires-Dist: httpx>=0.27.0; extra == 'all'
|
|
23
|
+
Requires-Dist: modelscope>=1.10.0; extra == 'all'
|
|
24
|
+
Requires-Dist: playwright>=1.48.0; extra == 'all'
|
|
25
|
+
Requires-Dist: sentence-transformers>=2.0.0; extra == 'all'
|
|
26
|
+
Requires-Dist: sse-starlette>=2.0.0; extra == 'all'
|
|
27
|
+
Requires-Dist: tree-sitter-language-pack>=1.0.0; extra == 'all'
|
|
28
|
+
Requires-Dist: usearch>=2.12.0; extra == 'all'
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.30.0; extra == 'all'
|
|
15
30
|
Provides-Extra: bedrock
|
|
16
31
|
Requires-Dist: anthropic[bedrock]>=0.40.0; extra == 'bedrock'
|
|
17
32
|
Provides-Extra: browser
|
|
@@ -30,12 +30,15 @@ pip install crabcode[search]
|
|
|
30
30
|
pip install crabcode[bedrock] # AWS Bedrock
|
|
31
31
|
pip install crabcode[vertex] # Google Vertex AI
|
|
32
32
|
|
|
33
|
+
# With gateway server support
|
|
34
|
+
pip install crabcode[gateway]
|
|
35
|
+
|
|
36
|
+
# Install all optional features at once
|
|
37
|
+
pip install crabcode[all]
|
|
38
|
+
|
|
33
39
|
# Combine extras
|
|
34
40
|
pip install crabcode[search,bedrock]
|
|
35
|
-
# Example: browser + search
|
|
36
41
|
pip install crabcode[browser,search]
|
|
37
|
-
# With gateway server support
|
|
38
|
-
pip install crabcode[gateway]
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
### Development
|
|
@@ -146,7 +149,7 @@ crabcode gateway --password secret
|
|
|
146
149
|
|
|
147
150
|
**WebSocket `/ws`** supports incoming commands (`send_message`, `permission_response`, `choice_response`, `push_context`) and outgoing event payloads — a single connection handles all interaction, making it ideal for VSCode extensions.
|
|
148
151
|
|
|
149
|
-
**gRPC** service is available when `--grpc-port` is set, with streaming `SendMessage` and `SubscribeEvents` RPCs. See `packages/gateway/crabcode_gateway/
|
|
152
|
+
**gRPC** service is available when `--grpc-port` is set, with streaming `SendMessage` and `SubscribeEvents` RPCs. See `packages/gateway/crabcode_gateway/grpc/proto/crabcode.proto` for the full service definition.
|
|
150
153
|
|
|
151
154
|
### ACP (Agent Client Protocol) Support
|
|
152
155
|
|
|
@@ -243,6 +246,9 @@ Or configure in `~/.crabcode/settings.json`:
|
|
|
243
246
|
"model": "gpt-4o",
|
|
244
247
|
"base_url": "https://api.openai.com/v1",
|
|
245
248
|
"api_key_env": "OPENAI_API_KEY",
|
|
249
|
+
"http_headers": {
|
|
250
|
+
"X-Workspace": "crabcode"
|
|
251
|
+
},
|
|
246
252
|
"thinking_enabled": false,
|
|
247
253
|
"max_tokens": 16384
|
|
248
254
|
},
|
|
@@ -260,12 +266,17 @@ Or configure in `~/.crabcode/settings.json`:
|
|
|
260
266
|
| `model` | Model ID | — |
|
|
261
267
|
| `base_url` | Custom API endpoint (for routers or local deployments) | — |
|
|
262
268
|
| `api_key_env` | **Name** of the env var that holds the API key (not the key itself) | `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` |
|
|
269
|
+
| `http_headers` | Extra HTTP headers to attach to every API request for this config | `{}` |
|
|
263
270
|
| `format` | Wire format for router mode: `anthropic` \| `openai` \| `codex` | — |
|
|
264
271
|
| `thinking_enabled` | Enable extended thinking (set `false` for models that don't support it) | `true` |
|
|
265
272
|
| `thinking_budget` | Thinking token budget | `10000` |
|
|
273
|
+
| `reasoning_effort` | OpenAI Responses/Codex reasoning effort; when set, this takes precedence over the `thinking_budget` mapping. Options: `none` \| `minimal` \| `low` \| `medium` \| `high` \| `xhigh` | — |
|
|
266
274
|
| `max_tokens` | Maximum output tokens | `16384` |
|
|
267
275
|
| `timeout` | API call timeout in seconds (prevents hanging on slow/unresponsive APIs) | `300` |
|
|
268
276
|
| `context_window` | Override the model's context window size (tokens). Used when auto-detection fails or is inaccurate — see [Context Window](#context-window) below. | auto-detected |
|
|
277
|
+
| `prompt_cache_key` | Prompt cache routing key for OpenAI Responses/Codex requests; defaults to `http_headers.session_id` when omitted | — |
|
|
278
|
+
| `prompt_cache_retention` | OpenAI Responses/Codex prompt cache retention policy: `in_memory` \| `24h` | — |
|
|
279
|
+
| `extra_body` | Provider-specific fields appended to the request JSON body | `{}` |
|
|
269
280
|
|
|
270
281
|
The `env` map lets you define environment variables directly in the config file — they are injected at startup so you don't need to `export` them in your shell.
|
|
271
282
|
|
|
@@ -1365,6 +1376,6 @@ crabcode/
|
|
|
1365
1376
|
│ │ ├── types.py # ACP types + tool kind mapping
|
|
1366
1377
|
│ │ └── transport.py # stdio transport (run_agent wrapper)
|
|
1367
1378
|
│ ├── routes/ # FastAPI route groups (session, agent, config, event, health)
|
|
1368
|
-
│ └──
|
|
1379
|
+
│ └── grpc/ # gRPC service + proto definition
|
|
1369
1380
|
└── tests/
|
|
1370
1381
|
```
|
|
@@ -30,12 +30,15 @@ pip install crabcode[search]
|
|
|
30
30
|
pip install crabcode[bedrock] # AWS Bedrock
|
|
31
31
|
pip install crabcode[vertex] # Google Vertex AI
|
|
32
32
|
|
|
33
|
+
# 含网关服务器支持
|
|
34
|
+
pip install crabcode[gateway]
|
|
35
|
+
|
|
36
|
+
# 一键安装全部可选特性
|
|
37
|
+
pip install crabcode[all]
|
|
38
|
+
|
|
33
39
|
# 组合安装
|
|
34
40
|
pip install crabcode[search,bedrock]
|
|
35
|
-
# 示例:浏览器 + 搜索
|
|
36
41
|
pip install crabcode[browser,search]
|
|
37
|
-
# 含网关服务器支持
|
|
38
|
-
pip install crabcode[gateway]
|
|
39
42
|
```
|
|
40
43
|
|
|
41
44
|
### 开发模式
|
|
@@ -146,7 +149,7 @@ crabcode gateway --password secret
|
|
|
146
149
|
|
|
147
150
|
**WebSocket `/ws`** 支持收发命令(`send_message`、`permission_response`、`choice_response`、`push_context`)和事件推送 —— 单个连接即可完成所有交互,适合 VSCode 扩展使用。
|
|
148
151
|
|
|
149
|
-
**gRPC** 在启用 `--grpc-port` 后可用,提供流式 `SendMessage` 和 `SubscribeEvents` RPC。完整服务定义见 `packages/gateway/crabcode_gateway/
|
|
152
|
+
**gRPC** 在启用 `--grpc-port` 后可用,提供流式 `SendMessage` 和 `SubscribeEvents` RPC。完整服务定义见 `packages/gateway/crabcode_gateway/grpc/proto/crabcode.proto`。
|
|
150
153
|
|
|
151
154
|
### ACP(Agent Client Protocol)支持
|
|
152
155
|
|
|
@@ -243,6 +246,9 @@ export AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com/
|
|
|
243
246
|
"model": "gpt-4o",
|
|
244
247
|
"base_url": "https://api.openai.com/v1",
|
|
245
248
|
"api_key_env": "OPENAI_API_KEY",
|
|
249
|
+
"http_headers": {
|
|
250
|
+
"X-Workspace": "crabcode"
|
|
251
|
+
},
|
|
246
252
|
"thinking_enabled": false,
|
|
247
253
|
"max_tokens": 16384
|
|
248
254
|
},
|
|
@@ -260,12 +266,17 @@ export AZURE_OPENAI_ENDPOINT=https://my-resource.openai.azure.com/
|
|
|
260
266
|
| `model` | 模型 ID | — |
|
|
261
267
|
| `base_url` | 自定义 API 地址(适用于第三方转发或本地部署) | — |
|
|
262
268
|
| `api_key_env` | 存放 API Key 的**环境变量名**(不是 Key 本身) | `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` |
|
|
269
|
+
| `http_headers` | 为该配置的每次 API 请求附加额外 HTTP Header | `{}` |
|
|
263
270
|
| `format` | Router 模式下的协议格式:`anthropic` \| `openai` \| `codex` \| `ollama` \| `gemini` \| `azure` | — |
|
|
264
271
|
| `thinking_enabled` | 是否启用思考模式(不支持该功能的模型需设为 `false`) | `true` |
|
|
265
272
|
| `thinking_budget` | 思考 token 预算 | `10000` |
|
|
273
|
+
| `reasoning_effort` | OpenAI Responses/Codex 推理强度;显式配置时优先于 `thinking_budget` 映射。可选:`none` \| `minimal` \| `low` \| `medium` \| `high` \| `xhigh` | — |
|
|
266
274
|
| `max_tokens` | 最大输出 token 数 | `16384` |
|
|
267
275
|
| `timeout` | API 调用超时时间(秒),防止网络卡住时无限等待 | `300` |
|
|
268
276
|
| `context_window` | 覆盖模型的上下文窗口大小(token 数)。当自动检测失败或不准确时使用——详见下方[上下文窗口管理](#上下文窗口管理)。 | 自动检测 |
|
|
277
|
+
| `prompt_cache_key` | OpenAI Responses/Codex 请求的 Prompt Cache 路由 key;未配置时默认使用 `http_headers.session_id` | — |
|
|
278
|
+
| `prompt_cache_retention` | OpenAI Responses/Codex Prompt Cache 保留策略:`in_memory` \| `24h` | — |
|
|
279
|
+
| `extra_body` | 追加到请求 JSON body 的 provider 专用字段 | `{}` |
|
|
269
280
|
|
|
270
281
|
`env` 字段用于直接在配置文件中定义环境变量,启动时会自动注入,无需在 shell 中 `export`。
|
|
271
282
|
|
|
@@ -1361,6 +1372,6 @@ crabcode/
|
|
|
1361
1372
|
│ │ ├── types.py # ACP 类型定义 + 工具类型映射
|
|
1362
1373
|
│ │ └── transport.py # stdio 传输层(run_agent 封装)
|
|
1363
1374
|
│ ├── routes/ # FastAPI 路由组(session、agent、config、event、health)
|
|
1364
|
-
│ └──
|
|
1375
|
+
│ └── grpc/ # gRPC 服务 + proto 定义
|
|
1365
1376
|
└── tests/
|
|
1366
1377
|
```
|
|
@@ -31,6 +31,7 @@ def main(
|
|
|
31
31
|
cwd: Optional[str] = typer.Option(None, "--cwd", help="Working directory"),
|
|
32
32
|
resume: Optional[str] = typer.Option(None, "-r", "--resume", help="Resume a session by ID"),
|
|
33
33
|
continue_last: bool = typer.Option(False, "-c", "--continue", help="Continue most recent session"),
|
|
34
|
+
image: Optional[list[str]] = typer.Option(None, "-i", "--image", help="Image file(s) to attach (repeatable)"),
|
|
34
35
|
) -> None:
|
|
35
36
|
"""CrabCode — AI coding assistant in the terminal."""
|
|
36
37
|
from crabcode_core.types.config import ApiConfig, CrabCodeSettings
|
|
@@ -51,6 +52,8 @@ def main(
|
|
|
51
52
|
settings.api.base_url = file_settings.api.base_url
|
|
52
53
|
if file_settings.api.api_key_env:
|
|
53
54
|
settings.api.api_key_env = file_settings.api.api_key_env
|
|
55
|
+
if file_settings.api.http_headers:
|
|
56
|
+
settings.api.http_headers = dict(file_settings.api.http_headers)
|
|
54
57
|
if file_settings.api.format:
|
|
55
58
|
settings.api.format = file_settings.api.format
|
|
56
59
|
if not file_settings.api.thinking_enabled:
|
|
@@ -94,6 +97,30 @@ def main(
|
|
|
94
97
|
err=True,
|
|
95
98
|
)
|
|
96
99
|
|
|
100
|
+
# Read image files into base64 attachments
|
|
101
|
+
image_attachments: list[dict[str, str]] | None = None
|
|
102
|
+
if image:
|
|
103
|
+
import base64
|
|
104
|
+
import mimetypes
|
|
105
|
+
from pathlib import Path
|
|
106
|
+
|
|
107
|
+
image_attachments = []
|
|
108
|
+
for img_path_str in image:
|
|
109
|
+
img_path = Path(img_path_str)
|
|
110
|
+
if not img_path.exists():
|
|
111
|
+
typer.echo(f"Error: image file not found: {img_path_str}", err=True)
|
|
112
|
+
raise typer.Exit(1)
|
|
113
|
+
if img_path.stat().st_size > 20 * 1024 * 1024:
|
|
114
|
+
typer.echo(f"Error: image file too large (>20MB): {img_path_str}", err=True)
|
|
115
|
+
raise typer.Exit(1)
|
|
116
|
+
media_type = mimetypes.guess_type(img_path_str)[0] or "image/png"
|
|
117
|
+
if not media_type.startswith("image/"):
|
|
118
|
+
typer.echo(f"Error: not an image file: {img_path_str}", err=True)
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
with open(img_path, "rb") as f:
|
|
121
|
+
data = base64.b64encode(f.read()).decode("ascii")
|
|
122
|
+
image_attachments.append({"media_type": media_type, "data": data})
|
|
123
|
+
|
|
97
124
|
stdin_text = ""
|
|
98
125
|
if not sys.stdin.isatty():
|
|
99
126
|
stdin_text = sys.stdin.read().strip()
|
|
@@ -105,12 +132,12 @@ def main(
|
|
|
105
132
|
raise typer.Exit(1)
|
|
106
133
|
|
|
107
134
|
from crabcode_cli.pipe import run_pipe
|
|
108
|
-
asyncio.run(run_pipe(text, settings=settings, cwd=work_dir))
|
|
135
|
+
asyncio.run(run_pipe(text, settings=settings, cwd=work_dir, images=image_attachments))
|
|
109
136
|
return
|
|
110
137
|
|
|
111
138
|
if prompt:
|
|
112
139
|
from crabcode_cli.pipe import run_pipe
|
|
113
|
-
asyncio.run(run_pipe(prompt, settings=settings, cwd=work_dir))
|
|
140
|
+
asyncio.run(run_pipe(prompt, settings=settings, cwd=work_dir, images=image_attachments))
|
|
114
141
|
return
|
|
115
142
|
|
|
116
143
|
resume_id: str | None = None
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
|
|
8
9
|
from crabcode_core.events import CoreSession
|
|
@@ -21,16 +22,56 @@ from crabcode_core.types.event import (
|
|
|
21
22
|
TurnCompleteEvent,
|
|
22
23
|
)
|
|
23
24
|
|
|
25
|
+
_DEBUG_TOOL_PAYLOAD = os.getenv("CRABCODE_DEBUG_TOOL_PAYLOAD", "").strip().lower() in {
|
|
26
|
+
"1",
|
|
27
|
+
"true",
|
|
28
|
+
"yes",
|
|
29
|
+
"on",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _format_token_count(tokens: int) -> str:
|
|
34
|
+
if tokens >= 1_000_000:
|
|
35
|
+
return f"{tokens / 1_000_000:.1f}M"
|
|
36
|
+
if tokens >= 1_000:
|
|
37
|
+
return f"{tokens // 1_000}k"
|
|
38
|
+
return str(tokens)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _format_percent(percent: float) -> str:
|
|
42
|
+
rounded = round(percent)
|
|
43
|
+
if abs(percent - rounded) < 0.05:
|
|
44
|
+
return f"{rounded}%"
|
|
45
|
+
return f"{percent:.1f}%"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _format_context_usage(event: TurnCompleteEvent) -> str | None:
|
|
49
|
+
used = max(0, int(getattr(event, "context_used_tokens", 0) or 0))
|
|
50
|
+
window = max(0, int(getattr(event, "context_window_tokens", 0) or 0))
|
|
51
|
+
if not used and not window:
|
|
52
|
+
return None
|
|
53
|
+
if not window:
|
|
54
|
+
return f"Context: {_format_token_count(used)} tokens used (window unknown)"
|
|
55
|
+
|
|
56
|
+
used_percent = max(0.0, float(getattr(event, "context_used_percent", 0.0) or 0.0))
|
|
57
|
+
remaining_percent = max(0.0, 100.0 - used_percent)
|
|
58
|
+
return (
|
|
59
|
+
f"Context: {_format_percent(used_percent)} used "
|
|
60
|
+
f"({_format_percent(remaining_percent)} remaining) · "
|
|
61
|
+
f"{_format_token_count(used)} tokens used of {_format_token_count(window)}"
|
|
62
|
+
)
|
|
63
|
+
|
|
24
64
|
|
|
25
65
|
async def run_pipe(
|
|
26
66
|
prompt: str,
|
|
27
67
|
settings: CrabCodeSettings | None = None,
|
|
28
68
|
cwd: str = ".",
|
|
69
|
+
images: list[dict[str, str]] | None = None,
|
|
29
70
|
) -> None:
|
|
30
71
|
"""Run a single prompt through the core and print the response."""
|
|
31
72
|
session = CoreSession(cwd=cwd, settings=settings)
|
|
32
73
|
try:
|
|
33
|
-
async for event in session.send_message(prompt):
|
|
74
|
+
async for event in session.send_message(prompt, images=images):
|
|
34
75
|
if isinstance(event, StreamTextEvent):
|
|
35
76
|
sys.stdout.write(safe_utf8_str(event.text))
|
|
36
77
|
sys.stdout.flush()
|
|
@@ -38,6 +79,9 @@ async def run_pipe(
|
|
|
38
79
|
pass
|
|
39
80
|
elif isinstance(event, ToolUseEvent):
|
|
40
81
|
sys.stderr.write(f"\n[Tool: {event.tool_name}]\n")
|
|
82
|
+
if _DEBUG_TOOL_PAYLOAD:
|
|
83
|
+
pretty = json.dumps(event.tool_input, ensure_ascii=False, indent=2)
|
|
84
|
+
sys.stderr.write(f"{pretty}\n")
|
|
41
85
|
sys.stderr.flush()
|
|
42
86
|
elif isinstance(event, PermissionRequestEvent):
|
|
43
87
|
await session.respond_permission(
|
|
@@ -64,7 +108,10 @@ async def run_pipe(
|
|
|
64
108
|
if not event.recoverable:
|
|
65
109
|
sys.exit(1)
|
|
66
110
|
elif isinstance(event, TurnCompleteEvent):
|
|
67
|
-
|
|
111
|
+
usage_text = _format_context_usage(event)
|
|
112
|
+
if usage_text:
|
|
113
|
+
sys.stderr.write(f"\n{usage_text}\n")
|
|
114
|
+
sys.stderr.flush()
|
|
68
115
|
finally:
|
|
69
116
|
await session.close()
|
|
70
117
|
|
|
@@ -4,11 +4,13 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import contextlib
|
|
7
|
+
import json
|
|
7
8
|
import os
|
|
8
9
|
import sys
|
|
9
10
|
import time
|
|
10
11
|
from datetime import datetime
|
|
11
12
|
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
12
14
|
|
|
13
15
|
from prompt_toolkit import PromptSession
|
|
14
16
|
from prompt_toolkit.completion import Completer, Completion
|
|
@@ -66,6 +68,42 @@ _ANSI_ENABLED = _supports_ansi_output()
|
|
|
66
68
|
console = Console(no_color=not _ANSI_ENABLED, force_terminal=_ANSI_ENABLED)
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
def _format_token_count(tokens: int) -> str:
|
|
72
|
+
if tokens >= 1_000_000:
|
|
73
|
+
return f"{tokens / 1_000_000:.1f}M"
|
|
74
|
+
if tokens >= 1_000:
|
|
75
|
+
return f"{tokens // 1_000}k"
|
|
76
|
+
return str(tokens)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _format_percent(percent: float) -> str:
|
|
80
|
+
rounded = round(percent)
|
|
81
|
+
if abs(percent - rounded) < 0.05:
|
|
82
|
+
return f"{rounded}%"
|
|
83
|
+
return f"{percent:.1f}%"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _render_context_usage(event: TurnCompleteEvent) -> None:
|
|
87
|
+
used = max(0, int(getattr(event, "context_used_tokens", 0) or 0))
|
|
88
|
+
window = max(0, int(getattr(event, "context_window_tokens", 0) or 0))
|
|
89
|
+
if not used and not window:
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if window:
|
|
93
|
+
used_percent = max(0.0, float(getattr(event, "context_used_percent", 0.0) or 0.0))
|
|
94
|
+
remaining_percent = max(0.0, 100.0 - used_percent)
|
|
95
|
+
console.print(
|
|
96
|
+
" [dim]Context: "
|
|
97
|
+
f"{_format_percent(used_percent)} used "
|
|
98
|
+
f"({_format_percent(remaining_percent)} remaining) · "
|
|
99
|
+
f"{_format_token_count(used)} tokens used of {_format_token_count(window)}[/]"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
console.print(
|
|
103
|
+
f" [dim]Context: {_format_token_count(used)} tokens used (window unknown)[/]"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
69
107
|
# Slash commands with their arguments for auto-completion
|
|
70
108
|
_SLASH_COMMANDS: dict[str, list[str]] = {
|
|
71
109
|
"/help": [],
|
|
@@ -98,6 +136,7 @@ _SLASH_COMMANDS: dict[str, list[str]] = {
|
|
|
98
136
|
"/resume": [], # Dynamic: session IDs
|
|
99
137
|
"/exit": [],
|
|
100
138
|
"/quit": [],
|
|
139
|
+
"/image": [], # Dynamic: file path
|
|
101
140
|
}
|
|
102
141
|
|
|
103
142
|
|
|
@@ -244,6 +283,7 @@ class _CrabCodeCompleter(Completer):
|
|
|
244
283
|
"/revert": "revert files + conversation to checkpoint",
|
|
245
284
|
"/undo": "undo last checkpoint (revert files + conversation)",
|
|
246
285
|
"/resume": "resume session",
|
|
286
|
+
"/image": "attach image to next message",
|
|
247
287
|
"/exit": "exit CrabCode",
|
|
248
288
|
"/quit": "exit CrabCode",
|
|
249
289
|
}
|
|
@@ -516,6 +556,27 @@ def _tool_summary(name: str, inp: dict) -> str:
|
|
|
516
556
|
return (raw[:200] + "…") if len(raw) > 200 else raw
|
|
517
557
|
|
|
518
558
|
|
|
559
|
+
def _debug_tool_payload_enabled() -> bool:
|
|
560
|
+
return os.getenv("CRABCODE_DEBUG_TOOL_PAYLOAD", "").strip().lower() in {
|
|
561
|
+
"1",
|
|
562
|
+
"true",
|
|
563
|
+
"yes",
|
|
564
|
+
"on",
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _render_full_tool_payload(tool_name: str, tool_input: dict) -> None:
|
|
569
|
+
payload = json.dumps(tool_input, ensure_ascii=False, indent=2)
|
|
570
|
+
console.print(
|
|
571
|
+
Panel(
|
|
572
|
+
Text(payload, style="dim"),
|
|
573
|
+
title=f"[bold yellow]Debug Payload: {tool_name}[/]",
|
|
574
|
+
border_style="yellow",
|
|
575
|
+
expand=False,
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
|
|
519
580
|
def _render_saved_partial_reply(text: str) -> None:
|
|
520
581
|
"""Echo assistant text that was persisted after interrupt (visible in scrollback)."""
|
|
521
582
|
body = text.rstrip()
|
|
@@ -558,6 +619,8 @@ def _render_tool_use(event: ToolUseEvent) -> None:
|
|
|
558
619
|
expand=False,
|
|
559
620
|
)
|
|
560
621
|
)
|
|
622
|
+
if _debug_tool_payload_enabled():
|
|
623
|
+
_render_full_tool_payload(event.tool_name, event.tool_input)
|
|
561
624
|
|
|
562
625
|
|
|
563
626
|
def _truncate_display(display: str, max_lines: int = 50, max_chars: int = 50_000) -> str:
|
|
@@ -699,8 +762,21 @@ def _render_session_history(messages: list[Message], max_messages: int = 50) ->
|
|
|
699
762
|
async def _prompt_permission(
|
|
700
763
|
event: PermissionRequestEvent,
|
|
701
764
|
session: CoreSession,
|
|
765
|
+
batch_state: dict | None = None,
|
|
702
766
|
) -> None:
|
|
703
767
|
"""Prompt the user for tool permission and push response to session."""
|
|
768
|
+
# If a previous request in this batch was denied, auto-deny silently
|
|
769
|
+
if batch_state and batch_state.get("denied"):
|
|
770
|
+
await session.respond_permission(
|
|
771
|
+
PermissionResponseEvent(
|
|
772
|
+
tool_use_id=event.tool_use_id, allowed=False, agent_id=event.agent_id
|
|
773
|
+
)
|
|
774
|
+
)
|
|
775
|
+
console.print(
|
|
776
|
+
f" [dim red]✗ {event.tool_name} auto-denied (batch)[/]"
|
|
777
|
+
)
|
|
778
|
+
return
|
|
779
|
+
|
|
704
780
|
summary = _tool_summary(event.tool_name, event.tool_input)
|
|
705
781
|
if event.reason:
|
|
706
782
|
summary = f"{summary}\n\nReason: {event.reason}"
|
|
@@ -713,17 +789,19 @@ async def _prompt_permission(
|
|
|
713
789
|
)
|
|
714
790
|
)
|
|
715
791
|
|
|
716
|
-
|
|
792
|
+
perm_prompt_session: PromptSession[str] = PromptSession()
|
|
717
793
|
while True:
|
|
718
794
|
try:
|
|
719
|
-
choice = await
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
).strip().lower(),
|
|
795
|
+
choice = await perm_prompt_session.prompt_async(
|
|
796
|
+
HTML(
|
|
797
|
+
f" Allow <b>{event.tool_name}</b>? "
|
|
798
|
+
"(y)es / (n)o / (a)lways allow / (f)eedback: "
|
|
799
|
+
)
|
|
725
800
|
)
|
|
801
|
+
choice = choice.strip().lower()
|
|
726
802
|
except (EOFError, KeyboardInterrupt):
|
|
803
|
+
if batch_state is not None:
|
|
804
|
+
batch_state["denied"] = True
|
|
727
805
|
await session.respond_permission(
|
|
728
806
|
PermissionResponseEvent(
|
|
729
807
|
tool_use_id=event.tool_use_id, allowed=False, agent_id=event.agent_id
|
|
@@ -739,6 +817,8 @@ async def _prompt_permission(
|
|
|
739
817
|
)
|
|
740
818
|
return
|
|
741
819
|
elif choice in ("n", "no"):
|
|
820
|
+
if batch_state is not None:
|
|
821
|
+
batch_state["denied"] = True
|
|
742
822
|
await session.respond_permission(
|
|
743
823
|
PermissionResponseEvent(
|
|
744
824
|
tool_use_id=event.tool_use_id, allowed=False, agent_id=event.agent_id
|
|
@@ -755,8 +835,28 @@ async def _prompt_permission(
|
|
|
755
835
|
)
|
|
756
836
|
)
|
|
757
837
|
return
|
|
838
|
+
elif choice in ("f", "feedback"):
|
|
839
|
+
try:
|
|
840
|
+
feedback_session: PromptSession[str] = PromptSession()
|
|
841
|
+
feedback = await feedback_session.prompt_async(
|
|
842
|
+
HTML(" Feedback (what to do instead): ")
|
|
843
|
+
)
|
|
844
|
+
feedback = feedback.strip()
|
|
845
|
+
except (EOFError, KeyboardInterrupt):
|
|
846
|
+
feedback = ""
|
|
847
|
+
if batch_state is not None:
|
|
848
|
+
batch_state["denied"] = True
|
|
849
|
+
await session.respond_permission(
|
|
850
|
+
PermissionResponseEvent(
|
|
851
|
+
tool_use_id=event.tool_use_id,
|
|
852
|
+
allowed=False,
|
|
853
|
+
agent_id=event.agent_id,
|
|
854
|
+
feedback=feedback or None,
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
return
|
|
758
858
|
else:
|
|
759
|
-
console.print(" [dim]Please enter y, n, or
|
|
859
|
+
console.print(" [dim]Please enter y, n, a, or f[/]")
|
|
760
860
|
|
|
761
861
|
|
|
762
862
|
async def _interactive_select(question: str, options: list[str], multiple: bool = False) -> list[str]:
|
|
@@ -1058,6 +1158,7 @@ async def _run_plan_executor_with_runtime_events(
|
|
|
1058
1158
|
producer = asyncio.create_task(_produce_plan_events())
|
|
1059
1159
|
forwarder = asyncio.create_task(_forward_agent_events())
|
|
1060
1160
|
active_stream_agent: str | None = None
|
|
1161
|
+
batch_state: dict = {"denied": False}
|
|
1061
1162
|
|
|
1062
1163
|
try:
|
|
1063
1164
|
while True:
|
|
@@ -1095,6 +1196,11 @@ async def _run_plan_executor_with_runtime_events(
|
|
|
1095
1196
|
console.print(
|
|
1096
1197
|
f" [{style}]agent {event.agent_id[:8]} · {event.status} · {event.title}[/]"
|
|
1097
1198
|
)
|
|
1199
|
+
elif isinstance(event, StreamModeEvent):
|
|
1200
|
+
if event.mode == "tool-input":
|
|
1201
|
+
batch_state["denied"] = False
|
|
1202
|
+
elif isinstance(event, TurnCompleteEvent):
|
|
1203
|
+
batch_state["denied"] = False
|
|
1098
1204
|
elif isinstance(event, ToolUseEvent):
|
|
1099
1205
|
_render_tool_use(event)
|
|
1100
1206
|
elif isinstance(event, ToolResultEvent):
|
|
@@ -1106,7 +1212,7 @@ async def _run_plan_executor_with_runtime_events(
|
|
|
1106
1212
|
else:
|
|
1107
1213
|
_render_tool_result(event)
|
|
1108
1214
|
elif isinstance(event, PermissionRequestEvent):
|
|
1109
|
-
await _prompt_permission(event, session)
|
|
1215
|
+
await _prompt_permission(event, session, batch_state)
|
|
1110
1216
|
elif isinstance(event, ChoiceRequestEvent):
|
|
1111
1217
|
await _prompt_choice(event, session)
|
|
1112
1218
|
elif isinstance(event, ErrorEvent):
|
|
@@ -1165,6 +1271,9 @@ async def run_repl(
|
|
|
1165
1271
|
|
|
1166
1272
|
session = CoreSession(cwd=cwd, settings=settings)
|
|
1167
1273
|
|
|
1274
|
+
# Pending image attachments for the next user message
|
|
1275
|
+
pending_images: list[dict[str, str]] = []
|
|
1276
|
+
|
|
1168
1277
|
global _display_settings
|
|
1169
1278
|
_display_settings = settings.display if settings else None
|
|
1170
1279
|
|
|
@@ -1227,6 +1336,8 @@ async def run_repl(
|
|
|
1227
1336
|
mode = getattr(session, '_agent_mode', 'agent')
|
|
1228
1337
|
if mode == "plan":
|
|
1229
1338
|
prompt_html = HTML("<b><ansiblue>[plan]</ansiblue> <ansicyan>❯ </ansicyan></b>")
|
|
1339
|
+
elif pending_images:
|
|
1340
|
+
prompt_html = HTML("<b><ansicyan>❯ 📎 </ansicyan></b>")
|
|
1230
1341
|
else:
|
|
1231
1342
|
prompt_html = HTML("<b><ansicyan>❯ </ansicyan></b>")
|
|
1232
1343
|
user_input = await prompt_session.prompt_async(prompt_html)
|
|
@@ -1270,7 +1381,7 @@ async def run_repl(
|
|
|
1270
1381
|
ctrl_c_exit.clear()
|
|
1271
1382
|
|
|
1272
1383
|
if user_input.startswith("/"):
|
|
1273
|
-
result = await _handle_command(user_input, session, settings)
|
|
1384
|
+
result = await _handle_command(user_input, session, settings, pending_images)
|
|
1274
1385
|
if result is False:
|
|
1275
1386
|
break
|
|
1276
1387
|
if isinstance(result, str):
|
|
@@ -1305,7 +1416,10 @@ async def run_repl(
|
|
|
1305
1416
|
|
|
1306
1417
|
plan_pending = False
|
|
1307
1418
|
try:
|
|
1308
|
-
|
|
1419
|
+
send_images = pending_images.copy() if pending_images else None
|
|
1420
|
+
pending_images.clear()
|
|
1421
|
+
batch_state: dict = {"denied": False}
|
|
1422
|
+
async for event in session.send_message(user_input, images=send_images):
|
|
1309
1423
|
if isinstance(event, StreamModeEvent):
|
|
1310
1424
|
if event.mode == "requesting":
|
|
1311
1425
|
spinner.start()
|
|
@@ -1318,6 +1432,7 @@ async def run_repl(
|
|
|
1318
1432
|
elif event.mode == "responding":
|
|
1319
1433
|
await _stop_spinner_with_thinking()
|
|
1320
1434
|
elif event.mode == "tool-input":
|
|
1435
|
+
batch_state["denied"] = False
|
|
1321
1436
|
await _stop_spinner_with_thinking()
|
|
1322
1437
|
if streamed_text:
|
|
1323
1438
|
sys.stdout.write("\n")
|
|
@@ -1378,7 +1493,7 @@ async def run_repl(
|
|
|
1378
1493
|
sys.stdout.write("\n")
|
|
1379
1494
|
sys.stdout.flush()
|
|
1380
1495
|
streamed_text = ""
|
|
1381
|
-
await _prompt_permission(event, session)
|
|
1496
|
+
await _prompt_permission(event, session, batch_state)
|
|
1382
1497
|
|
|
1383
1498
|
elif isinstance(event, ChoiceRequestEvent):
|
|
1384
1499
|
await _stop_spinner_with_thinking()
|
|
@@ -1468,6 +1583,11 @@ async def run_repl(
|
|
|
1468
1583
|
|
|
1469
1584
|
elif isinstance(event, TurnCompleteEvent):
|
|
1470
1585
|
await _stop_spinner_with_thinking()
|
|
1586
|
+
if streamed_text:
|
|
1587
|
+
sys.stdout.write("\n")
|
|
1588
|
+
sys.stdout.flush()
|
|
1589
|
+
streamed_text = ""
|
|
1590
|
+
_render_context_usage(event)
|
|
1471
1591
|
if getattr(session, '_current_plan', None) and session.agent_mode == "agent":
|
|
1472
1592
|
plan_pending = True
|
|
1473
1593
|
|
|
@@ -1568,6 +1688,7 @@ async def _handle_command(
|
|
|
1568
1688
|
command: str,
|
|
1569
1689
|
session: CoreSession,
|
|
1570
1690
|
settings: CrabCodeSettings | None,
|
|
1691
|
+
pending_images: list[dict[str, str]] | None = None,
|
|
1571
1692
|
) -> bool | str:
|
|
1572
1693
|
"""Handle slash commands.
|
|
1573
1694
|
|
|
@@ -1583,6 +1704,42 @@ async def _handle_command(
|
|
|
1583
1704
|
# --- Built-in commands take priority over skill names ---
|
|
1584
1705
|
skills = getattr(session, "skills", [])
|
|
1585
1706
|
|
|
1707
|
+
if cmd == "/image":
|
|
1708
|
+
if not arg:
|
|
1709
|
+
if pending_images is not None and pending_images:
|
|
1710
|
+
console.print(f"[dim]{len(pending_images)} image(s) attached for next message.[/]")
|
|
1711
|
+
else:
|
|
1712
|
+
console.print("[dim]Usage: /image <path> [path2 ...][/]")
|
|
1713
|
+
return True
|
|
1714
|
+
import base64
|
|
1715
|
+
import mimetypes
|
|
1716
|
+
from pathlib import Path
|
|
1717
|
+
|
|
1718
|
+
paths = arg.split()
|
|
1719
|
+
added = 0
|
|
1720
|
+
for p in paths:
|
|
1721
|
+
img_path = Path(p).expanduser()
|
|
1722
|
+
if not img_path.exists():
|
|
1723
|
+
console.print(f"[bold red]File not found: {p}[/]")
|
|
1724
|
+
continue
|
|
1725
|
+
if img_path.stat().st_size > 20 * 1024 * 1024:
|
|
1726
|
+
console.print(f"[bold red]File too large (>20MB): {p}[/]")
|
|
1727
|
+
continue
|
|
1728
|
+
media_type = mimetypes.guess_type(str(img_path))[0] or "image/png"
|
|
1729
|
+
if not media_type.startswith("image/"):
|
|
1730
|
+
console.print(f"[bold red]Not an image: {p} (detected: {media_type})[/]")
|
|
1731
|
+
continue
|
|
1732
|
+
with open(img_path, "rb") as f:
|
|
1733
|
+
data = base64.b64encode(f.read()).decode("ascii")
|
|
1734
|
+
if pending_images is not None:
|
|
1735
|
+
pending_images.append({"media_type": media_type, "data": data})
|
|
1736
|
+
added += 1
|
|
1737
|
+
size_kb = img_path.stat().st_size / 1024
|
|
1738
|
+
console.print(f" [green]✓[/] Attached: {p} ({media_type}, {size_kb:.0f}KB)")
|
|
1739
|
+
if added and pending_images is not None:
|
|
1740
|
+
console.print(f"[dim]{len(pending_images)} image(s) will be sent with your next message.[/]")
|
|
1741
|
+
return True
|
|
1742
|
+
|
|
1586
1743
|
if cmd == "/help":
|
|
1587
1744
|
skills_section = ""
|
|
1588
1745
|
if skills:
|
|
@@ -1629,6 +1786,7 @@ async def _handle_command(
|
|
|
1629
1786
|
"[bold]/revert <id|#>[/] — revert files + conversation to a checkpoint\n"
|
|
1630
1787
|
"[bold]/undo[/] — revert last checkpoint (files + conversation)\n"
|
|
1631
1788
|
"[bold]/resume <id>[/] — resume a previous session\n"
|
|
1789
|
+
"[bold]/image <path>[/] — attach image(s) to your next message\n"
|
|
1632
1790
|
"[bold]/exit[/] — exit CrabCode\n"
|
|
1633
1791
|
f"[bold]Ctrl+C[/] — interrupt; press again within {_CTRL_C_EXIT_WINDOW_S:.0f}s to exit\n"
|
|
1634
1792
|
"\n"
|