@audiofool/moondog 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.
Files changed (109) hide show
  1. package/README.md +125 -0
  2. package/assets/brand/moondog-lunar-record/moondog-logo-1x1.png +0 -0
  3. package/contracts/v1/capability-policy.schema.json +572 -0
  4. package/contracts/v1/definitions.schema.json +365 -0
  5. package/contracts/v1/library-track-observation.schema.json +138 -0
  6. package/contracts/v1/listening-event.schema.json +111 -0
  7. package/contracts/v1/profile-evidence.schema.json +237 -0
  8. package/contracts/v1/taste-event.schema.json +258 -0
  9. package/contracts/v1/tool-invocation.schema.json +318 -0
  10. package/contracts/v1/tool-receipt.schema.json +1066 -0
  11. package/contracts/v1/track-ref.schema.json +275 -0
  12. package/contracts/v2/profile-evidence.schema.json +284 -0
  13. package/package.json +105 -0
  14. package/scripts/contract-lib.mjs +361 -0
  15. package/scripts/contract-semantics.mjs +2630 -0
  16. package/scripts/demo-studio.mjs +58 -0
  17. package/scripts/moondog +52 -0
  18. package/scripts/moondog.mjs +954 -0
  19. package/src/core/apple-data-migration.mjs +50 -0
  20. package/src/core/apple-library-source-status.mjs +220 -0
  21. package/src/core/apple-projection-domain-services.mjs +2356 -0
  22. package/src/core/capability-catalog.mjs +577 -0
  23. package/src/core/listening-profile-domain-services.mjs +1010 -0
  24. package/src/core/lyric-library.mjs +121 -0
  25. package/src/core/lyric-profile.mjs +50 -0
  26. package/src/core/lyric-service.mjs +41 -0
  27. package/src/core/moondog-application.mjs +1827 -0
  28. package/src/core/state-directory.mjs +19 -0
  29. package/src/core/synthetic-domain-services.mjs +1485 -0
  30. package/src/core/uuid-v5.mjs +42 -0
  31. package/src/demo/fictional-spotify-history.mjs +400 -0
  32. package/src/demo/moondog-demo.mjs +355 -0
  33. package/src/demo/moondog-interactive-tasteprint-demo.mjs +293 -0
  34. package/src/demo/moondog-tasteprint-demo.mjs +631 -0
  35. package/src/importers/apple-music-library/import-batch.mjs +290 -0
  36. package/src/importers/apple-music-library/index.mjs +76 -0
  37. package/src/importers/apple-music-library/normalize.mjs +1126 -0
  38. package/src/importers/apple-music-library/parse-plist.mjs +357 -0
  39. package/src/importers/apple-music-library/projection-records.mjs +557 -0
  40. package/src/importers/apple-music-library/read-batch.mjs +276 -0
  41. package/src/importers/apple-music-library/sqlite-projection.mjs +1307 -0
  42. package/src/importers/apple-music-library/stable-ids.mjs +19 -0
  43. package/src/importers/apple-music-library/track-snapshot.schema.json +136 -0
  44. package/src/integrations/apple-music/catalog.mjs +770 -0
  45. package/src/integrations/cross-catalog-artist-identity.mjs +222 -0
  46. package/src/integrations/listenbrainz/history-file.mjs +560 -0
  47. package/src/integrations/lyrics/lrclib.mjs +55 -0
  48. package/src/integrations/open-music-similarity/artist-radio.mjs +806 -0
  49. package/src/integrations/public-playlists.mjs +104 -0
  50. package/src/integrations/spotify/account-data-history.mjs +545 -0
  51. package/src/integrations/spotify/account-data-profile.mjs +918 -0
  52. package/src/integrations/spotify/authentication.mjs +791 -0
  53. package/src/integrations/spotify/catalog-resolver.mjs +657 -0
  54. package/src/integrations/spotify/connection.mjs +125 -0
  55. package/src/integrations/spotify/credential-store.mjs +26 -0
  56. package/src/integrations/spotify/extended-streaming-history.mjs +525 -0
  57. package/src/integrations/spotify/history-archive.mjs +59 -0
  58. package/src/integrations/spotify/index.mjs +80 -0
  59. package/src/integrations/spotify/recent-activity.mjs +198 -0
  60. package/src/integrations/spotify/resolution-cache.mjs +260 -0
  61. package/src/integrations/spotify/service.mjs +713 -0
  62. package/src/integrations/spotify/settings.mjs +89 -0
  63. package/src/integrations/spotify/web-api-client.mjs +761 -0
  64. package/src/integrations/web/codex-web.mjs +263 -0
  65. package/src/integrations/youtube-music/takeout.mjs +143 -0
  66. package/src/memory/local-memory-store.mjs +1705 -0
  67. package/src/memory/reflection-worker.mjs +87 -0
  68. package/src/profile/apple-library-import.mjs +28 -0
  69. package/src/profile/history-import.mjs +163 -0
  70. package/src/profile/listener-corrections.mjs +217 -0
  71. package/src/profile/listening-history-store.mjs +2055 -0
  72. package/src/profile/listening-profile-projection.mjs +2520 -0
  73. package/src/profile/local-music-data.mjs +812 -0
  74. package/src/profile/music-import-bundle.mjs +91 -0
  75. package/src/profile/music-providers.mjs +25 -0
  76. package/src/profile/spotify-archive-catalog-hints.mjs +210 -0
  77. package/src/profile/spotify-archive-taste.mjs +482 -0
  78. package/src/runtime/pi/agent-runtime.mjs +6444 -0
  79. package/src/runtime/pi/authentication.mjs +104 -0
  80. package/src/runtime/pi/configured-runtime.mjs +98 -0
  81. package/src/runtime/pi/memory-agent-runtime.mjs +337 -0
  82. package/src/runtime/pi/model-catalog.mjs +62 -0
  83. package/src/runtime/pi/model-transport.mjs +117 -0
  84. package/src/runtime/pi/persistent-credential-store.mjs +670 -0
  85. package/src/runtime/pi/provider-registry.mjs +74 -0
  86. package/src/runtime/pi/runtime-settings.mjs +81 -0
  87. package/src/surfaces/cli/auth-command.mjs +355 -0
  88. package/src/surfaces/cli/brand-components.mjs +390 -0
  89. package/src/surfaces/cli/brand-theme.mjs +231 -0
  90. package/src/surfaces/cli/catalog-command.mjs +509 -0
  91. package/src/surfaces/cli/command-completions.mjs +113 -0
  92. package/src/surfaces/cli/data-command.mjs +214 -0
  93. package/src/surfaces/cli/format-output.mjs +750 -0
  94. package/src/surfaces/cli/history-import-view.mjs +445 -0
  95. package/src/surfaces/cli/import-guides.mjs +9 -0
  96. package/src/surfaces/cli/listenbrainz-command.mjs +129 -0
  97. package/src/surfaces/cli/listening-menu.mjs +171 -0
  98. package/src/surfaces/cli/profile-command.mjs +290 -0
  99. package/src/surfaces/cli/spotify-command.mjs +849 -0
  100. package/src/surfaces/cli/taste-profile-model.mjs +306 -0
  101. package/src/surfaces/cli/taste-profile-view.mjs +272 -0
  102. package/src/surfaces/cli/terminal-art.mjs +295 -0
  103. package/src/surfaces/cli/tui.mjs +1714 -0
  104. package/src/surfaces/cli/web-command.mjs +52 -0
  105. package/src/surfaces/html/tasteprint-artifact.mjs +96 -0
  106. package/src/surfaces/html/tasteprint.mjs +3575 -0
  107. package/src/surfaces/web/showcase.mjs +272 -0
  108. package/src/surfaces/web/studio-page.mjs +4148 -0
  109. package/src/surfaces/web/studio.mjs +2288 -0
@@ -0,0 +1,295 @@
1
+ export const LOGO_MOTION_FRAMES = 64;
2
+ export const LOGO_MOTION_INTERVAL_MS = 125;
3
+
4
+ const turn = Math.PI * 2;
5
+
6
+ const dog = [
7
+ [-0.62, 0.48], [-0.55, 0.29], [-0.47, 0.08], [-0.42, -0.16],
8
+ [-0.4, -0.34], [-0.32, -0.47], [-0.18, -0.61], [-0.17, -0.44],
9
+ [-0.12, -0.43], [-0.1, -0.32], [0.02, -0.27], [0.16, -0.24],
10
+ [0.24, -0.18], [0.28, -0.08], [0.35, -0.03], [0.53, 0.015],
11
+ [0.55, 0.055], [0.53, 0.16], [0.46, 0.22], [0.31, 0.26],
12
+ [0.16, 0.26], [0.13, 0.33], [0.13, 0.56], [0.045, 0.58],
13
+ [0.07, 0.77], [-0.13, 0.75], [-0.3, 0.7], [-0.48, 0.62],
14
+ ];
15
+
16
+ const smallAscii = [
17
+ " /",
18
+ " .-======-. /",
19
+ " .-' .--. [/]",
20
+ " / /\\ o \\",
21
+ " / / \\___ \\",
22
+ "| ((o))== `> |",
23
+ "| \\=======/ |",
24
+ " \\ |====| /",
25
+ " \\ o /====| /",
26
+ " '-.______.-'",
27
+ ];
28
+
29
+ const mediumAscii = [
30
+ " /",
31
+ " .-==========-. //",
32
+ " .-==' .------. '==. [/]",
33
+ " .-' .--' '--. '-/'",
34
+ " .' .-' (o) '-. '.",
35
+ " / .' /\\ (o) '. \\",
36
+ " / / / \\ \\ \\",
37
+ " | | / /\\ \\___ | |",
38
+ " | | / / \\====='-. | |",
39
+ " | | (( O ))======= `> | |",
40
+ " | | \\_//========__/ | |",
41
+ " | | /==========| | |",
42
+ " \\ \\ /===========| / /",
43
+ " \\ '. /============| .' /",
44
+ " '. '-.___________ .-' .'",
45
+ " '-. '--._____.--' .-'",
46
+ " '==-._________.-=='",
47
+ ];
48
+
49
+ const largeAscii = [
50
+ " /",
51
+ " //",
52
+ " .-================-. .//",
53
+ " .-==' .------------. '==. [//]",
54
+ " .-' .--' .------. '--. '-./'",
55
+ " .-' .-' .--' '--. '-. '-.",
56
+ " .' .-' .-' '-. '-. '.",
57
+ " / .' .-' ((o)) /\\ '-. '. \\",
58
+ " / / .' / \\ (o) '. \\ \\",
59
+ " / / / / /\\ \\ \\ \\ \\",
60
+ " | | | / / \\ \\____ | | |",
61
+ " | | | / / \\======'-. | | |",
62
+ " | | | / / \\========'-.| | |",
63
+ " | | | o (( O ))=========== `> | |",
64
+ " | | | \\___//==============/| | |",
65
+ " | | | /=================/ | | |",
66
+ " | | | /============| | | |",
67
+ " \\ \\ \\ /=============| / / /",
68
+ " \\ \\ '. /==============| .' / /",
69
+ " \\ '. '-. /===============| .-' .' /",
70
+ " '. '-. '-._______________.-' .-' .'",
71
+ " '-. '-. '--._______.--' .-' .-'",
72
+ " '-. '--._____________.--' .-'",
73
+ " '==-._________________.-=='",
74
+ ];
75
+
76
+ function insidePolygon(x, y, points) {
77
+ let inside = false;
78
+ for (let index = 0, previous = points.length - 1; index < points.length; previous = index++) {
79
+ const [ax, ay] = points[index];
80
+ const [bx, by] = points[previous];
81
+ if ((ay > y) !== (by > y) && x < (bx - ax) * (y - ay) / (by - ay) + ax) inside = !inside;
82
+ }
83
+ return inside;
84
+ }
85
+
86
+ function bezier(start, first, second, end) {
87
+ return Array.from({ length: 65 }, (_, index) => {
88
+ const t = index / 64;
89
+ const s = 1 - t;
90
+ return [0, 1].map((axis) =>
91
+ s ** 3 * start[axis] + 3 * s ** 2 * t * first[axis]
92
+ + 3 * s * t ** 2 * second[axis] + t ** 3 * end[axis],
93
+ );
94
+ });
95
+ }
96
+
97
+ function drawing(columns, rows) {
98
+ const width = columns * 2;
99
+ const height = rows * 4;
100
+ const dots = new Uint8Array(width * height);
101
+ const radius = Math.min((width - 4) / 2.35, (height - 4) / 2.24);
102
+ const centerX = (width - 1) / 2 - radius * 0.09;
103
+ const centerY = (height - 1) / 2 + radius * 0.045;
104
+ const pixel = 1 / radius;
105
+
106
+ function scan(left, top, right, bottom, predicate, ink) {
107
+ for (let y = Math.max(0, Math.floor(centerY + top * radius)); y <= Math.min(height - 1, Math.ceil(centerY + bottom * radius)); y += 1) {
108
+ for (let x = Math.max(0, Math.floor(centerX + left * radius)); x <= Math.min(width - 1, Math.ceil(centerX + right * radius)); x += 1) {
109
+ const nx = (x - centerX) / radius;
110
+ const ny = (y - centerY) / radius;
111
+ if (predicate(nx, ny, x, y)) dots[y * width + x] = ink ? 1 : 0;
112
+ }
113
+ }
114
+ }
115
+
116
+ function circle(x, y, size, lineWidth = pixel, ink = true) {
117
+ const half = lineWidth / 2;
118
+ scan(x - size - half, y - size - half, x + size + half, y + size + half,
119
+ (nx, ny) => Math.abs(Math.hypot(nx - x, ny - y) - size) <= half, ink);
120
+ }
121
+
122
+ function disc(x, y, size, ink = true) {
123
+ scan(x - size, y - size, x + size, y + size,
124
+ (nx, ny) => Math.hypot(nx - x, ny - y) <= size, ink);
125
+ }
126
+
127
+ function line(points, thickness = pixel, ink = true) {
128
+ for (let index = 1; index < points.length; index += 1) {
129
+ const [ax, ay] = points[index - 1];
130
+ const [bx, by] = points[index];
131
+ const length = Math.hypot(bx - ax, by - ay);
132
+ const steps = Math.max(1, Math.ceil(length * radius * 2));
133
+ for (let step = 0; step <= steps; step += 1) {
134
+ const t = step / steps;
135
+ disc(ax + (bx - ax) * t, ay + (by - ay) * t, thickness / 2, ink);
136
+ }
137
+ }
138
+ }
139
+
140
+ function polygon(points, ink, hatch = false) {
141
+ const xs = points.map(([x]) => x);
142
+ const ys = points.map(([, y]) => y);
143
+ scan(Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys),
144
+ (x, y, dx, dy) => insidePolygon(x, y, points)
145
+ && (!hatch || (dy % 4 < 2 && dx % 9 !== 0)), ink);
146
+ }
147
+
148
+ return { dots, width, height, radius, pixel, circle, disc, line, polygon };
149
+ }
150
+
151
+ function braille(columns, rows, phase) {
152
+ const art = drawing(columns, rows);
153
+ const { radius, pixel, circle, disc, line, polygon } = art;
154
+ const detail = radius >= 39 ? 2 : radius >= 27 ? 1 : 0;
155
+ const grooves = detail === 2
156
+ ? [1, 0.975, 0.935, 0.895, 0.85, 0.8, 0.74, 0.67, 0.59, 0.51]
157
+ : detail === 1 ? [1, 0.955, 0.89, 0.82, 0.74, 0.64, 0.53]
158
+ : [1, 0.91, 0.8, 0.66];
159
+ for (const groove of grooves) circle(0, 0, groove, pixel * 0.95);
160
+
161
+ const rotation = phase / LOGO_MOTION_FRAMES * turn;
162
+ const cosine = Math.cos(rotation);
163
+ const sine = Math.sin(rotation);
164
+
165
+ const craters = [
166
+ [-0.5, -0.64, 0.068], [0.47, -0.54, 0.057], [0.73, 0.28, 0.05],
167
+ ...(detail > 0 ? [[-0.71, -0.02, 0.047], [-0.57, 0.68, 0.047], [0.3, 0.81, 0.059]] : []),
168
+ ];
169
+ for (const [originalX, originalY, size] of craters) {
170
+ const x = originalX * cosine - originalY * sine;
171
+ const y = originalX * sine + originalY * cosine;
172
+ disc(x, y, size + pixel * 2, false);
173
+ circle(x, y, size, pixel);
174
+ if (size * radius > 2.8) circle(x + pixel * 0.5, y + pixel * 0.3, size * 0.53, pixel * 0.8);
175
+ }
176
+
177
+ // These short highlights follow fixed grooves; the record geometry never deforms.
178
+ // Painting the portrait afterward keeps the moving surface behind the dog and headphones.
179
+ for (const [groove, offset, length] of [[grooves[1], -0.91, 0.12], [grooves[3], 2.44, 0.09]]) {
180
+ const angle = rotation + offset;
181
+ const points = Array.from({ length: 13 }, (_, index) => {
182
+ const position = angle - length + index / 12 * length;
183
+ return [Math.cos(position) * groove, Math.sin(position) * groove];
184
+ });
185
+ line(points, pixel * 1.65);
186
+ disc(Math.cos(angle) * groove, Math.sin(angle) * groove, pixel * 1.1);
187
+ }
188
+
189
+ polygon(dog, false);
190
+ polygon(dog, true, true);
191
+ line([...dog, dog[0]], pixel * 0.8);
192
+
193
+ const headband = bezier([-0.42, -0.03], [-0.47, -0.31], [-0.27, -0.58], [-0.18, -0.59]);
194
+ line(headband, Math.max(pixel * 3.5, 0.065));
195
+ line(headband, Math.max(pixel * 1.2, 0.025), false);
196
+ const earX = -0.365;
197
+ const earY = -0.005;
198
+ const earRadius = detail > 0 ? 0.157 : 0.17;
199
+ disc(earX, earY, earRadius + pixel * 1.6, false);
200
+ circle(earX, earY, earRadius, pixel * 1.1);
201
+ circle(earX, earY, earRadius * 0.69, pixel);
202
+ if (detail > 0) circle(earX, earY, earRadius * 0.35, pixel);
203
+ else disc(earX, earY, pixel * 0.9);
204
+ // A small eye and nose keep the dog readable before the hatch pattern resolves.
205
+ disc(0.22, -0.13, pixel * 1.05, false);
206
+ disc(0.53, 0.045, pixel * 0.85);
207
+
208
+ line([[1.17, -1.095], [0.91, -0.8]], Math.max(pixel * 1.8, 0.03));
209
+ const cartridge = [[0.92, -0.845], [0.998, -0.777], [0.808, -0.567], [0.73, -0.635]];
210
+ polygon(cartridge, true);
211
+ line([...cartridge, cartridge[0]], pixel);
212
+ disc(0.914, -0.764, Math.max(pixel * 0.7, 0.021), false);
213
+ line([[0.775, -0.615], [0.735, -0.559]], pixel * 1.3);
214
+ const pulse = Math.sin(rotation * 4);
215
+ const angle = -0.612 + pulse * pixel * 0.85;
216
+ const contact = [Math.cos(angle) * 0.928, Math.sin(angle) * 0.928];
217
+ disc(contact[0], contact[1], pixel * (0.95 + pulse * 0.15));
218
+
219
+ const bit = [[1, 8], [2, 16], [4, 32], [64, 128]];
220
+ return Array.from({ length: rows }, (_, row) => Array.from({ length: columns }, (_, column) => {
221
+ let mask = 0;
222
+ for (let y = 0; y < 4; y += 1) {
223
+ for (let x = 0; x < 2; x += 1) {
224
+ if (art.dots[(row * 4 + y) * art.width + column * 2 + x]) mask |= bit[y][x];
225
+ }
226
+ }
227
+ return mask ? String.fromCodePoint(0x2800 + mask) : " ";
228
+ }).join(""));
229
+ }
230
+
231
+ function ascii(columns, rows, phase) {
232
+ const templates = [
233
+ { lines: largeAscii, orbit: [25.5, 12.5, 22, 9.5], needle: [37, 6] },
234
+ { lines: mediumAscii, orbit: [18, 8.5, 15.5, 6.9], needle: [29, 4] },
235
+ { lines: smallAscii, orbit: [9, 5.2, 7.7, 3.7], needle: [14, 3] },
236
+ ];
237
+ const selected = templates.find(({ lines }) => lines.length <= rows && Math.max(...lines.map((line) => line.length)) <= columns);
238
+ const template = selected?.lines ?? [" .-o-.", "/ (o)>\\", "\\___/ "];
239
+ const width = Math.max(...template.map((line) => line.length));
240
+ const left = Math.max(0, Math.floor((columns - width) / 2));
241
+ const top = Math.max(0, Math.floor((rows - template.length) / 2));
242
+ const animated = template.map((line) => Array.from(line.padEnd(width)));
243
+ if (selected) {
244
+ const [centerX, centerY, radiusX, radiusY] = selected.orbit;
245
+ const [needleX, needleY] = selected.needle;
246
+ const angle = phase / LOGO_MOTION_FRAMES * turn - 2.17;
247
+ // A crater and two lighter groove marks travel around the hand-drawn perimeter.
248
+ for (const [offset, mark] of [[-0.19, "."], [2.47, ":"], [0, "o"]]) {
249
+ const x = Math.round(centerX + Math.cos(angle + offset) * radiusX);
250
+ const y = Math.round(centerY + Math.sin(angle + offset) * radiusY);
251
+ if (x >= needleX && y <= needleY) continue;
252
+ if (animated[y]?.[x] !== undefined) animated[y][x] = mark;
253
+ }
254
+ }
255
+ return Array.from({ length: rows }, (_, row) => {
256
+ const content = animated[row - top]?.join("") ?? "";
257
+ return (" ".repeat(left) + content).slice(0, columns).padEnd(columns);
258
+ });
259
+ }
260
+
261
+ /**
262
+ * Original terminal drawing, composed from geometry and hand-lettered ASCII.
263
+ * Braille uses a 2-by-4 dot grid with square dots for a typical 1:2 terminal cell.
264
+ * phase advances the moon surface and groove highlights through an eight-second loop.
265
+ * The portrait and cartridge stay fixed; scheduling and motion preferences belong to the caller.
266
+ */
267
+ export function renderLunarRecord({ columns = 56, rows = 24, style = "braille", phase = 0 } = {}) {
268
+ const width = Number.isFinite(columns) ? Math.max(0, Math.floor(columns)) : 56;
269
+ const height = Number.isFinite(rows) ? Math.max(0, Math.floor(rows)) : 24;
270
+ if (!width || !height) return Array.from({ length: height }, () => "");
271
+ const frame = Number.isFinite(phase)
272
+ ? ((Math.floor(phase) % LOGO_MOTION_FRAMES) + LOGO_MOTION_FRAMES) % LOGO_MOTION_FRAMES
273
+ : 0;
274
+ if (style === "ascii" || width < 20 || height < 9) return ascii(width, height, frame);
275
+ return braille(width, height, frame);
276
+ }
277
+
278
+ /** Five-pixel letterforms, packed into three terminal rows without color or escapes. */
279
+ export function renderMoondogWordmark() {
280
+ const letters = {
281
+ M: ["10001", "11011", "10101", "10001", "10001"],
282
+ O: ["01110", "10001", "10001", "10001", "01110"],
283
+ N: ["10001", "11001", "10101", "10011", "10001"],
284
+ D: ["11110", "10001", "10001", "10001", "11110"],
285
+ G: ["01110", "10000", "10111", "10001", "01110"],
286
+ };
287
+ const blocks = [" ", "▄", "▀", "█"];
288
+ return Array.from({ length: 3 }, (_, row) => Array.from("MOONDOG", (letter) =>
289
+ Array.from({ length: 5 }, (_, column) => {
290
+ const upper = letters[letter][row * 2][column] === "1" ? 2 : 0;
291
+ const lower = letters[letter][row * 2 + 1]?.[column] === "1" ? 1 : 0;
292
+ return blocks[upper + lower];
293
+ }).join(""),
294
+ ).join(" "));
295
+ }