oh-my-customcode 0.65.2 → 0.67.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,485 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * gemini-wrapper.cjs
5
+ *
6
+ * Node.js wrapper for Google Gemini CLI (non-interactive execution).
7
+ * Executes gemini in prompt mode with structured JSON output.
8
+ *
9
+ * Usage:
10
+ * node gemini-wrapper.cjs --prompt "your prompt" [options]
11
+ *
12
+ * Options:
13
+ * --prompt <text> Required: prompt to execute
14
+ * --json Enable JSON output from gemini (-o json)
15
+ * --stream-json Enable stream-JSON output (-o stream-json)
16
+ * --output <path> Save response to file
17
+ * --model <name> Specify model (default: gemini CLI default)
18
+ * --timeout <ms> Execution timeout in milliseconds (default: 120000, max: 600000)
19
+ * --yolo Use yolo approval mode (auto-approve all actions)
20
+ * --sandbox Run in sandbox mode
21
+ * --plan Use plan approval mode
22
+ * --working-dir <dir> Set working directory for execution
23
+ *
24
+ * Output (JSON to stdout):
25
+ * Success: { "success": true, "output": "...", "duration_ms": 1234, ... }
26
+ * Failure: { "success": false, "error": "...", "stderr": "...", ... }
27
+ *
28
+ * Exit codes:
29
+ * 0 = success
30
+ * 1 = execution error
31
+ * 2 = validation error (missing binary/auth)
32
+ */
33
+
34
+ const { spawn, execFileSync } = require('child_process');
35
+ const fs = require('fs');
36
+ const path = require('path');
37
+ const os = require('os');
38
+
39
+ // Configuration
40
+ const DEFAULT_TIMEOUT_MS = 120000; // 2 minutes
41
+ const MAX_TIMEOUT_MS = 600000; // 10 minutes
42
+ const KILL_GRACE_PERIOD_MS = 5000; // 5 seconds for graceful shutdown
43
+
44
+ /**
45
+ * Parse command line arguments
46
+ * @returns {Object} Parsed arguments
47
+ */
48
+ function parseArgs() {
49
+ const args = {
50
+ prompt: null,
51
+ json: false,
52
+ streamJson: false,
53
+ output: null,
54
+ model: null,
55
+ timeout: DEFAULT_TIMEOUT_MS,
56
+ yolo: false,
57
+ sandbox: false,
58
+ plan: false,
59
+ workingDir: null,
60
+ };
61
+
62
+ for (let i = 2; i < process.argv.length; i++) {
63
+ const arg = process.argv[i];
64
+
65
+ switch (arg) {
66
+ case '--prompt':
67
+ if (i + 1 < process.argv.length) {
68
+ args.prompt = process.argv[++i];
69
+ }
70
+ break;
71
+ case '--json':
72
+ args.json = true;
73
+ break;
74
+ case '--stream-json':
75
+ args.streamJson = true;
76
+ break;
77
+ case '--output':
78
+ if (i + 1 < process.argv.length) {
79
+ args.output = process.argv[++i];
80
+ }
81
+ break;
82
+ case '--model':
83
+ if (i + 1 < process.argv.length) {
84
+ args.model = process.argv[++i];
85
+ }
86
+ break;
87
+ case '--timeout':
88
+ if (i + 1 < process.argv.length) {
89
+ const timeoutValue = parseInt(process.argv[++i], 10);
90
+ if (!isNaN(timeoutValue)) {
91
+ args.timeout = Math.min(timeoutValue, MAX_TIMEOUT_MS);
92
+ }
93
+ }
94
+ break;
95
+ case '--yolo':
96
+ args.yolo = true;
97
+ break;
98
+ case '--sandbox':
99
+ args.sandbox = true;
100
+ break;
101
+ case '--plan':
102
+ args.plan = true;
103
+ break;
104
+ case '--working-dir':
105
+ if (i + 1 < process.argv.length) {
106
+ args.workingDir = process.argv[++i];
107
+ }
108
+ break;
109
+ }
110
+ }
111
+
112
+ return args;
113
+ }
114
+
115
+ /**
116
+ * Validate environment for gemini execution
117
+ * @returns {Object} Validation result { valid: boolean, errors: string[] }
118
+ */
119
+ function validateEnvironment() {
120
+ const errors = [];
121
+
122
+ // Check for gemini binary
123
+ try {
124
+ execFileSync('which', ['gemini'], { stdio: 'pipe' });
125
+ } catch (error) {
126
+ // Try common installation paths
127
+ const commonPaths = [
128
+ '/usr/local/bin/gemini',
129
+ path.join(os.homedir(), '.local', 'bin', 'gemini'),
130
+ path.join(os.homedir(), 'bin', 'gemini'),
131
+ path.join(os.homedir(), '.npm-global', 'bin', 'gemini'),
132
+ ];
133
+
134
+ const geminiExists = commonPaths.some(p => fs.existsSync(p));
135
+ if (!geminiExists) {
136
+ errors.push('gemini binary not found in PATH or common locations');
137
+ }
138
+ }
139
+
140
+ // Check authentication (multiple methods supported)
141
+ const hasGoogleApiKey = !!process.env.GOOGLE_API_KEY;
142
+ const hasGeminiApiKey = !!process.env.GEMINI_API_KEY;
143
+
144
+ if (!hasGoogleApiKey && !hasGeminiApiKey) {
145
+ console.error('[gemini-wrapper] Note: GOOGLE_API_KEY/GEMINI_API_KEY not set, relying on gcloud auth');
146
+ }
147
+
148
+ return {
149
+ valid: errors.length === 0,
150
+ errors,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Build gemini command array
156
+ * @param {Object} options - Command options
157
+ * @returns {Object} Command structure { binary: string, args: string[] }
158
+ */
159
+ function buildCommand(options) {
160
+ const args = [];
161
+
162
+ // Prompt mode (non-interactive, ephemeral)
163
+ args.push('-p', options.prompt);
164
+
165
+ // Output format
166
+ if (options.streamJson) {
167
+ args.push('-o', 'stream-json');
168
+ } else if (options.json) {
169
+ args.push('-o', 'json');
170
+ }
171
+
172
+ // Model selection
173
+ if (options.model) {
174
+ args.push('-m', options.model);
175
+ }
176
+
177
+ // Approval mode
178
+ if (options.yolo) {
179
+ args.push('-y');
180
+ } else if (options.plan) {
181
+ args.push('--approval-mode', 'plan');
182
+ }
183
+
184
+ // Sandbox mode
185
+ if (options.sandbox) {
186
+ args.push('-s');
187
+ }
188
+
189
+ return {
190
+ binary: 'gemini',
191
+ args,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Execute gemini command
197
+ * @param {string} binary - Binary to execute
198
+ * @param {string[]} args - Command arguments
199
+ * @param {number} timeout - Timeout in milliseconds
200
+ * @param {string|null} workingDir - Working directory
201
+ * @returns {Promise<Object>} Execution result
202
+ */
203
+ function executeGemini(binary, args, timeout, workingDir = null) {
204
+ return new Promise((resolve) => {
205
+ const startTime = Date.now();
206
+ let stdout = '';
207
+ let stderr = '';
208
+ let timedOut = false;
209
+
210
+ const spawnOptions = {
211
+ cwd: workingDir || process.cwd(),
212
+ env: process.env,
213
+ };
214
+
215
+ const child = spawn(binary, args, spawnOptions);
216
+
217
+ // Collect output
218
+ child.stdout.on('data', (data) => {
219
+ stdout += data.toString();
220
+ });
221
+
222
+ child.stderr.on('data', (data) => {
223
+ stderr += data.toString();
224
+ });
225
+
226
+ // Set timeout
227
+ const timeoutHandle = setTimeout(() => {
228
+ timedOut = true;
229
+ console.error('[gemini-wrapper] Timeout reached, terminating process...');
230
+
231
+ // Graceful termination attempt
232
+ child.kill('SIGTERM');
233
+
234
+ // Force kill after grace period
235
+ setTimeout(() => {
236
+ if (!child.killed) {
237
+ console.error('[gemini-wrapper] Force killing process...');
238
+ child.kill('SIGKILL');
239
+ }
240
+ }, KILL_GRACE_PERIOD_MS);
241
+ }, timeout);
242
+
243
+ // Handle process exit
244
+ child.on('close', (exitCode) => {
245
+ clearTimeout(timeoutHandle);
246
+ const durationMs = Date.now() - startTime;
247
+
248
+ resolve({
249
+ exitCode: exitCode !== null ? exitCode : 1,
250
+ stdout,
251
+ stderr,
252
+ timedOut,
253
+ durationMs,
254
+ });
255
+ });
256
+
257
+ // Handle spawn errors
258
+ child.on('error', (error) => {
259
+ clearTimeout(timeoutHandle);
260
+ const durationMs = Date.now() - startTime;
261
+
262
+ resolve({
263
+ exitCode: 1,
264
+ stdout,
265
+ stderr: stderr + '\nSpawn error: ' + error.message,
266
+ timedOut: false,
267
+ durationMs,
268
+ });
269
+ });
270
+ });
271
+ }
272
+
273
+ /**
274
+ * Parse JSON output from gemini (-o json)
275
+ * Gemini JSON output is a single JSON object: { session_id, response, stats }
276
+ * @param {string} output - Raw output string
277
+ * @returns {Object} Parsed result { response: string|null, stats: object|null, parseError: string|null }
278
+ */
279
+ function parseJson(output) {
280
+ try {
281
+ const data = JSON.parse(output.trim());
282
+ return {
283
+ response: data.response || null,
284
+ stats: data.stats || null,
285
+ sessionId: data.session_id || null,
286
+ parseError: null,
287
+ };
288
+ } catch (error) {
289
+ return {
290
+ response: null,
291
+ stats: null,
292
+ sessionId: null,
293
+ parseError: `Failed to parse JSON: ${error.message}`,
294
+ };
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Parse stream-JSON output from gemini (-o stream-json)
300
+ * Stream format: newline-delimited JSON events
301
+ * { type: "init", ... }
302
+ * { type: "message", role: "user"|"assistant", content: "..." }
303
+ * { type: "result", stats: {...} }
304
+ * @param {string} output - Raw output string
305
+ * @returns {Object} Parsed result { events: object[], finalMessage: string|null, stats: object|null, parseErrors: string[] }
306
+ */
307
+ function parseStreamJson(output) {
308
+ const lines = output.split('\n').filter(line => line.trim().length > 0);
309
+ const events = [];
310
+ const parseErrors = [];
311
+ let finalMessage = null;
312
+ let stats = null;
313
+
314
+ for (const line of lines) {
315
+ try {
316
+ const event = JSON.parse(line);
317
+ events.push(event);
318
+
319
+ // Extract final assistant message
320
+ if (event.type === 'message' && event.role === 'assistant') {
321
+ finalMessage = event.content || event.text || finalMessage;
322
+ }
323
+
324
+ // Extract stats from result event
325
+ if (event.type === 'result') {
326
+ stats = event.stats || null;
327
+ if (event.response) {
328
+ finalMessage = event.response;
329
+ }
330
+ }
331
+
332
+ // Fallback: look for common response patterns
333
+ if (!finalMessage && event.content && event.role === 'model') {
334
+ finalMessage = event.content;
335
+ }
336
+ } catch (error) {
337
+ parseErrors.push(`Failed to parse line: ${error.message}`);
338
+ }
339
+ }
340
+
341
+ return {
342
+ events,
343
+ finalMessage,
344
+ stats,
345
+ parseErrors,
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Main execution function
351
+ */
352
+ async function main() {
353
+ const args = parseArgs();
354
+
355
+ // Validate required arguments
356
+ if (!args.prompt) {
357
+ const result = {
358
+ success: false,
359
+ error: 'Missing required argument: --prompt',
360
+ exit_code: 2,
361
+ };
362
+ console.log(JSON.stringify(result, null, 2));
363
+ process.exit(2);
364
+ }
365
+
366
+ // Validate environment
367
+ const validation = validateEnvironment();
368
+ if (!validation.valid) {
369
+ const result = {
370
+ success: false,
371
+ error: 'Environment validation failed',
372
+ validation_errors: validation.errors,
373
+ exit_code: 2,
374
+ };
375
+ console.log(JSON.stringify(result, null, 2));
376
+ process.exit(2);
377
+ }
378
+
379
+ console.error(`[gemini-wrapper] Executing gemini with timeout: ${args.timeout}ms`);
380
+ if (args.workingDir) {
381
+ console.error(`[gemini-wrapper] Working directory: ${args.workingDir}`);
382
+ }
383
+
384
+ // Build command
385
+ const command = buildCommand(args);
386
+ console.error(`[gemini-wrapper] Command: ${command.binary} ${command.args.join(' ')}`);
387
+
388
+ // Execute
389
+ const execResult = await executeGemini(
390
+ command.binary,
391
+ command.args,
392
+ args.timeout,
393
+ args.workingDir
394
+ );
395
+
396
+ // Process result
397
+ let output = null;
398
+ let eventsCount = 0;
399
+ let stats = null;
400
+
401
+ if (args.streamJson && execResult.stdout) {
402
+ const parsed = parseStreamJson(execResult.stdout);
403
+ eventsCount = parsed.events.length;
404
+ output = parsed.finalMessage;
405
+ stats = parsed.stats;
406
+
407
+ if (parsed.parseErrors.length > 0) {
408
+ console.error('[gemini-wrapper] Stream-JSON parse errors:', parsed.parseErrors.join('; '));
409
+ }
410
+ } else if (args.json && execResult.stdout) {
411
+ const parsed = parseJson(execResult.stdout);
412
+ output = parsed.response;
413
+ stats = parsed.stats;
414
+
415
+ if (parsed.parseError) {
416
+ console.error('[gemini-wrapper] JSON parse error:', parsed.parseError);
417
+ // Fallback to raw output
418
+ output = execResult.stdout.trim();
419
+ }
420
+ } else {
421
+ output = execResult.stdout.trim();
422
+ }
423
+
424
+ // Determine success
425
+ const success = execResult.exitCode === 0 && !execResult.timedOut;
426
+
427
+ // Build result object
428
+ const result = {
429
+ success,
430
+ duration_ms: execResult.durationMs,
431
+ exit_code: execResult.exitCode,
432
+ };
433
+
434
+ if (success) {
435
+ result.output = output || execResult.stdout;
436
+ result.model = args.model || '(default)';
437
+ if (args.streamJson) {
438
+ result.events_count = eventsCount;
439
+ }
440
+ if (stats) {
441
+ result.stats = stats;
442
+ }
443
+ } else {
444
+ if (execResult.timedOut) {
445
+ result.error = `Execution timed out after ${args.timeout}ms`;
446
+ } else {
447
+ result.error = 'Execution failed';
448
+ }
449
+ if (execResult.stderr) {
450
+ result.stderr = execResult.stderr.trim();
451
+ }
452
+ }
453
+
454
+ // Write output file if requested
455
+ if (args.output && output) {
456
+ try {
457
+ const outputDir = path.dirname(args.output);
458
+ if (!fs.existsSync(outputDir)) {
459
+ fs.mkdirSync(outputDir, { recursive: true });
460
+ }
461
+ fs.writeFileSync(args.output, output, 'utf-8');
462
+ console.error(`[gemini-wrapper] Output written to: ${args.output}`);
463
+ } catch (error) {
464
+ console.error(`[gemini-wrapper] Failed to write output file: ${error.message}`);
465
+ result.output_file_error = error.message;
466
+ }
467
+ }
468
+
469
+ // Output JSON result to stdout
470
+ console.log(JSON.stringify(result, null, 2));
471
+
472
+ process.exit(result.exit_code);
473
+ }
474
+
475
+ // Run
476
+ main().catch(error => {
477
+ const result = {
478
+ success: false,
479
+ error: 'Unexpected error: ' + error.message,
480
+ stack: error.stack,
481
+ exit_code: 1,
482
+ };
483
+ console.log(JSON.stringify(result, null, 2));
484
+ process.exit(1);
485
+ });
@@ -110,6 +110,14 @@ agents:
110
110
  supported_actions: [review, create, fix, refactor, test]
111
111
  base_confidence: 40
112
112
 
113
+ fe-design-expert:
114
+ keywords:
115
+ korean: [디자인, 타이포그래피, 색상, 모션, UX라이팅, 디자인시스템, 디자인리뷰]
116
+ english: [design, typography, color, motion, "ux writing", "design system", "design review", impeccable, "design audit", "ai slop"]
117
+ file_patterns: ["*.css", "*.scss", "*.less", "tailwind.config.*"]
118
+ supported_actions: [review, audit, critique, polish, normalize]
119
+ base_confidence: 40
120
+
113
121
  # SW Engineers - Backend
114
122
  be-fastapi-expert:
115
123
  keywords:
@@ -330,7 +338,7 @@ agents:
330
338
  - gather
331
339
  base_confidence: 50
332
340
  action_weight: 30
333
- routing_rule: "MUST use codex-exec --effort xhigh when codex available, fallback to WebFetch/WebSearch"
341
+ routing_rule: "MUST use codex-exec --effort xhigh when codex available, or gemini-exec when gemini available, fallback to WebFetch/WebSearch"
334
342
 
335
343
  # ---------------------------------------------------------------------------
336
344
  # Code Generation (hybrid workflow, skill-based)
@@ -359,7 +367,7 @@ agents:
359
367
  - scaffold
360
368
  base_confidence: 30
361
369
  action_weight: 25
362
- routing_rule: "Suggest codex-exec hybrid when codex available and task is new file creation"
370
+ routing_rule: "Suggest codex-exec hybrid when codex available, or gemini-exec hybrid when gemini available, for new file creation tasks"
363
371
 
364
372
  # Managers (continued)
365
373
  mgr-gitnerd:
@@ -411,3 +419,20 @@ agents:
411
419
  file_patterns: []
412
420
  supported_actions: [manage, create, coordinate]
413
421
  base_confidence: 40
422
+
423
+ professor-triage:
424
+ keywords:
425
+ korean: [트리아지, 이슈분석, 교차분석, 프로페서]
426
+ english: [triage, cross-analyze, professor, issue-analysis]
427
+ file_patterns: []
428
+ actions: [review, analyze]
429
+ base_confidence: 85
430
+ routing_rule: "MUST use professor-triage skill for cross-analyzing GitHub issues with omc_issue_analyzer comments"
431
+
432
+ rtk-exec:
433
+ keywords:
434
+ korean: [rtk, 토큰절감, 출력압축, 토큰최적화]
435
+ english: [rtk, "token savings", "compress output", "token optimization"]
436
+ file_patterns: []
437
+ supported_actions: [optimize, compress, proxy]
438
+ base_confidence: 85