@quantiya/codevibe-gemini-plugin 1.0.6 → 1.0.7

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/dist/http-api.js DELETED
@@ -1,582 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.HttpApi = void 0;
40
- const express_1 = __importDefault(require("express"));
41
- const fs = __importStar(require("fs"));
42
- const path = __importStar(require("path"));
43
- const os = __importStar(require("os"));
44
- const codevibe_core_1 = require("@quantiya/codevibe-core");
45
- const logger_1 = require("./logger");
46
- const types_1 = require("./types");
47
- class HttpApi {
48
- constructor() {
49
- this.assignedPort = 0;
50
- this.app = (0, express_1.default)();
51
- this.setupMiddleware();
52
- this.setupRoutes();
53
- }
54
- /**
55
- * Set the session ID for this server instance (used for port file naming)
56
- */
57
- setSessionId(sessionId) {
58
- this.sessionId = sessionId;
59
- }
60
- /**
61
- * Get the assigned port after server starts
62
- */
63
- getPort() {
64
- return this.assignedPort;
65
- }
66
- setupMiddleware() {
67
- // Parse JSON bodies
68
- this.app.use(express_1.default.json({ limit: '1mb' }));
69
- // Request logging
70
- this.app.use((req, res, next) => {
71
- logger_1.logger.debug(`${req.method} ${req.path}`, {
72
- body: req.body,
73
- query: req.query,
74
- });
75
- next();
76
- });
77
- // Error handling middleware
78
- this.app.use((err, req, res, next) => {
79
- logger_1.logger.error('Express error:', err);
80
- const response = {
81
- success: false,
82
- error: err.message || 'Internal server error',
83
- };
84
- res.status(500).json(response);
85
- });
86
- }
87
- setupRoutes() {
88
- // Health check endpoint
89
- this.app.get('/health', this.handleHealth.bind(this));
90
- // Event endpoint for hooks
91
- this.app.post('/event', this.handleEvent.bind(this));
92
- // Interactive prompt endpoints for BeforeTool hook
93
- this.app.post('/interactive-prompt', this.handleInteractivePrompt.bind(this));
94
- this.app.get('/prompt-response/:promptId', this.handleGetPromptResponse.bind(this));
95
- // Development testing endpoint
96
- if (process.env.NODE_ENV !== 'production') {
97
- this.app.post('/test/execute', this.handleTestExecute.bind(this));
98
- }
99
- }
100
- handleHealth(req, res) {
101
- const response = {
102
- success: true,
103
- data: {
104
- status: 'healthy',
105
- uptime: process.uptime(),
106
- version: '0.1.0',
107
- timestamp: new Date().toISOString(),
108
- },
109
- };
110
- res.json(response);
111
- }
112
- async handleEvent(req, res) {
113
- try {
114
- const hookInput = req.body;
115
- // Validate required fields
116
- if (!hookInput.session_id) {
117
- const response = {
118
- success: false,
119
- error: 'Missing required field: session_id',
120
- };
121
- res.status(400).json(response);
122
- return;
123
- }
124
- if (!hookInput.hook_event_name) {
125
- const response = {
126
- success: false,
127
- error: 'Missing required field: hook_event_name',
128
- };
129
- res.status(400).json(response);
130
- return;
131
- }
132
- // Transform hook input to event payload
133
- const eventPayload = this.transformHookToEvent(hookInput);
134
- logger_1.logger.info('Received event from hook', {
135
- sessionId: hookInput.session_id,
136
- hookEvent: hookInput.hook_event_name,
137
- type: eventPayload.type,
138
- });
139
- // Call event handler if registered
140
- if (this.eventHandler) {
141
- await this.eventHandler(eventPayload);
142
- }
143
- else {
144
- logger_1.logger.warn('No event handler registered');
145
- }
146
- const response = {
147
- success: true,
148
- message: 'Event processed successfully',
149
- };
150
- res.json(response);
151
- }
152
- catch (error) {
153
- logger_1.logger.error('Error handling event:', error);
154
- const response = {
155
- success: false,
156
- error: error instanceof Error ? error.message : 'Unknown error',
157
- };
158
- res.status(500).json(response);
159
- }
160
- }
161
- async handleTestExecute(req, res) {
162
- try {
163
- const { sessionId, prompt } = req.body;
164
- if (!sessionId || !prompt) {
165
- const response = {
166
- success: false,
167
- error: 'Missing required fields: sessionId, prompt',
168
- };
169
- res.status(400).json(response);
170
- return;
171
- }
172
- logger_1.logger.info('Test execute request', { sessionId, prompt });
173
- const response = {
174
- success: true,
175
- message: 'Test execution endpoint - not implemented yet',
176
- data: { sessionId, prompt },
177
- };
178
- res.json(response);
179
- }
180
- catch (error) {
181
- logger_1.logger.error('Error in test execute:', error);
182
- const response = {
183
- success: false,
184
- error: error instanceof Error ? error.message : 'Unknown error',
185
- };
186
- res.status(500).json(response);
187
- }
188
- }
189
- /**
190
- * Handle interactive prompt request from BeforeTool hook
191
- * Creates a prompt, sends to iOS, returns promptId for polling
192
- */
193
- async handleInteractivePrompt(req, res) {
194
- try {
195
- const request = req.body;
196
- // Validate required fields
197
- if (!request.sessionId) {
198
- res.status(400).json({
199
- success: false,
200
- error: 'Missing required field: sessionId',
201
- });
202
- return;
203
- }
204
- if (!request.toolName) {
205
- res.status(400).json({
206
- success: false,
207
- error: 'Missing required field: toolName',
208
- });
209
- return;
210
- }
211
- logger_1.logger.info('Received interactive prompt request', {
212
- sessionId: request.sessionId,
213
- toolName: request.toolName,
214
- });
215
- // Call handler if registered
216
- if (!this.interactivePromptHandler) {
217
- res.status(503).json({
218
- success: false,
219
- error: 'Interactive prompt handler not registered',
220
- });
221
- return;
222
- }
223
- const result = await this.interactivePromptHandler(request);
224
- res.json({
225
- success: true,
226
- promptId: result.promptId,
227
- status: result.status,
228
- });
229
- }
230
- catch (error) {
231
- logger_1.logger.error('Error handling interactive prompt:', error);
232
- res.status(500).json({
233
- success: false,
234
- error: error instanceof Error ? error.message : 'Unknown error',
235
- });
236
- }
237
- }
238
- /**
239
- * Handle polling for prompt response
240
- * Returns decision when mobile user responds
241
- */
242
- handleGetPromptResponse(req, res) {
243
- try {
244
- const { promptId } = req.params;
245
- if (!promptId) {
246
- res.status(400).json({
247
- success: false,
248
- error: 'Missing required parameter: promptId',
249
- });
250
- return;
251
- }
252
- // Call handler if registered
253
- if (!this.getPromptResponseHandler) {
254
- res.status(503).json({
255
- success: false,
256
- error: 'Prompt response handler not registered',
257
- });
258
- return;
259
- }
260
- const response = this.getPromptResponseHandler(promptId);
261
- if (!response) {
262
- res.status(404).json({
263
- success: false,
264
- error: 'Prompt not found or expired',
265
- });
266
- return;
267
- }
268
- res.json({
269
- success: true,
270
- promptId: response.promptId,
271
- decision: response.decision,
272
- reason: response.reason,
273
- });
274
- }
275
- catch (error) {
276
- logger_1.logger.error('Error getting prompt response:', error);
277
- res.status(500).json({
278
- success: false,
279
- error: error instanceof Error ? error.message : 'Unknown error',
280
- });
281
- }
282
- }
283
- transformHookToEvent(hookInput) {
284
- let type;
285
- let content;
286
- const metadata = {
287
- cwd: hookInput.cwd,
288
- hook_event_name: hookInput.hook_event_name,
289
- ...(hookInput.metadata || {}), // Merge any metadata from hook
290
- };
291
- // Check if hook explicitly provides type and content (from Stop hook transcript parsing)
292
- if (hookInput.type && hookInput.content !== undefined) {
293
- type = hookInput.type;
294
- content = hookInput.content;
295
- }
296
- else {
297
- // Determine event type and content based on hook event name
298
- switch (hookInput.hook_event_name) {
299
- case 'SessionStart':
300
- type = types_1.EventType.NOTIFICATION;
301
- content = 'Session started';
302
- metadata.source = hookInput.source;
303
- break;
304
- case 'SessionEnd':
305
- type = types_1.EventType.NOTIFICATION;
306
- content = `Session ended: ${hookInput.reason || 'unknown'}`;
307
- metadata.reason = hookInput.reason;
308
- break;
309
- case 'UserPromptSubmit':
310
- case 'BeforeAgent':
311
- type = types_1.EventType.USER_PROMPT;
312
- content = hookInput.prompt || '';
313
- break;
314
- case 'AfterAgent':
315
- type = types_1.EventType.ASSISTANT_RESPONSE;
316
- content = hookInput.content || hookInput.prompt_response || '';
317
- break;
318
- case 'PostToolUse':
319
- type = types_1.EventType.TOOL_USE;
320
- content = JSON.stringify({
321
- tool_name: hookInput.tool_name,
322
- tool_input: hookInput.tool_input,
323
- tool_response: hookInput.tool_response,
324
- });
325
- metadata.tool_name = hookInput.tool_name;
326
- break;
327
- case 'Notification':
328
- // Check if this is a ToolPermission notification (Edit/Write/Exec approval)
329
- if (hookInput.notification_type === 'ToolPermission' && hookInput.details) {
330
- type = types_1.EventType.INTERACTIVE_PROMPT;
331
- // Map Gemini tool types to iOS-expected names
332
- const toolType = hookInput.details.type || 'edit';
333
- const toolName = this.mapGeminiToolName(toolType);
334
- // Build tool_input to match Claude plugin format
335
- const toolInput = {};
336
- if (toolType === 'exec' || toolType === 'shell') {
337
- // Shell command — extract command for preview
338
- toolInput.command = hookInput.details.command || '';
339
- }
340
- else {
341
- toolInput.file_path = hookInput.details.filePath || hookInput.details.fileName;
342
- // For Edit operations, use fileDiff if available, otherwise fall back to old/new strings
343
- if (toolType === 'edit') {
344
- if (hookInput.details.fileDiff) {
345
- toolInput.old_string = this.extractOldLinesFromDiff(hookInput.details.fileDiff);
346
- toolInput.new_string = this.extractNewLinesFromDiff(hookInput.details.fileDiff);
347
- }
348
- else {
349
- toolInput.old_string = hookInput.details.originalContent || '';
350
- toolInput.new_string = hookInput.details.newContent || '';
351
- }
352
- }
353
- else if (toolType === 'write') {
354
- toolInput.content = hookInput.details.newContent || '';
355
- }
356
- }
357
- // Build content — use rootCommand for exec/shell to match desktop prompt
358
- if (toolType === 'exec' || toolType === 'shell') {
359
- const rootCmd = hookInput.details.rootCommand;
360
- content = rootCmd
361
- ? `Allow execution of: '${rootCmd}'?`
362
- : (hookInput.details.title || `Command: ${toolInput.command}`);
363
- }
364
- else {
365
- const fileName = hookInput.details.fileName || hookInput.details.filePath?.split('/').pop() || 'file';
366
- content = hookInput.details.title || `Gemini wants to ${toolName.toLowerCase()} ${fileName}`;
367
- }
368
- // Build metadata with tool info
369
- // Options will be populated dynamically by server.ts via tmux capture
370
- // Use fallback options here; server.ts will override with parsed options
371
- metadata.tool_name = toolName;
372
- metadata.tool_input = toolInput;
373
- metadata.options = types_1.DEFAULT_TOOL_APPROVAL_OPTIONS;
374
- metadata.instructions = 'Select an option';
375
- metadata.notification_type = hookInput.notification_type;
376
- }
377
- else {
378
- type = types_1.EventType.NOTIFICATION;
379
- content = hookInput.message || '';
380
- metadata.notification_type = hookInput.notification_type;
381
- }
382
- break;
383
- default:
384
- type = types_1.EventType.NOTIFICATION;
385
- content = `Hook event: ${hookInput.hook_event_name}`;
386
- }
387
- }
388
- return {
389
- session_id: hookInput.session_id,
390
- hook_event_name: hookInput.hook_event_name,
391
- type,
392
- source: types_1.EventSource.DESKTOP, // Events from hooks are always from desktop
393
- content,
394
- metadata,
395
- };
396
- }
397
- /**
398
- * Map Gemini CLI tool type names to iOS-expected display names
399
- */
400
- mapGeminiToolName(toolType) {
401
- switch (toolType.toLowerCase()) {
402
- case 'exec':
403
- case 'shell':
404
- return 'Bash';
405
- case 'edit':
406
- case 'edit_file':
407
- return 'Edit';
408
- case 'write':
409
- case 'write_file':
410
- return 'Write';
411
- case 'read':
412
- case 'read_file':
413
- return 'Read';
414
- default:
415
- return toolType;
416
- }
417
- }
418
- /**
419
- * Extract old (removed) lines from a unified diff format
420
- * Lines starting with '-' (but not '---') are removed lines
421
- */
422
- extractOldLinesFromDiff(diff) {
423
- const lines = diff.split('\n');
424
- const oldLines = [];
425
- for (const line of lines) {
426
- // Skip diff headers
427
- if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('Index:') || line.startsWith('===')) {
428
- continue;
429
- }
430
- // Skip hunk headers like @@ -1,5 +1,7 @@
431
- if (line.startsWith('@@')) {
432
- continue;
433
- }
434
- // Lines starting with '-' are removed lines
435
- if (line.startsWith('-')) {
436
- oldLines.push(line.substring(1)); // Remove the '-' prefix
437
- }
438
- // Context lines (no prefix or ' ' prefix) are part of both old and new
439
- else if (!line.startsWith('+') && line.length > 0) {
440
- // Include context lines that start with ' ' (remove the space)
441
- if (line.startsWith(' ')) {
442
- oldLines.push(line.substring(1));
443
- }
444
- else {
445
- oldLines.push(line);
446
- }
447
- }
448
- }
449
- return oldLines.join('\n');
450
- }
451
- /**
452
- * Extract new (added) lines from a unified diff format
453
- * Lines starting with '+' (but not '+++') are added lines
454
- */
455
- extractNewLinesFromDiff(diff) {
456
- const lines = diff.split('\n');
457
- const newLines = [];
458
- for (const line of lines) {
459
- // Skip diff headers
460
- if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('Index:') || line.startsWith('===')) {
461
- continue;
462
- }
463
- // Skip hunk headers like @@ -1,5 +1,7 @@
464
- if (line.startsWith('@@')) {
465
- continue;
466
- }
467
- // Lines starting with '+' are added lines
468
- if (line.startsWith('+')) {
469
- newLines.push(line.substring(1)); // Remove the '+' prefix
470
- }
471
- // Context lines (no prefix or ' ' prefix) are part of both old and new
472
- else if (!line.startsWith('-') && line.length > 0) {
473
- // Include context lines that start with ' ' (remove the space)
474
- if (line.startsWith(' ')) {
475
- newLines.push(line.substring(1));
476
- }
477
- else {
478
- newLines.push(line);
479
- }
480
- }
481
- }
482
- return newLines.join('\n');
483
- }
484
- // Register event handler
485
- onEvent(handler) {
486
- this.eventHandler = handler;
487
- }
488
- // Register interactive prompt handler
489
- onInteractivePrompt(handler) {
490
- this.interactivePromptHandler = handler;
491
- }
492
- // Register prompt response getter
493
- onGetPromptResponse(handler) {
494
- this.getPromptResponseHandler = handler;
495
- }
496
- // Start the HTTP server with dynamic port allocation
497
- async start(sessionId) {
498
- // Use provided sessionId or the one set earlier
499
- const sid = sessionId || this.sessionId;
500
- if (sid) {
501
- this.sessionId = sid;
502
- }
503
- return new Promise((resolve, reject) => {
504
- try {
505
- const config = (0, codevibe_core_1.getConfig)();
506
- // Use port 0 for dynamic allocation, or configured port as fallback
507
- const requestedPort = config.server.dynamicPort ? 0 : config.server.port;
508
- this.server = this.app.listen(requestedPort, config.server.host, () => {
509
- const address = this.server.address();
510
- this.assignedPort = address.port;
511
- logger_1.logger.info(`HTTP API listening on http://${config.server.host}:${this.assignedPort}`);
512
- // Write port to session-specific file for hooks to discover
513
- if (this.sessionId) {
514
- this.writePortFile(this.sessionId, this.assignedPort);
515
- }
516
- resolve(this.assignedPort);
517
- });
518
- this.server.on('error', (error) => {
519
- logger_1.logger.error('HTTP server error:', error);
520
- reject(error);
521
- });
522
- }
523
- catch (error) {
524
- reject(error);
525
- }
526
- });
527
- }
528
- /**
529
- * Write port to a session-specific file for hooks to discover
530
- */
531
- writePortFile(sessionId, port) {
532
- const portFilePath = path.join(os.tmpdir(), `codevibe-gemini-${sessionId}.port`);
533
- try {
534
- fs.writeFileSync(portFilePath, port.toString());
535
- logger_1.logger.info(`Port file written: ${portFilePath} -> ${port}`);
536
- }
537
- catch (error) {
538
- logger_1.logger.error(`Failed to write port file: ${portFilePath}`, error);
539
- }
540
- }
541
- /**
542
- * Remove the port file on shutdown
543
- */
544
- removePortFile() {
545
- if (this.sessionId) {
546
- const portFilePath = path.join(os.tmpdir(), `codevibe-gemini-${this.sessionId}.port`);
547
- try {
548
- if (fs.existsSync(portFilePath)) {
549
- fs.unlinkSync(portFilePath);
550
- logger_1.logger.info(`Port file removed: ${portFilePath}`);
551
- }
552
- }
553
- catch (error) {
554
- logger_1.logger.warn(`Failed to remove port file: ${portFilePath}`, error);
555
- }
556
- }
557
- }
558
- // Stop the HTTP server
559
- async stop() {
560
- return new Promise((resolve, reject) => {
561
- // Remove port file first
562
- this.removePortFile();
563
- if (this.server) {
564
- this.server.close((err) => {
565
- if (err) {
566
- logger_1.logger.error('Error stopping HTTP server:', err);
567
- reject(err);
568
- }
569
- else {
570
- logger_1.logger.info('HTTP API stopped');
571
- resolve();
572
- }
573
- });
574
- }
575
- else {
576
- resolve();
577
- }
578
- });
579
- }
580
- }
581
- exports.HttpApi = HttpApi;
582
- //# sourceMappingURL=http-api.js.map