mlx-code 0.0.30__tar.gz → 0.0.32__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 (31) hide show
  1. {mlx_code-0.0.30 → mlx_code-0.0.32}/PKG-INFO +9 -10
  2. {mlx_code-0.0.30 → mlx_code-0.0.32}/README.md +3 -3
  3. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/bats.py +20 -0
  4. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/gits.py +14 -4
  5. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/main.py +21 -40
  6. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/repl.py +44 -27
  7. mlx_code-0.0.32/mlx_code/view_git.py +995 -0
  8. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/web.py +37 -74
  9. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code.egg-info/PKG-INFO +9 -10
  10. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code.egg-info/requires.txt +5 -6
  11. {mlx_code-0.0.30 → mlx_code-0.0.32}/setup.py +12 -11
  12. mlx_code-0.0.30/mlx_code/view_git.py +0 -824
  13. {mlx_code-0.0.30 → mlx_code-0.0.32}/LICENSE +0 -0
  14. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/__init__.py +0 -0
  15. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/apis.py +0 -0
  16. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/bare.py +0 -0
  17. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/lsp_tool.py +0 -0
  18. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/mcb.py +0 -0
  19. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/mcb_tool.py +0 -0
  20. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/stream_log.py +0 -0
  21. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/tools.py +0 -0
  22. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/tui.py +0 -0
  23. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/util.py +0 -0
  24. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code/view_log.py +0 -0
  25. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code.egg-info/SOURCES.txt +0 -0
  26. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code.egg-info/dependency_links.txt +0 -0
  27. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code.egg-info/entry_points.txt +0 -0
  28. {mlx_code-0.0.30 → mlx_code-0.0.32}/mlx_code.egg-info/top_level.txt +0 -0
  29. {mlx_code-0.0.30 → mlx_code-0.0.32}/setup.cfg +0 -0
  30. {mlx_code-0.0.30 → mlx_code-0.0.32}/tests/__init__.py +0 -0
  31. {mlx_code-0.0.30 → mlx_code-0.0.32}/tests/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlx-code
3
- Version: 0.0.30
3
+ Version: 0.0.32
4
4
  Summary: Coding Agent for Mac
5
5
  Home-page: https://josefalbers.github.io/mlx-code/
6
6
  Author: J Joe
@@ -15,14 +15,13 @@ License-File: LICENSE
15
15
  Requires-Dist: mlx-lm>=0.31.3; platform_system == "Darwin"
16
16
  Requires-Dist: httpx
17
17
  Requires-Dist: pydantic
18
- Requires-Dist: textual>=8.2.7
19
- Requires-Dist: rich>=15.0.0
20
- Requires-Dist: starlette
21
- Requires-Dist: uvicorn
22
18
  Provides-Extra: all
23
- Requires-Dist: python-lsp-server[all]; extra == "all"
24
- Requires-Dist: GitPython; extra == "all"
19
+ Requires-Dist: starlette; extra == "all"
20
+ Requires-Dist: uvicorn; extra == "all"
25
21
  Requires-Dist: pygments; extra == "all"
22
+ Requires-Dist: textual>=8.2.7; extra == "all"
23
+ Requires-Dist: rich>=15.0.0; extra == "all"
24
+ Requires-Dist: python-lsp-server[all]; extra == "all"
26
25
  Dynamic: author
27
26
  Dynamic: author-email
28
27
  Dynamic: description
@@ -76,7 +75,7 @@ Agents: ├───────────────────
76
75
  │ │ Gemini │ │ Edit Bash │ │
77
76
  │ │ Claude │ │ Grep Find │ │
78
77
  │ │ Codex │ │ Ls Skill │ │
79
- │ │ DeepSeek │ │ Agent ─────────┼──┼───► Recursively spawns sub-Agents
78
+ │ │ DeepSeek │ │ Agent ────────────────► Recursively spawns sub-Agents
80
79
  │ └────────────────┘ └────────────────┘ │
81
80
  │ Git worktree │
82
81
  │ (isolation + session state) │
@@ -108,10 +107,10 @@ result = await agent.run('refactor utils.py to use dataclasses')
108
107
 
109
108
  ```bash
110
109
  # ephemeral run (no installation)
111
- uvx --from mlx-code mlc
110
+ uvx --from mlx-code[all] mlc
112
111
 
113
112
  # or install into the current environment
114
- pip install mlx-code
113
+ pip install mlx-code[all]
115
114
 
116
115
  # launch
117
116
  mlc # with a local MLX model
@@ -38,7 +38,7 @@ Agents: ├───────────────────
38
38
  │ │ Gemini │ │ Edit Bash │ │
39
39
  │ │ Claude │ │ Grep Find │ │
40
40
  │ │ Codex │ │ Ls Skill │ │
41
- │ │ DeepSeek │ │ Agent ─────────┼──┼───► Recursively spawns sub-Agents
41
+ │ │ DeepSeek │ │ Agent ────────────────► Recursively spawns sub-Agents
42
42
  │ └────────────────┘ └────────────────┘ │
43
43
  │ Git worktree │
44
44
  │ (isolation + session state) │
@@ -70,10 +70,10 @@ result = await agent.run('refactor utils.py to use dataclasses')
70
70
 
71
71
  ```bash
72
72
  # ephemeral run (no installation)
73
- uvx --from mlx-code mlc
73
+ uvx --from mlx-code[all] mlc
74
74
 
75
75
  # or install into the current environment
76
- pip install mlx-code
76
+ pip install mlx-code[all]
77
77
 
78
78
  # launch
79
79
  mlc # with a local MLX model
@@ -294,6 +294,26 @@ def make_batch_app(model_name: str, cache_dir: str='.cache'):
294
294
  n_cached = sum((1 for _ in pc.cache_dir.glob('*.safetensors')))
295
295
  return JSONResponse({'status': 'ok', 'model': model_name, 'active_sequences': len(state['active']), 'prefix_cache_files': n_cached})
296
296
  return Starlette(routes=[Route('/v1/models', list_models, methods=['GET']), Route('/v1/messages/count_tokens', count_tokens, methods=['POST']), Route('/v1/chat/completions', generate_endpoint, methods=['POST']), Route('/v1/messages', generate_endpoint, methods=['POST']), Route('/v1/responses', generate_endpoint, methods=['POST']), Route('/v1beta/models/{rest:path}', generate_endpoint, methods=['POST']), Route('/generate', simple_generate, methods=['POST']), Route('/health', health, methods=['GET'])], lifespan=lifespan)
297
+
298
+ class BatchServer:
299
+ import uvicorn
300
+
301
+ def __init__(self, app, host: str, port: int):
302
+ config = uvicorn.Config(app, host=host, port=port, loop='asyncio', log_level='warning')
303
+ self._server = uvicorn.Server(config)
304
+ self.host = host
305
+ self.port = port
306
+
307
+ def serve_forever(self):
308
+ self._server.run()
309
+
310
+ @property
311
+ def started(self) -> bool:
312
+ return self._server.started
313
+
314
+ def make_batch_server(host: str, port: int, model, cache_dir: str='.cache') -> BatchServer:
315
+ app = make_batch_app(model, cache_dir=cache_dir)
316
+ return BatchServer(app, host, port)
297
317
  if __name__ == '__main__':
298
318
  import uvicorn
299
319
  uvicorn.run(make_batch_app('mlx-community/Qwen3.5-4B-OptiQ-4bit'), host='0.0.0.0', port=8000)
@@ -9,7 +9,6 @@ import uuid
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
  logger = logging.getLogger(__name__)
12
- _ADD_EXCLUDES = ['_*', '*.bin', '*.gguf', '*.safetensors', '*.pt', '*.pth', '.cache/', '.log.json', '*.egg-info/', '.eggs/', 'build/', 'dist/', '__pycache__/', '*.pyc', '*.pyo', '*.pyd', '.pytest_cache/', '.tox/', '.nox/', '.coverage', 'htmlcov/', '.venv/', 'venv/', 'env/', '.DS_Store', 'Thumbs.db']
13
12
 
14
13
  class GitError(RuntimeError):
15
14
  pass
@@ -57,11 +56,22 @@ def _count_user_turns(commit_body: str) -> int:
57
56
  if messages:
58
57
  return sum((1 for m in messages if m.get('role') == 'user'))
59
58
  return 0
59
+ _ADD_EXCLUDES = ['_*', '_*/', '*.bin', '*.gguf', '*.safetensors', '*.pt', '*.pth', '.cache/', '.log.json', '*.egg-info/', '.eggs/', 'build/', 'dist/', '__pycache__/', '*.pyc', '*.pyo', '*.pyd', '.pytest_cache/', '.tox/', '.nox/', '.coverage', 'htmlcov/', '.venv/', 'venv/', 'env/', '.DS_Store', 'Thumbs.db']
60
+
61
+ def _exclude_pathspecs(patterns: list[str]) -> list[str]:
62
+ specs = []
63
+ for p in patterns:
64
+ if p.endswith('/'):
65
+ name = p[:-1]
66
+ specs.append(f':(exclude,glob)**/{name}/**')
67
+ else:
68
+ specs.append(f':(exclude,glob)**/{p}')
69
+ return specs
60
70
 
61
71
  def git_add_filtered(cwd: str) -> None:
62
- excludes = [f':(exclude){p}' for p in _ADD_EXCLUDES]
72
+ excludes = _exclude_pathspecs(_ADD_EXCLUDES)
63
73
  try:
64
- _git(cwd, 'add', '-A', '--', '.', *excludes)
74
+ _git(cwd, '-c', 'advice.addIgnoredFile=false', 'add', '-A', '--', '.', *excludes)
65
75
  except GitError as e:
66
76
  logger.warning('git add warning (ignored): %s', e)
67
77
 
@@ -83,7 +93,7 @@ def create_worktree(repo_dir: str, *, worktree_dir: str | None=None, ref: str='H
83
93
  root = repo_dir
84
94
  gi = os.path.join(root, '.gitignore')
85
95
  if not os.path.exists(gi):
86
- Path(gi).write_text('\n'.join(['_log.json']))
96
+ Path(gi).write_text('\n'.join(['.log.json']))
87
97
  if not _git(root, 'config', 'user.email', check=False):
88
98
  _git(root, 'config', 'user.email', 'agent@local')
89
99
  if not _git(root, 'config', 'user.name', check=False):
@@ -890,11 +890,14 @@ def _serve_cache(host, port, model, cache, system, tools, skips, *, fixed_port=F
890
890
  raise
891
891
 
892
892
  def _serve_batch(host, port, model, cache_dir='.cache', *, fixed_port=False):
893
- import uvicorn
894
- from .bats import make_batch_app
893
+ try:
894
+ from .bats import make_batch_server
895
+ except ImportError:
896
+ print()
897
+ print('[warning] uvicorn/starlette not installed')
898
+ print(' Install server deps with:\x1b[31m pip install mlx-code[all] \x1b[0m')
899
+ return _serve_cache(host, port, model, cache_dir, None, None, None, fixed_port=fixed_port)
895
900
  import socket
896
- import time
897
- app = make_batch_app(model, cache_dir=cache_dir)
898
901
  while True:
899
902
  try:
900
903
  with socket.socket() as s:
@@ -909,24 +912,10 @@ def _serve_batch(host, port, model, cache_dir='.cache', *, fixed_port=False):
909
912
  raise
910
913
  else:
911
914
  break
912
- config = uvicorn.Config(app, host=host, port=port, loop='asyncio', log_level='warning')
913
- uv_server = uvicorn.Server(config)
914
- t = threading.Thread(target=uv_server.run, daemon=True)
915
- t.start()
916
- start_time = time.time()
917
- notified = False
918
- while True:
919
- try:
920
- with socket.create_connection((host, port), timeout=0.1):
921
- break
922
- except OSError:
923
- if not notified and time.time() - start_time > 3.0:
924
- logger.info('Waiting for batch server to start (model may be downloading)...')
925
- notified = True
926
- time.sleep(0.2)
915
+ server = make_batch_server(host, port, model, cache_dir=cache_dir)
927
916
  url = f'http://{host}:{port}'
928
917
  logger.debug(f'Batch server bound to {url}')
929
- return (uv_server, url)
918
+ return (server, url)
930
919
 
931
920
  def main():
932
921
  parser = argparse.ArgumentParser(description='mlx-code MAIN')
@@ -967,28 +956,20 @@ def main():
967
956
  else:
968
957
  server, url = _serve_cache(host=args.host, port=port, model=args.model, cache=cache, system=None if args.leash in ('none', 'noapi') else args.system, tools=args.tools, skips=args.skips, fixed_port=fixed_port, gwt=gwt)
969
958
  if args.leash == 'none':
970
- if args.engine == 'batch':
971
- try:
972
- threading.Event().wait()
973
- except KeyboardInterrupt:
974
- print('\nShutting down server...')
975
- else:
976
- try:
977
- server.serve_forever()
978
- except KeyboardInterrupt:
979
- print('\nShutting down server...')
980
- server.server_close()
959
+ try:
960
+ server.serve_forever()
961
+ except KeyboardInterrupt:
962
+ print('\nShutting down server...')
963
+ server.server_close()
981
964
  else:
982
- if args.engine == 'cache':
983
- threading.Thread(target=server.serve_forever, daemon=True).start()
965
+ threading.Thread(target=server.serve_forever, daemon=True).start()
966
+ while not getattr(server, 'started', True):
967
+ time.sleep(0.05)
984
968
  if args.leash == 'noapi':
985
- if args.web:
986
- from .web import run_web
987
- web_port = args.web_port if args.web_port is not None else port + 80
988
- 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)
989
- else:
990
- from .repl import run_repl
991
- 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)
969
+ ui_mode = 'web' if args.web else 'bare' if args.bare else 'tui'
970
+ web_port = args.web_port if args.web_port is not None else port + 80
971
+ from .repl import run_repl
972
+ 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, ui_mode=ui_mode, web_host=args.host, web_port=web_port)
992
973
  else:
993
974
  env['GOOGLE_GEMINI_BASE_URL'] = url
994
975
  env['GEMINI_API_KEY'] = 'mc'
@@ -742,9 +742,9 @@ _AGENT_ENV_ALLOWLIST: re.Pattern = re.compile('\n ^(\n PATH\n | MANPATH
742
742
  def _make_agent_env(base: dict[str, str]) -> dict[str, str]:
743
743
  return {k: v for k, v in base.items() if _AGENT_ENV_ALLOWLIST.match(k)}
744
744
 
745
- async def repl(engine: CommandEngine, init_prompt=None, bare=False):
745
+ async def repl(engine: CommandEngine, init_prompt=None, ui_mode='tui'):
746
746
  is_tty = sys.stdin.isatty() and sys.stdout.isatty()
747
- if bare and is_tty:
747
+ if ui_mode == 'bare' and is_tty:
748
748
  from .bare import BareRepl
749
749
  r = BareRepl(engine, init_prompt=init_prompt)
750
750
  await r.run()
@@ -754,12 +754,23 @@ async def repl(engine: CommandEngine, init_prompt=None, bare=False):
754
754
  if user_input:
755
755
  await _stream_to_stdout(engine.active_tab.agent, user_input)
756
756
  return None
757
- from .tui import ReplApp
758
- app = ReplApp(engine, init_prompt=init_prompt)
759
- await app.run_async()
760
- return app
757
+ if ui_mode == 'tui':
758
+ try:
759
+ from .tui import ReplApp
760
+ except ImportError:
761
+ print()
762
+ print('[warning] textual/rich not installed → falling back to bare.')
763
+ print(' Install TUI deps with:\x1b[31m pip install mlx-code[all] \x1b[0m')
764
+ from .bare import BareRepl
765
+ r = BareRepl(engine, init_prompt=init_prompt)
766
+ await r.run()
767
+ return None
768
+ app = ReplApp(engine, init_prompt=init_prompt)
769
+ await app.run_async()
770
+ return app
771
+ return None
761
772
 
762
- def run_repl(*, base_url=None, model=None, api: Literal['claude', 'codex', 'gemini', 'deepseek', 'noapi']='noapi', system='', sdir=None, skills=None, env=None, tool_names=None, extra_tool_classes=None, api_key=None, gwt=None, ctx=None, init_prompt=None, resume_messages=None, repo=None, resume=None, stream=None, bare=False):
773
+ def run_repl(*, base_url=None, model=None, api='noapi', system='', sdir=None, skills=None, env=None, tool_names=None, extra_tool_classes=None, api_key=None, gwt=None, ctx=None, init_prompt=None, resume_messages=None, repo=None, resume=None, stream=None, ui_mode='tui', web_host='127.0.0.1', web_port=8080):
763
774
  repo = os.path.abspath(repo or os.getcwd())
764
775
  with tempfile.TemporaryDirectory(dir=tempfile.gettempdir()) as _home:
765
776
  if gwt is None:
@@ -785,9 +796,6 @@ def run_repl(*, base_url=None, model=None, api: Literal['claude', 'codex', 'gemi
785
796
  system = '\n\n'.join(filter(None, [system, skill_prompt]))
786
797
  merged_ctx = {'cwd': cwd, 'user_cwd': user_cwd, 'skills': skills, 'gwt': gwt, 'env': agent_env, **(ctx or {})}
787
798
  agent = Agent(system=system, api=api, model=model, tool_names=tool_names, extra_tool_classes=extra_tool_classes, api_key=api_key, base_url=base_url, ctx=merged_ctx)
788
- engine = CommandEngine()
789
- main_tab = TabModel(agent, title='main', is_main=True)
790
- engine.tabs = [main_tab]
791
799
  log_fp = None
792
800
  if stream is not None:
793
801
  from .stream_log import StreamLogger
@@ -800,26 +808,35 @@ def run_repl(*, base_url=None, model=None, api: Literal['claude', 'codex', 'gemi
800
808
  agent.messages = list(resume_messages)
801
809
  print(f'[resumed {len(resume_messages)} messages from checkpoint]')
802
810
  try:
803
- asyncio.run(repl(engine, init_prompt=init_prompt, bare=bare))
811
+ if ui_mode == 'web':
812
+ try:
813
+ from .web import run_web
814
+ except ImportError:
815
+ print('[warning] starlette/uvicorn not installed — falling back to bare.\n Install web deps with: pip install mlx-code[all]')
816
+ engine = CommandEngine()
817
+ main_tab = TabModel(agent, title='main', is_main=True)
818
+ engine.tabs = [main_tab]
819
+ asyncio.run(repl(engine, init_prompt=init_prompt, ui_mode='bare'))
820
+ else:
821
+ run_web(agent=agent, init_prompt=init_prompt, web_host=web_host, web_port=web_port)
822
+ else:
823
+ engine = CommandEngine()
824
+ main_tab = TabModel(agent, title='main', is_main=True)
825
+ engine.tabs = [main_tab]
826
+ asyncio.run(repl(engine, init_prompt=init_prompt, ui_mode=ui_mode))
804
827
  finally:
805
828
  if log_fp:
806
829
  log_fp.close()
807
- cleaned: set[str] = set()
808
- for tab in engine.tabs:
809
- gwt_ref = tab.agent.ctx.get('gwt')
810
- if gwt_ref and getattr(gwt_ref, 'worktree', None) and (gwt_ref.worktree not in cleaned):
811
- cleaned.add(gwt_ref.worktree)
812
- try:
813
- cleanup_worktree(gwt_ref)
814
- except Exception:
815
- pass
816
- if engine.exit_summary:
817
- print('\n--- Session Exit Summary ---')
818
- for item in engine.exit_summary:
819
- title = item['title']
820
- branch = item['branch'] or '(no branch)'
821
- marker = ' * <-- exit origin' if item['is_exit_tab'] else ''
822
- print(f' {title} ({branch}){marker}')
830
+ if 'engine' in locals() and hasattr(engine, 'tabs'):
831
+ cleaned: set[str] = set()
832
+ for tab in engine.tabs:
833
+ gwt_ref = tab.agent.ctx.get('gwt')
834
+ if gwt_ref and getattr(gwt_ref, 'worktree', None) and (gwt_ref.worktree not in cleaned):
835
+ cleaned.add(gwt_ref.worktree)
836
+ try:
837
+ cleanup_worktree(gwt_ref)
838
+ except Exception:
839
+ pass
823
840
 
824
841
  def main():
825
842
  import argparse