gemkit-cli 0.2.3 → 0.3.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 (160) hide show
  1. package/README.md +141 -7
  2. package/dist/commands/agent/index.d.ts +9 -0
  3. package/dist/commands/agent/index.js +1329 -0
  4. package/dist/commands/cache/index.d.ts +5 -0
  5. package/dist/commands/cache/index.js +43 -0
  6. package/dist/commands/catalog/index.d.ts +2 -0
  7. package/dist/commands/catalog/index.js +57 -0
  8. package/dist/commands/config/index.d.ts +7 -0
  9. package/dist/commands/config/index.js +122 -0
  10. package/dist/commands/convert/index.d.ts +8 -0
  11. package/dist/commands/convert/index.js +391 -0
  12. package/dist/commands/doctor/index.d.ts +2 -0
  13. package/dist/commands/doctor/index.js +243 -0
  14. package/dist/commands/extension/index.d.ts +5 -0
  15. package/dist/commands/extension/index.js +52 -0
  16. package/dist/commands/index.d.ts +5 -0
  17. package/dist/commands/index.js +37 -0
  18. package/dist/commands/init/index.d.ts +6 -0
  19. package/dist/commands/init/index.js +345 -0
  20. package/dist/commands/new/index.d.ts +5 -0
  21. package/dist/commands/new/index.js +49 -0
  22. package/dist/commands/office/index.d.ts +5 -0
  23. package/dist/commands/office/index.js +283 -0
  24. package/dist/commands/paste/index.d.ts +10 -0
  25. package/dist/commands/paste/index.js +533 -0
  26. package/dist/commands/plan/index.d.ts +8 -0
  27. package/dist/commands/plan/index.js +247 -0
  28. package/dist/commands/session/index.d.ts +8 -0
  29. package/dist/commands/session/index.js +289 -0
  30. package/dist/commands/tokens/index.d.ts +6 -0
  31. package/dist/commands/tokens/index.js +148 -0
  32. package/dist/commands/update/index.d.ts +26 -0
  33. package/dist/commands/update/index.js +199 -0
  34. package/dist/commands/versions/index.d.ts +5 -0
  35. package/dist/commands/versions/index.js +39 -0
  36. package/dist/domains/agent/index.d.ts +8 -0
  37. package/dist/domains/agent/index.js +8 -0
  38. package/dist/domains/agent/mappings.d.ts +32 -0
  39. package/dist/domains/agent/mappings.js +164 -0
  40. package/dist/domains/agent/profile.d.ts +26 -0
  41. package/dist/domains/agent/profile.js +225 -0
  42. package/dist/domains/agent/pty-context.d.ts +11 -0
  43. package/dist/domains/agent/pty-context.js +83 -0
  44. package/dist/domains/agent/pty-providers.d.ts +18 -0
  45. package/dist/domains/agent/pty-providers.js +66 -0
  46. package/dist/domains/agent/pty-session.d.ts +33 -0
  47. package/dist/domains/agent/pty-session.js +82 -0
  48. package/dist/domains/agent/pty-types.d.ts +127 -0
  49. package/dist/domains/agent/pty-types.js +4 -0
  50. package/dist/domains/agent/search.d.ts +45 -0
  51. package/dist/domains/agent/search.js +614 -0
  52. package/dist/domains/agent/types.d.ts +78 -0
  53. package/dist/domains/agent/types.js +5 -0
  54. package/dist/domains/agent-office/documents-scanner.d.ts +9 -0
  55. package/dist/domains/agent-office/documents-scanner.js +143 -0
  56. package/dist/domains/agent-office/event-emitter.d.ts +43 -0
  57. package/dist/domains/agent-office/event-emitter.js +86 -0
  58. package/dist/domains/agent-office/file-watcher.d.ts +40 -0
  59. package/dist/domains/agent-office/file-watcher.js +173 -0
  60. package/dist/domains/agent-office/icons.d.ts +11 -0
  61. package/dist/domains/agent-office/icons.js +36 -0
  62. package/dist/domains/agent-office/index.d.ts +12 -0
  63. package/dist/domains/agent-office/index.js +20 -0
  64. package/dist/domains/agent-office/renderer/web/assets.d.ts +11 -0
  65. package/dist/domains/agent-office/renderer/web/assets.js +3419 -0
  66. package/dist/domains/agent-office/renderer/web/server.d.ts +42 -0
  67. package/dist/domains/agent-office/renderer/web/server.js +228 -0
  68. package/dist/domains/agent-office/renderer/web.d.ts +30 -0
  69. package/dist/domains/agent-office/renderer/web.js +111 -0
  70. package/dist/domains/agent-office/session-bridge.d.ts +23 -0
  71. package/dist/domains/agent-office/session-bridge.js +171 -0
  72. package/dist/domains/agent-office/state-machine.d.ts +5 -0
  73. package/dist/domains/agent-office/state-machine.js +82 -0
  74. package/dist/domains/agent-office/types.d.ts +91 -0
  75. package/dist/domains/agent-office/types.js +4 -0
  76. package/dist/domains/cache/index.d.ts +1 -0
  77. package/dist/domains/cache/index.js +1 -0
  78. package/dist/domains/cache/manager.d.ts +22 -0
  79. package/dist/domains/cache/manager.js +84 -0
  80. package/dist/domains/config/index.d.ts +5 -0
  81. package/dist/domains/config/index.js +5 -0
  82. package/dist/domains/config/manager.d.ts +24 -0
  83. package/dist/domains/config/manager.js +85 -0
  84. package/dist/domains/config/schema.d.ts +17 -0
  85. package/dist/domains/config/schema.js +96 -0
  86. package/dist/domains/convert/converter.d.ts +78 -0
  87. package/dist/domains/convert/converter.js +471 -0
  88. package/dist/domains/convert/index.d.ts +5 -0
  89. package/dist/domains/convert/index.js +5 -0
  90. package/dist/domains/convert/types.d.ts +88 -0
  91. package/dist/domains/convert/types.js +18 -0
  92. package/dist/domains/github/download.d.ts +12 -0
  93. package/dist/domains/github/download.js +51 -0
  94. package/dist/domains/github/index.d.ts +2 -0
  95. package/dist/domains/github/index.js +2 -0
  96. package/dist/domains/github/releases.d.ts +16 -0
  97. package/dist/domains/github/releases.js +68 -0
  98. package/dist/domains/installation/conflict.d.ts +13 -0
  99. package/dist/domains/installation/conflict.js +38 -0
  100. package/dist/domains/installation/file-sync.d.ts +16 -0
  101. package/dist/domains/installation/file-sync.js +77 -0
  102. package/dist/domains/installation/index.d.ts +3 -0
  103. package/dist/domains/installation/index.js +3 -0
  104. package/dist/domains/installation/metadata.d.ts +20 -0
  105. package/dist/domains/installation/metadata.js +52 -0
  106. package/dist/domains/plan/index.d.ts +2 -0
  107. package/dist/domains/plan/index.js +2 -0
  108. package/dist/domains/plan/resolver.d.ts +24 -0
  109. package/dist/domains/plan/resolver.js +164 -0
  110. package/dist/domains/plan/types.d.ts +13 -0
  111. package/dist/domains/plan/types.js +4 -0
  112. package/dist/domains/session/env.d.ts +51 -0
  113. package/dist/domains/session/env.js +118 -0
  114. package/dist/domains/session/index.d.ts +8 -0
  115. package/dist/domains/session/index.js +8 -0
  116. package/dist/domains/session/manager.d.ts +56 -0
  117. package/dist/domains/session/manager.js +205 -0
  118. package/dist/domains/session/paths.d.ts +6 -0
  119. package/dist/domains/session/paths.js +6 -0
  120. package/dist/domains/session/types.d.ts +121 -0
  121. package/dist/domains/session/types.js +5 -0
  122. package/dist/domains/session/writer.d.ts +82 -0
  123. package/dist/domains/session/writer.js +431 -0
  124. package/dist/domains/tokens/index.d.ts +5 -0
  125. package/dist/domains/tokens/index.js +5 -0
  126. package/dist/domains/tokens/pricing.d.ts +38 -0
  127. package/dist/domains/tokens/pricing.js +129 -0
  128. package/dist/domains/tokens/scanner.d.ts +42 -0
  129. package/dist/domains/tokens/scanner.js +168 -0
  130. package/dist/index.d.ts +5 -0
  131. package/dist/index.js +87 -58
  132. package/dist/services/aipty.d.ts +76 -0
  133. package/dist/services/aipty.js +276 -0
  134. package/dist/services/archive.d.ts +22 -0
  135. package/dist/services/archive.js +53 -0
  136. package/dist/services/auto-update.d.ts +26 -0
  137. package/dist/services/auto-update.js +117 -0
  138. package/dist/services/hash.d.ts +36 -0
  139. package/dist/services/hash.js +63 -0
  140. package/dist/services/logger.d.ts +28 -0
  141. package/dist/services/logger.js +102 -0
  142. package/dist/services/music.d.ts +67 -0
  143. package/dist/services/music.js +290 -0
  144. package/dist/services/npm.d.ts +22 -0
  145. package/dist/services/npm.js +65 -0
  146. package/dist/services/pty-client.d.ts +66 -0
  147. package/dist/services/pty-client.js +154 -0
  148. package/dist/services/pty-server.d.ts +102 -0
  149. package/dist/services/pty-server.js +613 -0
  150. package/dist/types/index.d.ts +155 -0
  151. package/dist/types/index.js +4 -0
  152. package/dist/utils/colors.d.ts +43 -0
  153. package/dist/utils/colors.js +98 -0
  154. package/dist/utils/errors.d.ts +24 -0
  155. package/dist/utils/errors.js +56 -0
  156. package/dist/utils/paths.d.ts +46 -0
  157. package/dist/utils/paths.js +89 -0
  158. package/dist/utils/platform.d.ts +11 -0
  159. package/dist/utils/platform.js +31 -0
  160. package/package.json +55 -54
@@ -0,0 +1,10 @@
1
+ /**
2
+ * gk paste - Capture clipboard image or video for Gemini analysis
3
+ *
4
+ * Usage:
5
+ * gk paste - Capture image (default)
6
+ * gk paste --image - Capture image explicitly
7
+ * gk paste --video - Capture video from clipboard or recent recordings
8
+ */
9
+ import type { CAC } from 'cac';
10
+ export declare function registerPasteCommand(cli: CAC): void;
@@ -0,0 +1,533 @@
1
+ /**
2
+ * gk paste - Capture clipboard image or video for Gemini analysis
3
+ *
4
+ * Usage:
5
+ * gk paste - Capture image (default)
6
+ * gk paste --image - Capture image explicitly
7
+ * gk paste --video - Capture video from clipboard or recent recordings
8
+ */
9
+ import { spawnSync } from 'child_process';
10
+ import { existsSync, mkdirSync, writeFileSync, statSync, copyFileSync, readdirSync } from 'fs';
11
+ import { join, extname } from 'path';
12
+ import { homedir } from 'os';
13
+ import { getGeminiProjectHash } from '../../domains/session/env.js';
14
+ import { brand } from '../../utils/colors.js';
15
+ // ============================================================================
16
+ // Constants
17
+ // ============================================================================
18
+ const IMAGE_PREFIX = 'clipboard-image';
19
+ const VIDEO_PREFIX = 'clipboard-video';
20
+ const SUPPORTED_VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.gif'];
21
+ const RECENT_FILE_THRESHOLD_MINUTES = 5;
22
+ // ============================================================================
23
+ // Command Registration
24
+ // ============================================================================
25
+ export function registerPasteCommand(cli) {
26
+ cli
27
+ .command('paste', 'Capture clipboard image or video for Gemini analysis')
28
+ .alias('p')
29
+ .option('--image', 'Capture image from clipboard (default)')
30
+ .option('--video', 'Capture video from clipboard or recent recordings')
31
+ .option('--format <fmt>', 'Image format (png, jpg)', { default: 'png' })
32
+ .option('--json', 'Output as JSON')
33
+ .action(async (options) => {
34
+ // Default to image if neither specified
35
+ const captureVideo = options.video === true;
36
+ const result = captureVideo ? pasteVideo() : pasteImage(options);
37
+ const output = captureVideo ? formatVideoOutput(result) : formatImageOutput(result);
38
+ if (options.json) {
39
+ console.log(JSON.stringify(output, null, 2));
40
+ }
41
+ else {
42
+ console.log();
43
+ if (output.success) {
44
+ const type = captureVideo ? 'Video' : 'Image';
45
+ console.log(`${brand.success('✓')} ${type} captured successfully`);
46
+ console.log();
47
+ console.log(` ${brand.dim('Path:')} ${brand.primary(String(output.path))}`);
48
+ console.log(` ${brand.dim('Size:')} ${output.size}`);
49
+ if (captureVideo && output.format) {
50
+ console.log(` ${brand.dim('Format:')} ${output.format}`);
51
+ }
52
+ if (output.sourceType && output.sourceType !== 'clipboard') {
53
+ console.log(` ${brand.dim('Source:')} ${output.sourceType}`);
54
+ }
55
+ console.log(` ${brand.dim('Platform:')} ${output.platform}`);
56
+ }
57
+ else {
58
+ console.error(`${brand.error('✗')} ${output.error}`);
59
+ if (output.suggestion) {
60
+ console.log(` ${brand.dim('Hint:')} ${output.suggestion}`);
61
+ }
62
+ }
63
+ console.log();
64
+ }
65
+ process.exit(result.success ? 0 : 1);
66
+ });
67
+ }
68
+ // ============================================================================
69
+ // Shared Utilities
70
+ // ============================================================================
71
+ function getTempDir() {
72
+ const projectHash = getGeminiProjectHash();
73
+ if (projectHash) {
74
+ const tempDir = join(homedir(), '.gemini', 'tmp', projectHash);
75
+ if (!existsSync(tempDir)) {
76
+ mkdirSync(tempDir, { recursive: true });
77
+ }
78
+ return tempDir;
79
+ }
80
+ const fallbackDir = join(process.cwd(), '.gemini', 'tmp', 'clipboard');
81
+ if (!existsSync(fallbackDir)) {
82
+ mkdirSync(fallbackDir, { recursive: true });
83
+ }
84
+ return fallbackDir;
85
+ }
86
+ function generateImageFilename(format = 'png') {
87
+ const timestamp = Date.now();
88
+ const random = Math.random().toString(36).substring(2, 8);
89
+ return `${IMAGE_PREFIX}-${timestamp}-${random}.${format}`;
90
+ }
91
+ function generateVideoFilename(originalExt = '.mp4') {
92
+ const timestamp = Date.now();
93
+ const random = Math.random().toString(36).substring(2, 8);
94
+ const ext = SUPPORTED_VIDEO_EXTENSIONS.includes(originalExt.toLowerCase()) ? originalExt : '.mp4';
95
+ return `${VIDEO_PREFIX}-${timestamp}-${random}${ext}`;
96
+ }
97
+ function isVideoFile(filepath) {
98
+ const ext = extname(filepath).toLowerCase();
99
+ return SUPPORTED_VIDEO_EXTENSIONS.includes(ext);
100
+ }
101
+ function findRecentVideoInFolders(folders, destDir) {
102
+ for (const folder of folders) {
103
+ if (!existsSync(folder))
104
+ continue;
105
+ try {
106
+ const files = readdirSync(folder)
107
+ .filter((f) => isVideoFile(f))
108
+ .map((f) => {
109
+ const fullPath = join(folder, f);
110
+ return {
111
+ name: f,
112
+ path: fullPath,
113
+ mtime: statSync(fullPath).mtime
114
+ };
115
+ })
116
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
117
+ if (files.length > 0) {
118
+ const recent = files[0];
119
+ const ageMinutes = (Date.now() - recent.mtime.getTime()) / 60000;
120
+ if (ageMinutes <= RECENT_FILE_THRESHOLD_MINUTES) {
121
+ const ext = extname(recent.path);
122
+ const destFilename = generateVideoFilename(ext);
123
+ const destPath = join(destDir, destFilename);
124
+ copyFileSync(recent.path, destPath);
125
+ if (existsSync(destPath)) {
126
+ return {
127
+ success: true,
128
+ path: destPath,
129
+ sourcePath: recent.path,
130
+ sourceType: 'recent-recording'
131
+ };
132
+ }
133
+ }
134
+ }
135
+ }
136
+ catch {
137
+ // Continue to next folder
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ // ============================================================================
143
+ // Image Capture Functions
144
+ // ============================================================================
145
+ function pasteImage(options) {
146
+ const format = options.format || 'png';
147
+ const platform = process.platform;
148
+ const tempDir = getTempDir();
149
+ const filename = generateImageFilename(format);
150
+ const filepath = join(tempDir, filename);
151
+ let result;
152
+ switch (platform) {
153
+ case 'win32':
154
+ result = captureImageWindows(filepath);
155
+ break;
156
+ case 'darwin':
157
+ result = captureImageMacOS(filepath);
158
+ break;
159
+ case 'linux':
160
+ result = captureImageLinux(filepath);
161
+ break;
162
+ default:
163
+ result = { success: false, error: `Unsupported platform: ${platform}` };
164
+ }
165
+ result.platform = platform;
166
+ result.tempDir = tempDir;
167
+ return result;
168
+ }
169
+ function captureImageWindows(filepath) {
170
+ const escapedPath = filepath.replace(/\\/g, '\\\\');
171
+ const psScript = `
172
+ Add-Type -AssemblyName System.Windows.Forms
173
+ Add-Type -AssemblyName System.Drawing
174
+ $clipboard = [System.Windows.Forms.Clipboard]::GetImage()
175
+ if ($clipboard -eq $null) { Write-Error "NO_IMAGE"; exit 1 }
176
+ $dir = Split-Path -Parent "${escapedPath}"
177
+ if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
178
+ $clipboard.Save("${escapedPath}", [System.Drawing.Imaging.ImageFormat]::Png)
179
+ $clipboard.Dispose()
180
+ Write-Output "SUCCESS"
181
+ `.trim();
182
+ const result = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', psScript], {
183
+ encoding: 'utf-8',
184
+ windowsHide: true
185
+ });
186
+ if (result.stderr?.includes('NO_IMAGE')) {
187
+ return { success: false, error: 'No image in clipboard. Copy an image first.' };
188
+ }
189
+ if (result.status !== 0) {
190
+ return { success: false, error: `Failed: ${result.stderr || result.stdout}`.trim() };
191
+ }
192
+ return existsSync(filepath) ? { success: true, path: filepath } : { success: false, error: 'File not created' };
193
+ }
194
+ function captureImageMacOS(filepath) {
195
+ const which = spawnSync('which', ['pngpaste'], { encoding: 'utf-8' });
196
+ if (which.status === 0) {
197
+ const paste = spawnSync('pngpaste', [filepath], { encoding: 'utf-8' });
198
+ if (paste.status === 0 && existsSync(filepath)) {
199
+ return { success: true, path: filepath };
200
+ }
201
+ }
202
+ return { success: false, error: 'No image or pngpaste failed', suggestion: 'brew install pngpaste' };
203
+ }
204
+ function captureImageLinux(filepath) {
205
+ const which = spawnSync('which', ['xclip'], { encoding: 'utf-8' });
206
+ if (which.status !== 0) {
207
+ return { success: false, error: 'xclip not installed', suggestion: 'apt install xclip' };
208
+ }
209
+ const targets = spawnSync('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], { encoding: 'utf-8' });
210
+ if (!targets.stdout?.includes('image/png') && !targets.stdout?.includes('image/jpeg')) {
211
+ return { success: false, error: 'No image in clipboard' };
212
+ }
213
+ const type = targets.stdout.includes('image/png') ? 'image/png' : 'image/jpeg';
214
+ const result = spawnSync('xclip', ['-selection', 'clipboard', '-t', type, '-o'], { encoding: 'buffer' });
215
+ if (result.status !== 0 || !result.stdout?.length) {
216
+ return { success: false, error: 'Failed to read clipboard' };
217
+ }
218
+ writeFileSync(filepath, result.stdout);
219
+ return existsSync(filepath) ? { success: true, path: filepath } : { success: false, error: 'Save failed' };
220
+ }
221
+ function formatImageOutput(result) {
222
+ if (result.success && result.path) {
223
+ const stats = statSync(result.path);
224
+ return {
225
+ success: true,
226
+ message: 'Image captured',
227
+ path: result.path,
228
+ size: `${(stats.size / 1024).toFixed(1)} KB`,
229
+ platform: result.platform
230
+ };
231
+ }
232
+ return { success: false, error: result.error, suggestion: result.suggestion, platform: result.platform };
233
+ }
234
+ // ============================================================================
235
+ // Video Capture Functions
236
+ // ============================================================================
237
+ function pasteVideo() {
238
+ const platform = process.platform;
239
+ const tempDir = getTempDir();
240
+ let result;
241
+ switch (platform) {
242
+ case 'win32':
243
+ result = captureVideoWindows(tempDir);
244
+ break;
245
+ case 'darwin':
246
+ result = captureVideoMacOS(tempDir);
247
+ break;
248
+ case 'linux':
249
+ result = captureVideoLinux(tempDir);
250
+ break;
251
+ default:
252
+ result = {
253
+ success: false,
254
+ error: `Unsupported platform: ${platform}`,
255
+ suggestion: 'This tool supports Windows, macOS, and Linux only'
256
+ };
257
+ }
258
+ result.platform = platform;
259
+ result.tempDir = tempDir;
260
+ return result;
261
+ }
262
+ function captureVideoWindows(destDir) {
263
+ const psScript = `
264
+ Add-Type -AssemblyName System.Windows.Forms
265
+
266
+ # Try to get file drop list from clipboard (copied files)
267
+ $files = [System.Windows.Forms.Clipboard]::GetFileDropList()
268
+
269
+ if ($files -and $files.Count -gt 0) {
270
+ # Return first video file found in clipboard
271
+ foreach ($file in $files) {
272
+ $ext = [System.IO.Path]::GetExtension($file).ToLower()
273
+ if ($ext -in @('.mp4', '.webm', '.mov', '.avi', '.mkv', '.gif')) {
274
+ Write-Output "FILE:$file"
275
+ exit 0
276
+ }
277
+ }
278
+ }
279
+
280
+ # Fallback: Check for recent Snipping Tool videos
281
+ $snipFolder = [System.IO.Path]::Combine($env:USERPROFILE, 'Videos', 'Screen Recordings')
282
+ if (Test-Path $snipFolder) {
283
+ $recentVideo = Get-ChildItem -Path $snipFolder -Filter "*.mp4" |
284
+ Sort-Object LastWriteTime -Descending |
285
+ Select-Object -First 1
286
+
287
+ if ($recentVideo) {
288
+ $age = (Get-Date) - $recentVideo.LastWriteTime
289
+ if ($age.TotalMinutes -le 5) {
290
+ Write-Output "SNIP:$($recentVideo.FullName)"
291
+ exit 0
292
+ }
293
+ }
294
+ }
295
+
296
+ # Also check Screenshots folder (some versions save there)
297
+ $screenshotFolder = [System.IO.Path]::Combine($env:USERPROFILE, 'Pictures', 'Screenshots')
298
+ if (Test-Path $screenshotFolder) {
299
+ $recentVideo = Get-ChildItem -Path $screenshotFolder -Filter "*.mp4" |
300
+ Sort-Object LastWriteTime -Descending |
301
+ Select-Object -First 1
302
+
303
+ if ($recentVideo) {
304
+ $age = (Get-Date) - $recentVideo.LastWriteTime
305
+ if ($age.TotalMinutes -le 5) {
306
+ Write-Output "SNIP:$($recentVideo.FullName)"
307
+ exit 0
308
+ }
309
+ }
310
+ }
311
+
312
+ # Also check Videos folder root
313
+ $videosFolder = [System.IO.Path]::Combine($env:USERPROFILE, 'Videos')
314
+ if (Test-Path $videosFolder) {
315
+ $recentVideo = Get-ChildItem -Path $videosFolder -Filter "*.mp4" -File |
316
+ Sort-Object LastWriteTime -Descending |
317
+ Select-Object -First 1
318
+
319
+ if ($recentVideo) {
320
+ $age = (Get-Date) - $recentVideo.LastWriteTime
321
+ if ($age.TotalMinutes -le 5) {
322
+ Write-Output "SNIP:$($recentVideo.FullName)"
323
+ exit 0
324
+ }
325
+ }
326
+ }
327
+
328
+ Write-Error "NO_VIDEO_FOUND"
329
+ exit 1
330
+ `.trim();
331
+ try {
332
+ const result = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', psScript], {
333
+ encoding: 'utf-8',
334
+ windowsHide: true,
335
+ timeout: 10000
336
+ });
337
+ if (result.error) {
338
+ return { success: false, error: `PowerShell error: ${result.error.message}` };
339
+ }
340
+ if (result.stderr?.includes('NO_VIDEO_FOUND')) {
341
+ return {
342
+ success: false,
343
+ error: 'No video found in clipboard or recent recordings.',
344
+ suggestion: 'Copy a video file or record with Snipping Tool (Win+Shift+R) first.'
345
+ };
346
+ }
347
+ if (result.status !== 0) {
348
+ const errorMsg = result.stderr || result.stdout || 'Unknown PowerShell error';
349
+ return { success: false, error: `Failed to capture: ${errorMsg.trim()}` };
350
+ }
351
+ const output = result.stdout.trim();
352
+ let sourcePath = null;
353
+ let sourceType = 'clipboard';
354
+ if (output.startsWith('FILE:')) {
355
+ sourcePath = output.substring(5);
356
+ sourceType = 'clipboard';
357
+ }
358
+ else if (output.startsWith('SNIP:')) {
359
+ sourcePath = output.substring(5);
360
+ sourceType = 'snipping-tool';
361
+ }
362
+ if (!sourcePath || !existsSync(sourcePath)) {
363
+ return { success: false, error: 'Video file not found or inaccessible' };
364
+ }
365
+ // Copy video to temp directory
366
+ const ext = extname(sourcePath);
367
+ const destFilename = generateVideoFilename(ext);
368
+ const destPath = join(destDir, destFilename);
369
+ copyFileSync(sourcePath, destPath);
370
+ if (existsSync(destPath)) {
371
+ return {
372
+ success: true,
373
+ path: destPath,
374
+ sourcePath: sourcePath,
375
+ sourceType: sourceType
376
+ };
377
+ }
378
+ return { success: false, error: 'Failed to copy video file' };
379
+ }
380
+ catch (err) {
381
+ const message = err instanceof Error ? err.message : String(err);
382
+ return { success: false, error: `Windows video capture error: ${message}` };
383
+ }
384
+ }
385
+ function captureVideoMacOS(destDir) {
386
+ const appleScript = `
387
+ use scripting additions
388
+
389
+ set thePath to ""
390
+
391
+ try
392
+ set theFiles to the clipboard as «class furl»
393
+ set thePath to POSIX path of theFiles
394
+ on error
395
+ try
396
+ set theFiles to the clipboard as list
397
+ if (count of theFiles) > 0 then
398
+ set thePath to POSIX path of (item 1 of theFiles)
399
+ end if
400
+ end try
401
+ end try
402
+
403
+ if thePath is not "" then
404
+ return thePath
405
+ else
406
+ return "NO_FILE"
407
+ end if
408
+ `.trim();
409
+ try {
410
+ const result = spawnSync('osascript', ['-e', appleScript], {
411
+ encoding: 'utf-8',
412
+ timeout: 5000
413
+ });
414
+ if (result.stdout && result.stdout.trim() !== 'NO_FILE') {
415
+ const sourcePath = result.stdout.trim();
416
+ if (!isVideoFile(sourcePath)) {
417
+ return {
418
+ success: false,
419
+ error: 'Clipboard does not contain a video file',
420
+ suggestion: 'Copy a video file (.mp4, .mov, .webm) to clipboard first'
421
+ };
422
+ }
423
+ if (!existsSync(sourcePath)) {
424
+ return { success: false, error: 'Video file not found' };
425
+ }
426
+ // Copy video to temp directory
427
+ const ext = extname(sourcePath);
428
+ const destFilename = generateVideoFilename(ext);
429
+ const destPath = join(destDir, destFilename);
430
+ copyFileSync(sourcePath, destPath);
431
+ if (existsSync(destPath)) {
432
+ return {
433
+ success: true,
434
+ path: destPath,
435
+ sourcePath: sourcePath,
436
+ sourceType: 'clipboard'
437
+ };
438
+ }
439
+ }
440
+ // Fallback: Check for recent screen recordings
441
+ const recentResult = findRecentVideoInFolders([join(homedir(), 'Movies'), join(homedir(), 'Desktop')], destDir);
442
+ if (recentResult) {
443
+ return recentResult;
444
+ }
445
+ return {
446
+ success: false,
447
+ error: 'No video in clipboard or recent recordings',
448
+ suggestion: 'Copy a video file or use Cmd+Shift+5 to record screen'
449
+ };
450
+ }
451
+ catch (err) {
452
+ const message = err instanceof Error ? err.message : String(err);
453
+ return { success: false, error: `macOS video capture error: ${message}` };
454
+ }
455
+ }
456
+ function captureVideoLinux(destDir) {
457
+ try {
458
+ // Check if xclip is available
459
+ const whichResult = spawnSync('which', ['xclip'], { encoding: 'utf-8' });
460
+ if (whichResult.status !== 0) {
461
+ return {
462
+ success: false,
463
+ error: 'xclip is not installed',
464
+ suggestion: 'Install with: sudo apt install xclip'
465
+ };
466
+ }
467
+ // Try to get file path from clipboard
468
+ const result = spawnSync('xclip', ['-selection', 'clipboard', '-t', 'text/uri-list', '-o'], { encoding: 'utf-8', timeout: 5000 });
469
+ if (result.status === 0 && result.stdout) {
470
+ const uri = result.stdout.trim();
471
+ let sourcePath = uri;
472
+ // Convert file:// URI to path
473
+ if (uri.startsWith('file://')) {
474
+ sourcePath = decodeURIComponent(uri.replace('file://', ''));
475
+ }
476
+ if (isVideoFile(sourcePath) && existsSync(sourcePath)) {
477
+ const ext = extname(sourcePath);
478
+ const destFilename = generateVideoFilename(ext);
479
+ const destPath = join(destDir, destFilename);
480
+ copyFileSync(sourcePath, destPath);
481
+ if (existsSync(destPath)) {
482
+ return {
483
+ success: true,
484
+ path: destPath,
485
+ sourcePath: sourcePath,
486
+ sourceType: 'clipboard'
487
+ };
488
+ }
489
+ }
490
+ }
491
+ // Fallback: Check common video folders for recent recordings
492
+ const recentResult = findRecentVideoInFolders([
493
+ join(homedir(), 'Videos'),
494
+ join(homedir(), 'Screencasts'),
495
+ join(homedir(), 'Desktop')
496
+ ], destDir);
497
+ if (recentResult) {
498
+ return recentResult;
499
+ }
500
+ return {
501
+ success: false,
502
+ error: 'No video in clipboard or recent recordings',
503
+ suggestion: 'Copy a video file or use a screen recorder first'
504
+ };
505
+ }
506
+ catch (err) {
507
+ const message = err instanceof Error ? err.message : String(err);
508
+ return { success: false, error: `Linux video capture error: ${message}` };
509
+ }
510
+ }
511
+ function formatVideoOutput(result) {
512
+ if (result.success && result.path) {
513
+ const stats = statSync(result.path);
514
+ const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
515
+ const ext = extname(result.path).toLowerCase();
516
+ return {
517
+ success: true,
518
+ message: `Video captured from ${result.sourceType}`,
519
+ path: result.path,
520
+ sourcePath: result.sourcePath,
521
+ sourceType: result.sourceType,
522
+ size: `${sizeMB} MB`,
523
+ format: ext.substring(1).toUpperCase(),
524
+ platform: result.platform
525
+ };
526
+ }
527
+ return {
528
+ success: false,
529
+ error: result.error,
530
+ suggestion: result.suggestion || 'Record a video with Snipping Tool (Win+Shift+R) or copy a video file to clipboard',
531
+ platform: result.platform
532
+ };
533
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Plan command - list, status, create, set, info
3
+ * Replaces: gk-set-active-plan.cjs (set), get_plan_info.js (info)
4
+ *
5
+ * Subcommands organized with custom help display.
6
+ */
7
+ import type { CAC } from 'cac';
8
+ export declare function registerPlanCommand(cli: CAC): void;