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.
Files changed (130) hide show
  1. {dostuff-0.2.1 → dostuff-0.2.3}/PKG-INFO +20 -14
  2. {dostuff-0.2.1 → dostuff-0.2.3}/README.md +18 -11
  3. dostuff-0.2.3/dostuff/helpers/agent/get_model_token_limit.py +54 -0
  4. dostuff-0.2.3/dostuff/helpers/mcp/token_store.py +81 -0
  5. dostuff-0.2.3/dostuff/helpers/models/opencode.py +200 -0
  6. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_client.py +90 -36
  7. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/templates/config.example.yaml +1 -1
  8. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/PKG-INFO +20 -14
  9. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/requires.txt +1 -3
  10. {dostuff-0.2.1 → dostuff-0.2.3}/pyproject.toml +3 -5
  11. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_mcp_oauth.py +110 -1
  12. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_opencode.py +43 -0
  13. dostuff-0.2.1/dostuff/helpers/agent/get_model_token_limit.py +0 -29
  14. dostuff-0.2.1/dostuff/helpers/mcp/token_store.py +0 -46
  15. dostuff-0.2.1/dostuff/helpers/models/opencode.py +0 -76
  16. {dostuff-0.2.1 → dostuff-0.2.3}/LICENSE +0 -0
  17. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/__init__.py +0 -0
  18. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/__main__.py +0 -0
  19. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/call_agent.py +0 -0
  20. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/instructions.py +0 -0
  21. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/loop.py +0 -0
  22. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/permissions.py +0 -0
  23. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/agent/run_tool.py +0 -0
  24. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/cli.py +0 -0
  25. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/cli_tui.py +0 -0
  26. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/config.py +0 -0
  27. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/headless.py +0 -0
  28. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/append_step.py +0 -0
  29. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/constants.py +0 -0
  30. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/consume_stream.py +0 -0
  31. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/context_usage.py +0 -0
  32. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/environment_context.py +0 -0
  33. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/file_mentions.py +0 -0
  34. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/load_identity.py +0 -0
  35. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/load_project_instructions.py +0 -0
  36. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/loop_check_cancelled.py +0 -0
  37. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/loop_emit.py +0 -0
  38. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/manage_context.py +0 -0
  39. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/model_cost.py +0 -0
  40. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/pastes.py +0 -0
  41. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/quick_memory.py +0 -0
  42. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/slash_suggestion.py +0 -0
  43. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/agent/tool_output.py +0 -0
  44. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/mcp/load_mcp_config.py +0 -0
  45. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/mcp/mcp_oauth.py +0 -0
  46. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/mcp/server_config.py +0 -0
  47. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/memory/export_session.py +0 -0
  48. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/memory/resolve_memory_operation.py +0 -0
  49. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/memory/shell_step.py +0 -0
  50. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/models/add_model_entry.py +0 -0
  51. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/models/list_models.py +0 -0
  52. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/skills/custom_commands.py +0 -0
  53. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/skills/discover_skills.py +0 -0
  54. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/skills/skill_command.py +0 -0
  55. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/diff.py +0 -0
  56. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/generate_tool_schema.py +0 -0
  57. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/project_root.py +0 -0
  58. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/tools/resolve_safe_path.py +0 -0
  59. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/__init__.py +0 -0
  60. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/doctor.py +0 -0
  61. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/emit.py +0 -0
  62. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/prompt_history.py +0 -0
  63. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/helpers/ui/terminal_signals.py +0 -0
  64. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/checkpoints.py +0 -0
  65. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/exceptions.py +0 -0
  66. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_client_registration_store.py +0 -0
  67. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/mcp/mcp_tool_registry_store.py +0 -0
  68. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/chroma.py +0 -0
  69. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/episodic_memory_store.py +0 -0
  70. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/semantic_memory_store.py +0 -0
  71. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/session_store.py +0 -0
  72. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/stores.py +0 -0
  73. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/memory/types.py +0 -0
  74. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/model.py +0 -0
  75. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/tracing.py +0 -0
  76. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/lib/worktree.py +0 -0
  77. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/memory/__init__.py +0 -0
  78. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/setup_wizard.py +0 -0
  79. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/skills.py +0 -0
  80. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/templates/env.example +0 -0
  81. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/__init__.py +0 -0
  82. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/bash/bash_command.py +0 -0
  83. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/definitions.py +0 -0
  84. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/delegate_to_subagent.py +0 -0
  85. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/delete_file.py +0 -0
  86. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/edit_file.py +0 -0
  87. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/list_files.py +0 -0
  88. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/read_file.py +0 -0
  89. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/files/write_file.py +0 -0
  90. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/get_current_datetime.py +0 -0
  91. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/mcp/call_mcp_tool.py +0 -0
  92. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/mcp/get_mcp_tool_details.py +0 -0
  93. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/mcp/search_mcp_tools.py +0 -0
  94. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/memory/save_episode.py +0 -0
  95. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/memory/save_memory.py +0 -0
  96. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/search/glob.py +0 -0
  97. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/search/grep.py +0 -0
  98. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/search/walk.py +0 -0
  99. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/settings.py +0 -0
  100. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/todo_write.py +0 -0
  101. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/web_fetch.py +0 -0
  102. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff/tools/web_search.py +0 -0
  103. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/SOURCES.txt +0 -0
  104. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/dependency_links.txt +0 -0
  105. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/entry_points.txt +0 -0
  106. {dostuff-0.2.1 → dostuff-0.2.3}/dostuff.egg-info/top_level.txt +0 -0
  107. {dostuff-0.2.1 → dostuff-0.2.3}/setup.cfg +0 -0
  108. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_checkpoints_todos_caching.py +0 -0
  109. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_cli_commands.py +0 -0
  110. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_config_memory_mcp.py +0 -0
  111. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_file_mentions.py +0 -0
  112. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_file_tools.py +0 -0
  113. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_loop.py +0 -0
  114. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_mcp_e2e.py +0 -0
  115. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_optional_memory_and_instructions.py +0 -0
  116. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_pastes.py +0 -0
  117. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_permissions.py +0 -0
  118. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_roadmap_021.py +0 -0
  119. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_session_store.py +0 -0
  120. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_settings_tools.py +0 -0
  121. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_setup_wizard.py +0 -0
  122. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_skill_command.py +0 -0
  123. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_slash_suggestion.py +0 -0
  124. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_subagent.py +0 -0
  125. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_terminal_signals.py +0 -0
  126. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_tool_schemas.py +0 -0
  127. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_tui.py +0 -0
  128. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_web_fetch.py +0 -0
  129. {dostuff-0.2.1 → dostuff-0.2.3}/tests/test_web_search.py +0 -0
  130. {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.1
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
- Long-term memory (the agent remembers facts and past decisions across sessions) needs ChromaDB,
114
- a large download, so it's an extra:
112
+ Optional features come as extras:
115
113
 
116
- ```bash
117
- pip install "dostuff[memory]"
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
- Without it everything else works: the memory tools are hidden from the model and MCP tool search
121
- matches keywords instead of meaning. `dostuff doctor` shows whether memory is on.
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 ".[full]" # + memory + onnxruntime + grpcio
139
- pip install -e ".[keyring]" # persist MCP OAuth tokens in the OS keyring
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 doesn't know the model
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 kept in memory, or in the OS keyring if you install `dostuff[keyring]`, never in a plain file. A tool call that fails with 401 re-authenticates once and retries.
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
- Long-term memory (the agent remembers facts and past decisions across sessions) needs ChromaDB,
65
- a large download, so it's an extra:
64
+ Optional features come as extras:
66
65
 
67
- ```bash
68
- pip install "dostuff[memory]"
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
- Without it everything else works: the memory tools are hidden from the model and MCP tool search
72
- matches keywords instead of meaning. `dostuff doctor` shows whether memory is on.
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 ".[full]" # + memory + onnxruntime + grpcio
90
- pip install -e ".[keyring]" # persist MCP OAuth tokens in the OS keyring
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 doesn't know the model
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 kept in memory, or in the OS keyring if you install `dostuff[keyring]`, never in a plain file. A tool call that fails with 401 re-authenticates once and retries.
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
+ ]