@pilllesss/yorn 1.0.0

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 (95) hide show
  1. package/README.md +84 -0
  2. package/dist/modes/interactive/theme/dark.json +90 -0
  3. package/dist/modes/interactive/theme/light.json +89 -0
  4. package/dist/modes/interactive/theme/theme-schema.json +352 -0
  5. package/dist/providers/data/.manifest.json +1 -0
  6. package/dist/providers/data/amazon-bedrock.json +1 -0
  7. package/dist/providers/data/ant-ling.json +1 -0
  8. package/dist/providers/data/anthropic.json +1 -0
  9. package/dist/providers/data/azure-openai-responses.json +1 -0
  10. package/dist/providers/data/baseten.json +1 -0
  11. package/dist/providers/data/cerebras.json +1 -0
  12. package/dist/providers/data/cloudflare-ai-gateway.json +1 -0
  13. package/dist/providers/data/cloudflare-workers-ai.json +1 -0
  14. package/dist/providers/data/deepseek.json +1 -0
  15. package/dist/providers/data/fireworks.json +1 -0
  16. package/dist/providers/data/github-copilot.json +1 -0
  17. package/dist/providers/data/google-vertex.json +1 -0
  18. package/dist/providers/data/google.json +1 -0
  19. package/dist/providers/data/groq.json +1 -0
  20. package/dist/providers/data/huggingface.json +1 -0
  21. package/dist/providers/data/kimi-coding.json +1 -0
  22. package/dist/providers/data/minimax-cn.json +1 -0
  23. package/dist/providers/data/minimax.json +1 -0
  24. package/dist/providers/data/mistral.json +1 -0
  25. package/dist/providers/data/moonshotai-cn.json +1 -0
  26. package/dist/providers/data/moonshotai.json +1 -0
  27. package/dist/providers/data/nvidia.json +1 -0
  28. package/dist/providers/data/openai-codex.json +1 -0
  29. package/dist/providers/data/openai.json +1 -0
  30. package/dist/providers/data/opencode-go.json +1 -0
  31. package/dist/providers/data/opencode.json +1 -0
  32. package/dist/providers/data/openrouter.json +1 -0
  33. package/dist/providers/data/qwen-token-plan-cn.json +1 -0
  34. package/dist/providers/data/qwen-token-plan-individual.json +1 -0
  35. package/dist/providers/data/qwen-token-plan.json +1 -0
  36. package/dist/providers/data/together.json +1 -0
  37. package/dist/providers/data/vercel-ai-gateway.json +1 -0
  38. package/dist/providers/data/xai.json +1 -0
  39. package/dist/providers/data/xiaomi-token-plan-ams.json +1 -0
  40. package/dist/providers/data/xiaomi-token-plan-cn.json +1 -0
  41. package/dist/providers/data/xiaomi-token-plan-sgp.json +1 -0
  42. package/dist/providers/data/xiaomi.json +1 -0
  43. package/dist/providers/data/zai-coding-cn.json +1 -0
  44. package/dist/providers/data/zai.json +1 -0
  45. package/dist/skills/add-provider/SKILL.md +151 -0
  46. package/dist/skills/agent-browser/SKILL.md +51 -0
  47. package/dist/skills/frontend-design/LICENSE.txt +177 -0
  48. package/dist/skills/frontend-design/SKILL.md +55 -0
  49. package/dist/skills/goal/SKILL.md +44 -0
  50. package/dist/skills/goal/pyproject.toml +17 -0
  51. package/dist/skills/goal/src/goal/__init__.py +52 -0
  52. package/dist/skills/graphify/SKILL.md +710 -0
  53. package/dist/skills/graphify/references/add-watch.md +56 -0
  54. package/dist/skills/graphify/references/exports.md +87 -0
  55. package/dist/skills/graphify/references/extraction-spec.md +70 -0
  56. package/dist/skills/graphify/references/github-and-merge.md +46 -0
  57. package/dist/skills/graphify/references/hooks.md +33 -0
  58. package/dist/skills/graphify/references/query.md +311 -0
  59. package/dist/skills/graphify/references/transcribe.md +52 -0
  60. package/dist/skills/graphify/references/update.md +210 -0
  61. package/dist/skills/screenshot/SKILL.md +267 -0
  62. package/dist/skills/screenshot/scripts/ensure_macos_permissions.sh +54 -0
  63. package/dist/skills/screenshot/scripts/macos_display_info.swift +22 -0
  64. package/dist/skills/screenshot/scripts/macos_permissions.swift +40 -0
  65. package/dist/skills/screenshot/scripts/macos_window_info.swift +126 -0
  66. package/dist/skills/screenshot/scripts/take_screenshot.ps1 +163 -0
  67. package/dist/skills/screenshot/scripts/take_screenshot.py +585 -0
  68. package/dist/skills/security-best-practices/LICENSE.txt +201 -0
  69. package/dist/skills/security-best-practices/SKILL.md +86 -0
  70. package/dist/skills/security-best-practices/agents/openai.yaml +4 -0
  71. package/dist/skills/security-best-practices/references/golang-general-backend-security.md +826 -0
  72. package/dist/skills/security-best-practices/references/javascript-express-web-server-security.md +1158 -0
  73. package/dist/skills/security-best-practices/references/javascript-general-web-frontend-security.md +747 -0
  74. package/dist/skills/security-best-practices/references/javascript-jquery-web-frontend-security.md +678 -0
  75. package/dist/skills/security-best-practices/references/javascript-typescript-nextjs-web-server-security.md +1144 -0
  76. package/dist/skills/security-best-practices/references/javascript-typescript-react-web-frontend-security.md +990 -0
  77. package/dist/skills/security-best-practices/references/javascript-typescript-vue-web-frontend-security.md +791 -0
  78. package/dist/skills/security-best-practices/references/python-django-web-server-security.md +882 -0
  79. package/dist/skills/security-best-practices/references/python-fastapi-web-server-security.md +1036 -0
  80. package/dist/skills/security-best-practices/references/python-flask-web-server-security.md +705 -0
  81. package/dist/skills/theme-factory/LICENSE.txt +202 -0
  82. package/dist/skills/theme-factory/SKILL.md +59 -0
  83. package/dist/skills/theme-factory/theme-showcase.pdf +0 -0
  84. package/dist/skills/theme-factory/themes/arctic-frost.md +19 -0
  85. package/dist/skills/theme-factory/themes/botanical-garden.md +19 -0
  86. package/dist/skills/theme-factory/themes/desert-rose.md +19 -0
  87. package/dist/skills/theme-factory/themes/forest-canopy.md +19 -0
  88. package/dist/skills/theme-factory/themes/golden-hour.md +19 -0
  89. package/dist/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  90. package/dist/skills/theme-factory/themes/modern-minimalist.md +19 -0
  91. package/dist/skills/theme-factory/themes/ocean-depths.md +19 -0
  92. package/dist/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  93. package/dist/skills/theme-factory/themes/tech-innovation.md +19 -0
  94. package/dist/yorn.cjs +1773 -0
  95. package/package.json +86 -0
@@ -0,0 +1,210 @@
1
+ # graphify reference: incremental update and cluster-only
2
+
3
+ Load this only when the user passed `--update` or `--cluster-only`. A first-time full build never reads this file.
4
+
5
+ ## For --update (incremental re-extraction)
6
+
7
+ Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time.
8
+
9
+ ```bash
10
+ $(cat graphify-out/.graphify_python) -c "
11
+ import sys, json
12
+ from graphify.detect import detect_incremental, save_manifest
13
+ from pathlib import Path
14
+
15
+ result = detect_incremental(Path('INPUT_PATH'))
16
+ new_total = result.get('new_total', 0)
17
+ print(json.dumps(result, indent=2, ensure_ascii=False))
18
+ Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\")
19
+ deleted = list(result.get('deleted_files', []))
20
+ if new_total == 0 and not deleted:
21
+ print('No files changed since last run. Nothing to update.')
22
+ raise SystemExit(0)
23
+ if deleted:
24
+ print(f'{len(deleted)} deleted file(s) to prune.')
25
+ if new_total > 0:
26
+ print(f'{new_total} new/changed file(s) to re-extract.')
27
+ "
28
+ ```
29
+
30
+ Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context:
31
+
32
+ ```bash
33
+ $(cat graphify-out/.graphify_python) -c "
34
+ import json
35
+ from pathlib import Path
36
+ r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
37
+ Path('graphify-out/.graphify_detect.json').write_text(json.dumps({
38
+ 'files': r.get('new_files', {}),
39
+ 'all_files': r.get('files', {}),
40
+ 'total_files': r.get('new_total', 0),
41
+ 'total_words': r.get('total_words', 0),
42
+ 'skipped_sensitive': r.get('skipped_sensitive', []),
43
+ 'needs_graph': True,
44
+ }, ensure_ascii=False), encoding=\"utf-8\")
45
+ "
46
+ ```
47
+
48
+ If new files exist, first check whether all changed files are code files:
49
+
50
+ ```bash
51
+ $(cat graphify-out/.graphify_python) -c "
52
+ import json
53
+ from pathlib import Path
54
+
55
+ result = json.loads(open('graphify-out/.graphify_incremental.json', encoding='utf-8').read()) if Path('graphify-out/.graphify_incremental.json').exists() else {}
56
+ code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts','.lua','.toc','.f','.F','.f90','.F90','.f95','.F95','.f03','.F03','.f08','.F08'}
57
+ new_files = result.get('new_files', {})
58
+ all_changed = [f for files in new_files.values() for f in files]
59
+ code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed)
60
+ print('code_only:', code_only)
61
+ "
62
+ ```
63
+
64
+ If `code_only` is True: print `[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)`, run only Step 3A (AST) on the changed files, skip Step 3B entirely (no subagents), then go straight to merge and Steps 4–8.
65
+
66
+ If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal.
67
+
68
+
69
+ If no new files exist (only deletions), create an empty extraction so the merge step can prune:
70
+
71
+ ```bash
72
+ if [ ! -f graphify-out/.graphify_extract.json ]; then
73
+ echo '[graphify update] Only deletions -- creating empty extraction for merge.'
74
+ $(cat graphify-out/.graphify_python) -c "
75
+ import json
76
+ from pathlib import Path
77
+ Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
78
+ "
79
+ fi
80
+ ```
81
+
82
+
83
+ Then:
84
+
85
+ ```bash
86
+ $(cat graphify-out/.graphify_python) -c "
87
+ import json
88
+ from pathlib import Path
89
+ from graphify.build import build_merge
90
+ from graphify.detect import save_manifest
91
+
92
+ # Load new extraction and incremental state
93
+ new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
94
+ incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
95
+ deleted = list(incremental.get('deleted_files', []))
96
+ # prune_sources is ONLY for genuinely DELETED files. Changed/re-extracted files are
97
+ # handled by build_merge's replace-on-re-extract (#1344): every source_file in
98
+ # new_chunks is dropped from the base before merge, so old/stale nodes don't survive.
99
+ # Do NOT add `changed` here: with root= passed, prune_set relativizes to the same base
100
+ # as the freshly merged nodes and would DELETE the re-extracted content (#1178 is moot
101
+ # now that replace — not the dedup pass — reconciles changed files).
102
+ prune = list(deleted) or None
103
+
104
+ # Use build_merge() — reads graph.json directly without NetworkX round-trip
105
+ # so edge direction (calls, implements, imports) is always preserved (#801).
106
+ # Pass root= so prune_sources (absolute paths from detect_incremental) are
107
+ # relativized to match the graph's relative source_file values; without it
108
+ # nothing is pruned and stale nodes accumulate on every update (#1361).
109
+ # directed=IS_DIRECTED: replace IS_DIRECTED with True if --directed was given, else
110
+ # False. Without it a --directed --update silently rebuilds undirected and collapses
111
+ # reciprocal A<->B edges (#1392).
112
+ G = build_merge(
113
+ [new_extraction],
114
+ graph_path='graphify-out/graph.json',
115
+ prune_sources=prune,
116
+ root='INPUT_PATH',
117
+ directed=IS_DIRECTED,
118
+ )
119
+ print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
120
+
121
+ # Write merged result back to .graphify_extract.json so Step 4 sees the full graph
122
+ merged_out = {
123
+ 'nodes': [{'id': n, **d} for n, d in G.nodes(data=True)],
124
+ 'edges': [
125
+ # Explicit source/target last so they win over any stale attrs in d.
126
+ {**{k: val for k, val in d.items() if k not in ('_src', '_tgt', 'source', 'target')},
127
+ 'source': d.get('_src', u), 'target': d.get('_tgt', v)}
128
+ for u, v, d in G.edges(data=True)
129
+ ],
130
+ # G.graph["hyperedges"] holds hyperedges from both existing graph.json
131
+ # and new_extraction (build_merge combines them). Falling back to
132
+ # new_extraction only would silently drop prior-run hyperedges (#801).
133
+ 'hyperedges': list(G.graph.get('hyperedges', [])),
134
+ 'input_tokens': new_extraction.get('input_tokens', 0),
135
+ 'output_tokens': new_extraction.get('output_tokens', 0),
136
+ }
137
+ Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged_out, ensure_ascii=False), encoding=\"utf-8\")
138
+ print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"])} nodes, {len(merged_out[\"edges\"])} edges)')
139
+
140
+ # Save manifest so next --update diffs against today's state, not the
141
+ # prior run's baseline (prevents ghost-node reports on subsequent updates).
142
+ # root= matches the build_merge call above so the manifest keys stay relative to
143
+ # the scan root — portable across clones/machines, so --update keeps matching
144
+ # cached files instead of missing every one after a move (#1417).
145
+ #
146
+ # Only stamp semantic files (docs/papers/images) that ACTUALLY produced output
147
+ # THIS run (new_extraction is this run's fresh extraction, read above before the
148
+ # merge overwrote the file): a changed doc whose chunk failed must stay unstamped
149
+ # so the next --update re-queues it, otherwise it is marked done and its content
150
+ # is lost forever (#2015). Mirrors the library extract path
151
+ # (cli._stamped_manifest_files + clear_semantic + scan_corpus).
152
+ from graphify.cli import _stamped_manifest_files
153
+ _manifest_files = _stamped_manifest_files(incremental['files'], new_extraction, Path('INPUT_PATH'))
154
+ # Changed semantic files dispatched this run but NOT stamped had their chunk fail
155
+ # or be omitted; clear any stale semantic_hash so they are re-queued (#1948).
156
+ _sem_types = ('document', 'paper', 'image')
157
+ _dispatched = {f for t, fl in incremental.get('new_files', {}).items() if t in _sem_types for f in fl}
158
+ _stamped = {f for fl in _manifest_files.values() for f in fl}
159
+ _cleared = _dispatched - _stamped
160
+ # scan_corpus = the RAW full corpus so in-root files newly excluded since last run
161
+ # are dropped rather than masquerading as deletions; untouched rows preserved (#1908).
162
+ _scan = {f for fl in incremental['files'].values() for f in fl}
163
+ save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
164
+ print('[graphify update] Manifest saved.')
165
+ "
166
+ ```
167
+
168
+ Then run Steps 4–8 on the merged graph as normal.
169
+
170
+ After Step 4, show the graph diff:
171
+
172
+ ```bash
173
+ $(cat graphify-out/.graphify_python) -c "
174
+ import json
175
+ from graphify.analyze import graph_diff
176
+ from graphify.build import build_from_json
177
+ from networkx.readwrite import json_graph
178
+ import networkx as nx
179
+ from pathlib import Path
180
+
181
+ # Load old graph (before update) from backup written before merge
182
+ old_data = json.loads(Path('graphify-out/.graphify_old.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_old.json').exists() else None
183
+ new_extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
184
+ G_new = build_from_json(new_extract, directed=IS_DIRECTED)
185
+
186
+ if old_data:
187
+ G_old = json_graph.node_link_graph(old_data, edges='links')
188
+ diff = graph_diff(G_old, G_new)
189
+ print(diff['summary'])
190
+ if diff['new_nodes']:
191
+ print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5]))
192
+ if diff['new_edges']:
193
+ print('New edges:', len(diff['new_edges']))
194
+ "
195
+ ```
196
+
197
+ Before the merge step, save the old graph: `cp graphify-out/graph.json graphify-out/.graphify_old.json`
198
+ Clean up after: `rm -f graphify-out/.graphify_old.json`
199
+
200
+ ---
201
+
202
+ ## For --cluster-only
203
+
204
+ Skip Steps 1–3. Re-run clustering on the existing graph:
205
+
206
+ ```bash
207
+ graphify cluster-only .
208
+ ```
209
+
210
+ `graphify cluster-only .` is **self-contained**: it re-clusters, names communities, and regenerates `GRAPH_REPORT.md`, `graph.json`, and `graph.html` from the existing graph. **Do not re-run Steps 5–9** — they read intermediate files (`.graphify_extract.json`, `.graphify_detect.json`, `.graphify_analysis.json`) that a prior build's cleanup (Step 9) already deleted, so they raise `FileNotFoundError` (#1392). When it finishes, present the refreshed `GRAPH_REPORT.md` summary as usual.
@@ -0,0 +1,267 @@
1
+ ---
2
+ name: "screenshot"
3
+ description: "Use when the user explicitly asks for a desktop or system screenshot (full screen, specific app or window, or a pixel region), or when tool-specific capture capabilities are unavailable and an OS-level capture is needed."
4
+ ---
5
+
6
+
7
+ # Screenshot Capture
8
+
9
+ Follow these save-location rules every time:
10
+
11
+ 1) If the user specifies a path, save there.
12
+ 2) If the user asks for a screenshot without a path, save to the OS default screenshot location.
13
+ 3) If Codex needs a screenshot for its own inspection, save to the temp directory.
14
+
15
+ ## Tool priority
16
+
17
+ - Prefer tool-specific screenshot capabilities when available (for example: a Figma MCP/skill for Figma files, or Playwright/agent-browser tools for browsers and Electron apps).
18
+ - Use this skill when explicitly asked, for whole-system desktop captures, or when a tool-specific capture cannot get what you need.
19
+ - Otherwise, treat this skill as the default for desktop apps without a better-integrated capture tool.
20
+
21
+ ## macOS permission preflight (reduce repeated prompts)
22
+
23
+ On macOS, run the preflight helper once before window/app capture. It checks
24
+ Screen Recording permission, explains why it is needed, and requests it in one
25
+ place.
26
+
27
+ The helpers route Swift's module cache to `$TMPDIR/codex-swift-module-cache`
28
+ to avoid extra sandbox module-cache prompts.
29
+
30
+ ```bash
31
+ bash <path-to-skill>/scripts/ensure_macos_permissions.sh
32
+ ```
33
+
34
+ To avoid multiple sandbox approval prompts, combine preflight + capture in one
35
+ command when possible:
36
+
37
+ ```bash
38
+ bash <path-to-skill>/scripts/ensure_macos_permissions.sh && \
39
+ python3 <path-to-skill>/scripts/take_screenshot.py --app "Codex"
40
+ ```
41
+
42
+ For Codex inspection runs, keep the output in temp:
43
+
44
+ ```bash
45
+ bash <path-to-skill>/scripts/ensure_macos_permissions.sh && \
46
+ python3 <path-to-skill>/scripts/take_screenshot.py --app "<App>" --mode temp
47
+ ```
48
+
49
+ Use the bundled scripts to avoid re-deriving OS-specific commands.
50
+
51
+ ## macOS and Linux (Python helper)
52
+
53
+ Run the helper from the repo root:
54
+
55
+ ```bash
56
+ python3 <path-to-skill>/scripts/take_screenshot.py
57
+ ```
58
+
59
+ Common patterns:
60
+
61
+ - Default location (user asked for "a screenshot"):
62
+
63
+ ```bash
64
+ python3 <path-to-skill>/scripts/take_screenshot.py
65
+ ```
66
+
67
+ - Temp location (Codex visual check):
68
+
69
+ ```bash
70
+ python3 <path-to-skill>/scripts/take_screenshot.py --mode temp
71
+ ```
72
+
73
+ - Explicit location (user provided a path or filename):
74
+
75
+ ```bash
76
+ python3 <path-to-skill>/scripts/take_screenshot.py --path output/screen.png
77
+ ```
78
+
79
+ - App/window capture by app name (macOS only; substring match is OK; captures all matching windows):
80
+
81
+ ```bash
82
+ python3 <path-to-skill>/scripts/take_screenshot.py --app "Codex"
83
+ ```
84
+
85
+ - Specific window title within an app (macOS only):
86
+
87
+ ```bash
88
+ python3 <path-to-skill>/scripts/take_screenshot.py --app "Codex" --window-name "Settings"
89
+ ```
90
+
91
+ - List matching window ids before capturing (macOS only):
92
+
93
+ ```bash
94
+ python3 <path-to-skill>/scripts/take_screenshot.py --list-windows --app "Codex"
95
+ ```
96
+
97
+ - Pixel region (x,y,w,h):
98
+
99
+ ```bash
100
+ python3 <path-to-skill>/scripts/take_screenshot.py --mode temp --region 100,200,800,600
101
+ ```
102
+
103
+ - Focused/active window (captures only the frontmost window; use `--app` to capture all windows):
104
+
105
+ ```bash
106
+ python3 <path-to-skill>/scripts/take_screenshot.py --mode temp --active-window
107
+ ```
108
+
109
+ - Specific window id (use --list-windows on macOS to discover ids):
110
+
111
+ ```bash
112
+ python3 <path-to-skill>/scripts/take_screenshot.py --window-id 12345
113
+ ```
114
+
115
+ The script prints one path per capture. When multiple windows or displays match, it prints multiple paths (one per line) and adds suffixes like `-w<windowId>` or `-d<display>`. View each path sequentially with the image viewer tool, and only manipulate images if needed or requested.
116
+
117
+ ### Workflow examples
118
+
119
+ - "Take a look at <App> and tell me what you see": capture to temp, then view each printed path in order.
120
+
121
+ ```bash
122
+ bash <path-to-skill>/scripts/ensure_macos_permissions.sh && \
123
+ python3 <path-to-skill>/scripts/take_screenshot.py --app "<App>" --mode temp
124
+ ```
125
+
126
+ - "The design from Figma is not matching what is implemented": use a Figma MCP/skill to capture the design first, then capture the running app with this skill (typically to temp) and compare the raw screenshots before any manipulation.
127
+
128
+ ### Multi-display behavior
129
+
130
+ - On macOS, full-screen captures save one file per display when multiple monitors are connected.
131
+ - On Linux and Windows, full-screen captures use the virtual desktop (all monitors in one image); use `--region` to isolate a single display when needed.
132
+
133
+ ### Linux prerequisites and selection logic
134
+
135
+ The helper automatically selects the first available tool:
136
+
137
+ 1) `scrot`
138
+ 2) `gnome-screenshot`
139
+ 3) ImageMagick `import`
140
+
141
+ If none are available, ask the user to install one of them and retry.
142
+
143
+ Coordinate regions require `scrot` or ImageMagick `import`.
144
+
145
+ `--app`, `--window-name`, and `--list-windows` are macOS-only. On Linux, use
146
+ `--active-window` or provide `--window-id` when available.
147
+
148
+ ## Windows (PowerShell helper)
149
+
150
+ Run the PowerShell helper:
151
+
152
+ ```powershell
153
+ powershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1
154
+ ```
155
+
156
+ Common patterns:
157
+
158
+ - Default location:
159
+
160
+ ```powershell
161
+ powershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1
162
+ ```
163
+
164
+ - Temp location (Codex visual check):
165
+
166
+ ```powershell
167
+ powershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Mode temp
168
+ ```
169
+
170
+ - Explicit path:
171
+
172
+ ```powershell
173
+ powershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Path "C:\Temp\screen.png"
174
+ ```
175
+
176
+ - Pixel region (x,y,w,h):
177
+
178
+ ```powershell
179
+ powershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Mode temp -Region 100,200,800,600
180
+ ```
181
+
182
+ - Active window (ask the user to focus it first):
183
+
184
+ ```powershell
185
+ powershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -Mode temp -ActiveWindow
186
+ ```
187
+
188
+ - Specific window handle (only when provided):
189
+
190
+ ```powershell
191
+ powershell -ExecutionPolicy Bypass -File <path-to-skill>/scripts/take_screenshot.ps1 -WindowHandle 123456
192
+ ```
193
+
194
+ ## Direct OS commands (fallbacks)
195
+
196
+ Use these when you cannot run the helpers.
197
+
198
+ ### macOS
199
+
200
+ - Full screen to a specific path:
201
+
202
+ ```bash
203
+ screencapture -x output/screen.png
204
+ ```
205
+
206
+ - Pixel region:
207
+
208
+ ```bash
209
+ screencapture -x -R100,200,800,600 output/region.png
210
+ ```
211
+
212
+ - Specific window id:
213
+
214
+ ```bash
215
+ screencapture -x -l12345 output/window.png
216
+ ```
217
+
218
+ - Interactive selection or window pick:
219
+
220
+ ```bash
221
+ screencapture -x -i output/interactive.png
222
+ ```
223
+
224
+ ### Linux
225
+
226
+ - Full screen:
227
+
228
+ ```bash
229
+ scrot output/screen.png
230
+ ```
231
+
232
+ ```bash
233
+ gnome-screenshot -f output/screen.png
234
+ ```
235
+
236
+ ```bash
237
+ import -window root output/screen.png
238
+ ```
239
+
240
+ - Pixel region:
241
+
242
+ ```bash
243
+ scrot -a 100,200,800,600 output/region.png
244
+ ```
245
+
246
+ ```bash
247
+ import -window root -crop 800x600+100+200 output/region.png
248
+ ```
249
+
250
+ - Active window:
251
+
252
+ ```bash
253
+ scrot -u output/window.png
254
+ ```
255
+
256
+ ```bash
257
+ gnome-screenshot -w -f output/window.png
258
+ ```
259
+
260
+ ## Error handling
261
+
262
+ - On macOS, run `bash <path-to-skill>/scripts/ensure_macos_permissions.sh` first to request Screen Recording in one place.
263
+ - If you see "screen capture checks are blocked in the sandbox", "could not create image from display", or Swift `ModuleCache` permission errors in a sandboxed run, rerun the command with escalated permissions.
264
+ - If macOS app/window capture returns no matches, run `--list-windows --app "AppName"` and retry with `--window-id`, and make sure the app is visible on screen.
265
+ - If Linux region/window capture fails, check tool availability with `command -v scrot`, `command -v gnome-screenshot`, and `command -v import`.
266
+ - If saving to the OS default location fails with permission errors in a sandbox, rerun the command with escalated permissions.
267
+ - Always report the saved file path in the response.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [[ "$(uname)" != "Darwin" ]]; then
5
+ echo "ensure_macos_permissions.sh only supports macOS" >&2
6
+ exit 1
7
+ fi
8
+
9
+ if ! command -v swift >/dev/null 2>&1; then
10
+ echo "swift is required to check macOS screen capture permissions" >&2
11
+ exit 1
12
+ fi
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ PERM_SWIFT="$SCRIPT_DIR/macos_permissions.swift"
16
+ MODULE_CACHE="${TMPDIR:-/tmp}/codex-swift-module-cache"
17
+ mkdir -p "$MODULE_CACHE"
18
+
19
+ screen_capture_status() {
20
+ local json
21
+ json="$(swift -module-cache-path "$MODULE_CACHE" "$PERM_SWIFT" "$@")"
22
+ python3 -c 'import json, sys; data=json.loads(sys.argv[1]); print("1" if data.get("screenCapture") else "0")' "$json"
23
+ }
24
+
25
+ if [[ -n "${CODEX_SANDBOX:-}" ]]; then
26
+ echo "Screen capture checks are blocked in the sandbox; rerun with escalated permissions." >&2
27
+ exit 3
28
+ fi
29
+
30
+ if [[ "$(screen_capture_status)" == "1" ]]; then
31
+ echo "Screen Recording permission already granted."
32
+ exit 0
33
+ fi
34
+
35
+ cat <<'MSG'
36
+ This workflow needs macOS Screen Recording permission to capture screenshots.
37
+ macOS will show a single system prompt for Screen Recording. Approve it, then
38
+ return here. If macOS opens System Settings instead of prompting, enable Screen
39
+ Recording for your terminal and rerun the command.
40
+ MSG
41
+
42
+ # Request permission once after explaining why it is needed.
43
+ screen_capture_status --request >/dev/null || true
44
+
45
+ if [[ "$(screen_capture_status)" != "1" ]]; then
46
+ cat <<'MSG'
47
+ Screen Recording is still not granted.
48
+ Open System Settings > Privacy & Security > Screen Recording and enable it for
49
+ your terminal (and Codex if needed), then rerun your screenshot command.
50
+ MSG
51
+ exit 2
52
+ fi
53
+
54
+ echo "Screen Recording permission granted."
@@ -0,0 +1,22 @@
1
+ import AppKit
2
+ import Foundation
3
+
4
+ struct Response: Encodable {
5
+ let count: Int
6
+ let displays: [Int]
7
+ }
8
+
9
+ let count = max(NSScreen.screens.count, 1)
10
+ let displays = Array(1...count)
11
+
12
+ let response = Response(count: count, displays: displays)
13
+ let encoder = JSONEncoder()
14
+ encoder.outputFormatting = [.sortedKeys]
15
+
16
+ if let data = try? encoder.encode(response),
17
+ let json = String(data: data, encoding: .utf8) {
18
+ print(json)
19
+ } else {
20
+ fputs("{\"count\":\(count)}\n", stderr)
21
+ exit(1)
22
+ }
@@ -0,0 +1,40 @@
1
+ import CoreGraphics
2
+ import Foundation
3
+
4
+ struct Status: Encodable {
5
+ let screenCapture: Bool
6
+ let requested: Bool
7
+ }
8
+
9
+ let shouldRequest = CommandLine.arguments.contains("--request")
10
+
11
+ @available(macOS 10.15, *)
12
+ func screenCaptureGranted(request: Bool) -> Bool {
13
+ if CGPreflightScreenCaptureAccess() {
14
+ return true
15
+ }
16
+ if request {
17
+ _ = CGRequestScreenCaptureAccess()
18
+ return CGPreflightScreenCaptureAccess()
19
+ }
20
+ return false
21
+ }
22
+
23
+ let granted: Bool
24
+ if #available(macOS 10.15, *) {
25
+ granted = screenCaptureGranted(request: shouldRequest)
26
+ } else {
27
+ granted = true
28
+ }
29
+
30
+ let status = Status(screenCapture: granted, requested: shouldRequest)
31
+ let encoder = JSONEncoder()
32
+ encoder.outputFormatting = [.sortedKeys]
33
+
34
+ if let data = try? encoder.encode(status),
35
+ let json = String(data: data, encoding: .utf8) {
36
+ print(json)
37
+ } else {
38
+ fputs("{\"requested\":\(shouldRequest),\"screenCapture\":\(granted)}\n", stderr)
39
+ exit(1)
40
+ }