neoagent 2.3.0 → 2.3.1-beta.10

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.
Files changed (60) hide show
  1. package/.env.example +13 -0
  2. package/README.md +3 -1
  3. package/docs/automation.md +1 -1
  4. package/docs/capabilities.md +2 -2
  5. package/docs/configuration.md +14 -1
  6. package/docs/integrations.md +6 -1
  7. package/lib/manager.js +127 -1
  8. package/package.json +2 -1
  9. package/server/db/database.js +68 -0
  10. package/server/http/middleware.js +50 -0
  11. package/server/http/routes.js +3 -1
  12. package/server/index.js +1 -0
  13. package/server/public/.last_build_id +1 -1
  14. package/server/public/assets/NOTICES +61 -0
  15. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  16. package/server/public/flutter_bootstrap.js +1 -1
  17. package/server/public/main.dart.js +61049 -60444
  18. package/server/routes/integrations.js +97 -8
  19. package/server/routes/memory.js +11 -2
  20. package/server/routes/screenHistory.js +46 -0
  21. package/server/routes/triggers.js +81 -0
  22. package/server/services/ai/engine.js +9 -0
  23. package/server/services/ai/models.js +30 -0
  24. package/server/services/ai/providers/githubCopilot.js +97 -0
  25. package/server/services/ai/providers/openaiCodex.js +26 -0
  26. package/server/services/ai/settings.js +20 -0
  27. package/server/services/ai/systemPrompt.js +1 -1
  28. package/server/services/ai/tools.js +150 -11
  29. package/server/services/browser/controller.js +47 -3
  30. package/server/services/desktop/screenRecorder.js +126 -0
  31. package/server/services/integrations/env.js +19 -0
  32. package/server/services/integrations/github/common.js +106 -0
  33. package/server/services/integrations/github/provider.js +499 -0
  34. package/server/services/integrations/github/repos.js +1124 -0
  35. package/server/services/integrations/home_assistant/provider.js +630 -0
  36. package/server/services/integrations/manager.js +63 -7
  37. package/server/services/integrations/oauth_provider.js +13 -6
  38. package/server/services/integrations/provider_config_store.js +76 -0
  39. package/server/services/integrations/registry.js +10 -0
  40. package/server/services/integrations/spotify/provider.js +487 -0
  41. package/server/services/integrations/weather/provider.js +559 -0
  42. package/server/services/integrations/whatsapp/provider.js +6 -2
  43. package/server/services/manager.js +22 -0
  44. package/server/services/memory/manager.js +39 -2
  45. package/server/services/messaging/manager.js +29 -7
  46. package/server/services/skills/base_catalog.js +33 -0
  47. package/server/services/tasks/adapters/index.js +2 -0
  48. package/server/services/tasks/adapters/manual.js +12 -0
  49. package/server/services/tasks/adapters/schedule.js +33 -5
  50. package/server/services/tasks/adapters/weather_event.js +84 -0
  51. package/server/services/tasks/integration_runtime.js +85 -0
  52. package/server/services/tasks/runtime.js +2 -2
  53. package/server/services/voice/agentBridge.js +20 -4
  54. package/server/services/voice/message.js +3 -0
  55. package/server/services/voice/openaiClient.js +4 -1
  56. package/server/services/voice/providers.js +2 -1
  57. package/server/services/voice/runtimeManager.js +136 -1
  58. package/server/services/widgets/service.js +49 -4
  59. package/server/utils/local_secrets.js +56 -0
  60. package/server/utils/logger.js +37 -9
@@ -159,6 +159,9 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
159
159
  if (!session) {
160
160
  return res.status(404).send('Connection session not found.');
161
161
  }
162
+ const provider = manager.getProvider(req.params.provider);
163
+ const providerLabel = provider?.label || req.params.provider;
164
+ const appLabel = provider?.getApp?.(session.appKey)?.label || session.appKey || 'account';
162
165
  const trustedOrigin = JSON.stringify(getTrustedPostMessageOrigin(req));
163
166
  const statusUrl = `/api/integrations/${encodeURIComponent(req.params.provider)}/connect/${encodeURIComponent(req.params.sessionId)}/status?agentId=${encodeURIComponent(agentId)}`;
164
167
  res.send(`
@@ -166,7 +169,7 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
166
169
  <head>
167
170
  <meta charset="utf-8" />
168
171
  <meta name="viewport" content="width=device-width, initial-scale=1" />
169
- <title>Connect ${escapeHtml(req.params.provider)}</title>
172
+ <title>Connect ${escapeHtml(providerLabel)}</title>
170
173
  <style>
171
174
  body { font-family: ui-sans-serif, system-ui, sans-serif; background: #0b1220; color: #f8fafc; margin: 0; padding: 24px; }
172
175
  .card { max-width: 560px; margin: 0 auto; background: #111827; border: 1px solid #1f2937; border-radius: 20px; padding: 24px; }
@@ -179,11 +182,11 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
179
182
  <body>
180
183
  <div class="card">
181
184
  <div class="pill">Official integration</div>
182
- <h1>Connect WhatsApp</h1>
183
- <p class="muted">This link is isolated from the separate messaging-platform WhatsApp bridge. Scan the QR code with your personal WhatsApp account to finish linking.</p>
185
+ <h1>Connect ${escapeHtml(providerLabel)}</h1>
186
+ <p class="muted">Complete connection for ${escapeHtml(appLabel)}. This window closes automatically when linking is finished.</p>
184
187
  <div id="status" class="muted">Starting connection…</div>
185
- <img id="qr" alt="WhatsApp QR code" style="display:none;" />
186
- <p class="muted">When the link finishes, this window will close automatically. If it does not, you can close it manually.</p>
188
+ <img id="qr" alt="Integration QR code" style="display:none;" />
189
+ <p class="muted">If this flow needs QR scan approval, the code will appear below.</p>
187
190
  </div>
188
191
  <script>
189
192
  const statusEl = document.getElementById('status');
@@ -207,12 +210,12 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
207
210
  const data = await response.json();
208
211
  const status = String(data.status || 'connecting');
209
212
  if (status === 'awaiting_qr' && data.qr) {
210
- statusEl.textContent = 'Scan this QR code with WhatsApp on your phone.';
213
+ statusEl.textContent = 'Scan this QR code to continue linking.';
211
214
  qrEl.src = 'https://api.qrserver.com/v1/create-qr-code/?data=' + encodeURIComponent(data.qr) + '&size=320x320';
212
215
  qrEl.style.display = 'block';
213
216
  } else if (status === 'connected') {
214
217
  qrEl.style.display = 'none';
215
- statusEl.textContent = 'Connected as ' + (data.accountEmail || 'your WhatsApp account') + '. Closing…';
218
+ statusEl.textContent = 'Connected as ' + (data.accountEmail || 'your account') + '. Closing…';
216
219
  notifyOpener({
217
220
  type: 'integration_oauth_success',
218
221
  provider,
@@ -233,7 +236,7 @@ router.get('/:provider/connect/:sessionId', (req, res) => {
233
236
  });
234
237
  return;
235
238
  } else {
236
- statusEl.textContent = 'Waiting for WhatsApp to generate a QR code…';
239
+ statusEl.textContent = 'Waiting for the integration to finish linking…';
237
240
  }
238
241
  setTimeout(refresh, 1500);
239
242
  }
@@ -358,4 +361,90 @@ router.get('/:provider/tools/status', (req, res) => {
358
361
  }
359
362
  });
360
363
 
364
+ router.get('/:provider/config', (req, res) => {
365
+ try {
366
+ const manager = getIntegrationManager(req);
367
+ if (!manager) {
368
+ throw new Error('Official integration manager is not available on app.locals.integrationManager.');
369
+ }
370
+ const provider = manager.getProvider(req.params.provider);
371
+ if (!provider) {
372
+ throw new Error(`Unknown integration provider: ${req.params.provider}`);
373
+ }
374
+ if (typeof provider.getUserConfig !== 'function') {
375
+ return res.status(404).json({
376
+ error: `${provider.label} does not support per-user configuration.`,
377
+ });
378
+ }
379
+ const config = provider.getUserConfig({
380
+ userId: req.session.userId,
381
+ agentId: resolveAgentId(req.session.userId, getAgentIdFromRequest(req)),
382
+ });
383
+ res.json({
384
+ provider: provider.key,
385
+ config,
386
+ });
387
+ } catch (err) {
388
+ res.status(400).json({ error: sanitizeError(err) });
389
+ }
390
+ });
391
+
392
+ router.put('/:provider/config', async (req, res) => {
393
+ try {
394
+ const manager = getIntegrationManager(req);
395
+ if (!manager) {
396
+ throw new Error('Official integration manager is not available on app.locals.integrationManager.');
397
+ }
398
+ const provider = manager.getProvider(req.params.provider);
399
+ if (!provider) {
400
+ throw new Error(`Unknown integration provider: ${req.params.provider}`);
401
+ }
402
+ if (typeof provider.saveUserConfig !== 'function') {
403
+ return res.status(404).json({
404
+ error: `${provider.label} does not support per-user configuration.`,
405
+ });
406
+ }
407
+ const config = await provider.saveUserConfig({
408
+ userId: req.session.userId,
409
+ agentId: resolveAgentId(req.session.userId, getAgentIdFromRequest(req)),
410
+ config: req.body?.config || req.body || {},
411
+ });
412
+ res.json({
413
+ provider: provider.key,
414
+ config,
415
+ saved: true,
416
+ });
417
+ } catch (err) {
418
+ res.status(400).json({ error: sanitizeError(err) });
419
+ }
420
+ });
421
+
422
+ router.delete('/:provider/config', async (req, res) => {
423
+ try {
424
+ const manager = getIntegrationManager(req);
425
+ if (!manager) {
426
+ throw new Error('Official integration manager is not available on app.locals.integrationManager.');
427
+ }
428
+ const provider = manager.getProvider(req.params.provider);
429
+ if (!provider) {
430
+ throw new Error(`Unknown integration provider: ${req.params.provider}`);
431
+ }
432
+ if (typeof provider.clearUserConfig !== 'function') {
433
+ return res.status(404).json({
434
+ error: `${provider.label} does not support per-user configuration.`,
435
+ });
436
+ }
437
+ await provider.clearUserConfig({
438
+ userId: req.session.userId,
439
+ agentId: resolveAgentId(req.session.userId, getAgentIdFromRequest(req)),
440
+ });
441
+ res.json({
442
+ provider: provider.key,
443
+ cleared: true,
444
+ });
445
+ } catch (err) {
446
+ res.status(400).json({ error: sanitizeError(err) });
447
+ }
448
+ });
449
+
361
450
  module.exports = router;
@@ -1,11 +1,20 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
+ const rateLimit = require('express-rate-limit');
3
4
  const { requireAuth } = require('../middleware/auth');
4
5
  const { sanitizeError } = require('../utils/security');
5
6
  const { getAgentIdFromRequest, resolveAgentId } = require('../services/agents/manager');
6
7
 
7
8
  router.use(requireAuth);
8
9
 
10
+ const apiKeyMutationLimiter = rateLimit({
11
+ windowMs: 15 * 60 * 1000,
12
+ max: 60,
13
+ message: { error: 'Too many API key update attempts, try again later' },
14
+ standardHeaders: true,
15
+ legacyHeaders: false,
16
+ });
17
+
9
18
  function normalizeMemoryIds(value) {
10
19
  return [...new Set(
11
20
  (Array.isArray(value) ? value : [])
@@ -232,12 +241,12 @@ router.get('/api-keys', (req, res) => {
232
241
  res.json(masked);
233
242
  });
234
243
 
235
- router.put('/api-keys/:service', (req, res) => {
244
+ router.put('/api-keys/:service', apiKeyMutationLimiter, (req, res) => {
236
245
  req.app.locals.memoryManager.setApiKey(req.params.service, req.body.key, req.session.userId);
237
246
  res.json({ success: true });
238
247
  });
239
248
 
240
- router.delete('/api-keys/:service', (req, res) => {
249
+ router.delete('/api-keys/:service', apiKeyMutationLimiter, (req, res) => {
241
250
  req.app.locals.memoryManager.deleteApiKey(req.params.service, req.session.userId);
242
251
  res.json({ success: true });
243
252
  });
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const db = require('../db/database');
5
+ const { getErrorMessage } = require('../services/bootstrap_helpers');
6
+
7
+ const router = express.Router();
8
+
9
+ router.get('/search', (req, res) => {
10
+ const { q, limit = 50, offset = 0 } = req.query;
11
+
12
+ if (!req.user || !req.user.id) {
13
+ return res.status(401).json({ error: 'Unauthorized' });
14
+ }
15
+
16
+ try {
17
+ let results = [];
18
+ if (q) {
19
+ // Full text search
20
+ results = db.prepare(`
21
+ SELECT s.id, s.timestamp, s.app_name, s.text_content
22
+ FROM screen_history_fts fts
23
+ JOIN screen_history s ON fts.rowid = s.id
24
+ WHERE screen_history_fts MATCH ? AND s.user_id = ?
25
+ ORDER BY s.timestamp DESC
26
+ LIMIT ? OFFSET ?
27
+ `).all(q, req.user.id, Number(limit), Number(offset));
28
+ } else {
29
+ // Recent history
30
+ results = db.prepare(`
31
+ SELECT id, timestamp, app_name, text_content
32
+ FROM screen_history
33
+ WHERE user_id = ?
34
+ ORDER BY timestamp DESC
35
+ LIMIT ? OFFSET ?
36
+ `).all(req.user.id, Number(limit), Number(offset));
37
+ }
38
+
39
+ res.json({ results });
40
+ } catch (err) {
41
+ console.error('[ScreenHistory] Search error:', getErrorMessage(err));
42
+ res.status(500).json({ error: 'Failed to search screen history' });
43
+ }
44
+ });
45
+
46
+ module.exports = router;
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const db = require('../db/database');
5
+ const { getErrorMessage } = require('../services/bootstrap_helpers');
6
+ const { requireAuth } = require('../middleware/auth');
7
+
8
+ const router = express.Router();
9
+
10
+ router.use(requireAuth);
11
+
12
+ router.post('/geofence', async (req, res) => {
13
+ const { label, latitude, longitude, radius_meters, action } = req.body;
14
+
15
+ try {
16
+ const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.user.id);
17
+ if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
18
+
19
+ console.log(`[Triggers] Geofence entered: ${label} by user ${req.user.id}`);
20
+
21
+ // If an agentEngine is running, we can inject a prompt to process this context
22
+ const agentEngine = req.app.locals.agentEngine;
23
+ if (agentEngine) {
24
+ // Find active agent or use default
25
+ const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.user.id)?.id;
26
+
27
+ if (defaultAgentId) {
28
+ // Fire and forget a trigger message to the agent
29
+ agentEngine.handleBackgroundTrigger(req.user.id, defaultAgentId, {
30
+ source: 'geofence',
31
+ label,
32
+ action: action || 'User entered a geofenced area. Check if there are any active reminders or tasks related to this location.'
33
+ }).catch(err => console.error('[Triggers] Agent evaluation failed:', err));
34
+ }
35
+ }
36
+
37
+ res.json({ success: true, message: 'Geofence trigger processed' });
38
+ } catch (err) {
39
+ console.error('[Triggers] Geofence error:', getErrorMessage(err));
40
+ res.status(500).json({ error: 'Failed to process geofence trigger' });
41
+ }
42
+ });
43
+
44
+ router.post('/notification', async (req, res) => {
45
+ const { app_package, title, body, action_taken } = req.body;
46
+
47
+ try {
48
+ const userRow = db.prepare('SELECT id FROM users WHERE id = ?').get(req.user.id);
49
+ if (!userRow) return res.status(401).json({ error: 'Unauthorized' });
50
+
51
+ console.log(`[Triggers] Notification received: ${app_package} - ${title}`);
52
+
53
+ db.prepare(`
54
+ INSERT INTO notification_history (user_id, app_package, title, body, action_taken)
55
+ VALUES (?, ?, ?, ?, ?)
56
+ `).run(req.user.id, app_package || 'unknown', title || '', body || '', action_taken || 'none');
57
+
58
+ // Notify agent engine to proactively evaluate the notification
59
+ const agentEngine = req.app.locals.agentEngine;
60
+ if (agentEngine) {
61
+ const defaultAgentId = db.prepare('SELECT id FROM agents WHERE user_id = ? ORDER BY is_default DESC LIMIT 1').get(req.user.id)?.id;
62
+
63
+ if (defaultAgentId) {
64
+ agentEngine.handleBackgroundTrigger(req.user.id, defaultAgentId, {
65
+ source: 'notification',
66
+ app_package,
67
+ title,
68
+ body,
69
+ instruction: 'Evaluate this notification. If it is an important reminder, calendar event, or urgent message, inform the user or take appropriate action.'
70
+ }).catch(err => console.error('[Triggers] Agent evaluation failed:', err));
71
+ }
72
+ }
73
+
74
+ res.json({ success: true, message: 'Notification trigger processed and stored' });
75
+ } catch (err) {
76
+ console.error('[Triggers] Notification error:', getErrorMessage(err));
77
+ res.status(500).json({ error: 'Failed to process notification trigger' });
78
+ }
79
+ });
80
+
81
+ module.exports = router;
@@ -686,6 +686,7 @@ class AgentEngine {
686
686
  content,
687
687
  kind,
688
688
  expectsReply = false,
689
+ deferFollowUp = false,
689
690
  } = {}) {
690
691
  const runMeta = this.getRunMeta(runId);
691
692
  if (!runMeta || runMeta.aborted) {
@@ -715,6 +716,9 @@ class AgentEngine {
715
716
  kind: normalizedKind,
716
717
  expectsReply,
717
718
  });
719
+ if (deferFollowUp === true) {
720
+ metadata.defer_follow_up = true;
721
+ }
718
722
  const createdAt = new Date().toISOString();
719
723
 
720
724
  if (triggerSource === 'messaging') {
@@ -739,6 +743,7 @@ class AgentEngine {
739
743
  content: normalizedContent,
740
744
  kind: normalizedKind,
741
745
  expectsReply,
746
+ deferFollowUp,
742
747
  });
743
748
  } else {
744
749
  db.prepare(
@@ -758,6 +763,7 @@ class AgentEngine {
758
763
  content: normalizedContent,
759
764
  kind: normalizedKind,
760
765
  expectsReply: expectsReply === true,
766
+ deferFollowUp: deferFollowUp === true,
761
767
  createdAt,
762
768
  });
763
769
  runMeta.lastInterimMessage = normalizedContent;
@@ -767,6 +773,7 @@ class AgentEngine {
767
773
  content: normalizedContent,
768
774
  kind: normalizedKind,
769
775
  expectsReply: expectsReply === true,
776
+ deferFollowUp: deferFollowUp === true,
770
777
  triggerSource,
771
778
  platform: triggerSource === 'messaging' ? platform : 'web',
772
779
  });
@@ -783,6 +790,7 @@ class AgentEngine {
783
790
  latestInterim: {
784
791
  kind: normalizedKind,
785
792
  expectsReply: expectsReply === true,
793
+ deferFollowUp: deferFollowUp === true,
786
794
  content: normalizedContent,
787
795
  createdAt,
788
796
  },
@@ -795,6 +803,7 @@ class AgentEngine {
795
803
  sent: true,
796
804
  kind: normalizedKind,
797
805
  expectsReply: expectsReply === true,
806
+ deferFollowUp: deferFollowUp === true,
798
807
  content: normalizedContent,
799
808
  terminal: terminalInterim,
800
809
  };
@@ -3,6 +3,8 @@ const { GoogleProvider } = require('./providers/google');
3
3
  const { GrokProvider } = require('./providers/grok');
4
4
  const { OllamaProvider } = require('./providers/ollama');
5
5
  const { OpenAIProvider } = require('./providers/openai');
6
+ const { GithubCopilotProvider } = require('./providers/githubCopilot');
7
+ const { OpenAICodexProvider } = require('./providers/openaiCodex');
6
8
  const {
7
9
  AI_PROVIDER_DEFINITIONS,
8
10
  getProviderConfigs,
@@ -16,6 +18,30 @@ const STATIC_MODELS = [
16
18
  provider: 'grok',
17
19
  purpose: 'general'
18
20
  },
21
+ {
22
+ id: 'gpt-5.3',
23
+ label: 'GPT-5.3 (Copilot Default)',
24
+ provider: 'github-copilot',
25
+ purpose: 'general'
26
+ },
27
+ {
28
+ id: 'gpt-4.1',
29
+ label: 'GPT-4.1 (Copilot Fast)',
30
+ provider: 'github-copilot',
31
+ purpose: 'coding'
32
+ },
33
+ {
34
+ id: 'gpt-5.3-codex',
35
+ label: 'GPT-5.3 (Codex Default)',
36
+ provider: 'openai-codex',
37
+ purpose: 'general'
38
+ },
39
+ {
40
+ id: 'gpt-4.1-codex',
41
+ label: 'GPT-4.1 (Codex Fast)',
42
+ provider: 'openai-codex',
43
+ purpose: 'coding'
44
+ },
19
45
  {
20
46
  id: 'gpt-5-nano',
21
47
  label: 'GPT-5 Nano (Fast / Subagents)',
@@ -302,6 +328,10 @@ function createProviderInstance(providerStr, userId = null, configOverrides = {}
302
328
  return new AnthropicProvider({ apiKey: runtime.apiKey, baseUrl: runtime.baseUrl, ...providerOverrides });
303
329
  } else if (providerStr === 'ollama') {
304
330
  return new OllamaProvider({ baseUrl: runtime.baseUrl, ...providerOverrides });
331
+ } else if (providerStr === 'github-copilot') {
332
+ return new GithubCopilotProvider({ apiKey: runtime.apiKey, ...providerOverrides });
333
+ } else if (providerStr === 'openai-codex') {
334
+ return new OpenAICodexProvider({ apiKey: runtime.apiKey, ...providerOverrides });
305
335
  }
306
336
  throw new Error(`Unknown provider: ${providerStr}`);
307
337
  }
@@ -0,0 +1,97 @@
1
+ const { OpenAIProvider } = require('./openai');
2
+
3
+ class GithubCopilotProvider extends OpenAIProvider {
4
+ constructor(config = {}) {
5
+ // GitHub Copilot base URL defaults to the individual endpoint
6
+ const defaultBaseUrl = 'https://api.individual.githubcopilot.com';
7
+ const baseUrl = config.baseUrl || process.env.GITHUB_COPILOT_BASE_URL || defaultBaseUrl;
8
+
9
+ super({
10
+ ...config,
11
+ apiKey: config.apiKey || process.env.GITHUB_COPILOT_ACCESS_TOKEN,
12
+ baseUrl,
13
+ // Pass special headers required by GitHub Copilot
14
+ defaultHeaders: {
15
+ 'Editor-Version': 'vscode/1.90.0',
16
+ 'Editor-Plugin-Version': 'copilot-chat/0.15.0',
17
+ 'User-Agent': 'GithubCopilot/1.155.0',
18
+ 'X-Github-Api-Version': '2023-07-07',
19
+ 'Copilot-Integration-Id': 'vscode-chat'
20
+ }
21
+ });
22
+ this.name = 'github-copilot';
23
+ this.githubToken = config.apiKey || process.env.GITHUB_COPILOT_ACCESS_TOKEN;
24
+ this.copilotToken = null;
25
+ this.tokenExpiresAt = 0;
26
+ this._refreshPromise = null;
27
+ }
28
+
29
+ async _refreshCopilotToken() {
30
+ if (this._refreshPromise) return this._refreshPromise;
31
+
32
+ const now = Math.floor(Date.now() / 1000);
33
+ // Refresh token if missing or expiring in less than 5 minutes
34
+ if (this.copilotToken && this.tokenExpiresAt >= now + 300) {
35
+ return;
36
+ }
37
+
38
+ this._refreshPromise = (async () => {
39
+ try {
40
+ if (!this.githubToken) {
41
+ throw new Error('GitHub Copilot access token is missing. Please run `neoagent login github-copilot`.');
42
+ }
43
+
44
+ const res = await fetch('https://api.github.com/copilot_internal/v2/token', {
45
+ headers: {
46
+ 'Authorization': `token ${this.githubToken}`,
47
+ 'Accept': 'application/json',
48
+ 'User-Agent': 'NeoAgent/1.0.0'
49
+ }
50
+ });
51
+
52
+ if (!res.ok) {
53
+ const errorText = await res.text().catch(() => 'Unknown error');
54
+ throw new Error(`Failed to refresh GitHub Copilot token: HTTP ${res.status} - ${errorText}`);
55
+ }
56
+
57
+ const data = await res.json();
58
+ if (!data || typeof data.token !== 'string' || !data.token) {
59
+ throw new Error('Invalid token response from GitHub Copilot.');
60
+ }
61
+
62
+ this.copilotToken = data.token;
63
+ this.tokenExpiresAt = typeof data.expires_at === 'number'
64
+ ? data.expires_at
65
+ : Math.floor(new Date(data.expires_at).getTime() / 1000);
66
+
67
+ if (isNaN(this.tokenExpiresAt)) {
68
+ this.tokenExpiresAt = Math.floor(Date.now() / 1000) + 1800;
69
+ }
70
+
71
+ // Update the client's API key
72
+ this.client.apiKey = this.copilotToken;
73
+ } finally {
74
+ this._refreshPromise = null;
75
+ }
76
+ })();
77
+
78
+ return this._refreshPromise;
79
+ }
80
+
81
+ async chat(messages, tools = [], options = {}) {
82
+ await this._refreshCopilotToken();
83
+ return super.chat(messages, tools, options);
84
+ }
85
+
86
+ async *stream(messages, tools = [], options = {}) {
87
+ await this._refreshCopilotToken();
88
+ yield* super.stream(messages, tools, options);
89
+ }
90
+
91
+ async analyzeImage(options = {}) {
92
+ await this._refreshCopilotToken();
93
+ return super.analyzeImage(options);
94
+ }
95
+ }
96
+
97
+ module.exports = { GithubCopilotProvider };
@@ -0,0 +1,26 @@
1
+ const { OpenAIProvider } = require('./openai');
2
+
3
+ class OpenAICodexProvider extends OpenAIProvider {
4
+ constructor(config = {}) {
5
+ const officialBaseUrl = 'https://api.openai.com/v1';
6
+ const baseUrl = config.baseUrl || process.env.OPENAI_CODEX_BASE_URL || 'https://chatgpt.com/backend-api/codex';
7
+
8
+ if (!baseUrl.includes('api.openai.com') && !baseUrl.includes('chatgpt.com')) {
9
+ console.warn(`[OpenAICodex] Using non-official base URL: ${baseUrl}`);
10
+ } else if (baseUrl.includes('chatgpt.com')) {
11
+ console.info(`[OpenAICodex] Using ChatGPT subscription endpoint: ${baseUrl}`);
12
+ }
13
+
14
+ super({
15
+ ...config,
16
+ apiKey: config.apiKey || process.env.OPENAI_CODEX_ACCESS_TOKEN,
17
+ baseUrl
18
+ });
19
+ this.name = 'openai-codex';
20
+ }
21
+
22
+ // OpenAI Codex (subscription-based) uses the OAuth token directly as the API key.
23
+ // The base URL routes it through the ChatGPT backend-api.
24
+ }
25
+
26
+ module.exports = { OpenAICodexProvider };
@@ -59,6 +59,26 @@ const AI_PROVIDER_DEFINITIONS = Object.freeze({
59
59
  defaultEnabled: false,
60
60
  defaultBaseUrl: 'https://api.minimax.io/anthropic'
61
61
  },
62
+ 'github-copilot': {
63
+ id: 'github-copilot',
64
+ label: 'GitHub Copilot',
65
+ description: 'Use your GitHub Copilot subscription as an AI provider.',
66
+ envKey: 'GITHUB_COPILOT_ACCESS_TOKEN',
67
+ supportsApiKey: true,
68
+ supportsBaseUrl: true,
69
+ defaultEnabled: false,
70
+ defaultBaseUrl: 'https://api.githubcopilot.com'
71
+ },
72
+ 'openai-codex': {
73
+ id: 'openai-codex',
74
+ label: 'OpenAI Codex',
75
+ description: 'Use your ChatGPT/Codex subscription as an AI provider.',
76
+ envKey: 'OPENAI_CODEX_ACCESS_TOKEN',
77
+ supportsApiKey: true,
78
+ supportsBaseUrl: true,
79
+ defaultEnabled: false,
80
+ defaultBaseUrl: 'https://api.openai.com/v1'
81
+ },
62
82
  ollama: {
63
83
  id: 'ollama',
64
84
  label: 'Ollama',
@@ -138,7 +138,7 @@ When drafting on behalf of the user, match their likely voice from available con
138
138
  If the user approves a previously shown draft, send that draft rather than silently rewriting it.
139
139
 
140
140
  TASKS
141
- Use one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
141
+ Use manual triggers for run-on-demand tasks, one-time schedule triggers for single reminders or delayed actions, recurring schedule triggers for repeating automation, and official integration triggers when the task should react to connected Gmail, Outlook, Slack, Teams, or WhatsApp Personal events. When calling task tools, prefer one unified trigger section: trigger={ type, config }. Make task prompts self-contained: who/what to check, exact action to take, when to notify, and which channel to use if known.
142
142
  Do not create vague tasks like "check this" when the future run would not know what "this" means. Resolve references into names, links, file paths, IDs, dates, and success criteria before saving the task.
143
143
  For notification tasks, distinguish between notifying the user in their current messaging channel, emailing the user, and contacting someone else. Default reminders should notify the user through the active messaging channel unless the user explicitly asks for email, phone, or a third party.
144
144
  When creating or updating a task, include whether it should notify every time, only on change, only on errors, or only when a condition is met. If unspecified, choose the least noisy useful behavior and say what you chose.