mlx-code 0.0.1a0__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.
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: mlx-code
3
+ Version: 0.0.1a0
4
+ Summary: Local Claude Code-style coding agent via mlx-lm
5
+ Home-page: https://github.com/JosefAlbers/mlx-code
6
+ Author: J Joe
7
+ Author-email: albersj66@gmail.com
8
+ License: Apache-2.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: mlx-lm>=0.19.0
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: license
18
+ Dynamic: requires-dist
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # mlx-code
23
+
24
+ Local Claude Code-style agent via mlx-lm.
25
+
26
+ ```bash
27
+ pip install mlx-code
28
+ mlx-code
29
+ mlx-code --dir ~/myproject
30
+ mlx-code --model mlx-community/Qwen3.5-397B-A17B-8bit
31
+ ```
32
+
33
+ ![cli](https://github.com/user-attachments/assets/d02f90d2-8b3f-478f-be76-5e4444b303cf)
@@ -0,0 +1,12 @@
1
+ # mlx-code
2
+
3
+ Local Claude Code-style agent via mlx-lm.
4
+
5
+ ```bash
6
+ pip install mlx-code
7
+ mlx-code
8
+ mlx-code --dir ~/myproject
9
+ mlx-code --model mlx-community/Qwen3.5-397B-A17B-8bit
10
+ ```
11
+
12
+ ![cli](https://github.com/user-attachments/assets/d02f90d2-8b3f-478f-be76-5e4444b303cf)
@@ -0,0 +1,488 @@
1
+ #!/usr/bin/env python3
2
+ """mlx-code: local Claude Code-style agent via mlx-lm."""
3
+ from __future__ import annotations
4
+ import argparse, json, re, subprocess, sys, textwrap
5
+ from pathlib import Path
6
+
7
+ try:
8
+ from mlx_lm import load, stream_generate
9
+ except ImportError:
10
+ sys.exit("mlx-lm not found. Run: pip install mlx-lm")
11
+
12
+ FILES = {
13
+
14
+ "~/.claude/CLAUDE.md": """## Project
15
+ mlx-code — local Claude Code-style agent via mlx-lm on Apple Silicon.
16
+
17
+ ## Stack
18
+ Python 3.11+, mlx-lm, no other runtime deps.
19
+
20
+ ## Commands
21
+ - mlx-code run agent in current directory
22
+ - mlx-code --dir ~/project specify project directory
23
+ - mlx-code --model <hf-id> use a different model
24
+ - python main.py run without installing
25
+
26
+ ## Conventions
27
+ - Type hints on all function signatures
28
+ - grep before cat — never read whole files speculatively
29
+ - Functions under 40 lines
30
+ - f-strings, not .format()
31
+
32
+ ## Adding skills
33
+ Drop a SKILL.md into ~/.claude/skills/<n>/SKILL.md
34
+ Or ask mlx-code to create one for you.
35
+ description field drives auto-activation — make it keyword-rich.
36
+ disable-model-invocation: true for sensitive skills (deploy etc).
37
+ """,
38
+
39
+ "~/.claude/skills/code-search/SKILL.md": """---
40
+ name: code-search
41
+ description: Search and explore a codebase to understand how it works, find functions, trace data flow, or read code. Use when asked to explain, understand, find, read, or explore code.
42
+ ---
43
+
44
+ ## Rules
45
+ - ALWAYS grep before reading. Never cat a whole file without knowing its size.
46
+ - Check line count with `wc -l` before reading anything.
47
+ - Read specific ranges with `cat -n file.py | sed -n 'A,Bp'`.
48
+
49
+ ## Workflow
50
+
51
+ ### Find relevant code first
52
+ ```
53
+ grep -n "def agent_loop" main.py
54
+ grep -rn "class Trainer" src/
55
+ grep -n "import" main.py | head -20
56
+ ```
57
+
58
+ ### Then read only that section
59
+ ```
60
+ grep -n "def agent_loop" main.py # shows line 230
61
+ cat -n main.py | sed -n '230,280p' # read just those lines
62
+ ```
63
+
64
+ ### Map a large file before diving in
65
+ ```
66
+ wc -l bigfile.py
67
+ grep -n "^class\\|^def " bigfile.py | head -40
68
+ ```
69
+
70
+ ### Trace data flow
71
+ ```
72
+ grep -rn "history" main.py
73
+ grep -n "def.*history\\|history.*=" main.py
74
+ ```
75
+
76
+ ## Output format
77
+ 1. What the code does in 1-2 sentences
78
+ 2. Key data structures
79
+ 3. Execution flow step by step
80
+ 4. Any gotchas
81
+ """,
82
+
83
+ "~/.claude/skills/python-debug/SKILL.md": """---
84
+ name: python-debug
85
+ description: Debug Python errors, tracebacks, exceptions, crashes, and unexpected behaviour. Use when the user mentions an error, traceback, crash, bug, or asks why something is broken.
86
+ ---
87
+
88
+ ## Workflow
89
+ 1. Read the traceback — bottom line is the error, last frame in YOUR code is relevant
90
+ 2. Jump to the line: `grep -n "fn_name" file.py` then `cat -n file.py | sed -n 'A,Bp'`
91
+ 3. Reproduce mentally — what value arrives there, why does it fail
92
+ 4. Fix minimally — no refactoring while debugging
93
+ 5. Verify: `python -m pytest tests/ -x -q` or `python script.py`
94
+
95
+ ## Common errors
96
+ | Error | Cause |
97
+ |-------|-------|
98
+ | `AttributeError: 'NoneType'` | missing None check before .attr |
99
+ | `KeyError` | key absent — use .get() or check with `in` |
100
+ | `IndexError` | off-by-one or empty list |
101
+ | `TypeError: unsupported operand` | wrong type passed |
102
+ | `RecursionError` | missing base case |
103
+ | `ImportError` | missing package or wrong path |
104
+
105
+ ## Debug commands
106
+ ```
107
+ python -W all script.py
108
+ git diff HEAD~1 -- file.py
109
+ grep -n "TODO\\|FIXME\\|HACK" .
110
+ ```
111
+ """,
112
+
113
+ "~/.claude/skills/write-skill/SKILL.md": """---
114
+ name: write-skill
115
+ description: Create a new skill to teach the agent a reusable capability. Use when asked to add a skill, create a skill, or teach the agent to do something new.
116
+ ---
117
+
118
+ ## Steps
119
+ 1. Choose name: lowercase hyphens, e.g. `deploy-heroku`
120
+ 2. Use write_file with path `~/.claude/skills/<n>/SKILL.md` for global,
121
+ or `.claude/skills/<n>/SKILL.md` for project-only
122
+
123
+ ## SKILL.md structure
124
+ ```
125
+ ---
126
+ name: skill-name
127
+ description: What it does AND when to activate it. Keywords drive auto-activation.
128
+ disable-model-invocation: true # optional: manual /skill only
129
+ allowed-tools: bash # optional: restrict tools
130
+ ---
131
+
132
+ # Instructions
133
+ Step by step. Under 400 lines.
134
+ ```
135
+
136
+ ## Verify
137
+ ```
138
+ ls ~/.claude/skills/
139
+ cat ~/.claude/skills/<n>/SKILL.md
140
+ ```
141
+ The agent rescans after every turn — new skills appear immediately.
142
+ """,
143
+
144
+ } # end FILES
145
+
146
+
147
+ def _deploy_files() -> None:
148
+ """Write any missing FILES to disk. Skips files that already exist."""
149
+ deployed = []
150
+ for dest_s, content in FILES.items():
151
+ dest = Path(dest_s).expanduser()
152
+ if dest.exists():
153
+ continue
154
+ dest.parent.mkdir(parents=True, exist_ok=True)
155
+ dest.write_text(content)
156
+ deployed.append(str(dest))
157
+ if deployed:
158
+ print("mlx-code: installed missing files:")
159
+ for f in deployed: print(f" {f}")
160
+
161
+ _deploy_files()
162
+
163
+ DEFAULT_MODEL = "mlx-community/Qwen3.5-4B-OptiQ-4bit"
164
+ MAX_TOOL_TURNS = 30
165
+
166
+ R="\033[0m"; BOLD="\033[1m"; DIM="\033[2m"
167
+ CYAN="\033[36m"; GREEN="\033[32m"; YELLOW="\033[33m"; RED="\033[31m"
168
+ def cp(c,t): print(f"{c}{t}{R}",flush=True)
169
+
170
+ # ── CLAUDE.md ─────────────────────────────────────────────────────────────────
171
+
172
+ def load_claude_md(project_dir: Path) -> str:
173
+ parts = []
174
+ for path, label in [
175
+ (Path.home()/".claude"/"CLAUDE.md", "~/.claude/CLAUDE.md"),
176
+ (project_dir/"CLAUDE.md", "CLAUDE.md"),
177
+ ]:
178
+ if path.exists():
179
+ parts.append(f"<!-- {label} -->\n{path.read_text().strip()}")
180
+ root = project_dir/"CLAUDE.md"
181
+ for sub in sorted(project_dir.rglob("CLAUDE.md")):
182
+ if sub == root: continue
183
+ parts.append(f"<!-- {sub.relative_to(project_dir)} -->\n{sub.read_text().strip()}")
184
+ return "\n\n".join(parts)
185
+
186
+ # ── Skills ────────────────────────────────────────────────────────────────────
187
+
188
+ SKILL_DIRS = [Path.home()/".claude"/"skills"]
189
+
190
+ def _fm(text: str) -> dict:
191
+ m = re.match(r"^---[ \t]*\n(.*?)\n---[ \t]*\n", text, re.DOTALL)
192
+ if not m: return {}
193
+ fm: dict = {}
194
+ for line in m.group(1).splitlines():
195
+ if ":" in line:
196
+ k,_,v = line.partition(":")
197
+ fm[k.strip()] = v.strip().strip('"').strip("'")
198
+ return fm
199
+
200
+ def discover_skills(project_dir: Path) -> list[dict]:
201
+ dirs = SKILL_DIRS + [project_dir/".claude"/"skills"]
202
+ out: list[dict] = []; seen: set[str] = set()
203
+ for base in dirs:
204
+ if not base.exists(): continue
205
+ for md in sorted(base.rglob("SKILL.md")):
206
+ text = md.read_text(errors="replace")
207
+ fm = _fm(text)
208
+ name = (fm.get("name") or md.parent.name).strip()
209
+ if not name or name in seen: continue
210
+ seen.add(name)
211
+ desc = fm.get("description","").strip()
212
+ if not desc: continue
213
+ raw = fm.get("allowed-tools","")
214
+ out.append({
215
+ "name": name,
216
+ "desc": desc,
217
+ "path": md,
218
+ "allowed": {t.strip() for t in raw.split(",")} if raw else None,
219
+ "no_auto": fm.get("disable-model-invocation","false").lower() in ("true","1","yes"),
220
+ "no_user": fm.get("user-invocable","true").lower() in ("false","0","no"),
221
+ })
222
+ return out
223
+
224
+ # ── Prompt ────────────────────────────────────────────────────────────────────
225
+
226
+ CORE = """\
227
+ ━━━ TOOLS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
228
+ Call tools with a JSON object on its own line:
229
+
230
+ {"tool": "bash", "args": {"cmd": "..."}}
231
+ {"tool": "write_file", "args": {"path": "...", "content": "..."}}
232
+
233
+ bash cwd=project root, 30s timeout
234
+ ALWAYS grep/find before cat:
235
+ grep -n "def foo" main.py
236
+ grep -rn "pattern" src/
237
+ cat -n file.py | sed -n '40,80p'
238
+ wc -l file.py
239
+ Run: python -m pytest tests/ -x
240
+ Update: printf '\\n## Rule\\n- text\\n' >> CLAUDE.md
241
+
242
+ write_file path relative to project root, content = full file text\
243
+ """
244
+
245
+ def skill_tools(skills: list[dict]) -> str:
246
+ auto = [s for s in skills if not s["no_auto"]]
247
+ if not auto: return "\n(no skills installed)"
248
+ lines = ["\n━━━ SKILLS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
249
+ "Call a skill tool to load its full instructions into context:\n"]
250
+ for s in auto:
251
+ lines.append(f'{{"tool":"activate_skill_{s["name"]}","args":{{"reason":"..."}}}}\n → {s["desc"]}')
252
+ return "\n".join(lines)
253
+
254
+ def sys_prompt(cwd: Path, claude_md: str, skills: list[dict]) -> str:
255
+ return (
256
+ f"You are mlx-code, a local AI coding assistant (mlx-lm, Apple Silicon).\n"
257
+ f"Explore before writing. Persist knowledge to files.\n\n"
258
+ f"PROJECT: {cwd}\n\n"
259
+ f"━━━ CLAUDE.md ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
260
+ f"{claude_md.strip() or '(none)'}\n\n"
261
+ + CORE + skill_tools(skills)
262
+ )
263
+
264
+ # ── Tools ─────────────────────────────────────────────────────────────────────
265
+
266
+ def run_bash(cmd: str, cwd: Path) -> str:
267
+ if not cmd.strip(): return "ERROR: empty cmd"
268
+ try:
269
+ r = subprocess.run(cmd, shell=True, capture_output=True,
270
+ text=True, cwd=str(cwd), timeout=30)
271
+ out = (r.stdout+r.stderr).strip()
272
+ if not out: return f"(exit {r.returncode}, no output)"
273
+ return out[:8000]+("\n…[truncated]" if len(out)>8000 else "")
274
+ except subprocess.TimeoutExpired: return "ERROR: timed out"
275
+ except Exception as e: return f"ERROR: {e}"
276
+
277
+ def execute(name: str, args: dict, cwd: Path,
278
+ skills: list[dict], active: dict|None
279
+ ) -> tuple[str, bool, dict|None, str|None]:
280
+ if name == "done":
281
+ return args.get("answer",""), True, active, None
282
+ if name.startswith("activate_skill_"):
283
+ sname = name[len("activate_skill_"):]
284
+ s = next((x for x in skills if x["name"]==sname), None)
285
+ if not s: return f"ERROR: no skill '{sname}'", False, active, None
286
+ return f"Skill '{sname}' loaded.", False, s, s["path"].read_text(errors="replace")
287
+ if active and active.get("allowed"):
288
+ if name.lower() not in {t.lower() for t in active["allowed"]}:
289
+ return f"ERROR: skill '{active['name']}' restricts to {active['allowed']}", False, active, None
290
+ if name == "bash":
291
+ return run_bash(args.get("cmd",""), cwd), False, active, None
292
+ if name == "write_file":
293
+ p = args.get("path","")
294
+ if not p: return "ERROR: missing path", False, active, None
295
+ fp = cwd/p; fp.parent.mkdir(parents=True, exist_ok=True)
296
+ fp.write_text(args.get("content",""))
297
+ return f"OK: wrote {fp}", False, active, None
298
+ return f"ERROR: unknown tool '{name}'", False, active, None
299
+
300
+ # ── Parser ────────────────────────────────────────────────────────────────────
301
+
302
+ _JSON_RE = re.compile(r'\{\s*"tool"\s*:\s*"(?P<n>[^"]+)"\s*,\s*"args"\s*:\s*\{')
303
+ _FENCE_RE = re.compile(r'```(?:bash|sh)\n(.*?)```', re.DOTALL)
304
+
305
+ def _bend(text: str, start: int) -> int|None:
306
+ d=0; ins=False; esc=False
307
+ for i in range(start, len(text)):
308
+ c=text[i]
309
+ if esc: esc=False; continue
310
+ if c=="\\" and ins: esc=True; continue
311
+ if c=='"': ins=not ins; continue
312
+ if ins: continue
313
+ if c=="{": d+=1
314
+ elif c=="}":
315
+ d-=1
316
+ if d==0: return i
317
+ return None
318
+
319
+ # def parse_call(text: str) -> tuple[str,dict]|None:
320
+ # text = _FENCE_RE.sub(
321
+ # lambda m: json.dumps({"tool":"bash","args":{"cmd":m.group(1).strip()}}), text)
322
+ # for m in _JSON_RE.finditer(text):
323
+ # ab=m.end()-1; ae=_bend(text,ab)
324
+ # if ae is None: continue
325
+ # try: return m.group("n"), json.loads(text[ab:ae+1])
326
+ # except json.JSONDecodeError: continue
327
+ # return None
328
+
329
+ def parse_call(text: str):
330
+ last = text.strip().splitlines()[-1]
331
+
332
+ try:
333
+ obj = json.loads(last)
334
+ except Exception:
335
+ return None
336
+
337
+ if "tool" in obj and "args" in obj:
338
+ return obj["tool"], obj["args"]
339
+
340
+ return None
341
+
342
+ # ── Generate ──────────────────────────────────────────────────────────────────
343
+
344
+ def generate(model, tok, messages: list[dict], max_tok: int) -> str:
345
+ prompt = tok.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
346
+ buf: list[str] = []
347
+ for chunk in stream_generate(model, tok, prompt=prompt, max_tokens=max_tok):
348
+ print(chunk.text, end="", flush=True); buf.append(chunk.text)
349
+ print()
350
+ return "".join(buf)
351
+
352
+ # ── Agent loop ────────────────────────────────────────────────────────────────
353
+
354
+ def agent_loop(model, tok, user_input: str, cwd: Path,
355
+ claude_md: str, skills: list[dict], history: list[dict],
356
+ max_tok: int, forced_skill: dict|None=None
357
+ ) -> tuple[str, str, list[dict]]:
358
+ active: dict|None = None
359
+ preamble = ""
360
+ if forced_skill:
361
+ preamble = f"[Skill '{forced_skill['name']}' activated]\n{forced_skill['path'].read_text().strip()}\n\n"
362
+ active = forced_skill
363
+ cp(DIM, f" [skill '{forced_skill['name']}' force-loaded]")
364
+
365
+ history.append({"role":"user","content":preamble+user_input})
366
+ injected: list[str] = []
367
+ recent: list[str] = []
368
+
369
+ for _ in range(MAX_TOOL_TURNS):
370
+ msgs = ([{"role":"system","content":sys_prompt(cwd,claude_md,skills)}]
371
+ + [{"role":"system","content":b} for b in injected]
372
+ + history)
373
+ cp(CYAN, f"\n{'─'*60}"); cp(BOLD+CYAN, "Assistant:")
374
+ resp = generate(model, tok, msgs, max_tok)
375
+ history.append({"role":"assistant","content":resp})
376
+
377
+ call = parse_call(resp)
378
+ if call is None: return resp, claude_md, skills
379
+
380
+ name, args = call
381
+ if name == "done":
382
+ cp(GREEN,"\n✅ Done.")
383
+ return args.get("answer",resp), claude_md, skills
384
+
385
+ sig = json.dumps({"t":name,"a":args},sort_keys=True)
386
+ if sig in recent:
387
+ history.append({"role":"user","content":
388
+ "[SYSTEM] You already ran this exact call. Do not repeat it. "
389
+ "Write your final answer as plain text now."})
390
+ recent.clear(); continue
391
+ recent.append(sig)
392
+ if len(recent)>6: recent.pop(0)
393
+
394
+ cp(YELLOW, f"\n🔧 {name}: {json.dumps(args,ensure_ascii=False)[:160]}")
395
+ result, done, active, skill_body = execute(name, args, cwd, skills, active)
396
+
397
+ if done: cp(GREEN,"\n✅ Done."); return result, claude_md, skills
398
+ if skill_body:
399
+ injected.append(f"[Skill '{name[len('activate_skill_'):]}']\n{skill_body}")
400
+ cp(DIM, " [skill body injected]")
401
+
402
+ cp(DIM, textwrap.indent(result[:600]," "))
403
+
404
+ touched = args.get("cmd","") + args.get("path","")
405
+ if "CLAUDE.md" in touched:
406
+ claude_md = load_claude_md(cwd); cp(DIM," [CLAUDE.md reloaded]")
407
+ if ".claude/skills" in touched:
408
+ skills = discover_skills(cwd); cp(DIM,f" [skills reloaded: {len(skills)}]")
409
+
410
+ history.append({"role":"user","content":f"[tool result: {name}]\n{result}"})
411
+
412
+ cp(RED,f"\n⚠️ Hit {MAX_TOOL_TURNS}-turn limit.")
413
+ return "(turn limit reached)", claude_md, skills
414
+
415
+ # ── REPL ──────────────────────────────────────────────────────────────────────
416
+
417
+ HELP = """\
418
+ /help this message
419
+ /claude print CLAUDE.md
420
+ /skills list skills
421
+ /skill <n> force-invoke a skill
422
+ /reload rescan CLAUDE.md + skills
423
+ /clear clear conversation history
424
+ /quit exit"""
425
+
426
+ def main():
427
+ ap = argparse.ArgumentParser(description="mlx-code: local Claude Code via mlx-lm")
428
+ ap.add_argument("--model", default=DEFAULT_MODEL)
429
+ ap.add_argument("--dir", default=".")
430
+ ap.add_argument("--max-tokens", default=2048, type=int)
431
+ a = ap.parse_args(); cwd = Path(a.dir).resolve()
432
+
433
+ cp(BOLD+GREEN,"\n╔════════════════════════════════╗")
434
+ cp(BOLD+GREEN, "║ mlx-code ║")
435
+ cp(BOLD+GREEN, "║ Local Claude Code via mlx-lm ║")
436
+ cp(BOLD+GREEN, "╚════════════════════════════════╝\n")
437
+ cp(DIM,f"Project : {cwd}"); cp(DIM,f"Model : {a.model}\n")
438
+
439
+ cp(YELLOW,f"Loading {a.model} …")
440
+ tc = {"trust_remote_code":True} if "qwen" in a.model.lower() else {}
441
+ model, tok = load(a.model, tokenizer_config=tc)
442
+ cp(GREEN,"Model loaded ✓\n")
443
+
444
+ claude_md = load_claude_md(cwd)
445
+ skills = discover_skills(cwd)
446
+ cp(DIM,f"📄 CLAUDE.md : {'loaded' if claude_md else 'not found'}")
447
+ n = sum(1 for s in skills if not s["no_auto"])
448
+ cp(DIM,f"🧩 Skills : {len(skills)} total, {n} model-invocable" +
449
+ (f" ({', '.join(s['name'] for s in skills)})" if skills else
450
+ " (none — add ~/.claude/skills/<n>/SKILL.md)"))
451
+ cp(DIM,"\nType /help for commands.\n")
452
+
453
+ history: list[dict] = []; forced: dict|None = None
454
+ while True:
455
+ try:
456
+ cp(BOLD+GREEN,"\n> You: "); ui = input("").strip()
457
+ except (EOFError,KeyboardInterrupt):
458
+ cp(YELLOW,"\nGoodbye!"); break
459
+ if not ui: continue
460
+ if ui.startswith("/"):
461
+ ps=ui.split(maxsplit=1); cmd=ps[0].lower(); arg=ps[1].strip() if len(ps)>1 else ""
462
+ if cmd=="/quit": cp(YELLOW,"Goodbye!"); break
463
+ elif cmd=="/help": print(HELP)
464
+ elif cmd=="/claude": print(claude_md or "(empty)")
465
+ elif cmd=="/skills":
466
+ if skills:
467
+ for s in skills:
468
+ flags = " [manual-only]" if s["no_auto"] else ""
469
+ cp(GREEN,f" • {s['name']}{flags}"); print(f" {s['desc']}")
470
+ else: cp(DIM,"No skills found.")
471
+ elif cmd=="/skill":
472
+ m=next((s for s in skills if s["name"]==arg and not s["no_user"]),None)
473
+ if m: forced=m; cp(GREEN,f"Skill '{arg}' injected next turn.")
474
+ else: cp(RED,f"No skill '{arg}'. /skills to list.")
475
+ elif cmd=="/reload":
476
+ claude_md=load_claude_md(cwd); skills=discover_skills(cwd)
477
+ cp(GREEN,f"Reloaded. {len(skills)} skill(s).")
478
+ elif cmd=="/clear":
479
+ history.clear(); cp(GREEN,"History cleared.")
480
+ else: cp(RED,f"Unknown '{cmd}'. /help")
481
+ continue
482
+ _, claude_md, skills = agent_loop(
483
+ model,tok,ui,cwd,claude_md,skills,history,a.max_tokens,forced)
484
+ forced=None
485
+ claude_md=load_claude_md(cwd); skills=discover_skills(cwd)
486
+
487
+ if __name__=="__main__":
488
+ main()
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.4
2
+ Name: mlx-code
3
+ Version: 0.0.1a0
4
+ Summary: Local Claude Code-style coding agent via mlx-lm
5
+ Home-page: https://github.com/JosefAlbers/mlx-code
6
+ Author: J Joe
7
+ Author-email: albersj66@gmail.com
8
+ License: Apache-2.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: mlx-lm>=0.19.0
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: license
18
+ Dynamic: requires-dist
19
+ Dynamic: requires-python
20
+ Dynamic: summary
21
+
22
+ # mlx-code
23
+
24
+ Local Claude Code-style agent via mlx-lm.
25
+
26
+ ```bash
27
+ pip install mlx-code
28
+ mlx-code
29
+ mlx-code --dir ~/myproject
30
+ mlx-code --model mlx-community/Qwen3.5-397B-A17B-8bit
31
+ ```
32
+
33
+ ![cli](https://github.com/user-attachments/assets/d02f90d2-8b3f-478f-be76-5e4444b303cf)
@@ -0,0 +1,9 @@
1
+ README.md
2
+ main.py
3
+ setup.py
4
+ mlx_code.egg-info/PKG-INFO
5
+ mlx_code.egg-info/SOURCES.txt
6
+ mlx_code.egg-info/dependency_links.txt
7
+ mlx_code.egg-info/entry_points.txt
8
+ mlx_code.egg-info/requires.txt
9
+ mlx_code.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mlx-code = main:main
@@ -0,0 +1 @@
1
+ mlx-lm>=0.19.0
@@ -0,0 +1 @@
1
+ main
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,18 @@
1
+ from setuptools import setup
2
+
3
+ setup(
4
+ name="mlx-code",
5
+ url='https://github.com/JosefAlbers/mlx-code',
6
+ author_email="albersj66@gmail.com",
7
+ author="J Joe",
8
+ license="Apache-2.0",
9
+ version="0.0.1a0",
10
+ readme="README.md",
11
+ description="Local Claude Code-style coding agent via mlx-lm",
12
+ long_description=open("README.md").read(),
13
+ long_description_content_type="text/markdown",
14
+ python_requires=">=3.11",
15
+ install_requires=["mlx-lm>=0.19.0"],
16
+ py_modules=["main"],
17
+ entry_points={"console_scripts": ["mlx-code=main:main"]},
18
+ )