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.
Files changed (164) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +36 -1
  2. package/bundled-skills/007/scripts/full_audit.py +34 -22
  3. package/bundled-skills/007/scripts/quick_scan.py +24 -15
  4. package/bundled-skills/007/scripts/scanners/dependency_scanner.py +31 -19
  5. package/bundled-skills/007/scripts/scanners/injection_scanner.py +24 -14
  6. package/bundled-skills/007/scripts/scanners/secrets_scanner.py +24 -14
  7. package/bundled-skills/007/scripts/score_calculator.py +25 -13
  8. package/bundled-skills/2slides-ppt-generator/scripts/download_slides_pages_voices.py +15 -1
  9. package/bundled-skills/aegisops-ai/SKILL.md +1 -1
  10. package/bundled-skills/agent-self-scheduling/SKILL.md +88 -0
  11. package/bundled-skills/alpha-vantage/SKILL.md +3 -1
  12. package/bundled-skills/anti-sleep/SKILL.md +90 -0
  13. package/bundled-skills/api-security-best-practices/SKILL.md +1 -1
  14. package/bundled-skills/app-store-optimization/SKILL.md +7 -0
  15. package/bundled-skills/audio-transcriber/scripts/transcribe.py +17 -4
  16. package/bundled-skills/azure-mgmt-botservice-py/SKILL.md +2 -1
  17. package/bundled-skills/azure-resource-manager-mysql-dotnet/SKILL.md +2 -1
  18. package/bundled-skills/azure-resource-manager-postgresql-dotnet/SKILL.md +2 -1
  19. package/bundled-skills/azure-resource-manager-sql-dotnet/SKILL.md +2 -1
  20. package/bundled-skills/biopython/SKILL.md +2 -1
  21. package/bundled-skills/brain-to-docs/SKILL.md +54 -0
  22. package/bundled-skills/browser-harness/SKILL.md +220 -0
  23. package/bundled-skills/browser-harness/references/install.md +132 -0
  24. package/bundled-skills/bumblebee/scripts/render_report.py +16 -3
  25. package/bundled-skills/bun-development/SKILL.md +1 -1
  26. package/bundled-skills/cc-skill-security-review/SKILL.md +2 -2
  27. package/bundled-skills/claude-monitor/scripts/monitor.py +15 -2
  28. package/bundled-skills/cmux/SKILL.md +250 -0
  29. package/bundled-skills/code-polish/SKILL.md +192 -0
  30. package/bundled-skills/code-review-checklist/SKILL.md +1 -1
  31. package/bundled-skills/codex-subagent/SKILL.md +117 -0
  32. package/bundled-skills/competitor-analysis/scripts/capture_screenshots.mjs +24 -3
  33. package/bundled-skills/competitor-analysis/scripts/compile_report.mjs +13 -2
  34. package/bundled-skills/competitor-analysis/scripts/extract_vs_names.mjs +23 -2
  35. package/bundled-skills/competitor-analysis/scripts/gate_candidates.mjs +21 -0
  36. package/bundled-skills/competitor-analysis/scripts/list_urls.mjs +23 -2
  37. package/bundled-skills/competitor-analysis/scripts/merge_partials.mjs +24 -3
  38. package/bundled-skills/container-security-hardening/SKILL.md +1 -1
  39. package/bundled-skills/content-creator/scripts/brand_voice_analyzer.py +15 -1
  40. package/bundled-skills/content-creator/scripts/seo_optimizer.py +15 -1
  41. package/bundled-skills/context-kit/SKILL.md +135 -0
  42. package/bundled-skills/cyber-audit/SKILL.md +138 -0
  43. package/bundled-skills/deepapi/SKILL.md +649 -0
  44. package/bundled-skills/delegating-to-agents/SKILL.md +64 -0
  45. package/bundled-skills/developer-signup-flow/SKILL.md +1 -1
  46. package/bundled-skills/diary/scripts/fetch_diaries.py +14 -1
  47. package/bundled-skills/diary/scripts/prepare_context.py +15 -2
  48. package/bundled-skills/diary/scripts/sync_to_notion.py +14 -1
  49. package/bundled-skills/dispatch/SKILL.md +19 -7
  50. package/bundled-skills/distribute-skill-to-all-agents/SKILL.md +85 -0
  51. package/bundled-skills/docs/integrations/jetski-cortex.md +3 -3
  52. package/bundled-skills/docs/integrations/jetski-gemini-loader/README.md +1 -1
  53. package/bundled-skills/docs/maintainers/repo-growth-seo.md +3 -3
  54. package/bundled-skills/docs/maintainers/skills-update-guide.md +1 -1
  55. package/bundled-skills/docs/users/bundles.md +1 -1
  56. package/bundled-skills/docs/users/claude-code-skills.md +1 -1
  57. package/bundled-skills/docs/users/gemini-cli-skills.md +1 -1
  58. package/bundled-skills/docs/users/getting-started.md +1 -1
  59. package/bundled-skills/docs/users/kiro-integration.md +1 -1
  60. package/bundled-skills/docs/users/usage.md +4 -4
  61. package/bundled-skills/docs/users/visual-guide.md +4 -4
  62. package/bundled-skills/docx-official/ooxml/scripts/pack.py +30 -3
  63. package/bundled-skills/docx-official/ooxml/scripts/unpack.py +16 -3
  64. package/bundled-skills/drizzle-migration-conflict/scripts/check_drizzle_migrations.py +27 -17
  65. package/bundled-skills/ecl-harness-engineer/agents/creator-config.md +17 -3
  66. package/bundled-skills/ecl-harness-engineer/references/environment-detection-guide.md +9 -5
  67. package/bundled-skills/effective-agent-skills/SKILL.md +333 -0
  68. package/bundled-skills/electron-development/SKILL.md +6 -2
  69. package/bundled-skills/environment-setup-guide/SKILL.md +2 -5
  70. package/bundled-skills/expo-ui/scripts/list-components.js +5 -1
  71. package/bundled-skills/fable-safe-prompt/SKILL.md +79 -0
  72. package/bundled-skills/folder-specific-claude-and-agents-md/SKILL.md +96 -0
  73. package/bundled-skills/frontend-slides/scripts/extract-pptx.py +18 -4
  74. package/bundled-skills/gcp-cloud-run/SKILL.md +2 -1
  75. package/bundled-skills/gemini-api-integration/SKILL.md +3 -1
  76. package/bundled-skills/gemini-live-api-dev/SKILL.md +2 -1
  77. package/bundled-skills/gemini-omni-flash-api/scripts/video/generate_video.py +49 -14
  78. package/bundled-skills/goal-loop/SKILL.md +175 -0
  79. package/bundled-skills/hugging-face-evaluation/SKILL.md +1 -1
  80. package/bundled-skills/hugging-face-jobs/SKILL.md +1 -1
  81. package/bundled-skills/hugging-face-jobs/scripts/finepdfs-stats.py +14 -1
  82. package/bundled-skills/hugging-face-model-trainer/scripts/convert_to_gguf.py +10 -1
  83. package/bundled-skills/hugo-to-markdown/scripts/inventory_hugo_rules.py +14 -1
  84. package/bundled-skills/image-generator/SKILL.md +3 -1
  85. package/bundled-skills/instagram/scripts/export.py +18 -7
  86. package/bundled-skills/interview-style-doc-building/SKILL.md +86 -0
  87. package/bundled-skills/junta-leiloeiros/scripts/export.py +19 -10
  88. package/bundled-skills/landing-page-generator/scripts/landing_page_scaffolder.py +15 -1
  89. package/bundled-skills/linear-claude-skill/SKILL.md +4 -2
  90. package/bundled-skills/linkedin-post-writer/SKILL.md +159 -0
  91. package/bundled-skills/linkedin-post-writer/references/hook-formulas.md +482 -0
  92. package/bundled-skills/llm-council/SKILL.md +4 -2
  93. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package-lock.json +5 -6
  94. package/bundled-skills/loki-mode/examples/todo-app-generated/backend/package.json +2 -3
  95. package/bundled-skills/markdown-rendering/SKILL.md +72 -0
  96. package/bundled-skills/matematico-tao/scripts/complexity_analyzer.py +14 -1
  97. package/bundled-skills/matematico-tao/scripts/dependency_graph.py +15 -2
  98. package/bundled-skills/mobile-design/scripts/mobile_audit.py +20 -6
  99. package/bundled-skills/monte-carlo-validation-notebook/scripts/resolve_dbt_schema.py +15 -2
  100. package/bundled-skills/multi-agent-architect/SKILL.md +1 -1
  101. package/bundled-skills/odoo-rpc-api/SKILL.md +2 -1
  102. package/bundled-skills/pci-compliance/SKILL.md +3 -1
  103. package/bundled-skills/pdf-official/scripts/create_validation_image.py +17 -3
  104. package/bundled-skills/pdf-official/scripts/extract_form_field_info.py +1 -1
  105. package/bundled-skills/personal-tool-builder/SKILL.md +1 -1
  106. package/bundled-skills/pi-custom-model/SKILL.md +68 -0
  107. package/bundled-skills/pi-web-search/SKILL.md +74 -0
  108. package/bundled-skills/playwright-skill/run.js +19 -1
  109. package/bundled-skills/pptx-official/ooxml/scripts/pack.py +30 -3
  110. package/bundled-skills/pptx-official/ooxml/scripts/unpack.py +16 -3
  111. package/bundled-skills/pptx-official/scripts/inventory.py +17 -4
  112. package/bundled-skills/pptx-official/scripts/rearrange.py +15 -2
  113. package/bundled-skills/pptx-official/scripts/replace.py +16 -3
  114. package/bundled-skills/product-manager-toolkit/scripts/customer_interview_analyzer.py +15 -1
  115. package/bundled-skills/production-code-audit/SKILL.md +1 -1
  116. package/bundled-skills/push-skill-to-github/SKILL.md +61 -0
  117. package/bundled-skills/rclone-cli/references/commands/rclone_completion_powershell.md +3 -2
  118. package/bundled-skills/read-all-adrs/SKILL.md +35 -0
  119. package/bundled-skills/remote-gpu-trainer/scripts/verify_local.py +14 -1
  120. package/bundled-skills/research-prompt/SKILL.md +63 -0
  121. package/bundled-skills/run-deep-swe/SKILL.md +120 -0
  122. package/bundled-skills/senior-architect/scripts/architecture_diagram_generator.py +15 -2
  123. package/bundled-skills/senior-architect/scripts/dependency_analyzer.py +15 -2
  124. package/bundled-skills/senior-architect/scripts/project_architect.py +15 -2
  125. package/bundled-skills/senior-frontend/scripts/bundle_analyzer.py +14 -1
  126. package/bundled-skills/senior-frontend/scripts/frontend_scaffolder.py +14 -1
  127. package/bundled-skills/senior-fullstack/scripts/code_quality_analyzer.py +15 -2
  128. package/bundled-skills/senior-fullstack/scripts/fullstack_scaffolder.py +15 -2
  129. package/bundled-skills/senior-fullstack/scripts/project_scaffolder.py +15 -2
  130. package/bundled-skills/setup-help/SKILL.md +50 -0
  131. package/bundled-skills/shodan-reconnaissance/SKILL.md +2 -1
  132. package/bundled-skills/short/SKILL.md +28 -0
  133. package/bundled-skills/skill-creator/scripts/init_skill.py +15 -2
  134. package/bundled-skills/skill-creator/scripts/package_skill.py +29 -12
  135. package/bundled-skills/skill-creator/scripts/quick_validate.py +14 -2
  136. package/bundled-skills/skill-creator-ms/SKILL.md +1 -1
  137. package/bundled-skills/skill-installer/scripts/install_skill.py +42 -8
  138. package/bundled-skills/skill-installer/scripts/package_skill.py +29 -17
  139. package/bundled-skills/skill-installer/scripts/validate_skill.py +37 -22
  140. package/bundled-skills/stripe-integration/SKILL.md +7 -3
  141. package/bundled-skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +7 -1
  142. package/bundled-skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +7 -1
  143. package/bundled-skills/taisly-social-media-posting/SKILL.md +79 -0
  144. package/bundled-skills/technical-tutorials/SKILL.md +1 -1
  145. package/bundled-skills/telegram/scripts/setup_project.py +17 -2
  146. package/bundled-skills/time-ledger/SKILL.md +118 -0
  147. package/bundled-skills/trading-ledger/SKILL.md +121 -0
  148. package/bundled-skills/videodb/scripts/ws_listener.py +17 -4
  149. package/bundled-skills/voice-ai-development/SKILL.md +2 -1
  150. package/bundled-skills/vps-server-management/SKILL.md +72 -0
  151. package/bundled-skills/weaviate/scripts/weaviate_conn.py +20 -18
  152. package/bundled-skills/wgm/SKILL.md +137 -0
  153. package/bundled-skills/whatsapp-cloud-api/scripts/setup_project.py +16 -1
  154. package/bundled-skills/workorai/SKILL.md +137 -0
  155. package/bundled-skills/wp-site-health-auditor/SKILL.md +341 -0
  156. package/bundled-skills/wp-site-health-auditor/references/catalog.md +135 -0
  157. package/bundled-skills/writing-skills/render-graphs.js +13 -2
  158. package/bundled-skills/x-twitter-scraper/SKILL.md +3 -1
  159. package/bundled-skills/youtube-notetaker/scripts/serve.py +41 -31
  160. package/bundled-skills/youtube-notetaker/scripts/vtt_to_transcript.py +16 -2
  161. package/bundled-skills/youtube-notetaker/scripts/write_library_item.py +15 -1
  162. package/bundled-skills/youtube-transcript/SKILL.md +120 -0
  163. package/package.json +5 -1
  164. package/skills_index.json +786 -6
@@ -71,6 +71,19 @@ import re
71
71
  import json
72
72
  from pathlib import Path
73
73
 
74
+
75
+ def safe_user_path(path_value, base_dir="."):
76
+ """Resolve a CLI path under the current workspace."""
77
+ if base_dir != ".":
78
+ raise ValueError("Custom base directories are not supported for CLI paths")
79
+ base_path = Path.cwd().resolve()
80
+ resolved_path = Path(path_value).expanduser().resolve()
81
+ try:
82
+ resolved_path.relative_to(base_path)
83
+ except ValueError as exc:
84
+ raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
85
+ return resolved_path
86
+
74
87
  class MobileAuditor:
75
88
  def __init__(self):
76
89
  self.issues = []
@@ -612,11 +625,12 @@ class MobileAuditor:
612
625
 
613
626
  def audit_directory(self, directory: str) -> None:
614
627
  extensions = {'.tsx', '.ts', '.jsx', '.js', '.dart'}
615
- for root, dirs, files in os.walk(directory):
616
- dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'}]
617
- for file in files:
618
- if Path(file).suffix in extensions:
619
- self.audit_file(os.path.join(root, file))
628
+ skipped = {'node_modules', '.git', 'dist', 'build', '.next', 'ios', 'android', 'build', '.idea'}
629
+ for path in safe_user_path(directory).rglob("*"):
630
+ if not path.is_file() or any(part in skipped for part in path.parts):
631
+ continue
632
+ if path.suffix in extensions:
633
+ self.audit_file(str(path))
620
634
 
621
635
  def get_report(self):
622
636
  return {
@@ -633,7 +647,7 @@ def main():
633
647
  print("Usage: python mobile_audit.py <directory>")
634
648
  sys.exit(1)
635
649
 
636
- path = sys.argv[1]
650
+ path = safe_user_path(sys.argv[1])
637
651
  is_json = "--json" in sys.argv
638
652
 
639
653
  auditor = MobileAuditor()
@@ -12,6 +12,19 @@ import argparse
12
12
  import re
13
13
  import sys
14
14
  from pathlib import Path
15
+
16
+
17
+ def safe_user_path(path_value, base_dir="."):
18
+ """Resolve a CLI path under the current workspace."""
19
+ if base_dir != ".":
20
+ raise ValueError("Custom base directories are not supported for CLI paths")
21
+ base_path = Path.cwd().resolve()
22
+ resolved_path = Path(path_value).expanduser().resolve()
23
+ try:
24
+ resolved_path.relative_to(base_path)
25
+ except ValueError as exc:
26
+ raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
27
+ return resolved_path
15
28
  from typing import Dict, List, Optional, Tuple, Union
16
29
 
17
30
  import yaml
@@ -141,8 +154,8 @@ def main() -> None:
141
154
 
142
155
  args = parser.parse_args()
143
156
 
144
- dbt_project_path = Path(args.dbt_project_path)
145
- model_path = Path(args.model_path)
157
+ dbt_project_path = safe_user_path(args.dbt_project_path)
158
+ model_path = safe_user_path(args.model_path)
146
159
 
147
160
  if not dbt_project_path.exists():
148
161
  print(f"Error: dbt_project.yml not found: {dbt_project_path}", file=sys.stderr)
@@ -321,7 +321,7 @@ async def reflection_node(state: AgentState) -> AgentState:
321
321
  - Never expose API keys in generated code. All secrets must use environment variables:
322
322
  ```python
323
323
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") # ✅ correct
324
- OPENAI_API_KEY = "sk-..." # ❌ never do this
324
+ leaked_openai_token = "[redacted API key]" # ❌ never do this
325
325
  ```
326
326
  - Always validate and sanitize user inputs before injecting them into agent prompts — treat all user input as untrusted.
327
327
  - Add a permission layer before allowing agents to execute shell commands or write to filesystems.
@@ -29,12 +29,13 @@ Odoo exposes a powerful external API via JSON-RPC and XML-RPC, allowing any exte
29
29
  ### Example 1: Authenticate and Read Records (Python)
30
30
 
31
31
  ```python
32
+ import os
32
33
  import xmlrpc.client
33
34
 
34
35
  url = 'https://myodoo.example.com'
35
36
  db = 'my_database'
36
37
  username = 'admin'
37
- password = 'my_api_key' # Use API keys, not passwords, in production
38
+ password = os.environ["ODOO_API_KEY"] # Use API keys, not passwords, in production
38
39
 
39
40
  # Step 1: Authenticate
40
41
  common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
@@ -146,8 +146,10 @@ class TokenizedPayment:
146
146
  @staticmethod
147
147
  def charge_with_token(token_id, amount):
148
148
  """Charge using token (server-side)."""
149
+ import os
150
+
149
151
  # Your server only sees the token, never the card number
150
- stripe.api_key = "sk_..."
152
+ stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
151
153
 
152
154
  charge = stripe.Charge.create(
153
155
  amount=amount,
@@ -2,6 +2,20 @@ import json
2
2
  import sys
3
3
 
4
4
  from PIL import Image, ImageDraw
5
+ from pathlib import Path
6
+
7
+
8
+ def safe_user_path(path_value, base_dir="."):
9
+ """Resolve a CLI path under the current workspace."""
10
+ if base_dir != ".":
11
+ raise ValueError("Custom base directories are not supported for CLI paths")
12
+ base_path = Path.cwd().resolve()
13
+ resolved_path = Path(path_value).expanduser().resolve()
14
+ try:
15
+ resolved_path.relative_to(base_path)
16
+ except ValueError as exc:
17
+ raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
18
+ return resolved_path
5
19
 
6
20
 
7
21
  # Creates "validation" images with rectangles for the bounding box information that
@@ -35,7 +49,7 @@ if __name__ == "__main__":
35
49
  print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]")
36
50
  sys.exit(1)
37
51
  page_number = int(sys.argv[1])
38
- fields_json_path = sys.argv[2]
39
- input_image_path = sys.argv[3]
40
- output_image_path = sys.argv[4]
52
+ fields_json_path = safe_user_path(sys.argv[2])
53
+ input_image_path = safe_user_path(sys.argv[3])
54
+ output_image_path = safe_user_path(sys.argv[4])
41
55
  create_validation_image(page_number, fields_json_path, input_image_path, output_image_path)
@@ -141,7 +141,7 @@ def write_field_info(pdf_path: str, json_output_path: str):
141
141
  reader = PdfReader(pdf_path)
142
142
  field_info = get_field_info(reader)
143
143
  with open(json_output_path, "w") as f:
144
- json.dump(field_info, f, indent=2)
144
+ f.write(json.dumps(field_info, indent=2))
145
145
  print(f"Wrote {len(field_info)} fields to {json_output_path}")
146
146
 
147
147
 
@@ -590,7 +590,7 @@ Recommended fix:
590
590
  ### Credential Management
591
591
  ```javascript
592
592
  // Never in code
593
- const API_KEY = 'sk-xxx'; // BAD
593
+ const leakedToken = '[redacted API key]'; // BAD
594
594
 
595
595
  // Environment variable
596
596
  const API_KEY = process.env.MY_API_KEY;
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: pi-custom-model
3
+ description: "Register custom Pi Agent model slugs so saved OpenRouter variants resolve correctly."
4
+ category: operations
5
+ risk: critical
6
+ source: community
7
+ source_repo: davidondrej/skills
8
+ source_type: community
9
+ date_added: "2026-07-07"
10
+ author: davidondrej
11
+ tags: [pi-agent, models, openrouter]
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
+ # Pi custom / variant model
19
+
20
+ ## When to Use
21
+ Pi's saved default only loads if the exact `provider/id` exists in its model registry. Pi ships a static bundled list per provider — so OpenRouter **routing-shortcut variants** (`:nitro` = sort by throughput, `:floor` = cheapest, `:exacto` = quality tool-use) and any brand-new slug are NOT in it. When the default doesn't resolve, Pi silently falls through to its built-in per-provider default (for openrouter that's `moonshotai/kimi-k2.6`) — looking like Pi "reset" your model. Fix = register the slug as a custom model so `find(provider, id)` matches.
22
+
23
+ ## Files (global)
24
+ - `~/.pi/agent/settings.json` — `defaultProvider`, `defaultModel`, `defaultThinkingLevel`
25
+ - `~/.pi/agent/models.json` — custom models, keyed by provider
26
+ - `~/.pi/agent/auth.json` — provider credentials (check the provider key exists)
27
+
28
+ ## Steps
29
+ 1. **Confirm the slug is real** before adding it (e.g. check the OpenRouter model/variant exists). A typo'd id also silently falls back.
30
+ 2. **Confirm auth.** The provider must have a key in `auth.json` (or an env var like `OPENROUTER_API_KEY`). No auth → the model is registered but unavailable → still falls back.
31
+ 3. **Add the model to `models.json`** under `providers.<provider>.models`. For a **built-in provider** (openrouter, anthropic, etc.) you only supply metadata — `api`, `baseUrl`, and auth are inherited from the bundled defaults. Example:
32
+ ```json
33
+ {
34
+ "providers": {
35
+ "openrouter": {
36
+ "models": [
37
+ {
38
+ "id": "z-ai/glm-5.2:nitro",
39
+ "name": "Z.ai: GLM 5.2 (nitro)",
40
+ "reasoning": true,
41
+ "thinkingLevelMap": { "xhigh": "xhigh" },
42
+ "input": ["text"],
43
+ "cost": { "input": 0.95, "output": 3, "cacheRead": 0.18, "cacheWrite": 0 },
44
+ "contextWindow": 1048576,
45
+ "maxTokens": 32768,
46
+ "compat": { "supportsDeveloperRole": false, "thinkingFormat": "openrouter" }
47
+ }
48
+ ]
49
+ }
50
+ }
51
+ }
52
+ ```
53
+ Copy `cost`/`contextWindow`/`compat` from the base model (the variant shares them) — find the bundled entry in `<pi-pkg>/node_modules/@earendil-works/pi-ai/dist/providers/<provider>.models.js`. Don't hardcode generic 128k/16k if the real model is bigger.
54
+ 4. **Set the default** in `settings.json`: `defaultProvider` + `defaultModel` = the exact id. Leave `defaultThinkingLevel` as the user has it.
55
+ 5. **Verify:** `pi --list-models | grep <id>` shows it, and JSON parses. Optionally smoke-test: `pi --provider <p> --model "<id>" "which model are you?"`.
56
+
57
+ ## Quirks
58
+ - **Exact match only.** `find()` is exact `provider`+`id` — no fuzzy/colon-stripping for the *saved default* path. The slug in `settings.json` and `models.json` must be byte-identical.
59
+ - **Silent fallback.** Pi prints no error when the default doesn't resolve; it just shows a different model in the footer. That's the tell.
60
+ - **Don't edit `settings.json` alone.** Setting `defaultModel` to an unregistered slug does nothing — `models.json` is the actual fix.
61
+ - **`enabledModels`** (optional) pins the model picker so Ctrl+P cycling can't drift back: `"enabledModels": ["<provider>/<id>:<thinking>"]`.
62
+ - **Project override.** A repo's `.pi/settings.json` overrides global. If a default reverts only inside one project, check that file first.
63
+ - Restart Pi fully — the registry loads at startup.
64
+
65
+ ## Limitations
66
+
67
+ - Adapted from `davidondrej/skills`; verify local paths, tools, credentials, and agent features before acting.
68
+ - For commands, remote access, scheduling, browser automation, or file-changing workflows, get explicit user approval and confirm the target environment first.
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: pi-web-search
3
+ description: "Give Pi Agents a safe web-search and fetch workflow using the installed pi-web-access package."
4
+ category: research
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: [web-search, pi-agent, research]
12
+ tools: [claude, codex]
13
+ license: "MIT"
14
+ license_source: "https://github.com/davidondrej/skills/blob/main/LICENSE"
15
+ ---
16
+
17
+ # Web Search
18
+
19
+ ## When to Use
20
+
21
+ - Use when a Pi Agent task needs current web information, page fetches, PDFs, YouTube, or GitHub content.
22
+ - Use when Pi should use its own web-access package instead of another agent browser tool.
23
+
24
+ The `pi-web-access` package is installed globally. Zero-config via Exa MCP (no API key), with fallback Exa → Perplexity → Gemini.
25
+
26
+ ## CRITICAL: always pass `workflow: "none"`
27
+
28
+ Every `web_search` call MUST include `workflow: "none"`. This skips the interactive browser curator popup (the user does not want it opening). No exceptions — single query or batched `queries`, always set `workflow: "none"`.
29
+
30
+ ```
31
+ web_search({ queries: ["query 1", "query 2"], workflow: "none" })
32
+ ```
33
+
34
+ ## Tools
35
+
36
+ - `web_search` — search the web; returns synthesized answers with citations. Can be called many times per turn. **Always pass `workflow: "none"`.**
37
+ - `code_search` — zero-key Exa code-context. Use for library/API/code lookups instead of generic `web_search`.
38
+ - `fetch_content` — fetch URL(s) → markdown; handles PDFs, YouTube, GitHub.
39
+ - `get_search_content` — big pages (>30k chars) are truncated in responses but stored in full; call this to pull the rest on demand so they don't blow context.
40
+
41
+ ## fetch_content specifics
42
+
43
+ - **GitHub URLs are cloned, not scraped** — you get real files + a local path to explore with `read`/`bash` (private repos need the `gh` CLI). Use this for dev work.
44
+ - **PDFs** → auto-extracted to markdown in `~/Downloads/`, readable in sections (text-only, no OCR).
45
+ - **YouTube/video** → full raw transcripts + frame extraction. Needs a `GEMINI_API_KEY` (not zero-config); frame extraction also needs `ffmpeg`/`yt-dlp`.
46
+
47
+ ## Routing — match the user's phrasing
48
+
49
+ Always use the `web_search` tool. These counts are HARD MINIMUMS — count your queries before answering and do not stop short:
50
+
51
+ - **"web search"** → **at least 2** queries, varied keywords/angles, then synthesize.
52
+ - **"extensive web research"** → **at least 4** queries, totally different keywords and angles.
53
+ - **"deep research"** → **at least 8** queries, totally different keywords and angles, run across 2–3 successive batches (refine angles after each batch), to learn as much as possible about the topic.
54
+
55
+ A single batched `web_search` call counts each query in `queries[]` toward the total. If your first batch is under the minimum, fire another batch before synthesizing.
56
+
57
+ ## Fallback / alternative: DeepAPI web search
58
+
59
+ If the Exa → Perplexity → Gemini chain fails, or you need ranked results with URLs:
60
+
61
+ ```bash
62
+ test -n "$DEEPAPI_API_KEY" || { echo "DEEPAPI_API_KEY is not set"; exit 1; }
63
+ curl -s --max-time 60 "https://deepapi.co/v1/search/web" \
64
+ -H "Authorization: Bearer $DEEPAPI_API_KEY" -H "Content-Type: application/json" \
65
+ -H "Idempotency-Key: $(uuidgen)" \
66
+ -d '{"query": "your search terms", "maxResults": 5, "maxCostUsd": "0.05"}'
67
+ ```
68
+
69
+ Results are in `.output` (title, url, snippet per item). Query under 500 chars. Full details: `deepapi` skill.
70
+
71
+ ## Limitations
72
+
73
+ - Adapted from `davidondrej/skills`; verify local paths, tools, credentials, and agent features before acting.
74
+ - For commands, remote access, scheduling, browser automation, or file-changing workflows, get explicit user approval and confirm the target environment first.
@@ -13,10 +13,28 @@
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
15
  const { execSync } = require('child_process');
16
+ const sanitizeFilename = require('sanitize-filename');
16
17
 
17
18
  // Change to skill directory for proper module resolution
18
19
  process.chdir(__dirname);
19
20
 
21
+ function safeUserPath(pathValue, baseDir = process.cwd()) {
22
+ const root = path.resolve(baseDir);
23
+ const segments = String(pathValue ?? '').split(/[\\/]+/).filter(Boolean).map((segment) => {
24
+ const sanitized = sanitizeFilename(segment);
25
+ if (sanitized !== segment || !sanitized) {
26
+ throw new Error(`Unsafe path segment: ${segment}`);
27
+ }
28
+ return sanitized;
29
+ });
30
+ const target = path.resolve(root, ...segments);
31
+ const rel = path.relative(root, target);
32
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
33
+ throw new Error(`Path escapes allowed directory: ${pathValue}`);
34
+ }
35
+ return target;
36
+ }
37
+
20
38
  /**
21
39
  * Check if Playwright is installed
22
40
  */
@@ -54,7 +72,7 @@ function getCodeToExecute() {
54
72
 
55
73
  // Case 1: File path provided
56
74
  if (args.length > 0 && fs.existsSync(args[0])) {
57
- const filePath = path.resolve(args[0]);
75
+ const filePath = safeUserPath(args[0]);
58
76
  console.log(`📄 Executing file: ${filePath}`);
59
77
  return fs.readFileSync(filePath, 'utf8');
60
78
  }
@@ -16,6 +16,19 @@ import zipfile
16
16
  from pathlib import Path
17
17
 
18
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
30
+
31
+
19
32
  def validate_input_tree(input_dir: Path):
20
33
  root = input_dir.resolve(strict=True)
21
34
  for path in input_dir.rglob("*"):
@@ -27,6 +40,18 @@ def validate_input_tree(input_dir: Path):
27
40
  raise ValueError(f"Refusing to pack path outside input directory: {path}") from None
28
41
 
29
42
 
43
+ def copy_tree_contents(source_dir: Path, target_dir: Path) -> None:
44
+ target_dir.mkdir(parents=True, exist_ok=True)
45
+ for source_path in source_dir.rglob("*"):
46
+ relative_path = source_path.relative_to(source_dir)
47
+ target_path = target_dir / relative_path
48
+ if source_path.is_dir():
49
+ target_path.mkdir(parents=True, exist_ok=True)
50
+ elif source_path.is_file():
51
+ target_path.parent.mkdir(parents=True, exist_ok=True)
52
+ target_path.write_bytes(source_path.read_bytes())
53
+
54
+
30
55
  def main():
31
56
  parser = argparse.ArgumentParser(description="Pack a directory into an Office file")
32
57
  parser.add_argument("input_directory", help="Unpacked Office document directory")
@@ -65,7 +90,7 @@ def pack_document(input_dir, output_file, validate=False):
65
90
  bool: True if successful, False if validation failed
66
91
  """
67
92
  input_dir = Path(input_dir)
68
- output_file = Path(output_file)
93
+ output_file = safe_user_path(output_file)
69
94
 
70
95
  if not input_dir.is_dir():
71
96
  raise ValueError(f"{input_dir} is not a directory")
@@ -76,7 +101,7 @@ def pack_document(input_dir, output_file, validate=False):
76
101
  # Work in temporary directory to avoid modifying original
77
102
  with tempfile.TemporaryDirectory() as temp_dir:
78
103
  temp_content_dir = Path(temp_dir) / "content"
79
- shutil.copytree(input_dir, temp_content_dir)
104
+ copy_tree_contents(input_dir, temp_content_dir)
80
105
 
81
106
  # Process XML files to remove pretty-printing whitespace
82
107
  for pattern in ["*.xml", "*.rels"]:
@@ -85,10 +110,12 @@ def pack_document(input_dir, output_file, validate=False):
85
110
 
86
111
  # Create final Office file as zip archive
87
112
  output_file.parent.mkdir(parents=True, exist_ok=True)
88
- with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf:
113
+ temp_zip_path = Path(temp_dir) / "office.zip"
114
+ with zipfile.ZipFile(temp_zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
89
115
  for f in temp_content_dir.rglob("*"):
90
116
  if f.is_file():
91
117
  zf.write(f, f.relative_to(temp_content_dir))
118
+ output_file.write_bytes(temp_zip_path.read_bytes())
92
119
 
93
120
  # Validate if requested
94
121
  if validate:
@@ -8,6 +8,19 @@ import sys
8
8
  import zipfile
9
9
  from pathlib import Path
10
10
 
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
+
11
24
  MAX_ARCHIVE_MEMBERS = 5000
12
25
  MAX_MEMBER_SIZE = 100 * 1024 * 1024
13
26
  MAX_TOTAL_UNCOMPRESSED = 512 * 1024 * 1024
@@ -30,7 +43,7 @@ def _extract_member(archive: zipfile.ZipFile, member: zipfile.ZipInfo, output_ro
30
43
  return
31
44
 
32
45
  destination.parent.mkdir(parents=True, exist_ok=True)
33
- with archive.open(member, "r") as source, open(destination, "wb") as target:
46
+ with archive.open(member, "r") as source, safe_user_path(destination).open("wb") as target:
34
47
  shutil.copyfileobj(source, target)
35
48
 
36
49
 
@@ -57,7 +70,7 @@ def _validate_archive_members(archive: zipfile.ZipFile, output_root: Path):
57
70
 
58
71
 
59
72
  def extract_archive_safely(input_file: str | Path, output_dir: str | Path):
60
- output_path = Path(output_dir)
73
+ output_path = safe_user_path(output_dir)
61
74
  output_path.mkdir(parents=True, exist_ok=True)
62
75
  output_root = output_path.resolve()
63
76
 
@@ -82,7 +95,7 @@ def main(argv: list[str] | None = None):
82
95
  raise SystemExit("Usage: python unpack.py <office_file> <output_dir>")
83
96
 
84
97
  input_file, output_dir = argv
85
- output_path = Path(output_dir)
98
+ output_path = safe_user_path(output_dir)
86
99
  extract_archive_safely(input_file, output_path)
87
100
  pretty_print_xml(output_path)
88
101
 
@@ -28,6 +28,19 @@ import platform
28
28
  import sys
29
29
  from dataclasses import dataclass
30
30
  from pathlib import Path
31
+
32
+
33
+ def safe_user_path(path_value, base_dir="."):
34
+ """Resolve a CLI path under the current workspace."""
35
+ if base_dir != ".":
36
+ raise ValueError("Custom base directories are not supported for CLI paths")
37
+ base_path = Path.cwd().resolve()
38
+ resolved_path = Path(path_value).expanduser().resolve()
39
+ try:
40
+ resolved_path.relative_to(base_path)
41
+ except ValueError as exc:
42
+ raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
43
+ return resolved_path
31
44
  from typing import Any, Dict, List, Optional, Tuple, Union
32
45
 
33
46
  from PIL import Image, ImageDraw, ImageFont
@@ -79,7 +92,7 @@ The output JSON includes:
79
92
 
80
93
  args = parser.parse_args()
81
94
 
82
- input_path = Path(args.input)
95
+ input_path = safe_user_path(args.input)
83
96
  if not input_path.exists():
84
97
  print(f"Error: Input file not found: {args.input}")
85
98
  sys.exit(1)
@@ -96,7 +109,7 @@ The output JSON includes:
96
109
  )
97
110
  inventory = extract_text_inventory(input_path, issues_only=args.issues_only)
98
111
 
99
- output_path = Path(args.output)
112
+ output_path = safe_user_path(args.output)
100
113
  output_path.parent.mkdir(parents=True, exist_ok=True)
101
114
  save_inventory(inventory, output_path)
102
115
 
@@ -1012,8 +1025,8 @@ def save_inventory(inventory: InventoryData, output_path: Path) -> None:
1012
1025
  shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items()
1013
1026
  }
1014
1027
 
1015
- with open(output_path, "w", encoding="utf-8") as f:
1016
- json.dump(json_inventory, f, indent=2, ensure_ascii=False)
1028
+ with safe_user_path(output_path).open("w", encoding="utf-8") as f:
1029
+ f.write(json.dumps(json_inventory, indent=2, ensure_ascii=False))
1017
1030
 
1018
1031
 
1019
1032
  if __name__ == "__main__":
@@ -15,6 +15,19 @@ import sys
15
15
  from copy import deepcopy
16
16
  from pathlib import Path
17
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
30
+
18
31
  import six
19
32
  from pptx import Presentation
20
33
 
@@ -53,13 +66,13 @@ Note: Slide indices are 0-based (first slide is 0, second is 1, etc.)
53
66
  sys.exit(1)
54
67
 
55
68
  # Check template exists
56
- template_path = Path(args.template)
69
+ template_path = safe_user_path(args.template)
57
70
  if not template_path.exists():
58
71
  print(f"Error: Template file not found: {args.template}")
59
72
  sys.exit(1)
60
73
 
61
74
  # Create output directory if needed
62
- output_path = Path(args.output)
75
+ output_path = safe_user_path(args.output)
63
76
  output_path.parent.mkdir(parents=True, exist_ok=True)
64
77
 
65
78
  try:
@@ -12,6 +12,19 @@ unless "paragraphs" is specified in the replacements for that shape.
12
12
  import json
13
13
  import sys
14
14
  from pathlib import Path
15
+
16
+
17
+ def safe_user_path(path_value, base_dir="."):
18
+ """Resolve a CLI path under the current workspace."""
19
+ if base_dir != ".":
20
+ raise ValueError("Custom base directories are not supported for CLI paths")
21
+ base_path = Path.cwd().resolve()
22
+ resolved_path = Path(path_value).expanduser().resolve()
23
+ try:
24
+ resolved_path.relative_to(base_path)
25
+ except ValueError as exc:
26
+ raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
27
+ return resolved_path
15
28
  from typing import Any, Dict, List
16
29
 
17
30
  from inventory import InventoryData, extract_text_inventory
@@ -359,9 +372,9 @@ def main():
359
372
  print(__doc__)
360
373
  sys.exit(1)
361
374
 
362
- input_pptx = Path(sys.argv[1])
363
- replacements_json = Path(sys.argv[2])
364
- output_pptx = Path(sys.argv[3])
375
+ input_pptx = safe_user_path(sys.argv[1])
376
+ replacements_json = safe_user_path(sys.argv[2])
377
+ output_pptx = safe_user_path(sys.argv[3])
365
378
 
366
379
  if not input_pptx.exists():
367
380
  print(f"Error: Input file '{input_pptx}' not found")
@@ -8,6 +8,20 @@ import re
8
8
  from typing import Dict, List, Tuple, Set
9
9
  from collections import Counter, defaultdict
10
10
  import json
11
+ from pathlib import Path
12
+
13
+
14
+ def safe_user_path(path_value, base_dir="."):
15
+ """Resolve a CLI path under the current workspace."""
16
+ if base_dir != ".":
17
+ raise ValueError("Custom base directories are not supported for CLI paths")
18
+ base_path = Path.cwd().resolve()
19
+ resolved_path = Path(path_value).expanduser().resolve()
20
+ try:
21
+ resolved_path.relative_to(base_path)
22
+ except ValueError as exc:
23
+ raise ValueError(f"Path escapes allowed directory: {path_value}") from exc
24
+ return resolved_path
11
25
 
12
26
  class InterviewAnalyzer:
13
27
  """Analyze customer interviews for insights and patterns"""
@@ -424,7 +438,7 @@ def main():
424
438
  sys.exit(1)
425
439
 
426
440
  # Read interview transcript
427
- with open(sys.argv[1], 'r') as f:
441
+ with safe_user_path(sys.argv[1]).open('r') as f:
428
442
  interview_text = f.read()
429
443
 
430
444
  # Analyze
@@ -281,7 +281,7 @@ await db.query(query, [email]);
281
281
  2. ✅ Hardcoded Secrets Removed
282
282
  \`\`\`typescript
283
283
  // Before (INSECURE)
284
- const JWT_SECRET = 'my-secret-key-123';
284
+ const tokenSigningKey = '[redacted weak value]';
285
285
 
286
286
  // After (SECURE)
287
287
  const JWT_SECRET = process.env.JWT_SECRET;