agentskill-toolkit 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.
- agentskill_kit/__init__.py +5 -0
- agentskill_kit/__main__.py +6 -0
- agentskill_kit/cli.py +795 -0
- agentskill_kit/config.py +104 -0
- agentskill_kit/evalrun.py +95 -0
- agentskill_kit/export.py +188 -0
- agentskill_kit/forge.py +346 -0
- agentskill_kit/i18n.py +107 -0
- agentskill_kit/install.py +212 -0
- agentskill_kit/lint.py +201 -0
- agentskill_kit/mine.py +291 -0
- agentskill_kit/mockmodel.py +124 -0
- agentskill_kit/providers.py +344 -0
- agentskill_kit/publish.py +96 -0
- agentskill_kit/registry.py +175 -0
- agentskill_kit/runner.py +214 -0
- agentskill_kit/sandbox.py +118 -0
- agentskill_kit/scaffold.py +90 -0
- agentskill_kit/scan.py +303 -0
- agentskill_kit/scoring.py +93 -0
- agentskill_kit/server.py +69 -0
- agentskill_kit/site.py +24 -0
- agentskill_kit/skillfile.py +378 -0
- agentskill_kit/templates/site.html +275 -0
- agentskill_kit/ui.py +111 -0
- agentskill_toolkit-0.1.0.dist-info/METADATA +314 -0
- agentskill_toolkit-0.1.0.dist-info/RECORD +31 -0
- agentskill_toolkit-0.1.0.dist-info/WHEEL +5 -0
- agentskill_toolkit-0.1.0.dist-info/entry_points.txt +3 -0
- agentskill_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentskill_toolkit-0.1.0.dist-info/top_level.txt +1 -0
agentskill_kit/cli.py
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
"""Command line interface for agentskill-kit (``askit``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
12
|
+
|
|
13
|
+
from . import __version__, i18n, ui
|
|
14
|
+
from .config import (CONFIG_KEYS, DEFAULT_REGISTRY, DEFAULT_THRESHOLD, RAW_BASE, config_path, get_setting,
|
|
15
|
+
load_config, load_dotenv, set_setting)
|
|
16
|
+
from .i18n import t
|
|
17
|
+
from .skillfile import SkillError, discover_skills, load_skill, slugify
|
|
18
|
+
|
|
19
|
+
EXPORT_TARGET_CHOICES = None # filled lazily to keep import time low
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CliError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def _rel(path: Any) -> str:
|
|
31
|
+
try:
|
|
32
|
+
return os.path.relpath(str(path))
|
|
33
|
+
except ValueError:
|
|
34
|
+
return str(path)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _collect(paths: Sequence[str]) -> List[Path]:
|
|
38
|
+
"""Resolve CLI path arguments to skill directories."""
|
|
39
|
+
targets = list(paths)
|
|
40
|
+
if not targets:
|
|
41
|
+
targets = ["skills"] if Path("skills").is_dir() else ["."]
|
|
42
|
+
found: List[Path] = []
|
|
43
|
+
for p in targets:
|
|
44
|
+
base = Path(p)
|
|
45
|
+
if not base.exists():
|
|
46
|
+
raise CliError("not found: %s" % p)
|
|
47
|
+
for d in discover_skills(base):
|
|
48
|
+
if d not in found:
|
|
49
|
+
found.append(d)
|
|
50
|
+
if not found:
|
|
51
|
+
raise CliError(t("no_skills", path=", ".join(targets)))
|
|
52
|
+
return found
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _threshold(args: Any) -> float:
|
|
56
|
+
val = getattr(args, "threshold", None)
|
|
57
|
+
if val is None:
|
|
58
|
+
val = get_setting("threshold", DEFAULT_THRESHOLD)
|
|
59
|
+
try:
|
|
60
|
+
return float(val)
|
|
61
|
+
except (TypeError, ValueError):
|
|
62
|
+
return DEFAULT_THRESHOLD
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _add_llm_args(p: argparse.ArgumentParser) -> None:
|
|
66
|
+
p.add_argument("--provider", help="ollama, groq, gemini, openrouter, openai, anthropic, custom or mock")
|
|
67
|
+
p.add_argument("--model", help="model name (defaults depend on the provider)")
|
|
68
|
+
p.add_argument("--base-url", dest="base_url", help="override the API base URL")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _llm(args: Any):
|
|
72
|
+
from .providers import ProviderError, resolve_provider
|
|
73
|
+
try:
|
|
74
|
+
return resolve_provider(args.provider, args.model, args.base_url)
|
|
75
|
+
except ProviderError as exc:
|
|
76
|
+
raise CliError(str(exc))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _emit_json(data: Any) -> None:
|
|
80
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# commands
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def cmd_init(args) -> int:
|
|
88
|
+
from .scaffold import create_skill
|
|
89
|
+
tags = [x.strip() for x in (args.tags or "").split(",") if x.strip()]
|
|
90
|
+
try:
|
|
91
|
+
dest = create_skill(args.name, Path(args.dir), args.description or "", args.script, args.author or "",
|
|
92
|
+
tags, args.force)
|
|
93
|
+
except FileExistsError as exc:
|
|
94
|
+
print(ui.fail(t("exists", path=_rel(exc.args[0]))))
|
|
95
|
+
return 1
|
|
96
|
+
print(ui.ok(t("created", path=_rel(dest))))
|
|
97
|
+
print(" edit %s, then run: askit lint %s" % (_rel(dest / "SKILL.md"), _rel(dest)))
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_lint(args) -> int:
|
|
102
|
+
from .lint import format_github, lint_skill
|
|
103
|
+
skills = _collect(args.paths)
|
|
104
|
+
results = [lint_skill(p) for p in skills]
|
|
105
|
+
if args.format == "json":
|
|
106
|
+
_emit_json([r.to_dict() for r in results])
|
|
107
|
+
elif args.format == "github":
|
|
108
|
+
for r in results:
|
|
109
|
+
for line in format_github(r):
|
|
110
|
+
print(line)
|
|
111
|
+
else:
|
|
112
|
+
print(ui.heading(t("lint_title")))
|
|
113
|
+
for r in results:
|
|
114
|
+
tone = "green" if not r.errors else "red"
|
|
115
|
+
print("%s %s" % (_rel(r.path), ui.paint("%s %d/100" % (t("score"), r.score), tone)))
|
|
116
|
+
if not r.issues:
|
|
117
|
+
print(" " + ui.ok(t("no_issues")))
|
|
118
|
+
for i in r.issues:
|
|
119
|
+
mark = {"error": ui.paint("error", "red", "bold"), "warning": ui.paint("warn ", "yellow"),
|
|
120
|
+
"info": ui.paint("info ", "gray")}[i.severity]
|
|
121
|
+
loc = ":%d" % i.line if i.line else ""
|
|
122
|
+
print(" %s %s %s%s %s" % (mark, i.code, i.file, loc, i.message))
|
|
123
|
+
if i.hint and i.severity != "info":
|
|
124
|
+
print(" " + ui.paint(i.hint, "dim"))
|
|
125
|
+
errors = sum(len(r.errors) for r in results)
|
|
126
|
+
warnings = sum(len(r.warnings) for r in results)
|
|
127
|
+
if args.format == "text":
|
|
128
|
+
print(t("summary_lint", n=len(results), e=errors, w=warnings))
|
|
129
|
+
return 1 if errors or (args.strict and warnings) else 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cmd_scan(args) -> int:
|
|
133
|
+
from .scan import exceeds, scan_skill
|
|
134
|
+
skills = _collect(args.paths)
|
|
135
|
+
results = [scan_skill(p) for p in skills]
|
|
136
|
+
if args.format == "json":
|
|
137
|
+
_emit_json([r.to_dict() for r in results])
|
|
138
|
+
else:
|
|
139
|
+
print(ui.heading(t("scan_title")))
|
|
140
|
+
for r in results:
|
|
141
|
+
tone = {"clean": "green", "review": "yellow", "risky": "red"}[r.verdict]
|
|
142
|
+
print("%s %s" % (_rel(r.path), ui.paint("%s, safety %d/100" % (r.verdict, r.safety), tone)))
|
|
143
|
+
if not r.findings:
|
|
144
|
+
print(" " + ui.ok(t("no_findings")))
|
|
145
|
+
for f in r.findings:
|
|
146
|
+
sev = {"high": ui.paint("high ", "red", "bold"), "medium": ui.paint("medium", "yellow"),
|
|
147
|
+
"low": ui.paint("low ", "gray")}[f.severity]
|
|
148
|
+
loc = ":%d" % f.line if f.line else ""
|
|
149
|
+
print(" %s %s %s%s %s" % (sev, f.rule, f.file, loc, f.message))
|
|
150
|
+
if f.snippet:
|
|
151
|
+
print(" " + ui.paint(ui.truncate(f.snippet, 90), "dim"))
|
|
152
|
+
print(t("summary_scan", n=len(results), h=sum(r.count("high") for r in results),
|
|
153
|
+
m=sum(r.count("medium") for r in results), l=sum(r.count("low") for r in results)))
|
|
154
|
+
return 1 if any(exceeds(r, args.fail_on) for r in results) else 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def cmd_test(args) -> int:
|
|
158
|
+
from .runner import run_skill_tests
|
|
159
|
+
skills = _collect(args.paths)
|
|
160
|
+
reports = [run_skill_tests(p, timeout=args.timeout, allow_unsafe=args.allow_unsafe) for p in skills]
|
|
161
|
+
if args.format == "json":
|
|
162
|
+
_emit_json([r.to_dict() for r in reports])
|
|
163
|
+
else:
|
|
164
|
+
print(ui.heading(t("test_title")))
|
|
165
|
+
for p, r in zip(skills, reports):
|
|
166
|
+
if r.refused:
|
|
167
|
+
print("%s %s" % (_rel(p), ui.paint("refused", "red")))
|
|
168
|
+
print(" " + ui.fail(r.refused))
|
|
169
|
+
continue
|
|
170
|
+
if not r.has_tests:
|
|
171
|
+
print("%s %s" % (_rel(p), ui.paint(t("no_tests"), "gray")))
|
|
172
|
+
continue
|
|
173
|
+
tone = "green" if r.passed == r.total else "red"
|
|
174
|
+
print("%s %s" % (_rel(p), ui.paint("%d/%d" % (r.passed, r.total), tone)))
|
|
175
|
+
for c in r.cases:
|
|
176
|
+
line = "%s [%s]" % (c.name, c.kind)
|
|
177
|
+
print(" " + (ui.ok(line) if c.passed else ui.fail("%s: %s" % (line, c.detail))))
|
|
178
|
+
tested = [r for r in reports if r.has_tests]
|
|
179
|
+
print(t("summary_test", p=sum(r.passed for r in tested), t=sum(r.total for r in tested), n=len(tested)))
|
|
180
|
+
bad = any(r.refused or (r.has_tests and r.passed < r.total) for r in reports)
|
|
181
|
+
return 1 if bad else 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def cmd_score(args) -> int:
|
|
185
|
+
from .publish import check_gate
|
|
186
|
+
from .scoring import evaluate_skill
|
|
187
|
+
skills = _collect(args.paths)
|
|
188
|
+
thr = _threshold(args)
|
|
189
|
+
qualities = [evaluate_skill(p, run_tests=not args.no_tests) for p in skills]
|
|
190
|
+
if args.format == "json":
|
|
191
|
+
_emit_json([q.to_dict() for q in qualities])
|
|
192
|
+
else:
|
|
193
|
+
rows = []
|
|
194
|
+
for p, q in zip(skills, qualities):
|
|
195
|
+
tests = ("%d/%d" % (q.tests.passed, q.tests.total)) if q.tests and q.tests.has_tests else "-"
|
|
196
|
+
reasons = check_gate(q, thr)
|
|
197
|
+
rows.append((_rel(p), "%.2f" % q.score, q.grade, str(len(q.lint.errors)) + "/" + str(len(q.lint.warnings)),
|
|
198
|
+
str(q.scan.safety) if q.scan else "-", tests,
|
|
199
|
+
ui.paint("pass", "green") if not reasons else ui.paint("fail", "red")))
|
|
200
|
+
print(ui.table(rows, ["skill", "score", "grade", "lint e/w", "safety", "tests", "gate"]))
|
|
201
|
+
print(t("threshold_note", threshold=thr))
|
|
202
|
+
return 1 if any(check_gate(q, thr) for q in qualities) else 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def cmd_forge(args) -> int:
|
|
206
|
+
from .forge import ForgeError, forge
|
|
207
|
+
task = args.task or ""
|
|
208
|
+
context = ""
|
|
209
|
+
if args.from_file:
|
|
210
|
+
fp = Path(args.from_file)
|
|
211
|
+
if not fp.is_file():
|
|
212
|
+
raise CliError("not found: %s" % args.from_file)
|
|
213
|
+
context = fp.read_text(encoding="utf-8", errors="replace")
|
|
214
|
+
if not task:
|
|
215
|
+
task = "Turn the source material into a reusable skill."
|
|
216
|
+
if not task.strip():
|
|
217
|
+
raise CliError("give a task: askit forge --task \"summarize meeting notes into action items\"")
|
|
218
|
+
llm = _llm(args)
|
|
219
|
+
print(ui.info(t("forge_start", provider=llm.name, model=llm.model)))
|
|
220
|
+
if not llm.is_free:
|
|
221
|
+
print(ui.warn("this provider is paid; usage is billed to your account"))
|
|
222
|
+
out = args.out or get_setting("output_dir") or "skills"
|
|
223
|
+
thr = _threshold(args)
|
|
224
|
+
rounds = args.rounds or int(get_setting("max_rounds", 3))
|
|
225
|
+
try:
|
|
226
|
+
res = forge(task, llm, out, thr, rounds, args.force, args.dry_run, context, args.name or "",
|
|
227
|
+
not args.no_check, log=lambda m: print(ui.info(m)))
|
|
228
|
+
except ForgeError as exc:
|
|
229
|
+
raise CliError(str(exc))
|
|
230
|
+
q = res.quality
|
|
231
|
+
if q:
|
|
232
|
+
print("%s: score %.2f (%s) after %d round(s)" % (res.name, q.score, q.grade, res.rounds))
|
|
233
|
+
if res.written:
|
|
234
|
+
if not res.accepted:
|
|
235
|
+
print(ui.warn(t("forge_below", score="%.2f" % (q.score if q else 0), threshold=thr)))
|
|
236
|
+
print(ui.ok(t("forge_done", path=_rel(res.path), score="%.2f" % (q.score if q else 0))))
|
|
237
|
+
print(" next: askit test %s askit export %s --target claude" % (_rel(res.path), _rel(res.path)))
|
|
238
|
+
return 0
|
|
239
|
+
if res.accepted and args.dry_run:
|
|
240
|
+
print(ui.ok("dry run: skill would be written to %s" % _rel(Path(out) / res.name)))
|
|
241
|
+
if args.show:
|
|
242
|
+
for path, content in res.files.items():
|
|
243
|
+
print("\n--- %s ---\n%s" % (path, content))
|
|
244
|
+
return 0
|
|
245
|
+
print(ui.fail(t("forge_rejected", score="%.2f" % (q.score if q else 0), threshold=thr)))
|
|
246
|
+
return 1
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def cmd_eval(args) -> int:
|
|
250
|
+
from .evalrun import run_evals
|
|
251
|
+
skills = _collect([args.path])
|
|
252
|
+
llm = _llm(args)
|
|
253
|
+
rc = 0
|
|
254
|
+
for p in skills:
|
|
255
|
+
rep = run_evals(p, llm, args.baseline, args.limit)
|
|
256
|
+
if not rep.has_evals:
|
|
257
|
+
print("%s %s" % (_rel(p), ui.paint("no evals/evals.json", "gray")))
|
|
258
|
+
continue
|
|
259
|
+
base = rep.baseline_passed
|
|
260
|
+
extra = "" if base is None else " without skill: %d/%d" % (base, len(rep.cases))
|
|
261
|
+
print("%s %d/%d with skill%s" % (_rel(p), rep.passed, len(rep.cases), extra))
|
|
262
|
+
for c in rep.cases:
|
|
263
|
+
line = c.name + ("" if c.baseline_passed is None else " (baseline %s)" % ("pass" if c.baseline_passed else "fail"))
|
|
264
|
+
print(" " + (ui.ok(line) if c.passed else ui.fail("%s: %s" % (line, c.detail))))
|
|
265
|
+
if rep.passed < len(rep.cases):
|
|
266
|
+
rc = 1
|
|
267
|
+
return rc
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_mine(args) -> int:
|
|
271
|
+
from . import mine
|
|
272
|
+
files = [Path(f) for f in args.file] if args.file else None
|
|
273
|
+
if args.stdin:
|
|
274
|
+
cmds = mine.parse_history(sys.stdin.read())
|
|
275
|
+
else:
|
|
276
|
+
if not files and not mine.default_history_files():
|
|
277
|
+
raise CliError("no shell history found. Pass one with --file, or pipe it with --stdin")
|
|
278
|
+
cmds = mine.read_history(files)
|
|
279
|
+
cands = mine.find_candidates(cmds, args.min_count, args.max_len, args.limit)
|
|
280
|
+
if args.format == "json":
|
|
281
|
+
_emit_json([{"name": c.name, "kind": c.kind, "count": c.count, "steps": c.template,
|
|
282
|
+
"examples": c.examples, "score": round(c.score, 2)} for c in cands])
|
|
283
|
+
return 0
|
|
284
|
+
print(ui.info("read %d commands" % len(cmds)))
|
|
285
|
+
if not cands:
|
|
286
|
+
print(t("mine_none"))
|
|
287
|
+
return 0
|
|
288
|
+
print(t("mine_found", n=len(cands)))
|
|
289
|
+
for i, c in enumerate(cands, 1):
|
|
290
|
+
print("\n%s %s (%dx)" % (ui.paint("#%d" % i, "bold"), ui.paint(c.name, "cyan"), c.count))
|
|
291
|
+
for step in c.template:
|
|
292
|
+
print(" " + step)
|
|
293
|
+
if args.draft or args.forge:
|
|
294
|
+
n = args.draft or args.forge
|
|
295
|
+
if not 1 <= n <= len(cands):
|
|
296
|
+
raise CliError("pick a number between 1 and %d" % len(cands))
|
|
297
|
+
cand = cands[n - 1]
|
|
298
|
+
out = Path(args.out or "skills")
|
|
299
|
+
if args.draft:
|
|
300
|
+
files_map = mine.draft_skill_files(cand)
|
|
301
|
+
dest = out / slugify(cand.name)
|
|
302
|
+
if dest.exists() and not args.force:
|
|
303
|
+
raise CliError(t("exists", path=_rel(dest)))
|
|
304
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
(dest / "SKILL.md").write_text(files_map["SKILL.md"], encoding="utf-8")
|
|
306
|
+
print("\n" + ui.ok(t("created", path=_rel(dest))))
|
|
307
|
+
else:
|
|
308
|
+
from .forge import ForgeError, forge
|
|
309
|
+
llm = _llm(args)
|
|
310
|
+
task = ("Create a skill that automates this recurring workflow found in shell history (%d times). "
|
|
311
|
+
"Steps with placeholders:\n%s\nExample runs:\n%s" % (
|
|
312
|
+
cand.count, "\n".join(cand.template), "\n".join(cand.examples)))
|
|
313
|
+
try:
|
|
314
|
+
res = forge(task, llm, out, _threshold(args), 3, args.force, False, "", cand.name,
|
|
315
|
+
True, log=lambda m: print(ui.info(m)))
|
|
316
|
+
except ForgeError as exc:
|
|
317
|
+
raise CliError(str(exc))
|
|
318
|
+
print(ui.ok(t("forge_done", path=_rel(res.path), score="%.2f" % res.quality.score)) if res.written
|
|
319
|
+
else ui.fail(t("forge_rejected", score="%.2f" % (res.quality.score if res.quality else 0),
|
|
320
|
+
threshold=_threshold(args))))
|
|
321
|
+
else:
|
|
322
|
+
print("\nWrite one as a skill: askit mine --draft 1 (no model needed)")
|
|
323
|
+
print("Or let a model refine: askit mine --forge 1")
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def cmd_export(args) -> int:
|
|
328
|
+
from .export import ExportError, TARGETS, export_skill
|
|
329
|
+
skills = _collect([args.path])
|
|
330
|
+
targets = list(TARGETS) if args.target == "all" else [args.target]
|
|
331
|
+
if args.global_ and args.target == "all":
|
|
332
|
+
targets = [n for n, tg in TARGETS.items() if tg.global_]
|
|
333
|
+
base = Path(args.dest) if args.dest else None
|
|
334
|
+
for sk in skills:
|
|
335
|
+
name = load_skill(sk).name
|
|
336
|
+
for tg in targets:
|
|
337
|
+
try:
|
|
338
|
+
actions = export_skill(sk, tg, base, args.global_, args.dry_run)
|
|
339
|
+
except ExportError as exc:
|
|
340
|
+
raise CliError(str(exc))
|
|
341
|
+
print(ui.ok(t("exported", name=name, target=tg)) + (" (dry run)" if args.dry_run else ""))
|
|
342
|
+
for a in actions:
|
|
343
|
+
print(" " + a)
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def cmd_pack(args) -> int:
|
|
348
|
+
from .publish import pack
|
|
349
|
+
for sk in _collect([args.path]):
|
|
350
|
+
print(ui.ok(t("packed", path=_rel(pack(sk, args.out)))))
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def cmd_publish(args) -> int:
|
|
355
|
+
from .publish import PublishError, check_gate, pack, pr_commands, publish_to_registry, run_pr
|
|
356
|
+
from .scoring import evaluate_skill
|
|
357
|
+
skills = _collect([args.path])
|
|
358
|
+
thr = _threshold(args)
|
|
359
|
+
rc = 0
|
|
360
|
+
for sk in skills:
|
|
361
|
+
name = load_skill(sk).name
|
|
362
|
+
q = evaluate_skill(sk)
|
|
363
|
+
reasons = check_gate(q, thr)
|
|
364
|
+
print("%s: score %.2f (%s)" % (name, q.score, q.grade))
|
|
365
|
+
if reasons and not args.force:
|
|
366
|
+
print(ui.fail(t("gate_failed", reason="; ".join(reasons))))
|
|
367
|
+
rc = 1
|
|
368
|
+
continue
|
|
369
|
+
if args.registry_dir:
|
|
370
|
+
try:
|
|
371
|
+
info = publish_to_registry(sk, args.registry_dir, thr, args.force, args.dry_run)
|
|
372
|
+
except PublishError as exc:
|
|
373
|
+
raise CliError(str(exc))
|
|
374
|
+
for step in info["steps"]:
|
|
375
|
+
print(" " + step)
|
|
376
|
+
if args.open_pr and not args.dry_run:
|
|
377
|
+
ok_, msg = run_pr(name, args.registry_dir)
|
|
378
|
+
print(ui.ok(msg) if ok_ else ui.fail(msg))
|
|
379
|
+
rc = rc or (0 if ok_ else 1)
|
|
380
|
+
else:
|
|
381
|
+
print("\nOpen a pull request from the registry checkout:")
|
|
382
|
+
for c in pr_commands(name):
|
|
383
|
+
print(" " + c)
|
|
384
|
+
else:
|
|
385
|
+
archive = pack(sk, args.out)
|
|
386
|
+
print(ui.ok(t("published", name=name)))
|
|
387
|
+
print(ui.ok(t("packed", path=_rel(archive))))
|
|
388
|
+
print("\nShare it:")
|
|
389
|
+
print(" - Push the folder to a GitHub repo, then others run: askit install github:OWNER/REPO/%s" % _rel(sk).replace("\\", "/"))
|
|
390
|
+
print(" - Or add it to the community registry: fork https://github.com/Lethe044/agentskill-kit,")
|
|
391
|
+
print(" then run: askit publish %s --registry-dir PATH_TO_YOUR_FORK" % _rel(sk))
|
|
392
|
+
return rc
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _registry_source(args) -> str:
|
|
396
|
+
return getattr(args, "registry", None) or get_setting("registry") or DEFAULT_REGISTRY
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def cmd_search(args) -> int:
|
|
400
|
+
from . import registry
|
|
401
|
+
try:
|
|
402
|
+
index = registry.load_index(_registry_source(args))
|
|
403
|
+
except SkillError as exc:
|
|
404
|
+
raise CliError(str(exc))
|
|
405
|
+
hits = registry.search(index, " ".join(args.query), args.tag or "", args.limit)
|
|
406
|
+
if args.format == "json":
|
|
407
|
+
_emit_json(hits)
|
|
408
|
+
return 0
|
|
409
|
+
if not hits:
|
|
410
|
+
print(t("no_results"))
|
|
411
|
+
return 1
|
|
412
|
+
rows = [(s["name"], "%s %.2f" % (s.get("grade", "?"), s.get("score", 0)),
|
|
413
|
+
"yes" if s.get("tested") else "no", ui.truncate(s.get("description", ""), 70)) for s in hits]
|
|
414
|
+
print(ui.table(rows, ["name", "quality", "tested", "description"]))
|
|
415
|
+
print("\ninstall: askit install %s" % hits[0]["name"])
|
|
416
|
+
return 0
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def cmd_install(args) -> int:
|
|
420
|
+
from .install import InstallError, install
|
|
421
|
+
base = Path(args.dest) if args.dest else None
|
|
422
|
+
try:
|
|
423
|
+
res = install(args.source, args.target, base, args.global_, args.registry, args.force, args.dry_run)
|
|
424
|
+
except InstallError as exc:
|
|
425
|
+
raise CliError(str(exc))
|
|
426
|
+
print(ui.ok(t("installed", name=res["name"], target=args.target)) + (" (dry run)" if args.dry_run else ""))
|
|
427
|
+
for a in res["actions"]:
|
|
428
|
+
print(" " + a)
|
|
429
|
+
for n in res["notes"]:
|
|
430
|
+
print(ui.warn(n))
|
|
431
|
+
return 0
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def cmd_uninstall(args) -> int:
|
|
435
|
+
from .install import InstallError, uninstall
|
|
436
|
+
base = Path(args.dest) if args.dest else None
|
|
437
|
+
try:
|
|
438
|
+
done = uninstall(args.name, args.target, base, args.global_)
|
|
439
|
+
except InstallError as exc:
|
|
440
|
+
raise CliError(str(exc))
|
|
441
|
+
if not done:
|
|
442
|
+
print(ui.warn("%s was not found for target %s" % (args.name, args.target)))
|
|
443
|
+
return 1
|
|
444
|
+
print(ui.ok(t("uninstalled", name=args.name)))
|
|
445
|
+
for d in done:
|
|
446
|
+
print(" " + d)
|
|
447
|
+
return 0
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def cmd_list(args) -> int:
|
|
451
|
+
if args.installed:
|
|
452
|
+
from .install import load_ledger
|
|
453
|
+
rows = [(e["name"], e.get("version", ""), e["target"], "global" if e.get("global") else "project",
|
|
454
|
+
ui.truncate(e.get("source", ""), 50)) for e in load_ledger()["installed"]]
|
|
455
|
+
if not rows:
|
|
456
|
+
print("nothing installed through askit yet")
|
|
457
|
+
return 0
|
|
458
|
+
print(ui.table(rows, ["name", "version", "target", "scope", "source"]))
|
|
459
|
+
return 0
|
|
460
|
+
rows = []
|
|
461
|
+
for p in _collect(args.paths):
|
|
462
|
+
sk = load_skill(p)
|
|
463
|
+
rows.append((sk.name, sk.version, _rel(p), ui.truncate(sk.description, 60)))
|
|
464
|
+
print(ui.table(rows, ["name", "version", "path", "description"]))
|
|
465
|
+
return 0
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def cmd_registry(args) -> int:
|
|
469
|
+
from . import registry
|
|
470
|
+
from .site import write_site
|
|
471
|
+
roots = args.roots or (["skills"] if Path("skills").is_dir() else ["."])
|
|
472
|
+
for r in roots:
|
|
473
|
+
if not Path(r).exists():
|
|
474
|
+
raise CliError("not found: %s" % r)
|
|
475
|
+
base = Path(args.base).resolve() if getattr(args, "base", None) else Path.cwd()
|
|
476
|
+
if args.reg_cmd == "build":
|
|
477
|
+
index = registry.build_index(roots, base_url=args.base_url, include_body=not args.no_body,
|
|
478
|
+
relative_to=base, run_tests=not args.no_tests)
|
|
479
|
+
out = Path(args.out)
|
|
480
|
+
if args.check:
|
|
481
|
+
if registry.index_is_current(index, out):
|
|
482
|
+
print(ui.ok("%s is up to date (%d skills)" % (_rel(out), index["count"])))
|
|
483
|
+
return 0
|
|
484
|
+
print(ui.fail("%s is out of date. Run: askit registry build" % _rel(out)))
|
|
485
|
+
return 1
|
|
486
|
+
registry.write_index(index, out)
|
|
487
|
+
print(ui.ok("wrote %s (%d skills)" % (_rel(out), index["count"])))
|
|
488
|
+
return 0
|
|
489
|
+
index = registry.build_index(roots, base_url=args.base_url, include_body=True, relative_to=base,
|
|
490
|
+
run_tests=not args.no_tests)
|
|
491
|
+
page = write_site(index, args.out, args.title)
|
|
492
|
+
print(ui.ok("wrote %s (%d skills)" % (_rel(page), index["count"])))
|
|
493
|
+
return 0
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def cmd_serve(args) -> int:
|
|
497
|
+
from .server import default_roots, serve
|
|
498
|
+
roots = [Path(p) for p in args.paths] if args.paths else default_roots()
|
|
499
|
+
if not roots:
|
|
500
|
+
raise CliError("no skills folders found. Pass one: askit serve ./skills")
|
|
501
|
+
serve(roots, args.host, args.port, args.open, lambda url: print(ui.ok(t("serving", url=url))))
|
|
502
|
+
return 0
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def cmd_providers(args) -> int:
|
|
506
|
+
from .providers import PROVIDERS, ProviderError, provider_status, resolve_provider
|
|
507
|
+
if args.models:
|
|
508
|
+
try:
|
|
509
|
+
llm = resolve_provider(args.provider, None, None)
|
|
510
|
+
for m in llm.list_models():
|
|
511
|
+
print(m)
|
|
512
|
+
except (ProviderError, OSError) as exc:
|
|
513
|
+
raise CliError(str(exc))
|
|
514
|
+
return 0
|
|
515
|
+
rows = []
|
|
516
|
+
for r in provider_status():
|
|
517
|
+
cost = "free" if r["free"] else "paid"
|
|
518
|
+
setup = ("set " + r["key_env"]) if r["key_env"] and r["state"] == "no key" else ""
|
|
519
|
+
rows.append((r["name"], cost, r["state"], r["model"] or "(auto)", setup))
|
|
520
|
+
print(ui.table(rows, ["provider", "cost", "status", "default model", "setup"]))
|
|
521
|
+
print("\nFree options: ollama (local), groq, gemini, openrouter. Paid keys are optional.")
|
|
522
|
+
print("Pick one with --provider NAME or: askit config set provider NAME")
|
|
523
|
+
for name in ("groq", "gemini", "openrouter"):
|
|
524
|
+
spec = PROVIDERS[name]
|
|
525
|
+
print(" %-10s key: %s" % (name, spec.signup))
|
|
526
|
+
return 0
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def cmd_config(args) -> int:
|
|
530
|
+
if args.action == "show":
|
|
531
|
+
cfg = load_config()
|
|
532
|
+
print("file: %s" % config_path())
|
|
533
|
+
for k, desc in CONFIG_KEYS.items():
|
|
534
|
+
print(" %-11s %-18s %s" % (k, cfg.get(k, "(not set)"), ui.paint(desc, "dim")))
|
|
535
|
+
return 0
|
|
536
|
+
if args.action == "path":
|
|
537
|
+
print(config_path())
|
|
538
|
+
return 0
|
|
539
|
+
if not args.key or args.key not in CONFIG_KEYS:
|
|
540
|
+
raise CliError("choose a key: " + ", ".join(CONFIG_KEYS))
|
|
541
|
+
if args.action == "get":
|
|
542
|
+
print(get_setting(args.key, ""))
|
|
543
|
+
elif args.action == "set":
|
|
544
|
+
if args.value is None:
|
|
545
|
+
raise CliError("usage: askit config set KEY VALUE")
|
|
546
|
+
value: Any = args.value
|
|
547
|
+
if args.key in ("threshold",):
|
|
548
|
+
value = float(value)
|
|
549
|
+
if args.key in ("max_rounds",):
|
|
550
|
+
value = int(value)
|
|
551
|
+
set_setting(args.key, value)
|
|
552
|
+
print(ui.ok("%s = %s" % (args.key, value)))
|
|
553
|
+
else:
|
|
554
|
+
set_setting(args.key, None)
|
|
555
|
+
print(ui.ok("%s removed" % args.key))
|
|
556
|
+
return 0
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def cmd_doctor(args) -> int:
|
|
560
|
+
from . import mine
|
|
561
|
+
from .providers import provider_status
|
|
562
|
+
print(ui.heading(t("doctor_title")))
|
|
563
|
+
problems = 0
|
|
564
|
+
|
|
565
|
+
def row(good: bool, msg: str, bad_is_problem: bool = True) -> None:
|
|
566
|
+
nonlocal problems
|
|
567
|
+
print(" " + (ui.ok(msg) if good else (ui.fail(msg) if bad_is_problem else ui.warn(msg))))
|
|
568
|
+
if not good and bad_is_problem:
|
|
569
|
+
problems += 1
|
|
570
|
+
|
|
571
|
+
row(sys.version_info >= (3, 9), "Python %s" % sys.version.split()[0])
|
|
572
|
+
row(shutil.which("git") is not None, "git %s" % ("found" if shutil.which("git") else "not found"), False)
|
|
573
|
+
row(shutil.which("gh") is not None, "GitHub CLI %s (only needed for publish --open-pr)" % (
|
|
574
|
+
"found" if shutil.which("gh") else "not found"), False)
|
|
575
|
+
try:
|
|
576
|
+
from .config import home_dir
|
|
577
|
+
h = home_dir()
|
|
578
|
+
row(os.access(h, os.W_OK), "data directory %s" % h)
|
|
579
|
+
except OSError as exc:
|
|
580
|
+
row(False, "data directory: %s" % exc)
|
|
581
|
+
usable = [r for r in provider_status() if r["state"] in ("key found", "running", "configured")
|
|
582
|
+
or r["state"].startswith("running")]
|
|
583
|
+
row(bool(usable), "LLM providers ready: %s" % (", ".join(r["name"] for r in usable) or "none (try: askit providers)"), False)
|
|
584
|
+
hist = mine.default_history_files()
|
|
585
|
+
row(bool(hist), "shell history files: %d found" % len(hist), False)
|
|
586
|
+
if args.online:
|
|
587
|
+
from . import registry
|
|
588
|
+
src = _registry_source(args)
|
|
589
|
+
try:
|
|
590
|
+
idx = registry.load_index(src)
|
|
591
|
+
row(True, "registry reachable (%d skills)" % idx.get("count", len(idx.get("skills", []))))
|
|
592
|
+
except SkillError as exc:
|
|
593
|
+
row(False, str(exc))
|
|
594
|
+
print("\nagentskill-kit %s" % __version__)
|
|
595
|
+
return 1 if problems else 0
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
# ---------------------------------------------------------------------------
|
|
599
|
+
# parser
|
|
600
|
+
# ---------------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
603
|
+
from .export import TARGETS
|
|
604
|
+
|
|
605
|
+
p = argparse.ArgumentParser(
|
|
606
|
+
prog="askit", description="Create, lint, scan, test, share and export agent skills.",
|
|
607
|
+
epilog="Docs: https://github.com/Lethe044/agentskill-kit")
|
|
608
|
+
p.add_argument("--version", action="version", version="agentskill-kit %s" % __version__)
|
|
609
|
+
p.add_argument("--lang", choices=["en", "tr"], help="interface language")
|
|
610
|
+
p.add_argument("--no-color", action="store_true", help="disable coloured output")
|
|
611
|
+
sub = p.add_subparsers(dest="command", metavar="COMMAND")
|
|
612
|
+
|
|
613
|
+
def add(name: str, func, help_: str) -> argparse.ArgumentParser:
|
|
614
|
+
sp = sub.add_parser(name, help=help_, description=help_)
|
|
615
|
+
sp.set_defaults(func=func)
|
|
616
|
+
return sp
|
|
617
|
+
|
|
618
|
+
sp = add("init", cmd_init, "create a new skill from a template")
|
|
619
|
+
sp.add_argument("name")
|
|
620
|
+
sp.add_argument("--dir", default="skills", help="parent directory (default: skills)")
|
|
621
|
+
sp.add_argument("--description", "-d")
|
|
622
|
+
sp.add_argument("--script", action="store_true", help="include a Python script and tests")
|
|
623
|
+
sp.add_argument("--author")
|
|
624
|
+
sp.add_argument("--tags", help="comma separated tags")
|
|
625
|
+
sp.add_argument("--force", action="store_true")
|
|
626
|
+
|
|
627
|
+
sp = add("lint", cmd_lint, "check SKILL.md structure (offline, no model)")
|
|
628
|
+
sp.add_argument("paths", nargs="*")
|
|
629
|
+
sp.add_argument("--strict", action="store_true", help="treat warnings as errors")
|
|
630
|
+
sp.add_argument("--format", choices=["text", "json", "github"], default="text")
|
|
631
|
+
|
|
632
|
+
sp = add("scan", cmd_scan, "scan skills for security red flags")
|
|
633
|
+
sp.add_argument("paths", nargs="*")
|
|
634
|
+
sp.add_argument("--fail-on", choices=["high", "medium", "low", "never"], default="high")
|
|
635
|
+
sp.add_argument("--format", choices=["text", "json"], default="text")
|
|
636
|
+
|
|
637
|
+
sp = add("test", cmd_test, "run the tests that ship with a skill")
|
|
638
|
+
sp.add_argument("paths", nargs="*")
|
|
639
|
+
sp.add_argument("--timeout", type=float, default=20.0, help="seconds per case")
|
|
640
|
+
sp.add_argument("--allow-unsafe", action="store_true", help="run code even if the scanner flags it")
|
|
641
|
+
sp.add_argument("--format", choices=["text", "json"], default="text")
|
|
642
|
+
|
|
643
|
+
sp = add("score", cmd_score, "combine lint, scan and tests into a quality score")
|
|
644
|
+
sp.add_argument("paths", nargs="*")
|
|
645
|
+
sp.add_argument("--threshold", type=float)
|
|
646
|
+
sp.add_argument("--no-tests", action="store_true")
|
|
647
|
+
sp.add_argument("--format", choices=["text", "json"], default="text")
|
|
648
|
+
|
|
649
|
+
sp = add("forge", cmd_forge, "generate a tested skill from a task description")
|
|
650
|
+
sp.add_argument("--task", "-t", help="what the skill should do")
|
|
651
|
+
sp.add_argument("--from-file", help="notes, transcript or runbook to learn from")
|
|
652
|
+
sp.add_argument("--name", help="preferred skill name")
|
|
653
|
+
sp.add_argument("--out", help="output folder (default: skills)")
|
|
654
|
+
sp.add_argument("--threshold", type=float, help="minimum score to accept (default 0.8)")
|
|
655
|
+
sp.add_argument("--rounds", type=int, help="maximum refinement rounds (default 3)")
|
|
656
|
+
sp.add_argument("--force", action="store_true", help="write the best attempt even if it misses the threshold")
|
|
657
|
+
sp.add_argument("--dry-run", action="store_true")
|
|
658
|
+
sp.add_argument("--show", action="store_true", help="print the files on a dry run")
|
|
659
|
+
sp.add_argument("--no-check", action="store_true", help="skip the duplicate check")
|
|
660
|
+
_add_llm_args(sp)
|
|
661
|
+
|
|
662
|
+
sp = add("eval", cmd_eval, "check how well a model follows a skill (evals/evals.json)")
|
|
663
|
+
sp.add_argument("path")
|
|
664
|
+
sp.add_argument("--baseline", action="store_true", help="also run without the skill for comparison")
|
|
665
|
+
sp.add_argument("--limit", type=int, default=0)
|
|
666
|
+
_add_llm_args(sp)
|
|
667
|
+
|
|
668
|
+
sp = add("mine", cmd_mine, "find repeated workflows in shell history")
|
|
669
|
+
sp.add_argument("--file", action="append", help="history file (repeatable)")
|
|
670
|
+
sp.add_argument("--stdin", action="store_true", help="read history from standard input")
|
|
671
|
+
sp.add_argument("--min-count", type=int, default=3)
|
|
672
|
+
sp.add_argument("--max-len", type=int, default=4)
|
|
673
|
+
sp.add_argument("--limit", type=int, default=10)
|
|
674
|
+
sp.add_argument("--draft", type=int, metavar="N", help="write candidate N as a skill (no model)")
|
|
675
|
+
sp.add_argument("--forge", type=int, metavar="N", help="refine candidate N with a model")
|
|
676
|
+
sp.add_argument("--out", help="output folder (default: skills)")
|
|
677
|
+
sp.add_argument("--force", action="store_true")
|
|
678
|
+
sp.add_argument("--threshold", type=float)
|
|
679
|
+
sp.add_argument("--format", choices=["text", "json"], default="text")
|
|
680
|
+
_add_llm_args(sp)
|
|
681
|
+
|
|
682
|
+
sp = add("export", cmd_export, "convert and install a skill for another agent")
|
|
683
|
+
sp.add_argument("path")
|
|
684
|
+
sp.add_argument("--target", "-T", default="claude", choices=list(TARGETS) + ["all"])
|
|
685
|
+
sp.add_argument("--dest", help="project folder (default: current directory)")
|
|
686
|
+
sp.add_argument("--global", dest="global_", action="store_true", help="install in your home directory")
|
|
687
|
+
sp.add_argument("--dry-run", action="store_true")
|
|
688
|
+
|
|
689
|
+
sp = add("pack", cmd_pack, "bundle a skill into a zip with checksums")
|
|
690
|
+
sp.add_argument("path")
|
|
691
|
+
sp.add_argument("--out", default="dist")
|
|
692
|
+
|
|
693
|
+
sp = add("publish", cmd_publish, "check a skill against the quality gate and prepare it for sharing")
|
|
694
|
+
sp.add_argument("path")
|
|
695
|
+
sp.add_argument("--registry-dir", help="path to a checkout of a registry repository")
|
|
696
|
+
sp.add_argument("--open-pr", action="store_true", help="create the pull request with git and gh")
|
|
697
|
+
sp.add_argument("--threshold", type=float)
|
|
698
|
+
sp.add_argument("--force", action="store_true")
|
|
699
|
+
sp.add_argument("--dry-run", action="store_true")
|
|
700
|
+
sp.add_argument("--out", default="dist")
|
|
701
|
+
|
|
702
|
+
sp = add("search", cmd_search, "search the skill registry")
|
|
703
|
+
sp.add_argument("query", nargs="*")
|
|
704
|
+
sp.add_argument("--tag")
|
|
705
|
+
sp.add_argument("--limit", type=int, default=15)
|
|
706
|
+
sp.add_argument("--registry", help="registry index URL or file")
|
|
707
|
+
sp.add_argument("--format", choices=["text", "json"], default="text")
|
|
708
|
+
|
|
709
|
+
sp = add("install", cmd_install, "install a skill from the registry, GitHub or a folder")
|
|
710
|
+
sp.add_argument("source", help="registry name, ./folder or github:owner/repo/path[@ref]")
|
|
711
|
+
sp.add_argument("--target", "-T", default="claude", choices=list(TARGETS))
|
|
712
|
+
sp.add_argument("--dest", help="project folder (default: current directory)")
|
|
713
|
+
sp.add_argument("--global", dest="global_", action="store_true")
|
|
714
|
+
sp.add_argument("--registry", help="registry index URL or file")
|
|
715
|
+
sp.add_argument("--force", action="store_true", help="install even if lint or scan fail")
|
|
716
|
+
sp.add_argument("--dry-run", action="store_true")
|
|
717
|
+
|
|
718
|
+
sp = add("uninstall", cmd_uninstall, "remove an installed skill")
|
|
719
|
+
sp.add_argument("name")
|
|
720
|
+
sp.add_argument("--target", "-T", default="claude", choices=list(TARGETS))
|
|
721
|
+
sp.add_argument("--dest")
|
|
722
|
+
sp.add_argument("--global", dest="global_", action="store_true")
|
|
723
|
+
|
|
724
|
+
sp = add("list", cmd_list, "list local skills or what askit installed")
|
|
725
|
+
sp.add_argument("paths", nargs="*")
|
|
726
|
+
sp.add_argument("--installed", action="store_true")
|
|
727
|
+
|
|
728
|
+
sp = add("registry", cmd_registry, "build a registry index or a static website")
|
|
729
|
+
rsub = sp.add_subparsers(dest="reg_cmd", metavar="build|site", required=True)
|
|
730
|
+
rb = rsub.add_parser("build", help="write registry/index.json")
|
|
731
|
+
rb.add_argument("roots", nargs="*")
|
|
732
|
+
rb.add_argument("--out", default="registry/index.json")
|
|
733
|
+
rb.add_argument("--base-url", default=RAW_BASE)
|
|
734
|
+
rb.add_argument("--base", help="folder the skill paths are relative to (default: current directory)")
|
|
735
|
+
rb.add_argument("--check", action="store_true", help="exit 1 if the index is out of date")
|
|
736
|
+
rb.add_argument("--no-body", action="store_true")
|
|
737
|
+
rb.add_argument("--no-tests", action="store_true", help="skip running tests while scoring")
|
|
738
|
+
rs = rsub.add_parser("site", help="write a searchable static website")
|
|
739
|
+
rs.add_argument("roots", nargs="*")
|
|
740
|
+
rs.add_argument("--out", default="site")
|
|
741
|
+
rs.add_argument("--title", default="agentskill-kit skill registry")
|
|
742
|
+
rs.add_argument("--base-url", default=RAW_BASE)
|
|
743
|
+
rs.add_argument("--base")
|
|
744
|
+
rs.add_argument("--no-tests", action="store_true")
|
|
745
|
+
sp.set_defaults(func=cmd_registry)
|
|
746
|
+
|
|
747
|
+
sp = add("serve", cmd_serve, "browse local skills in your browser")
|
|
748
|
+
sp.add_argument("paths", nargs="*")
|
|
749
|
+
sp.add_argument("--host", default="127.0.0.1")
|
|
750
|
+
sp.add_argument("--port", type=int, default=8765)
|
|
751
|
+
sp.add_argument("--open", action="store_true", help="open the browser")
|
|
752
|
+
|
|
753
|
+
sp = add("providers", cmd_providers, "show LLM providers and what is ready")
|
|
754
|
+
sp.add_argument("--models", action="store_true", help="list models of --provider")
|
|
755
|
+
sp.add_argument("--provider")
|
|
756
|
+
|
|
757
|
+
sp = add("config", cmd_config, "show or change default settings")
|
|
758
|
+
sp.add_argument("action", choices=["show", "get", "set", "unset", "path"])
|
|
759
|
+
sp.add_argument("key", nargs="?")
|
|
760
|
+
sp.add_argument("value", nargs="?")
|
|
761
|
+
|
|
762
|
+
sp = add("doctor", cmd_doctor, "check your environment")
|
|
763
|
+
sp.add_argument("--online", action="store_true", help="also check the registry")
|
|
764
|
+
sp.add_argument("--registry")
|
|
765
|
+
return p
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
769
|
+
ui.setup_stdio()
|
|
770
|
+
load_dotenv()
|
|
771
|
+
parser = build_parser()
|
|
772
|
+
args = parser.parse_args(argv)
|
|
773
|
+
if args.no_color:
|
|
774
|
+
ui.set_no_color(True)
|
|
775
|
+
i18n.set_lang(args.lang or get_setting("lang") or i18n.detect_lang())
|
|
776
|
+
if not getattr(args, "command", None):
|
|
777
|
+
parser.print_help()
|
|
778
|
+
return 0
|
|
779
|
+
try:
|
|
780
|
+
return int(args.func(args) or 0)
|
|
781
|
+
except CliError as exc:
|
|
782
|
+
print(ui.fail(str(exc)), file=sys.stderr)
|
|
783
|
+
return 1
|
|
784
|
+
except SkillError as exc:
|
|
785
|
+
print(ui.fail(str(exc)), file=sys.stderr)
|
|
786
|
+
return 1
|
|
787
|
+
except KeyboardInterrupt:
|
|
788
|
+
print("", file=sys.stderr)
|
|
789
|
+
return 130
|
|
790
|
+
except BrokenPipeError:
|
|
791
|
+
return 0
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
if __name__ == "__main__":
|
|
795
|
+
sys.exit(main())
|