mlx-code 0.0.27__tar.gz → 0.0.29__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 (32) hide show
  1. {mlx_code-0.0.27 → mlx_code-0.0.29}/PKG-INFO +26 -13
  2. {mlx_code-0.0.27 → mlx_code-0.0.29}/README.md +25 -12
  3. mlx_code-0.0.29/mlx_code/bare.py +276 -0
  4. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/main.py +9 -2
  5. mlx_code-0.0.29/mlx_code/repl.py +808 -0
  6. mlx_code-0.0.29/mlx_code/tui.py +576 -0
  7. mlx_code-0.0.29/mlx_code/web.py +485 -0
  8. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/PKG-INFO +26 -13
  9. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/SOURCES.txt +2 -0
  10. {mlx_code-0.0.27 → mlx_code-0.0.29}/setup.py +1 -2
  11. mlx_code-0.0.27/mlx_code/bare.py +0 -484
  12. mlx_code-0.0.27/mlx_code/repl.py +0 -1149
  13. {mlx_code-0.0.27 → mlx_code-0.0.29}/LICENSE +0 -0
  14. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/__init__.py +0 -0
  15. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/apis.py +0 -0
  16. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/bats.py +0 -0
  17. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/gits.py +0 -0
  18. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/lsp_tool.py +0 -0
  19. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/mcb.py +0 -0
  20. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/mcb_tool.py +0 -0
  21. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/stream_log.py +0 -0
  22. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/tools.py +0 -0
  23. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/util.py +0 -0
  24. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/view_git.py +0 -0
  25. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code/view_log.py +0 -0
  26. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/dependency_links.txt +0 -0
  27. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/entry_points.txt +0 -0
  28. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/requires.txt +0 -0
  29. {mlx_code-0.0.27 → mlx_code-0.0.29}/mlx_code.egg-info/top_level.txt +0 -0
  30. {mlx_code-0.0.27 → mlx_code-0.0.29}/setup.cfg +0 -0
  31. {mlx_code-0.0.27 → mlx_code-0.0.29}/tests/__init__.py +0 -0
  32. {mlx_code-0.0.27 → mlx_code-0.0.29}/tests/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlx-code
3
- Version: 0.0.27
3
+ Version: 0.0.29
4
4
  Summary: Coding Agent for Mac
5
5
  Home-page: https://josefalbers.github.io/mlx-code/
6
6
  Author: J Joe
@@ -40,7 +40,7 @@ Dynamic: summary
40
40
 
41
41
  A Git-native coding agent that can run entirely on your Mac. No API keys, no cloud, and no data leaving your machine. Powered by Apple MLX, it turns commits, branches, and worktrees into the agent’s state, history, and execution model
42
42
 
43
- [![Link](https://raw.githubusercontent.com/JosefAlbers/mlx-code/main/assets/mlx-code-v0.0.20.gif)](https://youtu.be/0lkY7YQCyCo)
43
+ [![v0.0.27](https://github.com/user-attachments/assets/8a1c131a-dda1-4b52-9fa6-9c0fbccb5ea6)](https://youtube.com/shorts/1LuifKFKixc)
44
44
 
45
45
  ---
46
46
 
@@ -49,7 +49,7 @@ A Git-native coding agent that can run entirely on your Mac. No API keys, no clo
49
49
  ```
50
50
  Worktrees:
51
51
 
52
- main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat history
52
+ main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat hx
53
53
  │ │
54
54
  │ └── branch-1 ──●──●──●
55
55
  │ │ ┌────────────┐
@@ -57,16 +57,14 @@ Worktrees:
57
57
  │ └─────┬──────┘
58
58
  └── branch-0 ──●──●──● │
59
59
 
60
-
61
60
  Tabs: ├────────────► Tab = git branch + Agent
62
61
 
63
-
64
- ┌──────────────────────────────────────────────┼─────────┐
62
+ ┌──────────────────────────────────────────────│─────────┐
65
63
  │ TUI tabs │ │
66
64
  │ ┌──────┐ ┌──────────┐ ┌──────────┐ ┌─────┴──────┐ │
67
65
  │ │ main │ │ branch-0 │ │ branch-1 │ │ branch-1-0 │ │
68
66
  │ └──────┘ └────┬─────┘ └──────────┘ └────────────┘ │
69
- └─────────────────┼──────────────────────────────────────┘
67
+ └─────────────────│──────────────────────────────────────┘
70
68
 
71
69
  Agents: ├─────────────────────────────────────────► Each tab runs its own Agent
72
70
 
@@ -522,10 +520,10 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
522
520
 
523
521
  | Backend | Flag | Notes |
524
522
  |---------|------|-------|
525
- | MLX (local) | `--api noapi` | Default. Runs on-device, no API key needed |
523
+ | MLX-LM (local) | `--api noapi` | Default. Runs on-device, no API key needed |
526
524
  | Claude | `--api claude` | Requires `ANTHROPIC_API_KEY` |
527
525
  | Gemini | `--api gemini` | Requires `GOOGLE_API_KEY` |
528
- | DeepSeek | `--api deepseek` | DeepSeek API or compatible endpoint |
526
+ | DeepSeek | `--api deepseek` | Requires `DEEPSEEK_API_KEY` |
529
527
  | Codex | `--api codex` | OpenAI Codex CLI integration |
530
528
  | OpenAI | `--api openai` | Any OpenAI-compatible endpoint |
531
529
 
@@ -534,10 +532,25 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
534
532
  The local MLX server speaks OpenAI, Anthropic, and Gemini wire formats simultaneously, so you can use any compatible CLI as the frontend:
535
533
 
536
534
  ```bash
537
- mlc --leash claude # claude CLI routes through local model
538
- mlc --leash codex # codex CLI routes through local model
539
- mlc --leash gemini # gemini CLI routes through local model
540
- mlc --leash none # server only
535
+ mlc # default
536
+ mlc --web # web UI (api.mlx-code.com)
537
+ mlc --bare # no TUI
538
+ mlc --leash none # no harness
539
+ mlc --leash codex # codex CLI
540
+ mlc --leash gemini # gemini CLI
541
+ mlc --leash claude # claude code
542
+ ```
543
+
544
+ #### WebUI
545
+
546
+ ```bash
547
+ [protect & connect]-[networking]-[tunnels]-[add route]-[add published application]:
548
+ - subdomain: jjoe
549
+ - domain: mlx-code.com
550
+ - service url: http://host.containers.internal:8080
551
+ mlc --host 0.0.0.0 --engine batch --web &
552
+ podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_CFD_TOKEN
553
+ phone http://jjoe.mlx-code.com
541
554
  ```
542
555
 
543
556
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  A Git-native coding agent that can run entirely on your Mac. No API keys, no cloud, and no data leaving your machine. Powered by Apple MLX, it turns commits, branches, and worktrees into the agent’s state, history, and execution model
4
4
 
5
- [![Link](https://raw.githubusercontent.com/JosefAlbers/mlx-code/main/assets/mlx-code-v0.0.20.gif)](https://youtu.be/0lkY7YQCyCo)
5
+ [![v0.0.27](https://github.com/user-attachments/assets/8a1c131a-dda1-4b52-9fa6-9c0fbccb5ea6)](https://youtube.com/shorts/1LuifKFKixc)
6
6
 
7
7
  ---
8
8
 
@@ -11,7 +11,7 @@ A Git-native coding agent that can run entirely on your Mac. No API keys, no clo
11
11
  ```
12
12
  Worktrees:
13
13
 
14
- main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat history
14
+ main ──●──●──●──●──●──●──●──●──●──●──●──●──●──●───────────► Node = git commit + chat hx
15
15
  │ │
16
16
  │ └── branch-1 ──●──●──●
17
17
  │ │ ┌────────────┐
@@ -19,16 +19,14 @@ Worktrees:
19
19
  │ └─────┬──────┘
20
20
  └── branch-0 ──●──●──● │
21
21
 
22
-
23
22
  Tabs: ├────────────► Tab = git branch + Agent
24
23
 
25
-
26
- ┌──────────────────────────────────────────────┼─────────┐
24
+ ┌──────────────────────────────────────────────│─────────┐
27
25
  │ TUI tabs │ │
28
26
  │ ┌──────┐ ┌──────────┐ ┌──────────┐ ┌─────┴──────┐ │
29
27
  │ │ main │ │ branch-0 │ │ branch-1 │ │ branch-1-0 │ │
30
28
  │ └──────┘ └────┬─────┘ └──────────┘ └────────────┘ │
31
- └─────────────────┼──────────────────────────────────────┘
29
+ └─────────────────│──────────────────────────────────────┘
32
30
 
33
31
  Agents: ├─────────────────────────────────────────► Each tab runs its own Agent
34
32
 
@@ -484,10 +482,10 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
484
482
 
485
483
  | Backend | Flag | Notes |
486
484
  |---------|------|-------|
487
- | MLX (local) | `--api noapi` | Default. Runs on-device, no API key needed |
485
+ | MLX-LM (local) | `--api noapi` | Default. Runs on-device, no API key needed |
488
486
  | Claude | `--api claude` | Requires `ANTHROPIC_API_KEY` |
489
487
  | Gemini | `--api gemini` | Requires `GOOGLE_API_KEY` |
490
- | DeepSeek | `--api deepseek` | DeepSeek API or compatible endpoint |
488
+ | DeepSeek | `--api deepseek` | Requires `DEEPSEEK_API_KEY` |
491
489
  | Codex | `--api codex` | OpenAI Codex CLI integration |
492
490
  | OpenAI | `--api openai` | Any OpenAI-compatible endpoint |
493
491
 
@@ -496,10 +494,25 @@ All file tools enforce path sandboxing. The agent cannot read or write outside t
496
494
  The local MLX server speaks OpenAI, Anthropic, and Gemini wire formats simultaneously, so you can use any compatible CLI as the frontend:
497
495
 
498
496
  ```bash
499
- mlc --leash claude # claude CLI routes through local model
500
- mlc --leash codex # codex CLI routes through local model
501
- mlc --leash gemini # gemini CLI routes through local model
502
- mlc --leash none # server only
497
+ mlc # default
498
+ mlc --web # web UI (api.mlx-code.com)
499
+ mlc --bare # no TUI
500
+ mlc --leash none # no harness
501
+ mlc --leash codex # codex CLI
502
+ mlc --leash gemini # gemini CLI
503
+ mlc --leash claude # claude code
504
+ ```
505
+
506
+ #### WebUI
507
+
508
+ ```bash
509
+ [protect & connect]-[networking]-[tunnels]-[add route]-[add published application]:
510
+ - subdomain: jjoe
511
+ - domain: mlx-code.com
512
+ - service url: http://host.containers.internal:8080
513
+ mlc --host 0.0.0.0 --engine batch --web &
514
+ podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_CFD_TOKEN
515
+ phone http://jjoe.mlx-code.com
503
516
  ```
504
517
 
505
518
  ---
@@ -0,0 +1,276 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+ import datetime
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ import logging
9
+ from typing import Callable
10
+ from .repl import Agent, TabModel, CommandEngine, UIAdapter, HELP_TEXT
11
+ from .gits import GitError, get_branch_base_sha, get_diff_between_refs, get_commit_history_with_stats, find_rev_commit, create_worktree, git_new_branch, git_new_branch_at
12
+ logger = logging.getLogger(__name__)
13
+
14
+ class BareAdapter:
15
+
16
+ def __init__(self, repl: 'BareRepl'):
17
+ self.repl = repl
18
+
19
+ def show_error(self, text: str) -> None:
20
+ print(f'\n✗ {text}', flush=True)
21
+
22
+ def show_command_result(self, cmd: str, content: str | object) -> None:
23
+ if isinstance(content, str):
24
+ print(content)
25
+ else:
26
+ print(str(content))
27
+
28
+ def show_diff(self, diff_text: str, ref1_label: str, ref2_label: str) -> None:
29
+ print(diff_text)
30
+
31
+ def show_history_list(self, lines: list[str]) -> None:
32
+ print('\n'.join(lines))
33
+
34
+ def show_history_raw(self, json_text: str) -> None:
35
+ print(json_text)
36
+
37
+ async def add_tab(self, tab: TabModel) -> None:
38
+ pass
39
+
40
+ def remove_tab(self, removed_index: int) -> None:
41
+ if self.repl.engine.active_index >= len(self.repl.engine.tabs):
42
+ self.repl.engine.active_index = len(self.repl.engine.tabs) - 1
43
+
44
+ def switch_to_tab(self, index: int) -> None:
45
+ self.repl._render_tab_delimiter()
46
+ self.repl._print_history_for_tab(self.repl.engine.tabs[index])
47
+
48
+ def refresh_chrome(self) -> None:
49
+ pass
50
+
51
+ def clear_tab_display(self, tab: TabModel) -> None:
52
+ pass
53
+
54
+ def on_agent_event(self, event: dict, tab: TabModel) -> None:
55
+ self.repl._handle_event(event, tab)
56
+
57
+ async def run_captured_shell(self, command: str, cwd: str, env: dict | None) -> str:
58
+ proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env)
59
+ stdout, stderr = await proc.communicate()
60
+ out = stdout.decode(errors='replace').rstrip('\n')
61
+ err = stderr.decode(errors='replace').rstrip('\n')
62
+ body = out
63
+ if err:
64
+ body = (body + '\n' if body else '') + f'[stderr]\n{err}'
65
+ if proc.returncode:
66
+ body += f'\n[exit {proc.returncode}]'
67
+ return body
68
+
69
+ async def run_interactive_shell(self, command: str, cwd: str, env: dict | None) -> int:
70
+ proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdin=None, stdout=None, stderr=None, env=env)
71
+ await proc.wait()
72
+ return proc.returncode or 0
73
+
74
+ def exit_app(self, summary: list[dict]) -> None:
75
+ raise SystemExit
76
+
77
+ class BareRepl:
78
+
79
+ def __init__(self, engine: CommandEngine, init_prompt: str | None=None):
80
+ self.engine = engine
81
+ self.adapter = BareAdapter(self)
82
+ self.engine.bind(self.adapter)
83
+ self.engine.attach_agent(self.engine.tabs[0])
84
+ self.init_prompt = init_prompt
85
+ self._pending_nls: int = 0
86
+ self._awaiting_content: bool = False
87
+ self._has_output: bool = False
88
+ self._last_stream_type: str | None = None
89
+
90
+ async def run(self) -> None:
91
+ loop = asyncio.get_running_loop()
92
+ if self.init_prompt:
93
+ p, self.init_prompt = (self.init_prompt, None)
94
+ await self.engine.active_tab.agent.run(p)
95
+ while True:
96
+ try:
97
+ line = await loop.run_in_executor(None, self._read_input)
98
+ except KeyboardInterrupt:
99
+ print('\n(Use /exit or Ctrl-D to quit)')
100
+ continue
101
+ except EOFError:
102
+ print()
103
+ break
104
+ if line is None:
105
+ break
106
+ line = line.strip()
107
+ if not line:
108
+ continue
109
+ if line.lower() in {'exit', 'quit'}:
110
+ break
111
+ await self.engine.handle_input(line)
112
+ tab = self.engine.active_tab
113
+ if tab.running_task is not None:
114
+ try:
115
+ await tab.running_task
116
+ except asyncio.CancelledError:
117
+ pass
118
+
119
+ def _read_input(self) -> str | None:
120
+ tab = self.engine.active_tab
121
+ prompt = f'[{tab.title}] ≫ '
122
+ lines: list[str] = []
123
+ while True:
124
+ try:
125
+ line = input(prompt)
126
+ except EOFError:
127
+ return None
128
+ lines.append(line)
129
+ if line.endswith('\\'):
130
+ lines[-1] = line[:-1]
131
+ prompt = '... '
132
+ else:
133
+ break
134
+ return '\n'.join(lines)
135
+
136
+ def _handle_event(self, event: dict, tab: TabModel) -> None:
137
+ t, p = (event['type'], event.get('payload', {}))
138
+ if t in ('text_delta', 'thinking_delta'):
139
+ delta = p.get('delta', '')
140
+ if delta:
141
+ self._write_delta(delta, t)
142
+ elif t == 'tool_start':
143
+ self._pending_nls = 0
144
+ self._awaiting_content = False
145
+ self._has_output = True
146
+ self._last_stream_type = t
147
+ elif t == 'tool_end':
148
+ result_msg = p.get('result', {})
149
+ content = result_msg.get('content')
150
+ is_err = p.get('is_error', False)
151
+ out_text = ''
152
+ if content:
153
+ parts: list[str] = []
154
+ if isinstance(content, str):
155
+ parts.append(content)
156
+ elif isinstance(content, list):
157
+ for block in content:
158
+ if isinstance(block, dict) and block.get('type') == 'text':
159
+ parts.append(block.get('text', ''))
160
+ out_text = '\n'.join(parts).strip('\n')
161
+ if is_err:
162
+ prefix = '✗ '
163
+ if not out_text:
164
+ out_text = f'{p.get('name', '?')} failed'
165
+ else:
166
+ prefix = '→ ' if out_text else ''
167
+ if out_text:
168
+ self._write_delta(prefix + out_text, 'tool_result')
169
+ self._last_stream_type = t
170
+ print()
171
+ elif t == 'commit':
172
+ self._pending_nls = 0
173
+ self._awaiting_content = False
174
+ self._has_output = True
175
+ print(f'\n◇ [{p.get('sha', '')}] committed', flush=True)
176
+ self._last_stream_type = t
177
+ elif t == 'error':
178
+ self._pending_nls = 0
179
+ self._awaiting_content = False
180
+ self._has_output = True
181
+ err = str(p.get('error', p))
182
+ print(f'\n✗ {err}', flush=True)
183
+ self._last_stream_type = t
184
+ elif t in ('agent_start', 'turn_start'):
185
+ self._pending_nls = 0
186
+ self._awaiting_content = False
187
+ self._has_output = False
188
+ self._last_stream_type = None
189
+ elif t == 'agent_end':
190
+ self._pending_nls = 0
191
+ if self._has_output:
192
+ print()
193
+ self._last_stream_type = None
194
+ self._has_output = False
195
+ self._awaiting_content = False
196
+
197
+ def _write_delta(self, text: str, delta_type: str) -> None:
198
+ if delta_type != self._last_stream_type:
199
+ self._pending_nls = 0
200
+ self._awaiting_content = True
201
+ self._last_stream_type = delta_type
202
+ if self._awaiting_content:
203
+ text = text.lstrip('\n')
204
+ if not text:
205
+ return
206
+ if self._awaiting_content:
207
+ if self._has_output:
208
+ print()
209
+ self._awaiting_content = False
210
+ if not self._awaiting_content and self._pending_nls > 0:
211
+ print('\n' * self._pending_nls, end='', flush=True)
212
+ self._pending_nls = 0
213
+ rstripped = text.rstrip('\n')
214
+ if rstripped:
215
+ if delta_type == 'thinking_delta':
216
+ print(f'\x1b[2m{rstripped}\x1b[0m', end='', flush=True)
217
+ else:
218
+ print(rstripped, end='', flush=True)
219
+ self._has_output = True
220
+ self._pending_nls = len(text) - len(rstripped)
221
+
222
+ def _render_tab_delimiter(self) -> None:
223
+ tab_strs: list[str] = []
224
+ for i, t in enumerate(self.engine.tabs):
225
+ if i == self.engine.active_index:
226
+ tab_strs.append(f'\x1b[1m▶ {i + 1}. {t.title}\x1b[0m')
227
+ else:
228
+ tab_strs.append(f'\x1b[2m▷ {i + 1}. {t.title}\x1b[0m')
229
+ print('\n' + '┗━━┫ ' + ' ┃ '.join(tab_strs) + ' ┃')
230
+
231
+ def _print_history_for_tab(self, tab: TabModel) -> None:
232
+ for msg in tab.agent.messages:
233
+ role = msg.get('role', '')
234
+ content = msg.get('content', '')
235
+ is_error = msg.get('is_error', False)
236
+ if isinstance(content, list):
237
+ blocks = content
238
+ elif isinstance(content, str):
239
+ blocks = [{'type': 'text', 'text': content}]
240
+ else:
241
+ continue
242
+ if role == 'toolResult':
243
+ parts: list[str] = []
244
+ for block in blocks:
245
+ if isinstance(block, dict) and block.get('type') == 'text':
246
+ t = block.get('text', '').strip('\n')
247
+ if t:
248
+ parts.append(t)
249
+ if parts:
250
+ prefix = '✗ ' if is_error else '→ '
251
+ print(prefix + '\n'.join(parts))
252
+ continue
253
+ for block in blocks:
254
+ btype = block.get('type', 'text')
255
+ if btype == 'toolCall':
256
+ args = block.get('arguments', {})
257
+ if isinstance(args, dict):
258
+ args = json.dumps(args, ensure_ascii=False)
259
+ print(f'⚙ {block.get('name', '')} {args}')
260
+ continue
261
+ text = block.get('text', '') or block.get('thinking', '') or ''
262
+ text = text.strip('\n')
263
+ if not text:
264
+ continue
265
+ if btype == 'thinking':
266
+ print(f'\x1b[2m{text}\x1b[0m')
267
+ elif is_error:
268
+ print(f'✗ {text}')
269
+ elif role == 'user':
270
+ print(f'≫ {text}')
271
+ elif role == 'commit':
272
+ print(f'◇ {text}')
273
+ elif role == 'toolResult':
274
+ print(f'→ {text}')
275
+ else:
276
+ print(text)
@@ -944,6 +944,8 @@ def main():
944
944
  parser.add_argument('--skips', nargs='+', default=['(?m)^\\[SUGGESTION MODE[\\s\\S]*', '(?m)^<system-reminder>[\\s\\S]*?^</system-reminder>\\s*'], help='Regex patterns stripped from model output before it is returned to the client')
945
945
  parser.add_argument('--stream', default=None, help='File to stream log into')
946
946
  parser.add_argument('--bare', action='store_true', help='Use simple terminal REPL instead of TUI')
947
+ parser.add_argument('--web', action='store_true', help='Use web UI instead of TUI')
948
+ parser.add_argument('--web-port', type=int, default=None, help='Port for web UI (default: inference port + 80)')
947
949
  args, leash_args = parser.parse_known_args()
948
950
  logger.debug(f'args={args!r} leash_args={leash_args!r}')
949
951
  if args.engine == 'batch' and args.leash not in ('none', 'noapi'):
@@ -979,8 +981,13 @@ def main():
979
981
  if args.engine == 'cache':
980
982
  threading.Thread(target=server.serve_forever, daemon=True).start()
981
983
  if args.leash == 'noapi':
982
- from .repl import run_repl
983
- run_repl(base_url=url, api=args.leash, repo=cwd, env=env, system=args.system, tool_names=args.tools, sdir=args.skill, init_prompt=args.prompt, resume=args.resume, stream=args.stream, bare=args.bare)
984
+ if args.web:
985
+ from .web import run_web
986
+ web_port = args.web_port if args.web_port is not None else port + 80
987
+ run_web(base_url=url, api=args.leash, repo=cwd, env=env, system=args.system, tool_names=args.tools, sdir=args.skill, init_prompt=args.prompt, resume=args.resume, stream=args.stream, host=args.host, port=web_port)
988
+ else:
989
+ from .repl import run_repl
990
+ run_repl(base_url=url, api=args.leash, repo=cwd, env=env, system=args.system, tool_names=args.tools, sdir=args.skill, init_prompt=args.prompt, resume=args.resume, stream=args.stream, bare=args.bare)
984
991
  else:
985
992
  env['GOOGLE_GEMINI_BASE_URL'] = url
986
993
  env['GEMINI_API_KEY'] = 'mc'