@jojonax/codex-copilot 1.5.5 → 1.6.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.
@@ -1,279 +1,279 @@
1
- /**
2
- * Desktop IDE Automator — platform-aware process detection + auto-paste
3
- *
4
- * Supports macOS (AppleScript/osascript) and Windows (PowerShell/SendKeys).
5
- * Each IDE has its own keystroke recipe.
6
- * Falls back gracefully if automation fails.
7
- */
8
-
9
- import { execSync } from 'child_process';
10
- import { log } from './logger.js';
11
-
12
- const IS_MAC = process.platform === 'darwin';
13
- const IS_WIN = process.platform === 'win32';
14
-
15
- // ──────────────────────────────────────────────
16
- // Per-IDE Automation Recipes
17
- // ──────────────────────────────────────────────
18
-
19
- /**
20
- * Each recipe defines:
21
- * processName: { mac, win } — binary/process name to detect
22
- * appName: { mac, win } — display name for activation
23
- * preKeys: keystroke sequence BEFORE paste (e.g., open Composer in Cursor)
24
- * preDelay: ms to wait after preKeys
25
- * postKeys: keystroke sequence AFTER paste (usually Enter to send)
26
- * pasteDelay: ms to wait between paste and postKeys
27
- */
28
- const IDE_RECIPES = {
29
- 'codex-desktop': {
30
- processName: { mac: 'Codex', win: 'Codex' },
31
- appName: { mac: 'Codex', win: 'Codex' },
32
- // Codex Desktop: input is focused on activate → paste → Enter
33
- preKeys: null,
34
- preDelay: 0,
35
- postKeys: 'return',
36
- pasteDelay: 300,
37
- },
38
-
39
- 'cursor': {
40
- processName: { mac: 'Cursor', win: 'Cursor' },
41
- appName: { mac: 'Cursor', win: 'Cursor' },
42
- // Cursor: Cmd/Ctrl+I opens Composer, then paste, then Enter
43
- preKeys: IS_MAC
44
- ? 'keystroke "i" using {command down}'
45
- : '^i',
46
- preDelay: 800,
47
- postKeys: 'return',
48
- pasteDelay: 300,
49
- },
50
-
51
- 'antigravity': {
52
- processName: { mac: 'Antigravity', win: 'Antigravity' },
53
- appName: { mac: 'Antigravity', win: 'Antigravity' },
54
- // Antigravity: chat input auto-focused → paste → Enter
55
- preKeys: null,
56
- preDelay: 0,
57
- postKeys: 'return',
58
- pasteDelay: 300,
59
- },
60
- };
61
-
62
- // ──────────────────────────────────────────────
63
- // Process Detection
64
- // ──────────────────────────────────────────────
65
-
66
- /**
67
- * Check if a process is running
68
- * @param {string} processName - Name of the process
69
- * @returns {boolean}
70
- */
71
- export function isProcessRunning(processName) {
72
- try {
73
- if (IS_MAC) {
74
- const result = execSync(
75
- `pgrep -x "${processName}" || pgrep -fi "${processName}"`,
76
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
77
- ).trim();
78
- return result.length > 0;
79
- } else if (IS_WIN) {
80
- const result = execSync(
81
- `tasklist /FI "IMAGENAME eq ${processName}.exe" /NH`,
82
- { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
83
- ).trim();
84
- return !result.includes('No tasks');
85
- }
86
- return false;
87
- } catch {
88
- return false;
89
- }
90
- }
91
-
92
- // ──────────────────────────────────────────────
93
- // macOS Automation (AppleScript)
94
- // ──────────────────────────────────────────────
95
-
96
- function buildAppleScript(recipe) {
97
- const appName = recipe.appName.mac;
98
- const lines = [];
99
-
100
- // Activate the application
101
- lines.push(`tell application "${appName}" to activate`);
102
- lines.push('delay 0.5');
103
-
104
- // System Events block for keystrokes
105
- lines.push('tell application "System Events"');
106
-
107
- // Pre-keys (e.g., Cmd+I for Cursor Composer)
108
- if (recipe.preKeys) {
109
- lines.push(` ${recipe.preKeys}`);
110
- lines.push(` delay ${recipe.preDelay / 1000}`);
111
- }
112
-
113
- // Paste: Cmd+V
114
- lines.push(' keystroke "v" using {command down}');
115
- lines.push(` delay ${recipe.pasteDelay / 1000}`);
116
-
117
- // Post-keys: usually Enter to send
118
- if (recipe.postKeys === 'return') {
119
- lines.push(' keystroke return');
120
- }
121
-
122
- lines.push('end tell');
123
-
124
- return lines.join('\n');
125
- }
126
-
127
- function runAppleScript(script) {
128
- try {
129
- execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
130
- encoding: 'utf-8',
131
- stdio: ['pipe', 'pipe', 'pipe'],
132
- timeout: 10000,
133
- });
134
- return true;
135
- } catch (err) {
136
- log.warn(`AppleScript failed: ${err.message}`);
137
- if (err.message.includes('not allowed assistive access')) {
138
- log.error('⚠ Accessibility permission required:');
139
- log.error(' System Settings → Privacy & Security → Accessibility');
140
- log.error(' Add your terminal app (Terminal / iTerm2 / Warp)');
141
- }
142
- return false;
143
- }
144
- }
145
-
146
- // ──────────────────────────────────────────────
147
- // Windows Automation (PowerShell)
148
- // ──────────────────────────────────────────────
149
-
150
- function buildPowerShell(recipe) {
151
- const appName = recipe.appName.win;
152
- const lines = [];
153
-
154
- lines.push('Add-Type -AssemblyName System.Windows.Forms');
155
- lines.push('Add-Type -AssemblyName Microsoft.VisualBasic');
156
-
157
- // Activate window
158
- lines.push(`[Microsoft.VisualBasic.Interaction]::AppActivate("${appName}")`);
159
- lines.push('Start-Sleep -Milliseconds 500');
160
-
161
- // Pre-keys
162
- if (recipe.preKeys) {
163
- lines.push(`[System.Windows.Forms.SendKeys]::SendWait("${recipe.preKeys}")`);
164
- lines.push(`Start-Sleep -Milliseconds ${recipe.preDelay}`);
165
- }
166
-
167
- // Paste: Ctrl+V
168
- lines.push('[System.Windows.Forms.SendKeys]::SendWait("^v")');
169
- lines.push(`Start-Sleep -Milliseconds ${recipe.pasteDelay}`);
170
-
171
- // Post-keys: Enter
172
- if (recipe.postKeys === 'return') {
173
- lines.push('[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")');
174
- }
175
-
176
- return lines.join('; ');
177
- }
178
-
179
- function runPowerShell(script) {
180
- try {
181
- execSync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, {
182
- encoding: 'utf-8',
183
- stdio: ['pipe', 'pipe', 'pipe'],
184
- timeout: 10000,
185
- });
186
- return true;
187
- } catch (err) {
188
- log.warn(`PowerShell automation failed: ${err.message}`);
189
- return false;
190
- }
191
- }
192
-
193
- // ──────────────────────────────────────────────
194
- // Public API
195
- // ──────────────────────────────────────────────
196
-
197
- /**
198
- * Check if a specific IDE has a known automation recipe
199
- */
200
- export function hasRecipe(providerId) {
201
- return providerId in IDE_RECIPES;
202
- }
203
-
204
- /**
205
- * Check if the IDE is currently running
206
- */
207
- export function isIDERunning(providerId) {
208
- const recipe = IDE_RECIPES[providerId];
209
- if (!recipe) return false;
210
-
211
- const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
212
- return isProcessRunning(processName);
213
- }
214
-
215
- /**
216
- * Attempt to auto-paste prompt into the IDE and trigger send
217
- *
218
- * Flow: copy to clipboard → activate window → pre-keys → paste → post-keys
219
- *
220
- * @param {string} providerId - IDE provider ID
221
- * @param {string} text - Prompt text to paste
222
- * @returns {boolean} true if automation succeeded
223
- */
224
- export function activateAndPaste(providerId, text) {
225
- const recipe = IDE_RECIPES[providerId];
226
- if (!recipe) {
227
- log.warn(`No automation recipe for '${providerId}'`);
228
- return false;
229
- }
230
-
231
- // Step 1: Check process is running
232
- const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
233
- if (!isProcessRunning(processName)) {
234
- log.warn(`${recipe.appName[IS_MAC ? 'mac' : 'win']} is not running`);
235
- return false;
236
- }
237
-
238
- // Step 2: Copy to clipboard (handled by caller, but ensure it's done)
239
- copyToSystemClipboard(text);
240
-
241
- // Step 3: Run platform-specific automation
242
- log.info(`Auto-pasting into ${recipe.appName[IS_MAC ? 'mac' : 'win']}...`);
243
-
244
- if (IS_MAC) {
245
- const script = buildAppleScript(recipe);
246
- return runAppleScript(script);
247
- } else if (IS_WIN) {
248
- const script = buildPowerShell(recipe);
249
- return runPowerShell(script);
250
- } else {
251
- log.warn('Desktop automation not supported on this platform');
252
- return false;
253
- }
254
- }
255
-
256
- /**
257
- * Copy text to system clipboard (cross-platform)
258
- */
259
- function copyToSystemClipboard(text) {
260
- try {
261
- if (IS_MAC) {
262
- execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
263
- } else if (IS_WIN) {
264
- execSync('clip', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
265
- } else {
266
- try {
267
- execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
268
- } catch {
269
- execSync('xsel --clipboard --input', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
270
- }
271
- }
272
- } catch {
273
- // Clipboard failure is non-fatal — user can copy file manually
274
- }
275
- }
276
-
277
- export const automator = {
278
- isProcessRunning, hasRecipe, isIDERunning, activateAndPaste,
279
- };
1
+ /**
2
+ * Desktop IDE Automator — platform-aware process detection + auto-paste
3
+ *
4
+ * Supports macOS (AppleScript/osascript) and Windows (PowerShell/SendKeys).
5
+ * Each IDE has its own keystroke recipe.
6
+ * Falls back gracefully if automation fails.
7
+ */
8
+
9
+ import { execSync } from 'child_process';
10
+ import { log } from './logger.js';
11
+
12
+ const IS_MAC = process.platform === 'darwin';
13
+ const IS_WIN = process.platform === 'win32';
14
+
15
+ // ──────────────────────────────────────────────
16
+ // Per-IDE Automation Recipes
17
+ // ──────────────────────────────────────────────
18
+
19
+ /**
20
+ * Each recipe defines:
21
+ * processName: { mac, win } — binary/process name to detect
22
+ * appName: { mac, win } — display name for activation
23
+ * preKeys: keystroke sequence BEFORE paste (e.g., open Composer in Cursor)
24
+ * preDelay: ms to wait after preKeys
25
+ * postKeys: keystroke sequence AFTER paste (usually Enter to send)
26
+ * pasteDelay: ms to wait between paste and postKeys
27
+ */
28
+ const IDE_RECIPES = {
29
+ 'codex-desktop': {
30
+ processName: { mac: 'Codex', win: 'Codex' },
31
+ appName: { mac: 'Codex', win: 'Codex' },
32
+ // Codex Desktop: input is focused on activate → paste → Enter
33
+ preKeys: null,
34
+ preDelay: 0,
35
+ postKeys: 'return',
36
+ pasteDelay: 300,
37
+ },
38
+
39
+ 'cursor': {
40
+ processName: { mac: 'Cursor', win: 'Cursor' },
41
+ appName: { mac: 'Cursor', win: 'Cursor' },
42
+ // Cursor: Cmd/Ctrl+I opens Composer, then paste, then Enter
43
+ preKeys: IS_MAC
44
+ ? 'keystroke "i" using {command down}'
45
+ : '^i',
46
+ preDelay: 800,
47
+ postKeys: 'return',
48
+ pasteDelay: 300,
49
+ },
50
+
51
+ 'antigravity': {
52
+ processName: { mac: 'Antigravity', win: 'Antigravity' },
53
+ appName: { mac: 'Antigravity', win: 'Antigravity' },
54
+ // Antigravity: chat input auto-focused → paste → Enter
55
+ preKeys: null,
56
+ preDelay: 0,
57
+ postKeys: 'return',
58
+ pasteDelay: 300,
59
+ },
60
+ };
61
+
62
+ // ──────────────────────────────────────────────
63
+ // Process Detection
64
+ // ──────────────────────────────────────────────
65
+
66
+ /**
67
+ * Check if a process is running
68
+ * @param {string} processName - Name of the process
69
+ * @returns {boolean}
70
+ */
71
+ export function isProcessRunning(processName) {
72
+ try {
73
+ if (IS_MAC) {
74
+ const result = execSync(
75
+ `pgrep -x "${processName}" || pgrep -fi "${processName}"`,
76
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
77
+ ).trim();
78
+ return result.length > 0;
79
+ } else if (IS_WIN) {
80
+ const result = execSync(
81
+ `tasklist /FI "IMAGENAME eq ${processName}.exe" /NH`,
82
+ { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
83
+ ).trim();
84
+ return !result.includes('No tasks');
85
+ }
86
+ return false;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ // ──────────────────────────────────────────────
93
+ // macOS Automation (AppleScript)
94
+ // ──────────────────────────────────────────────
95
+
96
+ function buildAppleScript(recipe) {
97
+ const appName = recipe.appName.mac;
98
+ const lines = [];
99
+
100
+ // Activate the application
101
+ lines.push(`tell application "${appName}" to activate`);
102
+ lines.push('delay 0.5');
103
+
104
+ // System Events block for keystrokes
105
+ lines.push('tell application "System Events"');
106
+
107
+ // Pre-keys (e.g., Cmd+I for Cursor Composer)
108
+ if (recipe.preKeys) {
109
+ lines.push(` ${recipe.preKeys}`);
110
+ lines.push(` delay ${recipe.preDelay / 1000}`);
111
+ }
112
+
113
+ // Paste: Cmd+V
114
+ lines.push(' keystroke "v" using {command down}');
115
+ lines.push(` delay ${recipe.pasteDelay / 1000}`);
116
+
117
+ // Post-keys: usually Enter to send
118
+ if (recipe.postKeys === 'return') {
119
+ lines.push(' keystroke return');
120
+ }
121
+
122
+ lines.push('end tell');
123
+
124
+ return lines.join('\n');
125
+ }
126
+
127
+ function runAppleScript(script) {
128
+ try {
129
+ execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
130
+ encoding: 'utf-8',
131
+ stdio: ['pipe', 'pipe', 'pipe'],
132
+ timeout: 10000,
133
+ });
134
+ return true;
135
+ } catch (err) {
136
+ log.warn(`AppleScript failed: ${err.message}`);
137
+ if (err.message.includes('not allowed assistive access')) {
138
+ log.error('⚠ Accessibility permission required:');
139
+ log.error(' System Settings → Privacy & Security → Accessibility');
140
+ log.error(' Add your terminal app (Terminal / iTerm2 / Warp)');
141
+ }
142
+ return false;
143
+ }
144
+ }
145
+
146
+ // ──────────────────────────────────────────────
147
+ // Windows Automation (PowerShell)
148
+ // ──────────────────────────────────────────────
149
+
150
+ function buildPowerShell(recipe) {
151
+ const appName = recipe.appName.win;
152
+ const lines = [];
153
+
154
+ lines.push('Add-Type -AssemblyName System.Windows.Forms');
155
+ lines.push('Add-Type -AssemblyName Microsoft.VisualBasic');
156
+
157
+ // Activate window
158
+ lines.push(`[Microsoft.VisualBasic.Interaction]::AppActivate("${appName}")`);
159
+ lines.push('Start-Sleep -Milliseconds 500');
160
+
161
+ // Pre-keys
162
+ if (recipe.preKeys) {
163
+ lines.push(`[System.Windows.Forms.SendKeys]::SendWait("${recipe.preKeys}")`);
164
+ lines.push(`Start-Sleep -Milliseconds ${recipe.preDelay}`);
165
+ }
166
+
167
+ // Paste: Ctrl+V
168
+ lines.push('[System.Windows.Forms.SendKeys]::SendWait("^v")');
169
+ lines.push(`Start-Sleep -Milliseconds ${recipe.pasteDelay}`);
170
+
171
+ // Post-keys: Enter
172
+ if (recipe.postKeys === 'return') {
173
+ lines.push('[System.Windows.Forms.SendKeys]::SendWait("{ENTER}")');
174
+ }
175
+
176
+ return lines.join('; ');
177
+ }
178
+
179
+ function runPowerShell(script) {
180
+ try {
181
+ execSync(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"')}"`, {
182
+ encoding: 'utf-8',
183
+ stdio: ['pipe', 'pipe', 'pipe'],
184
+ timeout: 10000,
185
+ });
186
+ return true;
187
+ } catch (err) {
188
+ log.warn(`PowerShell automation failed: ${err.message}`);
189
+ return false;
190
+ }
191
+ }
192
+
193
+ // ──────────────────────────────────────────────
194
+ // Public API
195
+ // ──────────────────────────────────────────────
196
+
197
+ /**
198
+ * Check if a specific IDE has a known automation recipe
199
+ */
200
+ export function hasRecipe(providerId) {
201
+ return providerId in IDE_RECIPES;
202
+ }
203
+
204
+ /**
205
+ * Check if the IDE is currently running
206
+ */
207
+ export function isIDERunning(providerId) {
208
+ const recipe = IDE_RECIPES[providerId];
209
+ if (!recipe) return false;
210
+
211
+ const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
212
+ return isProcessRunning(processName);
213
+ }
214
+
215
+ /**
216
+ * Attempt to auto-paste prompt into the IDE and trigger send
217
+ *
218
+ * Flow: copy to clipboard → activate window → pre-keys → paste → post-keys
219
+ *
220
+ * @param {string} providerId - IDE provider ID
221
+ * @param {string} text - Prompt text to paste
222
+ * @returns {boolean} true if automation succeeded
223
+ */
224
+ export function activateAndPaste(providerId, text) {
225
+ const recipe = IDE_RECIPES[providerId];
226
+ if (!recipe) {
227
+ log.warn(`No automation recipe for '${providerId}'`);
228
+ return false;
229
+ }
230
+
231
+ // Step 1: Check process is running
232
+ const processName = IS_MAC ? recipe.processName.mac : recipe.processName.win;
233
+ if (!isProcessRunning(processName)) {
234
+ log.warn(`${recipe.appName[IS_MAC ? 'mac' : 'win']} is not running`);
235
+ return false;
236
+ }
237
+
238
+ // Step 2: Copy to clipboard (handled by caller, but ensure it's done)
239
+ copyToSystemClipboard(text);
240
+
241
+ // Step 3: Run platform-specific automation
242
+ log.info(`Auto-pasting into ${recipe.appName[IS_MAC ? 'mac' : 'win']}...`);
243
+
244
+ if (IS_MAC) {
245
+ const script = buildAppleScript(recipe);
246
+ return runAppleScript(script);
247
+ } else if (IS_WIN) {
248
+ const script = buildPowerShell(recipe);
249
+ return runPowerShell(script);
250
+ } else {
251
+ log.warn('Desktop automation not supported on this platform');
252
+ return false;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Copy text to system clipboard (cross-platform)
258
+ */
259
+ function copyToSystemClipboard(text) {
260
+ try {
261
+ if (IS_MAC) {
262
+ execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
263
+ } else if (IS_WIN) {
264
+ execSync('clip', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
265
+ } else {
266
+ try {
267
+ execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
268
+ } catch {
269
+ execSync('xsel --clipboard --input', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
270
+ }
271
+ }
272
+ } catch {
273
+ // Clipboard failure is non-fatal — user can copy file manually
274
+ }
275
+ }
276
+
277
+ export const automator = {
278
+ isProcessRunning, hasRecipe, isIDERunning, activateAndPaste,
279
+ };