apexbot 1.0.2 → 1.0.5
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/agent/agentManager.js +52 -5
- package/dist/agent/toolExecutor.js +144 -0
- package/dist/channels/channelManager.js +3 -1
- package/dist/cli/index.js +127 -0
- package/dist/gateway/index.js +62 -8
- package/dist/skills/index.js +212 -0
- package/dist/skills/obsidian.js +440 -0
- package/dist/skills/reminder.js +430 -0
- package/dist/skills/spotify.js +611 -0
- package/dist/skills/system.js +360 -0
- package/dist/skills/weather.js +144 -0
- package/dist/tools/datetime.js +188 -0
- package/dist/tools/file.js +352 -0
- package/dist/tools/index.js +111 -0
- package/dist/tools/loader.js +68 -0
- package/dist/tools/math.js +248 -0
- package/dist/tools/shell.js +104 -0
- package/dist/tools/web.js +197 -0
- package/package.json +2 -2
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Spotify Skill
|
|
4
|
+
*
|
|
5
|
+
* Integration with Spotify for music playback control.
|
|
6
|
+
* Uses Spotify Web API - requires user authentication via OAuth.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
// Spotify API state
|
|
10
|
+
let accessToken = '';
|
|
11
|
+
let refreshToken = '';
|
|
12
|
+
let clientId = '';
|
|
13
|
+
let clientSecret = '';
|
|
14
|
+
let tokenExpiry = 0;
|
|
15
|
+
const SPOTIFY_API = 'https://api.spotify.com/v1';
|
|
16
|
+
const SPOTIFY_ACCOUNTS = 'https://accounts.spotify.com';
|
|
17
|
+
async function refreshAccessToken() {
|
|
18
|
+
if (!refreshToken || !clientId || !clientSecret) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${SPOTIFY_ACCOUNTS}/api/token`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
26
|
+
'Authorization': 'Basic ' + Buffer.from(`${clientId}:${clientSecret}`).toString('base64'),
|
|
27
|
+
},
|
|
28
|
+
body: `grant_type=refresh_token&refresh_token=${refreshToken}`,
|
|
29
|
+
});
|
|
30
|
+
if (!res.ok)
|
|
31
|
+
return false;
|
|
32
|
+
const data = await res.json();
|
|
33
|
+
accessToken = data.access_token;
|
|
34
|
+
tokenExpiry = Date.now() + (data.expires_in * 1000);
|
|
35
|
+
if (data.refresh_token) {
|
|
36
|
+
refreshToken = data.refresh_token;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function spotifyFetch(endpoint, options = {}) {
|
|
45
|
+
// Check if token needs refresh
|
|
46
|
+
if (Date.now() >= tokenExpiry - 60000) {
|
|
47
|
+
await refreshAccessToken();
|
|
48
|
+
}
|
|
49
|
+
if (!accessToken) {
|
|
50
|
+
throw new Error('Spotify not authenticated. Please configure your Spotify credentials.');
|
|
51
|
+
}
|
|
52
|
+
const res = await fetch(`${SPOTIFY_API}${endpoint}`, {
|
|
53
|
+
...options,
|
|
54
|
+
headers: {
|
|
55
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
...options.headers,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (res.status === 401) {
|
|
61
|
+
// Try refresh
|
|
62
|
+
if (await refreshAccessToken()) {
|
|
63
|
+
return spotifyFetch(endpoint, options);
|
|
64
|
+
}
|
|
65
|
+
throw new Error('Spotify authentication expired. Please re-authenticate.');
|
|
66
|
+
}
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const error = await res.json().catch(() => ({}));
|
|
69
|
+
throw new Error(error.error?.message || `Spotify API error: ${res.status}`);
|
|
70
|
+
}
|
|
71
|
+
// Handle empty responses
|
|
72
|
+
if (res.status === 204) {
|
|
73
|
+
return { success: true };
|
|
74
|
+
}
|
|
75
|
+
return res.json();
|
|
76
|
+
}
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────
|
|
78
|
+
// Spotify Tools
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────
|
|
80
|
+
const spotifyNowPlayingTool = {
|
|
81
|
+
definition: {
|
|
82
|
+
name: 'spotify_now_playing',
|
|
83
|
+
description: 'Get the currently playing track on Spotify.',
|
|
84
|
+
category: 'media',
|
|
85
|
+
parameters: [],
|
|
86
|
+
returns: 'Current track info',
|
|
87
|
+
},
|
|
88
|
+
async execute(params, context) {
|
|
89
|
+
try {
|
|
90
|
+
const data = await spotifyFetch('/me/player/currently-playing');
|
|
91
|
+
if (!data || !data.item) {
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
data: { playing: false, message: 'Nothing is currently playing' },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
success: true,
|
|
99
|
+
data: {
|
|
100
|
+
playing: data.is_playing,
|
|
101
|
+
track: data.item.name,
|
|
102
|
+
artist: data.item.artists.map((a) => a.name).join(', '),
|
|
103
|
+
album: data.item.album.name,
|
|
104
|
+
duration: Math.floor(data.item.duration_ms / 1000),
|
|
105
|
+
progress: Math.floor(data.progress_ms / 1000),
|
|
106
|
+
url: data.item.external_urls.spotify,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return { success: false, error: error.message };
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const spotifyPlayTool = {
|
|
116
|
+
definition: {
|
|
117
|
+
name: 'spotify_play',
|
|
118
|
+
description: 'Play music on Spotify. Can play a specific track, album, playlist, or resume playback.',
|
|
119
|
+
category: 'media',
|
|
120
|
+
parameters: [
|
|
121
|
+
{
|
|
122
|
+
name: 'query',
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Search query for track, album, or playlist (optional - omit to resume)',
|
|
125
|
+
required: false,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'type',
|
|
129
|
+
type: 'string',
|
|
130
|
+
description: 'Type to search: track, album, playlist, artist',
|
|
131
|
+
required: false,
|
|
132
|
+
default: 'track',
|
|
133
|
+
enum: ['track', 'album', 'playlist', 'artist'],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
returns: 'Playback status',
|
|
137
|
+
examples: [
|
|
138
|
+
'spotify_play() - Resume playback',
|
|
139
|
+
'spotify_play({ query: "Bohemian Rhapsody" })',
|
|
140
|
+
'spotify_play({ query: "Chill Vibes", type: "playlist" })',
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
async execute(params, context) {
|
|
144
|
+
try {
|
|
145
|
+
const { query, type = 'track' } = params;
|
|
146
|
+
if (!query) {
|
|
147
|
+
// Resume playback
|
|
148
|
+
await spotifyFetch('/me/player/play', { method: 'PUT' });
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
data: { action: 'resumed', message: 'Playback resumed' },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Search for content
|
|
155
|
+
const searchResult = await spotifyFetch(`/search?q=${encodeURIComponent(query)}&type=${type}&limit=1`);
|
|
156
|
+
const items = searchResult[`${type}s`]?.items;
|
|
157
|
+
if (!items || items.length === 0) {
|
|
158
|
+
return { success: false, error: `No ${type} found for: ${query}` };
|
|
159
|
+
}
|
|
160
|
+
const item = items[0];
|
|
161
|
+
let body = {};
|
|
162
|
+
if (type === 'track') {
|
|
163
|
+
body = { uris: [item.uri] };
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
body = { context_uri: item.uri };
|
|
167
|
+
}
|
|
168
|
+
await spotifyFetch('/me/player/play', {
|
|
169
|
+
method: 'PUT',
|
|
170
|
+
body: JSON.stringify(body),
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
data: {
|
|
175
|
+
action: 'playing',
|
|
176
|
+
type,
|
|
177
|
+
name: item.name,
|
|
178
|
+
artist: item.artists?.[0]?.name || item.owner?.display_name,
|
|
179
|
+
url: item.external_urls.spotify,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return { success: false, error: error.message };
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const spotifyPauseTool = {
|
|
189
|
+
definition: {
|
|
190
|
+
name: 'spotify_pause',
|
|
191
|
+
description: 'Pause Spotify playback.',
|
|
192
|
+
category: 'media',
|
|
193
|
+
parameters: [],
|
|
194
|
+
returns: 'Pause confirmation',
|
|
195
|
+
},
|
|
196
|
+
async execute(params, context) {
|
|
197
|
+
try {
|
|
198
|
+
await spotifyFetch('/me/player/pause', { method: 'PUT' });
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
data: { action: 'paused', message: 'Playback paused' },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return { success: false, error: error.message };
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
const spotifyNextTool = {
|
|
210
|
+
definition: {
|
|
211
|
+
name: 'spotify_next',
|
|
212
|
+
description: 'Skip to the next track.',
|
|
213
|
+
category: 'media',
|
|
214
|
+
parameters: [],
|
|
215
|
+
returns: 'Skip confirmation',
|
|
216
|
+
},
|
|
217
|
+
async execute(params, context) {
|
|
218
|
+
try {
|
|
219
|
+
await spotifyFetch('/me/player/next', { method: 'POST' });
|
|
220
|
+
return {
|
|
221
|
+
success: true,
|
|
222
|
+
data: { action: 'skipped', message: 'Skipped to next track' },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
return { success: false, error: error.message };
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
const spotifyPreviousTool = {
|
|
231
|
+
definition: {
|
|
232
|
+
name: 'spotify_previous',
|
|
233
|
+
description: 'Go back to the previous track.',
|
|
234
|
+
category: 'media',
|
|
235
|
+
parameters: [],
|
|
236
|
+
returns: 'Previous confirmation',
|
|
237
|
+
},
|
|
238
|
+
async execute(params, context) {
|
|
239
|
+
try {
|
|
240
|
+
await spotifyFetch('/me/player/previous', { method: 'POST' });
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
data: { action: 'previous', message: 'Playing previous track' },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
return { success: false, error: error.message };
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
const spotifyVolumeTool = {
|
|
252
|
+
definition: {
|
|
253
|
+
name: 'spotify_volume',
|
|
254
|
+
description: 'Set Spotify playback volume.',
|
|
255
|
+
category: 'media',
|
|
256
|
+
parameters: [
|
|
257
|
+
{
|
|
258
|
+
name: 'volume',
|
|
259
|
+
type: 'number',
|
|
260
|
+
description: 'Volume level (0-100)',
|
|
261
|
+
required: true,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
returns: 'Volume confirmation',
|
|
265
|
+
},
|
|
266
|
+
async execute(params, context) {
|
|
267
|
+
try {
|
|
268
|
+
const { volume } = params;
|
|
269
|
+
const level = Math.max(0, Math.min(100, volume));
|
|
270
|
+
await spotifyFetch(`/me/player/volume?volume_percent=${level}`, { method: 'PUT' });
|
|
271
|
+
return {
|
|
272
|
+
success: true,
|
|
273
|
+
data: { action: 'volume', level, message: `Volume set to ${level}%` },
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
return { success: false, error: error.message };
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
const spotifySearchTool = {
|
|
282
|
+
definition: {
|
|
283
|
+
name: 'spotify_search',
|
|
284
|
+
description: 'Search Spotify for tracks, albums, artists, or playlists.',
|
|
285
|
+
category: 'media',
|
|
286
|
+
parameters: [
|
|
287
|
+
{
|
|
288
|
+
name: 'query',
|
|
289
|
+
type: 'string',
|
|
290
|
+
description: 'Search query',
|
|
291
|
+
required: true,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: 'type',
|
|
295
|
+
type: 'string',
|
|
296
|
+
description: 'Type to search: track, album, artist, playlist',
|
|
297
|
+
required: false,
|
|
298
|
+
default: 'track',
|
|
299
|
+
enum: ['track', 'album', 'artist', 'playlist'],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'limit',
|
|
303
|
+
type: 'number',
|
|
304
|
+
description: 'Number of results (default: 5)',
|
|
305
|
+
required: false,
|
|
306
|
+
default: 5,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
returns: 'Search results',
|
|
310
|
+
},
|
|
311
|
+
async execute(params, context) {
|
|
312
|
+
try {
|
|
313
|
+
const { query, type = 'track', limit = 5 } = params;
|
|
314
|
+
const data = await spotifyFetch(`/search?q=${encodeURIComponent(query)}&type=${type}&limit=${limit}`);
|
|
315
|
+
const items = data[`${type}s`]?.items || [];
|
|
316
|
+
const results = items.map((item) => ({
|
|
317
|
+
name: item.name,
|
|
318
|
+
artist: item.artists?.[0]?.name || item.owner?.display_name,
|
|
319
|
+
album: item.album?.name,
|
|
320
|
+
url: item.external_urls.spotify,
|
|
321
|
+
uri: item.uri,
|
|
322
|
+
}));
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
data: {
|
|
326
|
+
query,
|
|
327
|
+
type,
|
|
328
|
+
results,
|
|
329
|
+
count: results.length,
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
return { success: false, error: error.message };
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
const spotifyPlaylistsTool = {
|
|
339
|
+
definition: {
|
|
340
|
+
name: 'spotify_playlists',
|
|
341
|
+
description: 'Get your Spotify playlists.',
|
|
342
|
+
category: 'media',
|
|
343
|
+
parameters: [
|
|
344
|
+
{
|
|
345
|
+
name: 'limit',
|
|
346
|
+
type: 'number',
|
|
347
|
+
description: 'Number of playlists (default: 20)',
|
|
348
|
+
required: false,
|
|
349
|
+
default: 20,
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
returns: 'List of playlists',
|
|
353
|
+
},
|
|
354
|
+
async execute(params, context) {
|
|
355
|
+
try {
|
|
356
|
+
const { limit = 20 } = params;
|
|
357
|
+
const data = await spotifyFetch(`/me/playlists?limit=${limit}`);
|
|
358
|
+
const playlists = data.items.map((p) => ({
|
|
359
|
+
name: p.name,
|
|
360
|
+
tracks: p.tracks.total,
|
|
361
|
+
owner: p.owner.display_name,
|
|
362
|
+
public: p.public,
|
|
363
|
+
url: p.external_urls.spotify,
|
|
364
|
+
uri: p.uri,
|
|
365
|
+
}));
|
|
366
|
+
return {
|
|
367
|
+
success: true,
|
|
368
|
+
data: {
|
|
369
|
+
playlists,
|
|
370
|
+
count: playlists.length,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
return { success: false, error: error.message };
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
const spotifyCreatePlaylistTool = {
|
|
380
|
+
definition: {
|
|
381
|
+
name: 'spotify_create_playlist',
|
|
382
|
+
description: 'Create a new Spotify playlist.',
|
|
383
|
+
category: 'media',
|
|
384
|
+
parameters: [
|
|
385
|
+
{
|
|
386
|
+
name: 'name',
|
|
387
|
+
type: 'string',
|
|
388
|
+
description: 'Playlist name',
|
|
389
|
+
required: true,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: 'description',
|
|
393
|
+
type: 'string',
|
|
394
|
+
description: 'Playlist description',
|
|
395
|
+
required: false,
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: 'public',
|
|
399
|
+
type: 'boolean',
|
|
400
|
+
description: 'Make playlist public',
|
|
401
|
+
required: false,
|
|
402
|
+
default: false,
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
returns: 'Created playlist info',
|
|
406
|
+
},
|
|
407
|
+
async execute(params, context) {
|
|
408
|
+
try {
|
|
409
|
+
const { name, description, public: isPublic = false } = params;
|
|
410
|
+
// Get user ID first
|
|
411
|
+
const user = await spotifyFetch('/me');
|
|
412
|
+
const playlist = await spotifyFetch(`/users/${user.id}/playlists`, {
|
|
413
|
+
method: 'POST',
|
|
414
|
+
body: JSON.stringify({
|
|
415
|
+
name,
|
|
416
|
+
description: description || '',
|
|
417
|
+
public: isPublic,
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
success: true,
|
|
422
|
+
data: {
|
|
423
|
+
action: 'created',
|
|
424
|
+
name: playlist.name,
|
|
425
|
+
id: playlist.id,
|
|
426
|
+
url: playlist.external_urls.spotify,
|
|
427
|
+
uri: playlist.uri,
|
|
428
|
+
},
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
return { success: false, error: error.message };
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
const spotifyAddToPlaylistTool = {
|
|
437
|
+
definition: {
|
|
438
|
+
name: 'spotify_add_to_playlist',
|
|
439
|
+
description: 'Add tracks to a playlist.',
|
|
440
|
+
category: 'media',
|
|
441
|
+
parameters: [
|
|
442
|
+
{
|
|
443
|
+
name: 'playlist',
|
|
444
|
+
type: 'string',
|
|
445
|
+
description: 'Playlist name or ID',
|
|
446
|
+
required: true,
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: 'tracks',
|
|
450
|
+
type: 'string',
|
|
451
|
+
description: 'Track name to search and add, or track URI',
|
|
452
|
+
required: true,
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
returns: 'Add confirmation',
|
|
456
|
+
},
|
|
457
|
+
async execute(params, context) {
|
|
458
|
+
try {
|
|
459
|
+
const { playlist, tracks } = params;
|
|
460
|
+
// Find playlist
|
|
461
|
+
let playlistId = playlist;
|
|
462
|
+
if (!playlist.startsWith('spotify:playlist:')) {
|
|
463
|
+
const playlists = await spotifyFetch('/me/playlists?limit=50');
|
|
464
|
+
const found = playlists.items.find((p) => p.name.toLowerCase() === playlist.toLowerCase() || p.id === playlist);
|
|
465
|
+
if (!found) {
|
|
466
|
+
return { success: false, error: `Playlist not found: ${playlist}` };
|
|
467
|
+
}
|
|
468
|
+
playlistId = found.id;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
playlistId = playlist.replace('spotify:playlist:', '');
|
|
472
|
+
}
|
|
473
|
+
// Find track if not URI
|
|
474
|
+
let trackUri = tracks;
|
|
475
|
+
if (!tracks.startsWith('spotify:track:')) {
|
|
476
|
+
const search = await spotifyFetch(`/search?q=${encodeURIComponent(tracks)}&type=track&limit=1`);
|
|
477
|
+
const track = search.tracks?.items?.[0];
|
|
478
|
+
if (!track) {
|
|
479
|
+
return { success: false, error: `Track not found: ${tracks}` };
|
|
480
|
+
}
|
|
481
|
+
trackUri = track.uri;
|
|
482
|
+
}
|
|
483
|
+
await spotifyFetch(`/playlists/${playlistId}/tracks`, {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
body: JSON.stringify({ uris: [trackUri] }),
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
success: true,
|
|
489
|
+
data: {
|
|
490
|
+
action: 'added',
|
|
491
|
+
playlist: playlistId,
|
|
492
|
+
track: trackUri,
|
|
493
|
+
message: 'Track added to playlist',
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return { success: false, error: error.message };
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
const spotifyDevicesTool = {
|
|
503
|
+
definition: {
|
|
504
|
+
name: 'spotify_devices',
|
|
505
|
+
description: 'List available Spotify playback devices.',
|
|
506
|
+
category: 'media',
|
|
507
|
+
parameters: [],
|
|
508
|
+
returns: 'List of devices',
|
|
509
|
+
},
|
|
510
|
+
async execute(params, context) {
|
|
511
|
+
try {
|
|
512
|
+
const data = await spotifyFetch('/me/player/devices');
|
|
513
|
+
const devices = data.devices.map((d) => ({
|
|
514
|
+
name: d.name,
|
|
515
|
+
type: d.type,
|
|
516
|
+
active: d.is_active,
|
|
517
|
+
volume: d.volume_percent,
|
|
518
|
+
id: d.id,
|
|
519
|
+
}));
|
|
520
|
+
return {
|
|
521
|
+
success: true,
|
|
522
|
+
data: {
|
|
523
|
+
devices,
|
|
524
|
+
count: devices.length,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
return { success: false, error: error.message };
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
// ─────────────────────────────────────────────────────────────────
|
|
534
|
+
// Skill Definition
|
|
535
|
+
// ─────────────────────────────────────────────────────────────────
|
|
536
|
+
const manifest = {
|
|
537
|
+
name: 'spotify',
|
|
538
|
+
version: '1.0.0',
|
|
539
|
+
description: 'Spotify music playback control and playlist management',
|
|
540
|
+
category: 'media',
|
|
541
|
+
icon: 'music',
|
|
542
|
+
homepage: 'https://developer.spotify.com/documentation/web-api',
|
|
543
|
+
tools: [
|
|
544
|
+
'spotify_now_playing',
|
|
545
|
+
'spotify_play',
|
|
546
|
+
'spotify_pause',
|
|
547
|
+
'spotify_next',
|
|
548
|
+
'spotify_previous',
|
|
549
|
+
'spotify_volume',
|
|
550
|
+
'spotify_search',
|
|
551
|
+
'spotify_playlists',
|
|
552
|
+
'spotify_create_playlist',
|
|
553
|
+
'spotify_add_to_playlist',
|
|
554
|
+
'spotify_devices',
|
|
555
|
+
],
|
|
556
|
+
config: [
|
|
557
|
+
{
|
|
558
|
+
name: 'clientId',
|
|
559
|
+
type: 'string',
|
|
560
|
+
description: 'Spotify App Client ID (from developer.spotify.com)',
|
|
561
|
+
required: true,
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: 'clientSecret',
|
|
565
|
+
type: 'secret',
|
|
566
|
+
description: 'Spotify App Client Secret',
|
|
567
|
+
required: true,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: 'refreshToken',
|
|
571
|
+
type: 'secret',
|
|
572
|
+
description: 'Spotify OAuth Refresh Token',
|
|
573
|
+
required: true,
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
permissions: ['user-read-playback-state', 'user-modify-playback-state', 'playlist-read-private', 'playlist-modify-public', 'playlist-modify-private'],
|
|
577
|
+
};
|
|
578
|
+
const spotifySkill = {
|
|
579
|
+
manifest,
|
|
580
|
+
tools: [
|
|
581
|
+
spotifyNowPlayingTool,
|
|
582
|
+
spotifyPlayTool,
|
|
583
|
+
spotifyPauseTool,
|
|
584
|
+
spotifyNextTool,
|
|
585
|
+
spotifyPreviousTool,
|
|
586
|
+
spotifyVolumeTool,
|
|
587
|
+
spotifySearchTool,
|
|
588
|
+
spotifyPlaylistsTool,
|
|
589
|
+
spotifyCreatePlaylistTool,
|
|
590
|
+
spotifyAddToPlaylistTool,
|
|
591
|
+
spotifyDevicesTool,
|
|
592
|
+
],
|
|
593
|
+
async initialize(config) {
|
|
594
|
+
clientId = config.clientId || process.env.SPOTIFY_CLIENT_ID || '';
|
|
595
|
+
clientSecret = config.clientSecret || process.env.SPOTIFY_CLIENT_SECRET || '';
|
|
596
|
+
refreshToken = config.refreshToken || process.env.SPOTIFY_REFRESH_TOKEN || '';
|
|
597
|
+
if (clientId && clientSecret && refreshToken) {
|
|
598
|
+
await refreshAccessToken();
|
|
599
|
+
console.log('[Spotify] Initialized and authenticated');
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
console.log('[Spotify] Initialized (credentials not configured)');
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
async shutdown() {
|
|
606
|
+
accessToken = '';
|
|
607
|
+
refreshToken = '';
|
|
608
|
+
console.log('[Spotify] Shutdown');
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
exports.default = spotifySkill;
|