@jojonax/codex-copilot 1.0.2 → 1.1.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,332 @@
1
+ /**
2
+ * AI Provider Registry — supports multiple AI coding tools
3
+ *
4
+ * Each provider has its own invocation pattern:
5
+ * - Codex CLI: piped stdin → codex exec --full-auto
6
+ * - Claude Code: -p flag + --allowedTools for safe auto-execution
7
+ * - Cursor Agent: cursor-agent CLI with -p headless mode
8
+ * - Gemini CLI: gemini -p for non-interactive prompt
9
+ * - Codex Desktop / Cursor IDE / Antigravity IDE: clipboard + manual
10
+ */
11
+
12
+ import { execSync } from 'child_process';
13
+ import { readFileSync } from 'fs';
14
+ import { log } from './logger.js';
15
+ import { ask } from './prompt.js';
16
+ import { automator } from './automator.js';
17
+
18
+ // ──────────────────────────────────────────────
19
+ // Provider Registry — each provider is unique
20
+ // ──────────────────────────────────────────────
21
+
22
+ const PROVIDERS = {
23
+ // ─── CLI Providers (auto-execute) ───
24
+
25
+ 'codex-cli': {
26
+ name: 'Codex CLI',
27
+ type: 'cli',
28
+ detect: 'codex',
29
+ // Codex uses piped stdin: cat file | codex exec --full-auto -
30
+ buildCommand: (promptPath, cwd) =>
31
+ `cat ${shellEscape(promptPath)} | codex exec --full-auto -`,
32
+ description: 'OpenAI Codex CLI — pipes prompt via stdin',
33
+ },
34
+
35
+ 'claude-code': {
36
+ name: 'Claude Code',
37
+ type: 'cli',
38
+ detect: 'claude',
39
+ // Claude Code uses -p (print/non-interactive) + --allowedTools for permissions
40
+ // Reads the file content and passes as argument (not piped)
41
+ buildCommand: (promptPath, cwd) => {
42
+ const escaped = shellEscape(promptPath);
43
+ // Use subshell to read file content into -p argument
44
+ return `claude -p "$(cat ${escaped})" --allowedTools "Bash(git*),Read,Write,Edit"`;
45
+ },
46
+ description: 'Anthropic Claude Code CLI — -p mode with tool permissions',
47
+ },
48
+
49
+ 'cursor-agent': {
50
+ name: 'Cursor Agent',
51
+ type: 'cli',
52
+ detect: 'cursor-agent',
53
+ // Cursor Agent uses -p flag for headless/non-interactive mode
54
+ buildCommand: (promptPath, cwd) => {
55
+ const escaped = shellEscape(promptPath);
56
+ return `cursor-agent -p "$(cat ${escaped})"`;
57
+ },
58
+ description: 'Cursor Agent CLI — headless -p mode',
59
+ },
60
+
61
+ 'gemini-cli': {
62
+ name: 'Gemini CLI',
63
+ type: 'cli',
64
+ detect: 'gemini',
65
+ // Gemini CLI uses -p for non-interactive prompt execution
66
+ buildCommand: (promptPath, cwd) => {
67
+ const escaped = shellEscape(promptPath);
68
+ return `gemini -p "$(cat ${escaped})"`;
69
+ },
70
+ description: 'Google Gemini CLI — non-interactive -p mode',
71
+ },
72
+
73
+ // ─── IDE Providers (clipboard + manual) ───
74
+
75
+ 'codex-desktop': {
76
+ name: 'Codex Desktop',
77
+ type: 'ide',
78
+ instructions: 'Open Codex Desktop → paste the prompt → execute',
79
+ displayPrompt: true, // Show the prompt content in terminal box
80
+ },
81
+
82
+ 'cursor': {
83
+ name: 'Cursor IDE',
84
+ type: 'ide',
85
+ instructions: 'Open Cursor → Ctrl/Cmd+I (Composer) → paste the prompt → run as Agent',
86
+ displayPrompt: false, // Too long to display, just copy to clipboard
87
+ },
88
+
89
+ 'antigravity': {
90
+ name: 'Antigravity (Gemini)',
91
+ type: 'ide',
92
+ instructions: 'Open Antigravity → paste the prompt into chat → execute',
93
+ displayPrompt: false,
94
+ },
95
+ };
96
+
97
+ /**
98
+ * Get a provider by ID
99
+ */
100
+ export function getProvider(id) {
101
+ return PROVIDERS[id] || null;
102
+ }
103
+
104
+ /**
105
+ * Get all provider IDs
106
+ */
107
+ export function getAllProviderIds() {
108
+ return Object.keys(PROVIDERS);
109
+ }
110
+
111
+ /**
112
+ * Detect which CLI providers are installed on the system
113
+ * @returns {string[]} list of available provider IDs
114
+ */
115
+ export function detectAvailable() {
116
+ const available = [];
117
+ for (const [id, prov] of Object.entries(PROVIDERS)) {
118
+ if (prov.type === 'cli' && prov.detect) {
119
+ try {
120
+ const cmd = process.platform === 'win32'
121
+ ? `where ${prov.detect}`
122
+ : `which ${prov.detect}`;
123
+ execSync(cmd, { stdio: 'pipe' });
124
+ available.push(id);
125
+ } catch {
126
+ // Not installed
127
+ }
128
+ }
129
+ }
130
+ return available;
131
+ }
132
+
133
+ /**
134
+ * Build the selection menu for init
135
+ * Groups CLIs first (with detection status), then IDEs
136
+ * @returns {{ label: string, value: string }[]}
137
+ */
138
+ export function buildProviderChoices() {
139
+ const detected = detectAvailable();
140
+ const choices = [];
141
+
142
+ // CLI providers first with detection indicator
143
+ for (const [id, prov] of Object.entries(PROVIDERS)) {
144
+ if (prov.type === 'cli') {
145
+ const installed = detected.includes(id);
146
+ const tag = installed ? ' ✓ detected' : '';
147
+ choices.push({
148
+ label: `${prov.name}${tag} — ${prov.description}`,
149
+ value: id,
150
+ available: installed,
151
+ });
152
+ }
153
+ }
154
+
155
+ // IDE providers — show auto-paste capability if recipe exists
156
+ for (const [id, prov] of Object.entries(PROVIDERS)) {
157
+ if (prov.type === 'ide') {
158
+ const hasAutoPaste = automator.hasRecipe(id);
159
+ const tag = hasAutoPaste ? 'auto-paste' : 'clipboard + manual';
160
+ choices.push({
161
+ label: `${prov.name} — ${tag}`,
162
+ value: id,
163
+ available: true,
164
+ });
165
+ }
166
+ }
167
+
168
+ return choices;
169
+ }
170
+
171
+ /**
172
+ * Execute a prompt using the configured provider
173
+ *
174
+ * CLI providers: auto-execute via their specific command
175
+ * IDE providers: copy to clipboard + display instructions + wait
176
+ *
177
+ * @param {string} providerId - Provider ID from config
178
+ * @param {string} promptPath - Absolute path to prompt file
179
+ * @param {string} cwd - Working directory
180
+ * @returns {Promise<boolean>} true if execution succeeded
181
+ */
182
+ export async function executePrompt(providerId, promptPath, cwd) {
183
+ const prov = PROVIDERS[providerId];
184
+
185
+ if (!prov) {
186
+ log.warn(`Unknown provider '${providerId}', falling back to clipboard mode`);
187
+ return await clipboardFallback(promptPath);
188
+ }
189
+
190
+ if (prov.type === 'cli') {
191
+ return await executeCLI(prov, providerId, promptPath, cwd);
192
+ } else {
193
+ return await executeIDE(prov, providerId, promptPath);
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Execute via CLI provider — each tool has its own command pattern
199
+ */
200
+ async function executeCLI(prov, providerId, promptPath, cwd) {
201
+ // Verify the CLI is still available
202
+ if (prov.detect) {
203
+ try {
204
+ const cmd = process.platform === 'win32'
205
+ ? `where ${prov.detect}`
206
+ : `which ${prov.detect}`;
207
+ execSync(cmd, { stdio: 'pipe' });
208
+ } catch {
209
+ log.warn(`${prov.name} not found in PATH, falling back to clipboard mode`);
210
+ return await clipboardFallback(promptPath);
211
+ }
212
+ }
213
+
214
+ const command = prov.buildCommand(promptPath, cwd);
215
+ log.info(`Executing via ${prov.name}...`);
216
+ log.dim(` → ${command.substring(0, 80)}${command.length > 80 ? '...' : ''}`);
217
+
218
+ try {
219
+ execSync(command, { cwd, stdio: 'inherit', timeout: 0 });
220
+ log.info(`${prov.name} execution complete`);
221
+ return true;
222
+ } catch (err) {
223
+ log.warn(`${prov.name} execution failed: ${err.message}`);
224
+
225
+ // For quota exhaustion / auth errors, give a clear message
226
+ if (err.message.includes('rate_limit') || err.message.includes('quota') ||
227
+ err.message.includes('429') || err.message.includes('insufficient')) {
228
+ log.error('⚠ Possible quota exhaustion — checkpoint saved, you can resume later');
229
+ }
230
+
231
+ log.warn('Falling back to clipboard mode');
232
+ return await clipboardFallback(promptPath);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Execute via IDE provider
238
+ *
239
+ * Priority: auto-paste (if IDE running) → manual clipboard fallback
240
+ */
241
+ async function executeIDE(prov, providerId, promptPath) {
242
+ const prompt = readFileSync(promptPath, 'utf-8');
243
+
244
+ // ── Try auto-paste first ──
245
+ if (automator.hasRecipe(providerId)) {
246
+ if (automator.isIDERunning(providerId)) {
247
+ log.info(`${prov.name} detected — attempting auto-paste...`);
248
+ const ok = automator.activateAndPaste(providerId, prompt);
249
+ if (ok) {
250
+ log.info(`✅ Prompt auto-pasted into ${prov.name}`);
251
+ log.blank();
252
+ await ask(`Press Enter after ${prov.name} development is complete...`);
253
+ return true;
254
+ }
255
+ log.warn('Auto-paste failed, falling back to manual mode');
256
+ } else {
257
+ log.warn(`${prov.name} is not running — using clipboard mode`);
258
+ }
259
+ }
260
+
261
+ // ── Manual fallback ──
262
+ log.blank();
263
+ log.info(`📋 ${prov.instructions}`);
264
+ log.dim(` Prompt file: ${promptPath}`);
265
+ log.blank();
266
+
267
+ // Some providers benefit from seeing the prompt in terminal
268
+ if (prov.displayPrompt) {
269
+ const lines = prompt.split('\n');
270
+ const maxLines = 30;
271
+ const show = lines.slice(0, maxLines);
272
+ console.log(' ┌─── Prompt Preview ─────────────────────────────────────┐');
273
+ for (const line of show) {
274
+ console.log(` │ ${line}`);
275
+ }
276
+ if (lines.length > maxLines) {
277
+ console.log(` │ ... (${lines.length - maxLines} more lines — see full file)`);
278
+ }
279
+ console.log(' └────────────────────────────────────────────────────────┘');
280
+ log.blank();
281
+ }
282
+
283
+ copyToClipboard(prompt);
284
+
285
+ await ask(`Press Enter after ${prov.name} development is complete...`);
286
+ return true;
287
+ }
288
+
289
+ /**
290
+ * Fallback: copy to clipboard and wait for any provider
291
+ */
292
+ async function clipboardFallback(promptPath) {
293
+ const prompt = readFileSync(promptPath, 'utf-8');
294
+
295
+ log.blank();
296
+ log.info('📋 Paste the prompt into your AI coding tool and execute');
297
+ log.dim(` Prompt file: ${promptPath}`);
298
+ log.blank();
299
+
300
+ copyToClipboard(prompt);
301
+
302
+ await ask('Press Enter after development is complete...');
303
+ return true;
304
+ }
305
+
306
+ function copyToClipboard(text) {
307
+ try {
308
+ if (process.platform === 'darwin') {
309
+ execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
310
+ } else if (process.platform === 'linux') {
311
+ // Try xclip first, then xsel
312
+ try {
313
+ execSync('xclip -selection clipboard', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
314
+ } catch {
315
+ execSync('xsel --clipboard --input', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
316
+ }
317
+ }
318
+ // Windows: skip — no good CLI clipboard tool
319
+ log.info('📋 Copied to clipboard');
320
+ } catch {
321
+ log.dim('(Could not copy to clipboard — copy the file manually)');
322
+ }
323
+ }
324
+
325
+ function shellEscape(str) {
326
+ return `'${str.replace(/'/g, "'\\''")}'`;
327
+ }
328
+
329
+ export const provider = {
330
+ getProvider, getAllProviderIds, detectAvailable,
331
+ buildProviderChoices, executePrompt,
332
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Version update checker
3
+ *
4
+ * - Checks npm registry for latest version on startup
5
+ * - 24h cache to avoid frequent network requests
6
+ * - Prints update prompt if a newer version is available
7
+ */
8
+
9
+ import { execSync } from 'child_process';
10
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ import { resolve } from 'path';
12
+ import { homedir } from 'os';
13
+ import { log } from './logger.js';
14
+
15
+ const PACKAGE_NAME = '@jojonax/codex-copilot';
16
+ const CACHE_FILE = resolve(homedir(), '.codex-copilot-update-cache.json');
17
+ const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
18
+
19
+ /**
20
+ * Read cached version info
21
+ */
22
+ function readCache() {
23
+ try {
24
+ const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
25
+ if (Date.now() - data.timestamp < CACHE_TTL) {
26
+ return data.latestVersion;
27
+ }
28
+ } catch {
29
+ // Cache miss or corrupt — ignore
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /**
35
+ * Write version info to cache
36
+ */
37
+ function writeCache(latestVersion) {
38
+ try {
39
+ writeFileSync(CACHE_FILE, JSON.stringify({
40
+ latestVersion,
41
+ timestamp: Date.now(),
42
+ }));
43
+ } catch {
44
+ // Permission error — ignore silently
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Fetch latest version from npm registry
50
+ */
51
+ function fetchLatestVersion() {
52
+ try {
53
+ const output = execSync(`npm view ${PACKAGE_NAME} version`, {
54
+ encoding: 'utf-8',
55
+ stdio: ['pipe', 'pipe', 'pipe'],
56
+ timeout: 5000, // 5s timeout
57
+ }).trim();
58
+ return output || null;
59
+ } catch {
60
+ // Network error, npm not available — ignore
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Compare semantic version strings
67
+ * @returns {boolean} true if latest > current
68
+ */
69
+ function isNewer(current, latest) {
70
+ if (!current || !latest) return false;
71
+ const c = current.split('.').map(Number);
72
+ const l = latest.split('.').map(Number);
73
+ for (let i = 0; i < 3; i++) {
74
+ if ((l[i] || 0) > (c[i] || 0)) return true;
75
+ if ((l[i] || 0) < (c[i] || 0)) return false;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ /**
81
+ * Check for updates and print notification
82
+ * @param {string} currentVersion - Current installed version
83
+ */
84
+ export function checkForUpdates(currentVersion) {
85
+ // 1. Check cache first
86
+ let latest = readCache();
87
+
88
+ // 2. Cache miss → fetch from npm
89
+ if (!latest) {
90
+ latest = fetchLatestVersion();
91
+ if (latest) {
92
+ writeCache(latest);
93
+ }
94
+ }
95
+
96
+ // 3. Compare and notify
97
+ if (latest && isNewer(currentVersion, latest)) {
98
+ log.blank();
99
+ log.warn(`Update available: v${currentVersion} → v${latest}`);
100
+ log.dim(` Run the following command to update:`);
101
+ log.dim(` npm install -g ${PACKAGE_NAME}@${latest}`);
102
+ }
103
+ }