opencode-skills-collection 3.1.4 → 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.
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 +1 -1
  59. package/skills_index.json +5 -3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "updatedAt": "2026-06-23T02:01:07.659Z",
3
+ "updatedAt": "2026-06-24T02:01:40.611Z",
4
4
  "entries": [
5
5
  "00-andruia-consultant",
6
6
  "007",
@@ -853,10 +853,17 @@ def _generate_markdown_report(
853
853
  lines.append("")
854
854
  lines.append("| Check | Status | Details | Scanner |")
855
855
  lines.append("|-------|--------|---------|---------|")
856
+ def format_status(status: str) -> str:
857
+ if status == "PASS":
858
+ return "[PASS]"
859
+ if status == "WARN":
860
+ return "[WARN]"
861
+ if status == "FAIL":
862
+ return "[FAIL]"
863
+ return status
864
+
856
865
  for item in p3.get("checklist", []):
857
- status_icon = {"PASS": "[PASS]", "WARN": "[WARN]", "FAIL": "[FAIL]"}.get(
858
- item["status"], item["status"]
859
- )
866
+ status_icon = format_status(item["status"])
860
867
  lines.append(
861
868
  f"| {item['check']} | {status_icon} | {item['details']} | {item['scanner']} |"
862
869
  )
@@ -1 +1,3 @@
1
- requests>=2.31.0
1
+ requests>=2.33.0
2
+ urllib3>=2.7.0
3
+ idna>=3.15
@@ -8,11 +8,33 @@ import os
8
8
  import sys
9
9
  import json
10
10
  import argparse
11
+ import ipaddress
12
+ import re
13
+ import socket
11
14
  import requests
15
+ from urllib.parse import urlparse
12
16
  from typing import Optional, Dict, Any
13
17
 
14
18
 
15
19
  API_BASE_URL = "https://2slides.com/api/v1"
20
+ JOB_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$")
21
+
22
+
23
+ def validate_job_id(job_id: str) -> str:
24
+ if not JOB_ID_RE.match(job_id or ""):
25
+ raise ValueError("Job ID contains unsupported characters")
26
+ return job_id
27
+
28
+
29
+ def validate_public_https_url(url: str) -> str:
30
+ parsed = urlparse(url)
31
+ if parsed.scheme != "https" or not parsed.hostname:
32
+ raise ValueError("Download URL must be HTTPS")
33
+ for info in socket.getaddrinfo(parsed.hostname, None):
34
+ ip = ipaddress.ip_address(info[4][0])
35
+ if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
36
+ raise ValueError("Download URL resolves to a non-public address")
37
+ return url
16
38
 
17
39
 
18
40
  def get_api_key() -> str:
@@ -51,6 +73,7 @@ def download_slides_pages_voices(
51
73
  """
52
74
  if api_key is None:
53
75
  api_key = get_api_key()
76
+ job_id = validate_job_id(job_id)
54
77
 
55
78
  headers = {
56
79
  "Authorization": f"Bearer {api_key}",
@@ -83,6 +106,7 @@ def download_slides_pages_voices(
83
106
  download_url = data.get("downloadUrl")
84
107
  if not download_url:
85
108
  raise ValueError("No download URL in response")
109
+ download_url = validate_public_https_url(download_url)
86
110
 
87
111
  # Optional: log additional info
88
112
  file_name = data.get("fileName", "unknown.zip")
@@ -7,11 +7,27 @@ import os
7
7
  import sys
8
8
  import json
9
9
  import argparse
10
+ import re
10
11
  import requests
12
+ from urllib.parse import urlparse
11
13
  from typing import Optional, Dict, Any
12
14
 
13
15
 
14
16
  API_BASE_URL = "https://2slides.com/api/v1"
17
+ JOB_ID_RE = re.compile(r"^[A-Za-z0-9_-]+$")
18
+
19
+
20
+ def validate_job_id(job_id: str) -> str:
21
+ if not JOB_ID_RE.match(job_id or ""):
22
+ raise ValueError("Job ID contains unsupported characters")
23
+ return job_id
24
+
25
+
26
+ def validate_api_url(url: str) -> str:
27
+ parsed = urlparse(url)
28
+ if parsed.scheme != "https" or parsed.hostname != "2slides.com" or not parsed.path.startswith("/api/v1/jobs/"):
29
+ raise ValueError("Refusing unsafe 2slides API URL")
30
+ return url
15
31
 
16
32
 
17
33
  def get_api_key() -> str:
@@ -41,13 +57,14 @@ def get_job_status(
41
57
  """
42
58
  if api_key is None:
43
59
  api_key = get_api_key()
60
+ job_id = validate_job_id(job_id)
44
61
 
45
62
  headers = {
46
63
  "Authorization": f"Bearer {api_key}",
47
64
  "Content-Type": "application/json"
48
65
  }
49
66
 
50
- url = f"{API_BASE_URL}/jobs/{job_id}"
67
+ url = validate_api_url(f"{API_BASE_URL}/jobs/{job_id}")
51
68
 
52
69
  print(f"Checking job status: {job_id}...", file=sys.stderr)
53
70
  response = requests.get(url, headers=headers)
@@ -4,6 +4,10 @@ description: "Create custom AI subagents with proper plugin structure, persona g
4
4
  risk: critical
5
5
  source: community
6
6
  date_added: "2026-06-20"
7
+ plugin:
8
+ targets:
9
+ codex: blocked
10
+ claude: blocked
7
11
  ---
8
12
 
9
13
  # Agent Creator
@@ -37,6 +41,13 @@ If the user wants the agent inside an **existing plugin**, add the agent folder
37
41
  to that plugin's `agents/` directory. If no plugin is specified, create a new
38
42
  plugin named `<agent-name>-plugin`.
39
43
 
44
+ Before creating any path, validate both `<agent-name>` and `<plugin-name>`:
45
+
46
+ - accept only lowercase letters, numbers, and single hyphens: `^[a-z0-9]+(-[a-z0-9]+)*$`
47
+ - reject `/`, `\`, `.`, `..`, absolute paths, whitespace, shell metacharacters, and YAML metacharacters
48
+ - resolve the final target path and verify it stays under `<appDataDir>\config\plugins\`
49
+ - stop and ask for a safe replacement instead of sanitizing a suspicious name silently
50
+
40
51
  ## Workflow
41
52
 
42
53
  Follow these steps in order. Do NOT skip the interview — even a one-line
@@ -124,7 +135,7 @@ Write the `<agent-name>.md` file in the `agents/` folder following this exact st
124
135
  ---
125
136
  name: <agent-name>
126
137
  description: <One-line summary of what this agent does.>
127
- tools: ["Read", "Grep", "Glob", "Bash"]
138
+ tools: ["Read", "Grep", "Glob"]
128
139
  model: <current-model>
129
140
  ---
130
141
 
@@ -160,6 +171,9 @@ model: <current-model>
160
171
  <A checklist the agent should mentally run through before returning its response, to ensure quality.>
161
172
  ```
162
173
 
174
+ Grant `Bash` only when the user explicitly asks for command execution and the
175
+ agent's task genuinely needs it. Keep the default tool set read-only.
176
+
163
177
  ### Step 6: Write the companion routing skill (if requested)
164
178
 
165
179
  Create a `SKILL.md` inside `skills/use-<agent-name>/` that tells the main
@@ -132,9 +132,9 @@ CAPABILITY_MAP = {
132
132
 
133
133
  # ── Utility Functions ──────────────────────────────────────────────────────
134
134
 
135
- def md5_file(path: Path) -> str:
136
- """Compute MD5 hash of a file."""
137
- h = hashlib.md5()
135
+ def sha256_file(path: Path) -> str:
136
+ """Compute SHA-256 hash of a file."""
137
+ h = hashlib.sha256()
138
138
  with open(path, "rb") as f:
139
139
  for chunk in iter(lambda: f.read(8192), b""):
140
140
  h.update(chunk)
@@ -382,7 +382,7 @@ def scan(force: bool = False) -> dict:
382
382
  changed = False
383
383
 
384
384
  for path_str, path_obj in current_paths.items():
385
- current_hash = md5_file(path_obj)
385
+ current_hash = sha256_file(path_obj)
386
386
  new_hashes[path_str] = current_hash
387
387
 
388
388
  if force or path_str not in stored_hashes or stored_hashes[path_str] != current_hash:
@@ -74,7 +74,7 @@ const config: CapacitorConfig = {
74
74
 
75
75
  ```typescript
76
76
  import { Camera, CameraResultType } from '@capacitor/camera';
77
- import { Preferences } from '@capacitor/preferences';
77
+ import { SecureStorage } from '@aparajita/capacitor-secure-storage';
78
78
  import { PushNotifications } from '@capacitor/push-notifications';
79
79
  import { Geolocation } from '@capacitor/geolocation';
80
80
 
@@ -107,8 +107,8 @@ const initPush = async () => {
107
107
  if (permission.receive === 'granted') {
108
108
  await PushNotifications.register();
109
109
  }
110
- PushNotifications.addListener('registration', ({ value: token }) => {
111
- console.log('FCM Token:', token);
110
+ PushNotifications.addListener('registration', () => {
111
+ console.log('Push registration succeeded');
112
112
  });
113
113
  };
114
114
  ```
@@ -67,24 +67,27 @@ export const RootNavigator = () => {
67
67
  // Store secrets with a platform-backed module such as react-native-keychain
68
68
  // or expo-secure-store, and persist only non-sensitive UI state here.
69
69
  interface AuthState {
70
- token: string | null;
71
70
  isLoggedIn: boolean;
72
- setToken: (token: string) => void;
71
+ setLoggedIn: (value: boolean) => void;
73
72
  logout: () => void;
74
73
  }
75
74
 
76
75
  export const useAuthStore = create<AuthState>()(
77
76
  persist(
78
77
  (set) => ({
79
- token: null,
80
78
  isLoggedIn: false,
81
- setToken: (token) => set({ token, isLoggedIn: true }),
82
- logout: () => set({ token: null, isLoggedIn: false }),
79
+ setLoggedIn: (value) => set({ isLoggedIn: value }),
80
+ logout: () => set({ isLoggedIn: false }),
83
81
  }),
84
82
  { name: 'auth-ui-storage', storage: createJSONStorage(() => mmkvStorage) }
85
83
  )
86
84
  );
87
85
 
86
+ // Keep tokens outside persisted app state.
87
+ const getSecureToken = () => Keychain.getGenericPassword().then((r) => (r ? r.password : null));
88
+ const saveSecureToken = (token: string) => Keychain.setGenericPassword('auth', token);
89
+ const clearSecureToken = () => Keychain.resetGenericPassword();
90
+
88
91
  // Server state — React Query
89
92
  export const useItems = () =>
90
93
  useQuery({
@@ -142,8 +145,8 @@ const apiClient = axios.create({
142
145
  });
143
146
 
144
147
  // Auth token injection
145
- apiClient.interceptors.request.use((config) => {
146
- const token = useAuthStore.getState().token;
148
+ apiClient.interceptors.request.use(async (config) => {
149
+ const token = await getSecureToken();
147
150
  if (token) config.headers.Authorization = `Bearer ${token}`;
148
151
  return config;
149
152
  });
@@ -155,9 +158,11 @@ apiClient.interceptors.response.use(
155
158
  if (error.response?.status === 401) {
156
159
  const newToken = await refreshToken();
157
160
  if (newToken) {
158
- useAuthStore.getState().setToken(newToken);
161
+ await saveSecureToken(newToken);
162
+ useAuthStore.getState().setLoggedIn(true);
159
163
  return apiClient(error.config!);
160
164
  }
165
+ await clearSecureToken();
161
166
  useAuthStore.getState().logout();
162
167
  }
163
168
  return Promise.reject(error);
@@ -196,6 +201,7 @@ const getItems = async (): Promise<Item[]> => {
196
201
  "zustand": "^4.5.4",
197
202
  "axios": "^1.7.2",
198
203
  "zod": "^3.23.8",
204
+ "react-native-keychain": "^8.2.0",
199
205
  "react-native-mmkv": "^2.12.2",
200
206
  "react-native-safe-area-context": "^4.10.1",
201
207
  "react-native-screens": "^3.32.0"
@@ -7,7 +7,7 @@
7
7
  // Usage: node compile_report.mjs <research-dir> [--user-company "Acme"] [--template <path>] [--open]
8
8
 
9
9
  import { readdirSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
10
- import { join, dirname } from 'path';
10
+ import { basename, dirname, join, relative, resolve } from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { parseFrontmatter, parseBody, parseSections } from './md_utils.mjs';
13
13
 
@@ -15,6 +15,68 @@ const __filename = fileURLToPath(import.meta.url);
15
15
  const __dirname = dirname(__filename);
16
16
 
17
17
  const args = process.argv.slice(2);
18
+ const SAFE_SLUG_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
19
+
20
+ function safeJoin(base, ...parts) {
21
+ const root = resolve(base);
22
+ const target = resolve(root, ...parts);
23
+ const rel = relative(root, target);
24
+ if (rel.startsWith('..') || rel.startsWith('/')) {
25
+ throw new Error(`Path escapes research directory: ${parts.join('/')}`);
26
+ }
27
+ return target;
28
+ }
29
+
30
+ function safeResearchDir(rawDir) {
31
+ if (typeof rawDir !== 'string' || !rawDir.trim() || rawDir.includes('\0')) {
32
+ throw new Error('Research directory is required');
33
+ }
34
+ const root = resolve(process.cwd());
35
+ const target = resolve(root, rawDir);
36
+ const rel = relative(root, target);
37
+ if ((rel.startsWith('..') || rel.startsWith('/')) && process.env.COMPETITOR_ANALYSIS_ALLOW_EXTERNAL_DIR !== '1') {
38
+ throw new Error('Research directory must stay under the current working directory');
39
+ }
40
+ return target;
41
+ }
42
+
43
+ function safeTemplatePath(researchDir, rawPath) {
44
+ if (typeof rawPath !== 'string' || !rawPath.trim() || rawPath.includes('\0')) {
45
+ throw new Error('Template path is required');
46
+ }
47
+ const candidate = safeJoin(researchDir, rawPath);
48
+ if (!candidate.endsWith('.html')) {
49
+ throw new Error('Template path must point to an .html file inside the research directory');
50
+ }
51
+ return candidate;
52
+ }
53
+
54
+ function safeSlug(slug) {
55
+ if (!SAFE_SLUG_RE.test(slug) || slug.includes('..')) {
56
+ throw new Error(`Unsafe competitor slug: ${slug}`);
57
+ }
58
+ return slug;
59
+ }
60
+
61
+ function selfTest() {
62
+ const root = resolve('/tmp/research');
63
+ if (safeJoin(root, 'competitors', 'acme.html') !== resolve(root, 'competitors', 'acme.html')) {
64
+ throw new Error('safeJoin failed valid path');
65
+ }
66
+ for (const bad of ['../x', 'competitors/../../x']) {
67
+ try { safeJoin(root, bad); } catch { continue; }
68
+ throw new Error(`safeJoin accepted ${bad}`);
69
+ }
70
+ for (const bad of ['../acme', 'bad/name', '..']) {
71
+ try { safeSlug(bad); } catch { continue; }
72
+ throw new Error(`safeSlug accepted ${bad}`);
73
+ }
74
+ }
75
+
76
+ if (args.includes('--self-test')) {
77
+ selfTest();
78
+ process.exit(0);
79
+ }
18
80
 
19
81
  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
20
82
  console.error(`Usage: node compile_report.mjs <research-dir> [--user-company "<name>"] [--template <path>] [--open]
@@ -34,12 +96,12 @@ Options:
34
96
  process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1);
35
97
  }
36
98
 
37
- const dir = args[0];
99
+ const dir = safeResearchDir(args[0]);
38
100
  const shouldOpen = args.includes('--open');
39
101
  const userCompanyIdx = args.indexOf('--user-company');
40
102
  const userCompany = userCompanyIdx !== -1 ? args[userCompanyIdx + 1] : '';
41
103
  const templateIdx = args.indexOf('--template');
42
- let templatePath = templateIdx !== -1 ? args[templateIdx + 1] : null;
104
+ let templatePath = templateIdx !== -1 ? safeTemplatePath(dir, args[templateIdx + 1]) : null;
43
105
 
44
106
  if (!templatePath) {
45
107
  const candidates = [
@@ -226,14 +288,14 @@ function mdToHtml(md) {
226
288
 
227
289
  const competitors = [];
228
290
  for (const file of files) {
229
- const content = readFileSync(join(dir, file), 'utf-8');
291
+ const content = readFileSync(safeJoin(dir, file), 'utf-8');
230
292
  const fields = parseFrontmatter(content);
231
293
  if (!fields) continue;
232
294
  const body = parseBody(content);
233
295
  const sections = parseSections(body);
234
296
  const mentions = parseMentions(sections['Mentions']);
235
297
  const benchmarks = parseBenchmarks(sections['Benchmarks']);
236
- const slug = file.replace('.md', '');
298
+ const slug = safeSlug(file.replace('.md', ''));
237
299
  competitors.push({ ...fields, body, sections, mentions, benchmarks, slug, file });
238
300
  }
239
301
 
@@ -253,7 +315,7 @@ const deduped = [...seen.values()].sort((a, b) => (a.competitor_name || '').loca
253
315
  // whole matrix. Keep this block above the first use site to avoid temporal dead zones.
254
316
  let curatedMatrix = null;
255
317
  try {
256
- const p = join(dir, 'matrix.json');
318
+ const p = safeJoin(dir, 'matrix.json');
257
319
  if (existsSync(p)) curatedMatrix = JSON.parse(readFileSync(p, 'utf-8'));
258
320
  } catch (err) {
259
321
  console.error(`Warning: matrix.json present but unreadable — falling back to pipe split. ${err.message}`);
@@ -288,7 +350,7 @@ const totalMentions = competitorRows.reduce((sum, c) => sum + c.mentions.length,
288
350
  const totalBenchmarks = competitorRows.reduce((sum, c) => sum + c.benchmarks.length, 0);
289
351
  const withPricing = competitorRows.filter(c => c.pricing_tiers).length;
290
352
 
291
- const dirName = dir.split('/').pop();
353
+ const dirName = basename(dir);
292
354
  const title = dirName.replace(/_/g, ' ').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
293
355
  const genDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
294
356
  const metaLine = `${competitorRows.length} competitors · ${totalMentions} mentions · ${totalBenchmarks} benchmarks · ${genDate}`;
@@ -433,11 +495,11 @@ let indexHtml = template
433
495
  .replace(/\{\{STRATEGIC_SUMMARY\}\}/g, strategicSummary)
434
496
  .replace(/\{\{TABLE_ROWS\}\}/g, tableRows);
435
497
 
436
- writeFileSync(join(dir, 'index.html'), indexHtml);
498
+ writeFileSync(safeJoin(dir, 'index.html'), indexHtml);
437
499
 
438
500
  // ---------- competitors/{slug}.html ----------
439
501
 
440
- try { mkdirSync(join(dir, 'competitors'), { recursive: true }); } catch {}
502
+ try { mkdirSync(safeJoin(dir, 'competitors'), { recursive: true }); } catch {}
441
503
 
442
504
  const perCompetitorCss = `
443
505
  :root { --brand:#F03603; --blue:#4DA9E4; --black:#100D0D; --gray:#514F4F; --border:#edebeb; --bg:#F9F6F4; --card:#ffffff; --text:#100D0D; --muted:#514F4F; }
@@ -528,7 +590,7 @@ for (const c of competitorRows) {
528
590
  const findingsHtml = c.sections['Research Findings'] ? `<h2>Research Findings</h2>${mdToHtml(c.sections['Research Findings'])}` : '';
529
591
 
530
592
  // Screenshot — filename matches capture_screenshots.mjs output.
531
- const heroShot = existsSync(join(dir, 'screenshots', `${c.slug}-hero.png`));
593
+ const heroShot = existsSync(safeJoin(dir, 'screenshots', `${c.slug}-hero.png`));
532
594
  const screenshotsHtml = heroShot ? `
533
595
  <div class="shots">
534
596
  <div class="shot shot-hero"><div class="shot-label">Homepage</div><img src="../screenshots/${escapeHtml(c.slug)}-hero.png" alt="${escapeHtml(c.competitor_name)} homepage hero" loading="lazy"></div>
@@ -586,7 +648,7 @@ for (const c of competitorRows) {
586
648
  </body>
587
649
  </html>`;
588
650
 
589
- writeFileSync(join(dir, 'competitors', `${c.slug}.html`), companyHtml);
651
+ writeFileSync(safeJoin(dir, 'competitors', `${c.slug}.html`), companyHtml);
590
652
  }
591
653
 
592
654
  // ---------- matrix.html (side-by-side) ----------
@@ -739,7 +801,7 @@ const matrixHtml = `<!DOCTYPE html>
739
801
  </body>
740
802
  </html>`;
741
803
 
742
- writeFileSync(join(dir, 'matrix.html'), matrixHtml);
804
+ writeFileSync(safeJoin(dir, 'matrix.html'), matrixHtml);
743
805
 
744
806
  // ---------- mentions.html (feed + filter) ----------
745
807
 
@@ -870,7 +932,7 @@ const mentionsHtml = `<!DOCTYPE html>
870
932
  </body>
871
933
  </html>`;
872
934
 
873
- writeFileSync(join(dir, 'mentions.html'), mentionsHtml);
935
+ writeFileSync(safeJoin(dir, 'mentions.html'), mentionsHtml);
874
936
 
875
937
  // ---------- CSV ----------
876
938
 
@@ -900,7 +962,7 @@ function csvEscape(v) {
900
962
 
901
963
  const csvLines = [cols.join(',')];
902
964
  for (const row of flatRows) csvLines.push(cols.map(c => csvEscape(row[c] || '')).join(','));
903
- writeFileSync(join(dir, 'results.csv'), csvLines.join('\n') + '\n');
965
+ writeFileSync(safeJoin(dir, 'results.csv'), csvLines.join('\n') + '\n');
904
966
 
905
967
  // ---------- Summary ----------
906
968
 
@@ -911,19 +973,19 @@ console.error(JSON.stringify({
911
973
  with_pricing: withPricing,
912
974
  user_company: userCompany,
913
975
  files_generated: {
914
- index: join(dir, 'index.html'),
915
- matrix: join(dir, 'matrix.html'),
916
- mentions: join(dir, 'mentions.html'),
976
+ index: safeJoin(dir, 'index.html'),
977
+ matrix: safeJoin(dir, 'matrix.html'),
978
+ mentions: safeJoin(dir, 'mentions.html'),
917
979
  competitors: competitorRows.filter(c => c.body && c.body.length > 50).length,
918
- csv: join(dir, 'results.csv')
980
+ csv: safeJoin(dir, 'results.csv')
919
981
  }
920
982
  }, null, 2));
921
983
 
922
- console.log(join(dir, 'index.html'));
984
+ console.log(safeJoin(dir, 'index.html'));
923
985
 
924
986
  if (shouldOpen) {
925
987
  const { execFileSync } = await import('child_process');
926
988
  // Use execFileSync (not execSync with string interpolation) so a `dir` containing
927
989
  // shell metacharacters like `"`, `$`, or backticks can't break out into command exec.
928
- try { execFileSync('open', [join(dir, 'index.html')]); } catch {}
990
+ try { execFileSync('open', [safeJoin(dir, 'index.html')]); } catch {}
929
991
  }
@@ -1 +1,3 @@
1
- requests
1
+ requests>=2.33.0
2
+ urllib3>=2.7.0
3
+ idna>=3.15
@@ -1,4 +1,4 @@
1
- # Getting Started with Antigravity Awesome Skills (V13.1.0)
1
+ # Getting Started with Antigravity Awesome Skills (V13.1.1)
2
2
 
3
3
  **New here? This guide will help you supercharge your AI Agent in 5 minutes.**
4
4
 
@@ -3,11 +3,29 @@ Base validator with common validation logic for document files.
3
3
  """
4
4
 
5
5
  import re
6
+ import shutil
6
7
  from pathlib import Path
7
8
 
8
9
  import lxml.etree
9
10
 
10
11
 
12
+ def safe_extract_all(zip_ref, destination):
13
+ """Extract a zip archive without allowing members to escape destination."""
14
+ destination = Path(destination).resolve()
15
+ for member in zip_ref.infolist():
16
+ target = (destination / member.filename).resolve()
17
+ try:
18
+ target.relative_to(destination)
19
+ except ValueError as exc:
20
+ raise ValueError(f"Unsafe archive member: {member.filename}") from exc
21
+ if member.is_dir():
22
+ target.mkdir(parents=True, exist_ok=True)
23
+ continue
24
+ target.parent.mkdir(parents=True, exist_ok=True)
25
+ with zip_ref.open(member) as src, target.open("wb") as dst:
26
+ shutil.copyfileobj(src, dst)
27
+
28
+
11
29
  class BaseSchemaValidator:
12
30
  """Base validator with common validation logic for document files."""
13
31
 
@@ -888,7 +906,7 @@ class BaseSchemaValidator:
888
906
 
889
907
  # Extract original file
890
908
  with zipfile.ZipFile(self.original_file, "r") as zip_ref:
891
- zip_ref.extractall(temp_path)
909
+ safe_extract_all(zip_ref, temp_path)
892
910
 
893
911
  # Find corresponding file in original
894
912
  original_xml_file = temp_path / relative_path
@@ -3,14 +3,33 @@ Validator for Word document XML files against XSD schemas.
3
3
  """
4
4
 
5
5
  import re
6
+ import shutil
6
7
  import tempfile
7
8
  import zipfile
9
+ from pathlib import Path
8
10
 
9
11
  import lxml.etree
10
12
 
11
13
  from .base import BaseSchemaValidator
12
14
 
13
15
 
16
+ def safe_extract_all(zip_ref, destination):
17
+ """Extract a zip archive without allowing members to escape destination."""
18
+ destination = Path(destination).resolve()
19
+ for member in zip_ref.infolist():
20
+ target = (destination / member.filename).resolve()
21
+ try:
22
+ target.relative_to(destination)
23
+ except ValueError as exc:
24
+ raise ValueError(f"Unsafe archive member: {member.filename}") from exc
25
+ if member.is_dir():
26
+ target.mkdir(parents=True, exist_ok=True)
27
+ continue
28
+ target.parent.mkdir(parents=True, exist_ok=True)
29
+ with zip_ref.open(member) as src, target.open("wb") as dst:
30
+ shutil.copyfileobj(src, dst)
31
+
32
+
14
33
  class DOCXSchemaValidator(BaseSchemaValidator):
15
34
  """Validator for Word document XML files against XSD schemas."""
16
35
 
@@ -198,7 +217,7 @@ class DOCXSchemaValidator(BaseSchemaValidator):
198
217
  with tempfile.TemporaryDirectory() as temp_dir:
199
218
  # Unpack original docx
200
219
  with zipfile.ZipFile(self.original_file, "r") as zip_ref:
201
- zip_ref.extractall(temp_dir)
220
+ safe_extract_all(zip_ref, temp_dir)
202
221
 
203
222
  # Parse document.xml
204
223
  doc_xml_path = temp_dir + "/word/document.xml"
@@ -2,11 +2,31 @@
2
2
  Validator for tracked changes in Word documents.
3
3
  """
4
4
 
5
+ import shutil
5
6
  import subprocess
6
7
  import tempfile
7
8
  import zipfile
8
9
  from pathlib import Path
9
10
 
11
+ from defusedxml import ElementTree as ET
12
+
13
+
14
+ def safe_extract_all(zip_ref, destination):
15
+ """Extract a zip archive without allowing members to escape destination."""
16
+ destination = Path(destination).resolve()
17
+ for member in zip_ref.infolist():
18
+ target = (destination / member.filename).resolve()
19
+ try:
20
+ target.relative_to(destination)
21
+ except ValueError as exc:
22
+ raise ValueError(f"Unsafe archive member: {member.filename}") from exc
23
+ if member.is_dir():
24
+ target.mkdir(parents=True, exist_ok=True)
25
+ continue
26
+ target.parent.mkdir(parents=True, exist_ok=True)
27
+ with zip_ref.open(member) as src, target.open("wb") as dst:
28
+ shutil.copyfileobj(src, dst)
29
+
10
30
 
11
31
  class RedliningValidator:
12
32
  """Validator for tracked changes in Word documents."""
@@ -29,8 +49,6 @@ class RedliningValidator:
29
49
 
30
50
  # First, check if there are any tracked changes by Claude to validate
31
51
  try:
32
- import xml.etree.ElementTree as ET
33
-
34
52
  tree = ET.parse(modified_file)
35
53
  root = tree.getroot()
36
54
 
@@ -67,7 +85,7 @@ class RedliningValidator:
67
85
  # Unpack original docx
68
86
  try:
69
87
  with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
70
- zip_ref.extractall(temp_path)
88
+ safe_extract_all(zip_ref, temp_path)
71
89
  except Exception as e:
72
90
  print(f"FAILED - Error unpacking original docx: {e}")
73
91
  return False
@@ -81,8 +99,6 @@ class RedliningValidator:
81
99
 
82
100
  # Parse both XML files using xml.etree.ElementTree for redlining validation
83
101
  try:
84
- import xml.etree.ElementTree as ET
85
-
86
102
  modified_tree = ET.parse(modified_file)
87
103
  modified_root = modified_tree.getroot()
88
104
  original_tree = ET.parse(original_file)
@@ -81,7 +81,7 @@ harness/
81
81
  },
82
82
  "test_alternatives": {
83
83
  "sqlite_in_memory": "DB_DRIVER=sqlite3 DB_URL=:memory:",
84
- "docker": "docker run -d --name test-pg -p 5433:5432 -e POSTGRES_PASSWORD=test postgres:16"
84
+ "docker": "docker run -d --name test-pg -p 127.0.0.1:5433:5432 -e POSTGRES_PASSWORD=test postgres:16"
85
85
  }
86
86
  }
87
87
  ],