skillnav 0.3.1__tar.gz → 0.4.2__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.
- {skillnav-0.3.1 → skillnav-0.4.2}/PKG-INFO +3 -2
- {skillnav-0.3.1 → skillnav-0.4.2}/README.md +0 -1
- {skillnav-0.3.1 → skillnav-0.4.2}/pyproject.toml +22 -2
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/__init__.py +1 -1
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/__main__.py +1 -1
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/api.py +2 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/cli.py +74 -35
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/error_hints.py +23 -10
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/errors.py +0 -4
- skillnav-0.4.2/src/skillnav/output.py +874 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/PKG-INFO +3 -2
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/requires.txt +2 -0
- skillnav-0.3.1/src/skillnav/output.py +0 -446
- {skillnav-0.3.1 → skillnav-0.4.2}/setup.cfg +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/config.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/context.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/contributors.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/packages.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/publish_metadata.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/self_update.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/skill_categories.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/urls.py +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/SOURCES.txt +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/dependency_links.txt +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/entry_points.txt +0 -0
- {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skillnav
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: CLI client for the Skill management platform (SkillNavigator)
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: skill,cli,agent,registry
|
|
@@ -12,6 +12,8 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
Requires-Dist: typer>=0.12
|
|
13
13
|
Provides-Extra: dev
|
|
14
14
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
15
|
+
Requires-Dist: ruff>=0.9; extra == "dev"
|
|
16
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
15
17
|
|
|
16
18
|
# skillnav
|
|
17
19
|
|
|
@@ -36,7 +38,6 @@ skillnav config test
|
|
|
36
38
|
skillnav login --api-key sk_...
|
|
37
39
|
skillnav search demo
|
|
38
40
|
skillnav info demo-skill
|
|
39
|
-
skillnav review examples/demo-skill
|
|
40
41
|
skillnav publish examples/demo-skill --dry-run
|
|
41
42
|
skillnav download demo-skill -o /tmp/demo.zip
|
|
42
43
|
skillnav update # upgrade when PyPI has a newer release
|
|
@@ -21,7 +21,6 @@ skillnav config test
|
|
|
21
21
|
skillnav login --api-key sk_...
|
|
22
22
|
skillnav search demo
|
|
23
23
|
skillnav info demo-skill
|
|
24
|
-
skillnav review examples/demo-skill
|
|
25
24
|
skillnav publish examples/demo-skill --dry-run
|
|
26
25
|
skillnav download demo-skill -o /tmp/demo.zip
|
|
27
26
|
skillnav update # upgrade when PyPI has a newer release
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "skillnav"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.2"
|
|
8
8
|
description = "CLI client for the Skill management platform (SkillNavigator)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -20,10 +20,30 @@ dependencies = [
|
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
[project.optional-dependencies]
|
|
23
|
-
dev = [
|
|
23
|
+
dev = [
|
|
24
|
+
"pytest>=8.0",
|
|
25
|
+
"ruff>=0.9",
|
|
26
|
+
"mypy>=1.10",
|
|
27
|
+
]
|
|
24
28
|
|
|
25
29
|
[project.scripts]
|
|
26
30
|
skillnav = "skillnav.cli:main"
|
|
27
31
|
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
line-length = 100
|
|
34
|
+
target-version = "py39"
|
|
35
|
+
src = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff.lint]
|
|
38
|
+
# 默认集(F 全部 + E4/E7/E9);后续需要可扩展(I/UP/B 等)。
|
|
39
|
+
select = ["E4", "E7", "E9", "F"]
|
|
40
|
+
|
|
41
|
+
[tool.mypy]
|
|
42
|
+
python_version = "3.9"
|
|
43
|
+
check_untyped_defs = false
|
|
44
|
+
warn_redundant_casts = true
|
|
45
|
+
no_implicit_optional = true
|
|
46
|
+
ignore_missing_imports = true
|
|
47
|
+
|
|
28
48
|
[tool.setuptools.packages.find]
|
|
29
49
|
where = ["src"]
|
|
@@ -37,6 +37,8 @@ def request_bytes(
|
|
|
37
37
|
except urllib.error.URLError as exc:
|
|
38
38
|
reason = getattr(exc, "reason", exc)
|
|
39
39
|
raise NetworkError.from_hint(network_unreachable(str(reason), registry=url)) from exc
|
|
40
|
+
except OSError as exc:
|
|
41
|
+
raise NetworkError.from_hint(network_unreachable(str(exc), registry=url)) from exc
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def request_json(
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import sys
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Annotated, Any, Optional
|
|
8
7
|
|
|
@@ -16,7 +15,7 @@ from skillnav.api import (
|
|
|
16
15
|
request_bytes,
|
|
17
16
|
request_json,
|
|
18
17
|
)
|
|
19
|
-
from skillnav.config import get_profile, load_config, save_config
|
|
18
|
+
from skillnav.config import get_profile, load_config, resolve_profile_api_key, save_config
|
|
20
19
|
from skillnav.contributors import resolve_contributor_id
|
|
21
20
|
from skillnav.context import CliContext
|
|
22
21
|
from skillnav.error_hints import (
|
|
@@ -40,17 +39,16 @@ from skillnav.errors import (
|
|
|
40
39
|
from skillnav.output import (
|
|
41
40
|
emit_error,
|
|
42
41
|
emit_json,
|
|
42
|
+
filter_skill_body_version,
|
|
43
43
|
print_leaderboard,
|
|
44
44
|
print_report_version,
|
|
45
|
-
|
|
46
|
-
print_review_result,
|
|
45
|
+
print_inspection_result,
|
|
47
46
|
print_search_results,
|
|
48
47
|
print_skill_info,
|
|
49
48
|
print_skill_status,
|
|
50
|
-
print_virustotal_summary,
|
|
51
49
|
unwrap_resource_id,
|
|
52
50
|
)
|
|
53
|
-
from skillnav.packages import extract_zip_to_directory, package_to_base64
|
|
51
|
+
from skillnav.packages import extract_zip_to_directory, package_to_base64
|
|
54
52
|
from skillnav.publish_metadata import build_publish_metadata, read_frontmatter_hints, resolve_package_path
|
|
55
53
|
from skillnav.urls import join_registry_url, slug_path
|
|
56
54
|
|
|
@@ -133,7 +131,7 @@ def _version_callback(value: bool) -> None:
|
|
|
133
131
|
|
|
134
132
|
|
|
135
133
|
@app.callback()
|
|
136
|
-
def
|
|
134
|
+
def cli_root(
|
|
137
135
|
ctx: typer.Context,
|
|
138
136
|
registry: Annotated[
|
|
139
137
|
Optional[str], typer.Option("--registry", help="API base URL (overrides profile)")
|
|
@@ -400,8 +398,14 @@ def info_cmd(slug: Annotated[str, typer.Argument(help="Skill slug")]) -> None:
|
|
|
400
398
|
|
|
401
399
|
|
|
402
400
|
@app.command("status")
|
|
403
|
-
def status_cmd(
|
|
404
|
-
|
|
401
|
+
def status_cmd(
|
|
402
|
+
slug: Annotated[str, typer.Argument(help="Skill slug")],
|
|
403
|
+
version: Annotated[
|
|
404
|
+
Optional[str],
|
|
405
|
+
typer.Option("--version", help="Version to show (default: latest)"),
|
|
406
|
+
] = None,
|
|
407
|
+
) -> None:
|
|
408
|
+
"""Show publish and inspection status for one skill version."""
|
|
405
409
|
try:
|
|
406
410
|
cli = _ctx()
|
|
407
411
|
status, body = request_json(
|
|
@@ -410,10 +414,14 @@ def status_cmd(slug: Annotated[str, typer.Argument(help="Skill slug")]) -> None:
|
|
|
410
414
|
token=cli.token,
|
|
411
415
|
)
|
|
412
416
|
raise_for_api_status(status, body)
|
|
417
|
+
target_version = version or body.get("latestVersion")
|
|
418
|
+
if not target_version:
|
|
419
|
+
raise SkillnavError(f"No versions found for skill '{slug}'")
|
|
420
|
+
body = filter_skill_body_version(body, str(target_version))
|
|
413
421
|
if cli.json_output:
|
|
414
422
|
emit_json(body)
|
|
415
423
|
else:
|
|
416
|
-
print_skill_status(body)
|
|
424
|
+
print_skill_status(body, version=str(target_version))
|
|
417
425
|
except Exception as exc: # noqa: BLE001
|
|
418
426
|
_handle_error(exc)
|
|
419
427
|
|
|
@@ -443,7 +451,28 @@ def report_cmd(
|
|
|
443
451
|
_handle_error(exc)
|
|
444
452
|
|
|
445
453
|
|
|
446
|
-
# --- publish
|
|
454
|
+
# --- publish ---
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _print_publish_response(status: int, payload: dict[str, Any], *, waited: bool) -> None:
|
|
458
|
+
if status == 202 or payload.get("inspectionStatus") == "inspecting":
|
|
459
|
+
typer.echo(
|
|
460
|
+
f"Uploaded {payload.get('name')} ({payload.get('slug')})@{payload.get('version')}"
|
|
461
|
+
)
|
|
462
|
+
if waited:
|
|
463
|
+
typer.echo("Inspection started (inspectionStatus: inspecting).")
|
|
464
|
+
else:
|
|
465
|
+
typer.echo("Inspection started in the background (inspectionStatus: inspecting).")
|
|
466
|
+
typer.echo(f"Check progress: skillnav status {payload.get('slug')}")
|
|
467
|
+
return
|
|
468
|
+
|
|
469
|
+
typer.echo(
|
|
470
|
+
f"Published {payload.get('name')} ({payload.get('slug')})@{payload.get('version')}"
|
|
471
|
+
)
|
|
472
|
+
typer.echo(f"Status: {payload.get('status')}")
|
|
473
|
+
typer.echo(f"Hash: {payload.get('contentHash')}")
|
|
474
|
+
if payload.get("inspection") or payload.get("review") or payload.get("evaluation"):
|
|
475
|
+
print_inspection_result(payload)
|
|
447
476
|
|
|
448
477
|
|
|
449
478
|
@app.command("publish")
|
|
@@ -468,8 +497,15 @@ def publish_cmd(
|
|
|
468
497
|
] = None,
|
|
469
498
|
changelog: Annotated[Optional[str], typer.Option("--changelog", help="Changelog text")] = None,
|
|
470
499
|
dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview without publishing")] = False,
|
|
500
|
+
wait: Annotated[
|
|
501
|
+
bool,
|
|
502
|
+
typer.Option(
|
|
503
|
+
"--wait",
|
|
504
|
+
help="Wait for the full inspection pipeline to finish before returning (default: upload only, inspection in background)",
|
|
505
|
+
),
|
|
506
|
+
] = False,
|
|
471
507
|
) -> None:
|
|
472
|
-
"""
|
|
508
|
+
"""Upload a skill package to the registry; inspection runs in the background unless --wait."""
|
|
473
509
|
try:
|
|
474
510
|
cli = _ctx()
|
|
475
511
|
token = cli.require_token()
|
|
@@ -491,6 +527,8 @@ def publish_cmd(
|
|
|
491
527
|
body: dict[str, Any] = {"archiveBase64": archive_base64, "metadata": metadata}
|
|
492
528
|
if changelog:
|
|
493
529
|
body["changelog"] = changelog
|
|
530
|
+
if not dry_run:
|
|
531
|
+
body["async"] = not wait
|
|
494
532
|
path = "/skills/publish/preview" if dry_run else "/skills/publish"
|
|
495
533
|
status, payload = request_json(
|
|
496
534
|
method="POST",
|
|
@@ -515,41 +553,39 @@ def publish_cmd(
|
|
|
515
553
|
if payload.get("entryPath"):
|
|
516
554
|
typer.echo(f"Entry: {payload['entryPath']}")
|
|
517
555
|
return
|
|
518
|
-
|
|
519
|
-
f"Published {payload.get('name')} ({payload.get('slug')})@{payload.get('version')}"
|
|
520
|
-
)
|
|
521
|
-
typer.echo(f"Status: {payload.get('status')}")
|
|
522
|
-
typer.echo(f"Hash: {payload.get('contentHash')}")
|
|
523
|
-
if payload.get("review") or payload.get("evaluation"):
|
|
524
|
-
print_review_result(payload)
|
|
556
|
+
_print_publish_response(status, payload, waited=wait)
|
|
525
557
|
except Exception as exc: # noqa: BLE001
|
|
526
558
|
_handle_error(exc)
|
|
527
559
|
|
|
528
560
|
|
|
529
|
-
@app.command("
|
|
530
|
-
def
|
|
531
|
-
|
|
532
|
-
|
|
561
|
+
@app.command("retry-publish")
|
|
562
|
+
def retry_publish_cmd(
|
|
563
|
+
slug: Annotated[str, typer.Argument(help="Skill slug")],
|
|
564
|
+
wait: Annotated[
|
|
565
|
+
bool,
|
|
566
|
+
typer.Option(
|
|
567
|
+
"--wait",
|
|
568
|
+
help="Wait for the full inspection pipeline to finish before returning",
|
|
569
|
+
),
|
|
570
|
+
] = False,
|
|
533
571
|
) -> None:
|
|
534
|
-
"""
|
|
572
|
+
"""Re-run inspection on the stored package after a failed or incomplete inspection."""
|
|
535
573
|
try:
|
|
536
574
|
cli = _ctx()
|
|
537
|
-
|
|
538
|
-
body: dict[str, Any] = {"archiveBase64": archive_base64}
|
|
539
|
-
if version:
|
|
540
|
-
body["version"] = version
|
|
575
|
+
token = cli.require_token()
|
|
541
576
|
status, payload = request_json(
|
|
542
|
-
"POST",
|
|
543
|
-
join_registry_url(cli.registry, "/
|
|
544
|
-
body=
|
|
545
|
-
token=
|
|
577
|
+
method="POST",
|
|
578
|
+
url=join_registry_url(cli.registry, f"/skills/{slug_path(slug)}/retry-publish"),
|
|
579
|
+
body={"async": not wait},
|
|
580
|
+
token=token,
|
|
546
581
|
)
|
|
547
|
-
|
|
582
|
+
if status >= 400:
|
|
583
|
+
hint = enrich_api_error(api_error_message(payload), status=status, body=payload)
|
|
584
|
+
raise SkillnavError(hint.summary, hint=hint)
|
|
548
585
|
if cli.json_output:
|
|
549
586
|
emit_json(payload)
|
|
550
587
|
return
|
|
551
|
-
|
|
552
|
-
print_review_result(payload)
|
|
588
|
+
_print_publish_response(status, payload, waited=wait)
|
|
553
589
|
except Exception as exc: # noqa: BLE001
|
|
554
590
|
_handle_error(exc)
|
|
555
591
|
|
|
@@ -786,6 +822,9 @@ def remove_contributor_cmd(
|
|
|
786
822
|
raise_for_api_status(status, skill_body)
|
|
787
823
|
contributor_id = resolve_contributor_id(skill_body, username)
|
|
788
824
|
|
|
825
|
+
if contributor_id is None:
|
|
826
|
+
raise UsageError.from_hint(enrich_usage_error("no contributor id available to remove"))
|
|
827
|
+
|
|
789
828
|
status, payload = request_json(
|
|
790
829
|
"DELETE",
|
|
791
830
|
join_registry_url(
|
|
@@ -25,7 +25,7 @@ def not_logged_in(*, profile: str | None = None) -> ErrorHint:
|
|
|
25
25
|
return ErrorHint(
|
|
26
26
|
summary=f"Not logged in (no API key on profile{profile_label})",
|
|
27
27
|
detail=(
|
|
28
|
-
"Write commands (publish,
|
|
28
|
+
"Write commands (publish, download, install, rate, issue, contributors) "
|
|
29
29
|
"require a platform API key. None is configured for the active profile."
|
|
30
30
|
),
|
|
31
31
|
next_steps=_steps(
|
|
@@ -55,8 +55,9 @@ def network_unreachable(reason: str, *, registry: str | None = None) -> ErrorHin
|
|
|
55
55
|
return ErrorHint(
|
|
56
56
|
summary=f"Cannot reach the Skill platform API ({reason})",
|
|
57
57
|
detail=(
|
|
58
|
-
"The CLI could not
|
|
59
|
-
f"{registry_line} The API may be stopped, the URL wrong,
|
|
58
|
+
"The CLI could not complete an HTTP request to the registry."
|
|
59
|
+
f"{registry_line} The API may be stopped, crashed mid-response, the URL wrong,"
|
|
60
|
+
" or a firewall blocking the port."
|
|
60
61
|
),
|
|
61
62
|
next_steps=_steps(
|
|
62
63
|
"Start the API locally: npm run dev:api (or npm run dev for API + Web).",
|
|
@@ -115,13 +116,25 @@ def enrich_api_error(raw: str, *, status: int, body: Any = None) -> ErrorHint:
|
|
|
115
116
|
"Or publish under a new slug (first publish creates a new skill).",
|
|
116
117
|
),
|
|
117
118
|
),
|
|
118
|
-
"
|
|
119
|
-
summary="
|
|
119
|
+
"inspection_pipeline_incomplete": ErrorHint(
|
|
120
|
+
summary="Inspection pipeline incomplete (503)",
|
|
120
121
|
detail="SkillSpector, VirusTotal, or HaluCatch did not finish. The server marks this as retryable.",
|
|
121
122
|
next_steps=_steps(
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"If it persists, check API logs and
|
|
123
|
+
"The package may already be saved — retry inspection: skillnav retry-publish <slug>",
|
|
124
|
+
"Wait a few seconds and retry, or check progress: skillnav status <slug>",
|
|
125
|
+
"If it persists, check API logs and inspection-engine dependencies (Python, SkillSpector, HaluCatch).",
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
"pending_publish_use_retry": ErrorHint(
|
|
129
|
+
summary="Package already uploaded — use retry-publish",
|
|
130
|
+
detail=(
|
|
131
|
+
"This slug@version has a stored package from a previous upload. "
|
|
132
|
+
"Re-uploading is blocked; run inspection again on the saved package."
|
|
133
|
+
),
|
|
134
|
+
next_steps=_steps(
|
|
135
|
+
"Retry inspection: skillnav retry-publish <slug>",
|
|
136
|
+
"Inspect state: skillnav status <slug>",
|
|
137
|
+
"To upload changed files, bump the version in SKILL.md or pass --version.",
|
|
125
138
|
),
|
|
126
139
|
),
|
|
127
140
|
"publish_rate_limited": ErrorHint(
|
|
@@ -220,7 +233,7 @@ def enrich_api_error(raw: str, *, status: int, body: Any = None) -> ErrorHint:
|
|
|
220
233
|
summary=f"Version {got} is not greater than latest {latest}",
|
|
221
234
|
detail=f"New releases for '{slug}' must use semver strictly greater than {latest}.",
|
|
222
235
|
next_steps=_steps(
|
|
223
|
-
|
|
236
|
+
"Publish with a higher version: skillnav publish <package> --version <next-semver>",
|
|
224
237
|
f"Inspect versions: skillnav status {slug}",
|
|
225
238
|
),
|
|
226
239
|
)
|
|
@@ -272,7 +285,7 @@ def enrich_api_error(raw: str, *, status: int, body: Any = None) -> ErrorHint:
|
|
|
272
285
|
next_steps=_steps(
|
|
273
286
|
"Read the message above and fix the request input.",
|
|
274
287
|
"Use --json for the raw response body when debugging.",
|
|
275
|
-
"See Web docs: /docs/cli-
|
|
288
|
+
"See Web docs: /docs/cli-guide",
|
|
276
289
|
),
|
|
277
290
|
)
|
|
278
291
|
|
|
@@ -40,10 +40,6 @@ class SkillnavError(Exception):
|
|
|
40
40
|
def from_hint(cls, hint: ErrorHint) -> SkillnavError:
|
|
41
41
|
return cls(hint.summary, hint=hint)
|
|
42
42
|
|
|
43
|
-
@classmethod
|
|
44
|
-
def from_hint(cls, hint: ErrorHint) -> SkillnavError:
|
|
45
|
-
return cls(hint.summary, hint=hint)
|
|
46
|
-
|
|
47
43
|
|
|
48
44
|
class AuthError(SkillnavError):
|
|
49
45
|
exit_code = EXIT_AUTH
|