@maccesar/aiskills 1.22.0 → 1.23.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.
@@ -0,0 +1,427 @@
1
+ #!/usr/bin/env python3
2
+ """Reusable primitives copied into an approved technical-video package."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import os
8
+ import re
9
+ import shutil
10
+ import signal
11
+ import subprocess
12
+ import tempfile
13
+ import time
14
+ from pathlib import Path
15
+ from statistics import median
16
+
17
+
18
+ DEFAULT_VSCODE_PROFILE = {
19
+ 'logical_display': (1920, 1080),
20
+ 'minimum_window_size': (1850, 1000),
21
+ 'auxiliary_panel_key_code': 19,
22
+ 'terminal_open_delay': 1.5,
23
+ 'terminal_drag_x': 645,
24
+ 'terminal_target_y': 720,
25
+ 'terminal_search_y': (90, 950)
26
+ }
27
+
28
+ SWIFT_DRAG_SOURCE = r'''
29
+ import CoreGraphics
30
+ import Foundation
31
+
32
+ guard CommandLine.arguments.count == 5,
33
+ let startX = Double(CommandLine.arguments[1]),
34
+ let startY = Double(CommandLine.arguments[2]),
35
+ let endX = Double(CommandLine.arguments[3]),
36
+ let endY = Double(CommandLine.arguments[4]) else {
37
+ exit(2)
38
+ }
39
+
40
+ let start = CGPoint(x: startX, y: startY)
41
+ let end = CGPoint(x: endX, y: endY)
42
+ CGEvent(mouseEventSource: nil, mouseType: .mouseMoved,
43
+ mouseCursorPosition: start, mouseButton: .left)?.post(tap: .cghidEventTap)
44
+ usleep(100_000)
45
+ CGEvent(mouseEventSource: nil, mouseType: .leftMouseDown,
46
+ mouseCursorPosition: start, mouseButton: .left)?.post(tap: .cghidEventTap)
47
+ for step in 1...20 {
48
+ let progress = Double(step) / 20.0
49
+ let point = CGPoint(
50
+ x: startX + ((endX - startX) * progress),
51
+ y: startY + ((endY - startY) * progress)
52
+ )
53
+ CGEvent(mouseEventSource: nil, mouseType: .leftMouseDragged,
54
+ mouseCursorPosition: point, mouseButton: .left)?.post(tap: .cghidEventTap)
55
+ usleep(15_000)
56
+ }
57
+ CGEvent(mouseEventSource: nil, mouseType: .leftMouseUp,
58
+ mouseCursorPosition: end, mouseButton: .left)?.post(tap: .cghidEventTap)
59
+ '''
60
+
61
+
62
+ class MacVsCodeRecording:
63
+ def __init__(
64
+ self, source_project, output_video, output_events, slug,
65
+ copies_root=None, display_number=1
66
+ ):
67
+ self.source_project = Path(source_project).resolve()
68
+ self.output_video = Path(output_video).resolve()
69
+ self.output_events = Path(output_events).resolve()
70
+ self.slug = slug
71
+ configured_root = copies_root or os.environ.get('TECHNICAL_DEMO_COPIES_ROOT')
72
+ self.copies_root = Path(configured_root).expanduser().resolve() if configured_root else Path.home() / 'TechnicalDemos'
73
+ self.display_number = int(display_number)
74
+ self.temp_root = None
75
+ self.project = None
76
+ self.window_slug = None
77
+ self.capture = None
78
+ self.drag_helper = None
79
+ self.events = []
80
+
81
+ @staticmethod
82
+ def _apple_string(value):
83
+ return str(value).replace('\\', '\\\\').replace('"', '\\"')
84
+
85
+ def preflight(self, required_files=(), forbidden_files=()):
86
+ for tool in (
87
+ 'code', 'magick', 'osascript', 'screencapture', 'swift', 'swiftc'
88
+ ):
89
+ if shutil.which(tool) is None:
90
+ raise RuntimeError(f'missing required tool: {tool}')
91
+ if not self.source_project.is_dir():
92
+ raise RuntimeError(f'project not found: {self.source_project}')
93
+ missing = [path for path in required_files if not (self.source_project / path).exists()]
94
+ if missing:
95
+ raise RuntimeError('missing project files: ' + ', '.join(missing))
96
+ present = [path for path in forbidden_files if (self.source_project / path).exists()]
97
+ if present:
98
+ raise RuntimeError('source already contains generated files: ' + ', '.join(present))
99
+ for artifact in (self.output_video, self.output_events):
100
+ if artifact.exists():
101
+ raise RuntimeError(f'refusing to overwrite: {artifact}')
102
+
103
+ @staticmethod
104
+ def logical_display_size():
105
+ source = (
106
+ 'import AppKit; '
107
+ 'let f = NSScreen.main!.frame; '
108
+ 'print(Int(f.width), Int(f.height))'
109
+ )
110
+ result = subprocess.run(
111
+ ['swift', '-e', source], check=True, capture_output=True, text=True
112
+ )
113
+ width, height = result.stdout.strip().split()
114
+ return int(width), int(height)
115
+
116
+ def create_copy(self):
117
+ copies_root = self.copies_root
118
+ copies_root.mkdir(parents=True, exist_ok=True)
119
+ if not copies_root.is_dir():
120
+ raise RuntimeError(f'recording copies folder not found: {copies_root}')
121
+ self.temp_root = Path(tempfile.mkdtemp(prefix=f'{self.slug}-recording.'))
122
+ self.project = Path(tempfile.mkdtemp(prefix=f'{self.slug}.', dir=copies_root))
123
+ self.window_slug = self.project.name
124
+ shutil.copytree(self.source_project, self.project, dirs_exist_ok=True)
125
+ self.drag_helper = self.temp_root / 'drag'
126
+ subprocess.run(
127
+ ['swiftc', '-o', str(self.drag_helper), '-'],
128
+ input=SWIFT_DRAG_SOURCE, check=True, capture_output=True, text=True
129
+ )
130
+ return self.project
131
+
132
+ def open_vscode(self):
133
+ subprocess.run(['code', '-n', str(self.project)], check=True)
134
+
135
+ def targeted(self, body, capture_output=False):
136
+ slug = self._apple_string(self.window_slug)
137
+ script = f'''
138
+ set slug to "{slug}"
139
+ tell application "System Events"
140
+ tell process "Code"
141
+ set targetWindow to missing value
142
+ repeat with candidateWindow in windows
143
+ if name of candidateWindow contains slug then
144
+ set targetWindow to candidateWindow
145
+ exit repeat
146
+ end if
147
+ end repeat
148
+ if targetWindow is missing value then error "Target window not found: " & slug
149
+ perform action "AXRaise" of targetWindow
150
+ set frontmost to true
151
+ delay 0.15
152
+ if name of front window does not contain slug then error "Wrong front window"
153
+ {body}
154
+ end tell
155
+ end tell
156
+ '''
157
+ result = subprocess.run(
158
+ ['osascript', '-e', script], check=True,
159
+ capture_output=capture_output, text=True
160
+ )
161
+ return result.stdout.strip() if capture_output else ''
162
+
163
+ def wait_for_window(self, timeout=30):
164
+ deadline = time.monotonic() + timeout
165
+ while time.monotonic() < deadline:
166
+ try:
167
+ self.targeted('return name of front window', True)
168
+ return
169
+ except subprocess.CalledProcessError:
170
+ time.sleep(0.4)
171
+ raise RuntimeError('VS Code window did not appear')
172
+
173
+ def assert_window_size(self, minimum_size):
174
+ raw_size = self.targeted(
175
+ 'set s to size of front window\n'
176
+ 'return (item 1 of s as text) & "," & (item 2 of s as text)',
177
+ True
178
+ )
179
+ width, height = [int(value) for value in raw_size.split(',')]
180
+ if width < minimum_size[0] or height < minimum_size[1]:
181
+ raise RuntimeError(
182
+ f'VS Code window is {width}x{height}; minimum is '
183
+ f'{minimum_size[0]}x{minimum_size[1]}'
184
+ )
185
+
186
+ def window_geometry(self):
187
+ raw_geometry = self.targeted(
188
+ 'set p to position of front window\n'
189
+ 'set s to size of front window\n'
190
+ 'return (item 1 of p as text) & "," & '
191
+ '(item 2 of p as text) & "," & '
192
+ '(item 1 of s as text) & "," & (item 2 of s as text)',
193
+ True
194
+ )
195
+ return tuple(int(value) for value in raw_geometry.split(','))
196
+
197
+ def open_terminal(self):
198
+ self.targeted(
199
+ 'click menu item "New Terminal" of menu 1 of '
200
+ 'menu bar item "Terminal" of menu bar 1'
201
+ )
202
+
203
+ def drag(self, start, end):
204
+ self.targeted('return name of front window', True)
205
+ subprocess.run([
206
+ str(self.drag_helper), str(start[0]), str(start[1]),
207
+ str(end[0]), str(end[1])
208
+ ], check=True)
209
+
210
+ @staticmethod
211
+ def _column_pixels(image_path, x, height):
212
+ result = subprocess.run([
213
+ 'magick', str(image_path), '-crop', f'1x{height}+{x}+0',
214
+ '-depth', '8', 'txt:-'
215
+ ], check=True, capture_output=True, text=True)
216
+ pattern = re.compile(r'^0,(\d+): \((\d+),(\d+),(\d+)')
217
+ pixels = []
218
+ for line in result.stdout.splitlines():
219
+ match = pattern.match(line)
220
+ if match:
221
+ pixels.append(tuple(int(value) for value in match.groups()[1:]))
222
+ if len(pixels) != height:
223
+ raise RuntimeError(
224
+ f'could not read screenshot column at x={x}: '
225
+ f'expected {height} pixels, found {len(pixels)}'
226
+ )
227
+ return pixels
228
+
229
+ @staticmethod
230
+ def _average_color(pixels, start, end):
231
+ count = end - start
232
+ return tuple(
233
+ sum(pixels[index][channel] for index in range(start, end)) / count
234
+ for channel in range(3)
235
+ )
236
+
237
+ def detect_terminal_divider(self, screenshot_path, search_range):
238
+ window_x, window_y, window_width, window_height = self.window_geometry()
239
+ dimensions = subprocess.run([
240
+ 'magick', 'identify', '-format', '%w,%h', str(screenshot_path)
241
+ ], check=True, capture_output=True, text=True).stdout.strip()
242
+ image_width, image_height = [int(value) for value in dimensions.split(',')]
243
+ logical_width, logical_height = self.logical_display_size()
244
+ scale_x = image_width / logical_width
245
+ scale_y = image_height / logical_height
246
+ if abs(scale_x - scale_y) > 0.01:
247
+ raise RuntimeError('display screenshot has inconsistent pixel scaling')
248
+ scale = scale_x
249
+
250
+ logical_xs = [
251
+ window_x + round(window_width * fraction)
252
+ for fraction in (0.20, 0.35, 0.55)
253
+ ]
254
+ columns = [
255
+ self._column_pixels(
256
+ screenshot_path, round(logical_x * scale), image_height
257
+ )
258
+ for logical_x in logical_xs
259
+ ]
260
+ first_y = max(window_y + search_range[0], window_y + 80)
261
+ last_y = min(
262
+ window_y + search_range[1], window_y + window_height - 100
263
+ )
264
+ start = max(round(first_y * scale), 16)
265
+ end = min(round(last_y * scale), image_height - 16)
266
+
267
+ best = None
268
+ for y in range(start, end):
269
+ column_scores = []
270
+ for pixels in columns:
271
+ above = self._average_color(pixels, y - 12, y - 4)
272
+ below = self._average_color(pixels, y + 4, y + 12)
273
+ distance = sum(
274
+ (above[channel] - below[channel]) ** 2
275
+ for channel in range(3)
276
+ ) ** 0.5
277
+ column_scores.append(distance)
278
+ score = median(column_scores)
279
+ if min(column_scores) < score * 0.6:
280
+ continue
281
+ if best is None or score > best[0]:
282
+ best = (score, y)
283
+
284
+ if best is None or best[0] < 6:
285
+ raise RuntimeError(
286
+ 'could not locate the horizontal terminal divider with confidence'
287
+ )
288
+ return round(best[1] / scale)
289
+
290
+ def resize_terminal_panel(self, x, target_y, search_range=(90, 950)):
291
+ if self.temp_root is None:
292
+ raise RuntimeError('disposable project has not been created')
293
+ before = self.temp_root / 'terminal-before.png'
294
+ after = self.temp_root / 'terminal-after.png'
295
+ self.targeted('return name of front window', True)
296
+ subprocess.run([
297
+ '/usr/sbin/screencapture', '-x', f'-D{self.display_number}', str(before)
298
+ ], check=True)
299
+ handle_offset = 5
300
+ start_y = (
301
+ self.detect_terminal_divider(before, search_range) + handle_offset
302
+ )
303
+ if abs(start_y - target_y) > 4:
304
+ self.drag((x, start_y), (x, target_y))
305
+ time.sleep(0.5)
306
+ subprocess.run([
307
+ '/usr/sbin/screencapture', '-x', f'-D{self.display_number}', str(after)
308
+ ], check=True)
309
+ actual_y = (
310
+ self.detect_terminal_divider(after, search_range) + handle_offset
311
+ )
312
+ if abs(actual_y - target_y) > 8:
313
+ raise RuntimeError(
314
+ f'terminal divider verification failed: expected y={target_y}, '
315
+ f'found y={actual_y} (started at y={start_y})'
316
+ )
317
+ before.unlink(missing_ok=True)
318
+ after.unlink(missing_ok=True)
319
+ return start_y, actual_y
320
+
321
+ def prepare_default_vscode(self, profile=None):
322
+ selected = profile or DEFAULT_VSCODE_PROFILE
323
+ if self.logical_display_size() != tuple(selected['logical_display']):
324
+ raise RuntimeError('logical display does not match the accepted profile')
325
+ self.create_copy()
326
+ self.open_vscode()
327
+ self.wait_for_window()
328
+ self.assert_window_size(selected['minimum_window_size'])
329
+ auxiliary_panel_key_code = selected.get('auxiliary_panel_key_code')
330
+ if auxiliary_panel_key_code is not None:
331
+ self.hotkey(auxiliary_panel_key_code)
332
+ time.sleep(0.4)
333
+ self.open_terminal()
334
+ time.sleep(selected.get('terminal_open_delay', 1.5))
335
+ self.resize_terminal_panel(
336
+ selected['terminal_drag_x'], selected['terminal_target_y'],
337
+ selected['terminal_search_y']
338
+ )
339
+ time.sleep(0.4)
340
+
341
+ def quick_open(self, relative_path):
342
+ path = self._apple_string(relative_path)
343
+ self.targeted(f'''
344
+ keystroke "p" using command down
345
+ delay 0.25
346
+ set savedClipboard to missing value
347
+ try
348
+ set savedClipboard to the clipboard as record
349
+ end try
350
+ set the clipboard to "{path}"
351
+ keystroke "v" using command down
352
+ delay 0.2
353
+ key code 36
354
+ if savedClipboard is not missing value then set the clipboard to savedClipboard
355
+ ''')
356
+
357
+ def hotkey(self, key_code, modifiers='command down'):
358
+ self.targeted(f'key code {key_code} using {modifiers}')
359
+
360
+ def type_text(self, text, character_delay=0.045):
361
+ safe = self._apple_string(text)
362
+ self.targeted(f'''
363
+ repeat with currentCharacter in characters of "{safe}"
364
+ keystroke currentCharacter
365
+ delay {character_delay}
366
+ end repeat
367
+ ''')
368
+
369
+ def wait_for_files(self, relative_paths, timeout=60):
370
+ deadline = time.monotonic() + timeout
371
+ while time.monotonic() < deadline:
372
+ if all((self.project / path).exists() for path in relative_paths):
373
+ return
374
+ time.sleep(0.2)
375
+ missing = [path for path in relative_paths if not (self.project / path).exists()]
376
+ raise RuntimeError('timed out waiting for: ' + ', '.join(missing))
377
+
378
+ def mark(self, name, **details):
379
+ self.events.append({
380
+ 'name': name,
381
+ 'timestamp_ms': round(time.monotonic() * 1000),
382
+ **details
383
+ })
384
+
385
+ def start_capture(self):
386
+ self.output_video.parent.mkdir(parents=True, exist_ok=True)
387
+ self.capture = subprocess.Popen([
388
+ '/usr/sbin/screencapture', '-v', f'-D{self.display_number}', '-k', str(self.output_video)
389
+ ])
390
+ self.mark('recording_started')
391
+
392
+ def stop_capture(self):
393
+ if self.capture is None:
394
+ return
395
+ if self.capture.poll() is None:
396
+ self.capture.send_signal(signal.SIGINT)
397
+ code = self.capture.wait(timeout=30)
398
+ self.capture = None
399
+ if code != 0 or not self.output_video.is_file():
400
+ raise RuntimeError(f'screen capture failed with exit code {code}')
401
+ self.mark('recording_stopped')
402
+
403
+ def write_events(self, metadata=None):
404
+ payload = {'schema_version': 1, **(metadata or {}), 'events': self.events}
405
+ self.output_events.write_text(json.dumps(payload, indent=2) + '\n')
406
+
407
+ def cleanup(self):
408
+ if self.capture is not None:
409
+ try:
410
+ self.stop_capture()
411
+ except Exception:
412
+ if self.capture is not None:
413
+ self.capture.kill()
414
+ self.capture.wait()
415
+ self.capture = None
416
+ if self.window_slug:
417
+ try:
418
+ self.targeted('keystroke "w" using {command down, shift down}')
419
+ except Exception:
420
+ pass
421
+ if self.project:
422
+ time.sleep(0.8)
423
+ shutil.rmtree(self.project, ignore_errors=True)
424
+ time.sleep(0.4)
425
+ shutil.rmtree(self.project, ignore_errors=True)
426
+ if self.temp_root:
427
+ shutil.rmtree(self.temp_root, ignore_errors=True)