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 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.
@@ -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,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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ supervis = supervisor.main:main
@@ -0,0 +1 @@
1
+ openai>=2.0.0
@@ -0,0 +1 @@
1
+ supervisor
@@ -0,0 +1,3 @@
1
+ """DeepSeek Supervisor — Claude Code orchestrator."""
2
+
3
+ __version__ = "0.1.0"
@@ -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}"