apexbot 1.1.1 → 1.1.2

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/cli/index.js CHANGED
@@ -581,6 +581,107 @@ async function startGatewayServer(config, options = {}) {
581
581
  });
582
582
  }
583
583
  // ─────────────────────────────────────────────────────────────────
584
+ // GOOGLE-AUTH Command - OAuth2 authorization for Google Calendar
585
+ // ─────────────────────────────────────────────────────────────────
586
+ program
587
+ .command('google-auth')
588
+ .description('Authorize ApexBot to access your Google Calendar')
589
+ .action(async () => {
590
+ showBanner();
591
+ console.log(chalk.cyan('\n📅 Google Calendar Authorization\n'));
592
+ console.log(chalk.gray('This will allow ApexBot to create events in your Google Calendar.\n'));
593
+ // Check for credentials
594
+ const clientId = process.env.GOOGLE_CLIENT_ID;
595
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
596
+ if (!clientId || !clientSecret) {
597
+ console.log(chalk.yellow('⚠️ Google OAuth2 credentials not found.\n'));
598
+ console.log('To set up Google Calendar integration:\n');
599
+ console.log('1. Go to https://console.cloud.google.com/apis/credentials');
600
+ console.log('2. Create OAuth 2.0 Client ID (type: Desktop app)');
601
+ console.log('3. Enable Google Calendar API');
602
+ console.log('4. Set environment variables:\n');
603
+ console.log(chalk.cyan(' GOOGLE_CLIENT_ID=your-client-id'));
604
+ console.log(chalk.cyan(' GOOGLE_CLIENT_SECRET=your-client-secret\n'));
605
+ console.log('5. Run this command again\n');
606
+ process.exit(1);
607
+ }
608
+ // Build OAuth URL
609
+ const redirectUri = 'urn:ietf:wg:oauth:2.0:oob'; // Manual copy/paste flow
610
+ const scope = 'https://www.googleapis.com/auth/calendar';
611
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
612
+ `client_id=${encodeURIComponent(clientId)}` +
613
+ `&redirect_uri=${encodeURIComponent(redirectUri)}` +
614
+ `&response_type=code` +
615
+ `&scope=${encodeURIComponent(scope)}` +
616
+ `&access_type=offline` +
617
+ `&prompt=consent`;
618
+ console.log(chalk.green('✓ Credentials found!\n'));
619
+ console.log('Open this URL in your browser to authorize:\n');
620
+ console.log(chalk.cyan(authUrl) + '\n');
621
+ // Try to open browser automatically using native OS command
622
+ const { exec } = require('child_process');
623
+ const platform = process.platform;
624
+ const openCmd = platform === 'win32' ? 'start ""' :
625
+ platform === 'darwin' ? 'open' : 'xdg-open';
626
+ exec(`${openCmd} "${authUrl}"`, (err) => {
627
+ if (!err) {
628
+ console.log(chalk.gray('(Browser should open automatically)\n'));
629
+ }
630
+ });
631
+ // Get authorization code from user
632
+ const { code } = await inquirer.prompt([
633
+ {
634
+ type: 'input',
635
+ name: 'code',
636
+ message: 'Paste the authorization code here:',
637
+ validate: (input) => input.length > 10 || 'Please enter the full authorization code',
638
+ },
639
+ ]);
640
+ console.log(chalk.gray('\nExchanging code for tokens...'));
641
+ // Exchange code for tokens
642
+ try {
643
+ const response = await fetch('https://oauth2.googleapis.com/token', {
644
+ method: 'POST',
645
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
646
+ body: new URLSearchParams({
647
+ client_id: clientId,
648
+ client_secret: clientSecret,
649
+ code: code.trim(),
650
+ grant_type: 'authorization_code',
651
+ redirect_uri: redirectUri,
652
+ }),
653
+ });
654
+ if (!response.ok) {
655
+ const error = await response.json();
656
+ throw new Error(error.error_description || error.error || 'Token exchange failed');
657
+ }
658
+ const tokens = await response.json();
659
+ // Save tokens
660
+ const tokenPath = path.join(CONFIG_DIR, 'google_tokens.json');
661
+ fs.writeFileSync(tokenPath, JSON.stringify({
662
+ access_token: tokens.access_token,
663
+ refresh_token: tokens.refresh_token,
664
+ expiry: Date.now() + (tokens.expires_in * 1000),
665
+ }, null, 2));
666
+ console.log(chalk.green('\n✅ Google Calendar authorized successfully!\n'));
667
+ console.log('ApexBot can now create events in your Google Calendar.');
668
+ console.log(`Tokens saved to: ${chalk.gray(tokenPath)}\n`);
669
+ // Test the connection
670
+ console.log(chalk.gray('Testing connection...'));
671
+ const testResponse = await fetch('https://www.googleapis.com/calendar/v3/calendars/primary', {
672
+ headers: { 'Authorization': `Bearer ${tokens.access_token}` },
673
+ });
674
+ if (testResponse.ok) {
675
+ const calendar = await testResponse.json();
676
+ console.log(chalk.green(`✓ Connected to: ${calendar.summary || 'Primary Calendar'}\n`));
677
+ }
678
+ }
679
+ catch (error) {
680
+ console.log(chalk.red(`\n❌ Authorization failed: ${error.message}\n`));
681
+ process.exit(1);
682
+ }
683
+ });
684
+ // ─────────────────────────────────────────────────────────────────
584
685
  // GATEWAY Command
585
686
  // ─────────────────────────────────────────────────────────────────
586
687
  program
@@ -3,7 +3,7 @@
3
3
  * Calendar Skill
4
4
  *
5
5
  * Integration with Google Calendar and local calendar storage.
6
- * Similar to Clawdbot's calendar skill.
6
+ * Supports OAuth2 for full read/write access to Google Calendar.
7
7
  */
8
8
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
9
  if (k2 === undefined) k2 = k;
@@ -94,6 +94,82 @@ let dataPath = '';
94
94
  let events = [];
95
95
  let googleApiKey = '';
96
96
  let googleCalendarId = 'primary';
97
+ // Google OAuth2 credentials
98
+ let googleClientId = '';
99
+ let googleClientSecret = '';
100
+ let googleRefreshToken = '';
101
+ let googleAccessToken = '';
102
+ let googleTokenExpiry = 0;
103
+ let configDir = '';
104
+ // ─────────────────────────────────────────────────────────────────
105
+ // Google OAuth2 Token Management
106
+ // ─────────────────────────────────────────────────────────────────
107
+ async function loadGoogleTokens() {
108
+ try {
109
+ const tokenPath = path.join(configDir, 'google_tokens.json');
110
+ const data = await fs.readFile(tokenPath, 'utf-8');
111
+ const tokens = JSON.parse(data);
112
+ googleAccessToken = tokens.access_token || '';
113
+ googleRefreshToken = tokens.refresh_token || '';
114
+ googleTokenExpiry = tokens.expiry || 0;
115
+ }
116
+ catch {
117
+ // No tokens saved yet
118
+ }
119
+ }
120
+ async function saveGoogleTokens() {
121
+ const tokenPath = path.join(configDir, 'google_tokens.json');
122
+ await fs.writeFile(tokenPath, JSON.stringify({
123
+ access_token: googleAccessToken,
124
+ refresh_token: googleRefreshToken,
125
+ expiry: googleTokenExpiry,
126
+ }, null, 2));
127
+ }
128
+ async function refreshGoogleToken() {
129
+ if (!googleClientId || !googleClientSecret || !googleRefreshToken) {
130
+ return false;
131
+ }
132
+ try {
133
+ const response = await fetch('https://oauth2.googleapis.com/token', {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
136
+ body: new URLSearchParams({
137
+ client_id: googleClientId,
138
+ client_secret: googleClientSecret,
139
+ refresh_token: googleRefreshToken,
140
+ grant_type: 'refresh_token',
141
+ }),
142
+ });
143
+ if (!response.ok) {
144
+ console.error('[Calendar] Failed to refresh Google token');
145
+ return false;
146
+ }
147
+ const data = await response.json();
148
+ googleAccessToken = data.access_token;
149
+ googleTokenExpiry = Date.now() + (data.expires_in * 1000) - 60000; // 1 min buffer
150
+ await saveGoogleTokens();
151
+ console.log('[Calendar] Google token refreshed');
152
+ return true;
153
+ }
154
+ catch (error) {
155
+ console.error('[Calendar] Token refresh error:', error.message);
156
+ return false;
157
+ }
158
+ }
159
+ async function getValidGoogleToken() {
160
+ // Check if we have a valid token
161
+ if (googleAccessToken && Date.now() < googleTokenExpiry) {
162
+ return googleAccessToken;
163
+ }
164
+ // Try to refresh
165
+ if (await refreshGoogleToken()) {
166
+ return googleAccessToken;
167
+ }
168
+ return null;
169
+ }
170
+ function isGoogleCalendarConfigured() {
171
+ return !!(googleClientId && googleClientSecret && googleRefreshToken);
172
+ }
97
173
  // ─────────────────────────────────────────────────────────────────
98
174
  // Local Calendar Storage
99
175
  // ─────────────────────────────────────────────────────────────────
@@ -168,6 +244,19 @@ function parseDate(input) {
168
244
  return null;
169
245
  }
170
246
  function parseDuration(input) {
247
+ // If already a number (minutes), return it
248
+ if (typeof input === 'number') {
249
+ return input;
250
+ }
251
+ // If not a string, default to 60 minutes
252
+ if (typeof input !== 'string') {
253
+ return 60;
254
+ }
255
+ // Check if it's just a number string
256
+ const numOnly = parseInt(input);
257
+ if (!isNaN(numOnly) && input.trim() === String(numOnly)) {
258
+ return numOnly; // Assume minutes
259
+ }
171
260
  // Duration in minutes
172
261
  const match = input.match(/^(\d+)\s*(minute|min|hour|hr|h|m)s?$/i);
173
262
  if (match) {
@@ -183,11 +272,31 @@ function parseDuration(input) {
183
272
  // ─────────────────────────────────────────────────────────────────
184
273
  // Google Calendar Integration
185
274
  // ─────────────────────────────────────────────────────────────────
275
+ // Google Calendar API (supports both API key read-only and OAuth2 read/write)
186
276
  async function googleCalendarFetch(endpoint, options = {}) {
277
+ const baseUrl = 'https://www.googleapis.com/calendar/v3';
278
+ // Try OAuth2 first for write operations
279
+ const token = await getValidGoogleToken();
280
+ if (token) {
281
+ const url = `${baseUrl}${endpoint}`;
282
+ const response = await fetch(url, {
283
+ ...options,
284
+ headers: {
285
+ 'Content-Type': 'application/json',
286
+ 'Authorization': `Bearer ${token}`,
287
+ ...options.headers,
288
+ },
289
+ });
290
+ if (!response.ok) {
291
+ const error = await response.json().catch(() => ({}));
292
+ throw new Error(error.error?.message || `Google Calendar API error: ${response.status}`);
293
+ }
294
+ return response.json();
295
+ }
296
+ // Fallback to API key (read-only)
187
297
  if (!googleApiKey) {
188
- throw new Error('Google Calendar API not configured');
298
+ throw new Error('Google Calendar nie skonfigurowany. Ustaw GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET i uruchom "apexbot google-auth"');
189
299
  }
190
- const baseUrl = 'https://www.googleapis.com/calendar/v3';
191
300
  const url = `${baseUrl}${endpoint}${endpoint.includes('?') ? '&' : '?'}key=${googleApiKey}`;
192
301
  const response = await fetch(url, {
193
302
  ...options,
@@ -202,6 +311,34 @@ async function googleCalendarFetch(endpoint, options = {}) {
202
311
  }
203
312
  return response.json();
204
313
  }
314
+ // Create event in Google Calendar
315
+ async function createGoogleCalendarEvent(event) {
316
+ if (!isGoogleCalendarConfigured()) {
317
+ throw new Error('Google Calendar OAuth nie skonfigurowany. Uruchom "apexbot google-auth" aby połączyć konto.');
318
+ }
319
+ const calendarId = googleCalendarId || 'primary';
320
+ const googleEvent = {
321
+ summary: event.title,
322
+ description: event.description,
323
+ location: event.location,
324
+ start: {
325
+ dateTime: event.start,
326
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
327
+ },
328
+ end: {
329
+ dateTime: event.end,
330
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
331
+ },
332
+ reminders: event.reminders ? {
333
+ useDefault: false,
334
+ overrides: event.reminders.map(mins => ({ method: 'popup', minutes: mins })),
335
+ } : { useDefault: true },
336
+ };
337
+ return googleCalendarFetch(`/calendars/${encodeURIComponent(calendarId)}/events`, {
338
+ method: 'POST',
339
+ body: JSON.stringify(googleEvent),
340
+ });
341
+ }
205
342
  // ─────────────────────────────────────────────────────────────────
206
343
  // Calendar Tools
207
344
  // ─────────────────────────────────────────────────────────────────
@@ -267,6 +404,59 @@ const createEventTool = {
267
404
  }
268
405
  const durationMinutes = allDay ? 24 * 60 : parseDuration(duration);
269
406
  const endDate = new Date(startDate.getTime() + durationMinutes * 60000);
407
+ // Format date for display
408
+ const dateStr = startDate.toLocaleDateString('pl-PL', {
409
+ weekday: 'long',
410
+ year: 'numeric',
411
+ month: 'long',
412
+ day: 'numeric',
413
+ hour: '2-digit',
414
+ minute: '2-digit'
415
+ });
416
+ // Try Google Calendar first if configured
417
+ if (isGoogleCalendarConfigured()) {
418
+ try {
419
+ const googleEvent = await createGoogleCalendarEvent({
420
+ title,
421
+ description,
422
+ location,
423
+ start: startDate.toISOString(),
424
+ end: endDate.toISOString(),
425
+ reminders: [15], // 15 min before
426
+ });
427
+ // Also save locally for notifications
428
+ const event = {
429
+ id: googleEvent.id || generateId(),
430
+ title,
431
+ description,
432
+ start: startDate.toISOString(),
433
+ end: endDate.toISOString(),
434
+ location,
435
+ allDay,
436
+ calendar: 'google',
437
+ };
438
+ events.push(event);
439
+ await saveEvents();
440
+ return {
441
+ success: true,
442
+ data: {
443
+ id: googleEvent.id,
444
+ title,
445
+ start: startDate.toISOString(),
446
+ end: endDate.toISOString(),
447
+ location,
448
+ calendar: 'Google Calendar',
449
+ htmlLink: googleEvent.htmlLink,
450
+ message: `✅ Wydarzenie "${title}" utworzone w Google Calendar na ${dateStr}. Link: ${googleEvent.htmlLink}`,
451
+ },
452
+ };
453
+ }
454
+ catch (error) {
455
+ console.error('[Calendar] Google Calendar error:', error.message);
456
+ // Fall through to local storage
457
+ }
458
+ }
459
+ // Local storage fallback
270
460
  const event = {
271
461
  id: generateId(),
272
462
  title,
@@ -279,15 +469,10 @@ const createEventTool = {
279
469
  };
280
470
  events.push(event);
281
471
  await saveEvents();
282
- // Format nicely for Polish users
283
- const dateStr = startDate.toLocaleDateString('pl-PL', {
284
- weekday: 'long',
285
- year: 'numeric',
286
- month: 'long',
287
- day: 'numeric',
288
- hour: '2-digit',
289
- minute: '2-digit'
290
- });
472
+ // Check if Google Calendar is configured but failed
473
+ const googleNote = isGoogleCalendarConfigured()
474
+ ? '⚠️ Nie udało się zapisać w Google Calendar, zapisano lokalnie.'
475
+ : 'ℹ️ Aby zapisywać wydarzenia w Google Calendar, uruchom "apexbot google-auth".';
291
476
  return {
292
477
  success: true,
293
478
  data: {
@@ -297,8 +482,7 @@ const createEventTool = {
297
482
  end: event.end,
298
483
  location: event.location,
299
484
  calendar: 'ApexBot (lokalny)',
300
- message: `📅 Wydarzenie "${title}" zapisane w lokalnym kalendarzu ApexBot na ${dateStr}. Dostaniesz powiadomienie 15 minut przed wydarzeniem.`,
301
- note: 'Aby zsynchronizować z Google Calendar, skonfiguruj GOOGLE_CALENDAR_API_KEY.',
485
+ message: `📅 Wydarzenie "${title}" zapisane w kalendarzu ApexBot na ${dateStr}. Dostaniesz powiadomienie 15 minut przed. ${googleNote}`,
302
486
  },
303
487
  };
304
488
  },
@@ -616,11 +800,25 @@ const calendarSkill = {
616
800
  manifest,
617
801
  tools: [createEventTool, listEventsTool, deleteEventTool, checkAvailabilityTool, findTimeTool],
618
802
  async initialize(config) {
619
- dataPath = config.dataPath || path.join(process.env.HOME || process.env.USERPROFILE || '', '.apexbot', 'calendar.json');
803
+ configDir = config.configDir || path.join(process.env.HOME || process.env.USERPROFILE || '', '.apexbot');
804
+ dataPath = config.dataPath || path.join(configDir, 'calendar.json');
805
+ // Google Calendar OAuth2 credentials
806
+ googleClientId = config.googleClientId || process.env.GOOGLE_CLIENT_ID || '';
807
+ googleClientSecret = config.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || '';
620
808
  googleApiKey = config.googleApiKey || process.env.GOOGLE_CALENDAR_API_KEY || '';
621
- googleCalendarId = config.defaultCalendar || 'primary';
809
+ googleCalendarId = config.defaultCalendar || process.env.GOOGLE_CALENDAR_ID || 'primary';
622
810
  await loadEvents();
623
- console.log(`[Calendar] Initialized with ${events.length} events`);
811
+ await loadGoogleTokens();
812
+ if (isGoogleCalendarConfigured()) {
813
+ console.log(`[Calendar] Initialized with Google Calendar integration`);
814
+ }
815
+ else if (googleApiKey) {
816
+ console.log(`[Calendar] Initialized with Google Calendar (read-only)`);
817
+ }
818
+ else {
819
+ console.log(`[Calendar] Initialized with local storage only`);
820
+ }
821
+ console.log(`[Calendar] ${events.length} events loaded`);
624
822
  },
625
823
  async shutdown() {
626
824
  await saveEvents();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apexbot",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "ApexBot - Your free, private AI assistant. 100% free with Ollama (local AI). Multi-channel: Telegram, Discord, WebChat. Tools & Skills system with Spotify, Obsidian, and more!",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -84,4 +84,4 @@
84
84
  "ts-node": "^10.9.1",
85
85
  "typescript": "^5.2.2"
86
86
  }
87
- }
87
+ }