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.
Files changed (59) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +1 -1
  2. package/bundled-skills/007/scripts/full_audit.py +10 -3
  3. package/bundled-skills/2slides-ppt-generator/requirements.txt +3 -1
  4. package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +24 -0
  5. package/bundled-skills/2slides-ppt-generator/scripts/get_job_status.py +18 -1
  6. package/bundled-skills/agent-creator/SKILL.md +15 -1
  7. package/bundled-skills/agent-orchestrator/scripts/scan_registry.py +4 -4
  8. package/bundled-skills/android-dev/references/hybrid.md +3 -3
  9. package/bundled-skills/android-dev/references/react-native.md +14 -8
  10. package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +82 -20
  11. package/bundled-skills/diary/requirements.txt +3 -1
  12. package/bundled-skills/docs/users/getting-started.md +1 -1
  13. package/bundled-skills/docx-official/ooxml/scripts/validation/base.py +19 -1
  14. package/bundled-skills/docx-official/ooxml/scripts/validation/docx.py +20 -1
  15. package/bundled-skills/docx-official/ooxml/scripts/validation/redlining.py +21 -5
  16. package/bundled-skills/ecl-harness-engineer/references/environment-detection-guide.md +1 -1
  17. package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +44 -23
  18. package/bundled-skills/instagram/scripts/db.py +120 -18
  19. package/bundled-skills/instagram/scripts/export.py +41 -8
  20. package/bundled-skills/instagram/scripts/publish.py +7 -7
  21. package/bundled-skills/instagram/scripts/run_all.py +2 -2
  22. package/bundled-skills/instagram/scripts/schedule.py +6 -5
  23. package/bundled-skills/instagram/static/dashboard.html +63 -16
  24. package/bundled-skills/junta-leiloeiros/scripts/requirements.txt +1 -1
  25. package/bundled-skills/k8s-manifest-generator/assets/deployment-template.yaml +20 -8
  26. package/bundled-skills/k8s-manifest-generator/assets/service-template.yaml +2 -3
  27. package/bundled-skills/last30days/scripts/lib/reddit_enrich.py +3 -1
  28. package/bundled-skills/loki-mode/benchmarks/results/2026-01-05-00-49-17/humaneval-solutions/162.py +1 -1
  29. package/bundled-skills/loki-mode/benchmarks/results/humaneval-loki-solutions/162.py +1 -1
  30. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/src/index.ts +1 -0
  31. package/bundled-skills/loop-library/SKILL.md +11 -11
  32. package/bundled-skills/mcp-builder/scripts/evaluation.py +6 -2
  33. package/bundled-skills/notebooklm/scripts/run.py +23 -8
  34. package/bundled-skills/playwright-skill/lib/helpers.js +15 -17
  35. package/bundled-skills/pptx-official/ooxml/scripts/validation/base.py +19 -1
  36. package/bundled-skills/pptx-official/ooxml/scripts/validation/docx.py +20 -1
  37. package/bundled-skills/pptx-official/ooxml/scripts/validation/redlining.py +21 -5
  38. package/bundled-skills/remote-gpu-trainer/profiles/runpod.md +2 -2
  39. package/bundled-skills/senior-frontend/scripts/component_generator.py +67 -10
  40. package/bundled-skills/shopify-development/scripts/requirements.txt +1 -0
  41. package/bundled-skills/shopify-development/scripts/tests/test_shopify_init.py +13 -9
  42. package/bundled-skills/skill-installer/scripts/install_skill.py +73 -34
  43. package/bundled-skills/skill-installer/scripts/package_skill.py +36 -8
  44. package/bundled-skills/skill-installer/scripts/validate_skill.py +22 -7
  45. package/bundled-skills/skill-sentinel/scripts/db.py +69 -15
  46. package/bundled-skills/slack-gif-creator/requirements.txt +3 -2
  47. package/bundled-skills/stability-ai/scripts/requirements.txt +1 -1
  48. package/bundled-skills/telegram/assets/boilerplate/nodejs/src/bot-client.ts +1 -0
  49. package/bundled-skills/telegram/assets/boilerplate/python/requirements.txt +3 -1
  50. package/bundled-skills/telegram/scripts/send_message.py +39 -9
  51. package/bundled-skills/webapp-testing/scripts/with_server.py +105 -8
  52. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/nodejs/src/index.ts +1 -0
  53. package/bundled-skills/whatsapp-cloud-api/assets/boilerplate/python/requirements.txt +1 -0
  54. package/bundled-skills/whatsapp-cloud-api/scripts/setup_project.py +31 -3
  55. package/bundled-skills/writing-skills/render-graphs.js +30 -5
  56. package/bundled-skills/youtube-notetaker/reference/artifact.html +29 -18
  57. package/bundled-skills/youtube-notetaker/scripts/serve.py +49 -8
  58. package/package.json +2 -2
  59. 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 MD5 hash of all files in a directory.
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
- h = hashlib.md5()
139
- for root, dirs, files in os.walk(path):
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 = fp.relative_to(path).as_posix()
181
+ rel = resolved_fp.relative_to(root_path).as_posix()
147
182
  h.update(rel.encode("utf-8"))
148
- with open(fp, "rb") as fh:
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
- source_path = Path(source).resolve()
266
- if not source_path.exists():
267
- return {"success": False, "error": f"Source does not exist: {source_path}"}
268
- if not (source_path / "SKILL.md").exists():
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 / skill_name
320
- claude_dest = CLAUDE_SKILLS / skill_name
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 / skill_name
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 / backup_name
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 / skill_name
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
- dest = SKILLS_ROOT / skill_name
392
- staging = STAGING_DIR / skill_name
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
- source_skill_md = SKILLS_ROOT / skill_name / "SKILL.md"
452
- claude_dest_dir = CLAUDE_SKILLS / skill_name
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 = SKILLS_ROOT / skill_name / "references"
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 / skill_name
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 / skill_name / "SKILL.md"
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 / skill_name),
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
- if not source.exists() or not (source / "SKILL.md").exists():
650
- result["error"] = f"Invalid source: {source}"
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(f"Source invalid: {source}")
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 / skill_name
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 / skill_name)
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 / skill_name
940
- claude_dest = CLAUDE_SKILLS / skill_name
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 / f"{skill_name}_{timestamp}"
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" / f"{skill_name}.zip"
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 / skill_name
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
- skill_dir = Path(skill_dir).resolve()
167
-
168
- if not skill_dir.exists():
169
- return {"success": False, "error": f"Directory not found: {skill_dir}"}
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
- output_dir = Path(output_dir).resolve()
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 = Path(args[idx + 1])
413
+ output_dir = resolve_output_dir(args[idx + 1])
386
414
 
387
415
  if do_verify:
388
- result = verify_zips(Path(output_dir) if output_dir else None)
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(Path(source), output_dir)
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
- skill_dir = Path(skill_dir).resolve()
351
-
352
- if not skill_dir.exists():
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": [f"Directory does not exist: {skill_dir}"],
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
- skill_dir = Path(sys.argv[1]).resolve()
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
- keys = list(data.keys())
189
- placeholders = ", ".join(f":{k}" for k in keys)
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(sql, data)
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
- keys = list(data.keys())
220
- placeholders = ", ".join(f":{k}" for k in keys)
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(sql, data)
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
- keys = list(data.keys())
273
- placeholders = ", ".join(f":{k}" for k in keys)
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(sql, data)
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]]:
@@ -1,4 +1,5 @@
1
- pillow>=10.0.0
1
+ pillow>=12.2.0
2
2
  imageio>=2.31.0
3
3
  imageio-ffmpeg>=0.4.9
4
- numpy>=1.24.0
4
+ numpy>=1.24.0
5
+ setuptools>=78.1.1
@@ -1,4 +1,4 @@
1
1
  # Stability AI Skill - Dependencies
2
2
  # Instalacao: pip install -r requirements.txt
3
3
 
4
- Pillow>=10.0.0
4
+ Pillow>=12.2.0
@@ -19,6 +19,7 @@ export class TelegramBotClient {
19
19
 
20
20
  async startWebhook(port: number, webhookUrl: string, secret?: string): Promise<void> {
21
21
  const app = express();
22
+ app.disable('x-powered-by');
22
23
  app.use(express.json());
23
24
 
24
25
  app.post('/webhook', async (req, res) => {
@@ -1,4 +1,6 @@
1
1
  python-telegram-bot>=21.0
2
2
  python-dotenv>=1.0.0
3
3
  flask>=3.0.0
4
- requests>=2.31.0
4
+ requests>=2.33.0
5
+ urllib3>=2.7.0
6
+ idna>=3.15
@@ -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.request import urlopen, Request
17
- from urllib.error import HTTPError
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
- url = f"https://api.telegram.org/bot{token}/{method}"
56
+ api_path = _safe_api_path(token, method)
30
57
  payload = json.dumps(data).encode("utf-8")
31
- req = Request(url, data=payload, headers={"Content-Type": "application/json"})
58
+ headers = {"Content-Type": "application/json"}
59
+ conn = http.client.HTTPSConnection("api.telegram.org", timeout=30)
32
60
 
33
61
  try:
34
- with urlopen(req, timeout=30) as resp:
35
- return json.loads(resp.read().decode())
36
- except HTTPError as e:
37
- error_body = json.loads(e.read().decode())
38
- return error_body
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,