mlx-code 0.0.33__tar.gz → 0.0.35__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {mlx_code-0.0.33 → mlx_code-0.0.35}/PKG-INFO +13 -4
  2. {mlx_code-0.0.33 → mlx_code-0.0.35}/README.md +10 -1
  3. mlx_code-0.0.35/mlx_code/apis.py +373 -0
  4. mlx_code-0.0.35/mlx_code/bare.py +177 -0
  5. mlx_code-0.0.35/mlx_code/bats.py +262 -0
  6. mlx_code-0.0.35/mlx_code/bats_plan.py +43 -0
  7. mlx_code-0.0.35/mlx_code/bench_bats.py +146 -0
  8. mlx_code-0.0.35/mlx_code/gits.py +293 -0
  9. mlx_code-0.0.35/mlx_code/lsp_tool.py +326 -0
  10. mlx_code-0.0.35/mlx_code/main.py +513 -0
  11. mlx_code-0.0.35/mlx_code/mcb.py +91 -0
  12. mlx_code-0.0.35/mlx_code/mcb_tool.py +74 -0
  13. mlx_code-0.0.35/mlx_code/mlxs.py +101 -0
  14. mlx_code-0.0.35/mlx_code/repl.py +587 -0
  15. mlx_code-0.0.35/mlx_code/stream_log.py +28 -0
  16. mlx_code-0.0.35/mlx_code/test_bats_plan.py +69 -0
  17. mlx_code-0.0.35/mlx_code/tools.py +205 -0
  18. mlx_code-0.0.35/mlx_code/tui.py +278 -0
  19. mlx_code-0.0.35/mlx_code/util.py +16 -0
  20. mlx_code-0.0.35/mlx_code/view_git.py +647 -0
  21. mlx_code-0.0.35/mlx_code/view_log.py +311 -0
  22. mlx_code-0.0.35/mlx_code/vlls.py +88 -0
  23. mlx_code-0.0.35/mlx_code/web.py +114 -0
  24. {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/PKG-INFO +13 -4
  25. {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/SOURCES.txt +5 -0
  26. {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/requires.txt +3 -1
  27. {mlx_code-0.0.33 → mlx_code-0.0.35}/setup.py +10 -4
  28. mlx_code-0.0.33/mlx_code/apis.py +0 -574
  29. mlx_code-0.0.33/mlx_code/bare.py +0 -276
  30. mlx_code-0.0.33/mlx_code/bats.py +0 -318
  31. mlx_code-0.0.33/mlx_code/gits.py +0 -379
  32. mlx_code-0.0.33/mlx_code/lsp_tool.py +0 -599
  33. mlx_code-0.0.33/mlx_code/main.py +0 -987
  34. mlx_code-0.0.33/mlx_code/mcb.py +0 -183
  35. mlx_code-0.0.33/mlx_code/mcb_tool.py +0 -61
  36. mlx_code-0.0.33/mlx_code/repl.py +0 -874
  37. mlx_code-0.0.33/mlx_code/stream_log.py +0 -62
  38. mlx_code-0.0.33/mlx_code/tools.py +0 -425
  39. mlx_code-0.0.33/mlx_code/tui.py +0 -575
  40. mlx_code-0.0.33/mlx_code/util.py +0 -30
  41. mlx_code-0.0.33/mlx_code/view_git.py +0 -995
  42. mlx_code-0.0.33/mlx_code/view_log.py +0 -625
  43. mlx_code-0.0.33/mlx_code/web.py +0 -448
  44. {mlx_code-0.0.33 → mlx_code-0.0.35}/LICENSE +0 -0
  45. {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code/__init__.py +0 -0
  46. {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/dependency_links.txt +0 -0
  47. {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/entry_points.txt +0 -0
  48. {mlx_code-0.0.33 → mlx_code-0.0.35}/mlx_code.egg-info/top_level.txt +0 -0
  49. {mlx_code-0.0.33 → mlx_code-0.0.35}/setup.cfg +0 -0
  50. {mlx_code-0.0.33 → mlx_code-0.0.35}/tests/__init__.py +0 -0
  51. {mlx_code-0.0.33 → mlx_code-0.0.35}/tests/test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mlx-code
3
- Version: 0.0.33
3
+ Version: 0.0.35
4
4
  Summary: Coding Agent for Mac
5
5
  Home-page: https://josefalbers.github.io/mlx-code/
6
6
  Author: J Joe
@@ -8,11 +8,12 @@ Author-email: albersj66@gmail.com
8
8
  License: Apache-2.0
9
9
  Project-URL: Source, https://github.com/JosefAlbers/mlx-code
10
10
  Project-URL: Issues, https://github.com/JosefAlbers/mlx-code/issues
11
- Project-URL: Documentation, https://josefalbers.github.io/mlx-code/
11
+ Project-URL: Documentation, https://www.mlx-code.com/
12
12
  Requires-Python: >=3.12.8
13
13
  Description-Content-Type: text/markdown
14
14
  License-File: LICENSE
15
15
  Requires-Dist: mlx-lm>=0.31.3; platform_system == "Darwin"
16
+ Requires-Dist: vllm; platform_system != "Darwin"
16
17
  Requires-Dist: httpx
17
18
  Requires-Dist: pydantic
18
19
  Provides-Extra: all
@@ -21,7 +22,6 @@ Requires-Dist: uvicorn; extra == "all"
21
22
  Requires-Dist: pygments; extra == "all"
22
23
  Requires-Dist: textual>=8.2.7; extra == "all"
23
24
  Requires-Dist: rich>=15.0.0; extra == "all"
24
- Requires-Dist: python-lsp-server[all]; extra == "all"
25
25
  Dynamic: author
26
26
  Dynamic: author-email
27
27
  Dynamic: description
@@ -107,7 +107,7 @@ result = await agent.run('refactor utils.py to use dataclasses')
107
107
 
108
108
  ```bash
109
109
  # ephemeral run (no installation)
110
- uvx --from mlx-code[all] mlc
110
+ uvx --from "mlx-code[all]" mlc
111
111
 
112
112
  # or install into the current environment
113
113
  pip install mlx-code[all]
@@ -563,6 +563,15 @@ podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_
563
563
  phone http://jjoe.mlx-code.com
564
564
  ```
565
565
 
566
+ #### VLLM
567
+
568
+ ```
569
+ curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm-metal/main/install.sh | bash
570
+ source ~/.venv-vllm-metal/bin/activate # or `source ~/.venv-vllm-metal/bin/activate.fish` if fish
571
+ pip install mlx-code[all]
572
+ mlc --backend vllm -m Qwen/Qwen3.5-0.8B
573
+ ```
574
+
566
575
  ---
567
576
 
568
577
  ## Credits
@@ -70,7 +70,7 @@ result = await agent.run('refactor utils.py to use dataclasses')
70
70
 
71
71
  ```bash
72
72
  # ephemeral run (no installation)
73
- uvx --from mlx-code[all] mlc
73
+ uvx --from "mlx-code[all]" mlc
74
74
 
75
75
  # or install into the current environment
76
76
  pip install mlx-code[all]
@@ -526,6 +526,15 @@ podman run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token $JJ_
526
526
  phone http://jjoe.mlx-code.com
527
527
  ```
528
528
 
529
+ #### VLLM
530
+
531
+ ```
532
+ curl -fsSL https://raw.githubusercontent.com/vllm-project/vllm-metal/main/install.sh | bash
533
+ source ~/.venv-vllm-metal/bin/activate # or `source ~/.venv-vllm-metal/bin/activate.fish` if fish
534
+ pip install mlx-code[all]
535
+ mlc --backend vllm -m Qwen/Qwen3.5-0.8B
536
+ ```
537
+
529
538
  ---
530
539
 
531
540
  ## Credits
@@ -0,0 +1,373 @@
1
+ from __future__ import annotations
2
+ _AN='function_call'
3
+ _AM='Authorization'
4
+ _AL='reasoning_content'
5
+ _AK='output_tokens'
6
+ _AJ='input_tokens'
7
+ _AI='redacted_data'
8
+ _AH='redacted_thinking'
9
+ _AG='response'
10
+ _AF='functionCall'
11
+ _AE='Content-Type'
12
+ _AD='object'
13
+ _AC='parameters'
14
+ _AB='description'
15
+ _AA='tool_calls'
16
+ _A9='tool_choice'
17
+ _A8='stream'
18
+ _A7='thought'
19
+ _A6='reason'
20
+ _A5='done'
21
+ _A4='tool_call'
22
+ _A3='toolcall_end'
23
+ _A2='thinking_delta'
24
+ _A1='index'
25
+ _A0='data: '
26
+ _z='start'
27
+ _y='POST'
28
+ _x='timestamp'
29
+ _w='application/json'
30
+ _v='tools'
31
+ _u='temperature'
32
+ _t='max_tokens'
33
+ _s='model'
34
+ _r='No api key'
35
+ _q='call_id'
36
+ _p='image_url'
37
+ _o='length'
38
+ _n='text_delta'
39
+ _m='cache_write'
40
+ _l='cache_read'
41
+ _k=True
42
+ _j='toolResult'
43
+ _i='redacted'
44
+ _h='mime_type'
45
+ _g='args'
46
+ _f='parts'
47
+ _e='properties'
48
+ _d='tool_call_id'
49
+ _c='tool_use'
50
+ _b='args_buf'
51
+ _a='message'
52
+ _Z='stop'
53
+ _Y='error_message'
54
+ _X='required'
55
+ _W='function'
56
+ _V='toolCall'
57
+ _U='signature'
58
+ _T='data'
59
+ _S='assistant'
60
+ _R='error'
61
+ _Q='delta'
62
+ _P='user'
63
+ _O='output'
64
+ _N='input'
65
+ _M='arguments'
66
+ _L='partial'
67
+ _K='stop_reason'
68
+ _J='id'
69
+ _I='thinking'
70
+ _H='payload'
71
+ _G='usage'
72
+ _F='role'
73
+ _E='name'
74
+ _D='text'
75
+ _C='content'
76
+ _B=None
77
+ _A='type'
78
+ import asyncio,json,os,time,uuid,logging,httpx
79
+ from typing import Any,Literal
80
+ from.tools import Tool
81
+ logger=logging.getLogger(__name__)
82
+ class EventStream:
83
+ def __init__(A)->_B:(A._queue):asyncio.Queue[dict|_B]=asyncio.Queue();(A._result):dict|_B=_B;(A._task):asyncio.Task|_B=_B
84
+ def _attach(A,task:asyncio.Task)->_B:A._task=task
85
+ def push(A,event:dict)->_B:A._queue.put_nowait(event)
86
+ def finish(A,result:dict)->_B:A._result=result;A._queue.put_nowait(_B)
87
+ async def result(A)->dict:
88
+ if A._result is _B:
89
+ async for B in A:0
90
+ assert A._result is not _B;return A._result
91
+ def __aiter__(A)->'EventStream':return A
92
+ async def __anext__(B)->dict:
93
+ A=await B._queue.get()
94
+ if A is _B:raise StopAsyncIteration
95
+ return A
96
+ _REASONING_BUDGET:dict[str,int]={'minimal':512,'low':1024,'medium':8192,'high':16000,'xhigh':32000}
97
+ class ClaudeChat:
98
+ def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,reasoning:Literal['off','minimal','low','medium','high','xhigh']='off',tool_choice=_B):
99
+ A.model=model or'claude-haiku-4-5';A.api_key=api_key or os.environ.get('ANTHROPIC_API_KEY');A.base_url=(base_url or'https://api.anthropic.com').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.reasoning=reasoning;A.tool_choice=tool_choice
100
+ if not A.api_key:logger.debug(_r)
101
+ def _fmt_content(H,content)->str|list:
102
+ G='image';F=content;C='cache_control'
103
+ if isinstance(F,str):return F
104
+ B=[]
105
+ for A in F:
106
+ E=A[_A]
107
+ if E==_D:
108
+ D={_A:_D,_D:A[_D]}
109
+ if A.get(C):D[C]={_A:A[C]}
110
+ B.append(D)
111
+ elif E==G:
112
+ D={_A:G,'source':{_A:'base64','media_type':A[_h],_T:A[_T]}}
113
+ if A.get(C):D[C]={_A:A[C]}
114
+ B.append(D)
115
+ elif E==_I:
116
+ if A[_i]:B.append({_A:_AH,_T:A.get(_I,A.get(_AI,''))})
117
+ else:B.append({_A:_I,_I:A[_I],_U:A.get(_U)or''})
118
+ elif E==_V:B.append({_A:_c,_J:A[_J],_E:A[_E],_N:A[_M]})
119
+ return B
120
+ def _build_messages(E,messages:list[dict])->list[dict]:
121
+ H='is_error';B=messages;D=[];A=0
122
+ while A<len(B):
123
+ C=B[A]
124
+ if C[_F]==_P:D.append({_F:_P,_C:E._fmt_content(C[_C])});A+=1
125
+ elif C[_F]==_S:D.append({_F:_S,_C:E._fmt_content(C[_C])});A+=1
126
+ elif C[_F]==_j:
127
+ G=[]
128
+ while A<len(B)and B[A][_F]==_j:F=B[A];G.append({_A:'tool_result','tool_use_id':F[_d],_C:E._fmt_content(F[_C]),H:F[H]});A+=1
129
+ D.append({_F:_P,_C:G})
130
+ else:A+=1
131
+ return D
132
+ async def stream(C,messages:list[dict],system:str,tools:list[Tool])->EventStream:
133
+ E=tools;B=system;D=EventStream();G:dict[str,Any]={_s:C.model,_t:C.max_tokens,'messages':C._build_messages(messages),_A8:_k}
134
+ if B:G['system']=[{_A:_D,_D:B}]
135
+ if C.temperature:G[_u]=C.temperature
136
+ if C.reasoning!='off':G[_I]={_A:'enabled','budget_tokens':_REASONING_BUDGET[C.reasoning]}
137
+ if E:
138
+ G[_v]=[A.schema()for A in E];A=C.tool_choice
139
+ if A is not _B:G[_A9]={_A:'tool',_E:A[_E]}if isinstance(A,dict)else{_A:'any'}if A==_X else{_A:'none'}if A=='none'else{_A:'auto'}
140
+ W={'x-api-key':str(C.api_key),'anthropic-version':'2023-06-01','content-type':_w};logger.debug(json.dumps(G,indent=2))
141
+ async def F()->_B:
142
+ A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
143
+ try:
144
+ async with httpx.AsyncClient(timeout=12e1)as X:
145
+ async with X.stream(_y,f"{C.base_url}/v1/messages",json=G,headers=W)as N:
146
+ if N.status_code>=400:raise RuntimeError(f"HTTP {N.status_code}: {(await N.aread()).decode()}")
147
+ D.push({_A:_z,_H:{_L:A}});O:dict[int,str]={};H:dict[int,int]={}
148
+ async for T in N.aiter_lines():
149
+ if not T.startswith(_A0):continue
150
+ U=T[6:]
151
+ if U=='[DONE]':break
152
+ E=json.loads(U);I=E.get(_A)
153
+ if I=='message_start':J=E.get(_a,{}).get(_G,{});A[_G][_N]=J.get(_AJ,0);A[_G][_l]=J.get('cache_read_input_tokens',0);A[_G][_m]=J.get('cache_creation_input_tokens',0)
154
+ elif I=='content_block_start':
155
+ B=E.get(_A1,0);P=E.get('content_block',{});Q=P.get(_A)
156
+ if Q==_D:H[B]=len(A[_C]);A[_C].append({_A:_D,_D:''})
157
+ elif Q==_I:H[B]=len(A[_C]);A[_C].append({_A:_I,_I:'',_U:_B,_i:False})
158
+ elif Q==_AH:H[B]=len(A[_C]);A[_C].append({_A:_I,_AI:P.get(_T,''),_U:_B,_i:_k})
159
+ elif Q==_c:H[B]=len(A[_C]);A[_C].append({_A:_V,_J:P[_J],_E:P[_E],_M:{}});O[B]=''
160
+ elif I=='content_block_delta':
161
+ B=E.get(_A1,0);L=E.get(_Q,{});R=L.get(_A);K=H.get(B)
162
+ if K is _B:continue
163
+ F=A[_C][K]
164
+ if R==_n:M=L.get(_D,'');F[_D]+=M;D.push({_A:_n,_H:{_Q:M,_L:A}})
165
+ elif R==_A2:M=L.get(_I,'');F[_I]+=M;D.push({_A:_A2,_H:{_Q:M,_L:A}})
166
+ elif R=='signature_delta':F[_U]=(F.get(_U)or'')+L.get(_U,'')
167
+ elif R=='input_json_delta':O[B]=O.get(B,'')+L.get('partial_json','')
168
+ elif I=='content_block_stop':
169
+ B=E.get(_A1,0);K=H.get(B)
170
+ if K is not _B and K<len(A[_C]):
171
+ F=A[_C][K]
172
+ if F[_A]==_V:
173
+ V=O.pop(B,'')
174
+ if V:
175
+ try:F[_M]=json.loads(V)
176
+ except json.JSONDecodeError:pass
177
+ D.push({_A:_A3,_H:{_A4:F,_L:A}})
178
+ elif I=='message_delta':
179
+ S=E.get(_Q,{}).get(_K)
180
+ if S==_c:A[_K]=_c
181
+ elif S==_t:A[_K]=_o
182
+ elif S:A[_K]=_Z
183
+ J=E.get(_G,{})
184
+ if J:A[_G][_O]=J.get(_AK,A[_G][_O])
185
+ elif I=='message_stop':D.push({_A:_A5,_H:{_A6:A[_K],_a:A}})
186
+ except Exception as Y:A[_K]=_R;A[_Y]=str(Y);D.push({_A:_R,_H:{_R:A}})
187
+ D.finish(A)
188
+ D._attach(asyncio.create_task(F()));return D
189
+ class DefaultChat:
190
+ def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,tool_choice=_B):
191
+ C='noapi';B=api_key;A.model=model or C;A.api_key=B or C;A.base_url=(base_url or'http://127.0.0.1:8000').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.tool_choice=tool_choice
192
+ if not B:logger.debug(_r)
193
+ def _build_messages(K,messages:list[dict],system:str)->list[dict]:
194
+ J='url';F=system;B=[]
195
+ if F:B.append({_F:'system',_C:F})
196
+ for A in messages:
197
+ if A[_F]==_P:
198
+ E=A[_C]
199
+ if isinstance(E,str):B.append({_F:_P,_C:E})
200
+ else:B.append({_F:_P,_C:[{_A:_D,_D:A[_D]}if A[_A]==_D else{_A:_p,_p:{J:f"data:{A[_h]};base64,{A[_T]}"}}for A in E]})
201
+ elif A[_F]==_S:
202
+ G=[A for A in A[_C]if A[_A]==_D];H=[A for A in A[_C]if A[_A]==_V];I=[A for A in A[_C]if A[_A]==_I];C:dict[str,Any]={_F:_S}
203
+ if G:C[_C]=''.join(A[_D]for A in G)
204
+ if I:C[_AL]=''.join(A[_I]for A in I)
205
+ if H:C[_AA]=[{_J:A[_J],_A:_W,_W:{_E:A[_E],_M:json.dumps(A[_M])}}for A in H]
206
+ B.append(C)
207
+ elif A[_F]==_j:D=A[_C];B.append({_F:'tool',_d:A[_d],_C:D[0][_D]if len(D)==1 and D[0][_A]==_D else[{_A:_D,_D:A[_D]}if A[_A]==_D else{_A:_p,_p:{J:f"data:{A[_h]};base64,{A[_T]}"}}for A in D]})
208
+ return B
209
+ async def stream(C,messages:list[dict],system:str,tools:list[Tool])->EventStream:
210
+ D=tools;B=EventStream();E:dict[str,Any]={_s:C.model,_t:C.max_tokens,'messages':C._build_messages(messages,system),_A8:_k}
211
+ if C.temperature:E[_u]=C.temperature
212
+ if D:
213
+ E[_v]=[{_A:_W,_W:{_E:A.name,_AB:A.description,_AC:{_A:_AD,_e:A.parameters.model_json_schema().get(_e,{}),_X:A.parameters.model_json_schema().get(_X,[])}}}for A in D];A=C.tool_choice
214
+ if A is not _B:E[_A9]={_A:_W,_W:{_E:A[_E]}}if isinstance(A,dict)else A
215
+ X={_AM:f"Bearer {C.api_key}",_AE:_w};logger.debug(json.dumps(E,indent=2))
216
+ async def F()->_B:
217
+ A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
218
+ try:
219
+ async with httpx.AsyncClient(timeout=_B)as Y:
220
+ async with Y.stream(_y,f"{C.base_url}/v1/chat/completions",json=E,headers=X)as G:
221
+ if G.status_code>=400:raise RuntimeError(f"HTTP {G.status_code}: {(await G.aread()).decode()}")
222
+ B.push({_A:_z,_H:{_L:A}});F:dict[int,dict]={};L=M='';H=_B
223
+ async for P in G.aiter_lines():
224
+ if not P.startswith(_A0):continue
225
+ N=P[6:].strip()
226
+ if N=='[DONE]'or not N:continue
227
+ Q=json.loads(N);R=(Q.get('choices')or[{}])[0];I=R.get(_Q,{});H=R.get('finish_reason')or H
228
+ if(S:=I.get(_AL)or I.get(_I)):M+=S;B.push({_A:_A2,_H:{_Q:S,_L:A}})
229
+ if(T:=I.get(_C)):L+=T;B.push({_A:_n,_H:{_Q:T,_L:A}})
230
+ for O in I.get(_AA)or[]:
231
+ J=O.get(_A1,0);D=O.get(_W,{})
232
+ if J not in F:F[J]={_J:O.get(_J,str(uuid.uuid4())),_E:D.get(_E,''),_b:D.get(_M,'')}
233
+ else:
234
+ if _E in D:F[J][_E]+=D[_E]
235
+ if _M in D:F[J][_b]+=D[_M]
236
+ if(U:=Q.get(_G)):A[_G][_N]=U.get('prompt_tokens',A[_G][_N]);A[_G][_O]=U.get('completion_tokens',A[_G][_O])
237
+ if M:A[_C].append({_A:_I,_I:M,_U:_B,_i:False})
238
+ if L:A[_C].append({_A:_D,_D:L})
239
+ for(a,K)in sorted(F.items()):
240
+ try:V=json.loads(K[_b])if K[_b]else{}
241
+ except json.JSONDecodeError:V={}
242
+ W={_A:_V,_J:K[_J],_E:K[_E],_M:V};A[_C].append(W);B.push({_A:_A3,_H:{_A4:W,_L:A}})
243
+ A[_K]=_c if H==_AA else _o if H in(_o,_t)else _Z;B.push({_A:_A5,_H:{_A6:A[_K],_a:A}})
244
+ except Exception as Z:A[_K]=_R;A[_Y]=str(Z);B.push({_A:_R,_H:{_R:A}})
245
+ B.finish(A)
246
+ B._attach(asyncio.create_task(F()));return B
247
+ class GeminiChat:
248
+ def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,thinking=False,thinking_budget=8192,tool_choice=_B):
249
+ A.model=model or'gemini-3.1-flash-lite-preview';A.api_key=api_key or os.environ.get('GEMINI_API_KEY');A.base_url=(base_url or'https://generativelanguage.googleapis.com').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.thinking=thinking;A.thinking_budget=thinking_budget;A.tool_choice=tool_choice
250
+ if not A.api_key:logger.debug(_r)
251
+ def _build_contents(M,messages:list[dict])->list[dict]:
252
+ L='tool_name';C=[];E:list[dict]=[]
253
+ def G():
254
+ if E:C.append({_F:_P,_f:list(E)});E.clear()
255
+ for A in messages:
256
+ if A[_F]==_P:
257
+ G();H=A[_C]
258
+ if isinstance(H,str):C.append({_F:_P,_f:[{_D:H}]})
259
+ else:C.append({_F:_P,_f:[{_D:A[_D]}if A[_A]==_D else{'inlineData':{'mimeType':A[_h],_T:A[_T]}}for A in H]})
260
+ elif A[_F]==_S:
261
+ G();D=[]
262
+ for B in A[_C]:
263
+ if B[_A]==_I:D.append({_A7:B[_I]})
264
+ elif B[_A]==_D:D.append({_D:B[_D]})
265
+ elif B[_A]==_V:
266
+ I:dict={_E:B[_E],_g:B[_M]}
267
+ if B[_J]!=B[_E]:I[_J]=B[_J]
268
+ D.append({_AF:I})
269
+ if D:C.append({_F:_s,_f:D})
270
+ elif A[_F]==_j:
271
+ F=A[_C]
272
+ try:J=json.loads(F[0][_D])if F else{}
273
+ except(json.JSONDecodeError,KeyError):J={'result':F[0][_D]if F else''}
274
+ K:dict={_E:A[L],_AG:J}
275
+ if A[_d]!=A[L]:K[_J]=A[_d]
276
+ E.append({'functionResponse':K})
277
+ G();return C
278
+ async def stream(A,messages:list[dict],system:str,tools:list[Tool])->EventStream:
279
+ I='ANY';H=tools;G=system;F='generationConfig';E='mode';B=EventStream();D:dict[str,Any]={'contents':A._build_contents(messages),F:{'maxOutputTokens':A.max_tokens}}
280
+ if G:D['systemInstruction']={_f:[{_D:G}]}
281
+ if A.temperature:D[F][_u]=A.temperature
282
+ if A.thinking:D[F]['thinkingConfig']={'includeThoughts':_k,'thinkingBudget':A.thinking_budget}
283
+ if H:
284
+ D[_v]=[{'functionDeclarations':[{_E:A.name,_AB:A.description,_AC:{_A:_AD,_e:A.parameters.model_json_schema().get(_e,{}),_X:A.parameters.model_json_schema().get(_X,[])}}for A in H]}];C=A.tool_choice
285
+ if C is not _B:D['toolConfig']={'functionCallingConfig':{E:I,'allowedFunctionNames':[C[_E]]}if isinstance(C,dict)else{E:I}if C==_X else{E:'NONE'}if C=='none'else{E:'AUTO'}}
286
+ S=f"{A.base_url}/v1beta/models/{A.model}:streamGenerateContent?alt=sse&key={A.api_key}";logger.debug(json.dumps(D,indent=2))
287
+ async def J()->_B:
288
+ A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
289
+ try:
290
+ async with httpx.AsyncClient(timeout=12e1)as T:
291
+ async with T.stream(_y,S,json=D,headers={_AE:_w})as H:
292
+ if H.status_code>=400:raise RuntimeError(f"HTTP {H.status_code}: {(await H.aread()).decode()}")
293
+ B.push({_A:_z,_H:{_L:A}});I=J='';E:dict[str,dict]={};K=_B
294
+ async for L in H.aiter_lines():
295
+ if not L.startswith(_A0):continue
296
+ M=L[6:].strip()
297
+ if not M:continue
298
+ N=json.loads(M);O=(N.get('candidates')or[{}])[0];K=O.get('finishReason')or K
299
+ for C in O.get(_C,{}).get(_f)or[]:
300
+ if _A7 in C:J+=C[_A7];B.push({_A:_A2,_H:{_Q:C[_A7],_L:A}})
301
+ elif _D in C:I+=C[_D];B.push({_A:_n,_H:{_Q:C[_D],_L:A}})
302
+ elif _AF in C:
303
+ F=C[_AF];G=F.get(_J)or F[_E]
304
+ if G not in E:E[G]={_E:F[_E],_g:F.get(_g,{})}
305
+ else:E[G][_g].update(F.get(_g,{}))
306
+ if(P:=N.get('usageMetadata')):A[_G][_N]=P.get('promptTokenCount',A[_G][_N]);A[_G][_O]=P.get('candidatesTokenCount',A[_G][_O])
307
+ if J:A[_C].append({_A:_I,_I:J,_U:_B,_i:False})
308
+ if I:A[_C].append({_A:_D,_D:I})
309
+ for(G,Q)in E.items():R={_A:_V,_J:G,_E:Q[_E],_M:Q[_g]};A[_C].append(R);B.push({_A:_A3,_H:{_A4:R,_L:A}})
310
+ A[_K]=_o if K=='MAX_TOKENS'else _c if E else _Z;B.push({_A:_A5,_H:{_A6:A[_K],_a:A}});B.finish(A);return
311
+ except Exception as U:A[_K]=_R;A[_Y]=str(U);B.push({_A:_R,_H:{_R:A}})
312
+ B.finish(A)
313
+ B._attach(asyncio.create_task(J()));return B
314
+ class CodexChat:
315
+ def __init__(A,*,model=_B,api_key=_B,base_url=_B,max_tokens=8192,temperature=_B,tool_choice=_B):
316
+ A.model=model or'gpt-5.4-mini';A.api_key=api_key or os.environ.get('OPENAI_API_KEY');A.base_url=(base_url or'https://api.openai.com').rstrip('/');A.max_tokens=max_tokens;A.temperature=temperature;A.tool_choice=tool_choice
317
+ if not A.api_key:logger.debug(_r)
318
+ def _build_input(F,messages:list[dict],system:str)->list[dict]:
319
+ E=system;B=[]
320
+ if E:B.append({_A:_a,_F:'developer',_C:E})
321
+ for A in messages:
322
+ if A[_F]==_P:D=A[_C];B.append({_A:_a,_F:_P,_C:D if isinstance(D,str)else[{_A:'input_text',_D:A[_D]}if A[_A]==_D else{_A:'input_image',_p:f"data:{A[_h]};base64,{A[_T]}"}for A in D]})
323
+ elif A[_F]==_S:
324
+ for C in A[_C]:
325
+ if C[_A]==_D:B.append({_A:_a,_F:_S,_C:[{_A:'output_text',_D:C[_D]}]})
326
+ elif C[_A]==_V:B.append({_A:_AN,_q:C[_J],_E:C[_E],_M:json.dumps(C[_M])})
327
+ elif A[_F]==_j:B.append({_A:'function_call_output',_q:A[_d],_O:A[_C][0][_D]if A[_C]else''})
328
+ return B
329
+ async def stream(B,messages:list[dict],system:str,tools:list[Tool])->EventStream:
330
+ D=tools;C=EventStream();E:dict[str,Any]={_s:B.model,'max_output_tokens':B.max_tokens,_N:B._build_input(messages,system),_A8:_k}
331
+ if B.temperature:E[_u]=B.temperature
332
+ if D:
333
+ E[_v]=[{_A:_W,_E:A.name,_AB:A.description,_AC:{_A:_AD,_e:A.parameters.model_json_schema().get(_e,{}),_X:A.parameters.model_json_schema().get(_X,[])}}for A in D];A=B.tool_choice
334
+ if A is not _B:E[_A9]={_A:_W,_E:A[_E]}if isinstance(A,dict)else A
335
+ T={_AM:f"Bearer {B.api_key}",_AE:_w};logger.debug(json.dumps(E,indent=2))
336
+ async def F()->_B:
337
+ A={_F:_S,_C:[],_K:_Z,_G:{_N:0,_O:0,_l:0,_m:0},_Y:_B,_x:int(time.time()*1000)}
338
+ try:
339
+ async with httpx.AsyncClient(timeout=12e1)as U:
340
+ async with U.stream(_y,f"{B.base_url}/v1/responses",json=E,headers=T)as H:
341
+ if H.status_code>=400:raise RuntimeError(f"HTTP {H.status_code}: {(await H.aread()).decode()}")
342
+ C.push({_A:_z,_H:{_L:A}});L='';F:dict[str,dict]={};M=_B
343
+ async for N in H.aiter_lines():
344
+ if not N.startswith(_A0):continue
345
+ O=N[6:].strip()
346
+ if not O:continue
347
+ D=json.loads(O);I=D.get(_A,'')
348
+ if I=='response.output_text.delta':P=D.get(_Q,'');L+=P;C.push({_A:_n,_H:{_Q:P,_L:A}})
349
+ elif I=='response.output_item.added':
350
+ J=D.get('item',{})
351
+ if J.get(_A)==_AN:G=J[_J];F[G]={_E:J.get(_E,''),_q:J.get(_q,G),_b:''}
352
+ elif I=='response.function_call_arguments.delta':
353
+ G=D.get('item_id','')
354
+ if G in F:F[G][_b]+=D.get(_Q,'')
355
+ elif I=='response.completed':
356
+ M=D.get(_AG,{}).get('status')
357
+ if(Q:=D.get(_AG,{}).get(_G)):A[_G][_N]=Q.get(_AJ,A[_G][_N]);A[_G][_O]=Q.get(_AK,A[_G][_O])
358
+ if L:A[_C].append({_A:_D,_D:L})
359
+ for K in F.values():
360
+ try:R=json.loads(K[_b])if K[_b]else{}
361
+ except json.JSONDecodeError:R={}
362
+ S={_A:_V,_J:K[_q],_E:K[_E],_M:R};A[_C].append(S);C.push({_A:_A3,_H:{_A4:S,_L:A}})
363
+ A[_K]=_c if F else _o if M=='incomplete'else _Z;C.push({_A:_A5,_H:{_A6:A[_K],_a:A}})
364
+ except Exception as V:A[_K]=_R;A[_Y]=str(V);C.push({_A:_R,_H:{_R:A}})
365
+ C.finish(A)
366
+ C._attach(asyncio.create_task(F()));return C
367
+ def resolve_api(api,*,model,api_key,base_url):
368
+ D=base_url;C=api_key;B=model;A=api
369
+ if not isinstance(A,str):return A
370
+ if A=='claude':return ClaudeChat(model=B,api_key=C,base_url=D)
371
+ if A=='gemini':return GeminiChat(model=B,api_key=C,base_url=D)
372
+ if A=='codex':return CodexChat(model=B,api_key=C,base_url=D)
373
+ return DefaultChat(model=B,api_key=C,base_url=D)
@@ -0,0 +1,177 @@
1
+ from __future__ import annotations
2
+ _J='commit'
3
+ _I='is_error'
4
+ _H='content'
5
+ _G='thinking_delta'
6
+ _F='type'
7
+ _E='text'
8
+ _D='\n'
9
+ _C=False
10
+ _B=True
11
+ _A=None
12
+ import asyncio,json,logging,select,signal,sys,threading
13
+ from typing import Callable
14
+ from.repl import Agent,TabModel,CommandEngine,UIAdapter,HELP_TEXT,format_exit_summary
15
+ logger=logging.getLogger(__name__)
16
+ _INTERRUPTED=object()
17
+ class BareAdapter(UIAdapter):
18
+ def __init__(A,repl:'BareRepl'):A.repl=repl
19
+ def show_error(A,text:str)->_A:print(f"\n✗ {text}",flush=_B)
20
+ def show_command_result(B,cmd:str,content:str|object)->_A:
21
+ A=content
22
+ if isinstance(A,str):print(A)
23
+ else:print(str(A))
24
+ def show_diff(A,diff_text:str,ref1_label:str,ref2_label:str)->_A:print(diff_text)
25
+ def show_history_list(A,lines:list[str])->_A:print(_D.join(lines))
26
+ def show_history_raw(A,json_text:str)->_A:print(json_text)
27
+ async def add_tab(A,tab:TabModel)->_A:0
28
+ def remove_tab(A,removed_index:int)->_A:
29
+ if A.repl.engine.active_index>=len(A.repl.engine.tabs):A.repl.engine.active_index=len(A.repl.engine.tabs)-1
30
+ def switch_to_tab(A,index:int)->_A:A.repl._render_tab_delimiter();A.repl._print_history_for_tab(A.repl.engine.tabs[index])
31
+ def refresh_chrome(A)->_A:0
32
+ def clear_tab_display(A,tab:TabModel)->_A:0
33
+ def clear_ephemeral(A,tab:TabModel)->_A:0
34
+ def on_agent_event(A,event:dict,tab:TabModel)->_A:
35
+ if tab is not A.repl.engine.active_tab:return
36
+ A.repl._handle_event(event,tab)
37
+ async def run_interactive_shell(B,command:str,cwd:str,env:dict|_A)->int:A=await asyncio.create_subprocess_shell(command,cwd=cwd,stdin=_A,stdout=_A,stderr=_A,env=env);await A.wait();return A.returncode or 0
38
+ def exit_app(B,summary:list[dict])->_A:
39
+ A=format_exit_summary(summary)
40
+ if A:print(A)
41
+ B.repl._should_exit=_B
42
+ def peek_tree_text(A,tab:TabModel)->str:return'Peek is not supported in bare mode.'
43
+ async def peek_open(A,tab:TabModel,path:list[Agent])->_A:0
44
+ def peek_close(A,tab:TabModel,agent_id:str|_A)->bool:return _C
45
+ class BareRepl:
46
+ def __init__(A,engine:CommandEngine,init_prompt:str|_A=_A):A.engine=engine;A.adapter=BareAdapter(A);A.engine.bind(A.adapter);A.engine.attach_agent(A.engine.tabs[0]);A.init_prompt=init_prompt;(A._pending_nls):int=0;(A._awaiting_content):bool=_C;(A._has_output):bool=_C;(A._last_stream_type):str|_A=_A;A._should_exit=_C;A._interrupted=threading.Event()
47
+ async def run(A)->_A:
48
+ C=asyncio.get_running_loop()
49
+ if A.init_prompt:D,A.init_prompt=A.init_prompt,_A;await A.engine.handle_input(D);await A._await_active_task(C)
50
+ while _B:
51
+ if A._should_exit:break
52
+ B=await A._read_input_interruptible(C)
53
+ if B is _INTERRUPTED:continue
54
+ if B is _A:print();break
55
+ B=B.strip()
56
+ if not B:continue
57
+ if B.lower()in{'exit','quit'}:break
58
+ await A.engine.handle_input(B)
59
+ if A._should_exit:break
60
+ await A._await_active_task(C)
61
+ async def _read_input_interruptible(B,loop:asyncio.AbstractEventLoop):
62
+ A=loop
63
+ def D():B._interrupted.set()
64
+ C=_C
65
+ try:A.add_signal_handler(signal.SIGINT,D);C=_B
66
+ except(NotImplementedError,RuntimeError):pass
67
+ try:return await A.run_in_executor(_A,B._read_input)
68
+ finally:
69
+ if C:A.remove_signal_handler(signal.SIGINT)
70
+ async def _await_active_task(C,loop:asyncio.AbstractEventLoop)->_A:
71
+ A=C.engine.active_tab
72
+ if A.running_task is _A:return
73
+ D=A.agent
74
+ def E()->_A:D.abort();print('\n(aborting…)')
75
+ B=_C
76
+ try:loop.add_signal_handler(signal.SIGINT,E);B=_B
77
+ except(NotImplementedError,RuntimeError):pass
78
+ try:await A.running_task
79
+ except asyncio.CancelledError:pass
80
+ finally:
81
+ if B:loop.remove_signal_handler(signal.SIGINT)
82
+ def _read_input(A):
83
+ G=A.engine.active_tab;D=f"[{G.title}] ≫ ";B:list[str]=[]
84
+ while _B:
85
+ sys.stdout.write(D);sys.stdout.flush()
86
+ while _B:
87
+ if A._interrupted.is_set():A._interrupted.clear();print();return _INTERRUPTED
88
+ try:E,H,H=select.select([sys.stdin],[],[],.1)
89
+ except(OSError,ValueError):E=[sys.stdin]
90
+ if E:break
91
+ F=sys.stdin.readline()
92
+ if F=='':return
93
+ C=F.rstrip(_D);B.append(C)
94
+ if C.endswith('\\'):B[-1]=C[:-1];D='... '
95
+ else:break
96
+ return _D.join(B)
97
+ def _handle_event(A,event:dict,tab:TabModel)->_A:
98
+ K='error';H=event;B,C=H[_F],H.get('payload',{})
99
+ if B in('text_delta',_G):
100
+ I=C.get('delta','')
101
+ if I:A._write_delta(I,B)
102
+ elif B=='tool_start':A._pending_nls=0;A._awaiting_content=_C;A._has_output=_B;A._last_stream_type=B
103
+ elif B=='tool_end':
104
+ L=C.get('result',{});E=L.get(_H);M=C.get(_I,_C);D=''
105
+ if E:
106
+ F:list[str]=[]
107
+ if isinstance(E,str):F.append(E)
108
+ elif isinstance(E,list):
109
+ for G in E:
110
+ if isinstance(G,dict)and G.get(_F)==_E:F.append(G.get(_E,''))
111
+ D=_D.join(F).strip(_D)
112
+ if M:
113
+ J='✗ '
114
+ if not D:D=f"{C.get("name","?")} failed"
115
+ else:J='→ 'if D else''
116
+ if D:A._write_delta(J+D,'tool_result')
117
+ A._last_stream_type=B;print()
118
+ elif B==_J:A._pending_nls=0;A._awaiting_content=_C;A._has_output=_B;print(f"\n◇ [{C.get("sha","")}] committed",flush=_B);A._last_stream_type=B
119
+ elif B==K:A._pending_nls=0;A._awaiting_content=_C;A._has_output=_B;N=str(C.get(K,C));print(f"\n✗ {N}",flush=_B);A._last_stream_type=B
120
+ elif B in('agent_start','turn_start'):A._pending_nls=0;A._awaiting_content=_C;A._has_output=_C;A._last_stream_type=_A
121
+ elif B=='agent_end':
122
+ A._pending_nls=0
123
+ if A._has_output:print()
124
+ A._last_stream_type=_A;A._has_output=_C;A._awaiting_content=_C
125
+ def _write_delta(A,text:str,delta_type:str)->_A:
126
+ D=delta_type;B=text
127
+ if D!=A._last_stream_type:A._pending_nls=0;A._awaiting_content=_B;A._last_stream_type=D
128
+ if A._awaiting_content:
129
+ B=B.lstrip(_D)
130
+ if not B:return
131
+ if A._awaiting_content:
132
+ if A._has_output:print()
133
+ A._awaiting_content=_C
134
+ if not A._awaiting_content and A._pending_nls>0:print(_D*A._pending_nls,end='',flush=_B);A._pending_nls=0
135
+ C=B.rstrip(_D)
136
+ if C:
137
+ if D==_G:print(f"{C}",end='',flush=_B)
138
+ else:print(C,end='',flush=_B)
139
+ A._has_output=_B
140
+ A._pending_nls=len(B)-len(C)
141
+ def _render_tab_delimiter(C)->_A:
142
+ A:list[str]=[]
143
+ for(B,D)in enumerate(C.engine.tabs):
144
+ if B==C.engine.active_index:A.append(f"▶ {B+1}. {D.title}")
145
+ else:A.append(f"▷ {B+1}. {D.title}")
146
+ print(_D+'┗━━┫ '+' ┃ '.join(A)+' ┃')
147
+ def _print_history_for_tab(B,tab:TabModel)->_A:
148
+ A=tab
149
+ for C in A.agent.messages:B._print_message(C)
150
+ if A.agent.stream is not _A:B._print_message(A.agent.stream)
151
+ def _print_message(O,msg:dict)->_A:
152
+ M='thinking';L='toolResult';F=msg;C=F.get('role','');D=F.get(_H,'');I=F.get(_I,_C)
153
+ if isinstance(D,list):G=D
154
+ elif isinstance(D,str):G=[{_F:_E,_E:D}]
155
+ else:return
156
+ if C==L:
157
+ H:list[str]=[]
158
+ for A in G:
159
+ if isinstance(A,dict)and A.get(_F)==_E:
160
+ J=A.get(_E,'').strip(_D)
161
+ if J:H.append(J)
162
+ if H:N='✗ 'if I else'→ ';print(N+_D.join(H))
163
+ return
164
+ for A in G:
165
+ K=A.get(_F,_E)
166
+ if K=='toolCall':
167
+ E=A.get('arguments',{})
168
+ if isinstance(E,dict):E=json.dumps(E,ensure_ascii=_C)
169
+ print(f"⚙ {A.get("name","")} {E}");continue
170
+ B=A.get(_E,'')or A.get(M,'')or'';B=B.strip(_D)
171
+ if not B:continue
172
+ if K==M:print(f"{B}")
173
+ elif I:print(f"✗ {B}")
174
+ elif C=='user':print(f"≫ {B}")
175
+ elif C==_J:print(f"◇ {B}")
176
+ elif C==L:print(f"→ {B}")
177
+ else:print(B)