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.
- package/README.md +141 -7
- package/dist/commands/agent/index.d.ts +9 -0
- package/dist/commands/agent/index.js +1329 -0
- package/dist/commands/cache/index.d.ts +5 -0
- package/dist/commands/cache/index.js +43 -0
- package/dist/commands/catalog/index.d.ts +2 -0
- package/dist/commands/catalog/index.js +57 -0
- package/dist/commands/config/index.d.ts +7 -0
- package/dist/commands/config/index.js +122 -0
- package/dist/commands/convert/index.d.ts +8 -0
- package/dist/commands/convert/index.js +391 -0
- package/dist/commands/doctor/index.d.ts +2 -0
- package/dist/commands/doctor/index.js +243 -0
- package/dist/commands/extension/index.d.ts +5 -0
- package/dist/commands/extension/index.js +52 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.js +37 -0
- package/dist/commands/init/index.d.ts +6 -0
- package/dist/commands/init/index.js +345 -0
- package/dist/commands/new/index.d.ts +5 -0
- package/dist/commands/new/index.js +49 -0
- package/dist/commands/office/index.d.ts +5 -0
- package/dist/commands/office/index.js +283 -0
- package/dist/commands/paste/index.d.ts +10 -0
- package/dist/commands/paste/index.js +533 -0
- package/dist/commands/plan/index.d.ts +8 -0
- package/dist/commands/plan/index.js +247 -0
- package/dist/commands/session/index.d.ts +8 -0
- package/dist/commands/session/index.js +289 -0
- package/dist/commands/tokens/index.d.ts +6 -0
- package/dist/commands/tokens/index.js +148 -0
- package/dist/commands/update/index.d.ts +26 -0
- package/dist/commands/update/index.js +199 -0
- package/dist/commands/versions/index.d.ts +5 -0
- package/dist/commands/versions/index.js +39 -0
- package/dist/domains/agent/index.d.ts +8 -0
- package/dist/domains/agent/index.js +8 -0
- package/dist/domains/agent/mappings.d.ts +32 -0
- package/dist/domains/agent/mappings.js +164 -0
- package/dist/domains/agent/profile.d.ts +26 -0
- package/dist/domains/agent/profile.js +225 -0
- package/dist/domains/agent/pty-context.d.ts +11 -0
- package/dist/domains/agent/pty-context.js +83 -0
- package/dist/domains/agent/pty-providers.d.ts +18 -0
- package/dist/domains/agent/pty-providers.js +66 -0
- package/dist/domains/agent/pty-session.d.ts +33 -0
- package/dist/domains/agent/pty-session.js +82 -0
- package/dist/domains/agent/pty-types.d.ts +127 -0
- package/dist/domains/agent/pty-types.js +4 -0
- package/dist/domains/agent/search.d.ts +45 -0
- package/dist/domains/agent/search.js +614 -0
- package/dist/domains/agent/types.d.ts +78 -0
- package/dist/domains/agent/types.js +5 -0
- package/dist/domains/agent-office/documents-scanner.d.ts +9 -0
- package/dist/domains/agent-office/documents-scanner.js +143 -0
- package/dist/domains/agent-office/event-emitter.d.ts +43 -0
- package/dist/domains/agent-office/event-emitter.js +86 -0
- package/dist/domains/agent-office/file-watcher.d.ts +40 -0
- package/dist/domains/agent-office/file-watcher.js +173 -0
- package/dist/domains/agent-office/icons.d.ts +11 -0
- package/dist/domains/agent-office/icons.js +36 -0
- package/dist/domains/agent-office/index.d.ts +12 -0
- package/dist/domains/agent-office/index.js +20 -0
- package/dist/domains/agent-office/renderer/web/assets.d.ts +11 -0
- package/dist/domains/agent-office/renderer/web/assets.js +3419 -0
- package/dist/domains/agent-office/renderer/web/server.d.ts +42 -0
- package/dist/domains/agent-office/renderer/web/server.js +228 -0
- package/dist/domains/agent-office/renderer/web.d.ts +30 -0
- package/dist/domains/agent-office/renderer/web.js +111 -0
- package/dist/domains/agent-office/session-bridge.d.ts +23 -0
- package/dist/domains/agent-office/session-bridge.js +171 -0
- package/dist/domains/agent-office/state-machine.d.ts +5 -0
- package/dist/domains/agent-office/state-machine.js +82 -0
- package/dist/domains/agent-office/types.d.ts +91 -0
- package/dist/domains/agent-office/types.js +4 -0
- package/dist/domains/cache/index.d.ts +1 -0
- package/dist/domains/cache/index.js +1 -0
- package/dist/domains/cache/manager.d.ts +22 -0
- package/dist/domains/cache/manager.js +84 -0
- package/dist/domains/config/index.d.ts +5 -0
- package/dist/domains/config/index.js +5 -0
- package/dist/domains/config/manager.d.ts +24 -0
- package/dist/domains/config/manager.js +85 -0
- package/dist/domains/config/schema.d.ts +17 -0
- package/dist/domains/config/schema.js +96 -0
- package/dist/domains/convert/converter.d.ts +78 -0
- package/dist/domains/convert/converter.js +471 -0
- package/dist/domains/convert/index.d.ts +5 -0
- package/dist/domains/convert/index.js +5 -0
- package/dist/domains/convert/types.d.ts +88 -0
- package/dist/domains/convert/types.js +18 -0
- package/dist/domains/github/download.d.ts +12 -0
- package/dist/domains/github/download.js +51 -0
- package/dist/domains/github/index.d.ts +2 -0
- package/dist/domains/github/index.js +2 -0
- package/dist/domains/github/releases.d.ts +16 -0
- package/dist/domains/github/releases.js +68 -0
- package/dist/domains/installation/conflict.d.ts +13 -0
- package/dist/domains/installation/conflict.js +38 -0
- package/dist/domains/installation/file-sync.d.ts +16 -0
- package/dist/domains/installation/file-sync.js +77 -0
- package/dist/domains/installation/index.d.ts +3 -0
- package/dist/domains/installation/index.js +3 -0
- package/dist/domains/installation/metadata.d.ts +20 -0
- package/dist/domains/installation/metadata.js +52 -0
- package/dist/domains/plan/index.d.ts +2 -0
- package/dist/domains/plan/index.js +2 -0
- package/dist/domains/plan/resolver.d.ts +24 -0
- package/dist/domains/plan/resolver.js +164 -0
- package/dist/domains/plan/types.d.ts +13 -0
- package/dist/domains/plan/types.js +4 -0
- package/dist/domains/session/env.d.ts +51 -0
- package/dist/domains/session/env.js +118 -0
- package/dist/domains/session/index.d.ts +8 -0
- package/dist/domains/session/index.js +8 -0
- package/dist/domains/session/manager.d.ts +56 -0
- package/dist/domains/session/manager.js +205 -0
- package/dist/domains/session/paths.d.ts +6 -0
- package/dist/domains/session/paths.js +6 -0
- package/dist/domains/session/types.d.ts +121 -0
- package/dist/domains/session/types.js +5 -0
- package/dist/domains/session/writer.d.ts +82 -0
- package/dist/domains/session/writer.js +431 -0
- package/dist/domains/tokens/index.d.ts +5 -0
- package/dist/domains/tokens/index.js +5 -0
- package/dist/domains/tokens/pricing.d.ts +38 -0
- package/dist/domains/tokens/pricing.js +129 -0
- package/dist/domains/tokens/scanner.d.ts +42 -0
- package/dist/domains/tokens/scanner.js +168 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +87 -58
- package/dist/services/aipty.d.ts +76 -0
- package/dist/services/aipty.js +276 -0
- package/dist/services/archive.d.ts +22 -0
- package/dist/services/archive.js +53 -0
- package/dist/services/auto-update.d.ts +26 -0
- package/dist/services/auto-update.js +117 -0
- package/dist/services/hash.d.ts +36 -0
- package/dist/services/hash.js +63 -0
- package/dist/services/logger.d.ts +28 -0
- package/dist/services/logger.js +102 -0
- package/dist/services/music.d.ts +67 -0
- package/dist/services/music.js +290 -0
- package/dist/services/npm.d.ts +22 -0
- package/dist/services/npm.js +65 -0
- package/dist/services/pty-client.d.ts +66 -0
- package/dist/services/pty-client.js +154 -0
- package/dist/services/pty-server.d.ts +102 -0
- package/dist/services/pty-server.js +613 -0
- package/dist/types/index.d.ts +155 -0
- package/dist/types/index.js +4 -0
- package/dist/utils/colors.d.ts +43 -0
- package/dist/utils/colors.js +98 -0
- package/dist/utils/errors.d.ts +24 -0
- package/dist/utils/errors.js +56 -0
- package/dist/utils/paths.d.ts +46 -0
- package/dist/utils/paths.js +89 -0
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +31 -0
- 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;
|