@pulso/companion 0.1.5 → 0.1.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.
Files changed (2) hide show
  1. package/dist/index.js +106 -43
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -55,54 +55,101 @@ function runSwift(code, timeout = 1e4) {
55
55
  child.stdin?.end();
56
56
  });
57
57
  }
58
- var spotifyToken = null;
59
- var spotifyTokenExpiry = 0;
60
- async function getSpotifyToken() {
61
- if (spotifyToken && Date.now() < spotifyTokenExpiry) return spotifyToken;
58
+ var UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
59
+ var searchCache = /* @__PURE__ */ new Map();
60
+ var CACHE_TTL = 7 * 24 * 3600 * 1e3;
61
+ async function spotifySearch(query) {
62
+ const key = query.toLowerCase().trim();
63
+ const cached = searchCache.get(key);
64
+ if (cached && Date.now() - cached.ts < CACHE_TTL) {
65
+ return cached;
66
+ }
67
+ try {
68
+ const res = await fetch(
69
+ `${API_URL}/tools/spotify/search?q=${encodeURIComponent(query)}`,
70
+ { headers: { Authorization: `Bearer ${TOKEN}` } }
71
+ );
72
+ if (res.ok) {
73
+ const data = await res.json();
74
+ if (data.uri) {
75
+ searchCache.set(key, { ...data, ts: Date.now() });
76
+ return data;
77
+ }
78
+ }
79
+ } catch {
80
+ }
81
+ const trackIds = await searchBraveForSpotifyTracks(query);
82
+ if (trackIds.length === 0) return null;
83
+ const meta = await getTrackMetadata(trackIds[0]);
84
+ const result = {
85
+ uri: `spotify:track:${trackIds[0]}`,
86
+ name: meta?.name ?? query,
87
+ artist: meta?.artist ?? "Unknown"
88
+ };
89
+ searchCache.set(key, { ...result, ts: Date.now() });
90
+ pushToServerCache(query, result).catch(() => {
91
+ });
92
+ return result;
93
+ }
94
+ async function searchBraveForSpotifyTracks(query) {
62
95
  try {
63
- const res = await fetch("https://open.spotify.com/embed/track/4u7EnebtmKWzUH433cf5Qv", {
64
- headers: { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" }
96
+ const searchQuery = `spotify track ${query} site:open.spotify.com`;
97
+ const url = `https://search.brave.com/search?q=${encodeURIComponent(searchQuery)}&source=web`;
98
+ const controller = new AbortController();
99
+ const timeout = setTimeout(() => controller.abort(), 8e3);
100
+ const res = await fetch(url, {
101
+ headers: { "User-Agent": UA, Accept: "text/html" },
102
+ signal: controller.signal
65
103
  });
104
+ clearTimeout(timeout);
105
+ if (!res.ok) return [];
66
106
  const html = await res.text();
67
- const match = html.match(/"accessToken":"([^"]+)"/);
68
- if (!match) return null;
69
- spotifyToken = match[1];
70
- spotifyTokenExpiry = Date.now() + 55 * 60 * 1e3;
71
- return spotifyToken;
107
+ const trackIds = /* @__PURE__ */ new Set();
108
+ for (const m of html.matchAll(/https?:\/\/open\.spotify\.com\/track\/([a-zA-Z0-9]{22})/g)) {
109
+ trackIds.add(m[1]);
110
+ }
111
+ return [...trackIds];
72
112
  } catch {
73
- return null;
113
+ return [];
74
114
  }
75
115
  }
76
- async function spotifySearch(query) {
77
- const token = await getSpotifyToken();
78
- if (!token) return null;
116
+ async function getTrackMetadata(trackId) {
79
117
  try {
80
- const res = await fetch(
81
- `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1`,
82
- { headers: { Authorization: `Bearer ${token}` } }
83
- );
84
- if (!res.ok) {
85
- if (res.status === 401) {
86
- spotifyToken = null;
87
- spotifyTokenExpiry = 0;
88
- const newToken = await getSpotifyToken();
89
- if (!newToken) return null;
90
- const retry = await fetch(
91
- `https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1`,
92
- { headers: { Authorization: `Bearer ${newToken}` } }
93
- );
94
- if (!retry.ok) return null;
95
- const retryData = await retry.json();
96
- return retryData.tracks?.items?.[0]?.uri ?? null;
97
- }
98
- return null;
99
- }
100
- const data = await res.json();
101
- return data.tracks?.items?.[0]?.uri ?? null;
118
+ const controller = new AbortController();
119
+ const timeout = setTimeout(() => controller.abort(), 5e3);
120
+ const res = await fetch(`https://open.spotify.com/embed/track/${trackId}`, {
121
+ headers: { "User-Agent": UA, Accept: "text/html" },
122
+ signal: controller.signal
123
+ });
124
+ clearTimeout(timeout);
125
+ if (!res.ok) return null;
126
+ const html = await res.text();
127
+ const scriptMatch = html.match(/<script[^>]*>(\{"props":\{"pageProps".*?\})<\/script>/s);
128
+ if (!scriptMatch) return null;
129
+ const data = JSON.parse(scriptMatch[1]);
130
+ const entity = data?.props?.pageProps?.state?.data?.entity;
131
+ if (!entity?.name) return null;
132
+ return {
133
+ name: entity.name,
134
+ artist: entity.artists?.map((a) => a.name).join(", ") ?? "Unknown"
135
+ };
102
136
  } catch {
103
137
  return null;
104
138
  }
105
139
  }
140
+ async function pushToServerCache(query, result) {
141
+ try {
142
+ await fetch(`${API_URL}/tools/spotify/cache`, {
143
+ method: "POST",
144
+ headers: {
145
+ Authorization: `Bearer ${TOKEN}`,
146
+ "Content-Type": "application/json"
147
+ },
148
+ body: JSON.stringify({ query, ...result })
149
+ });
150
+ } catch {
151
+ }
152
+ }
106
153
  async function handleCommand(command, params) {
107
154
  try {
108
155
  switch (command) {
@@ -170,20 +217,36 @@ async function handleCommand(command, params) {
170
217
  case "search_play": {
171
218
  const query = params.query;
172
219
  if (!query) return { success: false, error: "Missing search query" };
173
- const trackUri = await spotifySearch(query);
174
- if (trackUri) {
175
- await runAppleScript(`tell application "Spotify" to play track "${trackUri}"`);
220
+ const result = await spotifySearch(query);
221
+ if (result) {
222
+ await runAppleScript(`tell application "Spotify" to play track "${result.uri}"`);
176
223
  await new Promise((r) => setTimeout(r, 1500));
177
224
  try {
178
225
  const track = await runAppleScript('tell application "Spotify" to name of current track');
179
226
  const artist = await runAppleScript('tell application "Spotify" to artist of current track');
180
- return { success: true, data: { searched: query, nowPlaying: `${track} - ${artist}`, state: "playing" } };
227
+ const state = await runAppleScript('tell application "Spotify" to player state as string');
228
+ return {
229
+ success: true,
230
+ data: {
231
+ searched: query,
232
+ resolved: `${result.name} - ${result.artist}`,
233
+ nowPlaying: `${track} - ${artist}`,
234
+ state
235
+ }
236
+ };
181
237
  } catch {
182
- return { success: true, data: { searched: query, note: "Playing track" } };
238
+ return { success: true, data: { searched: query, resolved: `${result.name} - ${result.artist}`, note: "Playing track" } };
183
239
  }
184
240
  }
185
241
  await runShell(`open "spotify:search:${encodeURIComponent(query)}"`);
186
- return { success: true, data: { searched: query, note: "Opened Spotify search (API unavailable). Select a song to play." } };
242
+ return {
243
+ success: true,
244
+ data: {
245
+ searched: query,
246
+ note: "Search engine unavailable. Opened Spotify search \u2014 use computer-use (screenshot + click) to select the correct song.",
247
+ requiresComputerUse: true
248
+ }
249
+ };
187
250
  }
188
251
  case "volume": {
189
252
  const level = params.level;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulso/companion",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Pulso Companion — gives your AI agent real control over your computer",
6
6
  "bin": {