opencode-skills-collection 3.1.4 → 3.1.6
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.
- package/bundled-skills/.antigravity-install-manifest.json +1 -1
- package/bundled-skills/007/scripts/full_audit.py +10 -3
- package/bundled-skills/2slides-ppt-generator/requirements.txt +3 -1
- package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +24 -0
- package/bundled-skills/2slides-ppt-generator/scripts/get_job_status.py +18 -1
- package/bundled-skills/agent-creator/SKILL.md +15 -1
- package/bundled-skills/agent-orchestrator/scripts/scan_registry.py +4 -4
- package/bundled-skills/android-dev/references/hybrid.md +3 -3
- package/bundled-skills/android-dev/references/react-native.md +14 -8
- package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +82 -20
- package/bundled-skills/diary/requirements.txt +3 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docx-official/ooxml/scripts/validation/base.py +19 -1
- package/bundled-skills/docx-official/ooxml/scripts/validation/docx.py +20 -1
- package/bundled-skills/docx-official/ooxml/scripts/validation/redlining.py +21 -5
- package/bundled-skills/ecl-harness-engineer/references/environment-detection-guide.md +1 -1
- package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +44 -23
- package/bundled-skills/instagram/scripts/db.py +120 -18
- package/bundled-skills/instagram/scripts/export.py +41 -8
- package/bundled-skills/instagram/scripts/publish.py +7 -7
- package/bundled-skills/instagram/scripts/run_all.py +2 -2
- package/bundled-skills/instagram/scripts/schedule.py +6 -5
- package/bundled-skills/instagram/static/dashboard.html +63 -16
- package/bundled-skills/junta-leiloeiros/scripts/requirements.txt +1 -1
- package/bundled-skills/k8s-manifest-generator/assets/deployment-template.yaml +20 -8
- package/bundled-skills/k8s-manifest-generator/assets/service-template.yaml +2 -3
- package/bundled-skills/last30days/scripts/lib/reddit_enrich.py +3 -1
- package/bundled-skills/loki-mode/benchmarks/results/2026-01-05-00-49-17/humaneval-solutions/162.py +1 -1
- package/bundled-skills/loki-mode/benchmarks/results/humaneval-loki-solutions/162.py +1 -1
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/index.ts +1 -0
- package/bundled-skills/loop-library/SKILL.md +11 -11
- package/bundled-skills/mcp-builder/scripts/evaluation.py +6 -2
- package/bundled-skills/notebooklm/scripts/run.py +23 -8
- package/bundled-skills/playwright-skill/lib/helpers.js +15 -17
- package/bundled-skills/pptx-official/ooxml/scripts/validation/base.py +19 -1
- package/bundled-skills/pptx-official/ooxml/scripts/validation/docx.py +20 -1
- package/bundled-skills/pptx-official/ooxml/scripts/validation/redlining.py +21 -5
- package/bundled-skills/remote-gpu-trainer/profiles/runpod.md +2 -2
- package/bundled-skills/senior-frontend/scripts/component_generator.py +67 -10
- package/bundled-skills/shopify-development/scripts/requirements.txt +1 -0
- package/bundled-skills/shopify-development/scripts/tests/test_shopify_init.py +13 -9
- package/bundled-skills/skill-installer/scripts/install_skill.py +73 -34
- package/bundled-skills/skill-installer/scripts/package_skill.py +36 -8
- package/bundled-skills/skill-installer/scripts/validate_skill.py +22 -7
- package/bundled-skills/skill-sentinel/scripts/db.py +69 -15
- package/bundled-skills/slack-gif-creator/requirements.txt +3 -2
- package/bundled-skills/stability-ai/scripts/requirements.txt +1 -1
- package/bundled-skills/telegram/assets/boilerplate/nodejs/src/bot-client.ts +1 -0
- package/bundled-skills/telegram/assets/boilerplate/python/requirements.txt +3 -1
- package/bundled-skills/telegram/scripts/send_message.py +39 -9
- package/bundled-skills/webapp-testing/scripts/with_server.py +105 -8
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/index.ts +1 -0
- package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/requirements.txt +1 -0
- package/bundled-skills/whatsapp-cloud-api/scripts/setup_project.py +31 -3
- package/bundled-skills/writing-skills/render-graphs.js +30 -5
- package/bundled-skills/youtube-notetaker/reference/artifact.html +29 -18
- package/bundled-skills/youtube-notetaker/scripts/serve.py +49 -8
- package/package.json +2 -2
- package/skills_index.json +27 -3
|
@@ -126,8 +126,37 @@ def sanitize_name(name: str) -> str:
|
|
|
126
126
|
return name.strip("-")
|
|
127
127
|
|
|
128
128
|
|
|
129
|
+
def safe_child_path(root: Path, *parts: str) -> Path:
|
|
130
|
+
"""Resolve a child path and ensure it remains under root."""
|
|
131
|
+
root = root.resolve()
|
|
132
|
+
child = root.joinpath(*parts).resolve()
|
|
133
|
+
try:
|
|
134
|
+
child.relative_to(root)
|
|
135
|
+
except ValueError as exc:
|
|
136
|
+
raise ValueError(f"Path escapes {root}: {child}") from exc
|
|
137
|
+
return child
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def safe_skill_path(root: Path, skill_name: str) -> Path:
|
|
141
|
+
"""Build a path from a sanitized skill name under a trusted root."""
|
|
142
|
+
clean_name = sanitize_name(skill_name)
|
|
143
|
+
if not clean_name:
|
|
144
|
+
raise ValueError("Invalid empty skill name")
|
|
145
|
+
return safe_child_path(root, clean_name)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def resolve_skill_source(source: str) -> Path:
|
|
149
|
+
"""Resolve and validate a local skill source directory."""
|
|
150
|
+
source_path = Path(source).expanduser().resolve()
|
|
151
|
+
if not source_path.is_dir():
|
|
152
|
+
raise ValueError(f"Source does not exist or is not a directory: {source_path}")
|
|
153
|
+
if not (source_path / "SKILL.md").is_file():
|
|
154
|
+
raise ValueError(f"No SKILL.md found in {source_path}")
|
|
155
|
+
return source_path
|
|
156
|
+
|
|
157
|
+
|
|
129
158
|
def md5_dir(path: Path, exclude_dirs: set = None) -> str:
|
|
130
|
-
"""Compute combined
|
|
159
|
+
"""Compute combined SHA-256 hash of all files in a directory.
|
|
131
160
|
|
|
132
161
|
Excludes backup/staging dirs and normalizes paths to forward slashes
|
|
133
162
|
for cross-platform consistency.
|
|
@@ -135,17 +164,23 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str:
|
|
|
135
164
|
if exclude_dirs is None:
|
|
136
165
|
exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"}
|
|
137
166
|
|
|
138
|
-
|
|
139
|
-
|
|
167
|
+
root_path = Path(path).resolve(strict=True)
|
|
168
|
+
if not root_path.is_dir():
|
|
169
|
+
raise ValueError(f"Hash target must be a directory: {root_path}")
|
|
170
|
+
|
|
171
|
+
h = hashlib.sha256()
|
|
172
|
+
for root, dirs, files in os.walk(root_path, followlinks=False):
|
|
140
173
|
# Filter out excluded directories
|
|
141
174
|
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
142
175
|
for f in sorted(files):
|
|
143
176
|
fp = Path(root) / f
|
|
144
177
|
try:
|
|
178
|
+
resolved_fp = fp.resolve(strict=True)
|
|
179
|
+
resolved_fp.relative_to(root_path)
|
|
145
180
|
# Normalize to forward slashes for consistent hashing
|
|
146
|
-
rel =
|
|
181
|
+
rel = resolved_fp.relative_to(root_path).as_posix()
|
|
147
182
|
h.update(rel.encode("utf-8"))
|
|
148
|
-
with open(
|
|
183
|
+
with resolved_fp.open("rb") as fh:
|
|
149
184
|
for chunk in iter(lambda: fh.read(8192), b""):
|
|
150
185
|
h.update(chunk)
|
|
151
186
|
except Exception:
|
|
@@ -262,11 +297,10 @@ def get_all_skill_dirs() -> list:
|
|
|
262
297
|
def step1_resolve_source(source: str = None, do_detect: bool = False, auto: bool = False) -> dict:
|
|
263
298
|
"""STEP 1: Resolve source directory."""
|
|
264
299
|
if source:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return {"success": False, "error": f"No SKILL.md found in {source_path}"}
|
|
300
|
+
try:
|
|
301
|
+
source_path = resolve_skill_source(source)
|
|
302
|
+
except ValueError as e:
|
|
303
|
+
return {"success": False, "error": str(e)}
|
|
270
304
|
return {"success": True, "sources": [str(source_path)]}
|
|
271
305
|
|
|
272
306
|
if do_detect:
|
|
@@ -316,8 +350,8 @@ def step3_determine_name(source_path: Path, name_override: str = None) -> str:
|
|
|
316
350
|
|
|
317
351
|
def step4_check_conflicts(skill_name: str) -> dict:
|
|
318
352
|
"""STEP 4: Check for existing skill with same name."""
|
|
319
|
-
dest = SKILLS_ROOT
|
|
320
|
-
claude_dest = CLAUDE_SKILLS
|
|
353
|
+
dest = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
354
|
+
claude_dest = safe_skill_path(CLAUDE_SKILLS, skill_name)
|
|
321
355
|
|
|
322
356
|
conflicts = []
|
|
323
357
|
if dest.exists():
|
|
@@ -339,6 +373,8 @@ def _backup_ignore(directory, contents):
|
|
|
339
373
|
dir_path = Path(directory)
|
|
340
374
|
for item in contents:
|
|
341
375
|
item_path = dir_path / item
|
|
376
|
+
if item_path.is_symlink():
|
|
377
|
+
ignored.add(item)
|
|
342
378
|
# Skip backup and staging directories to prevent recursion
|
|
343
379
|
if item in ("backups", "staging") and dir_path.name == "data":
|
|
344
380
|
ignored.add(item)
|
|
@@ -350,10 +386,10 @@ def _backup_ignore(directory, contents):
|
|
|
350
386
|
|
|
351
387
|
def step5_backup(skill_name: str) -> dict:
|
|
352
388
|
"""STEP 5: Backup existing skill before overwrite."""
|
|
353
|
-
dest = SKILLS_ROOT
|
|
389
|
+
dest = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
354
390
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
355
391
|
backup_name = f"{skill_name}_{timestamp}"
|
|
356
|
-
backup_path = BACKUPS_DIR
|
|
392
|
+
backup_path = safe_child_path(BACKUPS_DIR, backup_name)
|
|
357
393
|
|
|
358
394
|
BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
|
|
359
395
|
|
|
@@ -366,7 +402,7 @@ def step5_backup(skill_name: str) -> dict:
|
|
|
366
402
|
except Exception as e:
|
|
367
403
|
return {"success": False, "error": f"Backup failed for {dest}: {e}"}
|
|
368
404
|
|
|
369
|
-
claude_dest = CLAUDE_SKILLS
|
|
405
|
+
claude_dest = safe_skill_path(CLAUDE_SKILLS, skill_name)
|
|
370
406
|
if claude_dest.exists():
|
|
371
407
|
claude_backup = backup_path / ".claude-registration"
|
|
372
408
|
claude_backup.mkdir(parents=True, exist_ok=True)
|
|
@@ -388,8 +424,9 @@ def step5_backup(skill_name: str) -> dict:
|
|
|
388
424
|
|
|
389
425
|
def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
|
|
390
426
|
"""STEP 6: Copy to skills root via staging area."""
|
|
391
|
-
|
|
392
|
-
|
|
427
|
+
source_path = resolve_skill_source(str(source_path))
|
|
428
|
+
dest = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
429
|
+
staging = safe_skill_path(STAGING_DIR, skill_name)
|
|
393
430
|
|
|
394
431
|
STAGING_DIR.mkdir(parents=True, exist_ok=True)
|
|
395
432
|
|
|
@@ -448,8 +485,9 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
|
|
|
448
485
|
|
|
449
486
|
def step7_register_claude(skill_name: str) -> dict:
|
|
450
487
|
"""STEP 7: Register in .claude/skills/ for native Claude Code discovery."""
|
|
451
|
-
|
|
452
|
-
|
|
488
|
+
source_dir = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
489
|
+
source_skill_md = source_dir / "SKILL.md"
|
|
490
|
+
claude_dest_dir = safe_skill_path(CLAUDE_SKILLS, skill_name)
|
|
453
491
|
|
|
454
492
|
if not source_skill_md.exists():
|
|
455
493
|
return {"success": False, "error": f"SKILL.md not found at {source_skill_md}"}
|
|
@@ -463,7 +501,7 @@ def step7_register_claude(skill_name: str) -> dict:
|
|
|
463
501
|
return {"success": False, "error": f"Failed to copy SKILL.md to Claude skills: {e}"}
|
|
464
502
|
|
|
465
503
|
# Also copy references/ if it exists (useful for Claude to read)
|
|
466
|
-
refs_dir =
|
|
504
|
+
refs_dir = source_dir / "references"
|
|
467
505
|
if refs_dir.exists():
|
|
468
506
|
claude_refs = claude_dest_dir / "references"
|
|
469
507
|
try:
|
|
@@ -520,7 +558,7 @@ def step9_verify(skill_name: str) -> dict:
|
|
|
520
558
|
checks = []
|
|
521
559
|
|
|
522
560
|
# Check 1: Skill directory exists
|
|
523
|
-
dest = SKILLS_ROOT
|
|
561
|
+
dest = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
524
562
|
checks.append({
|
|
525
563
|
"check": "skill_dir_exists",
|
|
526
564
|
"pass": dest.exists(),
|
|
@@ -551,7 +589,7 @@ def step9_verify(skill_name: str) -> dict:
|
|
|
551
589
|
})
|
|
552
590
|
|
|
553
591
|
# Check 4: Claude Code registration
|
|
554
|
-
claude_skill_md = CLAUDE_SKILLS
|
|
592
|
+
claude_skill_md = safe_skill_path(CLAUDE_SKILLS, skill_name) / "SKILL.md"
|
|
555
593
|
checks.append({
|
|
556
594
|
"check": "claude_registered",
|
|
557
595
|
"pass": claude_skill_md.exists(),
|
|
@@ -590,7 +628,7 @@ def step10_log(skill_name: str, source: str, result: dict):
|
|
|
590
628
|
"action": "install",
|
|
591
629
|
"skill_name": skill_name,
|
|
592
630
|
"source": source,
|
|
593
|
-
"destination": str(SKILLS_ROOT
|
|
631
|
+
"destination": str(safe_skill_path(SKILLS_ROOT, skill_name)),
|
|
594
632
|
"registered": result.get("registered", False),
|
|
595
633
|
"registry_updated": result.get("registry_updated", False),
|
|
596
634
|
"backup_path": result.get("backup_path"),
|
|
@@ -625,7 +663,6 @@ def install_single(
|
|
|
625
663
|
dry_run: If True, simulate all steps without writing anything.
|
|
626
664
|
verbose: If True, print step-by-step progress to stdout.
|
|
627
665
|
"""
|
|
628
|
-
source = Path(source_path).resolve()
|
|
629
666
|
total_steps = 11
|
|
630
667
|
result = {
|
|
631
668
|
"success": False,
|
|
@@ -646,10 +683,12 @@ def install_single(
|
|
|
646
683
|
# STEP 1: Already resolved (source is provided)
|
|
647
684
|
if verbose:
|
|
648
685
|
_step(1, total_steps, "Resolving source...")
|
|
649
|
-
|
|
650
|
-
|
|
686
|
+
try:
|
|
687
|
+
source = resolve_skill_source(source_path)
|
|
688
|
+
except ValueError as e:
|
|
689
|
+
result["error"] = str(e)
|
|
651
690
|
if verbose:
|
|
652
|
-
_fail(
|
|
691
|
+
_fail(str(e))
|
|
653
692
|
return result
|
|
654
693
|
|
|
655
694
|
result["steps"]["1_resolve"] = {"success": True, "source": str(source)}
|
|
@@ -696,7 +735,7 @@ def install_single(
|
|
|
696
735
|
# Version comparison with installed
|
|
697
736
|
source_meta = parse_yaml_frontmatter(source / "SKILL.md")
|
|
698
737
|
source_version = source_meta.get("version", "")
|
|
699
|
-
dest = SKILLS_ROOT
|
|
738
|
+
dest = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
700
739
|
if dest.exists() and (dest / "SKILL.md").exists():
|
|
701
740
|
installed_meta = parse_yaml_frontmatter(dest / "SKILL.md")
|
|
702
741
|
installed_version = installed_meta.get("version", "")
|
|
@@ -879,7 +918,7 @@ def install_single(
|
|
|
879
918
|
zip_result = {"success": False, "skipped": True}
|
|
880
919
|
try:
|
|
881
920
|
from package_skill import package_skill as pkg_skill
|
|
882
|
-
zip_result = pkg_skill(SKILLS_ROOT
|
|
921
|
+
zip_result = pkg_skill(safe_skill_path(SKILLS_ROOT, skill_name))
|
|
883
922
|
result["steps"]["10_package"] = zip_result
|
|
884
923
|
result["zip_path"] = zip_result.get("zip_path") if zip_result["success"] else None
|
|
885
924
|
if verbose:
|
|
@@ -936,8 +975,8 @@ def uninstall_skill(skill_name: str, keep_backup: bool = True) -> dict:
|
|
|
936
975
|
"backup_path": None,
|
|
937
976
|
}
|
|
938
977
|
|
|
939
|
-
dest = SKILLS_ROOT
|
|
940
|
-
claude_dest = CLAUDE_SKILLS
|
|
978
|
+
dest = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
979
|
+
claude_dest = safe_skill_path(CLAUDE_SKILLS, skill_name)
|
|
941
980
|
|
|
942
981
|
if not dest.exists() and not claude_dest.exists():
|
|
943
982
|
result["error"] = f"Skill '{skill_name}' not found in any location"
|
|
@@ -946,7 +985,7 @@ def uninstall_skill(skill_name: str, keep_backup: bool = True) -> dict:
|
|
|
946
985
|
# Backup before removing
|
|
947
986
|
if keep_backup and dest.exists():
|
|
948
987
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
949
|
-
backup_path = BACKUPS_DIR
|
|
988
|
+
backup_path = safe_child_path(BACKUPS_DIR, f"{skill_name}_{timestamp}")
|
|
950
989
|
BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
|
|
951
990
|
try:
|
|
952
991
|
shutil.copytree(dest, backup_path, dirs_exist_ok=True)
|
|
@@ -976,7 +1015,7 @@ def uninstall_skill(skill_name: str, keep_backup: bool = True) -> dict:
|
|
|
976
1015
|
registry_result = step8_update_registry()
|
|
977
1016
|
|
|
978
1017
|
# Remove ZIP from Desktop if exists
|
|
979
|
-
zip_path = Path(os.path.expanduser("~")) / "Desktop"
|
|
1018
|
+
zip_path = safe_child_path(Path(os.path.expanduser("~")) / "Desktop", f"{skill_name}.zip")
|
|
980
1019
|
if zip_path.exists():
|
|
981
1020
|
try:
|
|
982
1021
|
zip_path.unlink()
|
|
@@ -1267,7 +1306,7 @@ def rollback_skill(skill_name: str, verbose: bool = True) -> dict:
|
|
|
1267
1306
|
print(f" Backup: {latest_backup.name} ({timestamp})")
|
|
1268
1307
|
|
|
1269
1308
|
# Restore to skills root
|
|
1270
|
-
dest = SKILLS_ROOT
|
|
1309
|
+
dest = safe_skill_path(SKILLS_ROOT, skill_name)
|
|
1271
1310
|
if verbose:
|
|
1272
1311
|
_step(1, 3, "Restoring from backup...")
|
|
1273
1312
|
|
|
@@ -46,6 +46,23 @@ EXCLUDE_EXTENSIONS = {
|
|
|
46
46
|
".pyc", ".pyo", ".db", ".sqlite", ".sqlite3",
|
|
47
47
|
".log", ".tmp", ".bak",
|
|
48
48
|
}
|
|
49
|
+
SAFE_ARCHIVE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_existing_dir(path) -> Path:
|
|
53
|
+
"""Resolve a user-provided directory and require it to exist."""
|
|
54
|
+
resolved = Path(path).expanduser().resolve()
|
|
55
|
+
if not resolved.is_dir():
|
|
56
|
+
raise ValueError(f"Directory not found: {resolved}")
|
|
57
|
+
return resolved
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_output_dir(path) -> Path:
|
|
61
|
+
"""Resolve a user-provided output directory."""
|
|
62
|
+
resolved = Path(path).expanduser().resolve()
|
|
63
|
+
if resolved.exists() and not resolved.is_dir():
|
|
64
|
+
raise ValueError(f"Output path is not a directory: {resolved}")
|
|
65
|
+
return resolved
|
|
49
66
|
|
|
50
67
|
|
|
51
68
|
# ── YAML Frontmatter Parser ───────────────────────────────────────────────
|
|
@@ -131,6 +148,12 @@ def validate_for_web(skill_dir: Path) -> dict:
|
|
|
131
148
|
|
|
132
149
|
def should_include(file_path: Path, skill_dir: Path) -> bool:
|
|
133
150
|
"""Check if a file should be included in the ZIP."""
|
|
151
|
+
if file_path.is_symlink():
|
|
152
|
+
return False
|
|
153
|
+
try:
|
|
154
|
+
file_path.resolve(strict=True).relative_to(skill_dir.resolve(strict=True))
|
|
155
|
+
except (OSError, ValueError):
|
|
156
|
+
return False
|
|
134
157
|
rel = file_path.relative_to(skill_dir)
|
|
135
158
|
|
|
136
159
|
# Check directory exclusions
|
|
@@ -163,10 +186,10 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict:
|
|
|
163
186
|
├── references/
|
|
164
187
|
└── ...
|
|
165
188
|
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return {"success": False, "error":
|
|
189
|
+
try:
|
|
190
|
+
skill_dir = resolve_existing_dir(skill_dir)
|
|
191
|
+
except ValueError as e:
|
|
192
|
+
return {"success": False, "error": str(e)}
|
|
170
193
|
|
|
171
194
|
# Validate
|
|
172
195
|
validation = validate_for_web(skill_dir)
|
|
@@ -179,11 +202,16 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict:
|
|
|
179
202
|
|
|
180
203
|
skill_name = validation["name"] or skill_dir.name
|
|
181
204
|
skill_name_lower = skill_name.lower()
|
|
205
|
+
if not SAFE_ARCHIVE_NAME_RE.fullmatch(skill_name_lower):
|
|
206
|
+
return {"success": False, "error": f"Unsafe archive skill name: {skill_name}"}
|
|
182
207
|
|
|
183
208
|
# Determine output path
|
|
184
209
|
if output_dir is None:
|
|
185
210
|
output_dir = DEFAULT_OUTPUT
|
|
186
|
-
|
|
211
|
+
try:
|
|
212
|
+
output_dir = resolve_output_dir(output_dir)
|
|
213
|
+
except ValueError as e:
|
|
214
|
+
return {"success": False, "error": str(e)}
|
|
187
215
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
188
216
|
|
|
189
217
|
zip_path = output_dir / f"{skill_name_lower}.zip"
|
|
@@ -382,10 +410,10 @@ def main():
|
|
|
382
410
|
if "--output" in args:
|
|
383
411
|
idx = args.index("--output")
|
|
384
412
|
if idx + 1 < len(args):
|
|
385
|
-
output_dir =
|
|
413
|
+
output_dir = resolve_output_dir(args[idx + 1])
|
|
386
414
|
|
|
387
415
|
if do_verify:
|
|
388
|
-
result = verify_zips(
|
|
416
|
+
result = verify_zips(output_dir if output_dir else None)
|
|
389
417
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
390
418
|
sys.exit(0 if result["invalid"] == 0 else 1)
|
|
391
419
|
|
|
@@ -404,7 +432,7 @@ def main():
|
|
|
404
432
|
sys.exit(1)
|
|
405
433
|
|
|
406
434
|
if source:
|
|
407
|
-
result = package_skill(
|
|
435
|
+
result = package_skill(source, output_dir)
|
|
408
436
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
409
437
|
sys.exit(0 if result["success"] else 1)
|
|
410
438
|
elif do_all:
|
|
@@ -41,6 +41,14 @@ SKILLS_ROOT = Path(r"C:\Users\renat\skills")
|
|
|
41
41
|
REGISTRY_PATH = SKILLS_ROOT / "agent-orchestrator" / "data" / "registry.json"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def resolve_existing_dir(path) -> Path:
|
|
45
|
+
"""Resolve a user-provided directory and require it to exist."""
|
|
46
|
+
resolved = Path(path).expanduser().resolve()
|
|
47
|
+
if not resolved.is_dir():
|
|
48
|
+
raise ValueError(f"Directory does not exist: {resolved}")
|
|
49
|
+
return resolved
|
|
50
|
+
|
|
51
|
+
|
|
44
52
|
# ── YAML Frontmatter Parser ───────────────────────────────────────────────
|
|
45
53
|
|
|
46
54
|
def parse_yaml_frontmatter(path: Path) -> dict:
|
|
@@ -347,15 +355,15 @@ def validate(skill_dir: Path, strict: bool = False, registry_path: Path = None)
|
|
|
347
355
|
if registry_path is None:
|
|
348
356
|
registry_path = REGISTRY_PATH
|
|
349
357
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
358
|
+
try:
|
|
359
|
+
skill_dir = resolve_existing_dir(skill_dir)
|
|
360
|
+
except ValueError as e:
|
|
353
361
|
return {
|
|
354
362
|
"valid": False,
|
|
355
|
-
"skill_dir": str(skill_dir),
|
|
363
|
+
"skill_dir": str(Path(skill_dir).expanduser()),
|
|
356
364
|
"checks": [],
|
|
357
365
|
"warnings": [],
|
|
358
|
-
"errors": [
|
|
366
|
+
"errors": [str(e)],
|
|
359
367
|
}
|
|
360
368
|
|
|
361
369
|
# Parse frontmatter once
|
|
@@ -411,14 +419,21 @@ def main():
|
|
|
411
419
|
}, indent=2))
|
|
412
420
|
sys.exit(1)
|
|
413
421
|
|
|
414
|
-
|
|
422
|
+
try:
|
|
423
|
+
skill_dir = resolve_existing_dir(sys.argv[1])
|
|
424
|
+
except ValueError as e:
|
|
425
|
+
print(json.dumps({
|
|
426
|
+
"valid": False,
|
|
427
|
+
"error": str(e),
|
|
428
|
+
}, indent=2))
|
|
429
|
+
sys.exit(1)
|
|
415
430
|
strict = "--strict" in sys.argv
|
|
416
431
|
registry_path = None
|
|
417
432
|
|
|
418
433
|
if "--registry" in sys.argv:
|
|
419
434
|
idx = sys.argv.index("--registry")
|
|
420
435
|
if idx + 1 < len(sys.argv):
|
|
421
|
-
registry_path = Path(sys.argv[idx + 1])
|
|
436
|
+
registry_path = Path(sys.argv[idx + 1]).expanduser().resolve()
|
|
422
437
|
|
|
423
438
|
result = validate(skill_dir, strict=strict, registry_path=registry_path)
|
|
424
439
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -116,6 +116,66 @@ CREATE INDEX IF NOT EXISTS idx_history_time ON score_history (recorded_at);
|
|
|
116
116
|
CREATE INDEX IF NOT EXISTS idx_action_log_time ON action_log (created_at);
|
|
117
117
|
"""
|
|
118
118
|
|
|
119
|
+
_SKILL_SNAPSHOT_COLUMNS = frozenset({
|
|
120
|
+
"audit_run_id", "skill_name", "skill_path", "version", "file_count",
|
|
121
|
+
"line_count", "overall_score", "code_quality", "security", "performance",
|
|
122
|
+
"governance", "documentation", "dependencies", "raw_metrics", "created_at",
|
|
123
|
+
})
|
|
124
|
+
_FINDING_COLUMNS = frozenset({
|
|
125
|
+
"audit_run_id", "skill_name", "dimension", "severity", "category", "title",
|
|
126
|
+
"description", "file_path", "line_number", "recommendation", "effort",
|
|
127
|
+
"impact", "created_at",
|
|
128
|
+
})
|
|
129
|
+
_RECOMMENDATION_COLUMNS = frozenset({
|
|
130
|
+
"audit_run_id", "suggested_name", "rationale", "capabilities", "priority",
|
|
131
|
+
"skill_md_draft", "created_at",
|
|
132
|
+
})
|
|
133
|
+
_SKILL_SNAPSHOT_INSERT_COLUMNS = (
|
|
134
|
+
"audit_run_id", "skill_name", "skill_path", "version", "file_count",
|
|
135
|
+
"line_count", "overall_score", "code_quality", "security", "performance",
|
|
136
|
+
"governance", "documentation", "dependencies", "raw_metrics",
|
|
137
|
+
)
|
|
138
|
+
_FINDING_INSERT_COLUMNS = (
|
|
139
|
+
"audit_run_id", "skill_name", "dimension", "severity", "category", "title",
|
|
140
|
+
"description", "file_path", "line_number", "recommendation", "effort",
|
|
141
|
+
"impact",
|
|
142
|
+
)
|
|
143
|
+
_RECOMMENDATION_INSERT_COLUMNS = (
|
|
144
|
+
"audit_run_id", "suggested_name", "rationale", "capabilities", "priority",
|
|
145
|
+
"skill_md_draft",
|
|
146
|
+
)
|
|
147
|
+
_INSERT_SKILL_SNAPSHOT_SQL = """
|
|
148
|
+
INSERT INTO skill_snapshots (
|
|
149
|
+
audit_run_id, skill_name, skill_path, version, file_count, line_count,
|
|
150
|
+
overall_score, code_quality, security, performance, governance,
|
|
151
|
+
documentation, dependencies, raw_metrics
|
|
152
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
153
|
+
"""
|
|
154
|
+
_INSERT_FINDING_SQL = """
|
|
155
|
+
INSERT INTO findings (
|
|
156
|
+
audit_run_id, skill_name, dimension, severity, category, title,
|
|
157
|
+
description, file_path, line_number, recommendation, effort, impact
|
|
158
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
159
|
+
"""
|
|
160
|
+
_INSERT_RECOMMENDATION_SQL = """
|
|
161
|
+
INSERT INTO skill_recommendations (
|
|
162
|
+
audit_run_id, suggested_name, rationale, capabilities, priority, skill_md_draft
|
|
163
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _quote_identifier(name: str, allowed: frozenset[str]) -> str:
|
|
168
|
+
if name not in allowed:
|
|
169
|
+
raise ValueError(f"Invalid column name: {name}")
|
|
170
|
+
return '"' + name.replace('"', '""') + '"'
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _filter_allowed_columns(data: Dict[str, Any], allowed: frozenset[str]) -> Dict[str, Any]:
|
|
174
|
+
filtered = {k: v for k, v in data.items() if k in allowed}
|
|
175
|
+
if not filtered:
|
|
176
|
+
raise ValueError("No valid columns provided")
|
|
177
|
+
return filtered
|
|
178
|
+
|
|
119
179
|
|
|
120
180
|
class Database:
|
|
121
181
|
def __init__(self, db_path: Path = DB_PATH):
|
|
@@ -185,12 +245,10 @@ class Database:
|
|
|
185
245
|
data["audit_run_id"] = run_id
|
|
186
246
|
if "raw_metrics" in data and isinstance(data["raw_metrics"], dict):
|
|
187
247
|
data["raw_metrics"] = json.dumps(data["raw_metrics"], ensure_ascii=False)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
columns = ", ".join(keys)
|
|
191
|
-
sql = f"INSERT INTO skill_snapshots ({columns}) VALUES ({placeholders})"
|
|
248
|
+
data = _filter_allowed_columns(data, _SKILL_SNAPSHOT_COLUMNS)
|
|
249
|
+
values = [data.get(column) for column in _SKILL_SNAPSHOT_INSERT_COLUMNS]
|
|
192
250
|
with self._connect() as conn:
|
|
193
|
-
cursor = conn.execute(
|
|
251
|
+
cursor = conn.execute(_INSERT_SKILL_SNAPSHOT_SQL, values)
|
|
194
252
|
return cursor.lastrowid
|
|
195
253
|
|
|
196
254
|
def get_snapshots_for_run(self, run_id: int) -> List[Dict[str, Any]]:
|
|
@@ -216,12 +274,10 @@ class Database:
|
|
|
216
274
|
def insert_finding(self, run_id: int, data: Dict[str, Any]) -> int:
|
|
217
275
|
"""Insere um finding. Retorna o id."""
|
|
218
276
|
data["audit_run_id"] = run_id
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
columns = ", ".join(keys)
|
|
222
|
-
sql = f"INSERT INTO findings ({columns}) VALUES ({placeholders})"
|
|
277
|
+
data = _filter_allowed_columns(data, _FINDING_COLUMNS)
|
|
278
|
+
values = [data.get(column) for column in _FINDING_INSERT_COLUMNS]
|
|
223
279
|
with self._connect() as conn:
|
|
224
|
-
cursor = conn.execute(
|
|
280
|
+
cursor = conn.execute(_INSERT_FINDING_SQL, values)
|
|
225
281
|
return cursor.lastrowid
|
|
226
282
|
|
|
227
283
|
def insert_findings_batch(self, run_id: int, findings: List[Dict[str, Any]]) -> int:
|
|
@@ -269,12 +325,10 @@ class Database:
|
|
|
269
325
|
data["audit_run_id"] = run_id
|
|
270
326
|
if "capabilities" in data and isinstance(data["capabilities"], list):
|
|
271
327
|
data["capabilities"] = json.dumps(data["capabilities"], ensure_ascii=False)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
columns = ", ".join(keys)
|
|
275
|
-
sql = f"INSERT INTO skill_recommendations ({columns}) VALUES ({placeholders})"
|
|
328
|
+
data = _filter_allowed_columns(data, _RECOMMENDATION_COLUMNS)
|
|
329
|
+
values = [data.get(column) for column in _RECOMMENDATION_INSERT_COLUMNS]
|
|
276
330
|
with self._connect() as conn:
|
|
277
|
-
cursor = conn.execute(
|
|
331
|
+
cursor = conn.execute(_INSERT_RECOMMENDATION_SQL, values)
|
|
278
332
|
return cursor.lastrowid
|
|
279
333
|
|
|
280
334
|
def get_recommendations_for_run(self, run_id: int) -> List[Dict[str, Any]]:
|
|
@@ -10,11 +10,21 @@ Usage:
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
|
+
import http.client
|
|
13
14
|
import json
|
|
14
15
|
import os
|
|
16
|
+
import re
|
|
15
17
|
import sys
|
|
16
|
-
from urllib.
|
|
17
|
-
|
|
18
|
+
from urllib.parse import urlparse
|
|
19
|
+
|
|
20
|
+
ALLOWED_METHODS = {
|
|
21
|
+
"sendMessage",
|
|
22
|
+
"sendPhoto",
|
|
23
|
+
"sendDocument",
|
|
24
|
+
"sendLocation",
|
|
25
|
+
"sendPoll",
|
|
26
|
+
}
|
|
27
|
+
BOT_TOKEN_RE = re.compile(r"^\d{6,20}:[A-Za-z0-9_-]{20,}$")
|
|
18
28
|
|
|
19
29
|
|
|
20
30
|
def _mask_token(token: str) -> str:
|
|
@@ -24,18 +34,38 @@ def _mask_token(token: str) -> str:
|
|
|
24
34
|
return f"{token[:8]}...masked"
|
|
25
35
|
|
|
26
36
|
|
|
37
|
+
def _safe_api_url(token: str, method: str) -> str:
|
|
38
|
+
if not BOT_TOKEN_RE.match(token or ""):
|
|
39
|
+
raise ValueError("Invalid Telegram bot token format")
|
|
40
|
+
if method not in ALLOWED_METHODS:
|
|
41
|
+
raise ValueError(f"Unsupported Telegram method: {method}")
|
|
42
|
+
url = f"https://api.telegram.org/bot{token}/{method}"
|
|
43
|
+
parsed = urlparse(url)
|
|
44
|
+
if parsed.scheme != "https" or parsed.hostname != "api.telegram.org":
|
|
45
|
+
raise ValueError("Refusing unsafe Telegram API URL")
|
|
46
|
+
return url
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _safe_api_path(token: str, method: str) -> str:
|
|
50
|
+
_safe_api_url(token, method)
|
|
51
|
+
return f"/bot{token}/{method}"
|
|
52
|
+
|
|
53
|
+
|
|
27
54
|
def api_call(token: str, method: str, data: dict) -> dict:
|
|
28
55
|
"""Make a Telegram Bot API call."""
|
|
29
|
-
|
|
56
|
+
api_path = _safe_api_path(token, method)
|
|
30
57
|
payload = json.dumps(data).encode("utf-8")
|
|
31
|
-
|
|
58
|
+
headers = {"Content-Type": "application/json"}
|
|
59
|
+
conn = http.client.HTTPSConnection("api.telegram.org", timeout=30)
|
|
32
60
|
|
|
33
61
|
try:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
62
|
+
conn.request("POST", api_path, body=payload, headers=headers)
|
|
63
|
+
resp = conn.getresponse()
|
|
64
|
+
body = resp.read().decode()
|
|
65
|
+
parsed = json.loads(body)
|
|
66
|
+
return parsed
|
|
67
|
+
finally:
|
|
68
|
+
conn.close()
|
|
39
69
|
|
|
40
70
|
|
|
41
71
|
def send_text(token: str, chat_id: str, text: str, parse_mode: str = None,
|