@mulmoclaude/spotify-plugin 0.1.0

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.
@@ -0,0 +1,170 @@
1
+ import { z } from 'zod';
2
+ import { TokensSchema, ClientConfigSchema, PendingAuthSchema } from './schemas';
3
+ /** Persisted OAuth tokens (`tokens.json`). Refresh-token rotation
4
+ * policy: when Spotify omits `refreshToken` on a refresh response,
5
+ * the prior value is kept. `scopes` is the granted scope set so
6
+ * callers can fail fast on a kind-vs-scope mismatch. */
7
+ export type SpotifyTokens = z.infer<typeof TokensSchema>;
8
+ /** Persisted client config (`client.json`). User pastes their
9
+ * Spotify Developer Dashboard Client ID into this file (or
10
+ * configures it via the View). Per-machine secret. */
11
+ export type SpotifyClientConfig = z.infer<typeof ClientConfigSchema>;
12
+ /** In-memory record kept between `connect` and `oauthCallback`,
13
+ * keyed by single-use `state`. Lives in the host process only —
14
+ * not persisted. */
15
+ export type PendingAuthorization = z.infer<typeof PendingAuthSchema>;
16
+ /** Refresh-response fields after parsing the raw Spotify token
17
+ * response. `refreshToken` is optional because Spotify normally
18
+ * omits it (the prior token stays valid). */
19
+ export interface RefreshResponseFields {
20
+ accessToken: string;
21
+ refreshToken?: string;
22
+ expiresInSec: number;
23
+ scopes?: readonly string[];
24
+ }
25
+ /** Reason codes the plugin returns to the LLM / View when an
26
+ * operation can't proceed. The dispatch contract documented in
27
+ * plans/done/feat-spotify-plugin.md keeps these aligned with the
28
+ * user-facing `instructions` strings. */
29
+ export type SpotifyError = {
30
+ kind: "client_id_missing";
31
+ instructions: string;
32
+ setupGuide: string;
33
+ } | {
34
+ kind: "not_connected";
35
+ instructions: string;
36
+ } | {
37
+ kind: "auth_expired";
38
+ detail: string;
39
+ instructions: string;
40
+ } | {
41
+ kind: "transient_error";
42
+ detail: string;
43
+ instructions: string;
44
+ } | {
45
+ kind: "unknown_state";
46
+ instructions: string;
47
+ } | {
48
+ kind: "redirect_uri_mismatch";
49
+ instructions: string;
50
+ } | {
51
+ kind: "rate_limited";
52
+ retryAfterSec: number;
53
+ instructions: string;
54
+ } | {
55
+ kind: "spotify_api_error";
56
+ status: number;
57
+ body: string;
58
+ instructions: string;
59
+ };
60
+ /** A track in a normalised, View-friendly shape. The full Spotify
61
+ * response carries dozens of fields the View doesn't render; we
62
+ * reduce it at the plugin boundary to (1) cap response size for
63
+ * the LLM context window, (2) decouple the View from Spotify's
64
+ * API drift. */
65
+ export interface NormalisedTrack {
66
+ id: string;
67
+ name: string;
68
+ artists: string[];
69
+ album: string;
70
+ durationMs: number;
71
+ /** Spotify Web URL — the View uses `runtime.openUrl(track.url)`
72
+ * to open the track in the user's Spotify client. Optional
73
+ * because locally-uploaded tracks and podcast episodes carry no
74
+ * `external_urls.spotify`; the View must guard the click handler
75
+ * against an undefined value. */
76
+ url?: string;
77
+ /** Cover-art URL (smallest available). Optional: tracks under
78
+ * podcasts / locally-uploaded files don't carry album art. */
79
+ imageUrl?: string;
80
+ }
81
+ export interface NormalisedPlaylist {
82
+ id: string;
83
+ name: string;
84
+ /** Author-provided description; empty string when absent. */
85
+ description: string;
86
+ trackCount: number;
87
+ /** Optional for the same reason as NormalisedTrack.url. */
88
+ url?: string;
89
+ imageUrl?: string;
90
+ }
91
+ /** A `recently-played` item carries a `playedAt` timestamp the
92
+ * Liked / Playlists endpoints don't. Composed of `NormalisedTrack`
93
+ * + the play timestamp. */
94
+ export interface RecentlyPlayedItem {
95
+ track: NormalisedTrack;
96
+ /** ISO-8601 timestamp from Spotify's `played_at`. */
97
+ playedAt: string;
98
+ }
99
+ /** Search results return mixed entity types; Spotify groups them
100
+ * by category (`tracks.items[]`, `artists.items[]`, etc.). The
101
+ * plugin normalises each category separately. */
102
+ export interface NormalisedArtist {
103
+ id: string;
104
+ name: string;
105
+ /** Spotify's `genres` field — usually empty for niche artists. */
106
+ genres: string[];
107
+ /** 0-100. Optional because some search results don't carry it. */
108
+ popularity?: number;
109
+ url?: string;
110
+ imageUrl?: string;
111
+ }
112
+ export interface NormalisedAlbum {
113
+ id: string;
114
+ name: string;
115
+ artists: string[];
116
+ /** ISO date or year-only string Spotify returns ("2024" /
117
+ * "2024-05-15"). Stored verbatim — the View formats. */
118
+ releaseDate: string;
119
+ totalTracks: number;
120
+ url?: string;
121
+ imageUrl?: string;
122
+ }
123
+ /** Aggregate result from the `search` kind. Categories are present
124
+ * iff the caller asked for them; absent categories are simply
125
+ * omitted from the object. */
126
+ export interface SearchResult {
127
+ tracks?: NormalisedTrack[];
128
+ artists?: NormalisedArtist[];
129
+ albums?: NormalisedAlbum[];
130
+ playlists?: NormalisedPlaylist[];
131
+ }
132
+ /** Spotify Connect device (a place where the user can play music —
133
+ * desktop app, phone, web player, smart speaker). The View shows
134
+ * a dropdown so the user can pick a target device.
135
+ *
136
+ * `id` may be null when Spotify returns a restricted device — for
137
+ * some account states / DRM restrictions, Spotify lists a device
138
+ * but withholds its ID, leaving it informational but un-targetable.
139
+ * Dropping these would underreport the user's setup; the View
140
+ * surfaces them but disables the Transfer button (Codex review on
141
+ * PR #1171). */
142
+ export interface NormalisedDevice {
143
+ id: string | null;
144
+ name: string;
145
+ /** "Computer" / "Smartphone" / "Speaker" — Spotify's `type`. */
146
+ type: string;
147
+ isActive: boolean;
148
+ /** 0-100, present when the device exposes volume control. */
149
+ volumePercent?: number;
150
+ }
151
+ /** Persisted at `runtime.files.config/profile.json`. Caches
152
+ * `/v1/me`'s `product` field so we don't re-call Spotify on every
153
+ * `play` dispatch. TTL keeps the cache fresh enough that a user
154
+ * upgrading from Free → Premium sees controls within ~24h
155
+ * without manually reconnecting. */
156
+ export interface SpotifyProfile {
157
+ /** Spotify user ID (`/v1/me`'s `id` field). Bound to the cache
158
+ * so reconnecting with a different Spotify account doesn't
159
+ * serve the previous account's `product` for the rest of the
160
+ * TTL — Codex review on PR #1171. Empty string for cache
161
+ * records persisted before the account-scoping fix landed. */
162
+ userId: string;
163
+ /** "premium" / "free" / "open" (open is a legacy free-tier
164
+ * marker Spotify still emits for some accounts). */
165
+ product: string;
166
+ /** Free-form display name from `/v1/me`; surfaced in `diagnose`. */
167
+ displayName: string;
168
+ /** Epoch ms when this snapshot was fetched. */
169
+ fetchedAtMs: number;
170
+ }
package/dist/vue.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ export declare const plugin: {
2
+ toolDefinition: {
3
+ type: "function";
4
+ name: "manageSpotify";
5
+ description: string;
6
+ parameters: {
7
+ type: "object";
8
+ properties: {
9
+ kind: {
10
+ type: string;
11
+ enum: ("connect" | "oauthCallback" | "status" | "diagnose" | "liked" | "playlists" | "playlistTracks" | "recent" | "nowPlaying" | "search" | "play" | "pause" | "next" | "previous" | "seek" | "setVolume" | "transferPlayback" | "getDevices")[];
12
+ description: string;
13
+ };
14
+ redirectUri: {
15
+ type: string;
16
+ description: string;
17
+ };
18
+ code: {
19
+ type: string;
20
+ };
21
+ state: {
22
+ type: string;
23
+ };
24
+ error: {
25
+ type: string;
26
+ };
27
+ limit: {
28
+ type: string;
29
+ description: string;
30
+ };
31
+ playlistId: {
32
+ type: string;
33
+ description: string;
34
+ };
35
+ query: {
36
+ type: string;
37
+ description: string;
38
+ };
39
+ types: {
40
+ type: string;
41
+ items: {
42
+ type: string;
43
+ enum: ("track" | "artist" | "album" | "playlist")[];
44
+ };
45
+ description: string;
46
+ };
47
+ deviceId: {
48
+ type: string;
49
+ description: string;
50
+ };
51
+ contextUri: {
52
+ type: string;
53
+ description: string;
54
+ };
55
+ trackUris: {
56
+ type: string;
57
+ items: {
58
+ type: string;
59
+ };
60
+ description: string;
61
+ };
62
+ positionMs: {
63
+ type: string;
64
+ description: string;
65
+ };
66
+ volumePercent: {
67
+ type: string;
68
+ description: string;
69
+ };
70
+ play: {
71
+ type: string;
72
+ description: string;
73
+ };
74
+ };
75
+ required: string[];
76
+ };
77
+ };
78
+ viewComponent: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, any>;
79
+ previewComponent: import('vue').DefineComponent<import('./Preview.vue').Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<import('./Preview.vue').Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
80
+ };