mlx-code 0.0.1a2__tar.gz → 0.0.1a3__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.
- mlx_code-0.0.1a3/PKG-INFO +204 -0
- mlx_code-0.0.1a3/README.md +182 -0
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/main.py +189 -140
- mlx_code-0.0.1a3/mlx_code.egg-info/PKG-INFO +204 -0
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/mlx_code.egg-info/requires.txt +1 -0
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/setup.py +2 -2
- mlx_code-0.0.1a2/PKG-INFO +0 -24
- mlx_code-0.0.1a2/README.md +0 -3
- mlx_code-0.0.1a2/mlx_code.egg-info/PKG-INFO +0 -24
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/mlx_code.egg-info/SOURCES.txt +0 -0
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/mlx_code.egg-info/dependency_links.txt +0 -0
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/mlx_code.egg-info/entry_points.txt +0 -0
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/mlx_code.egg-info/top_level.txt +0 -0
- {mlx_code-0.0.1a2 → mlx_code-0.0.1a3}/setup.cfg +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mlx-code
|
|
3
|
+
Version: 0.0.1a3
|
|
4
|
+
Summary: Local Claude Code for Mac
|
|
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
|
+
Requires-Dist: PyYAML
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
23
|
+
# mlx-code: Local Claude Code for Mac
|
|
24
|
+
|
|
25
|
+
<img width="1024" height="1536" alt="mlxcodelogo" src="https://github.com/user-attachments/assets/df605c1d-f8e7-433c-9aaf-176c73e3d628" />
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
brew install --cask claude-code
|
|
32
|
+
pip install mlx-code
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Commands
|
|
36
|
+
| Command | What it does | Example |
|
|
37
|
+
|--------|--------------|--------|
|
|
38
|
+
| `mlx-code` | Start interactive mode | `mlx-code` |
|
|
39
|
+
| `mlx-code "task"` | Run a one-time task | `mlx-code "fix the build error"` |
|
|
40
|
+
| `mlx-code -p "query"` | Run one-off query, then exit | `mlx-code -p "explain this function"` |
|
|
41
|
+
| `mlx-code -c` | Continue most recent conversation in current directory | `mlx-code -c` |
|
|
42
|
+
| `mlx-code -r` | Resume a previous conversation | `mlx-code -r` |
|
|
43
|
+
| `mlx-code commit` | Create a Git commit | `mlx-code commit` |
|
|
44
|
+
| `/clear` | Clear conversation history | `/clear` |
|
|
45
|
+
| `/help` | Show available commands | `/help` |
|
|
46
|
+
| `exit` or `Ctrl+C` | Exit Claude Code | `exit` |
|
|
47
|
+
|
|
48
|
+
<details><summary>Click to expand</summary><pre>
|
|
49
|
+
╭─── Claude Code v2.1.74 ──────────────────────────────────────────────────────────────────────────────────╮
|
|
50
|
+
│ │ Tips for getting started │
|
|
51
|
+
│ Welcome back! │ Run /init to create a CLAUDE.md file with instruct… │
|
|
52
|
+
│ │ ─────────────────────────────────────────────────── │
|
|
53
|
+
│ ▐▛███▜▌ │ Recent activity │
|
|
54
|
+
│ ▝▜█████▛▘ │ No recent activity │
|
|
55
|
+
│ ▘▘ ▝▝ │ │
|
|
56
|
+
│ │ │
|
|
57
|
+
│ mlx-community/Qwen3.5-4B-Opti… · API Usage Billing │ │
|
|
58
|
+
│ ~/Downloads/tmp │ │
|
|
59
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
60
|
+
|
|
61
|
+
❯ list the files in this directory and explain what each one does
|
|
62
|
+
|
|
63
|
+
⏺ The user wants to understand the files in the current directory, so I'll use the Glob tool to list all files first.
|
|
64
|
+
|
|
65
|
+
⏺ Searched for 1 pattern (ctrl+o to expand)
|
|
66
|
+
|
|
67
|
+
⏺ I need to fix the Glob tool call - the parameter should be pattern, not key.
|
|
68
|
+
|
|
69
|
+
⏺ Searched for 1 pattern (ctrl+o to expand)
|
|
70
|
+
|
|
71
|
+
⏺ The user wants me to explain what each file does. I should organize these by project directory and explain the purpose of each file. Let me read a few key files to provide accurate explanations.
|
|
72
|
+
|
|
73
|
+
This directory contains several project folders. Let me explain each one:
|
|
74
|
+
|
|
75
|
+
Main Projects
|
|
76
|
+
|
|
77
|
+
cyberpunk/
|
|
78
|
+
|
|
79
|
+
- main.zig - Main entry point for a Zig-based cyberpunk project
|
|
80
|
+
- build.zig - Build configuration file for Zig
|
|
81
|
+
- map.c - C source file, likely for map rendering or game logic
|
|
82
|
+
- help.py - Python helper script, possibly for documentation or utilities
|
|
83
|
+
|
|
84
|
+
rts/
|
|
85
|
+
|
|
86
|
+
- main.zig - Main entry point for a real-time strategy (RTS) game in Zig
|
|
87
|
+
- build.zig - Build configuration for Zig
|
|
88
|
+
- battle_sim.c - C file for battle simulation logic
|
|
89
|
+
|
|
90
|
+
splat_zig_raylib/
|
|
91
|
+
|
|
92
|
+
- main.zig (two versions - one in splat_zig/, one in root) - Main entry point for a Raylib-based game
|
|
93
|
+
- build.zig - Build configuration
|
|
94
|
+
- gsplat_transform.py - Python script for splat transform calculations
|
|
95
|
+
- index.html - Web entry point for the game
|
|
96
|
+
- build_web.sh - Shell script for building web version
|
|
97
|
+
- README.md - Project documentation
|
|
98
|
+
|
|
99
|
+
tarot/
|
|
100
|
+
|
|
101
|
+
- tarot.py - Python script for tarot-related functionality
|
|
102
|
+
- index.html - Web entry point
|
|
103
|
+
|
|
104
|
+
yorha/
|
|
105
|
+
|
|
106
|
+
- index.html - Web entry point (likely a simple web page)
|
|
107
|
+
|
|
108
|
+
jina.py
|
|
109
|
+
|
|
110
|
+
- jina.py - Python script, possibly related to Jina AI or a utility
|
|
111
|
+
|
|
112
|
+
mlx-code-v1.py & mlx-code.py
|
|
113
|
+
|
|
114
|
+
- These appear to be different versions of the mlx-code agent wrapper (the local Claude Code-style agent for Apple Silicon)
|
|
115
|
+
|
|
116
|
+
tmp.md
|
|
117
|
+
|
|
118
|
+
- tmp.md - Temporary markdown file (contents unknown)
|
|
119
|
+
|
|
120
|
+
mlx_trace.log
|
|
121
|
+
|
|
122
|
+
- mlx_trace.log - Trace log file from the mlx model runtime
|
|
123
|
+
|
|
124
|
+
.DS_Store files
|
|
125
|
+
|
|
126
|
+
- macOS metadata files (can be ignored)
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
Summary: This is a temporary download folder (/Users/jjms/Downloads/tmp) containing multiple small projects:
|
|
130
|
+
- Game projects in Zig (cyberpunk, RTS, Raylib-based game)
|
|
131
|
+
- Python scripts for various utilities
|
|
132
|
+
- Web entry points (HTML files)
|
|
133
|
+
- Build configurations (Zig build files)
|
|
134
|
+
|
|
135
|
+
The Zig projects appear to be the main focus, with game-related code and build systems.
|
|
136
|
+
|
|
137
|
+
✻ Brewed for 42s
|
|
138
|
+
|
|
139
|
+
❯ explain how output is parsed in mlx-code.py
|
|
140
|
+
|
|
141
|
+
⏺ The user wants to understand how output is parsed in mlx-code.py, so I need to read that file first to understand its structure and find the output parsing logic.
|
|
142
|
+
|
|
143
|
+
⏺ Read 1 file (ctrl+o to expand)
|
|
144
|
+
|
|
145
|
+
⏺ I need to fix the Read tool call - it should use file_path not key.
|
|
146
|
+
|
|
147
|
+
⏺ Read 1 file (ctrl+o to expand)
|
|
148
|
+
|
|
149
|
+
⏺ Now I have the full file. Let me explain the output parsing logic, which is the key part the user asked about.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
The output parsing logic is in the parse_output(raw: str) function (lines 233-315). Here's how it works:
|
|
153
|
+
|
|
154
|
+
Output Parsing Flow
|
|
155
|
+
|
|
156
|
+
1. Extract Thinking Blocks (lines 237-244)
|
|
157
|
+
|
|
158
|
+
First, it extracts any `
|
|
159
|
+
|
|
160
|
+
⏺ The output parsing logic is in the parse_output(raw: str) function (lines 233-315). Here's how it works:
|
|
161
|
+
|
|
162
|
+
Output Parsing Flow
|
|
163
|
+
|
|
164
|
+
1. Extract Thinking Blocks (lines 237-244)
|
|
165
|
+
|
|
166
|
+
First, it extracts any `
|
|
167
|
+
|
|
168
|
+
⏺ ` blocks by finding the earliest occurrences of both markers and splitting them out:
|
|
169
|
+
while "
|
|
170
|
+
|
|
171
|
+
⏺ " in remaining:
|
|
172
|
+
s = remaining.index("
|
|
173
|
+
|
|
174
|
+
⏺ ")
|
|
175
|
+
# Extract thinking block
|
|
176
|
+
remaining = remaining[e + 8:].strip()
|
|
177
|
+
|
|
178
|
+
### 2. **Extract Tool Use Blocks** (lines 247-289)
|
|
179
|
+
It searches for `<tool_call>...</tool_call>` markers and tries to parse the content:
|
|
180
|
+
- **First attempt**: Try parsing as JSON with `name` or `tool` field
|
|
181
|
+
- **Fallback**: Parse as XML-like format with `<function=...>` and `<parameter=...>` tags
|
|
182
|
+
- Creates tool blocks with `type: "tool_use"`, unique ID, name, and input
|
|
183
|
+
|
|
184
|
+
### 3. **Handle JSON Tool Response** (lines 291-311)
|
|
185
|
+
If no `<tool_call>...</tool_call>` blocks were found, it checks if the remaining output is a JSON object with `name` field (indicating a tool response).
|
|
186
|
+
|
|
187
|
+
### 4. **Return Result** (line 315)
|
|
188
|
+
Returns a tuple of `(blocks, stop_reason)`:
|
|
189
|
+
- `blocks`: List of parsed blocks (`{"type": "text"}`, `{"type": "thinking"}`, `{"type": "tool_use"}`)
|
|
190
|
+
- `stop_reason`: `"tool_use"` if tools were detected, `"end_turn"` otherwise
|
|
191
|
+
|
|
192
|
+
## Key Patterns Used
|
|
193
|
+
|
|
194
|
+
| Pattern | Purpose |
|
|
195
|
+
|---------|---------|
|
|
196
|
+
| `
|
|
197
|
+
|
|
198
|
+
⏺ | Thinking block markers | |...| Tool invocation markers | | JSON withname/tool| Tool response format | |<function=...>/<parameter=...>` | Alternative tool format |
|
|
199
|
+
|
|
200
|
+
The parser is designed to handle both the tool invocation format (<tool_call>...</tool_call>) and tool response format (JSON with name field) used by the system.
|
|
201
|
+
|
|
202
|
+
✻ Worked for 1m 9s
|
|
203
|
+
</pre></details><br>
|
|
204
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# mlx-code: Local Claude Code for Mac
|
|
2
|
+
|
|
3
|
+
<img width="1024" height="1536" alt="mlxcodelogo" src="https://github.com/user-attachments/assets/df605c1d-f8e7-433c-9aaf-176c73e3d628" />
|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
### Quick Start
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
brew install --cask claude-code
|
|
10
|
+
pip install mlx-code
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Commands
|
|
14
|
+
| Command | What it does | Example |
|
|
15
|
+
|--------|--------------|--------|
|
|
16
|
+
| `mlx-code` | Start interactive mode | `mlx-code` |
|
|
17
|
+
| `mlx-code "task"` | Run a one-time task | `mlx-code "fix the build error"` |
|
|
18
|
+
| `mlx-code -p "query"` | Run one-off query, then exit | `mlx-code -p "explain this function"` |
|
|
19
|
+
| `mlx-code -c` | Continue most recent conversation in current directory | `mlx-code -c` |
|
|
20
|
+
| `mlx-code -r` | Resume a previous conversation | `mlx-code -r` |
|
|
21
|
+
| `mlx-code commit` | Create a Git commit | `mlx-code commit` |
|
|
22
|
+
| `/clear` | Clear conversation history | `/clear` |
|
|
23
|
+
| `/help` | Show available commands | `/help` |
|
|
24
|
+
| `exit` or `Ctrl+C` | Exit Claude Code | `exit` |
|
|
25
|
+
|
|
26
|
+
<details><summary>Click to expand</summary><pre>
|
|
27
|
+
╭─── Claude Code v2.1.74 ──────────────────────────────────────────────────────────────────────────────────╮
|
|
28
|
+
│ │ Tips for getting started │
|
|
29
|
+
│ Welcome back! │ Run /init to create a CLAUDE.md file with instruct… │
|
|
30
|
+
│ │ ─────────────────────────────────────────────────── │
|
|
31
|
+
│ ▐▛███▜▌ │ Recent activity │
|
|
32
|
+
│ ▝▜█████▛▘ │ No recent activity │
|
|
33
|
+
│ ▘▘ ▝▝ │ │
|
|
34
|
+
│ │ │
|
|
35
|
+
│ mlx-community/Qwen3.5-4B-Opti… · API Usage Billing │ │
|
|
36
|
+
│ ~/Downloads/tmp │ │
|
|
37
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
38
|
+
|
|
39
|
+
❯ list the files in this directory and explain what each one does
|
|
40
|
+
|
|
41
|
+
⏺ The user wants to understand the files in the current directory, so I'll use the Glob tool to list all files first.
|
|
42
|
+
|
|
43
|
+
⏺ Searched for 1 pattern (ctrl+o to expand)
|
|
44
|
+
|
|
45
|
+
⏺ I need to fix the Glob tool call - the parameter should be pattern, not key.
|
|
46
|
+
|
|
47
|
+
⏺ Searched for 1 pattern (ctrl+o to expand)
|
|
48
|
+
|
|
49
|
+
⏺ The user wants me to explain what each file does. I should organize these by project directory and explain the purpose of each file. Let me read a few key files to provide accurate explanations.
|
|
50
|
+
|
|
51
|
+
This directory contains several project folders. Let me explain each one:
|
|
52
|
+
|
|
53
|
+
Main Projects
|
|
54
|
+
|
|
55
|
+
cyberpunk/
|
|
56
|
+
|
|
57
|
+
- main.zig - Main entry point for a Zig-based cyberpunk project
|
|
58
|
+
- build.zig - Build configuration file for Zig
|
|
59
|
+
- map.c - C source file, likely for map rendering or game logic
|
|
60
|
+
- help.py - Python helper script, possibly for documentation or utilities
|
|
61
|
+
|
|
62
|
+
rts/
|
|
63
|
+
|
|
64
|
+
- main.zig - Main entry point for a real-time strategy (RTS) game in Zig
|
|
65
|
+
- build.zig - Build configuration for Zig
|
|
66
|
+
- battle_sim.c - C file for battle simulation logic
|
|
67
|
+
|
|
68
|
+
splat_zig_raylib/
|
|
69
|
+
|
|
70
|
+
- main.zig (two versions - one in splat_zig/, one in root) - Main entry point for a Raylib-based game
|
|
71
|
+
- build.zig - Build configuration
|
|
72
|
+
- gsplat_transform.py - Python script for splat transform calculations
|
|
73
|
+
- index.html - Web entry point for the game
|
|
74
|
+
- build_web.sh - Shell script for building web version
|
|
75
|
+
- README.md - Project documentation
|
|
76
|
+
|
|
77
|
+
tarot/
|
|
78
|
+
|
|
79
|
+
- tarot.py - Python script for tarot-related functionality
|
|
80
|
+
- index.html - Web entry point
|
|
81
|
+
|
|
82
|
+
yorha/
|
|
83
|
+
|
|
84
|
+
- index.html - Web entry point (likely a simple web page)
|
|
85
|
+
|
|
86
|
+
jina.py
|
|
87
|
+
|
|
88
|
+
- jina.py - Python script, possibly related to Jina AI or a utility
|
|
89
|
+
|
|
90
|
+
mlx-code-v1.py & mlx-code.py
|
|
91
|
+
|
|
92
|
+
- These appear to be different versions of the mlx-code agent wrapper (the local Claude Code-style agent for Apple Silicon)
|
|
93
|
+
|
|
94
|
+
tmp.md
|
|
95
|
+
|
|
96
|
+
- tmp.md - Temporary markdown file (contents unknown)
|
|
97
|
+
|
|
98
|
+
mlx_trace.log
|
|
99
|
+
|
|
100
|
+
- mlx_trace.log - Trace log file from the mlx model runtime
|
|
101
|
+
|
|
102
|
+
.DS_Store files
|
|
103
|
+
|
|
104
|
+
- macOS metadata files (can be ignored)
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
Summary: This is a temporary download folder (/Users/jjms/Downloads/tmp) containing multiple small projects:
|
|
108
|
+
- Game projects in Zig (cyberpunk, RTS, Raylib-based game)
|
|
109
|
+
- Python scripts for various utilities
|
|
110
|
+
- Web entry points (HTML files)
|
|
111
|
+
- Build configurations (Zig build files)
|
|
112
|
+
|
|
113
|
+
The Zig projects appear to be the main focus, with game-related code and build systems.
|
|
114
|
+
|
|
115
|
+
✻ Brewed for 42s
|
|
116
|
+
|
|
117
|
+
❯ explain how output is parsed in mlx-code.py
|
|
118
|
+
|
|
119
|
+
⏺ The user wants to understand how output is parsed in mlx-code.py, so I need to read that file first to understand its structure and find the output parsing logic.
|
|
120
|
+
|
|
121
|
+
⏺ Read 1 file (ctrl+o to expand)
|
|
122
|
+
|
|
123
|
+
⏺ I need to fix the Read tool call - it should use file_path not key.
|
|
124
|
+
|
|
125
|
+
⏺ Read 1 file (ctrl+o to expand)
|
|
126
|
+
|
|
127
|
+
⏺ Now I have the full file. Let me explain the output parsing logic, which is the key part the user asked about.
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
The output parsing logic is in the parse_output(raw: str) function (lines 233-315). Here's how it works:
|
|
131
|
+
|
|
132
|
+
Output Parsing Flow
|
|
133
|
+
|
|
134
|
+
1. Extract Thinking Blocks (lines 237-244)
|
|
135
|
+
|
|
136
|
+
First, it extracts any `
|
|
137
|
+
|
|
138
|
+
⏺ The output parsing logic is in the parse_output(raw: str) function (lines 233-315). Here's how it works:
|
|
139
|
+
|
|
140
|
+
Output Parsing Flow
|
|
141
|
+
|
|
142
|
+
1. Extract Thinking Blocks (lines 237-244)
|
|
143
|
+
|
|
144
|
+
First, it extracts any `
|
|
145
|
+
|
|
146
|
+
⏺ ` blocks by finding the earliest occurrences of both markers and splitting them out:
|
|
147
|
+
while "
|
|
148
|
+
|
|
149
|
+
⏺ " in remaining:
|
|
150
|
+
s = remaining.index("
|
|
151
|
+
|
|
152
|
+
⏺ ")
|
|
153
|
+
# Extract thinking block
|
|
154
|
+
remaining = remaining[e + 8:].strip()
|
|
155
|
+
|
|
156
|
+
### 2. **Extract Tool Use Blocks** (lines 247-289)
|
|
157
|
+
It searches for `<tool_call>...</tool_call>` markers and tries to parse the content:
|
|
158
|
+
- **First attempt**: Try parsing as JSON with `name` or `tool` field
|
|
159
|
+
- **Fallback**: Parse as XML-like format with `<function=...>` and `<parameter=...>` tags
|
|
160
|
+
- Creates tool blocks with `type: "tool_use"`, unique ID, name, and input
|
|
161
|
+
|
|
162
|
+
### 3. **Handle JSON Tool Response** (lines 291-311)
|
|
163
|
+
If no `<tool_call>...</tool_call>` blocks were found, it checks if the remaining output is a JSON object with `name` field (indicating a tool response).
|
|
164
|
+
|
|
165
|
+
### 4. **Return Result** (line 315)
|
|
166
|
+
Returns a tuple of `(blocks, stop_reason)`:
|
|
167
|
+
- `blocks`: List of parsed blocks (`{"type": "text"}`, `{"type": "thinking"}`, `{"type": "tool_use"}`)
|
|
168
|
+
- `stop_reason`: `"tool_use"` if tools were detected, `"end_turn"` otherwise
|
|
169
|
+
|
|
170
|
+
## Key Patterns Used
|
|
171
|
+
|
|
172
|
+
| Pattern | Purpose |
|
|
173
|
+
|---------|---------|
|
|
174
|
+
| `
|
|
175
|
+
|
|
176
|
+
⏺ | Thinking block markers | |...| Tool invocation markers | | JSON withname/tool| Tool response format | |<function=...>/<parameter=...>` | Alternative tool format |
|
|
177
|
+
|
|
178
|
+
The parser is designed to handle both the tool invocation format (<tool_call>...</tool_call>) and tool response format (JSON with name field) used by the system.
|
|
179
|
+
|
|
180
|
+
✻ Worked for 1m 9s
|
|
181
|
+
</pre></details><br>
|
|
182
|
+
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
|
+
import logging
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
import subprocess
|
|
@@ -19,27 +20,62 @@ DEFAULT_MODEL = "mlx-community/Qwen3.5-4B-OptiQ-4bit"
|
|
|
19
20
|
DEFAULT_SKILL_DIRS = ["./skills", os.path.expanduser("~/.claude/skills")]
|
|
20
21
|
LOG_FILE = "mlx_trace.log"
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
23
|
+
logging.basicConfig(
|
|
24
|
+
filename=LOG_FILE,
|
|
25
|
+
level=logging.DEBUG,
|
|
26
|
+
format="%(message)s",
|
|
27
|
+
)
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
class AppState:
|
|
31
|
+
def __init__(self, model, tokenizer, model_id: str, skills: dict):
|
|
32
|
+
self.model = model
|
|
33
|
+
self.tokenizer = tokenizer
|
|
34
|
+
self.model_id = model_id
|
|
35
|
+
self.skills = skills
|
|
36
|
+
self._counter = 0
|
|
37
|
+
self._lock = threading.Lock()
|
|
38
|
+
|
|
39
|
+
def trace(self, prompt: str, raw: str, elapsed: float):
|
|
40
|
+
with self._lock:
|
|
41
|
+
self._counter += 1
|
|
42
|
+
count = self._counter
|
|
43
|
+
sep = "=" * 80
|
|
44
|
+
logger.debug(
|
|
45
|
+
"\n%s\nCALL %d %s (%.1fs)\n%s\n--- PROMPT ---\n%s\n--- OUTPUT ---\n%s\n",
|
|
46
|
+
sep, count, time.strftime("%Y-%m-%d %H:%M:%S"), elapsed, sep, prompt, raw,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def skill_body(self, name: str) -> str:
|
|
50
|
+
if name not in self.skills:
|
|
51
|
+
available = ", ".join(self.skills) or "none"
|
|
52
|
+
return f"Unknown skill '{name}'. Available: {available}"
|
|
53
|
+
try:
|
|
54
|
+
return Path(self.skills[name]["path"]).read_text()
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return f"Error reading skill: {e}"
|
|
57
|
+
|
|
58
|
+
def generate(self, prompt: str, max_tokens: int, temp: float, top_p: float) -> str:
|
|
59
|
+
from mlx_lm import generate as mlx_gen
|
|
60
|
+
t0 = time.time()
|
|
61
|
+
raw = mlx_gen(
|
|
62
|
+
self.model, self.tokenizer,
|
|
63
|
+
prompt=prompt,
|
|
64
|
+
max_tokens=max_tokens,
|
|
65
|
+
verbose=False,
|
|
66
|
+
)
|
|
67
|
+
self.trace(prompt, raw, time.time() - t0)
|
|
68
|
+
return raw
|
|
69
|
+
|
|
70
|
+
def encode(self, text: str) -> list:
|
|
71
|
+
return self.tokenizer.encode(text)
|
|
72
|
+
|
|
73
|
+
def apply_chat_template(self, messages: list) -> str:
|
|
74
|
+
return self.tokenizer.apply_chat_template(
|
|
75
|
+
messages, tokenize=False, add_generation_prompt=True
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def parse_frontmatter(text: str):
|
|
43
79
|
m = re.match(r"^---\n(.*?)\n---\n", text, re.DOTALL)
|
|
44
80
|
if not m:
|
|
45
81
|
return {}, text
|
|
@@ -57,7 +93,8 @@ def parse_frontmatter(text):
|
|
|
57
93
|
fm[k.strip()] = v.strip()
|
|
58
94
|
return fm, body
|
|
59
95
|
|
|
60
|
-
|
|
96
|
+
|
|
97
|
+
def scan_skills(dirs: list) -> dict:
|
|
61
98
|
found = {}
|
|
62
99
|
for d in dirs:
|
|
63
100
|
p = Path(d)
|
|
@@ -75,37 +112,17 @@ def scan_skills(dirs):
|
|
|
75
112
|
print(f" warn: {f}: {e}", flush=True)
|
|
76
113
|
return found
|
|
77
114
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
except Exception as e:
|
|
84
|
-
return f"Error reading skill: {e}"
|
|
115
|
+
ANTI_LOOP_INSTRUCTION = (
|
|
116
|
+
"After you have written your response, stop immediately. "
|
|
117
|
+
"Do not re-read the question, do not re-draft the answer, "
|
|
118
|
+
"do not explain what you just did. Output your answer once and stop."
|
|
119
|
+
)
|
|
85
120
|
|
|
86
|
-
def load_model(path):
|
|
87
|
-
global model, tokenizer, model_id
|
|
88
|
-
from mlx_lm import load
|
|
89
|
-
print(f"Loading {path} …", flush=True)
|
|
90
|
-
model, tokenizer = load(path)
|
|
91
|
-
model_id = path
|
|
92
|
-
print("Ready.\n", flush=True)
|
|
93
121
|
|
|
94
|
-
def
|
|
95
|
-
|
|
96
|
-
t0 = time.time()
|
|
97
|
-
raw = mlx_gen(
|
|
98
|
-
model, tokenizer,
|
|
99
|
-
prompt=prompt,
|
|
100
|
-
max_tokens=max_tokens,
|
|
101
|
-
verbose=False,
|
|
102
|
-
)
|
|
103
|
-
trace(prompt, raw, time.time() - t0)
|
|
104
|
-
return raw
|
|
105
|
-
|
|
106
|
-
def skills_system_addon():
|
|
122
|
+
def skills_system_addon(skills: dict) -> str:
|
|
123
|
+
base = ANTI_LOOP_INSTRUCTION
|
|
107
124
|
if not skills:
|
|
108
|
-
return
|
|
125
|
+
return base
|
|
109
126
|
entries = "\n".join(
|
|
110
127
|
f"<skill><n>{n}</n><description>{s['description']}</description></skill>"
|
|
111
128
|
for n, s in skills.items()
|
|
@@ -113,10 +130,12 @@ def skills_system_addon():
|
|
|
113
130
|
return (
|
|
114
131
|
"\n\n<available_skills>\n" + entries +
|
|
115
132
|
"\nUse the read_skill tool to get full instructions before attempting "
|
|
116
|
-
"any task that matches a skill.\n</available_skills
|
|
133
|
+
"any task that matches a skill.\n</available_skills>\n\n"
|
|
134
|
+
+ base
|
|
117
135
|
)
|
|
118
136
|
|
|
119
|
-
|
|
137
|
+
|
|
138
|
+
def tools_to_text(tools: list) -> str:
|
|
120
139
|
header = (
|
|
121
140
|
"You have access to these tools. "
|
|
122
141
|
"To call a tool output ONLY a <tool_call> block:\n"
|
|
@@ -131,7 +150,8 @@ def tools_to_text(tools):
|
|
|
131
150
|
lines.append(f"- {name}({params}): {desc}")
|
|
132
151
|
return "\n".join(lines)
|
|
133
152
|
|
|
134
|
-
|
|
153
|
+
|
|
154
|
+
def build_messages(body: dict, skills: dict, extra: list = None) -> list:
|
|
135
155
|
msgs = []
|
|
136
156
|
|
|
137
157
|
sys_parts = []
|
|
@@ -139,15 +159,16 @@ def build_messages(body, extra=None):
|
|
|
139
159
|
if isinstance(raw, str) and raw:
|
|
140
160
|
sys_parts.append(raw)
|
|
141
161
|
elif isinstance(raw, list):
|
|
142
|
-
t = "\n".join(b.get("text","") for b in raw if b.get("type")=="text")
|
|
143
|
-
if t:
|
|
144
|
-
|
|
162
|
+
t = "\n".join(b.get("text", "") for b in raw if b.get("type") == "text")
|
|
163
|
+
if t:
|
|
164
|
+
sys_parts.append(t)
|
|
165
|
+
sys_parts.append(skills_system_addon(skills))
|
|
145
166
|
|
|
146
167
|
if skills:
|
|
147
168
|
sys_parts.append(tools_to_text([{
|
|
148
169
|
"name": "read_skill",
|
|
149
170
|
"description": "Read the full SKILL.md instructions for a skill before using it.",
|
|
150
|
-
"input_schema": {"type":"object","properties":{"name":{"type":"string"}},"required":["name"]},
|
|
171
|
+
"input_schema": {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]},
|
|
151
172
|
}]))
|
|
152
173
|
|
|
153
174
|
system = "\n".join(p for p in sys_parts if p).strip()
|
|
@@ -166,7 +187,7 @@ def build_messages(body, extra=None):
|
|
|
166
187
|
if t == "text":
|
|
167
188
|
parts.append(block["text"])
|
|
168
189
|
elif t == "thinking":
|
|
169
|
-
parts.append(f"<think>\n{block.get('thinking','')}\n</think>")
|
|
190
|
+
parts.append(f"<think>\n{block.get('thinking', '')}\n</think>")
|
|
170
191
|
elif t == "tool_use":
|
|
171
192
|
args = "".join(
|
|
172
193
|
f"<parameter={k}>\n{v}\n</parameter>"
|
|
@@ -176,7 +197,7 @@ def build_messages(body, extra=None):
|
|
|
176
197
|
elif t == "tool_result":
|
|
177
198
|
rc = block.get("content", "")
|
|
178
199
|
if isinstance(rc, list):
|
|
179
|
-
rc = "\n".join(c.get("text","") for c in rc if c.get("type")=="text")
|
|
200
|
+
rc = "\n".join(c.get("text", "") for c in rc if c.get("type") == "text")
|
|
180
201
|
parts.append(f"<tool_response>\n{rc}\n</tool_response>")
|
|
181
202
|
msgs.append({"role": role, "content": "\n".join(parts)})
|
|
182
203
|
|
|
@@ -184,22 +205,23 @@ def build_messages(body, extra=None):
|
|
|
184
205
|
msgs.extend(extra)
|
|
185
206
|
return msgs
|
|
186
207
|
|
|
187
|
-
def build_prompt(body, extra=None):
|
|
188
|
-
msgs = build_messages(body, extra=extra)
|
|
189
|
-
return tokenizer.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
|
190
208
|
|
|
191
|
-
def
|
|
209
|
+
def build_prompt(body: dict, state: AppState, extra: list = None) -> str:
|
|
210
|
+
msgs = build_messages(body, state.skills, extra=extra)
|
|
211
|
+
return state.apply_chat_template(msgs)
|
|
212
|
+
|
|
213
|
+
def parse_output(raw: str):
|
|
192
214
|
blocks = []
|
|
193
215
|
remaining = raw
|
|
194
216
|
|
|
195
217
|
while "<think>" in remaining and "</think>" in remaining:
|
|
196
|
-
s
|
|
197
|
-
e
|
|
218
|
+
s = remaining.index("<think>")
|
|
219
|
+
e = remaining.index("</think>")
|
|
198
220
|
before = remaining[:s].strip()
|
|
199
221
|
if before:
|
|
200
222
|
blocks.append({"type": "text", "text": before})
|
|
201
|
-
blocks.append({"type": "thinking", "thinking": remaining[s+7:e].strip()})
|
|
202
|
-
remaining = remaining[e+8:].strip()
|
|
223
|
+
blocks.append({"type": "thinking", "thinking": remaining[s + 7:e].strip()})
|
|
224
|
+
remaining = remaining[e + 8:].strip()
|
|
203
225
|
remaining = remaining.replace("</think>", "").strip()
|
|
204
226
|
|
|
205
227
|
tool_blocks = []
|
|
@@ -213,8 +235,10 @@ def parse_output(raw):
|
|
|
213
235
|
name = obj.get("name") or obj.get("tool")
|
|
214
236
|
args = obj.get("arguments") or obj.get("input") or {}
|
|
215
237
|
if isinstance(args, str):
|
|
216
|
-
try:
|
|
217
|
-
|
|
238
|
+
try:
|
|
239
|
+
args = json.loads(args)
|
|
240
|
+
except Exception:
|
|
241
|
+
args = {"raw": args}
|
|
218
242
|
if name:
|
|
219
243
|
parsed = {"name": name, "input": args}
|
|
220
244
|
except Exception:
|
|
@@ -252,8 +276,10 @@ def parse_output(raw):
|
|
|
252
276
|
args = obj.get("arguments") or obj.get("input") or {}
|
|
253
277
|
if name:
|
|
254
278
|
if isinstance(args, str):
|
|
255
|
-
try:
|
|
256
|
-
|
|
279
|
+
try:
|
|
280
|
+
args = json.loads(args)
|
|
281
|
+
except Exception:
|
|
282
|
+
args = {"raw": args}
|
|
257
283
|
blocks.append({
|
|
258
284
|
"type": "tool_use",
|
|
259
285
|
"id": f"toolu_{uuid.uuid4().hex[:8]}",
|
|
@@ -268,35 +294,44 @@ def parse_output(raw):
|
|
|
268
294
|
blocks.append({"type": "text", "text": remaining})
|
|
269
295
|
return blocks or [{"type": "text", "text": raw}], "end_turn"
|
|
270
296
|
|
|
271
|
-
|
|
272
|
-
|
|
297
|
+
|
|
298
|
+
def resolve_read_skill(
|
|
299
|
+
blocks: list, body: dict, state: AppState,
|
|
300
|
+
max_tokens: int, temp: float, top_p: float,
|
|
301
|
+
) -> tuple:
|
|
302
|
+
extra = []
|
|
303
|
+
stop_reason = "tool_use"
|
|
304
|
+
|
|
273
305
|
for _ in range(5):
|
|
274
|
-
skill_calls = [b for b in blocks if b.get("type")=="tool_use" and b["name"]=="read_skill"]
|
|
306
|
+
skill_calls = [b for b in blocks if b.get("type") == "tool_use" and b["name"] == "read_skill"]
|
|
275
307
|
if not skill_calls:
|
|
276
308
|
break
|
|
277
309
|
for c in skill_calls:
|
|
278
|
-
name = c["input"].get("name","")
|
|
279
|
-
content = skill_body(name)
|
|
310
|
+
name = c["input"].get("name", "")
|
|
311
|
+
content = state.skill_body(name)
|
|
280
312
|
args = f"<parameter=name>\n{name}\n</parameter>"
|
|
281
313
|
extra.append({"role": "assistant", "content": f"<tool_call>\n<function=read_skill>\n{args}</function>\n</tool_call>"})
|
|
282
|
-
extra.append({"role": "user",
|
|
283
|
-
prompt = build_prompt(body, extra=extra)
|
|
284
|
-
raw = generate(prompt, max_tokens, temp, top_p)
|
|
314
|
+
extra.append({"role": "user", "content": f"<tool_response>\n{content}\n</tool_response>"})
|
|
315
|
+
prompt = build_prompt(body, state, extra=extra)
|
|
316
|
+
raw = state.generate(prompt, max_tokens, temp, top_p)
|
|
285
317
|
blocks, stop_reason = parse_output(raw)
|
|
286
|
-
|
|
318
|
+
if stop_reason == "end_turn":
|
|
319
|
+
break
|
|
320
|
+
|
|
287
321
|
return blocks, stop_reason
|
|
288
322
|
|
|
289
|
-
def sse(event, data):
|
|
323
|
+
def sse(event: str, data: dict) -> bytes:
|
|
290
324
|
return f"event: {event}\ndata: {json.dumps(data)}\n\n".encode()
|
|
291
325
|
|
|
292
|
-
|
|
326
|
+
|
|
327
|
+
def blocks_to_sse(blocks: list, msg_id: str, stop_reason: str, in_tokens: int, out_tokens: int) -> bytes:
|
|
293
328
|
out = bytearray()
|
|
294
329
|
|
|
295
330
|
out += sse("message_start", {
|
|
296
331
|
"type": "message_start",
|
|
297
332
|
"message": {
|
|
298
333
|
"id": msg_id, "type": "message", "role": "assistant",
|
|
299
|
-
"model":
|
|
334
|
+
"model": "local", "content": [], "stop_reason": None,
|
|
300
335
|
"stop_sequence": None,
|
|
301
336
|
"usage": {"input_tokens": in_tokens, "output_tokens": 0},
|
|
302
337
|
},
|
|
@@ -349,77 +384,89 @@ def blocks_to_sse(blocks, msg_id, stop_reason, in_tokens, out_tokens):
|
|
|
349
384
|
|
|
350
385
|
return bytes(out)
|
|
351
386
|
|
|
352
|
-
|
|
387
|
+
def make_handler(state: AppState):
|
|
353
388
|
|
|
354
|
-
|
|
355
|
-
pass
|
|
389
|
+
class Handler(BaseHTTPRequestHandler):
|
|
356
390
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
self.send_response(code)
|
|
360
|
-
self.send_header("Content-Type", "application/json")
|
|
361
|
-
self.send_header("Content-Length", str(len(body)))
|
|
362
|
-
self.end_headers()
|
|
363
|
-
self.wfile.write(body)
|
|
391
|
+
def log_message(self, fmt, *args):
|
|
392
|
+
pass
|
|
364
393
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
394
|
+
def send_json(self, code: int, obj: dict):
|
|
395
|
+
body = json.dumps(obj).encode()
|
|
396
|
+
self.send_response(code)
|
|
397
|
+
self.send_header("Content-Type", "application/json")
|
|
398
|
+
self.send_header("Content-Length", str(len(body)))
|
|
399
|
+
self.end_headers()
|
|
400
|
+
self.wfile.write(body)
|
|
368
401
|
|
|
369
|
-
|
|
370
|
-
|
|
402
|
+
def read_json(self) -> dict:
|
|
403
|
+
n = int(self.headers.get("Content-Length", 0))
|
|
404
|
+
return json.loads(self.rfile.read(n))
|
|
371
405
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
self.send_json(200, {"data": [
|
|
375
|
-
{"id": model_id, "object": "model",
|
|
376
|
-
"created": int(time.time()), "owned_by": "local"},
|
|
377
|
-
]})
|
|
378
|
-
else:
|
|
379
|
-
self.send_json(404, {"error": "not found"})
|
|
406
|
+
def path_base(self) -> str:
|
|
407
|
+
return self.path.split("?")[0].rstrip("/")
|
|
380
408
|
|
|
381
|
-
|
|
382
|
-
|
|
409
|
+
def do_GET(self):
|
|
410
|
+
if self.path_base() == "/v1/models":
|
|
411
|
+
self.send_json(200, {"data": [
|
|
412
|
+
{"id": state.model_id, "object": "model",
|
|
413
|
+
"created": int(time.time()), "owned_by": "local"},
|
|
414
|
+
]})
|
|
415
|
+
else:
|
|
416
|
+
self.send_json(404, {"error": "not found"})
|
|
383
417
|
|
|
384
|
-
|
|
385
|
-
self.
|
|
386
|
-
return
|
|
418
|
+
def do_POST(self):
|
|
419
|
+
pb = self.path_base()
|
|
387
420
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
421
|
+
if pb == "/v1/messages/count_tokens":
|
|
422
|
+
self.send_json(200, {"input_tokens": 0})
|
|
423
|
+
return
|
|
391
424
|
|
|
392
|
-
|
|
393
|
-
|
|
425
|
+
if pb != "/v1/messages":
|
|
426
|
+
self.send_json(404, {"error": f"unknown endpoint {pb}"})
|
|
427
|
+
return
|
|
394
428
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
top_p = body.get("top_p", 0.9)
|
|
398
|
-
msg_id = f"msg_{uuid.uuid4().hex}"
|
|
429
|
+
body = self.read_json()
|
|
430
|
+
body["model"] = state.model_id
|
|
399
431
|
|
|
400
|
-
|
|
401
|
-
|
|
432
|
+
max_tokens = body.get("max_tokens", 8192)
|
|
433
|
+
temp = body.get("temperature", 0.7)
|
|
434
|
+
top_p = body.get("top_p", 0.9)
|
|
435
|
+
msg_id = f"msg_{uuid.uuid4().hex}"
|
|
402
436
|
|
|
403
|
-
|
|
437
|
+
prompt = build_prompt(body, state)
|
|
438
|
+
raw = state.generate(prompt, max_tokens, temp, top_p)
|
|
404
439
|
|
|
405
|
-
|
|
406
|
-
blocks, stop_reason = resolve_read_skill(blocks, body, max_tokens, temp, top_p)
|
|
440
|
+
blocks, stop_reason = parse_output(raw)
|
|
407
441
|
|
|
408
|
-
|
|
409
|
-
|
|
442
|
+
if any(b.get("type") == "tool_use" and b["name"] == "read_skill" for b in blocks):
|
|
443
|
+
blocks, stop_reason = resolve_read_skill(blocks, body, state, max_tokens, temp, top_p)
|
|
410
444
|
|
|
411
|
-
|
|
445
|
+
in_tokens = len(state.encode(prompt))
|
|
446
|
+
out_tokens = len(state.encode(raw))
|
|
447
|
+
|
|
448
|
+
sse_bytes = blocks_to_sse(blocks, msg_id, stop_reason, in_tokens, out_tokens)
|
|
449
|
+
|
|
450
|
+
self.send_response(200)
|
|
451
|
+
self.send_header("Content-Type", "text/event-stream")
|
|
452
|
+
self.send_header("Cache-Control", "no-cache")
|
|
453
|
+
self.send_header("Content-Length", str(len(sse_bytes)))
|
|
454
|
+
self.end_headers()
|
|
455
|
+
try:
|
|
456
|
+
self.wfile.write(sse_bytes)
|
|
457
|
+
self.wfile.flush()
|
|
458
|
+
except BrokenPipeError:
|
|
459
|
+
pass
|
|
460
|
+
|
|
461
|
+
return Handler
|
|
462
|
+
|
|
463
|
+
def load_model(path: str):
|
|
464
|
+
from mlx_lm import load
|
|
465
|
+
print(f"Loading {path} …", flush=True)
|
|
466
|
+
model, tokenizer = load(path)
|
|
467
|
+
print("Ready.\n", flush=True)
|
|
468
|
+
return model, tokenizer
|
|
412
469
|
|
|
413
|
-
self.send_response(200)
|
|
414
|
-
self.send_header("Content-Type", "text/event-stream")
|
|
415
|
-
self.send_header("Cache-Control", "no-cache")
|
|
416
|
-
self.send_header("Content-Length", str(len(sse_bytes)))
|
|
417
|
-
self.end_headers()
|
|
418
|
-
try:
|
|
419
|
-
self.wfile.write(sse_bytes)
|
|
420
|
-
self.wfile.flush()
|
|
421
|
-
except BrokenPipeError:
|
|
422
|
-
pass
|
|
423
470
|
|
|
424
471
|
def main():
|
|
425
472
|
parser = argparse.ArgumentParser()
|
|
@@ -433,9 +480,10 @@ def main():
|
|
|
433
480
|
print("Scanning skills …", flush=True)
|
|
434
481
|
skills = scan_skills(skill_dirs)
|
|
435
482
|
|
|
436
|
-
load_model(args.model)
|
|
483
|
+
model, tokenizer = load_model(args.model)
|
|
484
|
+
state = AppState(model, tokenizer, args.model, skills)
|
|
437
485
|
|
|
438
|
-
server = HTTPServer((args.host, args.port),
|
|
486
|
+
server = HTTPServer((args.host, args.port), make_handler(state))
|
|
439
487
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
440
488
|
thread.start()
|
|
441
489
|
|
|
@@ -453,5 +501,6 @@ def main():
|
|
|
453
501
|
result = subprocess.run(["claude"] + claude_args, env=env)
|
|
454
502
|
sys.exit(result.returncode)
|
|
455
503
|
|
|
504
|
+
|
|
456
505
|
if __name__ == "__main__":
|
|
457
506
|
main()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mlx-code
|
|
3
|
+
Version: 0.0.1a3
|
|
4
|
+
Summary: Local Claude Code for Mac
|
|
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
|
+
Requires-Dist: PyYAML
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: license
|
|
19
|
+
Dynamic: requires-dist
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
Dynamic: summary
|
|
22
|
+
|
|
23
|
+
# mlx-code: Local Claude Code for Mac
|
|
24
|
+
|
|
25
|
+
<img width="1024" height="1536" alt="mlxcodelogo" src="https://github.com/user-attachments/assets/df605c1d-f8e7-433c-9aaf-176c73e3d628" />
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
### Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
brew install --cask claude-code
|
|
32
|
+
pip install mlx-code
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Commands
|
|
36
|
+
| Command | What it does | Example |
|
|
37
|
+
|--------|--------------|--------|
|
|
38
|
+
| `mlx-code` | Start interactive mode | `mlx-code` |
|
|
39
|
+
| `mlx-code "task"` | Run a one-time task | `mlx-code "fix the build error"` |
|
|
40
|
+
| `mlx-code -p "query"` | Run one-off query, then exit | `mlx-code -p "explain this function"` |
|
|
41
|
+
| `mlx-code -c` | Continue most recent conversation in current directory | `mlx-code -c` |
|
|
42
|
+
| `mlx-code -r` | Resume a previous conversation | `mlx-code -r` |
|
|
43
|
+
| `mlx-code commit` | Create a Git commit | `mlx-code commit` |
|
|
44
|
+
| `/clear` | Clear conversation history | `/clear` |
|
|
45
|
+
| `/help` | Show available commands | `/help` |
|
|
46
|
+
| `exit` or `Ctrl+C` | Exit Claude Code | `exit` |
|
|
47
|
+
|
|
48
|
+
<details><summary>Click to expand</summary><pre>
|
|
49
|
+
╭─── Claude Code v2.1.74 ──────────────────────────────────────────────────────────────────────────────────╮
|
|
50
|
+
│ │ Tips for getting started │
|
|
51
|
+
│ Welcome back! │ Run /init to create a CLAUDE.md file with instruct… │
|
|
52
|
+
│ │ ─────────────────────────────────────────────────── │
|
|
53
|
+
│ ▐▛███▜▌ │ Recent activity │
|
|
54
|
+
│ ▝▜█████▛▘ │ No recent activity │
|
|
55
|
+
│ ▘▘ ▝▝ │ │
|
|
56
|
+
│ │ │
|
|
57
|
+
│ mlx-community/Qwen3.5-4B-Opti… · API Usage Billing │ │
|
|
58
|
+
│ ~/Downloads/tmp │ │
|
|
59
|
+
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
60
|
+
|
|
61
|
+
❯ list the files in this directory and explain what each one does
|
|
62
|
+
|
|
63
|
+
⏺ The user wants to understand the files in the current directory, so I'll use the Glob tool to list all files first.
|
|
64
|
+
|
|
65
|
+
⏺ Searched for 1 pattern (ctrl+o to expand)
|
|
66
|
+
|
|
67
|
+
⏺ I need to fix the Glob tool call - the parameter should be pattern, not key.
|
|
68
|
+
|
|
69
|
+
⏺ Searched for 1 pattern (ctrl+o to expand)
|
|
70
|
+
|
|
71
|
+
⏺ The user wants me to explain what each file does. I should organize these by project directory and explain the purpose of each file. Let me read a few key files to provide accurate explanations.
|
|
72
|
+
|
|
73
|
+
This directory contains several project folders. Let me explain each one:
|
|
74
|
+
|
|
75
|
+
Main Projects
|
|
76
|
+
|
|
77
|
+
cyberpunk/
|
|
78
|
+
|
|
79
|
+
- main.zig - Main entry point for a Zig-based cyberpunk project
|
|
80
|
+
- build.zig - Build configuration file for Zig
|
|
81
|
+
- map.c - C source file, likely for map rendering or game logic
|
|
82
|
+
- help.py - Python helper script, possibly for documentation or utilities
|
|
83
|
+
|
|
84
|
+
rts/
|
|
85
|
+
|
|
86
|
+
- main.zig - Main entry point for a real-time strategy (RTS) game in Zig
|
|
87
|
+
- build.zig - Build configuration for Zig
|
|
88
|
+
- battle_sim.c - C file for battle simulation logic
|
|
89
|
+
|
|
90
|
+
splat_zig_raylib/
|
|
91
|
+
|
|
92
|
+
- main.zig (two versions - one in splat_zig/, one in root) - Main entry point for a Raylib-based game
|
|
93
|
+
- build.zig - Build configuration
|
|
94
|
+
- gsplat_transform.py - Python script for splat transform calculations
|
|
95
|
+
- index.html - Web entry point for the game
|
|
96
|
+
- build_web.sh - Shell script for building web version
|
|
97
|
+
- README.md - Project documentation
|
|
98
|
+
|
|
99
|
+
tarot/
|
|
100
|
+
|
|
101
|
+
- tarot.py - Python script for tarot-related functionality
|
|
102
|
+
- index.html - Web entry point
|
|
103
|
+
|
|
104
|
+
yorha/
|
|
105
|
+
|
|
106
|
+
- index.html - Web entry point (likely a simple web page)
|
|
107
|
+
|
|
108
|
+
jina.py
|
|
109
|
+
|
|
110
|
+
- jina.py - Python script, possibly related to Jina AI or a utility
|
|
111
|
+
|
|
112
|
+
mlx-code-v1.py & mlx-code.py
|
|
113
|
+
|
|
114
|
+
- These appear to be different versions of the mlx-code agent wrapper (the local Claude Code-style agent for Apple Silicon)
|
|
115
|
+
|
|
116
|
+
tmp.md
|
|
117
|
+
|
|
118
|
+
- tmp.md - Temporary markdown file (contents unknown)
|
|
119
|
+
|
|
120
|
+
mlx_trace.log
|
|
121
|
+
|
|
122
|
+
- mlx_trace.log - Trace log file from the mlx model runtime
|
|
123
|
+
|
|
124
|
+
.DS_Store files
|
|
125
|
+
|
|
126
|
+
- macOS metadata files (can be ignored)
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
Summary: This is a temporary download folder (/Users/jjms/Downloads/tmp) containing multiple small projects:
|
|
130
|
+
- Game projects in Zig (cyberpunk, RTS, Raylib-based game)
|
|
131
|
+
- Python scripts for various utilities
|
|
132
|
+
- Web entry points (HTML files)
|
|
133
|
+
- Build configurations (Zig build files)
|
|
134
|
+
|
|
135
|
+
The Zig projects appear to be the main focus, with game-related code and build systems.
|
|
136
|
+
|
|
137
|
+
✻ Brewed for 42s
|
|
138
|
+
|
|
139
|
+
❯ explain how output is parsed in mlx-code.py
|
|
140
|
+
|
|
141
|
+
⏺ The user wants to understand how output is parsed in mlx-code.py, so I need to read that file first to understand its structure and find the output parsing logic.
|
|
142
|
+
|
|
143
|
+
⏺ Read 1 file (ctrl+o to expand)
|
|
144
|
+
|
|
145
|
+
⏺ I need to fix the Read tool call - it should use file_path not key.
|
|
146
|
+
|
|
147
|
+
⏺ Read 1 file (ctrl+o to expand)
|
|
148
|
+
|
|
149
|
+
⏺ Now I have the full file. Let me explain the output parsing logic, which is the key part the user asked about.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
The output parsing logic is in the parse_output(raw: str) function (lines 233-315). Here's how it works:
|
|
153
|
+
|
|
154
|
+
Output Parsing Flow
|
|
155
|
+
|
|
156
|
+
1. Extract Thinking Blocks (lines 237-244)
|
|
157
|
+
|
|
158
|
+
First, it extracts any `
|
|
159
|
+
|
|
160
|
+
⏺ The output parsing logic is in the parse_output(raw: str) function (lines 233-315). Here's how it works:
|
|
161
|
+
|
|
162
|
+
Output Parsing Flow
|
|
163
|
+
|
|
164
|
+
1. Extract Thinking Blocks (lines 237-244)
|
|
165
|
+
|
|
166
|
+
First, it extracts any `
|
|
167
|
+
|
|
168
|
+
⏺ ` blocks by finding the earliest occurrences of both markers and splitting them out:
|
|
169
|
+
while "
|
|
170
|
+
|
|
171
|
+
⏺ " in remaining:
|
|
172
|
+
s = remaining.index("
|
|
173
|
+
|
|
174
|
+
⏺ ")
|
|
175
|
+
# Extract thinking block
|
|
176
|
+
remaining = remaining[e + 8:].strip()
|
|
177
|
+
|
|
178
|
+
### 2. **Extract Tool Use Blocks** (lines 247-289)
|
|
179
|
+
It searches for `<tool_call>...</tool_call>` markers and tries to parse the content:
|
|
180
|
+
- **First attempt**: Try parsing as JSON with `name` or `tool` field
|
|
181
|
+
- **Fallback**: Parse as XML-like format with `<function=...>` and `<parameter=...>` tags
|
|
182
|
+
- Creates tool blocks with `type: "tool_use"`, unique ID, name, and input
|
|
183
|
+
|
|
184
|
+
### 3. **Handle JSON Tool Response** (lines 291-311)
|
|
185
|
+
If no `<tool_call>...</tool_call>` blocks were found, it checks if the remaining output is a JSON object with `name` field (indicating a tool response).
|
|
186
|
+
|
|
187
|
+
### 4. **Return Result** (line 315)
|
|
188
|
+
Returns a tuple of `(blocks, stop_reason)`:
|
|
189
|
+
- `blocks`: List of parsed blocks (`{"type": "text"}`, `{"type": "thinking"}`, `{"type": "tool_use"}`)
|
|
190
|
+
- `stop_reason`: `"tool_use"` if tools were detected, `"end_turn"` otherwise
|
|
191
|
+
|
|
192
|
+
## Key Patterns Used
|
|
193
|
+
|
|
194
|
+
| Pattern | Purpose |
|
|
195
|
+
|---------|---------|
|
|
196
|
+
| `
|
|
197
|
+
|
|
198
|
+
⏺ | Thinking block markers | |...| Tool invocation markers | | JSON withname/tool| Tool response format | |<function=...>/<parameter=...>` | Alternative tool format |
|
|
199
|
+
|
|
200
|
+
The parser is designed to handle both the tool invocation format (<tool_call>...</tool_call>) and tool response format (JSON with name field) used by the system.
|
|
201
|
+
|
|
202
|
+
✻ Worked for 1m 9s
|
|
203
|
+
</pre></details><br>
|
|
204
|
+
|
|
@@ -6,13 +6,13 @@ setup(
|
|
|
6
6
|
author_email="albersj66@gmail.com",
|
|
7
7
|
author="J Joe",
|
|
8
8
|
license="Apache-2.0",
|
|
9
|
-
version="0.0.
|
|
9
|
+
version="0.0.1a3",
|
|
10
10
|
readme="README.md",
|
|
11
11
|
description="Local Claude Code for Mac",
|
|
12
12
|
long_description=open("README.md").read(),
|
|
13
13
|
long_description_content_type="text/markdown",
|
|
14
14
|
python_requires=">=3.11",
|
|
15
|
-
install_requires=["mlx-lm>=0.19.0"],
|
|
15
|
+
install_requires=["mlx-lm>=0.19.0", "PyYAML"],
|
|
16
16
|
py_modules=["main"],
|
|
17
17
|
entry_points={"console_scripts": ["mlx-code=main:main"]},
|
|
18
18
|
)
|
mlx_code-0.0.1a2/PKG-INFO
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mlx-code
|
|
3
|
-
Version: 0.0.1a2
|
|
4
|
-
Summary: Local Claude Code for Mac
|
|
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: Local Claude Code for Mac
|
|
23
|
-
|
|
24
|
-

|
mlx_code-0.0.1a2/README.md
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mlx-code
|
|
3
|
-
Version: 0.0.1a2
|
|
4
|
-
Summary: Local Claude Code for Mac
|
|
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: Local Claude Code for Mac
|
|
23
|
-
|
|
24
|
-

|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|