n8n-nodes-github-copilot 3.0.0 → 3.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.
@@ -31,7 +31,7 @@ async function makeApiRequest(context, endpoint, body) {
31
31
  headers: {
32
32
  'Authorization': `Bearer ${credentials.accessToken}`,
33
33
  'Content-Type': 'application/json',
34
- 'User-Agent': 'n8n-github-copilot-chat/2.0.0',
34
+ 'User-Agent': 'n8n-github-copilot-chat/3.0.0',
35
35
  },
36
36
  body: JSON.stringify(body),
37
37
  };
@@ -42,6 +42,109 @@ async function makeApiRequest(context, endpoint, body) {
42
42
  }
43
43
  return await response.json();
44
44
  }
45
+ async function downloadFileFromUrl(url) {
46
+ const response = await fetch(url);
47
+ if (!response.ok) {
48
+ throw new Error(`Failed to download file from URL: ${response.status} ${response.statusText}`);
49
+ }
50
+ return Buffer.from(await response.arrayBuffer());
51
+ }
52
+ async function getFileFromBinary(context, itemIndex, propertyName) {
53
+ const items = context.getInputData();
54
+ const item = items[itemIndex];
55
+ if (!item.binary || !item.binary[propertyName]) {
56
+ throw new Error(`No binary data found in property "${propertyName}"`);
57
+ }
58
+ const binaryData = item.binary[propertyName];
59
+ if (binaryData.data) {
60
+ return Buffer.from(binaryData.data, 'base64');
61
+ }
62
+ else if (binaryData.id) {
63
+ return await context.helpers.getBinaryDataBuffer(itemIndex, propertyName);
64
+ }
65
+ else {
66
+ throw new Error(`Invalid binary data format in property "${propertyName}"`);
67
+ }
68
+ }
69
+ async function processAudioFile(context, itemIndex) {
70
+ const audioSource = context.getNodeParameter('audioSource', itemIndex);
71
+ let audioBuffer;
72
+ let filename = 'audio.mp3';
73
+ switch (audioSource) {
74
+ case 'manual':
75
+ const audioFile = context.getNodeParameter('audioFile', itemIndex);
76
+ if (audioFile.startsWith('data:audio/')) {
77
+ const base64Data = audioFile.split(',')[1];
78
+ audioBuffer = Buffer.from(base64Data, 'base64');
79
+ }
80
+ else if (audioFile.match(/^[A-Za-z0-9+/=]+$/)) {
81
+ audioBuffer = Buffer.from(audioFile, 'base64');
82
+ }
83
+ else {
84
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
85
+ audioBuffer = fs.readFileSync(audioFile);
86
+ filename = audioFile.split(/[\\\/]/).pop() || 'audio.mp3';
87
+ }
88
+ break;
89
+ case 'url':
90
+ const audioUrl = context.getNodeParameter('audioUrl', itemIndex);
91
+ audioBuffer = await downloadFileFromUrl(audioUrl);
92
+ filename = audioUrl.split('/').pop() || 'audio.mp3';
93
+ break;
94
+ case 'binary':
95
+ const audioBinaryProperty = context.getNodeParameter('audioBinaryProperty', itemIndex);
96
+ audioBuffer = await getFileFromBinary(context, itemIndex, audioBinaryProperty);
97
+ const items = context.getInputData();
98
+ const item = items[itemIndex];
99
+ if (item.binary && item.binary[audioBinaryProperty] && item.binary[audioBinaryProperty].fileName) {
100
+ filename = item.binary[audioBinaryProperty].fileName || 'audio.mp3';
101
+ }
102
+ break;
103
+ default:
104
+ throw new Error(`Unknown audio source: ${audioSource}`);
105
+ }
106
+ const mimeType = getAudioMimeType(filename);
107
+ return `data:${mimeType};base64,${audioBuffer.toString('base64')}`;
108
+ }
109
+ async function processImageFile(context, itemIndex) {
110
+ const imageSource = context.getNodeParameter('imageSource', itemIndex);
111
+ let imageBuffer;
112
+ let filename = 'image.jpg';
113
+ switch (imageSource) {
114
+ case 'manual':
115
+ const imageFile = context.getNodeParameter('imageFile', itemIndex);
116
+ if (imageFile.startsWith('data:image/')) {
117
+ return imageFile;
118
+ }
119
+ else if (imageFile.match(/^[A-Za-z0-9+/=]+$/)) {
120
+ imageBuffer = Buffer.from(imageFile, 'base64');
121
+ }
122
+ else {
123
+ const fs = await Promise.resolve().then(() => __importStar(require('fs')));
124
+ imageBuffer = fs.readFileSync(imageFile);
125
+ filename = imageFile.split(/[\\\/]/).pop() || 'image.jpg';
126
+ }
127
+ break;
128
+ case 'url':
129
+ const imageUrl = context.getNodeParameter('imageUrl', itemIndex);
130
+ imageBuffer = await downloadFileFromUrl(imageUrl);
131
+ filename = imageUrl.split('/').pop() || 'image.jpg';
132
+ break;
133
+ case 'binary':
134
+ const imageBinaryProperty = context.getNodeParameter('imageBinaryProperty', itemIndex);
135
+ imageBuffer = await getFileFromBinary(context, itemIndex, imageBinaryProperty);
136
+ const items = context.getInputData();
137
+ const item = items[itemIndex];
138
+ if (item.binary && item.binary[imageBinaryProperty] && item.binary[imageBinaryProperty].fileName) {
139
+ filename = item.binary[imageBinaryProperty].fileName || 'image.jpg';
140
+ }
141
+ break;
142
+ default:
143
+ throw new Error(`Unknown image source: ${imageSource}`);
144
+ }
145
+ const mimeType = getMimeType(filename);
146
+ return `data:${mimeType};base64,${imageBuffer.toString('base64')}`;
147
+ }
45
148
  function getMimeType(filename) {
46
149
  const ext = filename.toLowerCase().split('.').pop();
47
150
  switch (ext) {
@@ -58,6 +161,25 @@ function getMimeType(filename) {
58
161
  return 'image/jpeg';
59
162
  }
60
163
  }
164
+ function getAudioMimeType(filename) {
165
+ const ext = filename.toLowerCase().split('.').pop();
166
+ switch (ext) {
167
+ case 'mp3':
168
+ return 'audio/mpeg';
169
+ case 'wav':
170
+ return 'audio/wav';
171
+ case 'm4a':
172
+ return 'audio/mp4';
173
+ case 'flac':
174
+ return 'audio/flac';
175
+ case 'ogg':
176
+ return 'audio/ogg';
177
+ case 'aac':
178
+ return 'audio/aac';
179
+ default:
180
+ return 'audio/mpeg';
181
+ }
182
+ }
61
183
  class GitHubCopilotChatAPI {
62
184
  constructor() {
63
185
  this.description = {
@@ -158,6 +280,23 @@ class GitHubCopilotChatAPI {
158
280
  },
159
281
  },
160
282
  },
283
+ {
284
+ displayName: 'Audio Source',
285
+ name: 'audioSource',
286
+ type: 'options',
287
+ options: [
288
+ { name: 'File Path or Base64', value: 'manual' },
289
+ { name: 'Download from URL', value: 'url' },
290
+ { name: 'From Previous Node Binary', value: 'binary' },
291
+ ],
292
+ default: 'manual',
293
+ description: 'Choose how to provide the audio file',
294
+ displayOptions: {
295
+ show: {
296
+ operation: ['audioTranscription'],
297
+ },
298
+ },
299
+ },
161
300
  {
162
301
  displayName: 'Audio File',
163
302
  name: 'audioFile',
@@ -168,6 +307,35 @@ class GitHubCopilotChatAPI {
168
307
  displayOptions: {
169
308
  show: {
170
309
  operation: ['audioTranscription'],
310
+ audioSource: ['manual'],
311
+ },
312
+ },
313
+ },
314
+ {
315
+ displayName: 'Audio URL',
316
+ name: 'audioUrl',
317
+ type: 'string',
318
+ default: '',
319
+ placeholder: 'https://example.com/audio.mp3',
320
+ description: 'URL to download audio file from',
321
+ displayOptions: {
322
+ show: {
323
+ operation: ['audioTranscription'],
324
+ audioSource: ['url'],
325
+ },
326
+ },
327
+ },
328
+ {
329
+ displayName: 'Binary Property Name',
330
+ name: 'audioBinaryProperty',
331
+ type: 'string',
332
+ default: 'data',
333
+ placeholder: 'data',
334
+ description: 'Name of the binary property containing the audio file',
335
+ displayOptions: {
336
+ show: {
337
+ operation: ['audioTranscription'],
338
+ audioSource: ['binary'],
171
339
  },
172
340
  },
173
341
  },
@@ -194,6 +362,23 @@ class GitHubCopilotChatAPI {
194
362
  },
195
363
  },
196
364
  },
365
+ {
366
+ displayName: 'Image Source',
367
+ name: 'imageSource',
368
+ type: 'options',
369
+ options: [
370
+ { name: 'File Path or Base64', value: 'manual' },
371
+ { name: 'Download from URL', value: 'url' },
372
+ { name: 'From Previous Node Binary', value: 'binary' },
373
+ ],
374
+ default: 'manual',
375
+ description: 'Choose how to provide the image file',
376
+ displayOptions: {
377
+ show: {
378
+ operation: ['imageAnalysis'],
379
+ },
380
+ },
381
+ },
197
382
  {
198
383
  displayName: 'Image File',
199
384
  name: 'imageFile',
@@ -204,6 +389,35 @@ class GitHubCopilotChatAPI {
204
389
  displayOptions: {
205
390
  show: {
206
391
  operation: ['imageAnalysis'],
392
+ imageSource: ['manual'],
393
+ },
394
+ },
395
+ },
396
+ {
397
+ displayName: 'Image URL',
398
+ name: 'imageUrl',
399
+ type: 'string',
400
+ default: '',
401
+ placeholder: 'https://example.com/image.jpg',
402
+ description: 'URL to download image file from',
403
+ displayOptions: {
404
+ show: {
405
+ operation: ['imageAnalysis'],
406
+ imageSource: ['url'],
407
+ },
408
+ },
409
+ },
410
+ {
411
+ displayName: 'Binary Property Name',
412
+ name: 'imageBinaryProperty',
413
+ type: 'string',
414
+ default: 'data',
415
+ placeholder: 'data',
416
+ description: 'Name of the binary property containing the image file',
417
+ displayOptions: {
418
+ show: {
419
+ operation: ['imageAnalysis'],
420
+ imageSource: ['binary'],
207
421
  },
208
422
  },
209
423
  },
@@ -294,8 +508,8 @@ class GitHubCopilotChatAPI {
294
508
  };
295
509
  }
296
510
  else if (operation === 'audioTranscription') {
297
- const audioFile = this.getNodeParameter('audioFile', i);
298
511
  const language = this.getNodeParameter('audioLanguage', i);
512
+ const audioDataUrl = await processAudioFile(this, i);
299
513
  const transcriptionPrompt = language === 'auto'
300
514
  ? `Please transcribe this audio file to text. Detect the language automatically and provide the transcription.`
301
515
  : `Please transcribe this audio file to text. The audio is in ${language} language.`;
@@ -306,7 +520,7 @@ class GitHubCopilotChatAPI {
306
520
  },
307
521
  {
308
522
  role: 'user',
309
- content: `${transcriptionPrompt}\n\nAudio file: ${audioFile}`
523
+ content: `${transcriptionPrompt}\n\nAudio file: ${audioDataUrl}`
310
524
  }
311
525
  ];
312
526
  const requestBody = {
@@ -327,23 +541,10 @@ class GitHubCopilotChatAPI {
327
541
  }
328
542
  else if (operation === 'imageAnalysis') {
329
543
  const message = this.getNodeParameter('message', i);
330
- const imageFile = this.getNodeParameter('imageFile', i);
331
544
  const systemPrompt = this.getNodeParameter('systemPrompt', i);
332
545
  const temperature = this.getNodeParameter('temperature', i);
333
546
  const maxTokens = this.getNodeParameter('maxTokens', i);
334
- let imageBase64;
335
- if (imageFile.startsWith('data:image/')) {
336
- imageBase64 = imageFile;
337
- }
338
- else if (imageFile.match(/^[A-Za-z0-9+/=]+$/)) {
339
- imageBase64 = `data:image/jpeg;base64,${imageFile}`;
340
- }
341
- else {
342
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
343
- const imageBuffer = fs.readFileSync(imageFile);
344
- const mimeType = getMimeType(imageFile);
345
- imageBase64 = `data:${mimeType};base64,${imageBuffer.toString('base64')}`;
346
- }
547
+ const imageBase64 = await processImageFile(this, i);
347
548
  const messages = [];
348
549
  if (systemPrompt) {
349
550
  messages.push({ role: 'system', content: systemPrompt });
@@ -0,0 +1,34 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
2
+ <defs>
3
+ <linearGradient id="copilotGradient" x1="0%" y1="0%" x2="100%" y2="100%">
4
+ <stop offset="0%" style="stop-color:#1f6feb;stop-opacity:1" />
5
+ <stop offset="100%" style="stop-color:#0969da;stop-opacity:1" />
6
+ </linearGradient>
7
+ </defs>
8
+
9
+ <!-- GitHub Copilot inspired icon -->
10
+ <circle cx="12" cy="12" r="11" fill="url(#copilotGradient)" stroke="#ffffff" stroke-width="1"/>
11
+
12
+ <!-- Copilot face -->
13
+ <ellipse cx="12" cy="10" rx="8" ry="6" fill="#ffffff" opacity="0.9"/>
14
+
15
+ <!-- Eyes -->
16
+ <circle cx="9" cy="9" r="1.5" fill="#1f6feb"/>
17
+ <circle cx="15" cy="9" r="1.5" fill="#1f6feb"/>
18
+
19
+ <!-- Light reflection in eyes -->
20
+ <circle cx="9.5" cy="8.5" r="0.5" fill="#ffffff"/>
21
+ <circle cx="15.5" cy="8.5" r="0.5" fill="#ffffff"/>
22
+
23
+ <!-- Mouth/Interface line -->
24
+ <path d="M8 12 L16 12" stroke="#1f6feb" stroke-width="1.5" stroke-linecap="round"/>
25
+
26
+ <!-- Code brackets -->
27
+ <path d="M6 15 L8 17 L6 19" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
28
+ <path d="M18 15 L16 17 L18 19" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
29
+
30
+ <!-- AI indicator dots -->
31
+ <circle cx="10" cy="17" r="0.5" fill="#ffffff" opacity="0.8"/>
32
+ <circle cx="12" cy="17" r="0.5" fill="#ffffff" opacity="0.6"/>
33
+ <circle cx="14" cy="17" r="0.5" fill="#ffffff" opacity="0.4"/>
34
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-github-copilot",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "n8n community node for GitHub Copilot with CLI integration and official Chat API access to GPT-5, Claude, Gemini and more using your existing Copilot credits",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/sufficit/n8n-nodes-github-copilot",