mlx-code 0.0.18__tar.gz → 0.0.19__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.18 → mlx_code-0.0.19}/PKG-INFO +2 -2
- {mlx_code-0.0.18 → mlx_code-0.0.19}/README.md +1 -1
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/gits.py +52 -21
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/repl.py +70 -32
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/tools.py +1 -1
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code.egg-info/PKG-INFO +2 -2
- {mlx_code-0.0.18 → mlx_code-0.0.19}/setup.py +1 -1
- {mlx_code-0.0.18 → mlx_code-0.0.19}/LICENSE +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/__init__.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/apis.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/lsp_tool.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/main.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/mcb.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/mcb_tool.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/stream_log.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/util.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/view_git.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code/view_log.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code.egg-info/SOURCES.txt +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code.egg-info/dependency_links.txt +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code.egg-info/entry_points.txt +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code.egg-info/requires.txt +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/mlx_code.egg-info/top_level.txt +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/setup.cfg +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/tests/__init__.py +0 -0
- {mlx_code-0.0.18 → mlx_code-0.0.19}/tests/test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -38,7 +38,7 @@ Dynamic: summary
|
|
|
38
38
|
|
|
39
39
|
A lightweight coding agent built on Apple's MLX framework.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
https://github.com/user-attachments/assets/0569d101-8d0a-4e67-9e82-fce84a5ef3f0
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight coding agent built on Apple's MLX framework.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
https://github.com/user-attachments/assets/0569d101-8d0a-4e67-9e82-fce84a5ef3f0
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -19,7 +19,8 @@ def _git(cwd: str, *args: str, check: bool=True) -> str:
|
|
|
19
19
|
r = subprocess.run(['git', *args], cwd=cwd, capture_output=True, text=True, check=check)
|
|
20
20
|
return r.stdout.strip()
|
|
21
21
|
except subprocess.CalledProcessError as exc:
|
|
22
|
-
|
|
22
|
+
args_str = repr(' '.join(args))
|
|
23
|
+
raise GitError(f'git {args_str} failed in {cwd!r}: {exc.stderr.strip()}') from exc
|
|
23
24
|
except FileNotFoundError as exc:
|
|
24
25
|
raise GitError('git executable not found in PATH') from exc
|
|
25
26
|
|
|
@@ -28,19 +29,33 @@ def _make_commit_message(messages) -> str:
|
|
|
28
29
|
return messages
|
|
29
30
|
filtered = [m for m in messages if m.get('role') != 'commit']
|
|
30
31
|
last_user = next((m['content'] for m in reversed(filtered) if m.get('role') == 'user'), None)
|
|
31
|
-
title = last_user.replace('\n', ' ').strip()[:
|
|
32
|
-
|
|
32
|
+
title = last_user.replace('\n', ' ').strip()[:60] if isinstance(last_user, str) else 'update'
|
|
33
|
+
body = json.dumps(filtered, indent=2, ensure_ascii=False)
|
|
34
|
+
return f'{title}\n\n--- BEGIN MESSAGES ---\n{body}'
|
|
33
35
|
|
|
34
36
|
def _parse_messages_from_commit(raw: str) -> list[dict]:
|
|
35
37
|
if not raw or raw in ('snapshot', 'update'):
|
|
36
38
|
return []
|
|
37
|
-
|
|
39
|
+
marker = '--- BEGIN MESSAGES ---\n'
|
|
40
|
+
idx = raw.find(marker)
|
|
41
|
+
if idx != -1:
|
|
42
|
+
payload = raw[idx + len(marker):]
|
|
43
|
+
else:
|
|
38
44
|
parts = raw.split('\n\n', 1)
|
|
39
|
-
|
|
45
|
+
payload = parts[1] if len(parts) == 2 else parts[0]
|
|
46
|
+
try:
|
|
47
|
+
msgs = json.loads(payload)
|
|
40
48
|
return msgs if isinstance(msgs, list) else []
|
|
41
|
-
except
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
logger.warning('_parse_messages_from_commit: could not parse JSON from commit body')
|
|
42
51
|
return []
|
|
43
52
|
|
|
53
|
+
def _count_user_turns(commit_body: str) -> int:
|
|
54
|
+
messages = _parse_messages_from_commit(commit_body)
|
|
55
|
+
if messages:
|
|
56
|
+
return sum((1 for m in messages if m.get('role') == 'user'))
|
|
57
|
+
return 0
|
|
58
|
+
|
|
44
59
|
def git_add_filtered(cwd: str) -> None:
|
|
45
60
|
excludes = [f':(exclude){p}' for p in _ADD_EXCLUDES]
|
|
46
61
|
try:
|
|
@@ -117,22 +132,37 @@ def commit_worktree(point: LedgerPoint | None, message: str='update') -> tuple[L
|
|
|
117
132
|
return (None, '')
|
|
118
133
|
|
|
119
134
|
def cleanup_worktree(point: LedgerPoint, *, remove_branch: bool=False) -> None:
|
|
120
|
-
root
|
|
121
|
-
if root:
|
|
122
|
-
try:
|
|
123
|
-
_git(root, 'worktree', 'remove', '--force', point.worktree)
|
|
124
|
-
except Exception as e:
|
|
125
|
-
logger.warning('git worktree remove failed: %s', e)
|
|
135
|
+
root: str | None = None
|
|
126
136
|
if os.path.exists(point.worktree):
|
|
137
|
+
root = _git(point.worktree, 'rev-parse', '--show-toplevel', check=False) or None
|
|
138
|
+
if root:
|
|
139
|
+
try:
|
|
140
|
+
_git(root, 'worktree', 'remove', '--force', point.worktree)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning('git worktree remove failed: %s', e)
|
|
127
143
|
try:
|
|
128
144
|
shutil.rmtree(point.worktree, ignore_errors=True)
|
|
129
145
|
except Exception as e:
|
|
130
146
|
logger.warning('Filesystem cleanup failed for %s: %s', point.worktree, e)
|
|
131
|
-
|
|
147
|
+
else:
|
|
148
|
+
logger.warning('cleanup_worktree: worktree path %r no longer exists; skipping removal', point.worktree)
|
|
149
|
+
try:
|
|
150
|
+
root = _git(os.path.dirname(point.worktree), 'rev-parse', '--show-toplevel', check=False) or None
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
if root:
|
|
132
154
|
try:
|
|
133
|
-
_git(root, '
|
|
155
|
+
_git(root, 'worktree', 'prune')
|
|
134
156
|
except Exception as e:
|
|
135
|
-
logger.warning('git
|
|
157
|
+
logger.warning('git worktree prune failed: %s', e)
|
|
158
|
+
if remove_branch:
|
|
159
|
+
if root:
|
|
160
|
+
try:
|
|
161
|
+
_git(root, 'branch', '-D', point.branch)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.warning('git branch deletion failed for %r: %s', point.branch, e)
|
|
164
|
+
else:
|
|
165
|
+
logger.warning('cleanup_worktree: could not locate repo root; branch %r was NOT deleted', point.branch)
|
|
136
166
|
|
|
137
167
|
def resume_worktree(repo_dir: str, commit: str, *, worktree_dir: str | None=None, prefix: str='resume') -> tuple[LedgerPoint | None, list[dict]]:
|
|
138
168
|
try:
|
|
@@ -208,7 +238,7 @@ def get_commit_history_with_stats(worktree: str, limit: int=50) -> list[dict]:
|
|
|
208
238
|
if line.startswith('COMMIT:'):
|
|
209
239
|
if current_commit:
|
|
210
240
|
body_text = '\n'.join(body_lines)
|
|
211
|
-
current_commit['user_turns'] = body_text
|
|
241
|
+
current_commit['user_turns'] = _count_user_turns(body_text)
|
|
212
242
|
commits.append(current_commit)
|
|
213
243
|
current_commit = {'sha': line[7:], 'short_sha': '', 'refs': '', 'subject': '', 'files': []}
|
|
214
244
|
in_body = False
|
|
@@ -237,16 +267,17 @@ def get_commit_history_with_stats(worktree: str, limit: int=50) -> list[dict]:
|
|
|
237
267
|
current_commit['files'].append(line)
|
|
238
268
|
if current_commit:
|
|
239
269
|
body_text = '\n'.join(body_lines)
|
|
240
|
-
current_commit['user_turns'] = body_text
|
|
270
|
+
current_commit['user_turns'] = _count_user_turns(body_text)
|
|
241
271
|
commits.append(current_commit)
|
|
242
272
|
return commits
|
|
243
273
|
except Exception as e:
|
|
244
274
|
logger.warning(f'get_commit_history_with_stats failed: {e}')
|
|
245
275
|
return []
|
|
246
276
|
|
|
247
|
-
def find_rev_commit(worktree: str, n: int) -> str | None:
|
|
277
|
+
def find_rev_commit(worktree: str, n: int, *, limit: int=0) -> str | None:
|
|
278
|
+
limit_args = ['-n', str(limit)] if limit > 0 else []
|
|
248
279
|
try:
|
|
249
|
-
result = subprocess.run(['git', 'log', '--format=COMMIT:%H%n%b%nEND_BODY',
|
|
280
|
+
result = subprocess.run(['git', 'log', '--format=COMMIT:%H%n%b%nEND_BODY', *limit_args], cwd=worktree, capture_output=True, text=True, check=True)
|
|
250
281
|
output = result.stdout
|
|
251
282
|
best_exact: str | None = None
|
|
252
283
|
best_below: str | None = None
|
|
@@ -256,7 +287,7 @@ def find_rev_commit(worktree: str, n: int) -> str | None:
|
|
|
256
287
|
if line.startswith('COMMIT:'):
|
|
257
288
|
if current_sha is not None:
|
|
258
289
|
body = '\n'.join(body_lines)
|
|
259
|
-
count = body
|
|
290
|
+
count = _count_user_turns(body)
|
|
260
291
|
if count == n and best_exact is None:
|
|
261
292
|
best_exact = current_sha
|
|
262
293
|
if count < n and best_below is None:
|
|
@@ -269,7 +300,7 @@ def find_rev_commit(worktree: str, n: int) -> str | None:
|
|
|
269
300
|
body_lines.append(line)
|
|
270
301
|
if current_sha is not None:
|
|
271
302
|
body = '\n'.join(body_lines)
|
|
272
|
-
count = body
|
|
303
|
+
count = _count_user_turns(body)
|
|
273
304
|
if count == n and best_exact is None:
|
|
274
305
|
best_exact = current_sha
|
|
275
306
|
if count < n and best_below is None:
|
|
@@ -27,6 +27,7 @@ from rich.text import Text as RichText
|
|
|
27
27
|
from rich.table import Table
|
|
28
28
|
from rich.markup import escape
|
|
29
29
|
from rich.markdown import Markdown
|
|
30
|
+
from rich.cells import cell_len
|
|
30
31
|
|
|
31
32
|
class Agent:
|
|
32
33
|
|
|
@@ -51,7 +52,7 @@ class Agent:
|
|
|
51
52
|
self.tools = tools
|
|
52
53
|
|
|
53
54
|
def spawn(self, **overrides) -> 'Agent':
|
|
54
|
-
kwargs = {'
|
|
55
|
+
kwargs = {'system': self.system, 'extra_tool_classes': self._extra_tool_classes, 'model': self.model, 'api_key': self.api_key, 'base_url': self.base_url, 'ctx': {k: v for k, v in self.ctx.items() if k != 'agent'}}
|
|
55
56
|
kwargs.update(overrides)
|
|
56
57
|
return Agent(**kwargs)
|
|
57
58
|
|
|
@@ -63,6 +64,8 @@ class Agent:
|
|
|
63
64
|
async def run(self, prompt: str) -> dict:
|
|
64
65
|
await self._wait()
|
|
65
66
|
self._signal = None
|
|
67
|
+
self._last_result_sig = None
|
|
68
|
+
self._same_result_count = 0
|
|
66
69
|
self.messages.append({'role': 'user', 'content': prompt})
|
|
67
70
|
return await self._loop()
|
|
68
71
|
|
|
@@ -250,10 +253,13 @@ def _render_split_diff(diff_text: str, ref1_label: str='HEAD~1', ref2_label: str
|
|
|
250
253
|
_flush(left_lines, right_lines)
|
|
251
254
|
return tbl
|
|
252
255
|
|
|
253
|
-
def
|
|
256
|
+
def _make_empty_history_table() -> Table:
|
|
254
257
|
tbl = Table(show_header=False, show_lines=False, box=None, pad_edge=False, expand=True, padding=0)
|
|
255
258
|
tbl.add_column(width=2)
|
|
256
259
|
tbl.add_column(ratio=1)
|
|
260
|
+
return tbl
|
|
261
|
+
|
|
262
|
+
def append_to_history_table(tbl: Table, messages: list[dict]) -> None:
|
|
257
263
|
for msg in messages:
|
|
258
264
|
role = msg.get('role')
|
|
259
265
|
content = msg.get('content')
|
|
@@ -313,6 +319,10 @@ def render_history(messages: list[dict]) -> Table:
|
|
|
313
319
|
else:
|
|
314
320
|
body = RichText(escape(text), style=style)
|
|
315
321
|
tbl.add_row(RichText(prefix, style=style), body)
|
|
322
|
+
|
|
323
|
+
def render_history(messages: list[dict]) -> Table:
|
|
324
|
+
tbl = _make_empty_history_table()
|
|
325
|
+
append_to_history_table(tbl, messages)
|
|
316
326
|
return tbl
|
|
317
327
|
REPL_HELP = '\nCommands:\n/help show this message\n/clear clear conversation (transcript + agent memory)\n/history show full conversation transcript\n/history --raw show raw API message log (debug)\n/diff [--all] show side-by-side diff of changes\n/errors show timestamped error log for this tab\n/tools list active tools\n/branch [--rev N] [--as-worktree] [prompt]\n open a branch tab; optional prompt runs immediately\n/abort abort the running agent\n/export [path] export session to JSON\n/exit /quit close branch tab, or exit the app\n!command run shell command in worktree (output captured in TUI)\n!!command run interactive shell command (TUI suspends, terminal handed to process)\n e.g. !ls !git diff !cat file.py\n !!vim file.py !!yazi !!less log.txt\nKeys:\nEnter submit\nCtrl-J insert newline in editor\nAlt-1 … Alt-9 jump directly to tab N\nTab / Shift-Tab cycle through tabs\nCtrl-C abort running agent\nCtrl-D close branch tab, or exit app\nCtrl-R recall last prompt into editor\n'
|
|
318
328
|
|
|
@@ -368,6 +378,7 @@ class Tab(Vertical):
|
|
|
368
378
|
self._stream_blocks: list[dict] = []
|
|
369
379
|
self._command_blocks: list[dict] = []
|
|
370
380
|
self._cache_count: int = -1
|
|
381
|
+
self._rendered_cache: Table | None = None
|
|
371
382
|
|
|
372
383
|
@property
|
|
373
384
|
def is_running(self) -> bool:
|
|
@@ -439,12 +450,19 @@ class Tab(Vertical):
|
|
|
439
450
|
self.status = 'idle'
|
|
440
451
|
|
|
441
452
|
def refresh_cache(self) -> None:
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
453
|
+
new_count = len(self.agent.messages)
|
|
454
|
+
if new_count == self._cache_count:
|
|
455
|
+
return
|
|
456
|
+
if self._rendered_cache is None or new_count < self._cache_count:
|
|
457
|
+
self._rendered_cache = render_history(self.agent.messages)
|
|
458
|
+
else:
|
|
459
|
+
new_msgs = self.agent.messages[self._cache_count:]
|
|
460
|
+
append_to_history_table(self._rendered_cache, new_msgs)
|
|
461
|
+
self._cache_count = new_count
|
|
462
|
+
self.query_one('#cache', Static).update(self._rendered_cache)
|
|
463
|
+
scroll = self.query_one('#scroll', VerticalScroll)
|
|
464
|
+
if scroll.max_scroll_y - scroll.scroll_y < 3:
|
|
465
|
+
self.app.call_after_refresh(scroll.scroll_end, animate=False)
|
|
448
466
|
|
|
449
467
|
def refresh_stream(self) -> None:
|
|
450
468
|
msgs = []
|
|
@@ -468,6 +486,7 @@ class Tab(Vertical):
|
|
|
468
486
|
self.query_one('#cache', Static).update('')
|
|
469
487
|
self.query_one('#stream', Static).update('')
|
|
470
488
|
self._cache_count = 0
|
|
489
|
+
self._rendered_cache = None
|
|
471
490
|
self._command_blocks = []
|
|
472
491
|
self._stream_blocks = []
|
|
473
492
|
|
|
@@ -490,8 +509,9 @@ class TabBar(Static):
|
|
|
490
509
|
for i, tab in enumerate(tabs):
|
|
491
510
|
marker = '●' if tab.is_running else ' '
|
|
492
511
|
label = f' {marker}{i + 1}:{tab.title} '
|
|
493
|
-
|
|
494
|
-
x
|
|
512
|
+
width = cell_len(label)
|
|
513
|
+
self._ranges.append((x, x + width, i))
|
|
514
|
+
x += width
|
|
495
515
|
if i == active_index:
|
|
496
516
|
t.append(label, style='bold green reverse')
|
|
497
517
|
elif tab.is_running:
|
|
@@ -621,6 +641,11 @@ class ReplApp(App[None]):
|
|
|
621
641
|
tab.errors.clear()
|
|
622
642
|
tab.last_error = ''
|
|
623
643
|
self.query_one('#helpbar', HelpBar).show_idle()
|
|
644
|
+
if text.startswith('!!'):
|
|
645
|
+
command = text[2:].strip()
|
|
646
|
+
if command:
|
|
647
|
+
await self._run_interactive_command(tab, command)
|
|
648
|
+
return
|
|
624
649
|
if text.startswith('!'):
|
|
625
650
|
command = text[1:].strip()
|
|
626
651
|
if not command:
|
|
@@ -662,15 +687,28 @@ class ReplApp(App[None]):
|
|
|
662
687
|
cwd = gwt.worktree if gwt and getattr(gwt, 'worktree', None) else tab.agent.ctx.get('cwd') or os.getcwd()
|
|
663
688
|
env = tab.agent.ctx.get('env')
|
|
664
689
|
try:
|
|
665
|
-
proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.
|
|
666
|
-
stdout,
|
|
667
|
-
|
|
690
|
+
proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env if env else None)
|
|
691
|
+
stdout, stderr = await proc.communicate()
|
|
692
|
+
out = stdout.decode(errors='replace').rstrip('\n')
|
|
693
|
+
err = stderr.decode(errors='replace').rstrip('\n')
|
|
694
|
+
body = out
|
|
695
|
+
if err:
|
|
696
|
+
body = (body + '\n' if body else '') + f'[stderr]\n{err}'
|
|
668
697
|
if proc.returncode and proc.returncode != 0:
|
|
669
|
-
|
|
670
|
-
tab.show_command(f'!{command}',
|
|
698
|
+
body += f'\n[exit {proc.returncode}]'
|
|
699
|
+
tab.show_command(f'!{command}', body or '(no output)')
|
|
671
700
|
except Exception as e:
|
|
672
701
|
tab.show_command(f'!{command}', f'Error: {e}')
|
|
673
702
|
|
|
703
|
+
async def _run_interactive_command(self, tab: Tab, command: str) -> None:
|
|
704
|
+
gwt = tab.agent.ctx.get('gwt')
|
|
705
|
+
cwd = gwt.worktree if gwt and getattr(gwt, 'worktree', None) else tab.agent.ctx.get('cwd') or os.getcwd()
|
|
706
|
+
env = tab.agent.ctx.get('env')
|
|
707
|
+
with self.suspend():
|
|
708
|
+
proc = await asyncio.create_subprocess_shell(command, cwd=cwd, stdin=None, stdout=None, stderr=None, env=env if env else None)
|
|
709
|
+
returncode = await proc.wait()
|
|
710
|
+
tab.show_command(f'!!{command}', f'[exited {returncode}]')
|
|
711
|
+
|
|
674
712
|
async def _handle_command(self, tab: Tab, text: str) -> None:
|
|
675
713
|
cmd, _, arg = text.partition(' ')
|
|
676
714
|
cmd = cmd.lower().strip()
|
|
@@ -856,7 +894,11 @@ class ReplApp(App[None]):
|
|
|
856
894
|
await self._run_submit(prompt)
|
|
857
895
|
elif cmd == '/export':
|
|
858
896
|
ts = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
859
|
-
|
|
897
|
+
user_cwd = tab.agent.ctx.get('user_cwd', os.getcwd())
|
|
898
|
+
if arg:
|
|
899
|
+
path = arg if os.path.isabs(arg) else os.path.join(user_cwd, arg)
|
|
900
|
+
else:
|
|
901
|
+
path = os.path.join(user_cwd, f'session_{ts}.json')
|
|
860
902
|
data = {'version': 1, 'exported_at': ts, 'system': tab.agent.system, 'messages': tab.agent.messages}
|
|
861
903
|
try:
|
|
862
904
|
with open(path, 'w', encoding='utf-8') as f:
|
|
@@ -981,14 +1023,14 @@ def collect_skills(skills_dir, skills=None):
|
|
|
981
1023
|
skills.append({'name': name, 'description': description, 'content': text})
|
|
982
1024
|
skill_prompt = 'Available skills (use Skill to load full instructions when needed):\n' + '\n'.join((f'- {s['name']}: {s['description']}' for s in skills)) if skills else ''
|
|
983
1025
|
return (skills, skill_prompt)
|
|
1026
|
+
_AGENT_ENV_ALLOWLIST: re.Pattern = re.compile('\n ^(\n # ── Execution paths ────────────────────────────────────────────────────\n PATH\n | MANPATH | INFOPATH\n\n # ── Python / conda / virtualenv ────────────────────────────────────────\n | CONDA_PREFIX | CONDA_DEFAULT_ENV | CONDA_EXE | CONDA_PYTHON_EXE\n | CONDA_SHLVL | CONDA_PROMPT_MODIFIER\n | MAMBA_ROOT_PREFIX | MAMBA_EXE\n | VIRTUAL_ENV | VIRTUAL_ENV_PROMPT\n | PYTHONPATH | PYTHONHOME | PYTHONSTARTUP\n | PYTHONDONTWRITEBYTECODE | PYTHONUNBUFFERED\n | PYTHONFAULTHANDLER | PYTHONUTF8\n | PIP_INDEX_URL | PIP_EXTRA_INDEX_URL # private PyPI mirrors (no auth tokens)\n | PIPENV_PIPFILE | POETRY_VIRTUALENVS_IN_PROJECT\n\n # ── Native / compiled libs ─────────────────────────────────────────────\n | LD_LIBRARY_PATH | LD_PRELOAD\n | DYLD_LIBRARY_PATH | DYLD_FALLBACK_LIBRARY_PATH # macOS\n | PKG_CONFIG_PATH\n | CMAKE_PREFIX_PATH | CMAKE_BUILD_TYPE\n\n # ── CUDA / GPU ─────────────────────────────────────────────────────────\n | CUDA_HOME | CUDA_PATH | CUDA_VISIBLE_DEVICES\n | NVIDIA_VISIBLE_DEVICES | NVIDIA_DRIVER_CAPABILITIES\n | HIP_PATH | ROCR_VISIBLE_DEVICES # AMD\n | METAL_DEVICE_WRAPPER_TYPE # Apple\n\n # ── Locale / encoding ──────────────────────────────────────────────────\n | LANG | LANGUAGE | LC_ALL | LC_CTYPE | LC_MESSAGES\n | LC_NUMERIC | LC_TIME | LC_COLLATE\n | PYTHONUTF8\n\n # ── Terminal (needed by tools that check if output is a tty) ───────────\n | TERM | TERM_PROGRAM | COLORTERM\n | NO_COLOR | CLICOLOR | CLICOLOR_FORCE\n | COLUMNS | LINES\n\n # ── Process identity (non-secret) ──────────────────────────────────────\n | HOME # overridden to temp dir by run_repl — safe\n | SHELL\n | TMPDIR | TEMP | TMP\n | XDG_RUNTIME_DIR | XDG_CACHE_HOME | XDG_CONFIG_HOME | XDG_DATA_HOME\n\n # ── Toolchain / build (no secrets) ─────────────────────────────────────\n | CC | CXX | AR | LD | FC\n | CFLAGS | CXXFLAGS | LDFLAGS | MAKEFLAGS\n | JAVA_HOME | GRADLE_HOME | MAVEN_HOME\n | GOPATH | GOROOT | GOMODCACHE\n | CARGO_HOME | RUSTUP_HOME\n | NODE_PATH # not NODE_AUTH_TOKEN\n | GEM_HOME | GEM_PATH | BUNDLE_PATH\n )$\n', re.VERBOSE)
|
|
1027
|
+
|
|
1028
|
+
def _make_agent_env(base: dict[str, str]) -> dict[str, str]:
|
|
1029
|
+
return {k: v for k, v in base.items() if _AGENT_ENV_ALLOWLIST.match(k)}
|
|
984
1030
|
|
|
985
1031
|
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, verbose_transcript=False):
|
|
986
1032
|
repo = os.path.abspath(repo or os.getcwd())
|
|
987
1033
|
with tempfile.TemporaryDirectory(dir=tempfile.gettempdir()) as _home:
|
|
988
|
-
if env is None:
|
|
989
|
-
env = os.environ.copy()
|
|
990
|
-
env['HOME'] = _home
|
|
991
|
-
env.setdefault('SHELL', '/bin/bash')
|
|
992
1034
|
if gwt is None:
|
|
993
1035
|
if resume:
|
|
994
1036
|
result = resume_worktree(repo, resume, worktree_dir=os.path.join(_home, 'workspace'))
|
|
@@ -1000,15 +1042,17 @@ def run_repl(*, base_url=None, model=None, api: Literal['claude', 'codex', 'gemi
|
|
|
1000
1042
|
else:
|
|
1001
1043
|
gwt = create_worktree(repo, worktree_dir=os.path.join(_home, 'workspace'))
|
|
1002
1044
|
cwd = gwt.worktree if gwt else repo
|
|
1003
|
-
env
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1045
|
+
if env is None:
|
|
1046
|
+
env = os.environ.copy()
|
|
1047
|
+
env.setdefault('SHELL', '/bin/bash')
|
|
1048
|
+
agent_env = _make_agent_env(env)
|
|
1049
|
+
agent_env['HOME'] = _home
|
|
1050
|
+
agent_env['PWD'] = cwd
|
|
1051
|
+
user_cwd = os.path.abspath(os.getcwd())
|
|
1008
1052
|
sdir = os.path.abspath(sdir or cwd)
|
|
1009
1053
|
skills, skill_prompt = collect_skills(sdir, skills)
|
|
1010
1054
|
system = '\n\n'.join(filter(None, [system, skill_prompt]))
|
|
1011
|
-
merged_ctx = {'cwd': cwd, 'skills': skills, 'gwt': gwt, 'env':
|
|
1055
|
+
merged_ctx = {'cwd': cwd, 'user_cwd': user_cwd, 'skills': skills, 'gwt': gwt, 'env': agent_env, **(ctx or {})}
|
|
1012
1056
|
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)
|
|
1013
1057
|
log_fp = None
|
|
1014
1058
|
if stream is not None:
|
|
@@ -1027,12 +1071,6 @@ def run_repl(*, base_url=None, model=None, api: Literal['claude', 'codex', 'gemi
|
|
|
1027
1071
|
finally:
|
|
1028
1072
|
if log_fp:
|
|
1029
1073
|
log_fp.close()
|
|
1030
|
-
try:
|
|
1031
|
-
os.chdir(_orig_cwd)
|
|
1032
|
-
except OSError as exc:
|
|
1033
|
-
logger.warning('Could not restore original cwd %r: %s', _orig_cwd, exc)
|
|
1034
|
-
os.environ.clear()
|
|
1035
|
-
os.environ.update(_orig_environ)
|
|
1036
1074
|
if app_instance and hasattr(app_instance, '_exit_summary') and app_instance._exit_summary:
|
|
1037
1075
|
print('\n--- Session Exit Summary ---')
|
|
1038
1076
|
for item in app_instance._exit_summary:
|
|
@@ -213,7 +213,7 @@ class BashTool(Tool):
|
|
|
213
213
|
parameters = BashParams
|
|
214
214
|
|
|
215
215
|
async def execute(self, params: BashParams, signal=None) -> dict:
|
|
216
|
-
proc_env =
|
|
216
|
+
proc_env = self.ctx.get('env')
|
|
217
217
|
proc = await asyncio.create_subprocess_shell(params.command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, cwd=self.ctx['cwd'], env=proc_env)
|
|
218
218
|
communicate_task = asyncio.create_task(proc.communicate())
|
|
219
219
|
abort_task: asyncio.Task | None = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mlx-code
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
4
4
|
Summary: Coding Agent for Mac
|
|
5
5
|
Home-page: https://josefalbers.github.io/mlx-code/
|
|
6
6
|
Author: J Joe
|
|
@@ -38,7 +38,7 @@ Dynamic: summary
|
|
|
38
38
|
|
|
39
39
|
A lightweight coding agent built on Apple's MLX framework.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
https://github.com/user-attachments/assets/0569d101-8d0a-4e67-9e82-fce84a5ef3f0
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|