@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,214 @@
1
+ import {
2
+ LOCAL_MUSIC_DATA_SCOPES,
3
+ exportLocalMusicData,
4
+ inspectLocalMusicData,
5
+ resetLocalMusicData,
6
+ resolveLocalMusicDataPaths,
7
+ } from "../../profile/local-music-data.mjs";
8
+ import { sanitizeTerminalText } from "./format-output.mjs";
9
+
10
+ export function dataHelpText() {
11
+ return `# Moondog local music data
12
+
13
+ Commands:
14
+
15
+ - \`moondog data inspect --scope <listening|apple|profile> [--json]\` - preview the selected local state and get its exact reset token
16
+ - \`moondog data export --scope <listening|apple|profile> --output <directory> [--json]\` - create a private, checksummed snapshot without changing the source
17
+ - \`moondog data reset --scope <listening|apple|profile> --confirm <token> [--json]\` - archive the selected state without deleting it
18
+
19
+ Scopes are always explicit.
20
+ The \`profile\` scope combines listening history, the Spotify resolution cache, Tasteprints, and Apple import/projection state.
21
+ OAuth credentials, model settings, conversation memory, Spotify client configuration, and original provider ZIPs are excluded.
22
+ Reset uses recoverable sibling archives and never silently deletes local music data.`;
23
+ }
24
+
25
+ function parseDataArguments(args) {
26
+ const values = [...args];
27
+ const action = values.shift();
28
+ if (action === "-h" || action === "--help") {
29
+ return { action: "help", scope: null, confirmation: null };
30
+ }
31
+ let scope = null;
32
+ let confirmation = null;
33
+ while (values.length > 0) {
34
+ const option = values.shift();
35
+ if (option !== "--scope" && option !== "--confirm") {
36
+ throw new Error(`Unknown data option: ${option}`);
37
+ }
38
+ const value = values.shift();
39
+ if (!value || value.startsWith("--")) {
40
+ throw new Error(`The ${option} option requires a value.`);
41
+ }
42
+ if (option === "--scope") {
43
+ if (scope !== null) {
44
+ throw new Error("The --scope option may be provided only once.");
45
+ }
46
+ scope = value;
47
+ } else {
48
+ if (confirmation !== null) {
49
+ throw new Error("The --confirm option may be provided only once.");
50
+ }
51
+ confirmation = value;
52
+ }
53
+ }
54
+ return { action, scope, confirmation };
55
+ }
56
+
57
+ function requireScope(scope) {
58
+ if (!LOCAL_MUSIC_DATA_SCOPES.includes(scope)) {
59
+ throw new Error(
60
+ `The data command requires --scope ${LOCAL_MUSIC_DATA_SCOPES.join("|")}.`,
61
+ );
62
+ }
63
+ return scope;
64
+ }
65
+
66
+ function writeLine(output, value = "") {
67
+ output.write(`${sanitizeTerminalText(value)}\n`);
68
+ }
69
+
70
+ function formatBytes(value) {
71
+ if (!Number.isSafeInteger(value) || value < 0) return "unknown";
72
+ if (value < 1024) return `${value} B`;
73
+ const units = ["KiB", "MiB", "GiB", "TiB"];
74
+ let amount = value / 1024;
75
+ let unit = units[0];
76
+ for (let index = 1; index < units.length && amount >= 1024; index += 1) {
77
+ amount /= 1024;
78
+ unit = units[index];
79
+ }
80
+ return `${amount.toFixed(amount >= 10 ? 1 : 2)} ${unit}`;
81
+ }
82
+
83
+ function formatInspection(value) {
84
+ const lines = [
85
+ "# Local music data",
86
+ "",
87
+ `- Scope: ${value.scope}`,
88
+ `- State: ${value.state}`,
89
+ `- Subject: ${value.subject_id ?? "not bound"}`,
90
+ `- Selected data: ${value.totals.files.toLocaleString("en-US")} files, ${formatBytes(value.totals.bytes)}`,
91
+ "- Writes: none",
92
+ "",
93
+ "## Components",
94
+ "",
95
+ ...value.components.map(
96
+ (component) =>
97
+ `- ${component.id}: ${component.state}, ${component.files.toLocaleString("en-US")} files, ${formatBytes(component.bytes)} - ${component.path}`,
98
+ ),
99
+ "",
100
+ "## Excluded",
101
+ "",
102
+ ...value.excluded.map((item) => `- ${item}`),
103
+ ];
104
+ if (value.state === "empty") {
105
+ lines.push("", "No selected local music data is available to export or reset.");
106
+ } else if (value.state === "subject_conflict") {
107
+ lines.push(
108
+ "",
109
+ "The selected sources belong to different local music subjects.",
110
+ "Export and reset are disabled until that conflict is reviewed.",
111
+ );
112
+ } else {
113
+ lines.push(
114
+ "",
115
+ "Reset preview:",
116
+ `moondog data reset --scope ${value.scope} --confirm ${value.reset_confirmation}`,
117
+ "This archives the selected state and deletes nothing.",
118
+ );
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+
123
+ function formatExport(value) {
124
+ return [
125
+ "Created a private local music data export.",
126
+ `Scope: ${value.scope}`,
127
+ `Path: ${value.path}`,
128
+ `Contents: ${value.files.toLocaleString("en-US")} files, ${formatBytes(value.bytes)}`,
129
+ `Manifest SHA-256: ${value.manifest_sha256}`,
130
+ "Source changed: no",
131
+ "OAuth, configuration, memory, and original provider ZIPs were not included.",
132
+ ].join("\n");
133
+ }
134
+
135
+ function formatReset(value) {
136
+ return [
137
+ "Archived the selected local music data without deleting files.",
138
+ `Scope: ${value.scope}`,
139
+ `Reset ID: ${value.reset_id}`,
140
+ `Manifest: ${value.manifest_path}`,
141
+ "Archives:",
142
+ ...value.archives.map((archive) => `- ${archive.component}: ${archive.path}`),
143
+ "Deletion: none",
144
+ "OAuth, configuration, memory, and original provider ZIPs were not changed.",
145
+ ].join("\n");
146
+ }
147
+
148
+ export async function runDataCommand({
149
+ args,
150
+ json = false,
151
+ outputPath = null,
152
+ environment = process.env,
153
+ paths = null,
154
+ output = process.stdout,
155
+ now,
156
+ } = {}) {
157
+ const options = parseDataArguments(args ?? []);
158
+ if (!options.action || options.action === "help") {
159
+ if (options.scope || options.confirmation || outputPath || json) {
160
+ throw new Error("Usage: moondog data help.");
161
+ }
162
+ writeLine(output, dataHelpText());
163
+ return;
164
+ }
165
+ if (!new Set(["inspect", "export", "reset"]).has(options.action)) {
166
+ throw new Error(`Unknown data command: ${options.action}`);
167
+ }
168
+ const scope = requireScope(options.scope);
169
+ const resolvedPaths = paths ?? resolveLocalMusicDataPaths({ environment });
170
+ let value;
171
+ if (options.action === "inspect") {
172
+ if (options.confirmation || outputPath) {
173
+ throw new Error(
174
+ "Usage: moondog data inspect --scope <listening|apple|profile> [--json].",
175
+ );
176
+ }
177
+ value = await inspectLocalMusicData({ scope, paths: resolvedPaths });
178
+ } else if (options.action === "export") {
179
+ if (options.confirmation || !outputPath) {
180
+ throw new Error(
181
+ "Usage: moondog data export --scope <listening|apple|profile> --output <directory> [--json].",
182
+ );
183
+ }
184
+ value = await exportLocalMusicData({
185
+ scope,
186
+ outputPath,
187
+ paths: resolvedPaths,
188
+ ...(now ? { now } : {}),
189
+ });
190
+ } else {
191
+ if (!options.confirmation || outputPath) {
192
+ throw new Error(
193
+ "Usage: moondog data reset --scope <listening|apple|profile> --confirm <token> [--json].",
194
+ );
195
+ }
196
+ value = await resetLocalMusicData({
197
+ scope,
198
+ confirmation: options.confirmation,
199
+ paths: resolvedPaths,
200
+ ...(now ? { now } : {}),
201
+ });
202
+ }
203
+ writeLine(
204
+ output,
205
+ json
206
+ ? JSON.stringify(value, null, 2)
207
+ : options.action === "inspect"
208
+ ? formatInspection(value)
209
+ : options.action === "export"
210
+ ? formatExport(value)
211
+ : formatReset(value),
212
+ );
213
+ return value;
214
+ }