engine-dj-mcp 0.11.3 → 0.15.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.
@@ -4,8 +4,63 @@ export interface CreatePlaylistResult {
4
4
  playlist_id: number;
5
5
  title: string;
6
6
  tracks_added: number;
7
+ library: LibraryRef;
7
8
  backup_path: string;
8
9
  }
10
+ /**
11
+ * Which library a write actually landed in. Two connected libraries -- a USB
12
+ * drive and its copy on the computer -- is the ordinary setup, so "which one
13
+ * did that go to" is a question every write result has to answer on its own,
14
+ * without the caller re-deriving it from an argument it may not have passed.
15
+ *
16
+ * It is also the context `undo` needs: an undo reverses the edit in this
17
+ * library and cannot reach a copy Engine DJ has since propagated to another
18
+ * one (measured 2026-09-01, see README).
19
+ */
20
+ export interface LibraryRef {
21
+ uuid: string;
22
+ /** The m.db path, in the `~/...` form list_libraries prints. */
23
+ path: string;
24
+ }
25
+ /**
26
+ * What editing an existing playlist's entries returns. One shape for
27
+ * add/remove/reorder alike -- each op leaves the fields it did not touch
28
+ * undefined rather than the module growing a result type per verb.
29
+ */
30
+ export interface EditResult {
31
+ playlist_id: number;
32
+ tracks_added?: number;
33
+ tracks_removed?: number;
34
+ positions?: number[];
35
+ removed?: {
36
+ position: number;
37
+ track_id: number | null;
38
+ }[];
39
+ undo: UndoStep[];
40
+ /**
41
+ * Whether replaying every step of `undo` puts the playlist back exactly as
42
+ * it was. Always present, never inferred from `undo`'s length: a client
43
+ * that read a missing field as false -- or a full-looking `undo` as
44
+ * complete -- would get the one question that matters here backwards.
45
+ *
46
+ * False only for removeTracksFromPlaylist, and only for a removal that
47
+ * included an entry whose stored origin pair matches no track in this
48
+ * library. Such an entry has no track id to hand back to
49
+ * add_tracks_to_playlist, so no undo step for it can exist; `undo_note`
50
+ * then names those positions. The steps that *are* emitted still run, and
51
+ * still restore everything else to its original position.
52
+ */
53
+ undo_complete: boolean;
54
+ /** Set only when `undo_complete` is false: which positions have no way back, and why. */
55
+ undo_note?: string;
56
+ library: LibraryRef;
57
+ backup_path: string;
58
+ }
59
+ /** One step of the tool call that would undo an edit, in the shape a client replays it. */
60
+ export interface UndoStep {
61
+ tool: string;
62
+ arguments: Record<string, unknown>;
63
+ }
9
64
  export interface OriginRef {
10
65
  uuid: string;
11
66
  trackId: number;
@@ -28,6 +83,48 @@ export declare function resetSessionSnapshots(): void;
28
83
  * re-originated-track test is for, not this readback.
29
84
  */
30
85
  export declare function walkFrom(db: DatabaseSync, listId: number, headId: number): OriginRef[];
86
+ /**
87
+ * One playlist's `PlaylistEntity` rows, in the shape `checkChain` wants: an
88
+ * entry id and the id it links to (0 for "links to nothing").
89
+ *
90
+ * Shared by every op that edits an *existing* playlist's entries --
91
+ * createPlaylist never calls this, because it builds a chain from nothing
92
+ * rather than reading one back.
93
+ */
94
+ export declare function readChain(db: DatabaseSync, listId: number): {
95
+ id: number;
96
+ next: number;
97
+ }[];
98
+ export interface ChainCheck {
99
+ ok: boolean;
100
+ reason?: string;
101
+ /** Entry ids head to tail. Meaningful only when `ok`. */
102
+ order: number[];
103
+ }
104
+ /**
105
+ * Whether one playlist's entry chain is sound enough to edit.
106
+ *
107
+ * Deliberately not `orderByChain` from src/playlists.ts. That function's
108
+ * contract is that it returns every node it was given, degrading a damaged
109
+ * chain to a warning, because a reader that silently returned 30 of 43
110
+ * entries would be worse than one that guesses an order and says so. An edit
111
+ * needs the opposite: a yes or no.
112
+ *
113
+ * Four conditions, and each is needed because different breakages trip
114
+ * different ones. Checking only that the walk covered every row is the trap:
115
+ * a list severed into two runs has two heads, and walking from both covers
116
+ * everything -- measured, on a chain broken on purpose, as "5 of 5" while the
117
+ * walk from the real head reached 2. Checking coverage without also checking
118
+ * that the walk *ended* is a second, subtler version of the same trap: a row
119
+ * whose next points back into an already-linked interior row (two
120
+ * predecessors, no row pointing at 0) can visit every row and still never
121
+ * terminate -- the walk stops only because it revisits a row it has already
122
+ * seen, not because it reached the end.
123
+ */
124
+ export declare function checkChain(rows: {
125
+ id: number;
126
+ next: number;
127
+ }[]): ChainCheck;
31
128
  export declare function sameOrder(a: OriginRef[], b: OriginRef[]): boolean;
32
129
  export declare function createPlaylist(mdbPath: string, uuid: string, input: {
33
130
  title: string;
@@ -35,3 +132,92 @@ export declare function createPlaylist(mdbPath: string, uuid: string, input: {
35
132
  }, opts: {
36
133
  backupDir: string;
37
134
  }): Promise<CreatePlaylistResult | EngineError>;
135
+ /** Where `addTracksToPlaylist`'s `at` can put the new run. */
136
+ export type InsertAt = "end" | "start" | {
137
+ after_position: number;
138
+ };
139
+ /**
140
+ * Adds one or more tracks to an existing playlist, at the start, the end, or
141
+ * after a named position in its current order.
142
+ *
143
+ * Shares its skeleton with createPlaylist: a read-only pre-check first (cheap
144
+ * enough to rule out the common failure modes without ever opening the
145
+ * library for writing), then withWriteTransaction. Where createPlaylist
146
+ * builds a chain from nothing, this extends one that already exists, so it
147
+ * also has to confirm that chain is sound before it touches it -- twice. The
148
+ * pre-check gates it once, both to fail fast (and skip the snapshot) for a
149
+ * playlist that cannot be edited at all, and because validating `at` needs
150
+ * to know how many entries the playlist currently has. The transaction gates
151
+ * it again after BEGIN IMMEDIATE, because that lock is the first moment
152
+ * nothing else can change the chain -- gating on the pre-check's read alone,
153
+ * or reusing the insert position it computed, would be trusting one that
154
+ * could already be stale.
155
+ */
156
+ export declare function addTracksToPlaylist(mdbPath: string, uuid: string, input: {
157
+ listId: number;
158
+ trackIds: number[];
159
+ at: InsertAt;
160
+ }, opts: {
161
+ backupDir: string;
162
+ }): Promise<EditResult | EngineError>;
163
+ /**
164
+ * Removes one or more tracks from an existing playlist by their current
165
+ * position.
166
+ *
167
+ * `trigger_before_delete_PlaylistEntity` (see gen-library.ts's copy of it,
168
+ * taken verbatim from a real 3.0.2 library) relinks each deleted row's
169
+ * predecessor onto its successor as SQLite processes the delete -- verified
170
+ * for a row removed at the head, the middle and the tail, and, by
171
+ * construction of the trigger itself, for a batch that removes several
172
+ * rows, adjacent or not, in one statement. So this function does no chain
173
+ * maintenance of its own; writing any would just be fighting Engine's own
174
+ * trigger. What it does own is checking that the trigger's job actually
175
+ * landed: its `WHEN OLD.trackId > 0` means a row with trackId <= 0 is
176
+ * deleted *without* relinking, leaving its predecessor pointing at a row
177
+ * that is now gone. No real library measured has such a row, but the
178
+ * post-delete check below is the only thing that would ever notice one --
179
+ * and it checks the surviving order against what was expected, not only
180
+ * that some sound chain is left, because "sound" and "right" are different
181
+ * questions and only the second one is what the caller asked for.
182
+ *
183
+ * Shares createPlaylist/addTracksToPlaylist's skeleton: a read-only
184
+ * pre-check first, then withWriteTransaction.
185
+ */
186
+ export declare function removeTracksFromPlaylist(mdbPath: string, uuid: string, input: {
187
+ listId: number;
188
+ positions: number[];
189
+ expectTrackIds?: (number | null)[];
190
+ }, opts: {
191
+ backupDir: string;
192
+ }): Promise<EditResult | EngineError>;
193
+ /**
194
+ * Reorders an existing playlist's entries to a caller-given permutation of
195
+ * its current order.
196
+ *
197
+ * Unlike add/remove, this rewrites links only -- no INSERT, no DELETE -- so
198
+ * none of Engine's PlaylistEntity triggers fire and there is no trigger
199
+ * behaviour to trust or verify here, only the links this function writes
200
+ * itself. `order[i]` names the *current* 1-based position of the track that
201
+ * should end up at position `i + 1` (see validatePermutation for why the
202
+ * spec takes a full permutation rather than a move instruction). Only the
203
+ * entries whose successor actually changes get an UPDATE -- measured on a
204
+ * real library, moving an entry from the middle to the front took exactly
205
+ * two updates, and the identity permutation writes no PlaylistEntity row at
206
+ * all. It is still not a no-op: like every other op here it stamps
207
+ * `Playlist.lastEditTime`, and the session's snapshot is copied before the
208
+ * body ever runs, so the identity case costs a timestamp and (once per
209
+ * session) a snapshot. Left that way deliberately -- "did this permutation
210
+ * change anything" can only be answered honestly after BEGIN IMMEDIATE, by
211
+ * which point the snapshot is already taken, and an edit that reports
212
+ * success without touching lastEditTime would be the one op whose result
213
+ * Engine cannot see.
214
+ *
215
+ * Shares createPlaylist/addTracksToPlaylist/removeTracksFromPlaylist's
216
+ * skeleton: a read-only pre-check first, then withWriteTransaction.
217
+ */
218
+ export declare function reorderPlaylist(mdbPath: string, uuid: string, input: {
219
+ listId: number;
220
+ order: number[];
221
+ }, opts: {
222
+ backupDir: string;
223
+ }): Promise<EditResult | EngineError>;