engine-dj-mcp 0.11.2 → 0.11.3

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/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # engine-dj-mcp
2
2
 
3
+ [![CI](https://github.com/Venut-Labs/engine-dj-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/Venut-Labs/engine-dj-mcp/actions/workflows/ci.yml)
3
4
  [![npm](https://img.shields.io/npm/v/engine-dj-mcp)](https://www.npmjs.com/package/engine-dj-mcp)
4
5
  [![licence](https://img.shields.io/npm/l/engine-dj-mcp)](./LICENSE)
5
6
 
@@ -58,7 +59,9 @@ the configuration you are reading:
58
59
  }
59
60
  ```
60
61
 
61
- **Requirements:** Node.js 22.13 or newer (for the unflagged `node:sqlite`;
62
+ **Requirements:** Node.js 22.16 or newer (`node:sqlite` stopped needing a
63
+ flag in 22.13, but the pre-write snapshot uses its `backup()`, added in
64
+ 22.16;
62
65
  there are no native dependencies), and an Engine DJ library at schema 3.0.0
63
66
  through 3.0.2 — Engine DJ 4.5 and 5.x.
64
67
 
@@ -1,2 +1,26 @@
1
1
  import { type EngineError } from "../errors.js";
2
+ /**
3
+ * The snapshots to delete, oldest first, from a directory listing.
4
+ *
5
+ * Two name shapes live here: the tagged `${uuid}-${tag}-${stamp}.db` written
6
+ * now, and the untagged `${uuid}-${stamp}.db` an earlier version wrote.
7
+ * Without the second, those sat outside every namespace and were never
8
+ * reclaimed -- up to KEEP full copies of a library, kept forever, on any
9
+ * upgrading user. Two libraries sharing a uuid is precisely why the tag
10
+ * exists, and an untagged file cannot say which of them it came from, so
11
+ * ageing them out under whichever library writes next is the only thing left
12
+ * to do with them.
13
+ *
14
+ * Ordering is by the stamp alone, never by the whole filename. Sorting the
15
+ * names as text compares a tag against a year at the same offset, and a tag
16
+ * is hex: `1e269292c523` sorts *before* `2026-...`, so a library whose path
17
+ * happens to hash to a tag starting 0 or 1 had its newest snapshot land at
18
+ * the head of the list and be deleted -- the file the caller had just been
19
+ * handed as its way back. Measured: it passed locally under the tag
20
+ * `cf11e2d00f88` and failed in CI under `1e269292c523`, same code.
21
+ *
22
+ * Untagged files sort before every tagged one regardless of stamp: they
23
+ * predate the tagged scheme, so they predate anything written since.
24
+ */
25
+ export declare function evictable(names: string[], uuid: string, tag: string): string[];
2
26
  export declare function snapshotLibrary(mdbPath: string, uuid: string, baseDir: string): Promise<string | EngineError>;
@@ -33,7 +33,59 @@ function stamp() {
33
33
  const iso = new Date().toISOString().replace(/[:.]/g, "-");
34
34
  return `${iso}-${String(++counter).padStart(10, "0")}`;
35
35
  }
36
+ /**
37
+ * The snapshots to delete, oldest first, from a directory listing.
38
+ *
39
+ * Two name shapes live here: the tagged `${uuid}-${tag}-${stamp}.db` written
40
+ * now, and the untagged `${uuid}-${stamp}.db` an earlier version wrote.
41
+ * Without the second, those sat outside every namespace and were never
42
+ * reclaimed -- up to KEEP full copies of a library, kept forever, on any
43
+ * upgrading user. Two libraries sharing a uuid is precisely why the tag
44
+ * exists, and an untagged file cannot say which of them it came from, so
45
+ * ageing them out under whichever library writes next is the only thing left
46
+ * to do with them.
47
+ *
48
+ * Ordering is by the stamp alone, never by the whole filename. Sorting the
49
+ * names as text compares a tag against a year at the same offset, and a tag
50
+ * is hex: `1e269292c523` sorts *before* `2026-...`, so a library whose path
51
+ * happens to hash to a tag starting 0 or 1 had its newest snapshot land at
52
+ * the head of the list and be deleted -- the file the caller had just been
53
+ * handed as its way back. Measured: it passed locally under the tag
54
+ * `cf11e2d00f88` and failed in CI under `1e269292c523`, same code.
55
+ *
56
+ * Untagged files sort before every tagged one regardless of stamp: they
57
+ * predate the tagged scheme, so they predate anything written since.
58
+ */
59
+ export function evictable(names, uuid, tag) {
60
+ const tagged = `${uuid}-${tag}-`;
61
+ const legacy = new RegExp(`^${uuid}-(\\d{4}-.*)\\.db$`);
62
+ const mine = [];
63
+ for (const name of names) {
64
+ if (!name.endsWith(".db"))
65
+ continue;
66
+ if (name.startsWith(tagged)) {
67
+ mine.push({ name, old: false, stamp: name.slice(tagged.length, -3) });
68
+ continue;
69
+ }
70
+ const m = legacy.exec(name);
71
+ if (m)
72
+ mine.push({ name, old: true, stamp: m[1] });
73
+ }
74
+ mine.sort((a, b) => (a.old !== b.old ? (a.old ? -1 : 1) : a.stamp < b.stamp ? -1 : a.stamp > b.stamp ? 1 : 0));
75
+ return mine.slice(0, Math.max(0, mine.length - KEEP)).map((x) => x.name);
76
+ }
36
77
  export async function snapshotLibrary(mdbPath, uuid, baseDir) {
78
+ // node:sqlite stopped needing a flag in 22.13, which is where this
79
+ // project's floor used to sit -- but backup() only arrived in 22.16. On
80
+ // 22.13 through 22.15 the read path works perfectly and this one throws
81
+ // "backup is not a function", which is what CI reported on its very first
82
+ // run against the declared floor. `engines` now says 22.16, and npm only
83
+ // enforces that under engine-strict, so the check is here too: a version
84
+ // number a user can act on beats a TypeError from inside a dependency.
85
+ if (typeof backup !== "function") {
86
+ return err("library_unreadable", `This Node cannot snapshot a library before writing to it: node:sqlite gained backup() in ` +
87
+ `22.16.0 and this is ${process.version}. Upgrade Node, or run without --allow-writes.`);
88
+ }
37
89
  let src;
38
90
  try {
39
91
  mkdirSync(baseDir, { recursive: true });
@@ -50,21 +102,8 @@ export async function snapshotLibrary(mdbPath, uuid, baseDir) {
50
102
  await backup(src, dest);
51
103
  src.close();
52
104
  src = undefined;
53
- // Snapshots this library owns: the tagged shape above, plus the untagged
54
- // `${uuid}-${stamp}.db` an earlier version wrote. Without the second,
55
- // those sat outside every namespace and were never reclaimed -- up to
56
- // KEEP full copies of a library, kept forever, on any upgrading user.
57
- // They predate the tag and therefore predate everything written since,
58
- // which is why folding them into one window evicts them first. Two
59
- // libraries sharing a uuid is precisely why the tag exists, and an
60
- // untagged file cannot say which of them it came from -- ageing them out
61
- // under whichever library writes next is the only thing left to do.
62
- const legacy = new RegExp(`^${uuid}-\\d{4}-`);
63
- const mine = readdirSync(baseDir)
64
- .filter((f) => f.endsWith(".db") && (f.startsWith(prefix) || legacy.test(f)))
65
- .sort();
66
- for (const old of mine.slice(0, Math.max(0, mine.length - KEEP))) {
67
- rmSync(join(baseDir, old), { force: true });
105
+ for (const stale of evictable(readdirSync(baseDir), uuid, libraryTag(mdbPath))) {
106
+ rmSync(join(baseDir, stale), { force: true });
68
107
  }
69
108
  return dest;
70
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "engine-dj-mcp",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "MCP server for an Engine DJ library: search and audit it, read cues and beatgrids, and build playlists when you ask. Not affiliated with inMusic or Denon DJ.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "type": "module",
29
29
  "engines": {
30
- "node": ">=22.13.0"
30
+ "node": ">=22.16.0"
31
31
  },
32
32
  "bin": {
33
33
  "engine-dj-mcp": "dist/index.js"