opencode-skills-collection 3.1.14 → 3.1.16
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 +36 -1
- package/bundled-skills/007/scripts/full_audit.py +34 -22
- package/bundled-skills/007/scripts/quick_scan.py +24 -15
- package/bundled-skills/007/scripts/scanners/dependency_scanner.py +31 -19
- package/bundled-skills/007/scripts/scanners/injection_scanner.py +24 -14
- package/bundled-skills/007/scripts/scanners/secrets_scanner.py +24 -14
- package/bundled-skills/007/scripts/score_calculator.py +25 -13
- package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +15 -1
- package/bundled-skills/aegisops-ai/SKILL.md +1 -1
- package/bundled-skills/agent-self-scheduling/SKILL.md +88 -0
- package/bundled-skills/alpha-vantage/SKILL.md +3 -1
- package/bundled-skills/anti-sleep/SKILL.md +90 -0
- package/bundled-skills/api-security-best-practices/SKILL.md +1 -1
- package/bundled-skills/app-store-optimization/SKILL.md +7 -0
- package/bundled-skills/audio-transcriber/scripts/transcribe.py +17 -4
- package/bundled-skills/azure-mgmt-botservice-py/SKILL.md +2 -1
- package/bundled-skills/azure-resource-manager-mysql-dotnet/SKILL.md +2 -1
- package/bundled-skills/azure-resource-manager-postgresql-dotnet/SKILL.md +2 -1
- package/bundled-skills/azure-resource-manager-sql-dotnet/SKILL.md +2 -1
- package/bundled-skills/biopython/SKILL.md +2 -1
- package/bundled-skills/brain-to-docs/SKILL.md +54 -0
- package/bundled-skills/browser-harness/SKILL.md +220 -0
- package/bundled-skills/browser-harness/references/install.md +132 -0
- package/bundled-skills/bumblebee/scripts/render_report.py +16 -3
- package/bundled-skills/bun-development/SKILL.md +1 -1
- package/bundled-skills/cc-skill-security-review/SKILL.md +2 -2
- package/bundled-skills/claude-monitor/scripts/monitor.py +15 -2
- package/bundled-skills/cmux/SKILL.md +250 -0
- package/bundled-skills/code-polish/SKILL.md +192 -0
- package/bundled-skills/code-review-checklist/SKILL.md +1 -1
- package/bundled-skills/codex-subagent/SKILL.md +117 -0
- package/bundled-skills/competitor-analysis/scripts/capture_screenshots.mjs +24 -3
- package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +13 -2
- package/bundled-skills/competitor-analysis/scripts/extract_vs_names.mjs +23 -2
- package/bundled-skills/competitor-analysis/scripts/gate_candidates.mjs +21 -0
- package/bundled-skills/competitor-analysis/scripts/list_urls.mjs +23 -2
- package/bundled-skills/competitor-analysis/scripts/merge_partials.mjs +24 -3
- package/bundled-skills/container-security-hardening/SKILL.md +1 -1
- package/bundled-skills/content-creator/scripts/brand_voice_analyzer.py +15 -1
- package/bundled-skills/content-creator/scripts/seo_optimizer.py +15 -1
- package/bundled-skills/context-kit/SKILL.md +135 -0
- package/bundled-skills/cyber-audit/SKILL.md +138 -0
- package/bundled-skills/deepapi/SKILL.md +649 -0
- package/bundled-skills/delegating-to-agents/SKILL.md +64 -0
- package/bundled-skills/developer-signup-flow/SKILL.md +1 -1
- package/bundled-skills/diary/scripts/fetch_diaries.py +14 -1
- package/bundled-skills/diary/scripts/prepare_context.py +15 -2
- package/bundled-skills/diary/scripts/sync_to_notion.py +14 -1
- package/bundled-skills/dispatch/SKILL.md +19 -7
- package/bundled-skills/distribute-skill-to-all-agents/SKILL.md +85 -0
- package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
- package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
- package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
- package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
- package/bundled-skills/docs/users/bundles.md +1 -1
- package/bundled-skills/docs/users/claude-code-skills.md +1 -1
- package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
- package/bundled-skills/docs/users/getting-started.md +1 -1
- package/bundled-skills/docs/users/kiro-integration.md +1 -1
- package/bundled-skills/docs/users/usage.md +4 -4
- package/bundled-skills/docs/users/visual-guide.md +4 -4
- package/bundled-skills/docx-official/ooxml/scripts/pack.py +30 -3
- package/bundled-skills/docx-official/ooxml/scripts/unpack.py +16 -3
- package/bundled-skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py +27 -17
- package/bundled-skills/ecl-harness-engineer/agents/creator-config.md +17 -3
- package/bundled-skills/ecl-harness-engineer/references/environment-detection-guide.md +9 -5
- package/bundled-skills/effective-agent-skills/SKILL.md +333 -0
- package/bundled-skills/electron-development/SKILL.md +6 -2
- package/bundled-skills/environment-setup-guide/SKILL.md +2 -5
- package/bundled-skills/expo-ui/scripts/list-components.js +5 -1
- package/bundled-skills/fable-safe-prompt/SKILL.md +79 -0
- package/bundled-skills/folder-specific-claude-and-agents-md/SKILL.md +96 -0
- package/bundled-skills/frontend-slides/scripts/extract-pptx.py +18 -4
- package/bundled-skills/gcp-cloud-run/SKILL.md +2 -1
- package/bundled-skills/gemini-api-integration/SKILL.md +3 -1
- package/bundled-skills/gemini-live-api-dev/SKILL.md +2 -1
- package/bundled-skills/gemini-omni-flash-api/scripts/video/generate_video.py +49 -14
- package/bundled-skills/goal-loop/SKILL.md +175 -0
- package/bundled-skills/hugging-face-evaluation/SKILL.md +1 -1
- package/bundled-skills/hugging-face-jobs/SKILL.md +1 -1
- package/bundled-skills/hugging-face-jobs/scripts/finepdfs-stats.py +14 -1
- package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +10 -1
- package/bundled-skills/hugo-to-markdown/scripts/inventory_hugo_rules.py +14 -1
- package/bundled-skills/image-generator/SKILL.md +3 -1
- package/bundled-skills/instagram/scripts/export.py +18 -7
- package/bundled-skills/interview-style-doc-building/SKILL.md +86 -0
- package/bundled-skills/junta-leiloeiros/scripts/export.py +19 -10
- package/bundled-skills/landing-page-generator/scripts/landing_page_scaffolder.py +15 -1
- package/bundled-skills/linear-claude-skill/SKILL.md +4 -2
- package/bundled-skills/linkedin-post-writer/SKILL.md +159 -0
- package/bundled-skills/linkedin-post-writer/references/hook-formulas.md +482 -0
- package/bundled-skills/llm-council/SKILL.md +4 -2
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +5 -6
- package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +2 -3
- package/bundled-skills/markdown-rendering/SKILL.md +72 -0
- package/bundled-skills/matematico-tao/scripts/complexity_analyzer.py +14 -1
- package/bundled-skills/matematico-tao/scripts/dependency_graph.py +15 -2
- package/bundled-skills/mobile-design/scripts/mobile_audit.py +20 -6
- package/bundled-skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py +15 -2
- package/bundled-skills/multi-agent-architect/SKILL.md +1 -1
- package/bundled-skills/odoo-rpc-api/SKILL.md +2 -1
- package/bundled-skills/pci-compliance/SKILL.md +3 -1
- package/bundled-skills/pdf-official/scripts/create_validation_image.py +17 -3
- package/bundled-skills/pdf-official/scripts/extract_form_field_info.py +1 -1
- package/bundled-skills/personal-tool-builder/SKILL.md +1 -1
- package/bundled-skills/pi-custom-model/SKILL.md +68 -0
- package/bundled-skills/pi-web-search/SKILL.md +74 -0
- package/bundled-skills/playwright-skill/run.js +19 -1
- package/bundled-skills/pptx-official/ooxml/scripts/pack.py +30 -3
- package/bundled-skills/pptx-official/ooxml/scripts/unpack.py +16 -3
- package/bundled-skills/pptx-official/scripts/inventory.py +17 -4
- package/bundled-skills/pptx-official/scripts/rearrange.py +15 -2
- package/bundled-skills/pptx-official/scripts/replace.py +16 -3
- package/bundled-skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +15 -1
- package/bundled-skills/production-code-audit/SKILL.md +1 -1
- package/bundled-skills/push-skill-to-github/SKILL.md +61 -0
- package/bundled-skills/rclone-cli/references/commands/rclone_completion_powershell.md +3 -2
- package/bundled-skills/read-all-adrs/SKILL.md +35 -0
- package/bundled-skills/remote-gpu-trainer/scripts/verify_local.py +14 -1
- package/bundled-skills/research-prompt/SKILL.md +63 -0
- package/bundled-skills/run-deep-swe/SKILL.md +120 -0
- package/bundled-skills/senior-architect/scripts/architecture_diagram_generator.py +15 -2
- package/bundled-skills/senior-architect/scripts/dependency_analyzer.py +15 -2
- package/bundled-skills/senior-architect/scripts/project_architect.py +15 -2
- package/bundled-skills/senior-frontend/scripts/bundle_analyzer.py +14 -1
- package/bundled-skills/senior-frontend/scripts/frontend_scaffolder.py +14 -1
- package/bundled-skills/senior-fullstack/scripts/code_quality_analyzer.py +15 -2
- package/bundled-skills/senior-fullstack/scripts/fullstack_scaffolder.py +15 -2
- package/bundled-skills/senior-fullstack/scripts/project_scaffolder.py +15 -2
- package/bundled-skills/setup-help/SKILL.md +50 -0
- package/bundled-skills/shodan-reconnaissance/SKILL.md +2 -1
- package/bundled-skills/short/SKILL.md +28 -0
- package/bundled-skills/skill-creator/scripts/init_skill.py +15 -2
- package/bundled-skills/skill-creator/scripts/package_skill.py +29 -12
- package/bundled-skills/skill-creator/scripts/quick_validate.py +14 -2
- package/bundled-skills/skill-creator-ms/SKILL.md +1 -1
- package/bundled-skills/skill-installer/scripts/install_skill.py +42 -8
- package/bundled-skills/skill-installer/scripts/package_skill.py +29 -17
- package/bundled-skills/skill-installer/scripts/validate_skill.py +37 -22
- package/bundled-skills/stripe-integration/SKILL.md +7 -3
- package/bundled-skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +7 -1
- package/bundled-skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +7 -1
- package/bundled-skills/taisly-social-media-posting/SKILL.md +79 -0
- package/bundled-skills/technical-tutorials/SKILL.md +1 -1
- package/bundled-skills/telegram/scripts/setup_project.py +17 -2
- package/bundled-skills/time-ledger/SKILL.md +118 -0
- package/bundled-skills/trading-ledger/SKILL.md +121 -0
- package/bundled-skills/videodb/scripts/ws_listener.py +17 -4
- package/bundled-skills/voice-ai-development/SKILL.md +2 -1
- package/bundled-skills/vps-server-management/SKILL.md +72 -0
- package/bundled-skills/weaviate/scripts/weaviate_conn.py +20 -18
- package/bundled-skills/wgm/SKILL.md +137 -0
- package/bundled-skills/whatsapp-cloud-api/scripts/setup_project.py +16 -1
- package/bundled-skills/workorai/SKILL.md +137 -0
- package/bundled-skills/wp-site-health-auditor/SKILL.md +341 -0
- package/bundled-skills/wp-site-health-auditor/references/catalog.md +135 -0
- package/bundled-skills/writing-skills/render-graphs.js +13 -2
- package/bundled-skills/x-twitter-scraper/SKILL.md +3 -1
- package/bundled-skills/youtube-notetaker/scripts/serve.py +41 -31
- package/bundled-skills/youtube-notetaker/scripts/vtt_to_transcript.py +16 -2
- package/bundled-skills/youtube-notetaker/scripts/write_library_item.py +15 -1
- package/bundled-skills/youtube-transcript/SKILL.md +120 -0
- package/package.json +5 -1
- package/skills_index.json +786 -6
|
@@ -437,8 +437,9 @@ shodan search 'ssl.cert.issuer.cn:self-signed'
|
|
|
437
437
|
#!/usr/bin/env python3
|
|
438
438
|
import shodan
|
|
439
439
|
import json
|
|
440
|
+
import os
|
|
440
441
|
|
|
441
|
-
API_KEY =
|
|
442
|
+
API_KEY = os.environ["SHODAN_API_KEY"]
|
|
442
443
|
api = shodan.Shodan(API_KEY)
|
|
443
444
|
|
|
444
445
|
def recon_organization(org_name):
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: short
|
|
3
|
+
description: "Rewrite the previous response more briefly while preserving the substance."
|
|
4
|
+
category: writing
|
|
5
|
+
risk: safe
|
|
6
|
+
source: community
|
|
7
|
+
source_repo: davidondrej/skills
|
|
8
|
+
source_type: community
|
|
9
|
+
date_added: "2026-07-07"
|
|
10
|
+
author: davidondrej
|
|
11
|
+
tags: [writing, editing, concise]
|
|
12
|
+
tools: [claude, codex]
|
|
13
|
+
license: "MIT"
|
|
14
|
+
license_source: "https://github.com/davidondrej/skills/blob/main/LICENSE"
|
|
15
|
+
disable-model-invocation: true
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
rewrite your last response to be simpler & shorter. do not do anything else.
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
|
|
22
|
+
- Use when the user asks for a shorter, simpler, or TLDR version of the previous response.
|
|
23
|
+
- Use when the current answer should be compressed without changing the substance.
|
|
24
|
+
|
|
25
|
+
## Limitations
|
|
26
|
+
|
|
27
|
+
- Adapted from `davidondrej/skills`; verify local paths, tools, credentials, and agent features before acting.
|
|
28
|
+
- For commands, remote access, scheduling, browser automation, or file-changing workflows, get explicit user approval and confirm the target environment first.
|
|
@@ -15,6 +15,19 @@ import sys
|
|
|
15
15
|
from pathlib import Path
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def safe_user_path(path_value, base_dir="."):
|
|
19
|
+
"""Resolve a CLI path under the current workspace."""
|
|
20
|
+
if base_dir != ".":
|
|
21
|
+
raise ValueError("Custom base directories are not supported for CLI paths")
|
|
22
|
+
base_path = Path.cwd().resolve()
|
|
23
|
+
resolved_path = Path(path_value).expanduser().resolve()
|
|
24
|
+
try:
|
|
25
|
+
resolved_path.relative_to(base_path)
|
|
26
|
+
except ValueError as exc:
|
|
27
|
+
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
|
28
|
+
return resolved_path
|
|
29
|
+
|
|
30
|
+
|
|
18
31
|
SKILL_TEMPLATE = """---
|
|
19
32
|
name: {skill_name}
|
|
20
33
|
description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.]
|
|
@@ -203,7 +216,7 @@ def init_skill(skill_name, path):
|
|
|
203
216
|
Path to created skill directory, or None if error
|
|
204
217
|
"""
|
|
205
218
|
# Determine skill directory path
|
|
206
|
-
skill_dir =
|
|
219
|
+
skill_dir = safe_user_path(path).resolve() / skill_name
|
|
207
220
|
|
|
208
221
|
# Check if directory already exists
|
|
209
222
|
if skill_dir.exists():
|
|
@@ -285,7 +298,7 @@ def main():
|
|
|
285
298
|
sys.exit(1)
|
|
286
299
|
|
|
287
300
|
skill_name = sys.argv[1]
|
|
288
|
-
path = sys.argv[3]
|
|
301
|
+
path = safe_user_path(sys.argv[3])
|
|
289
302
|
|
|
290
303
|
print(f"🚀 Initializing skill: {skill_name}")
|
|
291
304
|
print(f" Location: {path}")
|
|
@@ -12,7 +12,21 @@ Example:
|
|
|
12
12
|
|
|
13
13
|
import sys
|
|
14
14
|
import zipfile
|
|
15
|
+
import tempfile
|
|
15
16
|
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def safe_user_path(path_value, base_dir="."):
|
|
20
|
+
"""Resolve a CLI path under the current workspace."""
|
|
21
|
+
if base_dir != ".":
|
|
22
|
+
raise ValueError("Custom base directories are not supported for CLI paths")
|
|
23
|
+
base_path = Path.cwd().resolve()
|
|
24
|
+
resolved_path = Path(path_value).expanduser().resolve()
|
|
25
|
+
try:
|
|
26
|
+
resolved_path.relative_to(base_path)
|
|
27
|
+
except ValueError as exc:
|
|
28
|
+
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
|
29
|
+
return resolved_path
|
|
16
30
|
from quick_validate import validate_skill
|
|
17
31
|
|
|
18
32
|
|
|
@@ -37,7 +51,7 @@ def package_skill(skill_path, output_dir=None):
|
|
|
37
51
|
Returns:
|
|
38
52
|
Path to the created .skill file, or None if error
|
|
39
53
|
"""
|
|
40
|
-
skill_path =
|
|
54
|
+
skill_path = safe_user_path(skill_path).resolve()
|
|
41
55
|
|
|
42
56
|
# Validate skill folder exists
|
|
43
57
|
if not skill_path.exists():
|
|
@@ -66,7 +80,7 @@ def package_skill(skill_path, output_dir=None):
|
|
|
66
80
|
# Determine output location
|
|
67
81
|
skill_name = skill_path.name
|
|
68
82
|
if output_dir:
|
|
69
|
-
output_path =
|
|
83
|
+
output_path = safe_user_path(output_dir).resolve()
|
|
70
84
|
output_path.mkdir(parents=True, exist_ok=True)
|
|
71
85
|
else:
|
|
72
86
|
output_path = Path.cwd()
|
|
@@ -75,14 +89,17 @@ def package_skill(skill_path, output_dir=None):
|
|
|
75
89
|
|
|
76
90
|
# Create the .skill file (zip format)
|
|
77
91
|
try:
|
|
78
|
-
with
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
93
|
+
temp_zip_path = Path(temp_dir) / "skill.zip"
|
|
94
|
+
with zipfile.ZipFile(temp_zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
95
|
+
# Walk through the skill directory
|
|
96
|
+
for file_path in skill_path.rglob('*'):
|
|
97
|
+
if should_include(file_path, skill_path):
|
|
98
|
+
# Calculate the relative path within the zip
|
|
99
|
+
arcname = file_path.relative_to(skill_path.parent)
|
|
100
|
+
zipf.write(file_path, arcname)
|
|
101
|
+
print(f" Added: {arcname}")
|
|
102
|
+
skill_filename.write_bytes(temp_zip_path.read_bytes())
|
|
86
103
|
|
|
87
104
|
print(f"\n✅ Successfully packaged skill to: {skill_filename}")
|
|
88
105
|
return skill_filename
|
|
@@ -100,8 +117,8 @@ def main():
|
|
|
100
117
|
print(" python utils/package_skill.py skills/public/my-skill ./dist")
|
|
101
118
|
sys.exit(1)
|
|
102
119
|
|
|
103
|
-
skill_path = sys.argv[1]
|
|
104
|
-
output_dir = sys.argv[2] if len(sys.argv) > 2 else None
|
|
120
|
+
skill_path = safe_user_path(sys.argv[1])
|
|
121
|
+
output_dir = safe_user_path(sys.argv[2]) if len(sys.argv) > 2 else None
|
|
105
122
|
|
|
106
123
|
print(f"📦 Packaging skill: {skill_path}")
|
|
107
124
|
if output_dir:
|
|
@@ -9,9 +9,21 @@ import re
|
|
|
9
9
|
import yaml
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
def safe_user_path(path_value, base_dir="."):
|
|
13
|
+
"""Resolve a CLI path under the current workspace."""
|
|
14
|
+
if base_dir != ".":
|
|
15
|
+
raise ValueError("Custom base directories are not supported for CLI paths")
|
|
16
|
+
base_path = Path.cwd().resolve()
|
|
17
|
+
resolved_path = Path(path_value).expanduser().resolve()
|
|
18
|
+
try:
|
|
19
|
+
resolved_path.relative_to(base_path)
|
|
20
|
+
except ValueError as exc:
|
|
21
|
+
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
|
22
|
+
return resolved_path
|
|
23
|
+
|
|
12
24
|
def validate_skill(skill_path):
|
|
13
25
|
"""Basic validation of a skill"""
|
|
14
|
-
skill_path =
|
|
26
|
+
skill_path = safe_user_path(skill_path)
|
|
15
27
|
|
|
16
28
|
# Check SKILL.md exists
|
|
17
29
|
skill_md = skill_path / 'SKILL.md'
|
|
@@ -92,4 +104,4 @@ if __name__ == "__main__":
|
|
|
92
104
|
|
|
93
105
|
valid, message = validate_skill(sys.argv[1])
|
|
94
106
|
print(message)
|
|
95
|
-
sys.exit(0 if valid else 1)
|
|
107
|
+
sys.exit(0 if valid else 1)
|
|
@@ -412,7 +412,7 @@ client = MyClient(endpoint, credential)
|
|
|
412
412
|
|
|
413
413
|
#### ❌ INCORRECT: Hardcoded Credentials
|
|
414
414
|
\`\`\`python
|
|
415
|
-
client = MyClient(endpoint,
|
|
415
|
+
client = MyClient(endpoint, credential="[redacted API key]") # Security risk
|
|
416
416
|
\`\`\`
|
|
417
417
|
```
|
|
418
418
|
|
|
@@ -33,6 +33,40 @@ import re
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from datetime import datetime
|
|
35
35
|
|
|
36
|
+
|
|
37
|
+
def safe_user_path(path_value, base_dir="."):
|
|
38
|
+
"""Resolve a CLI path under the current workspace."""
|
|
39
|
+
if base_dir != ".":
|
|
40
|
+
raise ValueError("Custom base directories are not supported for CLI paths")
|
|
41
|
+
base_path = Path.cwd().resolve()
|
|
42
|
+
resolved_path = Path(path_value).expanduser().resolve()
|
|
43
|
+
try:
|
|
44
|
+
resolved_path.relative_to(base_path)
|
|
45
|
+
except ValueError as exc:
|
|
46
|
+
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
|
47
|
+
return resolved_path
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def copy_tree_contents(source_dir: Path, target_dir: Path, *, ignore=None) -> None:
|
|
51
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
|
|
53
|
+
for current_dir, dirs, files in os.walk(source_dir, followlinks=False):
|
|
54
|
+
current_path = Path(current_dir)
|
|
55
|
+
ignored_names = set(ignore(str(current_path), dirs + files)) if ignore is not None else set()
|
|
56
|
+
dirs[:] = [directory for directory in dirs if directory not in ignored_names]
|
|
57
|
+
|
|
58
|
+
relative_dir = current_path.relative_to(source_dir)
|
|
59
|
+
target_current_dir = target_dir / relative_dir
|
|
60
|
+
target_current_dir.mkdir(parents=True, exist_ok=True)
|
|
61
|
+
|
|
62
|
+
for file_name in files:
|
|
63
|
+
if file_name in ignored_names:
|
|
64
|
+
continue
|
|
65
|
+
source_path = current_path / file_name
|
|
66
|
+
if not source_path.is_file():
|
|
67
|
+
continue
|
|
68
|
+
(target_current_dir / file_name).write_bytes(source_path.read_bytes())
|
|
69
|
+
|
|
36
70
|
# Add scripts directory to path for imports
|
|
37
71
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
|
38
72
|
sys.path.insert(0, str(SCRIPT_DIR))
|
|
@@ -147,7 +181,7 @@ def safe_skill_path(root: Path, skill_name: str) -> Path:
|
|
|
147
181
|
|
|
148
182
|
def resolve_skill_source(source: str) -> Path:
|
|
149
183
|
"""Resolve and validate a local skill source directory."""
|
|
150
|
-
source_path =
|
|
184
|
+
source_path = safe_user_path(source).expanduser().resolve()
|
|
151
185
|
if not source_path.is_dir():
|
|
152
186
|
raise ValueError(f"Source does not exist or is not a directory: {source_path}")
|
|
153
187
|
if not (source_path / "SKILL.md").is_file():
|
|
@@ -164,7 +198,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str:
|
|
|
164
198
|
if exclude_dirs is None:
|
|
165
199
|
exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"}
|
|
166
200
|
|
|
167
|
-
root_path =
|
|
201
|
+
root_path = safe_user_path(path).resolve(strict=True)
|
|
168
202
|
if not root_path.is_dir():
|
|
169
203
|
raise ValueError(f"Hash target must be a directory: {root_path}")
|
|
170
204
|
|
|
@@ -173,7 +207,7 @@ def md5_dir(path: Path, exclude_dirs: set = None) -> str:
|
|
|
173
207
|
# Filter out excluded directories
|
|
174
208
|
dirs[:] = [d for d in dirs if d not in exclude_dirs]
|
|
175
209
|
for f in sorted(files):
|
|
176
|
-
fp =
|
|
210
|
+
fp = safe_user_path(root) / f
|
|
177
211
|
try:
|
|
178
212
|
resolved_fp = fp.resolve(strict=True)
|
|
179
213
|
resolved_fp.relative_to(root_path)
|
|
@@ -370,7 +404,7 @@ def step4_check_conflicts(skill_name: str) -> dict:
|
|
|
370
404
|
def _backup_ignore(directory, contents):
|
|
371
405
|
"""Ignore function for shutil.copytree to skip backup/staging dirs."""
|
|
372
406
|
ignored = set()
|
|
373
|
-
dir_path =
|
|
407
|
+
dir_path = safe_user_path(directory)
|
|
374
408
|
for item in contents:
|
|
375
409
|
item_path = dir_path / item
|
|
376
410
|
if item_path.is_symlink():
|
|
@@ -436,7 +470,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
|
|
|
436
470
|
|
|
437
471
|
# Copy to staging first (skip backups/staging to prevent recursion)
|
|
438
472
|
try:
|
|
439
|
-
|
|
473
|
+
copy_tree_contents(source_path, staging, ignore=_backup_ignore)
|
|
440
474
|
except Exception as e:
|
|
441
475
|
return {"success": False, "error": f"Copy to staging failed: {e}"}
|
|
442
476
|
|
|
@@ -470,7 +504,7 @@ def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
|
|
|
470
504
|
except Exception as e:
|
|
471
505
|
# Try copy + delete as fallback (cross-device moves)
|
|
472
506
|
try:
|
|
473
|
-
|
|
507
|
+
copy_tree_contents(staging, dest)
|
|
474
508
|
shutil.rmtree(staging, ignore_errors=True)
|
|
475
509
|
except Exception as e2:
|
|
476
510
|
shutil.rmtree(staging, ignore_errors=True)
|
|
@@ -496,7 +530,7 @@ def step7_register_claude(skill_name: str) -> dict:
|
|
|
496
530
|
|
|
497
531
|
# Copy SKILL.md
|
|
498
532
|
try:
|
|
499
|
-
|
|
533
|
+
(claude_dest_dir / "SKILL.md").write_bytes(source_skill_md.read_bytes())
|
|
500
534
|
except Exception as e:
|
|
501
535
|
return {"success": False, "error": f"Failed to copy SKILL.md to Claude skills: {e}"}
|
|
502
536
|
|
|
@@ -507,7 +541,7 @@ def step7_register_claude(skill_name: str) -> dict:
|
|
|
507
541
|
try:
|
|
508
542
|
if claude_refs.exists():
|
|
509
543
|
shutil.rmtree(claude_refs)
|
|
510
|
-
|
|
544
|
+
copy_tree_contents(refs_dir, claude_refs)
|
|
511
545
|
except Exception:
|
|
512
546
|
pass # Non-critical
|
|
513
547
|
|
|
@@ -23,8 +23,22 @@ import sys
|
|
|
23
23
|
import json
|
|
24
24
|
import re
|
|
25
25
|
import zipfile
|
|
26
|
+
import tempfile
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
|
|
29
|
+
|
|
30
|
+
def safe_user_path(path_value, base_dir="."):
|
|
31
|
+
"""Resolve a CLI path under the current workspace."""
|
|
32
|
+
if base_dir != ".":
|
|
33
|
+
raise ValueError("Custom base directories are not supported for CLI paths")
|
|
34
|
+
base_path = Path.cwd().resolve()
|
|
35
|
+
resolved_path = Path(path_value).expanduser().resolve()
|
|
36
|
+
try:
|
|
37
|
+
resolved_path.relative_to(base_path)
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
|
40
|
+
return resolved_path
|
|
41
|
+
|
|
28
42
|
# ── Configuration ──────────────────────────────────────────────────────────
|
|
29
43
|
|
|
30
44
|
SKILLS_ROOT = Path(r"C:\Users\renat\skills")
|
|
@@ -51,7 +65,7 @@ SAFE_ARCHIVE_NAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$")
|
|
|
51
65
|
|
|
52
66
|
def resolve_existing_dir(path) -> Path:
|
|
53
67
|
"""Resolve a user-provided directory and require it to exist."""
|
|
54
|
-
resolved =
|
|
68
|
+
resolved = safe_user_path(path).expanduser().resolve()
|
|
55
69
|
if not resolved.is_dir():
|
|
56
70
|
raise ValueError(f"Directory not found: {resolved}")
|
|
57
71
|
return resolved
|
|
@@ -59,7 +73,7 @@ def resolve_existing_dir(path) -> Path:
|
|
|
59
73
|
|
|
60
74
|
def resolve_output_dir(path) -> Path:
|
|
61
75
|
"""Resolve a user-provided output directory."""
|
|
62
|
-
resolved =
|
|
76
|
+
resolved = safe_user_path(path).expanduser().resolve()
|
|
63
77
|
if resolved.exists() and not resolved.is_dir():
|
|
64
78
|
raise ValueError(f"Output path is not a directory: {resolved}")
|
|
65
79
|
return resolved
|
|
@@ -218,14 +232,9 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict:
|
|
|
218
232
|
|
|
219
233
|
# Collect files
|
|
220
234
|
files_to_include = []
|
|
221
|
-
for
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
for f in files:
|
|
226
|
-
fp = Path(root) / f
|
|
227
|
-
if should_include(fp, skill_dir):
|
|
228
|
-
files_to_include.append(fp)
|
|
235
|
+
for fp in safe_user_path(skill_dir).rglob("*"):
|
|
236
|
+
if fp.is_file() and should_include(fp, skill_dir):
|
|
237
|
+
files_to_include.append(fp)
|
|
229
238
|
|
|
230
239
|
if not files_to_include:
|
|
231
240
|
return {"success": False, "error": "No files to package"}
|
|
@@ -233,13 +242,16 @@ def package_skill(skill_dir: Path, output_dir: Path = None) -> dict:
|
|
|
233
242
|
# Create ZIP with skill folder as root
|
|
234
243
|
# CRITICAL: ZIP paths MUST use forward slashes, not Windows backslashes
|
|
235
244
|
try:
|
|
236
|
-
with
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
246
|
+
temp_zip_path = Path(temp_dir) / "skill.zip"
|
|
247
|
+
with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
248
|
+
for fp in sorted(files_to_include):
|
|
249
|
+
rel_path = fp.relative_to(skill_dir)
|
|
250
|
+
# Convert Windows backslash to forward slash for ZIP compatibility
|
|
251
|
+
rel_posix = rel_path.as_posix()
|
|
252
|
+
archive_path = f"{skill_name_lower}/{rel_posix}"
|
|
253
|
+
zf.write(fp, archive_path)
|
|
254
|
+
zip_path.write_bytes(temp_zip_path.read_bytes())
|
|
243
255
|
|
|
244
256
|
# Verify ZIP is not empty and valid
|
|
245
257
|
with zipfile.ZipFile(zip_path, "r") as zf_check:
|
|
@@ -17,6 +17,19 @@ import json
|
|
|
17
17
|
import re
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
|
|
20
|
+
|
|
21
|
+
def safe_user_path(path_value, base_dir="."):
|
|
22
|
+
"""Resolve a CLI path under the current workspace."""
|
|
23
|
+
if base_dir != ".":
|
|
24
|
+
raise ValueError("Custom base directories are not supported for CLI paths")
|
|
25
|
+
base_path = Path.cwd().resolve()
|
|
26
|
+
resolved_path = Path(path_value).expanduser().resolve()
|
|
27
|
+
try:
|
|
28
|
+
resolved_path.relative_to(base_path)
|
|
29
|
+
except ValueError as exc:
|
|
30
|
+
raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
|
|
31
|
+
return resolved_path
|
|
32
|
+
|
|
20
33
|
# ── Constants ──────────────────────────────────────────────────────────────
|
|
21
34
|
|
|
22
35
|
FORBIDDEN_PATTERNS = [
|
|
@@ -43,7 +56,7 @@ REGISTRY_PATH = SKILLS_ROOT / "agent-orchestrator" / "data" / "registry.json"
|
|
|
43
56
|
|
|
44
57
|
def resolve_existing_dir(path) -> Path:
|
|
45
58
|
"""Resolve a user-provided directory and require it to exist."""
|
|
46
|
-
resolved =
|
|
59
|
+
resolved = safe_user_path(path).expanduser().resolve()
|
|
47
60
|
if not resolved.is_dir():
|
|
48
61
|
raise ValueError(f"Directory does not exist: {resolved}")
|
|
49
62
|
return resolved
|
|
@@ -222,19 +235,20 @@ def check_forbidden_files(skill_dir: Path) -> dict:
|
|
|
222
235
|
"""Check 7: No forbidden files (.env, credentials, keys, etc.)."""
|
|
223
236
|
found_forbidden = []
|
|
224
237
|
|
|
225
|
-
for
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
for path in safe_user_path(skill_dir).rglob("*"):
|
|
239
|
+
if not path.is_file():
|
|
240
|
+
continue
|
|
241
|
+
f_lower = path.name.lower()
|
|
242
|
+
for pattern in FORBIDDEN_PATTERNS:
|
|
243
|
+
if pattern.startswith("*."):
|
|
244
|
+
ext = pattern[1:] # e.g., ".key"
|
|
245
|
+
if f_lower.endswith(ext):
|
|
246
|
+
found_forbidden.append(str(path))
|
|
247
|
+
break
|
|
248
|
+
else:
|
|
249
|
+
if f_lower == pattern.lower():
|
|
250
|
+
found_forbidden.append(str(path))
|
|
251
|
+
break
|
|
238
252
|
|
|
239
253
|
if found_forbidden:
|
|
240
254
|
return {
|
|
@@ -255,12 +269,13 @@ def check_forbidden_files(skill_dir: Path) -> dict:
|
|
|
255
269
|
def check_total_size(skill_dir: Path) -> dict:
|
|
256
270
|
"""Check 8: Total size is reasonable (warn if > 50MB)."""
|
|
257
271
|
total = 0
|
|
258
|
-
for
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
for path in safe_user_path(skill_dir).rglob("*"):
|
|
273
|
+
if not path.is_file():
|
|
274
|
+
continue
|
|
275
|
+
try:
|
|
276
|
+
total += path.stat().st_size
|
|
277
|
+
except OSError:
|
|
278
|
+
pass
|
|
264
279
|
|
|
265
280
|
size_mb = total / (1024 * 1024)
|
|
266
281
|
ok = size_mb <= MAX_SIZE_MB
|
|
@@ -360,7 +375,7 @@ def validate(skill_dir: Path, strict: bool = False, registry_path: Path = None)
|
|
|
360
375
|
except ValueError as e:
|
|
361
376
|
return {
|
|
362
377
|
"valid": False,
|
|
363
|
-
"skill_dir": str(
|
|
378
|
+
"skill_dir": str(safe_user_path(skill_dir).expanduser()),
|
|
364
379
|
"checks": [],
|
|
365
380
|
"warnings": [],
|
|
366
381
|
"errors": [str(e)],
|
|
@@ -433,7 +448,7 @@ def main():
|
|
|
433
448
|
if "--registry" in sys.argv:
|
|
434
449
|
idx = sys.argv.index("--registry")
|
|
435
450
|
if idx + 1 < len(sys.argv):
|
|
436
|
-
registry_path =
|
|
451
|
+
registry_path = safe_user_path(sys.argv[idx + 1]).expanduser().resolve()
|
|
437
452
|
|
|
438
453
|
result = validate(skill_dir, strict=strict, registry_path=registry_path)
|
|
439
454
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
@@ -77,9 +77,10 @@ Master Stripe payment processing integration for robust, PCI-compliant payment f
|
|
|
77
77
|
## Quick Start
|
|
78
78
|
|
|
79
79
|
```python
|
|
80
|
+
import os
|
|
80
81
|
import stripe
|
|
81
82
|
|
|
82
|
-
stripe.api_key = "
|
|
83
|
+
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
|
|
83
84
|
|
|
84
85
|
# Create a checkout session
|
|
85
86
|
session = stripe.checkout.Session.create(
|
|
@@ -222,12 +223,13 @@ def create_customer_portal_session(customer_id):
|
|
|
222
223
|
|
|
223
224
|
### Secure Webhook Endpoint
|
|
224
225
|
```python
|
|
226
|
+
import os
|
|
225
227
|
from flask import Flask, request
|
|
226
228
|
import stripe
|
|
227
229
|
|
|
228
230
|
app = Flask(__name__)
|
|
229
231
|
|
|
230
|
-
endpoint_secret =
|
|
232
|
+
endpoint_secret = os.environ["STRIPE_WEBHOOK_SECRET"]
|
|
231
233
|
|
|
232
234
|
@app.route('/webhook', methods=['POST'])
|
|
233
235
|
def webhook():
|
|
@@ -392,7 +394,9 @@ def handle_dispute(charge_id, evidence):
|
|
|
392
394
|
|
|
393
395
|
```python
|
|
394
396
|
# Use test mode keys
|
|
395
|
-
|
|
397
|
+
import os
|
|
398
|
+
|
|
399
|
+
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
|
|
396
400
|
|
|
397
401
|
# Test card numbers
|
|
398
402
|
TEST_CARDS = {
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import subprocess
|
|
5
|
-
import xml.etree.ElementTree as ET
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
8
|
+
try:
|
|
9
|
+
from defusedxml import ElementTree as ET
|
|
10
|
+
except ImportError: # pragma: no cover - guidance for direct script use
|
|
11
|
+
ET = None
|
|
12
|
+
|
|
9
13
|
|
|
10
14
|
@dataclass(frozen=True)
|
|
11
15
|
class RunInfo:
|
|
@@ -43,6 +47,8 @@ def toc(trace_path: Path) -> TraceInfo:
|
|
|
43
47
|
|
|
44
48
|
The TOC is small (a few KB) so we load it fully rather than streaming.
|
|
45
49
|
"""
|
|
50
|
+
if ET is None:
|
|
51
|
+
raise RuntimeError("Install defusedxml before parsing xctrace XML exports.")
|
|
46
52
|
xml_bytes = _run_export(trace_path, ["--toc"])
|
|
47
53
|
root = ET.fromstring(xml_bytes)
|
|
48
54
|
|
|
@@ -6,10 +6,14 @@ a global id cache for later ref lookups.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import xml.etree.ElementTree as ET
|
|
10
9
|
from collections.abc import Iterator
|
|
11
10
|
from dataclasses import dataclass
|
|
12
11
|
|
|
12
|
+
try:
|
|
13
|
+
from defusedxml import ElementTree as ET
|
|
14
|
+
except ImportError: # pragma: no cover - guidance for direct script use
|
|
15
|
+
ET = None
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
@dataclass(frozen=True)
|
|
15
19
|
class Column:
|
|
@@ -26,6 +30,8 @@ class RowStream:
|
|
|
26
30
|
"""
|
|
27
31
|
|
|
28
32
|
def __init__(self, xml_bytes: bytes):
|
|
33
|
+
if ET is None:
|
|
34
|
+
raise RuntimeError("Install defusedxml before parsing xctrace XML exports.")
|
|
29
35
|
self._xml = xml_bytes
|
|
30
36
|
self.columns: list[Column] = []
|
|
31
37
|
self._id_cache: dict[str, ET.Element] = {}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: taisly-social-media-posting
|
|
3
|
+
description: "Use Taisly Agent Kit to prepare and publish approved short-form video posts across TikTok, Instagram Reels, YouTube Shorts, X, and Facebook."
|
|
4
|
+
category: marketing
|
|
5
|
+
risk: critical
|
|
6
|
+
source: community
|
|
7
|
+
source_repo: taisly/agent
|
|
8
|
+
source_type: community
|
|
9
|
+
date_added: "2026-07-07"
|
|
10
|
+
author: taisly
|
|
11
|
+
tags: [social-media, video, publishing, mcp, cli, sdk, tiktok, instagram, youtube-shorts, x, facebook]
|
|
12
|
+
tools: [codex, claude]
|
|
13
|
+
license: "MIT"
|
|
14
|
+
license_source: "https://github.com/taisly/agent/blob/main/LICENSE"
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Taisly Social Media Posting
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
Taisly Agent Kit provides an MCP server, CLI, SDK, and agent docs for publishing
|
|
22
|
+
approved short-form videos to TikTok, Instagram Reels, YouTube Shorts, X, and
|
|
23
|
+
Facebook. Use this skill to plan a posting workflow around Taisly, verify that
|
|
24
|
+
the user has the required account access, and keep publishing actions behind an
|
|
25
|
+
explicit confirmation gate.
|
|
26
|
+
|
|
27
|
+
## When to Use
|
|
28
|
+
|
|
29
|
+
- Use when the user wants an agent-assisted workflow for publishing short-form
|
|
30
|
+
videos with Taisly.
|
|
31
|
+
- Use when the user mentions `taisly/agent`, the Taisly MCP server, Taisly CLI,
|
|
32
|
+
or the Taisly SDK.
|
|
33
|
+
- Use when coordinating final approval, caption metadata, target platforms, and
|
|
34
|
+
posting status for social video distribution.
|
|
35
|
+
|
|
36
|
+
## Workflow
|
|
37
|
+
|
|
38
|
+
1. Confirm the exact target platforms and video asset paths or URLs.
|
|
39
|
+
2. Confirm that the user has already connected the relevant social accounts in
|
|
40
|
+
Taisly or has provided the intended MCP/CLI setup path.
|
|
41
|
+
3. Draft or review captions, hashtags, titles, descriptions, and platform
|
|
42
|
+
metadata before any publishing command is run.
|
|
43
|
+
4. Present a final posting summary with platforms, media, captions, visibility,
|
|
44
|
+
and timing.
|
|
45
|
+
5. Wait for explicit user approval before invoking any Taisly command, MCP tool,
|
|
46
|
+
SDK call, or other state-changing publishing action.
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
Use Taisly to prepare this product demo for TikTok, Reels, Shorts, X, and Facebook.
|
|
52
|
+
Review the caption and metadata first; do not publish until I approve.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
Set up a Taisly MCP publishing workflow for approved video assets in ./campaign.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Safety Notes
|
|
60
|
+
|
|
61
|
+
- Treat publish, schedule, delete, account-linking, and metadata update actions
|
|
62
|
+
as state-changing operations requiring explicit user approval.
|
|
63
|
+
- Never request, print, or store platform passwords, OAuth secrets, API keys, or
|
|
64
|
+
session tokens. Use the user's existing Taisly/MCP/CLI authentication flow.
|
|
65
|
+
- If the requested action could violate platform policies, brand review, legal
|
|
66
|
+
constraints, or creator permissions, pause and ask for confirmation.
|
|
67
|
+
|
|
68
|
+
## Limitations
|
|
69
|
+
|
|
70
|
+
- Platform availability, media requirements, and API behavior depend on the
|
|
71
|
+
upstream Taisly Agent Kit and connected platform accounts.
|
|
72
|
+
- This skill does not replace human review for legal, brand, copyright, or
|
|
73
|
+
platform-compliance decisions.
|
|
74
|
+
- Verify the current Taisly setup instructions from `taisly/agent` before
|
|
75
|
+
installing or running tools in a new environment.
|
|
76
|
+
|
|
77
|
+
## Source
|
|
78
|
+
|
|
79
|
+
- GitHub: [taisly/agent](https://github.com/taisly/agent)
|