supervis 0.1.0__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.
- supervis-0.1.0/LICENSE +21 -0
- supervis-0.1.0/PKG-INFO +111 -0
- supervis-0.1.0/README.md +93 -0
- supervis-0.1.0/pyproject.toml +32 -0
- supervis-0.1.0/setup.cfg +4 -0
- supervis-0.1.0/supervis.egg-info/PKG-INFO +111 -0
- supervis-0.1.0/supervis.egg-info/SOURCES.txt +20 -0
- supervis-0.1.0/supervis.egg-info/dependency_links.txt +1 -0
- supervis-0.1.0/supervis.egg-info/entry_points.txt +2 -0
- supervis-0.1.0/supervis.egg-info/requires.txt +1 -0
- supervis-0.1.0/supervis.egg-info/top_level.txt +1 -0
- supervis-0.1.0/supervisor/__init__.py +3 -0
- supervis-0.1.0/supervisor/chat.py +255 -0
- supervis-0.1.0/supervisor/claude.py +86 -0
- supervis-0.1.0/supervisor/config.py +52 -0
- supervis-0.1.0/supervisor/cost.py +44 -0
- supervis-0.1.0/supervisor/deepseek.py +130 -0
- supervis-0.1.0/supervisor/display.py +26 -0
- supervis-0.1.0/supervisor/main.py +28 -0
- supervis-0.1.0/supervisor/memory.py +47 -0
- supervis-0.1.0/supervisor/prompts.py +28 -0
- supervis-0.1.0/supervisor/tools.py +181 -0
supervis-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 supervis contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
supervis-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: supervis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DeepSeek supervisor for Claude Code
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/yourname/supervis
|
|
7
|
+
Project-URL: Issues, https://github.com/yourname/supervis/issues
|
|
8
|
+
Keywords: claude,deepseek,ai,coding,agent,llm
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: openai>=2.0.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# supervis
|
|
20
|
+
|
|
21
|
+
DeepSeek reads your codebase and drives Claude Code so you don't have to babysit every prompt.
|
|
22
|
+
|
|
23
|
+
## What it does
|
|
24
|
+
|
|
25
|
+
- Explores the project before writing a single line
|
|
26
|
+
- Sends precise, context-aware prompts to Claude Code
|
|
27
|
+
- Reviews the diff, fixes mistakes, continues on its own
|
|
28
|
+
- Only asks you for real decisions (architecture, trade-offs)
|
|
29
|
+
- Queues your messages while it works, type anytime, nothing is lost
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipx install supervis
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and a [DeepSeek API key](https://platform.deepseek.com/api-keys).
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd myproject
|
|
43
|
+
supervis
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
You: add JWT authentication
|
|
48
|
+
|
|
49
|
+
→ DeepSeek reads the codebase
|
|
50
|
+
→ Claude Code writes the implementation
|
|
51
|
+
→ DeepSeek checks the diff, corrects if needed
|
|
52
|
+
→ "Done. Added verify_token() in auth/tokens.py, middleware in auth/middleware.py."
|
|
53
|
+
|
|
54
|
+
You: actually make it session-based ← typed while agent was working, queued automatically
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Controls
|
|
58
|
+
|
|
59
|
+
| Key | Action |
|
|
60
|
+
|-----|--------|
|
|
61
|
+
| `ESC` or `Ctrl+C` | Interrupt agent, return to prompt |
|
|
62
|
+
| `Ctrl+C` (idle) × 2 | Exit |
|
|
63
|
+
| `exit` | Exit |
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
| Command | Description |
|
|
68
|
+
|---------|-------------|
|
|
69
|
+
| `/reset` | Reset Claude session and conversation history |
|
|
70
|
+
| `/help` | Show available commands |
|
|
71
|
+
|
|
72
|
+
## API Key
|
|
73
|
+
|
|
74
|
+
First run will prompt you if no key is set:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
No DeepSeek API key found.
|
|
78
|
+
Get one at: https://platform.deepseek.com/api-keys
|
|
79
|
+
|
|
80
|
+
Enter your API key: sk-...
|
|
81
|
+
Saved to ~/.config/supervis/config
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or set it yourself (takes precedence):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
set -Ux DEEPSEEK_API_KEY sk-... # fish
|
|
88
|
+
export DEEPSEEK_API_KEY=sk-... # bash/zsh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How it works
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
You → DeepSeek (reads files, plans) → Claude Code (writes code) → DeepSeek (verifies) → You
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
DeepSeek uses [DeepSeek V3.2](https://platform.deepseek.com) via API. Claude Code runs locally with `bypassPermissions` so it edits files without asking for each one.
|
|
98
|
+
|
|
99
|
+
## Cost
|
|
100
|
+
|
|
101
|
+
Shown after each response:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
[in 12.3k 4.1k cached · out 0.8k · $0.0031]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
DeepSeek V3.2 pricing: $0.28/1M input · $0.028/1M cached · $0.42/1M output.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
supervis-0.1.0/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# supervis
|
|
2
|
+
|
|
3
|
+
DeepSeek reads your codebase and drives Claude Code so you don't have to babysit every prompt.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Explores the project before writing a single line
|
|
8
|
+
- Sends precise, context-aware prompts to Claude Code
|
|
9
|
+
- Reviews the diff, fixes mistakes, continues on its own
|
|
10
|
+
- Only asks you for real decisions (architecture, trade-offs)
|
|
11
|
+
- Queues your messages while it works, type anytime, nothing is lost
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pipx install supervis
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and a [DeepSeek API key](https://platform.deepseek.com/api-keys).
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd myproject
|
|
25
|
+
supervis
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
You: add JWT authentication
|
|
30
|
+
|
|
31
|
+
→ DeepSeek reads the codebase
|
|
32
|
+
→ Claude Code writes the implementation
|
|
33
|
+
→ DeepSeek checks the diff, corrects if needed
|
|
34
|
+
→ "Done. Added verify_token() in auth/tokens.py, middleware in auth/middleware.py."
|
|
35
|
+
|
|
36
|
+
You: actually make it session-based ← typed while agent was working, queued automatically
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Controls
|
|
40
|
+
|
|
41
|
+
| Key | Action |
|
|
42
|
+
|-----|--------|
|
|
43
|
+
| `ESC` or `Ctrl+C` | Interrupt agent, return to prompt |
|
|
44
|
+
| `Ctrl+C` (idle) × 2 | Exit |
|
|
45
|
+
| `exit` | Exit |
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `/reset` | Reset Claude session and conversation history |
|
|
52
|
+
| `/help` | Show available commands |
|
|
53
|
+
|
|
54
|
+
## API Key
|
|
55
|
+
|
|
56
|
+
First run will prompt you if no key is set:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
No DeepSeek API key found.
|
|
60
|
+
Get one at: https://platform.deepseek.com/api-keys
|
|
61
|
+
|
|
62
|
+
Enter your API key: sk-...
|
|
63
|
+
Saved to ~/.config/supervis/config
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or set it yourself (takes precedence):
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
set -Ux DEEPSEEK_API_KEY sk-... # fish
|
|
70
|
+
export DEEPSEEK_API_KEY=sk-... # bash/zsh
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## How it works
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
You → DeepSeek (reads files, plans) → Claude Code (writes code) → DeepSeek (verifies) → You
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
DeepSeek uses [DeepSeek V3.2](https://platform.deepseek.com) via API. Claude Code runs locally with `bypassPermissions` so it edits files without asking for each one.
|
|
80
|
+
|
|
81
|
+
## Cost
|
|
82
|
+
|
|
83
|
+
Shown after each response:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
[in 12.3k 4.1k cached · out 0.8k · $0.0031]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
DeepSeek V3.2 pricing: $0.28/1M input · $0.028/1M cached · $0.42/1M output.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "supervis"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "DeepSeek supervisor for Claude Code"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"openai>=2.0.0",
|
|
14
|
+
]
|
|
15
|
+
keywords = ["claude", "deepseek", "ai", "coding", "agent", "llm"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: POSIX :: Linux",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/yourname/supervis"
|
|
25
|
+
Issues = "https://github.com/yourname/supervis/issues"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
supervis = "supervisor.main:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
|
32
|
+
include = ["supervisor*"]
|
supervis-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: supervis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: DeepSeek supervisor for Claude Code
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/yourname/supervis
|
|
7
|
+
Project-URL: Issues, https://github.com/yourname/supervis/issues
|
|
8
|
+
Keywords: claude,deepseek,ai,coding,agent,llm
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: openai>=2.0.0
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# supervis
|
|
20
|
+
|
|
21
|
+
DeepSeek reads your codebase and drives Claude Code so you don't have to babysit every prompt.
|
|
22
|
+
|
|
23
|
+
## What it does
|
|
24
|
+
|
|
25
|
+
- Explores the project before writing a single line
|
|
26
|
+
- Sends precise, context-aware prompts to Claude Code
|
|
27
|
+
- Reviews the diff, fixes mistakes, continues on its own
|
|
28
|
+
- Only asks you for real decisions (architecture, trade-offs)
|
|
29
|
+
- Queues your messages while it works, type anytime, nothing is lost
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pipx install supervis
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires [Claude Code](https://docs.anthropic.com/en/docs/claude-code) and a [DeepSeek API key](https://platform.deepseek.com/api-keys).
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
cd myproject
|
|
43
|
+
supervis
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
You: add JWT authentication
|
|
48
|
+
|
|
49
|
+
→ DeepSeek reads the codebase
|
|
50
|
+
→ Claude Code writes the implementation
|
|
51
|
+
→ DeepSeek checks the diff, corrects if needed
|
|
52
|
+
→ "Done. Added verify_token() in auth/tokens.py, middleware in auth/middleware.py."
|
|
53
|
+
|
|
54
|
+
You: actually make it session-based ← typed while agent was working, queued automatically
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Controls
|
|
58
|
+
|
|
59
|
+
| Key | Action |
|
|
60
|
+
|-----|--------|
|
|
61
|
+
| `ESC` or `Ctrl+C` | Interrupt agent, return to prompt |
|
|
62
|
+
| `Ctrl+C` (idle) × 2 | Exit |
|
|
63
|
+
| `exit` | Exit |
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
| Command | Description |
|
|
68
|
+
|---------|-------------|
|
|
69
|
+
| `/reset` | Reset Claude session and conversation history |
|
|
70
|
+
| `/help` | Show available commands |
|
|
71
|
+
|
|
72
|
+
## API Key
|
|
73
|
+
|
|
74
|
+
First run will prompt you if no key is set:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
No DeepSeek API key found.
|
|
78
|
+
Get one at: https://platform.deepseek.com/api-keys
|
|
79
|
+
|
|
80
|
+
Enter your API key: sk-...
|
|
81
|
+
Saved to ~/.config/supervis/config
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or set it yourself (takes precedence):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
set -Ux DEEPSEEK_API_KEY sk-... # fish
|
|
88
|
+
export DEEPSEEK_API_KEY=sk-... # bash/zsh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## How it works
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
You → DeepSeek (reads files, plans) → Claude Code (writes code) → DeepSeek (verifies) → You
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
DeepSeek uses [DeepSeek V3.2](https://platform.deepseek.com) via API. Claude Code runs locally with `bypassPermissions` so it edits files without asking for each one.
|
|
98
|
+
|
|
99
|
+
## Cost
|
|
100
|
+
|
|
101
|
+
Shown after each response:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
[in 12.3k 4.1k cached · out 0.8k · $0.0031]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
DeepSeek V3.2 pricing: $0.28/1M input · $0.028/1M cached · $0.42/1M output.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
supervis.egg-info/PKG-INFO
|
|
5
|
+
supervis.egg-info/SOURCES.txt
|
|
6
|
+
supervis.egg-info/dependency_links.txt
|
|
7
|
+
supervis.egg-info/entry_points.txt
|
|
8
|
+
supervis.egg-info/requires.txt
|
|
9
|
+
supervis.egg-info/top_level.txt
|
|
10
|
+
supervisor/__init__.py
|
|
11
|
+
supervisor/chat.py
|
|
12
|
+
supervisor/claude.py
|
|
13
|
+
supervisor/config.py
|
|
14
|
+
supervisor/cost.py
|
|
15
|
+
supervisor/deepseek.py
|
|
16
|
+
supervisor/display.py
|
|
17
|
+
supervisor/main.py
|
|
18
|
+
supervisor/memory.py
|
|
19
|
+
supervisor/prompts.py
|
|
20
|
+
supervisor/tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openai>=2.0.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
supervisor
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Main chat loop, signal handling, input queue."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import termios
|
|
7
|
+
import tty
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import readline # arrow keys + history
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
from .deepseek import run_agent_loop
|
|
15
|
+
from .memory import summarize_if_needed
|
|
16
|
+
from .prompts import SYSTEM_PROMPT
|
|
17
|
+
from .display import GREEN, BOLD, DIM, YELLOW, CYAN, R, header
|
|
18
|
+
from .claude import get_proc, reset_session
|
|
19
|
+
from .deepseek import get_client
|
|
20
|
+
from . import cost
|
|
21
|
+
|
|
22
|
+
_EXIT_COMMANDS = {"exit", "quit", "q", "çıkış"}
|
|
23
|
+
_BUILTIN_COMMANDS = {"/reset", "/help"}
|
|
24
|
+
|
|
25
|
+
# Sentinels
|
|
26
|
+
_ESC = "__ESC__"
|
|
27
|
+
_CTRL_C = "__CTRL_C__"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ─── Raw stdin reader ────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
def _read_line_raw() -> str:
|
|
33
|
+
"""
|
|
34
|
+
Read one line in raw terminal mode.
|
|
35
|
+
Detects ESC → returns _ESC
|
|
36
|
+
Detects Ctrl+C → returns _CTRL_C
|
|
37
|
+
Handles backspace + echo manually.
|
|
38
|
+
Preserves readline history for normal lines.
|
|
39
|
+
"""
|
|
40
|
+
fd = sys.stdin.fileno()
|
|
41
|
+
old = termios.tcgetattr(fd)
|
|
42
|
+
buf: list[str] = []
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
tty.setraw(fd)
|
|
46
|
+
while True:
|
|
47
|
+
b = sys.stdin.buffer.read(1)
|
|
48
|
+
if not b:
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
byte = b[0]
|
|
52
|
+
|
|
53
|
+
if byte == 0x1b: # ESC — drain any escape sequence (arrow keys etc.)
|
|
54
|
+
sys.stdin.buffer.read(0)
|
|
55
|
+
return _ESC
|
|
56
|
+
|
|
57
|
+
if byte == 0x03: # Ctrl+C
|
|
58
|
+
return _CTRL_C
|
|
59
|
+
|
|
60
|
+
if byte in (0x0d, 0x0a): # Enter
|
|
61
|
+
sys.stdout.write("\r\n")
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
line = "".join(buf)
|
|
64
|
+
if line:
|
|
65
|
+
try:
|
|
66
|
+
readline.add_history(line)
|
|
67
|
+
except Exception:
|
|
68
|
+
pass
|
|
69
|
+
return line
|
|
70
|
+
|
|
71
|
+
if byte == 0x7f: # Backspace
|
|
72
|
+
if buf:
|
|
73
|
+
buf.pop()
|
|
74
|
+
sys.stdout.write("\b \b")
|
|
75
|
+
sys.stdout.flush()
|
|
76
|
+
|
|
77
|
+
elif 32 <= byte <= 126: # Printable ASCII
|
|
78
|
+
ch = chr(byte)
|
|
79
|
+
buf.append(ch)
|
|
80
|
+
sys.stdout.write(ch)
|
|
81
|
+
sys.stdout.flush()
|
|
82
|
+
|
|
83
|
+
finally:
|
|
84
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def _stdin_reader(queue: asyncio.Queue) -> None:
|
|
88
|
+
"""Runs _read_line_raw in executor, puts results to queue."""
|
|
89
|
+
loop = asyncio.get_running_loop()
|
|
90
|
+
while True:
|
|
91
|
+
try:
|
|
92
|
+
line = await loop.run_in_executor(None, _read_line_raw)
|
|
93
|
+
await queue.put(line)
|
|
94
|
+
except (EOFError, asyncio.CancelledError):
|
|
95
|
+
break
|
|
96
|
+
except Exception:
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def _drain_queue(queue: asyncio.Queue) -> list[str]:
|
|
103
|
+
items = []
|
|
104
|
+
while not queue.empty():
|
|
105
|
+
try:
|
|
106
|
+
items.append(queue.get_nowait())
|
|
107
|
+
except asyncio.QueueEmpty:
|
|
108
|
+
break
|
|
109
|
+
return items
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _handle_builtin(cmd: str, messages: list) -> tuple[list, bool]:
|
|
113
|
+
if cmd.lower() == "/reset":
|
|
114
|
+
reset_session()
|
|
115
|
+
cost.reset()
|
|
116
|
+
new_messages = [messages[0]]
|
|
117
|
+
print(f"{YELLOW}Session reset.{R}\n")
|
|
118
|
+
return new_messages, True
|
|
119
|
+
|
|
120
|
+
if cmd.lower() == "/help":
|
|
121
|
+
print(
|
|
122
|
+
f"\n{CYAN}Commands:{R}\n"
|
|
123
|
+
f" {BOLD}/reset{R} — reset Claude session and conversation\n"
|
|
124
|
+
f" {BOLD}/help{R} — show this\n"
|
|
125
|
+
f" {BOLD}exit{R} — quit\n"
|
|
126
|
+
f" {BOLD}ESC{R} — interrupt agent, return to prompt\n"
|
|
127
|
+
f" {BOLD}Ctrl+C{R} — interrupt agent (first) / exit (second)\n"
|
|
128
|
+
f"\n{DIM}Type anytime — messages queue while agent works.{R}\n"
|
|
129
|
+
)
|
|
130
|
+
return messages, True
|
|
131
|
+
|
|
132
|
+
return messages, False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ─── Main loop ───────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
async def chat_loop(project_dir: str) -> None:
|
|
138
|
+
os.chdir(project_dir)
|
|
139
|
+
|
|
140
|
+
header()
|
|
141
|
+
print(f"{DIM}Project: {project_dir}{R}")
|
|
142
|
+
print(f"{DIM}ESC / Ctrl+C = interrupt · Type anytime = queue · exit = quit{R}\n")
|
|
143
|
+
|
|
144
|
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
|
145
|
+
client = get_client()
|
|
146
|
+
|
|
147
|
+
input_queue: asyncio.Queue[str] = asyncio.Queue()
|
|
148
|
+
interrupt_event: asyncio.Event = asyncio.Event()
|
|
149
|
+
|
|
150
|
+
reader_task = asyncio.create_task(_stdin_reader(input_queue))
|
|
151
|
+
|
|
152
|
+
_ctrl_c_idle = False # second Ctrl+C while idle → exit
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
while True:
|
|
156
|
+
|
|
157
|
+
# ── Drain queue (typed while agent ran) ──────────────────────────
|
|
158
|
+
queued = _drain_queue(input_queue)
|
|
159
|
+
|
|
160
|
+
# Filter interrupts that came in during agent run
|
|
161
|
+
interrupts = [m for m in queued if m in (_ESC, _CTRL_C)]
|
|
162
|
+
queued = [m for m in queued if m not in (_ESC, _CTRL_C)]
|
|
163
|
+
|
|
164
|
+
if queued:
|
|
165
|
+
for msg in queued:
|
|
166
|
+
print(f"\n{YELLOW}[Queued]{R} {msg}")
|
|
167
|
+
|
|
168
|
+
if any(m.lower() in _EXIT_COMMANDS for m in queued):
|
|
169
|
+
print(f"\n{DIM}Goodbye.{R}\n")
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
for msg in queued:
|
|
173
|
+
if msg in _BUILTIN_COMMANDS:
|
|
174
|
+
messages, _ = _handle_builtin(msg, messages)
|
|
175
|
+
|
|
176
|
+
real = [
|
|
177
|
+
m for m in queued
|
|
178
|
+
if m not in _BUILTIN_COMMANDS and m.lower() not in _EXIT_COMMANDS
|
|
179
|
+
]
|
|
180
|
+
if real:
|
|
181
|
+
combined = "\n".join(real)
|
|
182
|
+
messages.append({"role": "user", "content": combined})
|
|
183
|
+
messages = await summarize_if_needed(messages, client)
|
|
184
|
+
interrupt_event.clear()
|
|
185
|
+
messages = await run_agent_loop(messages, interrupt_event)
|
|
186
|
+
print()
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
# ── Normal prompt ─────────────────────────────────────────────────
|
|
190
|
+
_ctrl_c_idle = False
|
|
191
|
+
interrupt_event.clear()
|
|
192
|
+
|
|
193
|
+
print(f"{GREEN}{BOLD}You:{R} ", end="", flush=True)
|
|
194
|
+
try:
|
|
195
|
+
user_input = await input_queue.get()
|
|
196
|
+
except asyncio.CancelledError:
|
|
197
|
+
print(f"\n{DIM}Goodbye.{R}\n")
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
# Handle ESC / Ctrl+C at idle
|
|
201
|
+
if user_input in (_ESC, _CTRL_C):
|
|
202
|
+
if _ctrl_c_idle or user_input == _ESC:
|
|
203
|
+
print(f"\n{DIM}Goodbye.{R}\n")
|
|
204
|
+
break
|
|
205
|
+
print(f"\n{YELLOW}(Press Ctrl+C again or type 'exit' to quit){R}\n")
|
|
206
|
+
_ctrl_c_idle = True
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
_ctrl_c_idle = False
|
|
210
|
+
|
|
211
|
+
if not user_input:
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if user_input.lower() in _EXIT_COMMANDS:
|
|
215
|
+
print(f"\n{DIM}Goodbye.{R}\n")
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
messages, handled = _handle_builtin(user_input, messages)
|
|
219
|
+
if handled:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
messages.append({"role": "user", "content": user_input})
|
|
223
|
+
messages = await summarize_if_needed(messages, client)
|
|
224
|
+
interrupt_event.clear()
|
|
225
|
+
|
|
226
|
+
# ── Agent loop — watch for interrupt ─────────────────────────────
|
|
227
|
+
agent_task = asyncio.create_task(
|
|
228
|
+
run_agent_loop(messages, interrupt_event)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# While agent runs, watch for ESC / Ctrl+C from queue
|
|
232
|
+
async def _watch_interrupt() -> None:
|
|
233
|
+
while not agent_task.done():
|
|
234
|
+
item = await input_queue.get()
|
|
235
|
+
if item in (_ESC, _CTRL_C):
|
|
236
|
+
interrupt_event.set()
|
|
237
|
+
proc = get_proc()
|
|
238
|
+
if proc and proc.returncode is None:
|
|
239
|
+
proc.terminate()
|
|
240
|
+
print(f"\n{YELLOW}[Interrupted]{R}", flush=True)
|
|
241
|
+
return
|
|
242
|
+
else:
|
|
243
|
+
# Re-queue non-interrupt messages
|
|
244
|
+
await input_queue.put(item)
|
|
245
|
+
|
|
246
|
+
watcher = asyncio.create_task(_watch_interrupt())
|
|
247
|
+
try:
|
|
248
|
+
messages = await agent_task
|
|
249
|
+
finally:
|
|
250
|
+
watcher.cancel()
|
|
251
|
+
|
|
252
|
+
print()
|
|
253
|
+
|
|
254
|
+
finally:
|
|
255
|
+
reader_task.cancel()
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Claude Code subprocess management."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from .display import BLUE, BOLD, DIM, R
|
|
7
|
+
|
|
8
|
+
_claude_proc: asyncio.subprocess.Process | None = None
|
|
9
|
+
_claude_first = True
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_proc() -> asyncio.subprocess.Process | None:
|
|
13
|
+
return _claude_proc
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def reset_session() -> None:
|
|
17
|
+
"""Taze Claude session'ı için state sıfırla."""
|
|
18
|
+
global _claude_first
|
|
19
|
+
_claude_first = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def run_claude(prompt: str, continue_session: bool = True) -> str:
|
|
23
|
+
global _claude_proc, _claude_first
|
|
24
|
+
|
|
25
|
+
print(f"\n{BLUE}{BOLD}┌─ Claude Code ──────────────────────────────────────────{R}")
|
|
26
|
+
print(f"{BLUE}{DIM}{prompt}{R}\n")
|
|
27
|
+
|
|
28
|
+
cmd = [
|
|
29
|
+
"claude", "-p", prompt,
|
|
30
|
+
"--output-format", "stream-json",
|
|
31
|
+
"--verbose",
|
|
32
|
+
"--permission-mode", "bypassPermissions",
|
|
33
|
+
]
|
|
34
|
+
if continue_session and not _claude_first:
|
|
35
|
+
cmd.append("--continue")
|
|
36
|
+
_claude_first = False
|
|
37
|
+
|
|
38
|
+
proc = await asyncio.create_subprocess_exec(
|
|
39
|
+
*cmd,
|
|
40
|
+
stdout=asyncio.subprocess.PIPE,
|
|
41
|
+
stderr=asyncio.subprocess.PIPE,
|
|
42
|
+
cwd=os.getcwd(),
|
|
43
|
+
limit=1024 * 1024 * 10,
|
|
44
|
+
)
|
|
45
|
+
_claude_proc = proc
|
|
46
|
+
|
|
47
|
+
chunks: list[str] = []
|
|
48
|
+
try:
|
|
49
|
+
async for raw in proc.stdout:
|
|
50
|
+
line = raw.decode("utf-8", errors="replace").strip()
|
|
51
|
+
if not line:
|
|
52
|
+
continue
|
|
53
|
+
try:
|
|
54
|
+
data = json.loads(line)
|
|
55
|
+
t = data.get("type", "")
|
|
56
|
+
|
|
57
|
+
if t == "assistant":
|
|
58
|
+
for block in data.get("message", {}).get("content", []):
|
|
59
|
+
if not isinstance(block, dict):
|
|
60
|
+
continue
|
|
61
|
+
if block.get("type") == "text":
|
|
62
|
+
txt = block["text"]
|
|
63
|
+
print(f"{BLUE}{txt}{R}", end="", flush=True)
|
|
64
|
+
chunks.append(txt)
|
|
65
|
+
elif block.get("type") == "tool_use":
|
|
66
|
+
name = block.get("name", "")
|
|
67
|
+
inp = block.get("input", {})
|
|
68
|
+
hint = (
|
|
69
|
+
inp.get("path")
|
|
70
|
+
or inp.get("command", "")
|
|
71
|
+
or inp.get("description", "")
|
|
72
|
+
)[:60]
|
|
73
|
+
print(f"\n{DIM} ↳ {name}: {hint}{R}", flush=True)
|
|
74
|
+
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
except asyncio.CancelledError:
|
|
79
|
+
proc.terminate()
|
|
80
|
+
raise
|
|
81
|
+
finally:
|
|
82
|
+
_claude_proc = None
|
|
83
|
+
|
|
84
|
+
await proc.wait()
|
|
85
|
+
print(f"\n{BLUE}{BOLD}└────────────────────────────────────────────────────────{R}\n")
|
|
86
|
+
return "\n".join(chunks) or "(no output)"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""API key resolution: env var → config file → interactive prompt."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_CONFIG_DIR = Path.home() / ".config" / "supervis"
|
|
7
|
+
_CONFIG_FILE = _CONFIG_DIR / "config"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _read_saved() -> str | None:
|
|
11
|
+
try:
|
|
12
|
+
for line in _CONFIG_FILE.read_text().splitlines():
|
|
13
|
+
if line.startswith("DEEPSEEK_API_KEY="):
|
|
14
|
+
return line.split("=", 1)[1].strip()
|
|
15
|
+
except FileNotFoundError:
|
|
16
|
+
pass
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _save(key: str) -> None:
|
|
21
|
+
_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
_CONFIG_FILE.write_text(f"DEEPSEEK_API_KEY={key}\n")
|
|
23
|
+
_CONFIG_FILE.chmod(0o600) # owner read/write only
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_api_key() -> str:
|
|
27
|
+
# 1. Environment variable (takes precedence)
|
|
28
|
+
key = os.environ.get("DEEPSEEK_API_KEY", "").strip()
|
|
29
|
+
if key:
|
|
30
|
+
return key
|
|
31
|
+
|
|
32
|
+
# 2. Saved config file
|
|
33
|
+
key = _read_saved()
|
|
34
|
+
if key:
|
|
35
|
+
return key
|
|
36
|
+
|
|
37
|
+
# 3. Interactive prompt — first run
|
|
38
|
+
print("\nNo DeepSeek API key found.")
|
|
39
|
+
print("Get one at: https://platform.deepseek.com/api-keys\n")
|
|
40
|
+
try:
|
|
41
|
+
key = input("Enter your API key: ").strip()
|
|
42
|
+
except (EOFError, KeyboardInterrupt):
|
|
43
|
+
print("\nCancelled.")
|
|
44
|
+
raise SystemExit(1)
|
|
45
|
+
|
|
46
|
+
if not key:
|
|
47
|
+
print("No key entered. Exiting.")
|
|
48
|
+
raise SystemExit(1)
|
|
49
|
+
|
|
50
|
+
_save(key)
|
|
51
|
+
print(f"Saved to {_CONFIG_FILE}\n")
|
|
52
|
+
return key
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Session token and cost tracking."""
|
|
2
|
+
|
|
3
|
+
# DeepSeek V3.2 pricing (per 1M tokens) — updated March 2026
|
|
4
|
+
_PRICE_INPUT = 0.28 # cache miss
|
|
5
|
+
_PRICE_INPUT_CACHED = 0.028 # cache hit (90% off)
|
|
6
|
+
_PRICE_OUTPUT = 0.42
|
|
7
|
+
|
|
8
|
+
_session_input = 0
|
|
9
|
+
_session_input_cached = 0
|
|
10
|
+
_session_output = 0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def record(input_tokens: int, output_tokens: int, cached_tokens: int = 0) -> None:
|
|
14
|
+
global _session_input, _session_input_cached, _session_output
|
|
15
|
+
_session_input += input_tokens - cached_tokens
|
|
16
|
+
_session_input_cached += cached_tokens
|
|
17
|
+
_session_output += output_tokens
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def session_cost() -> float:
|
|
21
|
+
return (
|
|
22
|
+
_session_input / 1_000_000 * _PRICE_INPUT +
|
|
23
|
+
_session_input_cached / 1_000_000 * _PRICE_INPUT_CACHED +
|
|
24
|
+
_session_output / 1_000_000 * _PRICE_OUTPUT
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def summary() -> str:
|
|
29
|
+
total_in = _session_input + _session_input_cached
|
|
30
|
+
cached = _session_input_cached
|
|
31
|
+
out = _session_output
|
|
32
|
+
cost = session_cost()
|
|
33
|
+
|
|
34
|
+
cached_note = f" {cached/1000:.1f}k cached" if cached else ""
|
|
35
|
+
return (
|
|
36
|
+
f"in {total_in/1000:.1f}k{cached_note} · "
|
|
37
|
+
f"out {out/1000:.1f}k · "
|
|
38
|
+
f"${cost:.4f}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def reset() -> None:
|
|
43
|
+
global _session_input, _session_input_cached, _session_output
|
|
44
|
+
_session_input = _session_input_cached = _session_output = 0
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""DeepSeek API client and streaming helper."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from openai import AsyncOpenAI
|
|
6
|
+
from .config import get_api_key
|
|
7
|
+
from .tools import TOOLS, execute_tool
|
|
8
|
+
from .display import CYAN, BOLD, DIM, R
|
|
9
|
+
from . import cost
|
|
10
|
+
|
|
11
|
+
_client: AsyncOpenAI | None = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_client() -> AsyncOpenAI:
|
|
15
|
+
global _client
|
|
16
|
+
if _client is None:
|
|
17
|
+
_client = AsyncOpenAI(api_key=get_api_key(), base_url="https://api.deepseek.com")
|
|
18
|
+
return _client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def stream_turn(messages: list) -> tuple[str, list]:
|
|
22
|
+
"""
|
|
23
|
+
Send messages to DeepSeek, stream response.
|
|
24
|
+
Returns (content, tool_calls).
|
|
25
|
+
"""
|
|
26
|
+
client = get_client()
|
|
27
|
+
|
|
28
|
+
print(f"\n{CYAN}{BOLD}DeepSeek:{R} ", end="", flush=True)
|
|
29
|
+
|
|
30
|
+
response = await client.chat.completions.create(
|
|
31
|
+
model="deepseek-chat",
|
|
32
|
+
messages=messages,
|
|
33
|
+
tools=TOOLS,
|
|
34
|
+
tool_choice="auto",
|
|
35
|
+
stream=True,
|
|
36
|
+
stream_options={"include_usage": True},
|
|
37
|
+
temperature=0.2,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
content = ""
|
|
41
|
+
tc_raw: dict[int, dict] = {}
|
|
42
|
+
|
|
43
|
+
async for chunk in response:
|
|
44
|
+
# Track usage from final chunk
|
|
45
|
+
if chunk.usage:
|
|
46
|
+
u = chunk.usage
|
|
47
|
+
cached = getattr(u.prompt_tokens_details, "cached_tokens", 0) or 0
|
|
48
|
+
cost.record(u.prompt_tokens, u.completion_tokens, cached)
|
|
49
|
+
print(f" {DIM}[{cost.summary()}]{R}", flush=True)
|
|
50
|
+
|
|
51
|
+
choice = chunk.choices[0] if chunk.choices else None
|
|
52
|
+
if not choice:
|
|
53
|
+
continue
|
|
54
|
+
delta = choice.delta
|
|
55
|
+
|
|
56
|
+
if delta.content:
|
|
57
|
+
print(f"{CYAN}{delta.content}{R}", end="", flush=True)
|
|
58
|
+
content += delta.content
|
|
59
|
+
|
|
60
|
+
if delta.tool_calls:
|
|
61
|
+
for tc in delta.tool_calls:
|
|
62
|
+
i = tc.index
|
|
63
|
+
if i not in tc_raw:
|
|
64
|
+
tc_raw[i] = {"id": "", "name": "", "arguments": ""}
|
|
65
|
+
if tc.id:
|
|
66
|
+
tc_raw[i]["id"] = tc.id
|
|
67
|
+
if tc.function:
|
|
68
|
+
if tc.function.name:
|
|
69
|
+
tc_raw[i]["name"] = tc.function.name
|
|
70
|
+
if tc.function.arguments:
|
|
71
|
+
tc_raw[i]["arguments"] += tc.function.arguments
|
|
72
|
+
|
|
73
|
+
if content:
|
|
74
|
+
print()
|
|
75
|
+
|
|
76
|
+
tool_calls = list(tc_raw.values())
|
|
77
|
+
return content, tool_calls
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def run_agent_loop(messages: list, interrupt_event: asyncio.Event | None = None) -> list:
|
|
81
|
+
"""
|
|
82
|
+
Agentic loop: keep calling DeepSeek + executing tools until
|
|
83
|
+
DeepSeek stops making tool calls or produces a user-facing response.
|
|
84
|
+
Returns updated messages list.
|
|
85
|
+
"""
|
|
86
|
+
while True:
|
|
87
|
+
content, tool_calls = await stream_turn(messages)
|
|
88
|
+
|
|
89
|
+
# DeepSeek spoke AND wants to call tools → return to user first
|
|
90
|
+
if content and tool_calls:
|
|
91
|
+
messages.append({"role": "assistant", "content": content})
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
messages.append({
|
|
95
|
+
"role": "assistant",
|
|
96
|
+
"content": content or None,
|
|
97
|
+
"tool_calls": [
|
|
98
|
+
{
|
|
99
|
+
"id": tc["id"],
|
|
100
|
+
"type": "function",
|
|
101
|
+
"function": {"name": tc["name"], "arguments": tc["arguments"]},
|
|
102
|
+
}
|
|
103
|
+
for tc in tool_calls
|
|
104
|
+
] if tool_calls else None,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# No tool calls → DeepSeek is done, return to user
|
|
108
|
+
if not tool_calls:
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
# Check for interrupt before executing tools
|
|
112
|
+
if interrupt_event and interrupt_event.is_set():
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
# Execute tools, append results, loop
|
|
116
|
+
print()
|
|
117
|
+
for tc in tool_calls:
|
|
118
|
+
try:
|
|
119
|
+
args = json.loads(tc["arguments"]) if tc["arguments"] else {}
|
|
120
|
+
except json.JSONDecodeError:
|
|
121
|
+
args = {}
|
|
122
|
+
|
|
123
|
+
result = await execute_tool(tc["name"], args)
|
|
124
|
+
messages.append({
|
|
125
|
+
"role": "tool",
|
|
126
|
+
"tool_call_id": tc["id"],
|
|
127
|
+
"content": str(result),
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
return messages
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Terminal display helpers — colors, banners, tags."""
|
|
2
|
+
|
|
3
|
+
R = "\033[0m"
|
|
4
|
+
CYAN = "\033[36m"
|
|
5
|
+
YELLOW = "\033[33m"
|
|
6
|
+
GREEN = "\033[32m"
|
|
7
|
+
BLUE = "\033[34m"
|
|
8
|
+
MAGENTA = "\033[35m"
|
|
9
|
+
BOLD = "\033[1m"
|
|
10
|
+
DIM = "\033[2m"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def banner(text: str, color: str = CYAN) -> None:
|
|
14
|
+
print(f"\n{color}{BOLD}{'─' * 60}{R}")
|
|
15
|
+
print(f"{color}{BOLD} {text}{R}")
|
|
16
|
+
print(f"{color}{BOLD}{'─' * 60}{R}", flush=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def header() -> None:
|
|
20
|
+
print(f"\n{CYAN}{BOLD}╔══════════════════════════════════════════════════════╗{R}")
|
|
21
|
+
print(f"{CYAN}{BOLD}║ DeepSeek Supervisor × Claude Code ║{R}")
|
|
22
|
+
print(f"{CYAN}{BOLD}╚══════════════════════════════════════════════════════╝{R}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def tool_tag(label: str) -> None:
|
|
26
|
+
print(f"{MAGENTA}{DIM}[{label}]{R} ", flush=True)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
import readline # noqa: F401 — enables arrow keys + history in input()
|
|
11
|
+
|
|
12
|
+
project_dir = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
|
|
13
|
+
project_dir = str(Path(project_dir).resolve())
|
|
14
|
+
|
|
15
|
+
if not Path(project_dir).is_dir():
|
|
16
|
+
print(f"Directory not found: {project_dir}")
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
from .chat import chat_loop
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
asyncio.run(chat_loop(project_dir))
|
|
23
|
+
except KeyboardInterrupt:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Conversation history management and summarization."""
|
|
2
|
+
|
|
3
|
+
from openai import AsyncOpenAI
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def summarize_if_needed(
|
|
7
|
+
messages: list,
|
|
8
|
+
client: AsyncOpenAI,
|
|
9
|
+
threshold: int = 20,
|
|
10
|
+
) -> list:
|
|
11
|
+
"""When history exceeds threshold, summarize older messages to save tokens."""
|
|
12
|
+
user_msgs = [m for m in messages if m["role"] != "system"]
|
|
13
|
+
if len(user_msgs) <= threshold:
|
|
14
|
+
return messages
|
|
15
|
+
|
|
16
|
+
to_summarize = messages[1:-8]
|
|
17
|
+
if not to_summarize:
|
|
18
|
+
return messages
|
|
19
|
+
|
|
20
|
+
print("\n\033[2m[Summarizing conversation history...]\033[0m", flush=True)
|
|
21
|
+
try:
|
|
22
|
+
resp = await client.chat.completions.create(
|
|
23
|
+
model="deepseek-chat",
|
|
24
|
+
messages=[
|
|
25
|
+
{
|
|
26
|
+
"role": "system",
|
|
27
|
+
"content": (
|
|
28
|
+
"Summarize this conversation history concisely. "
|
|
29
|
+
"Preserve key decisions, code changes made, file names, and important context. "
|
|
30
|
+
"Be brief."
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"role": "user",
|
|
35
|
+
"content": str(to_summarize)[:8000],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
max_tokens=600,
|
|
39
|
+
)
|
|
40
|
+
summary = resp.choices[0].message.content
|
|
41
|
+
return [
|
|
42
|
+
messages[0],
|
|
43
|
+
{"role": "assistant", "content": f"[Session summary: {summary}]"},
|
|
44
|
+
*messages[-8:],
|
|
45
|
+
]
|
|
46
|
+
except Exception:
|
|
47
|
+
return messages
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""System prompts."""
|
|
2
|
+
|
|
3
|
+
SYSTEM_PROMPT = """You are a senior software architect and technical lead. You supervise Claude Code, an AI coding assistant that reads, writes, and executes code autonomously.
|
|
4
|
+
|
|
5
|
+
## Your role
|
|
6
|
+
Help the user with their software projects through natural conversation. Use Claude Code as your implementation arm — you plan and verify, Claude Code executes.
|
|
7
|
+
|
|
8
|
+
## Workflow
|
|
9
|
+
1. Understand the user's request. Ask ONE clarifying question only if the goal is genuinely ambiguous.
|
|
10
|
+
2. Explore the codebase before touching anything (read_file, list_files, search_code).
|
|
11
|
+
3. Write precise, detailed prompts for Claude Code — include exact file names, function signatures, requirements, and acceptance criteria.
|
|
12
|
+
4. After Claude Code runs, verify with get_git_status and read changed files.
|
|
13
|
+
5. If something went wrong, correct course immediately with a follow-up prompt to Claude Code.
|
|
14
|
+
6. Report back to the user with a concise summary of what changed.
|
|
15
|
+
|
|
16
|
+
## Decision making
|
|
17
|
+
- Make small decisions yourself (naming, structure, patterns) — don't ask the user.
|
|
18
|
+
- Only surface genuine architectural trade-offs (e.g. "JWT vs session-based auth?") where the user's preference matters.
|
|
19
|
+
- If the user changes direction mid-task, adapt immediately without complaint.
|
|
20
|
+
|
|
21
|
+
## Claude Code prompts
|
|
22
|
+
Be specific and complete. Example:
|
|
23
|
+
"In src/auth/middleware.py, add a function `require_auth(f)` decorator that checks for a valid JWT in the Authorization header. Use the existing `verify_token()` from src/auth/tokens.py. Return 401 JSON on failure."
|
|
24
|
+
|
|
25
|
+
## Language
|
|
26
|
+
- Detect the user's language from their message and always reply in that same language.
|
|
27
|
+
- If the user writes in Turkish, respond in Turkish. If English, respond in English. Follow their lead on every message.
|
|
28
|
+
- Keep responses concise. Lead with action, not explanation."""
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Tool definitions and implementations for DeepSeek."""
|
|
2
|
+
|
|
3
|
+
import glob as _glob
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from .claude import run_claude
|
|
7
|
+
from .display import MAGENTA, DIM, R
|
|
8
|
+
|
|
9
|
+
# ─── Definitions ─────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
TOOLS = [
|
|
12
|
+
{
|
|
13
|
+
"type": "function",
|
|
14
|
+
"function": {
|
|
15
|
+
"name": "read_file",
|
|
16
|
+
"description": "Read a file's contents from the project.",
|
|
17
|
+
"parameters": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"path": {"type": "string", "description": "Relative or absolute file path"}
|
|
21
|
+
},
|
|
22
|
+
"required": ["path"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "function",
|
|
28
|
+
"function": {
|
|
29
|
+
"name": "list_files",
|
|
30
|
+
"description": "List files matching a glob pattern. Example: 'src/**/*.py'",
|
|
31
|
+
"parameters": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"pattern": {"type": "string"}
|
|
35
|
+
},
|
|
36
|
+
"required": ["pattern"],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"type": "function",
|
|
42
|
+
"function": {
|
|
43
|
+
"name": "search_code",
|
|
44
|
+
"description": "Search for a text pattern in the codebase (grep).",
|
|
45
|
+
"parameters": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"pattern": {"type": "string"},
|
|
49
|
+
"path": {"type": "string", "default": "."},
|
|
50
|
+
},
|
|
51
|
+
"required": ["pattern"],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"type": "function",
|
|
57
|
+
"function": {
|
|
58
|
+
"name": "run_shell",
|
|
59
|
+
"description": "Run a read-only shell command (ls, git log, cat, etc.).",
|
|
60
|
+
"parameters": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"command": {"type": "string"}
|
|
64
|
+
},
|
|
65
|
+
"required": ["command"],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "function",
|
|
71
|
+
"function": {
|
|
72
|
+
"name": "get_git_status",
|
|
73
|
+
"description": "Get git status and diff to see what Claude Code changed.",
|
|
74
|
+
"parameters": {"type": "object", "properties": {}},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"type": "function",
|
|
79
|
+
"function": {
|
|
80
|
+
"name": "run_claude",
|
|
81
|
+
"description": (
|
|
82
|
+
"Send a task to Claude Code. It will write/edit/run files autonomously. "
|
|
83
|
+
"Be specific: include file names, function signatures, and acceptance criteria."
|
|
84
|
+
),
|
|
85
|
+
"parameters": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"prompt": {"type": "string"},
|
|
89
|
+
"continue_session": {
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"description": "True to continue previous Claude session (keeps context).",
|
|
92
|
+
"default": True,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
"required": ["prompt"],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# ─── Implementations ─────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
_SKIP = {".git/", "__pycache__", "node_modules", ".venv"}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_file(path: str) -> str:
|
|
107
|
+
try:
|
|
108
|
+
content = Path(path).read_text(encoding="utf-8")
|
|
109
|
+
lines = content.splitlines()
|
|
110
|
+
if len(lines) > 300:
|
|
111
|
+
return "\n".join(lines[:300]) + f"\n... ({len(lines)} lines total)"
|
|
112
|
+
return content
|
|
113
|
+
except Exception as e:
|
|
114
|
+
return f"Error: {e}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _list_files(pattern: str) -> str:
|
|
118
|
+
files = _glob.glob(pattern, recursive=True)
|
|
119
|
+
files = [f for f in files if not any(s in f for s in _SKIP)]
|
|
120
|
+
return "\n".join(sorted(files)[:100]) if files else "No files found."
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _search_code(pattern: str, path: str = ".") -> str:
|
|
124
|
+
try:
|
|
125
|
+
result = subprocess.run(
|
|
126
|
+
["grep", "-r", "-n", pattern, path],
|
|
127
|
+
capture_output=True, text=True, timeout=10,
|
|
128
|
+
)
|
|
129
|
+
lines = [l for l in result.stdout.splitlines() if not any(s in l for s in _SKIP)]
|
|
130
|
+
return "\n".join(lines[:80]) if lines else "No matches."
|
|
131
|
+
except Exception as e:
|
|
132
|
+
return f"Error: {e}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _run_shell(command: str) -> str:
|
|
136
|
+
try:
|
|
137
|
+
result = subprocess.run(
|
|
138
|
+
command, shell=True, capture_output=True, text=True, timeout=15
|
|
139
|
+
)
|
|
140
|
+
out = (result.stdout + result.stderr).strip()
|
|
141
|
+
return out[:3000] if out else "(no output)"
|
|
142
|
+
except Exception as e:
|
|
143
|
+
return f"Error: {e}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _get_git_status() -> str:
|
|
147
|
+
status = _run_shell("git status --short")
|
|
148
|
+
diff = _run_shell("git diff --stat HEAD 2>/dev/null || git diff --stat")
|
|
149
|
+
log = _run_shell("git log --oneline -5 2>/dev/null || echo 'no git log'")
|
|
150
|
+
return f"=== Status ===\n{status}\n\n=== Diff ===\n{diff}\n\n=== Recent commits ===\n{log}"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ─── Dispatcher ──────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
async def execute_tool(name: str, args: dict) -> str:
|
|
156
|
+
labels = {
|
|
157
|
+
"read_file": f"📄 {args.get('path', '')}",
|
|
158
|
+
"list_files": f"📁 {args.get('pattern', '')}",
|
|
159
|
+
"search_code": f"🔍 '{args.get('pattern', '')}'",
|
|
160
|
+
"run_shell": f"$ {args.get('command', '')[:50]}",
|
|
161
|
+
"get_git_status": "git status",
|
|
162
|
+
"run_claude": "→ Claude Code",
|
|
163
|
+
}
|
|
164
|
+
if name != "run_claude":
|
|
165
|
+
print(f"{MAGENTA}{DIM}[{labels.get(name, name)}]{R}", flush=True)
|
|
166
|
+
|
|
167
|
+
match name:
|
|
168
|
+
case "read_file":
|
|
169
|
+
return _read_file(args["path"])
|
|
170
|
+
case "list_files":
|
|
171
|
+
return _list_files(args["pattern"])
|
|
172
|
+
case "search_code":
|
|
173
|
+
return _search_code(args["pattern"], args.get("path", "."))
|
|
174
|
+
case "run_shell":
|
|
175
|
+
return _run_shell(args["command"])
|
|
176
|
+
case "get_git_status":
|
|
177
|
+
return _get_git_status()
|
|
178
|
+
case "run_claude":
|
|
179
|
+
return await run_claude(args["prompt"], args.get("continue_session", True))
|
|
180
|
+
case _:
|
|
181
|
+
return f"Unknown tool: {name}"
|