apexbot 1.0.4 → 1.0.6
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 +105 -22
- package/dist/agent/toolExecutor.js +35 -6
- package/dist/cli/index.js +15 -2
- package/dist/skills/spotify.js +611 -0
- package/dist/tools/loader.js +2 -0
- package/package.json +2 -2
|
@@ -12,26 +12,34 @@ const toolExecutor_1 = require("./toolExecutor");
|
|
|
12
12
|
class AgentManager {
|
|
13
13
|
config = null;
|
|
14
14
|
googleClient = null;
|
|
15
|
-
defaultSystemPrompt = `You are ApexBot,
|
|
15
|
+
defaultSystemPrompt = `You are ApexBot, an autonomous AI assistant like Claude Code. You can execute real actions on the user's computer.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## CORE PRINCIPLES
|
|
18
|
+
1. **ACT, DON'T EXPLAIN** - When user asks for something, DO IT using tools. Don't explain how to do it.
|
|
19
|
+
2. **BE AUTONOMOUS** - Make decisions and execute actions automatically
|
|
20
|
+
3. **BE HELPFUL** - Provide direct answers and results, not tutorials
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
## CAPABILITIES
|
|
23
|
+
- Execute shell commands
|
|
24
|
+
- Read/write/edit files
|
|
25
|
+
- Get weather forecasts
|
|
26
|
+
- Set reminders and timers
|
|
27
|
+
- Control Spotify playback
|
|
28
|
+
- Manage Obsidian notes
|
|
29
|
+
- Search the web
|
|
30
|
+
- Do math calculations
|
|
31
|
+
- Get system information
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
33
|
+
## RESPONSE STYLE
|
|
34
|
+
- Be concise and direct
|
|
35
|
+
- Show results, not code tutorials
|
|
36
|
+
- Use natural language to explain results
|
|
37
|
+
- If something fails, explain why simply
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- Start with context analysis when working with code
|
|
39
|
+
## LANGUAGE
|
|
40
|
+
- Respond in the same language as the user
|
|
41
|
+
- If user writes in Polish, respond in Polish
|
|
42
|
+
- If user writes in English, respond in English
|
|
35
43
|
|
|
36
44
|
You are running locally on the user's machine. No data leaves their computer. You are free, private, and open-source.`;
|
|
37
45
|
constructor(config) {
|
|
@@ -103,12 +111,18 @@ You are running locally on the user's machine. No data leaves their computer. Yo
|
|
|
103
111
|
if (this.config.enableTools) {
|
|
104
112
|
const toolCalls = (0, toolExecutor_1.parseToolCalls)(response.text);
|
|
105
113
|
if (toolCalls.length > 0) {
|
|
106
|
-
console.log(`[Agent] Found ${toolCalls.length} tool calls
|
|
114
|
+
console.log(`[Agent] Found ${toolCalls.length} tool calls:`, toolCalls.map(t => t.name));
|
|
107
115
|
// Build tool context
|
|
108
|
-
const context = (0, toolExecutor_1.buildToolContext)(session.id, message.userId || 'unknown', message.channel, { workspaceDir: process.cwd() });
|
|
116
|
+
const context = (0, toolExecutor_1.buildToolContext)(session.id, message.userId || message.from || 'unknown', message.channel, { workspaceDir: process.cwd() });
|
|
109
117
|
// Execute tools
|
|
110
118
|
const toolResults = await (0, toolExecutor_1.executeToolCalls)(toolCalls, context);
|
|
111
119
|
response.toolCalls = toolResults;
|
|
120
|
+
// Log results
|
|
121
|
+
for (const { call, result } of toolResults) {
|
|
122
|
+
console.log(`[Agent] Tool ${call.name}: ${result.success ? 'SUCCESS' : 'FAILED'}`);
|
|
123
|
+
if (!result.success)
|
|
124
|
+
console.log(`[Agent] Error: ${result.error}`);
|
|
125
|
+
}
|
|
112
126
|
// Format results and continue conversation
|
|
113
127
|
const resultsText = (0, toolExecutor_1.formatToolResults)(toolResults);
|
|
114
128
|
if (resultsText) {
|
|
@@ -116,7 +130,7 @@ You are running locally on the user's machine. No data leaves their computer. Yo
|
|
|
116
130
|
const followUpHistory = [
|
|
117
131
|
...history,
|
|
118
132
|
{ role: 'assistant', content: response.text, timestamp: Date.now() },
|
|
119
|
-
{ role: 'user', content: `
|
|
133
|
+
{ role: 'user', content: `[TOOL EXECUTION COMPLETE]\n\nResults:\n${resultsText}\n\nNow provide a natural language response to the user summarizing these results. Be concise. Respond in the same language as the user's original message.`, timestamp: Date.now() },
|
|
120
134
|
];
|
|
121
135
|
// Get follow-up response
|
|
122
136
|
let followUp;
|
|
@@ -127,11 +141,21 @@ You are running locally on the user's machine. No data leaves their computer. Yo
|
|
|
127
141
|
case 'google':
|
|
128
142
|
followUp = await this.processWithGemini(followUpHistory);
|
|
129
143
|
break;
|
|
144
|
+
case 'anthropic':
|
|
145
|
+
followUp = await this.processWithClaude(followUpHistory);
|
|
146
|
+
break;
|
|
147
|
+
case 'openai':
|
|
148
|
+
followUp = await this.processWithOpenAI(followUpHistory);
|
|
149
|
+
break;
|
|
150
|
+
case 'kimi':
|
|
151
|
+
followUp = await this.processWithKimi(followUpHistory);
|
|
152
|
+
break;
|
|
130
153
|
default:
|
|
131
|
-
|
|
154
|
+
// Fallback: return formatted results directly
|
|
155
|
+
followUp = { text: this.formatResultsForUser(toolResults) };
|
|
132
156
|
}
|
|
133
|
-
//
|
|
134
|
-
response.text = followUp.text;
|
|
157
|
+
// Use follow-up response (or fallback to formatted results)
|
|
158
|
+
response.text = followUp.text || this.formatResultsForUser(toolResults);
|
|
135
159
|
}
|
|
136
160
|
}
|
|
137
161
|
}
|
|
@@ -149,6 +173,65 @@ You are running locally on the user's machine. No data leaves their computer. Yo
|
|
|
149
173
|
return { text: `Error: ${errorMsg}` };
|
|
150
174
|
}
|
|
151
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Format tool results for direct user display (fallback when AI can't summarize)
|
|
178
|
+
*/
|
|
179
|
+
formatResultsForUser(toolResults) {
|
|
180
|
+
const parts = [];
|
|
181
|
+
for (const { call, result } of toolResults) {
|
|
182
|
+
if (result.success) {
|
|
183
|
+
const data = result.data;
|
|
184
|
+
// Format based on tool type
|
|
185
|
+
if (call.name === 'weather' && data) {
|
|
186
|
+
parts.push(`Weather in ${data.location}: ${data.current?.temperature}°C, ${data.current?.condition}`);
|
|
187
|
+
}
|
|
188
|
+
else if (call.name === 'datetime' && data) {
|
|
189
|
+
parts.push(`Current time: ${data.formatted || data.time}`);
|
|
190
|
+
}
|
|
191
|
+
else if (call.name === 'math' && data) {
|
|
192
|
+
parts.push(`Result: ${data.result}`);
|
|
193
|
+
}
|
|
194
|
+
else if (call.name === 'system_info' && data) {
|
|
195
|
+
if (data.memory) {
|
|
196
|
+
parts.push(`RAM: ${data.memory.used} / ${data.memory.total} (${data.memory.usage} used)`);
|
|
197
|
+
}
|
|
198
|
+
if (data.cpu) {
|
|
199
|
+
parts.push(`CPU: ${data.cpu.model} (${data.cpu.cores} cores)`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else if (call.name.startsWith('reminder') && data) {
|
|
203
|
+
parts.push(data.message || `Reminder ${data.action || 'set'}`);
|
|
204
|
+
}
|
|
205
|
+
else if (call.name.startsWith('spotify') && data) {
|
|
206
|
+
if (data.track)
|
|
207
|
+
parts.push(`Now playing: ${data.track} by ${data.artist}`);
|
|
208
|
+
else if (data.message)
|
|
209
|
+
parts.push(data.message);
|
|
210
|
+
else if (data.action)
|
|
211
|
+
parts.push(`Spotify: ${data.action}`);
|
|
212
|
+
}
|
|
213
|
+
else if (data) {
|
|
214
|
+
// Generic formatting
|
|
215
|
+
if (typeof data === 'string') {
|
|
216
|
+
parts.push(data);
|
|
217
|
+
}
|
|
218
|
+
else if (data.message) {
|
|
219
|
+
parts.push(data.message);
|
|
220
|
+
}
|
|
221
|
+
else if (data.result) {
|
|
222
|
+
parts.push(String(data.result));
|
|
223
|
+
}
|
|
224
|
+
else if (data.output) {
|
|
225
|
+
parts.push(data.output);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
parts.push(`Error (${call.name}): ${result.error}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return parts.join('\n') || 'Done.';
|
|
234
|
+
}
|
|
152
235
|
async processWithKimi(history) {
|
|
153
236
|
// Generic Kimi 2.5 integration wrapper. This implementation is intentionally
|
|
154
237
|
// generic: it will POST to a user-provided `apiUrl` (in config or env) and
|
|
@@ -114,20 +114,49 @@ function getToolsSystemPrompt() {
|
|
|
114
114
|
}
|
|
115
115
|
const toolDescriptions = tools.map(t => {
|
|
116
116
|
const params = t.parameters.map(p => ` - ${p.name} (${p.type}${p.required ? ', required' : ''}): ${p.description}`).join('\n');
|
|
117
|
-
return
|
|
118
|
-
}).join('\n
|
|
117
|
+
return `- **${t.name}**: ${t.description}`;
|
|
118
|
+
}).join('\n');
|
|
119
119
|
return `
|
|
120
|
-
|
|
120
|
+
## TOOLS - IMPORTANT!
|
|
121
|
+
|
|
122
|
+
You have tools to interact with the real world. **USE THEM AUTOMATICALLY** when the user asks for something that requires them.
|
|
123
|
+
|
|
124
|
+
**HOW TO USE A TOOL:**
|
|
125
|
+
Include this EXACT format in your response (the system will execute it automatically):
|
|
121
126
|
|
|
122
127
|
\`\`\`json
|
|
123
|
-
{"tool": "tool_name", "args": {"
|
|
128
|
+
{"tool": "tool_name", "args": {"param": "value"}}
|
|
124
129
|
\`\`\`
|
|
125
130
|
|
|
126
|
-
|
|
131
|
+
**WHEN TO USE TOOLS (do it automatically, don't ask!):**
|
|
132
|
+
- "what's the weather" → use \`weather\` tool
|
|
133
|
+
- "set a reminder" → use \`reminder_set\` tool
|
|
134
|
+
- "what time is it" → use \`datetime\` tool
|
|
135
|
+
- "how much RAM" / system info → use \`system_info\` tool
|
|
136
|
+
- "calculate X" / math → use \`math\` tool
|
|
137
|
+
- "read file X" → use \`read_file\` tool
|
|
138
|
+
- "run command X" → use \`shell\` tool
|
|
139
|
+
- "search for X" → use \`web_search\` tool
|
|
140
|
+
- "play music" / spotify → use \`spotify_*\` tools
|
|
141
|
+
- "create playlist" → use \`spotify_create_playlist\` tool
|
|
142
|
+
- "my notes" / obsidian → use \`obsidian_*\` tools
|
|
127
143
|
|
|
144
|
+
**AVAILABLE TOOLS:**
|
|
128
145
|
${toolDescriptions}
|
|
129
146
|
|
|
130
|
-
|
|
147
|
+
**RULES:**
|
|
148
|
+
1. ALWAYS use a tool when the user's request matches a tool's capability
|
|
149
|
+
2. Do NOT explain how to use tools - just USE them
|
|
150
|
+
3. Do NOT show code examples - execute the tool directly
|
|
151
|
+
4. After tool execution, summarize the result in natural language
|
|
152
|
+
5. If a tool fails, explain why and suggest alternatives
|
|
153
|
+
|
|
154
|
+
**EXAMPLE:**
|
|
155
|
+
User: "What's the weather in Tokyo?"
|
|
156
|
+
Your response should include:
|
|
157
|
+
\`\`\`json
|
|
158
|
+
{"tool": "weather", "args": {"location": "Tokyo"}}
|
|
159
|
+
\`\`\`
|
|
131
160
|
`;
|
|
132
161
|
}
|
|
133
162
|
/**
|
package/dist/cli/index.js
CHANGED
|
@@ -864,12 +864,15 @@ program
|
|
|
864
864
|
}
|
|
865
865
|
}
|
|
866
866
|
catch (e) {
|
|
867
|
-
console.log(chalk.yellow('Gateway not running
|
|
867
|
+
console.log(chalk.yellow('Gateway not running or error connecting.'));
|
|
868
|
+
console.log(chalk.gray(`(${e.message || 'Connection refused'})\n`));
|
|
869
|
+
console.log('Available skills:');
|
|
868
870
|
console.log('');
|
|
869
871
|
console.log(' - obsidian Obsidian note-taking integration');
|
|
870
872
|
console.log(' - weather Weather forecasts');
|
|
871
873
|
console.log(' - reminder Reminders and timers');
|
|
872
874
|
console.log(' - system System info and process management');
|
|
875
|
+
console.log(' - spotify Spotify music playback control');
|
|
873
876
|
console.log('');
|
|
874
877
|
}
|
|
875
878
|
break;
|
|
@@ -925,13 +928,16 @@ program
|
|
|
925
928
|
}
|
|
926
929
|
}
|
|
927
930
|
catch (e) {
|
|
928
|
-
console.log(chalk.yellow('Gateway not running
|
|
931
|
+
console.log(chalk.yellow('Gateway not running or error connecting.'));
|
|
932
|
+
console.log(chalk.gray(`(${e.message || 'Connection refused'})\n`));
|
|
933
|
+
console.log('Built-in tools:');
|
|
929
934
|
console.log('');
|
|
930
935
|
console.log(' CORE');
|
|
931
936
|
console.log(' shell Execute shell commands');
|
|
932
937
|
console.log(' read_file Read file contents');
|
|
933
938
|
console.log(' write_file Write to files');
|
|
934
939
|
console.log(' list_dir List directory contents');
|
|
940
|
+
console.log(' delete_file Delete files/directories');
|
|
935
941
|
console.log(' edit_file Edit files (search/replace)');
|
|
936
942
|
console.log(' fetch_url Fetch web pages');
|
|
937
943
|
console.log(' web_search Search the web');
|
|
@@ -939,6 +945,13 @@ program
|
|
|
939
945
|
console.log(' math Mathematical calculations');
|
|
940
946
|
console.log(' convert Unit conversions');
|
|
941
947
|
console.log('');
|
|
948
|
+
console.log(' SKILLS (when enabled)');
|
|
949
|
+
console.log(' obsidian_* Obsidian notes');
|
|
950
|
+
console.log(' weather Weather forecasts');
|
|
951
|
+
console.log(' reminder_* Reminders & timers');
|
|
952
|
+
console.log(' system_* System info');
|
|
953
|
+
console.log(' spotify_* Spotify control');
|
|
954
|
+
console.log('');
|
|
942
955
|
}
|
|
943
956
|
});
|
|
944
957
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -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;
|
package/dist/tools/loader.js
CHANGED
|
@@ -24,6 +24,7 @@ const obsidian_1 = __importDefault(require("../skills/obsidian"));
|
|
|
24
24
|
const weather_1 = __importDefault(require("../skills/weather"));
|
|
25
25
|
const reminder_1 = __importDefault(require("../skills/reminder"));
|
|
26
26
|
const system_1 = __importDefault(require("../skills/system"));
|
|
27
|
+
const spotify_1 = __importDefault(require("../skills/spotify"));
|
|
27
28
|
/**
|
|
28
29
|
* Register all built-in tools
|
|
29
30
|
*/
|
|
@@ -49,6 +50,7 @@ async function registerSkills(configDir) {
|
|
|
49
50
|
skillManager.register(weather_1.default);
|
|
50
51
|
skillManager.register(reminder_1.default);
|
|
51
52
|
skillManager.register(system_1.default);
|
|
53
|
+
skillManager.register(spotify_1.default);
|
|
52
54
|
// Initialize enabled skills
|
|
53
55
|
await skillManager.initializeEnabled();
|
|
54
56
|
console.log(`[Loader] Registered ${skillManager.list().length} skills`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apexbot",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "ApexBot - Your free, private AI assistant. 100% free with Ollama (local AI). Multi-channel: Telegram, Discord, WebChat. Tools & Skills system
|
|
3
|
+
"version": "1.0.6",
|
|
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": {
|
|
7
7
|
"apexbot": "./dist/cli/index.js"
|