dostuff 0.2.1__tar.gz → 0.2.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.
- {dostuff-0.2.1 → dostuff-0.2.3}/PKG-INFO +20 -14
- {dostuff-0.2.1 → dostuff-0.2.3}/README.md +18 -11
- dostuff-0.2.3/dostuff/helpers/agent/get_model_token_limit.py +54 -0
- dostuff-0.2.3/dostuff/helpers/mcp/token_store.py +81 -0
- dostuff-0.2.3/dostuff/helpers/models/opencode.py +200 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_client.py +90 -36
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/templates/config.example.yaml +1 -1
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/PKG-INFO +20 -14
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/requires.txt +1 -3
- {dostuff-0.2.1 → dostuff-0.2.3}/pyproject.toml +3 -5
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_mcp_oauth.py +110 -1
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_opencode.py +43 -0
- dostuff-0.2.1/dostuff/helpers/agent/get_model_token_limit.py +0 -29
- dostuff-0.2.1/dostuff/helpers/mcp/token_store.py +0 -46
- dostuff-0.2.1/dostuff/helpers/models/opencode.py +0 -76
- {dostuff-0.2.1 → dostuff-0.2.3}/LICENSE +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/__init__.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/__main__.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/call_agent.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/instructions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/loop.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/permissions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/run_tool.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/cli.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/cli_tui.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/config.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/headless.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/append_step.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/constants.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/consume_stream.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/context_usage.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/environment_context.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/file_mentions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/load_identity.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/load_project_instructions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/loop_check_cancelled.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/loop_emit.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/manage_context.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/model_cost.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/pastes.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/quick_memory.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/slash_suggestion.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/tool_output.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/mcp/load_mcp_config.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/mcp/mcp_oauth.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/mcp/server_config.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/memory/export_session.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/memory/resolve_memory_operation.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/memory/shell_step.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/models/add_model_entry.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/models/list_models.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/skills/custom_commands.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/skills/discover_skills.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/skills/skill_command.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/diff.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/generate_tool_schema.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/project_root.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/resolve_safe_path.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/__init__.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/doctor.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/emit.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/prompt_history.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/terminal_signals.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/checkpoints.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/exceptions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_client_registration_store.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_tool_registry_store.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/chroma.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/episodic_memory_store.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/semantic_memory_store.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/session_store.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/stores.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/types.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/model.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/tracing.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/worktree.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/memory/__init__.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/setup_wizard.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/skills.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/templates/env.example +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/__init__.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/bash/bash_command.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/definitions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/delegate_to_subagent.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/delete_file.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/edit_file.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/list_files.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/read_file.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/write_file.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/get_current_datetime.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/mcp/call_mcp_tool.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/mcp/get_mcp_tool_details.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/mcp/search_mcp_tools.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/memory/save_episode.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/memory/save_memory.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/search/glob.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/search/grep.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/search/walk.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/settings.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/todo_write.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/web_fetch.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/web_search.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/SOURCES.txt +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/dependency_links.txt +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/entry_points.txt +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/top_level.txt +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/setup.cfg +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_checkpoints_todos_caching.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_cli_commands.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_config_memory_mcp.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_file_mentions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_file_tools.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_loop.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_mcp_e2e.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_optional_memory_and_instructions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_pastes.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_permissions.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_roadmap_021.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_session_store.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_settings_tools.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_setup_wizard.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_skill_command.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_slash_suggestion.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_subagent.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_terminal_signals.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_tool_schemas.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_tui.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_web_fetch.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_web_search.py +0 -0
- {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_worktree.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dostuff
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: DoStuff - a terminal coding agent for any LLM: TUI and headless mode, permissions, MCP, skills, web tools, sessions and git worktrees
|
|
5
5
|
Author-email: Varun Kumawat <varunkumawatleap2@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -32,6 +32,7 @@ Requires-Dist: opentelemetry-api>=1.20
|
|
|
32
32
|
Requires-Dist: opentelemetry-sdk>=1.20
|
|
33
33
|
Requires-Dist: textual>=0.50.0
|
|
34
34
|
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20
|
|
35
|
+
Requires-Dist: keyring>=24
|
|
35
36
|
Provides-Extra: memory
|
|
36
37
|
Requires-Dist: chromadb>=1.5.0; extra == "memory"
|
|
37
38
|
Provides-Extra: search
|
|
@@ -40,8 +41,6 @@ Provides-Extra: full
|
|
|
40
41
|
Requires-Dist: dostuff[memory,search]; extra == "full"
|
|
41
42
|
Requires-Dist: onnxruntime>=1.16; extra == "full"
|
|
42
43
|
Requires-Dist: grpcio>=1.52; extra == "full"
|
|
43
|
-
Provides-Extra: keyring
|
|
44
|
-
Requires-Dist: keyring>=24; extra == "keyring"
|
|
45
44
|
Provides-Extra: dev
|
|
46
45
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
47
46
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
@@ -110,15 +109,19 @@ pip install dostuff
|
|
|
110
109
|
|
|
111
110
|
This installs the `dostuff` command on your `PATH`. Done.
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
a large download, so it's an extra:
|
|
112
|
+
Optional features come as extras:
|
|
115
113
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
| Install | Adds |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `pip install dostuff` | Everything except the extras below, including MCP (with OAuth tokens kept in the OS keyring) |
|
|
117
|
+
| `pip install "dostuff[memory]"` | Long-term memory (facts and past decisions across sessions) and semantic MCP tool search, via ChromaDB. Large download |
|
|
118
|
+
| `pip install "dostuff[search]"` | Steadier `web_search` through the `ddgs` package (several engines) instead of DuckDuckGo's HTML page |
|
|
119
|
+
| `pip install "dostuff[full]"` | `memory` + `search` |
|
|
120
|
+
| `pip install "dostuff[dev]"` | `pytest` and `mypy`, for working on dostuff itself |
|
|
119
121
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
Extras combine, e.g. `pip install "dostuff[memory,search]"`. Without `memory` everything else
|
|
123
|
+
works: the memory tools are hidden from the model and MCP tool search matches keywords instead of
|
|
124
|
+
meaning. `dostuff doctor` shows whether memory is on.
|
|
122
125
|
|
|
123
126
|
### Other install methods
|
|
124
127
|
|
|
@@ -126,6 +129,7 @@ For an isolated install with [pipx](https://pypa.github.io/pipx/):
|
|
|
126
129
|
|
|
127
130
|
```bash
|
|
128
131
|
pipx install dostuff
|
|
132
|
+
pipx install "dostuff[full]" # with extras
|
|
129
133
|
```
|
|
130
134
|
|
|
131
135
|
For development (editable mode, from a clone):
|
|
@@ -135,8 +139,8 @@ git clone https://github.com/kVarunkk/DoStuff.git
|
|
|
135
139
|
cd DoStuff
|
|
136
140
|
pip install -e . # core
|
|
137
141
|
pip install -e ".[memory]" # + long-term memory (chromadb)
|
|
138
|
-
pip install -e ".[
|
|
139
|
-
pip install -e ".[
|
|
142
|
+
pip install -e ".[search]" # + ddgs web search
|
|
143
|
+
pip install -e ".[full]" # + memory + search
|
|
140
144
|
pip install -e ".[dev]" # + pytest + mypy
|
|
141
145
|
|
|
142
146
|
python -m pytest -q tests
|
|
@@ -348,7 +352,7 @@ models:
|
|
|
348
352
|
loop:
|
|
349
353
|
max_iterations: 100
|
|
350
354
|
subagent_max_iterations: 25
|
|
351
|
-
# context_window: 128000 # override when litellm
|
|
355
|
+
# context_window: 128000 # override when dostuff/litellm don't know the model
|
|
352
356
|
max_tool_output_chars: 30000 # longer tool results keep head + tail
|
|
353
357
|
max_retries: 6 # retries on rate limits, timeouts, 5xx (0 = none)
|
|
354
358
|
retry_max_delay: 60 # cap on each wait, seconds; Retry-After is honored
|
|
@@ -412,6 +416,8 @@ or `/model` inside the TUI. Model lists come from litellm's catalog (chat models
|
|
|
412
416
|
|
|
413
417
|
OpenCode models (`opencode/<id>` for Zen, `opencode-go/<id>` for a Go subscription) use the ids from the [OpenCode docs](https://opencode.ai/docs/zen/). litellm has no OpenCode provider, so DoStuff sends each model to the gateway endpoint for its family: Claude models (and a few Qwen / MiniMax models) to the Anthropic Messages endpoint, GPT / Grok / Muse models to the Responses endpoint, Gemini models to the Gemini endpoint, and all other models to chat completions. One console key in `OPENCODE_API_KEY` works for Zen and Go. OpenCode only allows its free models from its own app, so use paid models or Go.
|
|
414
418
|
|
|
419
|
+
The gateways don't publish a model's context window in `/v1/models`, so `helpers/models/opencode.py` carries one per model, taken from [models.dev](https://models.dev)'s `opencode` and `opencode-go` entries (the same id can differ per gateway: `opencode/minimax-m3` is 512k, `opencode-go/minimax-m3` is 1M). `/context`, the status bar and auto-compaction size themselves from it; set `loop.context_window` to override.
|
|
420
|
+
|
|
415
421
|
---
|
|
416
422
|
|
|
417
423
|
## MCP servers
|
|
@@ -462,7 +468,7 @@ MCP servers connect concurrently in the background, so the TUI never blanks duri
|
|
|
462
468
|
|
|
463
469
|
MCP tools are named `<server>__<tool>` (e.g. `github__create_issue`), so two servers can expose a tool with the same name. `search_mcp_tools` only returns tools of servers that are connected right now.
|
|
464
470
|
|
|
465
|
-
**OAuth (HTTP servers):** a browser login opens when the server answers 401. The flow uses PKCE, a random `state` and an RFC 8707 `resource` indicator; the local callback on port 8085 times out after 120 s. Tokens are
|
|
471
|
+
**OAuth (HTTP servers):** a browser login opens when the server answers 401. The flow uses PKCE, a random `state` and an RFC 8707 `resource` indicator; the local callback on port 8085 times out after 120 s. Tokens are stored in the OS keyring (memory only if no keyring backend is available), never in a plain file. Expired access tokens are refreshed with the refresh token, before a request or after a 401, without opening the browser; you sign in again only when the refresh token itself is rejected.
|
|
466
472
|
|
|
467
473
|
To force a re-registration, delete:
|
|
468
474
|
|
|
@@ -61,15 +61,19 @@ pip install dostuff
|
|
|
61
61
|
|
|
62
62
|
This installs the `dostuff` command on your `PATH`. Done.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
a large download, so it's an extra:
|
|
64
|
+
Optional features come as extras:
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
| Install | Adds |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `pip install dostuff` | Everything except the extras below, including MCP (with OAuth tokens kept in the OS keyring) |
|
|
69
|
+
| `pip install "dostuff[memory]"` | Long-term memory (facts and past decisions across sessions) and semantic MCP tool search, via ChromaDB. Large download |
|
|
70
|
+
| `pip install "dostuff[search]"` | Steadier `web_search` through the `ddgs` package (several engines) instead of DuckDuckGo's HTML page |
|
|
71
|
+
| `pip install "dostuff[full]"` | `memory` + `search` |
|
|
72
|
+
| `pip install "dostuff[dev]"` | `pytest` and `mypy`, for working on dostuff itself |
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
Extras combine, e.g. `pip install "dostuff[memory,search]"`. Without `memory` everything else
|
|
75
|
+
works: the memory tools are hidden from the model and MCP tool search matches keywords instead of
|
|
76
|
+
meaning. `dostuff doctor` shows whether memory is on.
|
|
73
77
|
|
|
74
78
|
### Other install methods
|
|
75
79
|
|
|
@@ -77,6 +81,7 @@ For an isolated install with [pipx](https://pypa.github.io/pipx/):
|
|
|
77
81
|
|
|
78
82
|
```bash
|
|
79
83
|
pipx install dostuff
|
|
84
|
+
pipx install "dostuff[full]" # with extras
|
|
80
85
|
```
|
|
81
86
|
|
|
82
87
|
For development (editable mode, from a clone):
|
|
@@ -86,8 +91,8 @@ git clone https://github.com/kVarunkk/DoStuff.git
|
|
|
86
91
|
cd DoStuff
|
|
87
92
|
pip install -e . # core
|
|
88
93
|
pip install -e ".[memory]" # + long-term memory (chromadb)
|
|
89
|
-
pip install -e ".[
|
|
90
|
-
pip install -e ".[
|
|
94
|
+
pip install -e ".[search]" # + ddgs web search
|
|
95
|
+
pip install -e ".[full]" # + memory + search
|
|
91
96
|
pip install -e ".[dev]" # + pytest + mypy
|
|
92
97
|
|
|
93
98
|
python -m pytest -q tests
|
|
@@ -299,7 +304,7 @@ models:
|
|
|
299
304
|
loop:
|
|
300
305
|
max_iterations: 100
|
|
301
306
|
subagent_max_iterations: 25
|
|
302
|
-
# context_window: 128000 # override when litellm
|
|
307
|
+
# context_window: 128000 # override when dostuff/litellm don't know the model
|
|
303
308
|
max_tool_output_chars: 30000 # longer tool results keep head + tail
|
|
304
309
|
max_retries: 6 # retries on rate limits, timeouts, 5xx (0 = none)
|
|
305
310
|
retry_max_delay: 60 # cap on each wait, seconds; Retry-After is honored
|
|
@@ -363,6 +368,8 @@ or `/model` inside the TUI. Model lists come from litellm's catalog (chat models
|
|
|
363
368
|
|
|
364
369
|
OpenCode models (`opencode/<id>` for Zen, `opencode-go/<id>` for a Go subscription) use the ids from the [OpenCode docs](https://opencode.ai/docs/zen/). litellm has no OpenCode provider, so DoStuff sends each model to the gateway endpoint for its family: Claude models (and a few Qwen / MiniMax models) to the Anthropic Messages endpoint, GPT / Grok / Muse models to the Responses endpoint, Gemini models to the Gemini endpoint, and all other models to chat completions. One console key in `OPENCODE_API_KEY` works for Zen and Go. OpenCode only allows its free models from its own app, so use paid models or Go.
|
|
365
370
|
|
|
371
|
+
The gateways don't publish a model's context window in `/v1/models`, so `helpers/models/opencode.py` carries one per model, taken from [models.dev](https://models.dev)'s `opencode` and `opencode-go` entries (the same id can differ per gateway: `opencode/minimax-m3` is 512k, `opencode-go/minimax-m3` is 1M). `/context`, the status bar and auto-compaction size themselves from it; set `loop.context_window` to override.
|
|
372
|
+
|
|
366
373
|
---
|
|
367
374
|
|
|
368
375
|
## MCP servers
|
|
@@ -413,7 +420,7 @@ MCP servers connect concurrently in the background, so the TUI never blanks duri
|
|
|
413
420
|
|
|
414
421
|
MCP tools are named `<server>__<tool>` (e.g. `github__create_issue`), so two servers can expose a tool with the same name. `search_mcp_tools` only returns tools of servers that are connected right now.
|
|
415
422
|
|
|
416
|
-
**OAuth (HTTP servers):** a browser login opens when the server answers 401. The flow uses PKCE, a random `state` and an RFC 8707 `resource` indicator; the local callback on port 8085 times out after 120 s. Tokens are
|
|
423
|
+
**OAuth (HTTP servers):** a browser login opens when the server answers 401. The flow uses PKCE, a random `state` and an RFC 8707 `resource` indicator; the local callback on port 8085 times out after 120 s. Tokens are stored in the OS keyring (memory only if no keyring backend is available), never in a plain file. Expired access tokens are refreshed with the refresh token, before a request or after a 401, without opening the browser; you sign in again only when the refresh token itself is rejected.
|
|
417
424
|
|
|
418
425
|
To force a re-registration, delete:
|
|
419
426
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import litellm
|
|
2
|
+
from dostuff.config import get_config
|
|
3
|
+
from dostuff.helpers.models.opencode import context_window, is_opencode
|
|
4
|
+
from dostuff.lib.model import get_litellm_model, get_model
|
|
5
|
+
|
|
6
|
+
FALLBACK_CONTEXT_WINDOW = 128_000
|
|
7
|
+
_warned_models: set[str] = set()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _litellm_limit(model: str) -> int | None:
|
|
11
|
+
"""litellm's `max_input_tokens` for `model`, or None when it doesn't map it."""
|
|
12
|
+
try:
|
|
13
|
+
limit = litellm.get_model_info(model=model).get("max_input_tokens")
|
|
14
|
+
except Exception:
|
|
15
|
+
return None
|
|
16
|
+
return int(limit) if limit else None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_model_token_limit() -> int:
|
|
20
|
+
"""Context window for the active model: `loop.context_window`, else the OpenCode table, else
|
|
21
|
+
litellm's model info, else 128k.
|
|
22
|
+
|
|
23
|
+
OpenCode ids are checked against `helpers/models/opencode._CONTEXT_WINDOWS` first: litellm only
|
|
24
|
+
knows their upstream `openai/` / `anthropic/` route for a few of them, and the same id can have a
|
|
25
|
+
different window on each gateway.
|
|
26
|
+
"""
|
|
27
|
+
configured = get_config().context_window
|
|
28
|
+
if configured > 0:
|
|
29
|
+
return configured
|
|
30
|
+
opencode = is_opencode(get_config().provider)
|
|
31
|
+
if opencode:
|
|
32
|
+
model_id = get_model().split("/", 1)[-1]
|
|
33
|
+
known = context_window(get_config().provider, model_id)
|
|
34
|
+
if known:
|
|
35
|
+
return known
|
|
36
|
+
model = get_litellm_model()
|
|
37
|
+
limit = _litellm_limit(model)
|
|
38
|
+
if limit:
|
|
39
|
+
return limit
|
|
40
|
+
if opencode:
|
|
41
|
+
# litellm keys some ids only without the route prefix, e.g. `deepseek-v4-pro` (1M) exists
|
|
42
|
+
# while `openai/deepseek-v4-pro` doesn't.
|
|
43
|
+
limit = _litellm_limit(get_model().split("/", 1)[-1])
|
|
44
|
+
if limit:
|
|
45
|
+
return limit
|
|
46
|
+
if model not in _warned_models:
|
|
47
|
+
_warned_models.add(model)
|
|
48
|
+
from dostuff.helpers.ui.emit import emit
|
|
49
|
+
emit(
|
|
50
|
+
f"Unknown context window for '{model}'; assuming {FALLBACK_CONTEXT_WINDOW:,} tokens. "
|
|
51
|
+
"Set loop.context_window in config.yaml to override.",
|
|
52
|
+
msg_type="system",
|
|
53
|
+
)
|
|
54
|
+
return FALLBACK_CONTEXT_WINDOW
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""OAuth token persistence in the OS keyring.
|
|
2
|
+
|
|
3
|
+
If `keyring` can't be imported or has no usable backend, tokens live in memory only; they are
|
|
4
|
+
never written to a plaintext file.
|
|
5
|
+
|
|
6
|
+
Windows Credential Manager caps a secret at 2560 bytes (1280 UTF-16 chars) and JWT access plus
|
|
7
|
+
refresh tokens easily exceed that, so long values are split across `<url>#<i>` entries and the
|
|
8
|
+
main entry holds `chunks:<n>`.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
SERVICE = "dostuff-mcp"
|
|
14
|
+
CHUNK_CHARS = 1000
|
|
15
|
+
_CHUNK_PREFIX = "chunks:"
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
import keyring as _keyring
|
|
19
|
+
except ImportError: # a required dependency, but don't crash a broken install
|
|
20
|
+
_keyring = None # type: ignore[assignment]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def keyring_available() -> bool:
|
|
24
|
+
return _keyring is not None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _chunk_count(head: str | None) -> int:
|
|
28
|
+
if head and head.startswith(_CHUNK_PREFIX):
|
|
29
|
+
try:
|
|
30
|
+
return int(head[len(_CHUNK_PREFIX):])
|
|
31
|
+
except ValueError:
|
|
32
|
+
return 0
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_token(server_url: str) -> dict[str, Any] | None:
|
|
37
|
+
if _keyring is None:
|
|
38
|
+
return None
|
|
39
|
+
try:
|
|
40
|
+
raw = _keyring.get_password(SERVICE, server_url)
|
|
41
|
+
if n := _chunk_count(raw):
|
|
42
|
+
parts = [_keyring.get_password(SERVICE, f"{server_url}#{i}") for i in range(n)]
|
|
43
|
+
chunks = [p for p in parts if p is not None]
|
|
44
|
+
raw = "".join(chunks) if len(chunks) == n else None
|
|
45
|
+
data = json.loads(raw) if raw else None
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
return data if isinstance(data, dict) and data.get("access_token") else None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def save_token(server_url: str, token: dict[str, Any]) -> bool:
|
|
52
|
+
"""True when the token reached the keyring."""
|
|
53
|
+
if _keyring is None:
|
|
54
|
+
return False
|
|
55
|
+
raw = json.dumps(token)
|
|
56
|
+
try:
|
|
57
|
+
delete_token(server_url)
|
|
58
|
+
if len(raw) <= CHUNK_CHARS:
|
|
59
|
+
_keyring.set_password(SERVICE, server_url, raw)
|
|
60
|
+
return True
|
|
61
|
+
parts = [raw[i:i + CHUNK_CHARS] for i in range(0, len(raw), CHUNK_CHARS)]
|
|
62
|
+
for i, part in enumerate(parts):
|
|
63
|
+
_keyring.set_password(SERVICE, f"{server_url}#{i}", part)
|
|
64
|
+
_keyring.set_password(SERVICE, server_url, f"{_CHUNK_PREFIX}{len(parts)}") # last: marks it complete
|
|
65
|
+
return True
|
|
66
|
+
except Exception:
|
|
67
|
+
return False # no usable keyring backend; keep the in-memory token
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def delete_token(server_url: str) -> None:
|
|
71
|
+
if _keyring is None:
|
|
72
|
+
return
|
|
73
|
+
try:
|
|
74
|
+
n = _chunk_count(_keyring.get_password(SERVICE, server_url))
|
|
75
|
+
except Exception:
|
|
76
|
+
n = 0
|
|
77
|
+
for name in [server_url, *(f"{server_url}#{i}" for i in range(n))]:
|
|
78
|
+
try:
|
|
79
|
+
_keyring.delete_password(SERVICE, name)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""OpenCode Zen (`opencode/<id>`) and OpenCode Go (`opencode-go/<id>`) models.
|
|
2
|
+
|
|
3
|
+
litellm has no OpenCode provider. Each gateway serves a model family on the endpoint of its
|
|
4
|
+
upstream API (https://opencode.ai/docs/zen/), so a model is sent through litellm's openai,
|
|
5
|
+
anthropic or gemini route with the gateway as api_base.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
OPENCODE_PROVIDERS = {
|
|
11
|
+
"opencode": "https://opencode.ai/inference",
|
|
12
|
+
"opencode-go": "https://opencode.ai/inference/go",
|
|
13
|
+
}
|
|
14
|
+
OPENCODE_API_KEY_ENV = "OPENCODE_API_KEY" # one console key works for Zen and Go
|
|
15
|
+
# Served on the Anthropic Messages endpoint besides Claude; they differ between the gateways
|
|
16
|
+
_ANTHROPIC_IDS = {
|
|
17
|
+
"opencode": {"qwen3.8-flash", "qwen3.6-plus", "qwen3.5-plus"},
|
|
18
|
+
"opencode-go": {"qwen3.8-max", "qwen3.8-flash", "qwen3.7-plus", "minimax-m3"},
|
|
19
|
+
}
|
|
20
|
+
# Input-token limit per model, from models.dev's `opencode` / `opencode-go` entries
|
|
21
|
+
# (https://models.dev — the catalog OpenCode's own clients size their context with). The gateways
|
|
22
|
+
# don't publish limits in /v1/models, litellm doesn't map their ids, and the same model can have a
|
|
23
|
+
# different window on each gateway, so this table is the only reliable source. Refresh it when
|
|
24
|
+
# `provider_models()` lists an id that is missing here (those fall back to litellm, then 128k).
|
|
25
|
+
_CONTEXT_WINDOWS = {
|
|
26
|
+
"opencode": {
|
|
27
|
+
"beam-free": 1_048_576,
|
|
28
|
+
"big-pickle": 160_000,
|
|
29
|
+
"claude-fable-5": 1_000_000,
|
|
30
|
+
"claude-fable-5-1": 1_000_000,
|
|
31
|
+
"claude-haiku-4-5": 200_000,
|
|
32
|
+
"claude-haiku-5-5": 1_000_000,
|
|
33
|
+
"claude-opus-4-5": 200_000,
|
|
34
|
+
"claude-opus-4-6": 1_000_000,
|
|
35
|
+
"claude-opus-4-7": 1_000_000,
|
|
36
|
+
"claude-opus-4-8": 1_000_000,
|
|
37
|
+
"claude-opus-5": 1_000_000,
|
|
38
|
+
"claude-opus-5-5": 1_000_000,
|
|
39
|
+
"claude-sonnet-4-5": 1_000_000,
|
|
40
|
+
"claude-sonnet-4-6": 1_000_000,
|
|
41
|
+
"claude-sonnet-5": 1_000_000,
|
|
42
|
+
"claude-sonnet-5-5": 1_000_000,
|
|
43
|
+
"deepseek-v4-pro": 1_000_000,
|
|
44
|
+
"deepseek-v4.1-flash": 1_000_000,
|
|
45
|
+
"deepseek-v4.1-flash-free": 200_000,
|
|
46
|
+
"flint-40b-free": 262_144,
|
|
47
|
+
"gemini-3-flash": 1_048_576,
|
|
48
|
+
"gemini-3.1-pro": 1_048_576,
|
|
49
|
+
"gemini-3.5-flash": 1_048_576,
|
|
50
|
+
"gemini-3.5-flash-lite": 1_048_576,
|
|
51
|
+
"gemini-3.6-flash": 1_048_576,
|
|
52
|
+
"gemini-3.7-flash": 1_048_576,
|
|
53
|
+
"gemini-3.8-flash": 1_048_576,
|
|
54
|
+
"gemini-4-argon": 1_048_576,
|
|
55
|
+
"glm-5": 204_800,
|
|
56
|
+
"glm-5.1": 204_800,
|
|
57
|
+
"glm-5.2": 1_000_000,
|
|
58
|
+
"glm-5.3": 1_000_000,
|
|
59
|
+
"glm-5.3-flash": 1_000_000,
|
|
60
|
+
"glm-5.3-flash-free": 200_000,
|
|
61
|
+
"gpt-5": 272_000,
|
|
62
|
+
"gpt-5-codex": 272_000,
|
|
63
|
+
"gpt-5-nano": 272_000,
|
|
64
|
+
"gpt-5.1": 272_000,
|
|
65
|
+
"gpt-5.1-codex": 272_000,
|
|
66
|
+
"gpt-5.1-codex-max": 272_000,
|
|
67
|
+
"gpt-5.1-codex-mini": 272_000,
|
|
68
|
+
"gpt-5.2": 272_000,
|
|
69
|
+
"gpt-5.2-codex": 272_000,
|
|
70
|
+
"gpt-5.3-codex": 272_000,
|
|
71
|
+
"gpt-5.3-codex-spark": 128_000,
|
|
72
|
+
"gpt-5.4": 922_000,
|
|
73
|
+
"gpt-5.4-mini": 272_000,
|
|
74
|
+
"gpt-5.4-nano": 272_000,
|
|
75
|
+
"gpt-5.4-pro": 922_000,
|
|
76
|
+
"gpt-5.5": 922_000,
|
|
77
|
+
"gpt-5.5-pro": 922_000,
|
|
78
|
+
"gpt-5.6-luna": 922_000,
|
|
79
|
+
"gpt-5.6-sol": 922_000,
|
|
80
|
+
"gpt-5.6-terra": 922_000,
|
|
81
|
+
"gpt-6-astra": 922_000,
|
|
82
|
+
"gpt-6-luna": 922_000,
|
|
83
|
+
"gpt-6-sol": 922_000,
|
|
84
|
+
"gpt-6.1-sol": 922_000,
|
|
85
|
+
"grok-4.5": 500_000,
|
|
86
|
+
"grok-4.6": 500_000,
|
|
87
|
+
"grok-4.7": 500_000,
|
|
88
|
+
"grok-build-0.1": 256_000,
|
|
89
|
+
"kimi-k2.5": 262_144,
|
|
90
|
+
"kimi-k2.6": 262_144,
|
|
91
|
+
"kimi-k2.7-code": 262_144,
|
|
92
|
+
"kimi-k3": 1_048_576,
|
|
93
|
+
"mimo-v2.6-flash-free": 200_000,
|
|
94
|
+
"minimax-m2.5": 204_800,
|
|
95
|
+
"minimax-m2.7": 204_800,
|
|
96
|
+
"minimax-m3": 512_000,
|
|
97
|
+
"mistral-large-4": 524_288,
|
|
98
|
+
"muse-spark-1.2": 1_048_576,
|
|
99
|
+
"muse-spark-1.3": 1_048_576,
|
|
100
|
+
"muse-wonder-1.0": 1_048_576,
|
|
101
|
+
"nemotron-3.5-lightning-free": 262_144,
|
|
102
|
+
"nemotron-3.5-super-vl-free": 1_048_576,
|
|
103
|
+
"qwen3.5-plus": 262_144,
|
|
104
|
+
"qwen3.6-plus": 262_144,
|
|
105
|
+
"qwen3.8-flash": 1_000_000,
|
|
106
|
+
"qwen3.8-max": 262_144,
|
|
107
|
+
},
|
|
108
|
+
"opencode-go": {
|
|
109
|
+
"beam-free": 1_048_576,
|
|
110
|
+
"claude-haiku-5-5": 1_000_000,
|
|
111
|
+
"deepseek-v4-pro": 1_000_000,
|
|
112
|
+
"deepseek-v4.1-flash": 1_000_000,
|
|
113
|
+
"gemini-4-argon": 1_048_576,
|
|
114
|
+
"glm-5.2": 1_000_000,
|
|
115
|
+
"glm-5.3": 1_000_000,
|
|
116
|
+
"glm-5.3-flash": 1_000_000,
|
|
117
|
+
"gpt-5.6-luna": 922_000,
|
|
118
|
+
"gpt-6-luna": 922_000,
|
|
119
|
+
"grok-4.7": 500_000,
|
|
120
|
+
"hy3": 192_000,
|
|
121
|
+
"hy4-preview": 1_024_000,
|
|
122
|
+
"iceman-contributor-free": 262_144,
|
|
123
|
+
"kimi-k2.7-code": 262_144,
|
|
124
|
+
"kimi-k3": 1_048_576,
|
|
125
|
+
"kimi-k3.1-pro": 1_048_576,
|
|
126
|
+
"longcat-2.0": 1_000_000,
|
|
127
|
+
"mimo-v2.6-flash": 1_048_576,
|
|
128
|
+
"mimo-v2.6-pro": 1_048_576,
|
|
129
|
+
"minimax-m3": 1_000_000,
|
|
130
|
+
"minimax-m3.1-flash": 1_000_000,
|
|
131
|
+
"muse-spark-1.2-contributor": 1_048_576,
|
|
132
|
+
"muse-spark-1.3-contributor": 1_048_576,
|
|
133
|
+
"qwen3.7-plus": 1_000_000,
|
|
134
|
+
"qwen3.8-flash": 1_000_000,
|
|
135
|
+
"qwen3.8-max": 1_000_000,
|
|
136
|
+
"step-5-preview": 1_000_000,
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
# In /v1/models but not chat models
|
|
140
|
+
_NOT_CHAT = ("jev-", "test")
|
|
141
|
+
|
|
142
|
+
Endpoint = Literal["chat", "responses", "anthropic", "google"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def is_opencode(provider: str) -> bool:
|
|
146
|
+
return provider in OPENCODE_PROVIDERS
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def context_window(provider: str, model_id: str) -> int | None:
|
|
150
|
+
"""Input-token limit for an OpenCode model id, or None if the table doesn't list it."""
|
|
151
|
+
return _CONTEXT_WINDOWS.get(provider, {}).get(model_id)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def endpoint_for(provider: str, model_id: str) -> Endpoint:
|
|
155
|
+
"""Which upstream API the gateway serves `model_id` on."""
|
|
156
|
+
if model_id.startswith("claude-") or model_id in _ANTHROPIC_IDS[provider]:
|
|
157
|
+
return "anthropic"
|
|
158
|
+
if model_id.startswith(("gpt-", "grok-", "muse-")):
|
|
159
|
+
return "responses"
|
|
160
|
+
if model_id.startswith("gemini-"):
|
|
161
|
+
return "google"
|
|
162
|
+
return "chat"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def litellm_route(provider: str, model_id: str) -> tuple[str, str]:
|
|
166
|
+
"""(litellm model, api_base) for an OpenCode model id."""
|
|
167
|
+
base = OPENCODE_PROVIDERS[provider]
|
|
168
|
+
endpoint = endpoint_for(provider, model_id)
|
|
169
|
+
if endpoint == "anthropic":
|
|
170
|
+
return f"anthropic/{model_id}", f"{base}/anthropic" # litellm appends /v1/messages
|
|
171
|
+
if endpoint == "google":
|
|
172
|
+
return f"gemini/{model_id}", f"{base}/google/v1beta"
|
|
173
|
+
if endpoint == "responses":
|
|
174
|
+
return f"openai/responses/{model_id}", f"{base}/openai/v1"
|
|
175
|
+
return f"openai/{model_id}", f"{base}/openai/v1"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def opencode_kwargs(provider: str, model_id: str, api_key: str | None, session_id: str, version: str) -> dict[str, Any]:
|
|
179
|
+
"""litellm kwargs for an OpenCode model. Go asks clients to name themselves and send a
|
|
180
|
+
stable per-conversation id; Zen gets the same headers."""
|
|
181
|
+
model, api_base = litellm_route(provider, model_id)
|
|
182
|
+
return {
|
|
183
|
+
"model": model,
|
|
184
|
+
"api_base": api_base,
|
|
185
|
+
# Never fall back to OPENAI_API_KEY / ANTHROPIC_API_KEY: those would go to opencode.ai
|
|
186
|
+
"api_key": api_key or os.environ.get(OPENCODE_API_KEY_ENV) or "missing-opencode-key",
|
|
187
|
+
"extra_headers": {"User-Agent": f"dostuff/{version}", "x-opencode-session": session_id},
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def models_url(provider: str) -> str:
|
|
192
|
+
return f"{OPENCODE_PROVIDERS[provider]}/v1/models"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def chat_model_ids(data: dict) -> list[str]:
|
|
196
|
+
"""Model ids from a /v1/models response, without the non-chat ones."""
|
|
197
|
+
return [
|
|
198
|
+
m["id"] for m in data.get("data") or []
|
|
199
|
+
if isinstance(m, dict) and m.get("id") and not str(m["id"]).startswith(_NOT_CHAT)
|
|
200
|
+
]
|