xcoding 0.1.0__py3-none-any.whl
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.
- xcode/__init__.py +3 -0
- xcode/__main__.py +4 -0
- xcode/agent.py +341 -0
- xcode/backends.py +141 -0
- xcode/cli.py +600 -0
- xcode/config.py +72 -0
- xcode/hooks.py +74 -0
- xcode/input_bar.py +357 -0
- xcode/mcp.py +157 -0
- xcode/memory.py +34 -0
- xcode/permissions.py +80 -0
- xcode/session.py +67 -0
- xcode/tools.py +451 -0
- xcode/ui.py +349 -0
- xcoding-0.1.0.dist-info/METADATA +190 -0
- xcoding-0.1.0.dist-info/RECORD +20 -0
- xcoding-0.1.0.dist-info/WHEEL +5 -0
- xcoding-0.1.0.dist-info/entry_points.txt +2 -0
- xcoding-0.1.0.dist-info/licenses/LICENSE +21 -0
- xcoding-0.1.0.dist-info/top_level.txt +1 -0
xcode/ui.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""Look & feel: themes, the ghost+trees logo, the welcome box, and the
|
|
2
|
+
input header. Kept separate from cli.py so the chrome is easy to tweak.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import random
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
from rich.box import ROUNDED
|
|
14
|
+
from rich.console import Group
|
|
15
|
+
from rich.panel import Panel
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
|
|
19
|
+
PREFS = Path(".xcode") / "ui.json"
|
|
20
|
+
# The source logo we render into the terminal (lives at the project root).
|
|
21
|
+
LOGO = Path(__file__).resolve().parent.parent / "logo.png"
|
|
22
|
+
|
|
23
|
+
# The little glyph that spins while we think, and the words it cycles through.
|
|
24
|
+
SPIN_GLYPHS = "✻✷✶✳✺✸"
|
|
25
|
+
THINKING_WORDS = [
|
|
26
|
+
"Envisioning", "Thinking", "Pondering", "Cooking", "Conjuring",
|
|
27
|
+
"Crunching", "Noodling", "Percolating", "Scheming", "Manifesting",
|
|
28
|
+
"Summoning", "Brewing",
|
|
29
|
+
]
|
|
30
|
+
TIPS = [
|
|
31
|
+
"Use /btw to ask a quick side question without interrupting",
|
|
32
|
+
"Press shift+tab to cycle normal → auto → plan mode",
|
|
33
|
+
"Run /theme matrix to go full hacker mode",
|
|
34
|
+
"Type /clear to wipe the conversation and start fresh",
|
|
35
|
+
"Hit ↑/↓ or w/s to move through any menu, enter to pick",
|
|
36
|
+
"Try 'fix typecheck errors'",
|
|
37
|
+
"Ask me to 'make it more compact'",
|
|
38
|
+
"Use @file to attach files to your message",
|
|
39
|
+
"Press ctrl+z to undo your last edit in the input",
|
|
40
|
+
"Type / to see every slash command with descriptions",
|
|
41
|
+
"Use /model to switch the active model on the fly",
|
|
42
|
+
"Run /init to explore the project and write an XCODE.md",
|
|
43
|
+
"Use /compact to summarize and free up context",
|
|
44
|
+
"Try 'explain this stack trace'",
|
|
45
|
+
"Try 'write a commit message for my changes'",
|
|
46
|
+
"Try 'find where we handle login'",
|
|
47
|
+
"Ask 'how does <filepath> work?'",
|
|
48
|
+
"Use /resume to pick up a previous session",
|
|
49
|
+
"Use /memory to see what the project memory holds",
|
|
50
|
+
"Use /perms reset to clear saved permissions",
|
|
51
|
+
"Try 'add tests for the auth module'",
|
|
52
|
+
"Press ctrl+c to cancel the current input",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Each theme picks colors for the border, the ghost, the trees, accents, etc.
|
|
56
|
+
THEMES: dict[str, dict] = {
|
|
57
|
+
"ghost": { # default — clean white ghost on black
|
|
58
|
+
"border": "grey93", "ghost": "grey93", "eyes": "grey42",
|
|
59
|
+
"tree": "grey58", "accent": "white", "title": "bold white",
|
|
60
|
+
"user": "bold white", "tool": "grey50", "mode": "white",
|
|
61
|
+
},
|
|
62
|
+
"matrix": {
|
|
63
|
+
"border": "green", "ghost": "green1", "eyes": "bright_green",
|
|
64
|
+
"tree": "green3", "accent": "green1", "title": "bold green",
|
|
65
|
+
"user": "bold green", "tool": "green4", "mode": "green",
|
|
66
|
+
},
|
|
67
|
+
"dracula": {
|
|
68
|
+
"border": "purple", "ghost": "grey93", "eyes": "bright_magenta",
|
|
69
|
+
"tree": "spring_green3", "accent": "orchid", "title": "bold purple",
|
|
70
|
+
"user": "bold orchid", "tool": "grey50", "mode": "orchid",
|
|
71
|
+
},
|
|
72
|
+
"ember": {
|
|
73
|
+
"border": "dark_orange3", "ghost": "wheat1", "eyes": "orange1",
|
|
74
|
+
"tree": "dark_olive_green3", "accent": "orange1", "title": "bold orange1",
|
|
75
|
+
"user": "bold orange1", "tool": "grey50", "mode": "dark_orange",
|
|
76
|
+
},
|
|
77
|
+
"mono": {
|
|
78
|
+
"border": "grey50", "ghost": "grey93", "eyes": "grey70",
|
|
79
|
+
"tree": "grey58", "accent": "grey85", "title": "bold white",
|
|
80
|
+
"user": "bold white", "tool": "grey42", "mode": "grey70",
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
DEFAULT_THEME = "ghost"
|
|
85
|
+
|
|
86
|
+
def _ghost_text(theme: dict) -> Text:
|
|
87
|
+
"""Tiny compact ghost facing right."""
|
|
88
|
+
g = theme["ghost"]
|
|
89
|
+
e = theme["eyes"]
|
|
90
|
+
|
|
91
|
+
# Ghost facing RIGHT
|
|
92
|
+
logo_lines = [
|
|
93
|
+
" ▓▓▓▓▓",
|
|
94
|
+
"▓▓▓▓▓▓▓",
|
|
95
|
+
" ▓▒▓▓▒▓",
|
|
96
|
+
"▓▓▓▓▓▓▓",
|
|
97
|
+
" ▓ ▓",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
t = Text()
|
|
101
|
+
for line in logo_lines:
|
|
102
|
+
# Color the eyes (▒) differently
|
|
103
|
+
if "▒" in line:
|
|
104
|
+
parts = line.split("▒")
|
|
105
|
+
for i, part in enumerate(parts):
|
|
106
|
+
if i > 0:
|
|
107
|
+
t.append("▒", style=e)
|
|
108
|
+
t.append(part, style=g)
|
|
109
|
+
else:
|
|
110
|
+
t.append(line, style=g)
|
|
111
|
+
t.append("\n")
|
|
112
|
+
t.rstrip()
|
|
113
|
+
return t
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _neighbors(x, y, w, h):
|
|
117
|
+
if x > 0: yield x - 1, y
|
|
118
|
+
if x < w - 1: yield x + 1, y
|
|
119
|
+
if y > 0: yield x, y - 1
|
|
120
|
+
if y < h - 1: yield x, y + 1
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _largest_blob(mask, w, h):
|
|
124
|
+
"""(bounding box, mask) of the biggest connected True region — used to crop
|
|
125
|
+
to the ghost and to drop the little floating sparkles around it."""
|
|
126
|
+
from collections import deque
|
|
127
|
+
seen = [[False] * w for _ in range(h)]
|
|
128
|
+
best, best_box, best_cells = 0, (0, 0, w - 1, h - 1), None
|
|
129
|
+
for sy in range(h):
|
|
130
|
+
for sx in range(w):
|
|
131
|
+
if not mask[sy][sx] or seen[sy][sx]:
|
|
132
|
+
continue
|
|
133
|
+
dq = deque([(sx, sy)]); seen[sy][sx] = True
|
|
134
|
+
cells, l, t, r, b = [], sx, sy, sx, sy
|
|
135
|
+
while dq:
|
|
136
|
+
x, y = dq.popleft(); cells.append((x, y))
|
|
137
|
+
l, t, r, b = min(l, x), min(t, y), max(r, x), max(b, y)
|
|
138
|
+
for nx, ny in _neighbors(x, y, w, h):
|
|
139
|
+
if mask[ny][nx] and not seen[ny][nx]:
|
|
140
|
+
seen[ny][nx] = True; dq.append((nx, ny))
|
|
141
|
+
if len(cells) > best:
|
|
142
|
+
best, best_box, best_cells = len(cells), (l, t, r, b), cells
|
|
143
|
+
keep = [[False] * w for _ in range(h)]
|
|
144
|
+
for x, y in (best_cells or []):
|
|
145
|
+
keep[y][x] = True
|
|
146
|
+
return best_box, keep
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _logo_segments(width: int, rows: int):
|
|
150
|
+
"""Read logo.png and label every pixel as background / body / feature.
|
|
151
|
+
|
|
152
|
+
The logo is RGBA with a *transparent* field, so the alpha channel tells us
|
|
153
|
+
the ghost from the background directly. We: (1) sample at high res, (2) take
|
|
154
|
+
the largest opaque blob as the ghost (dropping the floating sparkles),
|
|
155
|
+
(3) crop + transparent-pad to it, (4) at display size, split the ghost into
|
|
156
|
+
its white *body* and its enclosed black *features* (the eyes + </> mouth).
|
|
157
|
+
The black outline rim is folded into the body so the silhouette stays solid.
|
|
158
|
+
Result codes: 0=bg, 1=body, 2=feature.
|
|
159
|
+
"""
|
|
160
|
+
from collections import deque
|
|
161
|
+
|
|
162
|
+
from PIL import Image, ImageOps
|
|
163
|
+
|
|
164
|
+
base = 220
|
|
165
|
+
im = Image.open(LOGO).convert("RGBA").resize((base, base))
|
|
166
|
+
apx = im.load()
|
|
167
|
+
opaque = [[apx[x, y][3] >= 128 for x in range(base)] for y in range(base)]
|
|
168
|
+
(l, t, r, b), _ = _largest_blob(opaque, base, base)
|
|
169
|
+
im = im.crop((l, t, r + 1, b + 1))
|
|
170
|
+
im = ImageOps.expand(im, border=max(2, (r - l) // 20), fill=(0, 0, 0, 0))
|
|
171
|
+
im = im.resize((width, rows))
|
|
172
|
+
|
|
173
|
+
px = im.load()
|
|
174
|
+
opq = [[px[x, y][3] >= 128 for x in range(width)] for y in range(rows)]
|
|
175
|
+
_, ghost = _largest_blob(opq, width, rows) # drop resized sparkle bits
|
|
176
|
+
|
|
177
|
+
def lum(x, y):
|
|
178
|
+
p = px[x, y]
|
|
179
|
+
return (p[0] * 299 + p[1] * 587 + p[2] * 114) // 1000
|
|
180
|
+
|
|
181
|
+
dark = [[ghost[y][x] and lum(x, y) < 128 for x in range(width)] for y in range(rows)]
|
|
182
|
+
|
|
183
|
+
# outline = dark rim touching the background; flood it so only the enclosed
|
|
184
|
+
# dark (eyes + mouth) stays a 'feature'.
|
|
185
|
+
outline = [[False] * width for _ in range(rows)]
|
|
186
|
+
dq = deque()
|
|
187
|
+
for y in range(rows):
|
|
188
|
+
for x in range(width):
|
|
189
|
+
if dark[y][x] and any(not ghost[ny][nx]
|
|
190
|
+
for nx, ny in _neighbors(x, y, width, rows)):
|
|
191
|
+
outline[y][x] = True; dq.append((x, y))
|
|
192
|
+
while dq:
|
|
193
|
+
x, y = dq.popleft()
|
|
194
|
+
for nx, ny in _neighbors(x, y, width, rows):
|
|
195
|
+
if dark[ny][nx] and not outline[ny][nx]:
|
|
196
|
+
outline[ny][nx] = True; dq.append((nx, ny))
|
|
197
|
+
|
|
198
|
+
out = [[0] * width for _ in range(rows)]
|
|
199
|
+
for y in range(rows):
|
|
200
|
+
for x in range(width):
|
|
201
|
+
if not ghost[y][x]:
|
|
202
|
+
out[y][x] = 0 # background → transparent
|
|
203
|
+
elif dark[y][x] and not outline[y][x]:
|
|
204
|
+
out[y][x] = 2 # enclosed feature → black
|
|
205
|
+
else:
|
|
206
|
+
out[y][x] = 1 # body (fill + outline rim)
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def logo(theme: dict, width: int = 26) -> Text:
|
|
211
|
+
"""Render logo.png into the terminal with truecolor half-blocks.
|
|
212
|
+
|
|
213
|
+
Each character is the 'upper half block' ▀ whose *foreground* paints the
|
|
214
|
+
top pixel and *background* paints the bottom pixel — so one cell carries
|
|
215
|
+
two stacked pixels. The body fills with the theme's ghost colour; the eyes
|
|
216
|
+
and </> mouth fill black; the field stays transparent.
|
|
217
|
+
"""
|
|
218
|
+
from rich.style import Style
|
|
219
|
+
|
|
220
|
+
rows = width + (width & 1) # even → 2 pixels per cell
|
|
221
|
+
try:
|
|
222
|
+
seg = _logo_segments(width, rows)
|
|
223
|
+
except Exception:
|
|
224
|
+
return _ghost_text(theme) # no Pillow / no file → ASCII
|
|
225
|
+
|
|
226
|
+
fill = {0: None, 1: theme["ghost"], 2: "grey3"}
|
|
227
|
+
t = Text()
|
|
228
|
+
for cr in range(rows // 2):
|
|
229
|
+
for c in range(width):
|
|
230
|
+
top = fill[seg[2 * cr][c]]
|
|
231
|
+
bot = fill[seg[2 * cr + 1][c]]
|
|
232
|
+
if top and bot:
|
|
233
|
+
t.append("▀", style=Style(color=top, bgcolor=bot))
|
|
234
|
+
elif top:
|
|
235
|
+
t.append("▀", style=Style(color=top))
|
|
236
|
+
elif bot:
|
|
237
|
+
t.append("▄", style=Style(color=bot))
|
|
238
|
+
else:
|
|
239
|
+
t.append(" ")
|
|
240
|
+
t.append("\n")
|
|
241
|
+
t.rstrip()
|
|
242
|
+
return t
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def welcome(theme: dict, model: str, cwd: str, notes: str = "") -> Panel:
|
|
246
|
+
"""Landscape welcome card: ghost on the left, info on the right."""
|
|
247
|
+
info = Group(
|
|
248
|
+
Text("xcode", style=theme["title"]),
|
|
249
|
+
Text("local-model coding agent", style=theme["tool"]),
|
|
250
|
+
Text(""),
|
|
251
|
+
Text(model, style=theme["accent"]),
|
|
252
|
+
Text(cwd + (f" ·{notes}" if notes else ""), style=theme["tool"]),
|
|
253
|
+
)
|
|
254
|
+
grid = Table.grid(padding=(0, 2))
|
|
255
|
+
grid.add_column(justify="left")
|
|
256
|
+
grid.add_column(justify="left")
|
|
257
|
+
grid.add_row(_ghost_text(theme), info)
|
|
258
|
+
|
|
259
|
+
return Panel(grid, box=ROUNDED, border_style=theme["border"],
|
|
260
|
+
padding=(0, 2), expand=False)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def status_line(theme: dict, mode: str, tokens: int, budget: int,
|
|
264
|
+
model: str) -> Text:
|
|
265
|
+
t = Text(" ")
|
|
266
|
+
if mode == "auto":
|
|
267
|
+
t.append("⏵⏵ auto mode on ", style=f"bold {theme['mode']}")
|
|
268
|
+
t.append("(shift+tab to cycle)", style="dim")
|
|
269
|
+
elif mode == "plan":
|
|
270
|
+
t.append("◷ plan mode ", style=f"bold {theme['mode']}")
|
|
271
|
+
t.append("(read-only · shift+tab to cycle)", style="dim")
|
|
272
|
+
else:
|
|
273
|
+
t.append("·· normal mode ", style=theme["mode"])
|
|
274
|
+
t.append("(shift+tab to cycle)", style="dim")
|
|
275
|
+
pct = min(100, int(100 * tokens / budget)) if budget else 0
|
|
276
|
+
tok_color = "green" if pct < 60 else "yellow" if pct < 85 else "red"
|
|
277
|
+
t.append(" · ")
|
|
278
|
+
t.append(f"~{tokens/1000:.1f}k/{budget//1000}k", style=tok_color)
|
|
279
|
+
t.append(" · ")
|
|
280
|
+
t.append(f"● {model}", style=theme["accent"])
|
|
281
|
+
return t
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# ------------------------------------------------------------- thinking
|
|
285
|
+
|
|
286
|
+
class ThinkingStatus:
|
|
287
|
+
"""A live, self-animating status line shown while the model works.
|
|
288
|
+
|
|
289
|
+
Renders like Claude Code's:
|
|
290
|
+
|
|
291
|
+
✻ Envisioning… (48s · ↓ 1.7k tokens · 2 shells running) ⎿ Tip: Use /btw to ask a quick side question
|
|
292
|
+
|
|
293
|
+
Drive it with a rich.live.Live; the glyph spins, the timer ticks and the
|
|
294
|
+
token counter climbs on every refresh — no extra work needed from caller.
|
|
295
|
+
Mutate `.tokens` and `.preview` from the stream loop to update them.
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(self, theme: dict, get_shells: Callable[[], int] | None = None):
|
|
299
|
+
self.theme = theme
|
|
300
|
+
self.get_shells = get_shells or (lambda: 0)
|
|
301
|
+
self.start = time.monotonic()
|
|
302
|
+
self.word = random.choice(THINKING_WORDS)
|
|
303
|
+
self.tip = random.choice(TIPS)
|
|
304
|
+
self.tokens = 0
|
|
305
|
+
self.preview = ""
|
|
306
|
+
|
|
307
|
+
def __rich__(self) -> Group:
|
|
308
|
+
elapsed = time.monotonic() - self.start
|
|
309
|
+
glyph = SPIN_GLYPHS[int(elapsed * 6) % len(SPIN_GLYPHS)]
|
|
310
|
+
shells = self.get_shells()
|
|
311
|
+
|
|
312
|
+
head = Text()
|
|
313
|
+
head.append(f"{glyph} ", style=self.theme["accent"])
|
|
314
|
+
head.append(f"{self.word}… ", style=self.theme["title"])
|
|
315
|
+
meta = f"({int(elapsed)}s · ↓ {self.tokens / 1000:.1f}k tokens"
|
|
316
|
+
if shells:
|
|
317
|
+
meta += f" · {shells} shell{'s' if shells != 1 else ''} running"
|
|
318
|
+
meta += ")"
|
|
319
|
+
head.append(meta, style="dim")
|
|
320
|
+
|
|
321
|
+
lines = [head]
|
|
322
|
+
if self.preview:
|
|
323
|
+
snippet = self.preview.replace("\n", " ").strip()[-80:]
|
|
324
|
+
lines.append(Text(f" {snippet}", style="dim"))
|
|
325
|
+
lines.append(Text(f" ⎿ Tip: {self.tip}", style="dim"))
|
|
326
|
+
return Group(*lines)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ----------------------------------------------------------------- prefs
|
|
330
|
+
|
|
331
|
+
def load_prefs() -> dict:
|
|
332
|
+
if PREFS.exists():
|
|
333
|
+
try:
|
|
334
|
+
return json.loads(PREFS.read_text(encoding="utf-8"))
|
|
335
|
+
except Exception:
|
|
336
|
+
pass
|
|
337
|
+
return {"theme": DEFAULT_THEME, "mode": "normal"}
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def save_prefs(prefs: dict) -> None:
|
|
341
|
+
try:
|
|
342
|
+
PREFS.parent.mkdir(parents=True, exist_ok=True)
|
|
343
|
+
PREFS.write_text(json.dumps(prefs, indent=2), encoding="utf-8")
|
|
344
|
+
except Exception:
|
|
345
|
+
pass
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def get_theme(name: str) -> dict:
|
|
349
|
+
return THEMES.get(name, THEMES[DEFAULT_THEME])
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xcoding
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local-model coding agent — Claude Code, but powered by Ollama or llama.cpp.
|
|
5
|
+
Author: c7s89r nzv
|
|
6
|
+
Maintainer: c7s89r nzv
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/c7s89r/xcode
|
|
9
|
+
Project-URL: Repository, https://github.com/c7s89r/xcode
|
|
10
|
+
Project-URL: Issues, https://github.com/c7s89r/xcode/issues
|
|
11
|
+
Keywords: cli,coding-agent,ollama,llama.cpp,ai,local-llm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Software Development
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: openai>=1.30.0
|
|
22
|
+
Requires-Dist: httpx>=0.27.0
|
|
23
|
+
Requires-Dist: rich>=13.7.0
|
|
24
|
+
Requires-Dist: prompt_toolkit>=3.0.0
|
|
25
|
+
Requires-Dist: discord.py>=2.3.0
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# xcode
|
|
29
|
+
|
|
30
|
+
A local-model coding agent — like Claude Code, but it talks to a model
|
|
31
|
+
running on your own machine instead of a cloud API.
|
|
32
|
+
|
|
33
|
+
> **✅ Works with [Ollama](https://ollama.com) for now.** Just install Ollama,
|
|
34
|
+
> pull a tool-capable model, then `pip install xcode` and run `xcode`.
|
|
35
|
+
> (llama.cpp support is in too, but Ollama is the tested path.)
|
|
36
|
+
|
|
37
|
+
It auto-detects whichever backend is running, gives the model tools to read/write
|
|
38
|
+
files and run shell commands, and loops until your task is done. Every file write
|
|
39
|
+
and every shell command asks for your approval first.
|
|
40
|
+
|
|
41
|
+
### Quick start (Ollama)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
ollama serve
|
|
45
|
+
ollama pull qwen2.5-coder # a model that's good at tool use
|
|
46
|
+
pip install xcode
|
|
47
|
+
xcode
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install xcode
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then just run `xcode` from any project folder.
|
|
57
|
+
|
|
58
|
+
Or from source:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install -e .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
(Python 3.9+. Pulls in `openai`, `httpx`, `rich`.)
|
|
65
|
+
|
|
66
|
+
## Run a backend
|
|
67
|
+
|
|
68
|
+
**Ollama** (easiest — supports tool-calling natively):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
ollama serve
|
|
72
|
+
ollama pull qwen2.5-coder # a model that's good at tool use
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**llama.cpp** (raw GGUF files):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
llama-server -m your-model.gguf # listens on :8080, OpenAI-compatible
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> Tool-calling quality depends heavily on the model. Use a model trained for it
|
|
82
|
+
> (e.g. `qwen2.5-coder`, `llama3.1`, `mistral-nemo`). Tiny models will struggle.
|
|
83
|
+
|
|
84
|
+
## Use it
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
xcode
|
|
88
|
+
# or: python -m xcode
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then just talk to it:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
› add a /health endpoint to app.py that returns {"ok": true}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
In-REPL commands: `/help`, `/models`, `/model`, `/init`, `/todos`, `/perms`,
|
|
98
|
+
`/compact`, `/sessions`, `/resume`, `/reset`, `/exit`.
|
|
99
|
+
|
|
100
|
+
- Replies **stream** live; the prompt shows a **context meter** (`~3.2k/8k`).
|
|
101
|
+
- Writes/commands ask `y / n / a`; **a** ("always") is saved to
|
|
102
|
+
`.xcode/permissions.json`. Edits show a **colored diff** preview.
|
|
103
|
+
- Attach files inline with `@path` (e.g. `explain @xcode/agent.py`).
|
|
104
|
+
- The agent tracks a **todo list** for multi-step work (`/todos` to view).
|
|
105
|
+
- Old turns are **auto-compacted** when the context meter fills; `/compact`
|
|
106
|
+
forces it. Conversations are **saved** per project — `xcode --resume` or
|
|
107
|
+
`/resume` to pick up where you left off.
|
|
108
|
+
- Drop an **XCODE.md** at the repo root (or run `/init`) and it's auto-loaded
|
|
109
|
+
as project memory.
|
|
110
|
+
|
|
111
|
+
### Modes (shift+tab to cycle)
|
|
112
|
+
|
|
113
|
+
- **·· normal** — asks before writes/commands
|
|
114
|
+
- **⏵⏵ auto** — runs & writes without asking
|
|
115
|
+
- **◷ plan** — read-only; explores but makes no changes
|
|
116
|
+
|
|
117
|
+
### Sub-agents, web, MCP, hooks
|
|
118
|
+
|
|
119
|
+
- `spawn_agent` lets the model delegate an isolated subtask to a fresh context.
|
|
120
|
+
- `web_search` (DuckDuckGo) and `web_fetch` give it internet access.
|
|
121
|
+
- Drop a `.xcode/settings.json` to add **hooks** (run a formatter after every
|
|
122
|
+
edit), **env** vars, seed **permissions**, and declare **MCP servers**:
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"hooks": { "after_edit": ["ruff format {path}"] },
|
|
127
|
+
"permissions": { "commands": ["git", "ls", "python"] },
|
|
128
|
+
"mcpServers": {
|
|
129
|
+
"fs": { "command": "npx",
|
|
130
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "."] }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
MCP tools show up to the model as `mcp__<server>__<tool>`.
|
|
136
|
+
|
|
137
|
+
### Headless / scripting
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
xcode -p "summarize what this repo does" # read-only, prints, exits
|
|
141
|
+
xcode -p "bump the version to 0.2.0" --yes # auto-approve writes
|
|
142
|
+
xcode -p "what changed?" --resume # continue last session
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Configuration (env vars)
|
|
146
|
+
|
|
147
|
+
| var | meaning |
|
|
148
|
+
|------------------|------------------------------------------------------|
|
|
149
|
+
| `XCODE_BASE_URL` | point straight at any OpenAI-compatible `/v1` URL |
|
|
150
|
+
| `XCODE_MODEL` | force a specific model name |
|
|
151
|
+
| `XCODE_API_KEY` | token if your endpoint needs one (default `local`) |
|
|
152
|
+
| `XCODE_MAX_STEPS`| max tool round-trips per turn (default 25) |
|
|
153
|
+
|
|
154
|
+
## How it works
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
cli.py REPL + permission prompts (the only UI code)
|
|
158
|
+
agent.py the loop: model ⇄ tools until it stops calling tools
|
|
159
|
+
backends.py auto-detect Ollama (:11434) / llama.cpp (:8080)
|
|
160
|
+
tools.py read_file, write_file, list_dir, run_command + JSON schemas
|
|
161
|
+
config.py system prompt + knobs
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Roadmap
|
|
165
|
+
|
|
166
|
+
- [x] Streaming token output
|
|
167
|
+
- [x] `edit_file` (targeted edits instead of full rewrites)
|
|
168
|
+
- [x] `grep` / `glob_files` search tools
|
|
169
|
+
- [x] Persistent permission rules ("always allow `git …`")
|
|
170
|
+
- [x] `/model` picker + smart default-model selection
|
|
171
|
+
- [x] Context compaction for long sessions + context meter
|
|
172
|
+
- [x] Diff-style preview when confirming edits
|
|
173
|
+
- [x] Project memory (XCODE.md) + `/init`
|
|
174
|
+
- [x] Todo/task tracking
|
|
175
|
+
- [x] Session save + `--resume`
|
|
176
|
+
- [x] Headless mode (`-p`) + `@file` mentions
|
|
177
|
+
- [x] Web fetch / web search tools
|
|
178
|
+
- [x] Sub-agents (delegate a subtask to a fresh context)
|
|
179
|
+
- [x] MCP server support
|
|
180
|
+
- [x] Hooks + settings.json
|
|
181
|
+
- [x] Themes + ghost logo, shift+tab mode cycling (normal/auto/plan)
|
|
182
|
+
|
|
183
|
+
## Made by
|
|
184
|
+
|
|
185
|
+
Built by **@c7s89r** (nzv).
|
|
186
|
+
|
|
187
|
+
- GitHub: [@c7s89r](https://github.com/c7s89r)
|
|
188
|
+
- Discord: `c7s89r`
|
|
189
|
+
|
|
190
|
+
MIT licensed — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
xcode/__init__.py,sha256=bfuPcvsqB0fFWkasX5F3XKEtGuwBM6djqJiXbUHLgcI,108
|
|
2
|
+
xcode/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
+
xcode/agent.py,sha256=UUK78OArJNd0OGC_aCJhQcEl-klXik-_CJX417aN7XU,14149
|
|
4
|
+
xcode/backends.py,sha256=CEfkyzjGn-1psVr4GZKiE--Ggprqd82MMjaJsYrN3yk,4636
|
|
5
|
+
xcode/cli.py,sha256=ciBSoB0JBGI3fqNQVXpC4K70xD6cfi5LyignSVmbVEg,24391
|
|
6
|
+
xcode/config.py,sha256=eLuOCJsZkvzk4bpiJRMHyi1tijIslNVejciiA74cwa8,3883
|
|
7
|
+
xcode/hooks.py,sha256=zqWfe5dLZ_H09M1IPqGaTR5c8b_qA5mTyPPlIO3614k,2402
|
|
8
|
+
xcode/input_bar.py,sha256=dzCxTe9t2SnKhpph8oNKsoBZgMBpHoaqkO2AIzp2Xjs,13839
|
|
9
|
+
xcode/mcp.py,sha256=U4OQYwgaTEmF7cXM1rl8U58AotjbMGU1F8PXAU_TrLs,5725
|
|
10
|
+
xcode/memory.py,sha256=PNAr4bbzB5yjAntFjHckoWCfUH0X2VpazkCqmrQ2JcA,1217
|
|
11
|
+
xcode/permissions.py,sha256=EeggrynUfP_Yjm0oqMoLrP44IN9u9egN1WeA9HWNyiE,2661
|
|
12
|
+
xcode/session.py,sha256=KcjBR-PmVEJF5JDTMk7WphPi8YcpYofg-NTisv9V588,1757
|
|
13
|
+
xcode/tools.py,sha256=-eB4A5lnHo9hPIX-xSsxYhufsBkqD62jHaPFbKskQYk,17564
|
|
14
|
+
xcode/ui.py,sha256=qJ6_21hLxF_qdOEISxQdYC8JRnHEIFH1zDS_2eGTk9w,13477
|
|
15
|
+
xcoding-0.1.0.dist-info/licenses/LICENSE,sha256=E2ul7pgp6YEzKrl_f3mwu6X4P02Ap82BqMW0dyO7r20,1069
|
|
16
|
+
xcoding-0.1.0.dist-info/METADATA,sha256=OgjdcmLzf0Slew0VuJkuOoxFsK6J1vS3_UH-Kghp3l4,6210
|
|
17
|
+
xcoding-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
xcoding-0.1.0.dist-info/entry_points.txt,sha256=qvTz6nsCXyjnhJwuI6OQ7At8x3YlxWUTo1KhacIoXWk,41
|
|
19
|
+
xcoding-0.1.0.dist-info/top_level.txt,sha256=81BFRvNclHTxdaBmzzIyfb_JaVM_fwWEGSRkMhsoRBs,6
|
|
20
|
+
xcoding-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 c7s89r (nzv)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xcode
|