opencode-skills-collection 3.1.3 → 3.1.5
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 +1 -1
- package/skills_index.json +5 -3
|
@@ -19,6 +19,52 @@ import socket
|
|
|
19
19
|
import time
|
|
20
20
|
import sys
|
|
21
21
|
import argparse
|
|
22
|
+
import shlex
|
|
23
|
+
import shutil
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from tempfile import TemporaryDirectory
|
|
26
|
+
|
|
27
|
+
ALLOWED_EXECUTABLES = {
|
|
28
|
+
"npm", "npx", "pnpm", "yarn", "node", "python", "python3",
|
|
29
|
+
"uv", "pytest", "vitest", "playwright",
|
|
30
|
+
}
|
|
31
|
+
SHELL_METACHARS = {";", "&&", "||", "|", "`", "$(", ">", "<"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def safe_working_directory(raw_path):
|
|
35
|
+
root = Path.cwd().resolve()
|
|
36
|
+
path = Path(raw_path).expanduser()
|
|
37
|
+
resolved = (path if path.is_absolute() else root / path).resolve()
|
|
38
|
+
try:
|
|
39
|
+
resolved.relative_to(root)
|
|
40
|
+
except ValueError as exc:
|
|
41
|
+
raise ValueError(f"working directory escapes current project: {raw_path}") from exc
|
|
42
|
+
if not resolved.is_dir():
|
|
43
|
+
raise ValueError(f"working directory not found: {resolved}")
|
|
44
|
+
return resolved
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_allowed_executable(executable):
|
|
48
|
+
if Path(executable).name != executable:
|
|
49
|
+
raise ValueError(f"executable must be a bare command name: {executable}")
|
|
50
|
+
if executable not in ALLOWED_EXECUTABLES:
|
|
51
|
+
raise ValueError(f"unsupported executable: {executable}")
|
|
52
|
+
resolved = shutil.which(executable)
|
|
53
|
+
if not resolved:
|
|
54
|
+
raise ValueError(f"executable not found on PATH: {executable}")
|
|
55
|
+
return resolved
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validate_argv(parts):
|
|
59
|
+
if not parts:
|
|
60
|
+
raise ValueError("empty command")
|
|
61
|
+
exe = Path(parts[0]).name
|
|
62
|
+
resolved_exe = resolve_allowed_executable(exe)
|
|
63
|
+
for part in parts:
|
|
64
|
+
if any(token in part for token in SHELL_METACHARS):
|
|
65
|
+
raise ValueError(f"unsupported shell metacharacter in argument: {part}")
|
|
66
|
+
return [resolved_exe, *parts[1:]]
|
|
67
|
+
|
|
22
68
|
|
|
23
69
|
def is_server_ready(port, timeout=30):
|
|
24
70
|
"""Wait for server to be ready by polling the port."""
|
|
@@ -32,14 +78,64 @@ def is_server_ready(port, timeout=30):
|
|
|
32
78
|
return False
|
|
33
79
|
|
|
34
80
|
|
|
81
|
+
def parse_server_command(command):
|
|
82
|
+
"""Parse a server command without invoking a shell."""
|
|
83
|
+
parts = shlex.split(command)
|
|
84
|
+
cwd = None
|
|
85
|
+
if len(parts) >= 4 and parts[0] == "cd" and parts[2] == "&&":
|
|
86
|
+
cwd = safe_working_directory(parts[1])
|
|
87
|
+
parts = parts[3:]
|
|
88
|
+
if not parts:
|
|
89
|
+
raise ValueError("empty server command")
|
|
90
|
+
return validate_argv(parts), cwd
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def self_test():
|
|
94
|
+
npm_path = shutil.which("npm")
|
|
95
|
+
python_path = shutil.which("python") or shutil.which("python3")
|
|
96
|
+
assert npm_path, "npm required for self-test"
|
|
97
|
+
assert python_path, "python required for self-test"
|
|
98
|
+
with TemporaryDirectory() as tmp:
|
|
99
|
+
previous_cwd = Path.cwd()
|
|
100
|
+
try:
|
|
101
|
+
import os
|
|
102
|
+
os.chdir(tmp)
|
|
103
|
+
assert parse_server_command("npm run dev") == ([npm_path, "run", "dev"], None)
|
|
104
|
+
Path("backend").mkdir()
|
|
105
|
+
cmd, cwd = parse_server_command("cd backend && python server.py")
|
|
106
|
+
assert cmd == [python_path, "server.py"]
|
|
107
|
+
assert cwd == (Path(tmp) / "backend").resolve()
|
|
108
|
+
try:
|
|
109
|
+
validate_argv(["sh", "-c", "npm run dev"])
|
|
110
|
+
except ValueError:
|
|
111
|
+
pass
|
|
112
|
+
else:
|
|
113
|
+
raise AssertionError("shell launcher should be rejected")
|
|
114
|
+
try:
|
|
115
|
+
parse_server_command("cd ../outside && python server.py")
|
|
116
|
+
except ValueError:
|
|
117
|
+
pass
|
|
118
|
+
else:
|
|
119
|
+
raise AssertionError("escaping working directory should be rejected")
|
|
120
|
+
finally:
|
|
121
|
+
os.chdir(previous_cwd)
|
|
122
|
+
|
|
123
|
+
|
|
35
124
|
def main():
|
|
36
125
|
parser = argparse.ArgumentParser(description='Run command with one or more servers')
|
|
37
|
-
parser.add_argument('--
|
|
38
|
-
parser.add_argument('--
|
|
126
|
+
parser.add_argument('--self-test', action='store_true', help='Run parser self-test and exit')
|
|
127
|
+
parser.add_argument('--server', action='append', dest='servers', help='Server command (can be repeated)')
|
|
128
|
+
parser.add_argument('--port', action='append', dest='ports', type=int, help='Port for each server (must match --server count)')
|
|
39
129
|
parser.add_argument('--timeout', type=int, default=30, help='Timeout in seconds per server (default: 30)')
|
|
40
130
|
parser.add_argument('command', nargs=argparse.REMAINDER, help='Command to run after server(s) ready')
|
|
41
131
|
|
|
42
132
|
args = parser.parse_args()
|
|
133
|
+
if args.self_test:
|
|
134
|
+
self_test()
|
|
135
|
+
return
|
|
136
|
+
if not args.servers or not args.ports:
|
|
137
|
+
print("Error: --server and --port are required")
|
|
138
|
+
sys.exit(1)
|
|
43
139
|
|
|
44
140
|
# Remove the '--' separator if present
|
|
45
141
|
if args.command and args.command[0] == '--':
|
|
@@ -65,10 +161,10 @@ def main():
|
|
|
65
161
|
for i, server in enumerate(servers):
|
|
66
162
|
print(f"Starting server {i+1}/{len(servers)}: {server['cmd']}")
|
|
67
163
|
|
|
68
|
-
|
|
164
|
+
server_cmd, server_cwd = parse_server_command(server['cmd'])
|
|
69
165
|
process = subprocess.Popen(
|
|
70
|
-
|
|
71
|
-
|
|
166
|
+
server_cmd,
|
|
167
|
+
cwd=server_cwd,
|
|
72
168
|
stdout=subprocess.PIPE,
|
|
73
169
|
stderr=subprocess.PIPE
|
|
74
170
|
)
|
|
@@ -84,8 +180,9 @@ def main():
|
|
|
84
180
|
print(f"\nAll {len(servers)} server(s) ready")
|
|
85
181
|
|
|
86
182
|
# Run the command
|
|
87
|
-
|
|
88
|
-
|
|
183
|
+
test_command = validate_argv(args.command)
|
|
184
|
+
print(f"Running: {' '.join(test_command)}\n")
|
|
185
|
+
result = subprocess.run(test_command)
|
|
89
186
|
sys.exit(result.returncode)
|
|
90
187
|
|
|
91
188
|
finally:
|
|
@@ -103,4 +200,4 @@ def main():
|
|
|
103
200
|
|
|
104
201
|
|
|
105
202
|
if __name__ == '__main__':
|
|
106
|
-
main()
|
|
203
|
+
main()
|
|
@@ -18,6 +18,24 @@ def get_skill_dir() -> str:
|
|
|
18
18
|
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def _safe_target_path(path: str, skill_dir: str) -> str:
|
|
22
|
+
target_path = os.path.abspath(path)
|
|
23
|
+
skill_root = os.path.abspath(skill_dir)
|
|
24
|
+
if os.path.commonpath([target_path, skill_root]) == skill_root:
|
|
25
|
+
raise ValueError("Refusing to create a project inside the skill source directory")
|
|
26
|
+
return target_path
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def self_test() -> None:
|
|
30
|
+
skill_dir = get_skill_dir()
|
|
31
|
+
_safe_target_path(os.path.join(os.path.dirname(skill_dir), "my-whatsapp-project"), skill_dir)
|
|
32
|
+
try:
|
|
33
|
+
_safe_target_path(os.path.join(skill_dir, "assets", "x"), skill_dir)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return
|
|
36
|
+
raise AssertionError("accepted target inside skill source directory")
|
|
37
|
+
|
|
38
|
+
|
|
21
39
|
def setup_project(language: str, path: str, name: str | None = None) -> None:
|
|
22
40
|
"""Copy boilerplate and configure a new WhatsApp project."""
|
|
23
41
|
skill_dir = get_skill_dir()
|
|
@@ -28,7 +46,7 @@ def setup_project(language: str, path: str, name: str | None = None) -> None:
|
|
|
28
46
|
print(f"Available: nodejs, python")
|
|
29
47
|
sys.exit(1)
|
|
30
48
|
|
|
31
|
-
target_path =
|
|
49
|
+
target_path = _safe_target_path(path, skill_dir)
|
|
32
50
|
|
|
33
51
|
if os.path.exists(target_path) and os.listdir(target_path):
|
|
34
52
|
print(f"Warning: Directory '{target_path}' already exists and is not empty.")
|
|
@@ -93,15 +111,20 @@ def setup_project(language: str, path: str, name: str | None = None) -> None:
|
|
|
93
111
|
|
|
94
112
|
def main():
|
|
95
113
|
parser = argparse.ArgumentParser(description="Setup a new WhatsApp Cloud API project")
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"--self-test",
|
|
116
|
+
action="store_true",
|
|
117
|
+
help="Run safety self-checks",
|
|
118
|
+
)
|
|
96
119
|
parser.add_argument(
|
|
97
120
|
"--language",
|
|
98
121
|
choices=["nodejs", "python"],
|
|
99
|
-
required=
|
|
122
|
+
required=False,
|
|
100
123
|
help="Project language (nodejs or python)",
|
|
101
124
|
)
|
|
102
125
|
parser.add_argument(
|
|
103
126
|
"--path",
|
|
104
|
-
required=
|
|
127
|
+
required=False,
|
|
105
128
|
help="Path where the project will be created",
|
|
106
129
|
)
|
|
107
130
|
parser.add_argument(
|
|
@@ -111,6 +134,11 @@ def main():
|
|
|
111
134
|
)
|
|
112
135
|
|
|
113
136
|
args = parser.parse_args()
|
|
137
|
+
if args.self_test:
|
|
138
|
+
self_test()
|
|
139
|
+
return
|
|
140
|
+
if not args.language or not args.path:
|
|
141
|
+
parser.error("--language and --path are required unless --self-test is used")
|
|
114
142
|
setup_project(args.language, args.path, args.name)
|
|
115
143
|
|
|
116
144
|
|
|
@@ -17,6 +17,27 @@ const fs = require('fs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
const { execSync } = require('child_process');
|
|
19
19
|
|
|
20
|
+
function safeJoin(base, ...parts) {
|
|
21
|
+
const root = path.resolve(base);
|
|
22
|
+
const target = path.resolve(root, ...parts);
|
|
23
|
+
const rel = path.relative(root, target);
|
|
24
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
25
|
+
throw new Error(`Path escapes skill directory: ${parts.join('/')}`);
|
|
26
|
+
}
|
|
27
|
+
return target;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function selfTest() {
|
|
31
|
+
const root = path.resolve('/tmp/skill');
|
|
32
|
+
if (safeJoin(root, 'diagrams', 'a.svg') !== path.resolve(root, 'diagrams', 'a.svg')) {
|
|
33
|
+
throw new Error('safeJoin failed valid path');
|
|
34
|
+
}
|
|
35
|
+
for (const bad of ['../x', 'diagrams/../../x']) {
|
|
36
|
+
try { safeJoin(root, bad); } catch { continue; }
|
|
37
|
+
throw new Error(`safeJoin accepted ${bad}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
function extractDotBlocks(markdown) {
|
|
21
42
|
const blocks = [];
|
|
22
43
|
const regex = /```dot\n([\s\S]*?)```/g;
|
|
@@ -83,6 +104,10 @@ function renderToSvg(dotContent) {
|
|
|
83
104
|
|
|
84
105
|
function main() {
|
|
85
106
|
const args = process.argv.slice(2);
|
|
107
|
+
if (args.includes('--self-test')) {
|
|
108
|
+
selfTest();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
86
111
|
const combine = args.includes('--combine');
|
|
87
112
|
const skillDirArg = args.find(a => !a.startsWith('--'));
|
|
88
113
|
|
|
@@ -99,7 +124,7 @@ function main() {
|
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
const skillDir = path.resolve(skillDirArg);
|
|
102
|
-
const skillFile =
|
|
127
|
+
const skillFile = safeJoin(skillDir, 'SKILL.md');
|
|
103
128
|
const skillName = path.basename(skillDir).replace(/-/g, '_');
|
|
104
129
|
|
|
105
130
|
if (!fs.existsSync(skillFile)) {
|
|
@@ -127,7 +152,7 @@ function main() {
|
|
|
127
152
|
|
|
128
153
|
console.log(`Found ${blocks.length} diagram(s) in ${path.basename(skillDir)}/SKILL.md`);
|
|
129
154
|
|
|
130
|
-
const outputDir =
|
|
155
|
+
const outputDir = safeJoin(skillDir, 'diagrams');
|
|
131
156
|
if (!fs.existsSync(outputDir)) {
|
|
132
157
|
fs.mkdirSync(outputDir);
|
|
133
158
|
}
|
|
@@ -137,12 +162,12 @@ function main() {
|
|
|
137
162
|
const combined = combineGraphs(blocks, skillName);
|
|
138
163
|
const svg = renderToSvg(combined);
|
|
139
164
|
if (svg) {
|
|
140
|
-
const outputPath =
|
|
165
|
+
const outputPath = safeJoin(outputDir, `${skillName}_combined.svg`);
|
|
141
166
|
fs.writeFileSync(outputPath, svg);
|
|
142
167
|
console.log(` Rendered: ${skillName}_combined.svg`);
|
|
143
168
|
|
|
144
169
|
// Also write the dot source for debugging
|
|
145
|
-
const dotPath =
|
|
170
|
+
const dotPath = safeJoin(outputDir, `${skillName}_combined.dot`);
|
|
146
171
|
fs.writeFileSync(dotPath, combined);
|
|
147
172
|
console.log(` Source: ${skillName}_combined.dot`);
|
|
148
173
|
} else {
|
|
@@ -153,7 +178,7 @@ function main() {
|
|
|
153
178
|
for (const block of blocks) {
|
|
154
179
|
const svg = renderToSvg(block.content);
|
|
155
180
|
if (svg) {
|
|
156
|
-
const outputPath =
|
|
181
|
+
const outputPath = safeJoin(outputDir, `${block.name}.svg`);
|
|
157
182
|
fs.writeFileSync(outputPath, svg);
|
|
158
183
|
console.log(` Rendered: ${block.name}.svg`);
|
|
159
184
|
} else {
|
|
@@ -126,6 +126,9 @@ var CURRENT_ID=null, YTID=null, INDEX=null;
|
|
|
126
126
|
function onYouTubeIframeAPIReady(){player=new YT.Player('ytplayer',{events:{'onReady':function(){ready=true;if(pending!=null){doPlay(pending);pending=null;}}}});}
|
|
127
127
|
function fmt(t){t=Math.floor(t);return String(Math.floor(t/60)).padStart(2,'0')+':'+String(t%60).padStart(2,'0');}
|
|
128
128
|
function esc(s){var d=document.createElement('div');d.textContent=s==null?'':s;return d.innerHTML;}
|
|
129
|
+
function clear(el){while(el.firstChild)el.removeChild(el.firstChild);}
|
|
130
|
+
function node(tag,cls,text){var el=document.createElement(tag);if(cls)el.className=cls;if(text!=null)el.textContent=text;return el;}
|
|
131
|
+
function safeUrl(url,fallback){try{var u=new URL(url||'',window.location.href);return /^https?:$/.test(u.protocol)?u.href:(fallback||'#');}catch(e){return fallback||'#';}}
|
|
129
132
|
|
|
130
133
|
/* ---------------- Router ---------------- */
|
|
131
134
|
function route(){
|
|
@@ -154,16 +157,19 @@ async function loadIndex(){
|
|
|
154
157
|
var r=await fetch(API_URL); if(!r.ok) throw new Error('HTTP '+r.status);
|
|
155
158
|
var d=await r.json();
|
|
156
159
|
INDEX=(d.items||[]).filter(function(it){return it.youtube_id;});
|
|
157
|
-
var g=document.getElementById('grid'); g
|
|
158
|
-
if(!INDEX.length){
|
|
160
|
+
var g=document.getElementById('grid'); clear(g);
|
|
161
|
+
if(!INDEX.length){var empty=node('p','', 'No videos in the library yet.');empty.style.color='#7a6f5d';g.appendChild(empty);return;}
|
|
159
162
|
INDEX.forEach(function(it){
|
|
160
163
|
var slides=it.slides||[]; var thumb=(slides[0]&&slides[0].img)||'';
|
|
161
|
-
var tags=(it.tags||[]).slice(0,3).map(function(t){return '<span class="tag">'+esc(t)+'</span>';}).join('');
|
|
162
164
|
var a=document.createElement('a'); a.className='card'; a.href='#/'+encodeURIComponent(it.id);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
var thumbBox=node('div','thumb');
|
|
166
|
+
if(thumb){var img=document.createElement('img');img.src=safeUrl(thumb,'');img.alt='';thumbBox.appendChild(img);}
|
|
167
|
+
thumbBox.appendChild(node('span','play','▶'));
|
|
168
|
+
thumbBox.appendChild(node('span','badge',(it.slide_count||slides.length)+' slides'));
|
|
169
|
+
var body=node('div','body');body.appendChild(node('div','ct',it.title||it.id));body.appendChild(node('div','cs',it.speaker||''));
|
|
170
|
+
var tagList=(it.tags||[]).slice(0,3);
|
|
171
|
+
if(tagList.length){var tags=node('div','tags');tagList.forEach(function(t){tags.appendChild(node('span','tag',t));});body.appendChild(tags);}
|
|
172
|
+
a.appendChild(thumbBox);a.appendChild(body);
|
|
167
173
|
g.appendChild(a);
|
|
168
174
|
});
|
|
169
175
|
}catch(e){var el=document.getElementById('homeErr');el.style.display='block';el.textContent='Could not load the video library: '+e.message+'. Is the backend running?';}
|
|
@@ -183,7 +189,8 @@ async function loadVideo(id){
|
|
|
183
189
|
SLIDES=(m.slides||[]).slice().sort(function(a,b){return a.t-b.t;});
|
|
184
190
|
document.title=m.title||'Video deep-dive';
|
|
185
191
|
document.getElementById('title').textContent=m.title||'';
|
|
186
|
-
document.getElementById('speaker').
|
|
192
|
+
var speaker=document.getElementById('speaker');clear(speaker);speaker.appendChild(document.createTextNode((m.speaker||'')+' · '));
|
|
193
|
+
var watch=document.createElement('a');watch.target='_blank';watch.rel='noopener noreferrer';watch.href=safeUrl(m.source_url,'#');watch.textContent='watch on YouTube ↗';speaker.appendChild(watch);
|
|
187
194
|
document.getElementById('deckcount').textContent=SLIDES.length+' slides · drag the divider ⋮⋮ to resize';
|
|
188
195
|
document.getElementById('now-t').textContent='--:--';
|
|
189
196
|
document.getElementById('now-tx').textContent='Click any slide to play the video from that point.';
|
|
@@ -198,25 +205,29 @@ function parseTranscript(body){
|
|
|
198
205
|
return out;
|
|
199
206
|
}
|
|
200
207
|
function renderDeck(){
|
|
201
|
-
var deck=document.getElementById('deck');deck
|
|
208
|
+
var deck=document.getElementById('deck');clear(deck);
|
|
202
209
|
SLIDES.forEach(function(s,i){
|
|
203
210
|
var d=document.createElement('div');d.className='slide';d.id='slide-'+i;d.dataset.t=s.t;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
var imgBox=node('div','slide-img');
|
|
212
|
+
var img=document.createElement('img');img.src=safeUrl(s.img,'');img.alt=s.title||'';imgBox.appendChild(img);
|
|
213
|
+
imgBox.appendChild(node('span','play-badge','▶'));imgBox.appendChild(node('span','slide-t',s.mmss||fmt(s.t)));
|
|
214
|
+
var meta=node('div','slide-meta');meta.appendChild(node('h3','',s.title||''));
|
|
215
|
+
var playBtn=node('button','btn','▶ Play '+(s.mmss||fmt(s.t)));meta.appendChild(playBtn);
|
|
216
|
+
var label=node('label','note-lbl','Notes ');var saved=node('span','saved');saved.id='saved-'+i;label.appendChild(saved);
|
|
217
|
+
var note=document.createElement('textarea');note.className='note-area';note.id='note-'+i;
|
|
218
|
+
d.appendChild(imgBox);d.appendChild(meta);d.appendChild(label);d.appendChild(note);
|
|
208
219
|
deck.appendChild(d);
|
|
209
|
-
|
|
220
|
+
note.value=s.note||'';
|
|
210
221
|
d.querySelector('.slide-img').onclick=function(){play(i);};
|
|
211
|
-
|
|
212
|
-
|
|
222
|
+
playBtn.onclick=function(){play(i);};
|
|
223
|
+
note.addEventListener('input',function(){onNote(i,this.value);});
|
|
213
224
|
});
|
|
214
225
|
}
|
|
215
226
|
function renderTranscript(){
|
|
216
|
-
var c=document.getElementById('transcript');c
|
|
227
|
+
var c=document.getElementById('transcript');clear(c);
|
|
217
228
|
SEGS.forEach(function(seg){
|
|
218
229
|
var r=document.createElement('div');r.className='trow';r.dataset.t=seg.t;r.dataset.text=seg.text.toLowerCase();
|
|
219
|
-
r.
|
|
230
|
+
r.appendChild(node('span','tt',fmt(seg.t)));r.appendChild(node('span','tx',seg.text));
|
|
220
231
|
r.onclick=function(){seekOnly(seg.t);};c.appendChild(r);
|
|
221
232
|
});
|
|
222
233
|
}
|
|
@@ -33,7 +33,12 @@ API = "/api/video-deepdives"
|
|
|
33
33
|
FM_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)$", re.DOTALL)
|
|
34
34
|
SAFE_SLUG_RE = re.compile(r"^[A-Za-z0-9_-]+$")
|
|
35
35
|
SAFE_MEDIA_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
36
|
+
SAFE_PATH_PART_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
36
37
|
SAFE_CTYPE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*(?:; charset=[A-Za-z0-9._-]+)?$")
|
|
38
|
+
LOCAL_ORIGINS = {
|
|
39
|
+
"http://127.0.0.1:8000": "http://127.0.0.1:8000",
|
|
40
|
+
"http://localhost:8000": "http://localhost:8000",
|
|
41
|
+
}
|
|
37
42
|
|
|
38
43
|
|
|
39
44
|
def split_frontmatter(text):
|
|
@@ -52,7 +57,13 @@ def dump_file(meta, body):
|
|
|
52
57
|
|
|
53
58
|
def library_path(lib, *parts):
|
|
54
59
|
root = Path(lib).resolve()
|
|
55
|
-
candidate = root
|
|
60
|
+
candidate = root
|
|
61
|
+
for part in parts:
|
|
62
|
+
value = str(part)
|
|
63
|
+
if not SAFE_PATH_PART_RE.fullmatch(value) or value in {".", ".."}:
|
|
64
|
+
return None
|
|
65
|
+
candidate = candidate / value
|
|
66
|
+
candidate = candidate.resolve()
|
|
56
67
|
try:
|
|
57
68
|
candidate.relative_to(root)
|
|
58
69
|
except ValueError:
|
|
@@ -60,14 +71,38 @@ def library_path(lib, *parts):
|
|
|
60
71
|
return candidate
|
|
61
72
|
|
|
62
73
|
|
|
74
|
+
def media_path(lib, filename):
|
|
75
|
+
if not SAFE_MEDIA_RE.fullmatch(filename or ""):
|
|
76
|
+
return None
|
|
77
|
+
media_dir = library_path(lib, "_media")
|
|
78
|
+
if not media_dir or not media_dir.is_dir():
|
|
79
|
+
return None
|
|
80
|
+
for path in media_dir.iterdir():
|
|
81
|
+
if path.is_file() and path.name == filename:
|
|
82
|
+
return path
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def item_path(lib, slug):
|
|
87
|
+
if not SAFE_SLUG_RE.fullmatch(slug or ""):
|
|
88
|
+
return None
|
|
89
|
+
target = slug + ".md"
|
|
90
|
+
for path in Path(lib).resolve().iterdir():
|
|
91
|
+
if path.is_file() and path.name == target:
|
|
92
|
+
return path
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
63
96
|
def safe_content_type(ctype):
|
|
64
97
|
return ctype if isinstance(ctype, str) and SAFE_CTYPE_RE.match(ctype) else "application/octet-stream"
|
|
65
98
|
|
|
66
99
|
|
|
100
|
+
def safe_local_origin(origin):
|
|
101
|
+
return LOCAL_ORIGINS.get(origin or "")
|
|
102
|
+
|
|
103
|
+
|
|
67
104
|
def load_item(lib, slug):
|
|
68
|
-
|
|
69
|
-
return None
|
|
70
|
-
path = library_path(lib, slug + ".md")
|
|
105
|
+
path = item_path(lib, slug)
|
|
71
106
|
if not path or not path.is_file():
|
|
72
107
|
return None
|
|
73
108
|
meta, body = split_frontmatter(path.read_text(encoding="utf-8"))
|
|
@@ -110,9 +145,12 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
110
145
|
self.send_response(code)
|
|
111
146
|
self.send_header("Content-Type", ctype)
|
|
112
147
|
self.send_header("Content-Length", str(len(body)))
|
|
113
|
-
self.send_header("Access-Control-Allow-Origin", "*")
|
|
114
148
|
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
|
115
149
|
self.send_header("Access-Control-Allow-Headers", "Content-Type, X-Video-Library-Token")
|
|
150
|
+
origin = safe_local_origin(self.headers.get("Origin"))
|
|
151
|
+
if origin:
|
|
152
|
+
self.send_header("Access-Control-Allow-Origin", origin)
|
|
153
|
+
self.send_header("Vary", "Origin")
|
|
116
154
|
self.end_headers()
|
|
117
155
|
if self.command != "HEAD":
|
|
118
156
|
self.wfile.write(body)
|
|
@@ -134,9 +172,9 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
134
172
|
|
|
135
173
|
if path.startswith(API + "/_media/"):
|
|
136
174
|
fn = posixpath.basename(path) # strip any traversal
|
|
137
|
-
if not SAFE_MEDIA_RE.
|
|
175
|
+
if not SAFE_MEDIA_RE.fullmatch(fn):
|
|
138
176
|
return self._send(400, {"error": "bad media name"})
|
|
139
|
-
fp =
|
|
177
|
+
fp = media_path(self.lib, fn)
|
|
140
178
|
if not fp or not fp.is_file():
|
|
141
179
|
return self._send(404, {"error": "no such media"})
|
|
142
180
|
ctype = mimetypes.guess_type(str(fp))[0] or "application/octet-stream"
|
|
@@ -186,9 +224,12 @@ def self_test():
|
|
|
186
224
|
(root / "_media" / "video_1-slide-01.jpg").write_bytes(b"x")
|
|
187
225
|
assert load_item(str(root), "video_1")
|
|
188
226
|
assert load_item(str(root), "../secret") is None
|
|
189
|
-
assert library_path(str(root), "_media", "../video_1.md")
|
|
227
|
+
assert library_path(str(root), "_media", "../video_1.md") is None
|
|
190
228
|
assert safe_content_type("text/html; charset=utf-8") == "text/html; charset=utf-8"
|
|
191
229
|
assert safe_content_type("text/html\r\nX-Bad: 1") == "application/octet-stream"
|
|
230
|
+
assert safe_local_origin("http://localhost:8000") == LOCAL_ORIGINS["http://localhost:8000"]
|
|
231
|
+
assert safe_local_origin("http://localhost:3000") is None
|
|
232
|
+
assert safe_local_origin("http://localhost:8000\r\nX-Bad: 1") is None
|
|
192
233
|
|
|
193
234
|
|
|
194
235
|
def main():
|
package/package.json
CHANGED
package/skills_index.json
CHANGED
|
@@ -562,15 +562,17 @@
|
|
|
562
562
|
"date_added": "2026-06-20",
|
|
563
563
|
"plugin": {
|
|
564
564
|
"targets": {
|
|
565
|
-
"codex": "
|
|
566
|
-
"claude": "
|
|
565
|
+
"codex": "blocked",
|
|
566
|
+
"claude": "blocked"
|
|
567
567
|
},
|
|
568
568
|
"setup": {
|
|
569
569
|
"type": "none",
|
|
570
570
|
"summary": "",
|
|
571
571
|
"docs": null
|
|
572
572
|
},
|
|
573
|
-
"reasons": [
|
|
573
|
+
"reasons": [
|
|
574
|
+
"explicit_target_restriction"
|
|
575
|
+
]
|
|
574
576
|
}
|
|
575
577
|
},
|
|
576
578
|
{
|