claude-ai-clone-client 3.0.0__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.
- claude_ai_clone_client-3.0.0/PKG-INFO +7 -0
- claude_ai_clone_client-3.0.0/ai_cli.py +916 -0
- claude_ai_clone_client-3.0.0/claude_ai_clone_client.egg-info/PKG-INFO +7 -0
- claude_ai_clone_client-3.0.0/claude_ai_clone_client.egg-info/SOURCES.txt +8 -0
- claude_ai_clone_client-3.0.0/claude_ai_clone_client.egg-info/dependency_links.txt +1 -0
- claude_ai_clone_client-3.0.0/claude_ai_clone_client.egg-info/entry_points.txt +2 -0
- claude_ai_clone_client-3.0.0/claude_ai_clone_client.egg-info/requires.txt +1 -0
- claude_ai_clone_client-3.0.0/claude_ai_clone_client.egg-info/top_level.txt +1 -0
- claude_ai_clone_client-3.0.0/pyproject.toml +17 -0
- claude_ai_clone_client-3.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
"""CLI for the local AI platform.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
ai health Check backend health
|
|
5
|
+
ai hardware Show hardware info
|
|
6
|
+
ai model list List installed models
|
|
7
|
+
ai model versions <model_id> List versions of a model
|
|
8
|
+
ai model download <repo-url> Download from GitHub releases
|
|
9
|
+
ai model add <path-or-url> Import from local path or GitHub URL
|
|
10
|
+
ai model use <name>[:version] Load a model (optionally set version)
|
|
11
|
+
ai model update Check all sources for new versions
|
|
12
|
+
ai source list List tracked sources
|
|
13
|
+
ai source add Add a GitHub repo to track
|
|
14
|
+
ai source remove <model_id> Remove a tracked source
|
|
15
|
+
ai chat --model <id> Interactive chat
|
|
16
|
+
ai ask --model <id> --prompt .. One-shot prompt
|
|
17
|
+
ai run <model_id> Load model and start chatting
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
import requests
|
|
28
|
+
|
|
29
|
+
DEFAULT_HOST = "http://127.0.0.1:8090"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ── HTTP helpers ───────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _request(
|
|
36
|
+
method: str, url: str, payload: dict[str, Any] | None = None
|
|
37
|
+
) -> dict[str, Any]:
|
|
38
|
+
response = requests.request(method, url, json=payload, timeout=600)
|
|
39
|
+
response.raise_for_status()
|
|
40
|
+
return response.json()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _stream_chat(url: str, payload: dict[str, Any]) -> str:
|
|
44
|
+
with requests.post(url, json=payload, stream=True, timeout=600) as response:
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
final = []
|
|
47
|
+
for line in response.iter_lines(decode_unicode=True):
|
|
48
|
+
if not line or not line.startswith("data: "):
|
|
49
|
+
continue
|
|
50
|
+
event = json.loads(line[6:])
|
|
51
|
+
token = event.get("token")
|
|
52
|
+
if token:
|
|
53
|
+
print(token, end="", flush=True)
|
|
54
|
+
final.append(token)
|
|
55
|
+
print()
|
|
56
|
+
return "".join(final).strip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _print_json(data: Any) -> None:
|
|
60
|
+
print(json.dumps(data, indent=2))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _print_table(rows: list[dict], columns: list[str]) -> None:
|
|
64
|
+
if not rows:
|
|
65
|
+
print("(none)")
|
|
66
|
+
return
|
|
67
|
+
widths = {col: len(col) for col in columns}
|
|
68
|
+
for row in rows:
|
|
69
|
+
for col in columns:
|
|
70
|
+
widths[col] = max(widths[col], len(str(row.get(col, ""))))
|
|
71
|
+
header = " ".join(col.ljust(widths[col]) for col in columns)
|
|
72
|
+
print(header)
|
|
73
|
+
print(" ".join("-" * widths[col] for col in columns))
|
|
74
|
+
for row in rows:
|
|
75
|
+
print(" ".join(str(row.get(col, "")).ljust(widths[col]) for col in columns))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ── Command handlers ───────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def cmd_health(args):
|
|
82
|
+
data = _request("GET", f"{args.host}/health")
|
|
83
|
+
_print_json(data)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_hardware(args):
|
|
87
|
+
data = _request("GET", f"{args.host}/hardware")
|
|
88
|
+
_print_json(data)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def cmd_model_catalog(args):
|
|
92
|
+
"""Browse the curated catalog of free AI models."""
|
|
93
|
+
data = _request("GET", f"{args.host}/catalog")
|
|
94
|
+
models = data.get("models", [])
|
|
95
|
+
if args.json:
|
|
96
|
+
_print_json(data)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
# Also get installed models to show status
|
|
100
|
+
installed_data = _request("GET", f"{args.host}/models")
|
|
101
|
+
installed_ids = {m["model_id"] for m in installed_data.get("models", [])}
|
|
102
|
+
|
|
103
|
+
if args.use_case:
|
|
104
|
+
models = [m for m in models if args.use_case in m.get("use_case", [])]
|
|
105
|
+
|
|
106
|
+
rows = []
|
|
107
|
+
for m in models:
|
|
108
|
+
rows.append({
|
|
109
|
+
"model_id": m["model_id"],
|
|
110
|
+
"params": m.get("parameters", "?"),
|
|
111
|
+
"size": f"{m.get('size_gb', '?')} GB",
|
|
112
|
+
"license": (m.get("license") or "?")[:18],
|
|
113
|
+
"use_case": ", ".join(m.get("use_case", [])),
|
|
114
|
+
"status": "installed" if m["model_id"] in installed_ids else "available",
|
|
115
|
+
})
|
|
116
|
+
_print_table(rows, ["model_id", "params", "size", "license", "use_case", "status"])
|
|
117
|
+
print(f"\n {len(models)} models | To download: ai model download --model-id <id> --url <github-repo> --runtime llama_cpp")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def cmd_model_list(args):
|
|
121
|
+
data = _request("GET", f"{args.host}/models")
|
|
122
|
+
models = data.get("models", [])
|
|
123
|
+
if args.json:
|
|
124
|
+
_print_json(data)
|
|
125
|
+
return
|
|
126
|
+
rows = []
|
|
127
|
+
for m in models:
|
|
128
|
+
rows.append({
|
|
129
|
+
"model_id": m["model_id"],
|
|
130
|
+
"runtime": m["runtime"],
|
|
131
|
+
"format": m.get("format") or "—",
|
|
132
|
+
"active_v": f"v{m.get('active_version', '?')}",
|
|
133
|
+
"versions": len(m.get("versions", [])),
|
|
134
|
+
"source": m.get("source_repo") or "local",
|
|
135
|
+
})
|
|
136
|
+
_print_table(rows, ["model_id", "runtime", "format", "active_v", "versions", "source"])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def cmd_model_versions(args):
|
|
140
|
+
data = _request("GET", f"{args.host}/models/{args.model_id}/versions")
|
|
141
|
+
versions = data.get("versions", [])
|
|
142
|
+
if args.json:
|
|
143
|
+
_print_json(data)
|
|
144
|
+
return
|
|
145
|
+
rows = []
|
|
146
|
+
for v in versions:
|
|
147
|
+
rows.append({
|
|
148
|
+
"version": f"v{v['version']}",
|
|
149
|
+
"tag": v.get("version_tag") or "—",
|
|
150
|
+
"sha256": (v.get("sha256") or "—")[:12],
|
|
151
|
+
"size_gb": str(v.get("size_gb") or "—"),
|
|
152
|
+
"created": (v.get("created_at") or "—")[:19],
|
|
153
|
+
})
|
|
154
|
+
_print_table(rows, ["version", "tag", "sha256", "size_gb", "created"])
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def cmd_model_download(args):
|
|
158
|
+
"""Download a model — supports both direct URLs and GitHub repo discovery."""
|
|
159
|
+
url = args.url
|
|
160
|
+
|
|
161
|
+
# Check if this looks like a GitHub repo (not a direct file URL)
|
|
162
|
+
if _is_github_repo(url):
|
|
163
|
+
print(f"Discovering model assets in {url}...")
|
|
164
|
+
data = _request("GET", f"{args.host}/github/discover", None)
|
|
165
|
+
# Use query param instead
|
|
166
|
+
response = requests.get(
|
|
167
|
+
f"{args.host}/github/discover",
|
|
168
|
+
params={"repo_url": url},
|
|
169
|
+
timeout=30,
|
|
170
|
+
)
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
data = response.json()
|
|
173
|
+
assets = data.get("assets", [])
|
|
174
|
+
if not assets:
|
|
175
|
+
print("No model assets found in this repository's releases.", file=sys.stderr)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
# Filter by runtime/format if specified
|
|
179
|
+
pattern = args.asset_pattern or f"*.{_runtime_ext(args.runtime)}"
|
|
180
|
+
import fnmatch
|
|
181
|
+
matching = [a for a in assets if fnmatch.fnmatch(a["asset_name"].lower(), pattern.lower())]
|
|
182
|
+
|
|
183
|
+
if not matching:
|
|
184
|
+
print(f"No assets matching '{pattern}'. Available:", file=sys.stderr)
|
|
185
|
+
for a in assets[:10]:
|
|
186
|
+
print(f" {a['asset_name']} ({a['tag']})", file=sys.stderr)
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Download the latest matching asset
|
|
190
|
+
asset = matching[0]
|
|
191
|
+
print(f"Found: {asset['asset_name']} (tag: {asset['tag']}, {asset['size_bytes'] / 1e9:.1f} GB)")
|
|
192
|
+
payload = {
|
|
193
|
+
"model_id": args.model_id,
|
|
194
|
+
"source_url": asset["download_url"],
|
|
195
|
+
"runtime": args.runtime,
|
|
196
|
+
"filename": asset["asset_name"],
|
|
197
|
+
"format": asset.get("format"),
|
|
198
|
+
"version_tag": asset["tag"],
|
|
199
|
+
"license": args.license,
|
|
200
|
+
}
|
|
201
|
+
else:
|
|
202
|
+
# Direct URL download
|
|
203
|
+
payload = {
|
|
204
|
+
"model_id": args.model_id,
|
|
205
|
+
"source_url": url,
|
|
206
|
+
"runtime": args.runtime,
|
|
207
|
+
"filename": args.filename,
|
|
208
|
+
"sha256": args.sha256,
|
|
209
|
+
"license": args.license,
|
|
210
|
+
"size_gb": args.size_gb,
|
|
211
|
+
"format": args.format,
|
|
212
|
+
"version_tag": args.version_tag,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
print(f"Downloading {args.model_id}...")
|
|
216
|
+
data = _request("POST", f"{args.host}/models/download", payload)
|
|
217
|
+
model = data.get("model", {})
|
|
218
|
+
print(f"Registered {model.get('model_id')} v{model.get('active_version')} ({model.get('runtime')})")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def cmd_model_add(args):
|
|
222
|
+
"""Smart import: detects local path vs GitHub URL."""
|
|
223
|
+
target = args.target
|
|
224
|
+
|
|
225
|
+
# Local path?
|
|
226
|
+
path = Path(target)
|
|
227
|
+
if path.exists():
|
|
228
|
+
payload = {
|
|
229
|
+
"model_id": args.model_id,
|
|
230
|
+
"local_path": str(path.resolve()),
|
|
231
|
+
"runtime": args.runtime,
|
|
232
|
+
"license": args.license,
|
|
233
|
+
"format": args.format,
|
|
234
|
+
}
|
|
235
|
+
data = _request("POST", f"{args.host}/models/import", payload)
|
|
236
|
+
model = data.get("model", {})
|
|
237
|
+
print(f"Imported {model.get('model_id')} v{model.get('active_version')} from {target}")
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
# GitHub URL?
|
|
241
|
+
if "github.com" in target or "/" in target:
|
|
242
|
+
# Treat as a download
|
|
243
|
+
args.url = target
|
|
244
|
+
args.filename = None
|
|
245
|
+
args.sha256 = None
|
|
246
|
+
args.size_gb = None
|
|
247
|
+
args.version_tag = None
|
|
248
|
+
args.asset_pattern = None
|
|
249
|
+
cmd_model_download(args)
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
print(f"Cannot resolve '{target}': not a local path or GitHub URL", file=sys.stderr)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def cmd_model_use(args):
|
|
256
|
+
model_id, _, version_str = args.model_version.partition(":")
|
|
257
|
+
if version_str:
|
|
258
|
+
version = int(version_str.lstrip("v"))
|
|
259
|
+
_request("POST", f"{args.host}/models/set-version", {
|
|
260
|
+
"model_id": model_id,
|
|
261
|
+
"version": version,
|
|
262
|
+
})
|
|
263
|
+
print(f"Set {model_id} active version to v{version}")
|
|
264
|
+
|
|
265
|
+
payload = {"model_id": model_id}
|
|
266
|
+
if args.threads:
|
|
267
|
+
payload["threads"] = args.threads
|
|
268
|
+
data = _request("POST", f"{args.host}/models/load", payload)
|
|
269
|
+
print(f"Loaded {data.get('loaded_model_id')} v{data.get('loaded_version')} "
|
|
270
|
+
f"({data.get('loaded_runtime')}, {data.get('threads')} threads)")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def cmd_model_update(args):
|
|
274
|
+
"""Check tracked sources for new model versions."""
|
|
275
|
+
if args.model_id:
|
|
276
|
+
print(f"Checking updates for {args.model_id}...")
|
|
277
|
+
data = _request("POST", f"{args.host}/models/update/{args.model_id}")
|
|
278
|
+
else:
|
|
279
|
+
print("Checking all tracked sources for updates...")
|
|
280
|
+
data = _request("POST", f"{args.host}/models/update")
|
|
281
|
+
|
|
282
|
+
results = data.get("results", [])
|
|
283
|
+
if not results:
|
|
284
|
+
print("No tracked sources configured. Use 'ai source add' first.")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
for r in results:
|
|
288
|
+
new = r.get("new_versions", [])
|
|
289
|
+
skipped = r.get("skipped_existing", 0) + r.get("skipped_duplicate", 0)
|
|
290
|
+
errors = r.get("errors", [])
|
|
291
|
+
print(f"\n {r['model_id']}:")
|
|
292
|
+
if new:
|
|
293
|
+
print(f" New versions downloaded: {', '.join(new)}")
|
|
294
|
+
else:
|
|
295
|
+
print(f" No new versions (skipped {skipped} existing)")
|
|
296
|
+
for err in errors:
|
|
297
|
+
print(f" Error: {err}")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def cmd_source_list(args):
|
|
301
|
+
data = _request("GET", f"{args.host}/sources")
|
|
302
|
+
sources = data.get("sources", [])
|
|
303
|
+
if args.json:
|
|
304
|
+
_print_json(data)
|
|
305
|
+
return
|
|
306
|
+
rows = []
|
|
307
|
+
for s in sources:
|
|
308
|
+
rows.append({
|
|
309
|
+
"model_id": s["model_id"],
|
|
310
|
+
"repo": f"{s['owner']}/{s['repo']}",
|
|
311
|
+
"runtime": s["runtime"],
|
|
312
|
+
"pattern": s["asset_pattern"],
|
|
313
|
+
"auto": "yes" if s.get("auto_update", True) else "no",
|
|
314
|
+
"checked": (s.get("last_checked") or "never")[:19],
|
|
315
|
+
})
|
|
316
|
+
_print_table(rows, ["model_id", "repo", "runtime", "pattern", "auto", "checked"])
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def cmd_source_add(args):
|
|
320
|
+
payload = {
|
|
321
|
+
"repo_url": args.repo_url,
|
|
322
|
+
"model_id": args.model_id,
|
|
323
|
+
"runtime": args.runtime,
|
|
324
|
+
"asset_pattern": args.asset_pattern,
|
|
325
|
+
"license": args.license,
|
|
326
|
+
}
|
|
327
|
+
data = _request("POST", f"{args.host}/sources/add", payload)
|
|
328
|
+
s = data.get("source", {})
|
|
329
|
+
print(f"Tracking {s.get('owner')}/{s.get('repo')} -> {s.get('model_id')} (pattern: {s.get('asset_pattern')})")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def cmd_source_remove(args):
|
|
333
|
+
_request("DELETE", f"{args.host}/sources/{args.model_id}")
|
|
334
|
+
print(f"Removed source: {args.model_id}")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def cmd_chat(args):
|
|
338
|
+
conversation_id = args.conversation_id
|
|
339
|
+
print("Interactive chat (type 'exit' to quit)")
|
|
340
|
+
print("-" * 40)
|
|
341
|
+
while True:
|
|
342
|
+
try:
|
|
343
|
+
prompt = input("you> ").strip()
|
|
344
|
+
except EOFError:
|
|
345
|
+
break
|
|
346
|
+
if prompt.lower() in {"exit", "quit", "q"}:
|
|
347
|
+
break
|
|
348
|
+
if not prompt:
|
|
349
|
+
continue
|
|
350
|
+
payload = {
|
|
351
|
+
"model_id": args.model,
|
|
352
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
353
|
+
"temperature": args.temperature,
|
|
354
|
+
"max_tokens": args.max_tokens,
|
|
355
|
+
"stream": True,
|
|
356
|
+
"conversation_id": conversation_id,
|
|
357
|
+
}
|
|
358
|
+
print("ai> ", end="", flush=True)
|
|
359
|
+
_stream_chat(f"{args.host}/chat", payload)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def cmd_ask(args):
|
|
363
|
+
payload = {
|
|
364
|
+
"model_id": args.model,
|
|
365
|
+
"messages": [{"role": "user", "content": args.prompt}],
|
|
366
|
+
"temperature": args.temperature,
|
|
367
|
+
"max_tokens": args.max_tokens,
|
|
368
|
+
"stream": False,
|
|
369
|
+
}
|
|
370
|
+
result = _request("POST", f"{args.host}/chat", payload)
|
|
371
|
+
print(result.get("output", ""))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def cmd_run(args):
|
|
375
|
+
payload = {"model_id": args.model_id}
|
|
376
|
+
if args.threads:
|
|
377
|
+
payload["threads"] = args.threads
|
|
378
|
+
data = _request("POST", f"{args.host}/models/load", payload)
|
|
379
|
+
print(f"Loaded {data.get('loaded_model_id')} v{data.get('loaded_version')}")
|
|
380
|
+
args.model = args.model_id
|
|
381
|
+
args.conversation_id = None
|
|
382
|
+
args.temperature = 0.3
|
|
383
|
+
args.max_tokens = 512
|
|
384
|
+
cmd_chat(args)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def cmd_task(args):
|
|
388
|
+
"""Intelligent task execution with prompt expansion and deep directory awareness.
|
|
389
|
+
|
|
390
|
+
The agent:
|
|
391
|
+
1. Scans the entire directory tree (current dir + all children)
|
|
392
|
+
2. Detects languages, frameworks, dependencies
|
|
393
|
+
3. Reads git status for change context
|
|
394
|
+
4. Reads relevant file contents based on the task
|
|
395
|
+
5. Expands the user's prompt into a detailed plan
|
|
396
|
+
6. Executes the plan with full code output
|
|
397
|
+
"""
|
|
398
|
+
import subprocess
|
|
399
|
+
|
|
400
|
+
prompt_text = args.prompt
|
|
401
|
+
model = args.model
|
|
402
|
+
cwd = Path.cwd()
|
|
403
|
+
|
|
404
|
+
print("Analyzing project context...")
|
|
405
|
+
|
|
406
|
+
# ── Step 1: Deep directory scan ──────────────────────────
|
|
407
|
+
skip_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv",
|
|
408
|
+
"dist", "build", ".next", ".cache", "target", ".tox",
|
|
409
|
+
"egg-info", ".eggs", ".mypy_cache", ".pytest_cache"}
|
|
410
|
+
|
|
411
|
+
all_files = []
|
|
412
|
+
lang_counts: dict[str, int] = {}
|
|
413
|
+
ext_map = {
|
|
414
|
+
".py": "Python", ".js": "JavaScript", ".ts": "TypeScript", ".tsx": "TypeScript/React",
|
|
415
|
+
".jsx": "React", ".rs": "Rust", ".go": "Go", ".java": "Java", ".c": "C",
|
|
416
|
+
".cpp": "C++", ".h": "C/C++ Header", ".rb": "Ruby", ".php": "PHP",
|
|
417
|
+
".swift": "Swift", ".kt": "Kotlin", ".sql": "SQL", ".sh": "Shell",
|
|
418
|
+
".css": "CSS", ".html": "HTML", ".yaml": "YAML", ".yml": "YAML",
|
|
419
|
+
".json": "JSON", ".toml": "TOML", ".md": "Markdown",
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for p in sorted(cwd.rglob("*")):
|
|
423
|
+
rel = p.relative_to(cwd)
|
|
424
|
+
parts = rel.parts
|
|
425
|
+
if any(part.startswith(".") or any(skip in part for skip in skip_dirs) for part in parts):
|
|
426
|
+
continue
|
|
427
|
+
if p.is_file():
|
|
428
|
+
all_files.append(str(rel))
|
|
429
|
+
ext = p.suffix.lower()
|
|
430
|
+
if ext in ext_map:
|
|
431
|
+
lang = ext_map[ext]
|
|
432
|
+
lang_counts[lang] = lang_counts.get(lang, 0) + 1
|
|
433
|
+
|
|
434
|
+
# Detect primary language
|
|
435
|
+
sorted_langs = sorted(lang_counts.items(), key=lambda x: -x[1])
|
|
436
|
+
primary_lang = sorted_langs[0][0] if sorted_langs else "Unknown"
|
|
437
|
+
|
|
438
|
+
# ── Step 2: Git context ──────────────────────────────────
|
|
439
|
+
git_info = ""
|
|
440
|
+
try:
|
|
441
|
+
status = subprocess.check_output(["git", "status", "--short"], text=True, cwd=str(cwd), timeout=5).strip()
|
|
442
|
+
branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], text=True, cwd=str(cwd), timeout=5).strip()
|
|
443
|
+
recent = subprocess.check_output(
|
|
444
|
+
["git", "log", "--oneline", "-5"], text=True, cwd=str(cwd), timeout=5
|
|
445
|
+
).strip()
|
|
446
|
+
git_info = f"Branch: {branch}\nRecent commits:\n{recent}\n"
|
|
447
|
+
if status:
|
|
448
|
+
git_info += f"Uncommitted changes:\n{status}\n"
|
|
449
|
+
except Exception:
|
|
450
|
+
git_info = "(not a git repository)"
|
|
451
|
+
|
|
452
|
+
# ── Step 3: Read key project files ───────────────────────
|
|
453
|
+
config_files = [
|
|
454
|
+
"README.md", "pyproject.toml", "package.json", "Cargo.toml", "go.mod",
|
|
455
|
+
"Makefile", "Dockerfile", "docker-compose.yml", "requirements.txt",
|
|
456
|
+
"tsconfig.json", ".eslintrc.json", "setup.py", "setup.cfg",
|
|
457
|
+
]
|
|
458
|
+
config_excerpts = []
|
|
459
|
+
for cf in config_files:
|
|
460
|
+
cf_path = cwd / cf
|
|
461
|
+
if cf_path.exists():
|
|
462
|
+
try:
|
|
463
|
+
content = cf_path.read_text(encoding="utf-8")[:800]
|
|
464
|
+
config_excerpts.append(f"--- {cf} ---\n{content}")
|
|
465
|
+
except Exception:
|
|
466
|
+
pass
|
|
467
|
+
|
|
468
|
+
# ── Step 4: Find task-relevant files ─────────────────────
|
|
469
|
+
# Search for files that might be relevant to the task prompt
|
|
470
|
+
relevant_files = []
|
|
471
|
+
keywords = [w.lower() for w in prompt_text.split() if len(w) > 3]
|
|
472
|
+
for f in all_files:
|
|
473
|
+
f_lower = f.lower()
|
|
474
|
+
if any(kw in f_lower for kw in keywords):
|
|
475
|
+
relevant_files.append(f)
|
|
476
|
+
|
|
477
|
+
relevant_contents = []
|
|
478
|
+
for rf in relevant_files[:5]: # max 5 relevant files
|
|
479
|
+
rf_path = cwd / rf
|
|
480
|
+
try:
|
|
481
|
+
content = rf_path.read_text(encoding="utf-8")[:2000]
|
|
482
|
+
relevant_contents.append(f"--- {rf} ---\n{content}")
|
|
483
|
+
except Exception:
|
|
484
|
+
pass
|
|
485
|
+
|
|
486
|
+
# ── Build context ────────────────────────────────────────
|
|
487
|
+
context_parts = [
|
|
488
|
+
f"WORKING DIRECTORY: {cwd}",
|
|
489
|
+
f"PRIMARY LANGUAGE: {primary_lang}",
|
|
490
|
+
f"LANGUAGES DETECTED: {', '.join(f'{lang} ({n} files)' for lang, n in sorted_langs[:8])}",
|
|
491
|
+
f"TOTAL FILES: {len(all_files)}",
|
|
492
|
+
f"\nGIT STATUS:\n{git_info}",
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
# File tree (compact)
|
|
496
|
+
context_parts.append(f"\nFILE TREE ({len(all_files)} files):")
|
|
497
|
+
for f in all_files[:80]:
|
|
498
|
+
context_parts.append(f" {f}")
|
|
499
|
+
if len(all_files) > 80:
|
|
500
|
+
context_parts.append(f" ... and {len(all_files) - 80} more")
|
|
501
|
+
|
|
502
|
+
if config_excerpts:
|
|
503
|
+
context_parts.append("\nPROJECT CONFIG FILES:")
|
|
504
|
+
context_parts.extend(config_excerpts)
|
|
505
|
+
|
|
506
|
+
if relevant_contents:
|
|
507
|
+
context_parts.append(f"\nTASK-RELEVANT FILES (matched keywords from your prompt):")
|
|
508
|
+
context_parts.extend(relevant_contents)
|
|
509
|
+
|
|
510
|
+
context = "\n".join(context_parts)
|
|
511
|
+
|
|
512
|
+
print(f" Found {len(all_files)} files across {len(lang_counts)} languages")
|
|
513
|
+
print(f" Primary language: {primary_lang}")
|
|
514
|
+
if relevant_files:
|
|
515
|
+
print(f" Task-relevant files: {', '.join(relevant_files[:5])}")
|
|
516
|
+
|
|
517
|
+
# ── Step 5: Expand prompt into plan ──────────────────────
|
|
518
|
+
print("\nExpanding task into detailed plan...")
|
|
519
|
+
|
|
520
|
+
expand_prompt = (
|
|
521
|
+
f"You are an expert coding AI agent operating on the user's local machine. "
|
|
522
|
+
f"You have deep awareness of their entire project.\n\n"
|
|
523
|
+
f"PROJECT CONTEXT:\n{context}\n\n"
|
|
524
|
+
f"USER TASK: {prompt_text}\n\n"
|
|
525
|
+
f"Create a precise, numbered step-by-step plan:\n"
|
|
526
|
+
f"- For each step, specify the EXACT file path to modify or create\n"
|
|
527
|
+
f"- Describe the specific code changes (not vague descriptions)\n"
|
|
528
|
+
f"- Consider how changes interact with existing code\n"
|
|
529
|
+
f"- Include any new dependencies or configuration changes\n"
|
|
530
|
+
f"- Consider edge cases and error handling\n"
|
|
531
|
+
f"- If tests exist, include steps to update them\n\n"
|
|
532
|
+
f"Be actionable and complete. This plan will be executed."
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
payload = {
|
|
536
|
+
"model_id": model,
|
|
537
|
+
"messages": [{"role": "user", "content": expand_prompt}],
|
|
538
|
+
"temperature": 0.2,
|
|
539
|
+
"max_tokens": args.max_tokens,
|
|
540
|
+
"stream": True,
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
print("\n--- Task Plan ---\n")
|
|
544
|
+
plan = _stream_chat(f"{args.host}/chat", payload)
|
|
545
|
+
print()
|
|
546
|
+
|
|
547
|
+
# ── Step 6: Confirm ──────────────────────────────────────
|
|
548
|
+
if not args.auto:
|
|
549
|
+
try:
|
|
550
|
+
confirm = input("Execute this plan? [y/N] ").strip().lower()
|
|
551
|
+
if confirm not in {"y", "yes"}:
|
|
552
|
+
print("Aborted.")
|
|
553
|
+
return
|
|
554
|
+
except EOFError:
|
|
555
|
+
print("Aborted.")
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
# ── Step 7: Execute with full code output ────────────────
|
|
559
|
+
print("\n--- Executing ---\n")
|
|
560
|
+
|
|
561
|
+
execute_prompt = (
|
|
562
|
+
f"You are an expert coding AI agent. Execute this plan NOW.\n\n"
|
|
563
|
+
f"PROJECT CONTEXT:\n{context}\n\n"
|
|
564
|
+
f"PLAN TO EXECUTE:\n{plan}\n\n"
|
|
565
|
+
f"For each step, output the COMPLETE file contents or code changes.\n"
|
|
566
|
+
f"Format each file change as:\n"
|
|
567
|
+
f"```path/to/file.ext\n"
|
|
568
|
+
f"<complete file content or diff>\n"
|
|
569
|
+
f"```\n\n"
|
|
570
|
+
f"Rules:\n"
|
|
571
|
+
f"- Output COMPLETE, WORKING code — no placeholders, no '...', no truncation\n"
|
|
572
|
+
f"- Include all imports, error handling, and type annotations\n"
|
|
573
|
+
f"- Match the existing code style of the project\n"
|
|
574
|
+
f"- If creating new files, include all necessary boilerplate\n"
|
|
575
|
+
f"- If modifying existing files, show the complete modified version"
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
exec_payload = {
|
|
579
|
+
"model_id": model,
|
|
580
|
+
"messages": [{"role": "user", "content": execute_prompt}],
|
|
581
|
+
"temperature": 0.1,
|
|
582
|
+
"max_tokens": args.max_tokens,
|
|
583
|
+
"stream": True,
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
_stream_chat(f"{args.host}/chat", exec_payload)
|
|
587
|
+
print("\n\nTask complete.")
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
# ── Helpers ────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _is_github_repo(url: str) -> bool:
|
|
594
|
+
"""Check if URL points to a GitHub repo (not a direct file)."""
|
|
595
|
+
if not ("github.com" in url or "/" in url):
|
|
596
|
+
return False
|
|
597
|
+
# Direct file downloads have extensions
|
|
598
|
+
path = url.rstrip("/").split("/")[-1] if "/" in url else url
|
|
599
|
+
return "." not in path or path.endswith(".git")
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _runtime_ext(runtime: str) -> str:
|
|
603
|
+
return {"llama_cpp": "gguf", "onnx": "onnx"}.get(runtime, "safetensors")
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
# ── Argument parser ────────────────────────────────────────────
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _fetch_model_names(host: str = DEFAULT_HOST) -> list[str]:
|
|
610
|
+
"""Fetch available model names from the backend for shell completion."""
|
|
611
|
+
try:
|
|
612
|
+
resp = requests.get(f"{host}/models/names", timeout=2)
|
|
613
|
+
resp.raise_for_status()
|
|
614
|
+
return resp.json().get("names", [])
|
|
615
|
+
except Exception:
|
|
616
|
+
return []
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def cmd_completion(args):
|
|
620
|
+
"""Generate shell completion scripts."""
|
|
621
|
+
shell = args.shell
|
|
622
|
+
if shell == "bash":
|
|
623
|
+
print(_BASH_COMPLETION)
|
|
624
|
+
elif shell == "zsh":
|
|
625
|
+
print(_ZSH_COMPLETION)
|
|
626
|
+
elif shell == "fish":
|
|
627
|
+
print(_FISH_COMPLETION)
|
|
628
|
+
elif shell == "names":
|
|
629
|
+
# Raw model name list — used by completion scripts
|
|
630
|
+
for name in _fetch_model_names(args.host):
|
|
631
|
+
print(name)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
_BASH_COMPLETION = r'''
|
|
635
|
+
# bash completion for ai CLI — add to ~/.bashrc:
|
|
636
|
+
# eval "$(ai completion bash)"
|
|
637
|
+
|
|
638
|
+
_ai_complete() {
|
|
639
|
+
local cur prev words cword
|
|
640
|
+
_init_completion || return
|
|
641
|
+
|
|
642
|
+
case "${words[1]}" in
|
|
643
|
+
model)
|
|
644
|
+
case "${words[2]}" in
|
|
645
|
+
use|versions|update)
|
|
646
|
+
COMPREPLY=( $(compgen -W "$(ai completion names 2>/dev/null)" -- "$cur") )
|
|
647
|
+
return ;;
|
|
648
|
+
list|download|add) return ;;
|
|
649
|
+
esac
|
|
650
|
+
COMPREPLY=( $(compgen -W "list versions download add use update" -- "$cur") )
|
|
651
|
+
return ;;
|
|
652
|
+
source)
|
|
653
|
+
case "${words[2]}" in
|
|
654
|
+
remove)
|
|
655
|
+
COMPREPLY=( $(compgen -W "$(ai completion names 2>/dev/null)" -- "$cur") )
|
|
656
|
+
return ;;
|
|
657
|
+
esac
|
|
658
|
+
COMPREPLY=( $(compgen -W "list add remove" -- "$cur") )
|
|
659
|
+
return ;;
|
|
660
|
+
chat|ask)
|
|
661
|
+
case "$prev" in
|
|
662
|
+
--model)
|
|
663
|
+
COMPREPLY=( $(compgen -W "$(ai completion names 2>/dev/null)" -- "$cur") )
|
|
664
|
+
return ;;
|
|
665
|
+
esac
|
|
666
|
+
return ;;
|
|
667
|
+
run)
|
|
668
|
+
if [ $cword -eq 2 ]; then
|
|
669
|
+
COMPREPLY=( $(compgen -W "$(ai completion names 2>/dev/null)" -- "$cur") )
|
|
670
|
+
return
|
|
671
|
+
fi ;;
|
|
672
|
+
completion) return ;;
|
|
673
|
+
esac
|
|
674
|
+
COMPREPLY=( $(compgen -W "health hardware model source chat ask run completion" -- "$cur") )
|
|
675
|
+
}
|
|
676
|
+
complete -F _ai_complete ai
|
|
677
|
+
'''.strip()
|
|
678
|
+
|
|
679
|
+
_ZSH_COMPLETION = r'''
|
|
680
|
+
# zsh completion for ai CLI — add to ~/.zshrc:
|
|
681
|
+
# eval "$(ai completion zsh)"
|
|
682
|
+
|
|
683
|
+
_ai() {
|
|
684
|
+
local -a model_names
|
|
685
|
+
_get_model_names() { model_names=(${(f)"$(ai completion names 2>/dev/null)"}) }
|
|
686
|
+
|
|
687
|
+
_arguments -C '1:command:(health hardware model source chat ask run completion)' '*::arg:->args'
|
|
688
|
+
|
|
689
|
+
case $words[1] in
|
|
690
|
+
model)
|
|
691
|
+
_arguments -C '1:subcommand:(list versions download add use update)' '*::arg:->margs'
|
|
692
|
+
case $words[1] in
|
|
693
|
+
use|versions|update)
|
|
694
|
+
_get_model_names
|
|
695
|
+
_describe 'model' model_names ;;
|
|
696
|
+
esac ;;
|
|
697
|
+
source)
|
|
698
|
+
_arguments -C '1:subcommand:(list add remove)' '*::arg:->sargs'
|
|
699
|
+
case $words[1] in
|
|
700
|
+
remove) _get_model_names; _describe 'model' model_names ;;
|
|
701
|
+
esac ;;
|
|
702
|
+
chat|ask)
|
|
703
|
+
_arguments '--model[Model ID]:model:->mname'
|
|
704
|
+
if [[ $state == mname ]]; then
|
|
705
|
+
_get_model_names
|
|
706
|
+
_describe 'model' model_names
|
|
707
|
+
fi ;;
|
|
708
|
+
run)
|
|
709
|
+
_get_model_names; _describe 'model' model_names ;;
|
|
710
|
+
esac
|
|
711
|
+
}
|
|
712
|
+
compdef _ai ai
|
|
713
|
+
'''.strip()
|
|
714
|
+
|
|
715
|
+
_FISH_COMPLETION = r'''
|
|
716
|
+
# fish completion for ai CLI — add to ~/.config/fish/completions/ai.fish:
|
|
717
|
+
# ai completion fish > ~/.config/fish/completions/ai.fish
|
|
718
|
+
|
|
719
|
+
function __ai_model_names
|
|
720
|
+
ai completion names 2>/dev/null
|
|
721
|
+
end
|
|
722
|
+
complete -c ai -n "__fish_use_subcommand" -a "health hardware model source chat ask run completion"
|
|
723
|
+
complete -c ai -n "__fish_seen_subcommand_from model" -a "list versions download add use update"
|
|
724
|
+
complete -c ai -n "__fish_seen_subcommand_from model; and __fish_seen_subcommand_from use versions update" -a "(__ai_model_names)"
|
|
725
|
+
complete -c ai -n "__fish_seen_subcommand_from source" -a "list add remove"
|
|
726
|
+
complete -c ai -n "__fish_seen_subcommand_from source; and __fish_seen_subcommand_from remove" -a "(__ai_model_names)"
|
|
727
|
+
complete -c ai -n "__fish_seen_subcommand_from chat ask" -l model -a "(__ai_model_names)"
|
|
728
|
+
complete -c ai -n "__fish_seen_subcommand_from run" -a "(__ai_model_names)"
|
|
729
|
+
'''.strip()
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def build_argparser() -> argparse.ArgumentParser:
|
|
733
|
+
parser = argparse.ArgumentParser(
|
|
734
|
+
prog="ai",
|
|
735
|
+
description="Self-hosted local AI platform CLI — no external APIs, fully local",
|
|
736
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
737
|
+
epilog="""examples:
|
|
738
|
+
ai model list
|
|
739
|
+
ai model download --model-id tinyllama --url owner/repo --runtime llama_cpp
|
|
740
|
+
ai model download --model-id phi3 --url https://github.com/owner/repo/releases/download/v1/model.gguf --runtime llama_cpp
|
|
741
|
+
ai model add --model-id my-model --target ./model.gguf --runtime llama_cpp
|
|
742
|
+
ai model use tinyllama:v2
|
|
743
|
+
ai model update
|
|
744
|
+
ai source add --repo-url owner/repo --model-id tinyllama --runtime llama_cpp
|
|
745
|
+
ai source list
|
|
746
|
+
ai chat --model tinyllama
|
|
747
|
+
ai ask --model tinyllama --prompt "Explain quicksort"
|
|
748
|
+
ai run tinyllama
|
|
749
|
+
""",
|
|
750
|
+
)
|
|
751
|
+
parser.add_argument("--host", default=DEFAULT_HOST, help="Backend base URL")
|
|
752
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
753
|
+
|
|
754
|
+
# health
|
|
755
|
+
sub.add_parser("health", help="Check backend health")
|
|
756
|
+
|
|
757
|
+
# hardware
|
|
758
|
+
sub.add_parser("hardware", help="Show hardware info")
|
|
759
|
+
|
|
760
|
+
# ── model subcommands ──────────────────────────────────────
|
|
761
|
+
|
|
762
|
+
model_parser = sub.add_parser("model", help="Model management")
|
|
763
|
+
model_sub = model_parser.add_subparsers(dest="model_command", required=True)
|
|
764
|
+
|
|
765
|
+
# model catalog
|
|
766
|
+
mc = model_sub.add_parser("catalog", help="Browse free models available from GitHub")
|
|
767
|
+
mc.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
768
|
+
mc.add_argument("--use-case", choices=["coding", "chat", "general", "completion"],
|
|
769
|
+
help="Filter by use case")
|
|
770
|
+
|
|
771
|
+
# model list
|
|
772
|
+
ml = model_sub.add_parser("list", help="List installed models")
|
|
773
|
+
ml.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
774
|
+
|
|
775
|
+
# model versions
|
|
776
|
+
mv = model_sub.add_parser("versions", help="List versions of a model")
|
|
777
|
+
mv.add_argument("model_id", help="Model ID")
|
|
778
|
+
mv.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
779
|
+
|
|
780
|
+
# model download
|
|
781
|
+
md = model_sub.add_parser("download", help="Download from GitHub repo or direct URL")
|
|
782
|
+
md.add_argument("--model-id", required=True, help="Model identifier")
|
|
783
|
+
md.add_argument("--url", required=True, help="GitHub repo (owner/repo) or direct asset URL")
|
|
784
|
+
md.add_argument("--runtime", required=True, choices=["llama_cpp", "transformers", "vllm", "onnx"])
|
|
785
|
+
md.add_argument("--filename", help="Override filename")
|
|
786
|
+
md.add_argument("--sha256", help="Expected checksum")
|
|
787
|
+
md.add_argument("--license", help="License type")
|
|
788
|
+
md.add_argument("--size-gb", type=float, help="Expected size in GB")
|
|
789
|
+
md.add_argument("--format", choices=["gguf", "safetensors", "onnx", "pytorch"])
|
|
790
|
+
md.add_argument("--version-tag", help="Version tag for this download")
|
|
791
|
+
md.add_argument("--asset-pattern", help="Glob pattern for asset discovery (default: *.gguf)")
|
|
792
|
+
|
|
793
|
+
# model add (smart import)
|
|
794
|
+
ma = model_sub.add_parser("add", help="Import from local path or GitHub URL")
|
|
795
|
+
ma.add_argument("--model-id", required=True, help="Model identifier")
|
|
796
|
+
ma.add_argument("--target", required=True, help="Local path or GitHub URL")
|
|
797
|
+
ma.add_argument("--runtime", required=True, choices=["llama_cpp", "transformers", "vllm", "onnx"])
|
|
798
|
+
ma.add_argument("--license", help="License type")
|
|
799
|
+
ma.add_argument("--format", choices=["gguf", "safetensors", "onnx", "pytorch"])
|
|
800
|
+
|
|
801
|
+
# model use
|
|
802
|
+
mu = model_sub.add_parser("use", help="Load a model (optionally set version)")
|
|
803
|
+
mu.add_argument("model_version", help="model_id or model_id:vN")
|
|
804
|
+
mu.add_argument("--threads", type=int, help="CPU threads for inference")
|
|
805
|
+
|
|
806
|
+
# model update
|
|
807
|
+
mup = model_sub.add_parser("update", help="Check tracked sources for new versions")
|
|
808
|
+
mup.add_argument("model_id", nargs="?", help="Specific model to update (default: all)")
|
|
809
|
+
|
|
810
|
+
# ── source subcommands ─────────────────────────────────────
|
|
811
|
+
|
|
812
|
+
source_parser = sub.add_parser("source", help="Manage tracked GitHub sources")
|
|
813
|
+
source_sub = source_parser.add_subparsers(dest="source_command", required=True)
|
|
814
|
+
|
|
815
|
+
sl = source_sub.add_parser("list", help="List tracked sources")
|
|
816
|
+
sl.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
817
|
+
|
|
818
|
+
sa = source_sub.add_parser("add", help="Track a GitHub repository")
|
|
819
|
+
sa.add_argument("--repo-url", required=True, help="GitHub repo URL or owner/repo")
|
|
820
|
+
sa.add_argument("--model-id", required=True, help="Model identifier")
|
|
821
|
+
sa.add_argument("--runtime", default="llama_cpp", choices=["llama_cpp", "transformers", "vllm", "onnx"])
|
|
822
|
+
sa.add_argument("--asset-pattern", default="*.gguf", help="Glob pattern for model assets")
|
|
823
|
+
sa.add_argument("--license", help="License type")
|
|
824
|
+
|
|
825
|
+
sr = source_sub.add_parser("remove", help="Stop tracking a source")
|
|
826
|
+
sr.add_argument("model_id", help="Model ID to untrack")
|
|
827
|
+
|
|
828
|
+
# ── chat / ask / run ───────────────────────────────────────
|
|
829
|
+
|
|
830
|
+
chat_p = sub.add_parser("chat", help="Interactive chat session")
|
|
831
|
+
chat_p.add_argument("--model", required=True, help="Model ID")
|
|
832
|
+
chat_p.add_argument("--temperature", type=float, default=0.3)
|
|
833
|
+
chat_p.add_argument("--max-tokens", type=int, default=512)
|
|
834
|
+
chat_p.add_argument("--conversation-id", help="Resume a conversation")
|
|
835
|
+
|
|
836
|
+
ask_p = sub.add_parser("ask", help="One-shot prompt")
|
|
837
|
+
ask_p.add_argument("--model", required=True, help="Model ID")
|
|
838
|
+
ask_p.add_argument("--prompt", required=True, help="Prompt text")
|
|
839
|
+
ask_p.add_argument("--temperature", type=float, default=0.3)
|
|
840
|
+
ask_p.add_argument("--max-tokens", type=int, default=512)
|
|
841
|
+
|
|
842
|
+
run_p = sub.add_parser("run", help="Load model and start chatting")
|
|
843
|
+
run_p.add_argument("model_id", help="Model to load")
|
|
844
|
+
run_p.add_argument("--threads", type=int, help="CPU threads")
|
|
845
|
+
|
|
846
|
+
# task (AI agent)
|
|
847
|
+
task_p = sub.add_parser("task", help="AI agent: analyze, plan, and execute a coding task")
|
|
848
|
+
task_p.add_argument("prompt", help="Task description (natural language)")
|
|
849
|
+
task_p.add_argument("--model", required=True, help="Model ID to use")
|
|
850
|
+
task_p.add_argument("--auto", action="store_true", help="Execute without confirmation")
|
|
851
|
+
task_p.add_argument("--max-tokens", type=int, default=2048, help="Max tokens per response")
|
|
852
|
+
|
|
853
|
+
# completion
|
|
854
|
+
comp_p = sub.add_parser("completion", help="Generate shell completion scripts")
|
|
855
|
+
comp_p.add_argument("shell", choices=["bash", "zsh", "fish", "names"],
|
|
856
|
+
help="Shell type (or 'names' for raw model list)")
|
|
857
|
+
|
|
858
|
+
return parser
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
COMMAND_DISPATCH = {
|
|
862
|
+
"health": cmd_health,
|
|
863
|
+
"hardware": cmd_hardware,
|
|
864
|
+
"chat": cmd_chat,
|
|
865
|
+
"ask": cmd_ask,
|
|
866
|
+
"run": cmd_run,
|
|
867
|
+
"task": cmd_task,
|
|
868
|
+
"completion": cmd_completion,
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
MODEL_DISPATCH = {
|
|
872
|
+
"catalog": cmd_model_catalog,
|
|
873
|
+
"list": cmd_model_list,
|
|
874
|
+
"versions": cmd_model_versions,
|
|
875
|
+
"download": cmd_model_download,
|
|
876
|
+
"add": cmd_model_add,
|
|
877
|
+
"use": cmd_model_use,
|
|
878
|
+
"update": cmd_model_update,
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
SOURCE_DISPATCH = {
|
|
882
|
+
"list": cmd_source_list,
|
|
883
|
+
"add": cmd_source_add,
|
|
884
|
+
"remove": cmd_source_remove,
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def main() -> int:
|
|
889
|
+
args = build_argparser().parse_args()
|
|
890
|
+
try:
|
|
891
|
+
if args.command == "model":
|
|
892
|
+
handler = MODEL_DISPATCH.get(args.model_command)
|
|
893
|
+
if handler:
|
|
894
|
+
handler(args)
|
|
895
|
+
return 0
|
|
896
|
+
elif args.command == "source":
|
|
897
|
+
handler = SOURCE_DISPATCH.get(args.source_command)
|
|
898
|
+
if handler:
|
|
899
|
+
handler(args)
|
|
900
|
+
return 0
|
|
901
|
+
else:
|
|
902
|
+
handler = COMMAND_DISPATCH.get(args.command)
|
|
903
|
+
if handler:
|
|
904
|
+
handler(args)
|
|
905
|
+
return 0
|
|
906
|
+
print(f"Unknown command: {args.command}", file=sys.stderr)
|
|
907
|
+
return 1
|
|
908
|
+
except requests.RequestException as exc:
|
|
909
|
+
print(f"Request failed: {exc}", file=sys.stderr)
|
|
910
|
+
return 2
|
|
911
|
+
except KeyboardInterrupt:
|
|
912
|
+
return 130
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
if __name__ == "__main__":
|
|
916
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ai_cli.py
|
|
2
|
+
pyproject.toml
|
|
3
|
+
claude_ai_clone_client.egg-info/PKG-INFO
|
|
4
|
+
claude_ai_clone_client.egg-info/SOURCES.txt
|
|
5
|
+
claude_ai_clone_client.egg-info/dependency_links.txt
|
|
6
|
+
claude_ai_clone_client.egg-info/entry_points.txt
|
|
7
|
+
claude_ai_clone_client.egg-info/requires.txt
|
|
8
|
+
claude_ai_clone_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.32.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ai_cli
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "claude-ai-clone-client"
|
|
7
|
+
version = "3.0.0"
|
|
8
|
+
description = "CLI client for the local self-hosted AI platform — no external APIs, fully local"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
dependencies = ["requests>=2.32.3"]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
ai = "ai_cli:main"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools]
|
|
17
|
+
py-modules = ["ai_cli"]
|