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.
Files changed (26) hide show
  1. {skillnav-0.3.1 → skillnav-0.4.2}/PKG-INFO +3 -2
  2. {skillnav-0.3.1 → skillnav-0.4.2}/README.md +0 -1
  3. {skillnav-0.3.1 → skillnav-0.4.2}/pyproject.toml +22 -2
  4. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/__init__.py +1 -1
  5. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/__main__.py +1 -1
  6. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/api.py +2 -0
  7. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/cli.py +74 -35
  8. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/error_hints.py +23 -10
  9. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/errors.py +0 -4
  10. skillnav-0.4.2/src/skillnav/output.py +874 -0
  11. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/PKG-INFO +3 -2
  12. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/requires.txt +2 -0
  13. skillnav-0.3.1/src/skillnav/output.py +0 -446
  14. {skillnav-0.3.1 → skillnav-0.4.2}/setup.cfg +0 -0
  15. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/config.py +0 -0
  16. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/context.py +0 -0
  17. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/contributors.py +0 -0
  18. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/packages.py +0 -0
  19. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/publish_metadata.py +0 -0
  20. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/self_update.py +0 -0
  21. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/skill_categories.py +0 -0
  22. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav/urls.py +0 -0
  23. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/SOURCES.txt +0 -0
  24. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/dependency_links.txt +0 -0
  25. {skillnav-0.3.1 → skillnav-0.4.2}/src/skillnav.egg-info/entry_points.txt +0 -0
  26. {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.1
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.3.1"
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 = ["pytest>=8.0"]
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"]
@@ -1,3 +1,3 @@
1
1
  """skillnav — CLI client for the Skill management platform (SkillNavigator)."""
2
2
 
3
- __version__ = "0.3.1"
3
+ __version__ = "0.4.2"
@@ -1,4 +1,4 @@
1
1
  from skillnav.cli import main
2
2
 
3
3
  if __name__ == "__main__":
4
- raise SystemExit(main())
4
+ main()
@@ -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
- print_review,
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, resolve_user_path
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 main(
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(slug: Annotated[str, typer.Argument(help="Skill slug")]) -> None:
404
- """Show publish status and version review summaries."""
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 / review ---
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
- """Publish a skill package to the registry."""
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
- typer.echo(
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("review")
530
- def review_cmd(
531
- package: Annotated[str, typer.Argument(help="Skill directory or .zip")],
532
- version: Annotated[Optional[str], typer.Option("--version", help="Version label")] = None,
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
- """Run remote review without publishing."""
572
+ """Re-run inspection on the stored package after a failed or incomplete inspection."""
535
573
  try:
536
574
  cli = _ctx()
537
- archive_base64 = package_to_base64(resolve_user_path(package))
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, "/reviews/run"),
544
- body=body,
545
- token=cli.require_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
- raise_for_api_status(status, payload)
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
- if payload.get("review") or payload.get("evaluation"):
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, review, download, install, rate, issue, contributors) "
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 open an HTTP connection to the registry."
59
- f"{registry_line} The API may be stopped, the URL wrong, or a firewall blocking the port."
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
- "review_pipeline_incomplete": ErrorHint(
119
- summary="Review pipeline incomplete (503)",
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
- "Wait a few seconds and retry the same command.",
123
- "Run a local dry-run first: skillnav publish <package> --dry-run",
124
- "If it persists, check API logs and review-engine dependencies (Python, SkillSpector, HaluCatch).",
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
- f"Publish with a higher version: skillnav publish <package> --version <next-semver>",
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-developer-guide",
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