@audiofool/moondog 0.1.0 → 0.2.0-beta.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.
Files changed (125) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +156 -68
  3. package/contracts/v1/definitions.schema.json +1 -169
  4. package/package.json +7 -23
  5. package/scripts/contract-lib.mjs +0 -7
  6. package/scripts/contract-semantics.mjs +0 -1904
  7. package/scripts/moondog.mjs +285 -74
  8. package/src/core/apple-library-source-status.mjs +0 -16
  9. package/src/core/apple-projection-domain-services.mjs +253 -53
  10. package/src/core/application-update.mjs +320 -0
  11. package/src/core/capability-catalog.mjs +290 -55
  12. package/src/core/listener-avoids.mjs +101 -0
  13. package/src/core/listening-now.mjs +108 -0
  14. package/src/core/listening-profile-domain-services.mjs +189 -41
  15. package/src/core/listening-upkeep.mjs +31 -0
  16. package/src/core/moondog-application.mjs +2115 -107
  17. package/src/core/playback-preferences.mjs +97 -0
  18. package/src/core/spotify-listening-intent.mjs +124 -0
  19. package/src/core/synthetic-domain-services.mjs +24 -3
  20. package/src/core/track-title.mjs +107 -0
  21. package/src/demo/moondog-demo.mjs +4 -22
  22. package/src/i18n/index.mjs +285 -0
  23. package/src/i18n/messages/de.mjs +135 -0
  24. package/src/i18n/messages/en.mjs +136 -0
  25. package/src/i18n/messages/es.mjs +135 -0
  26. package/src/i18n/messages/fr.mjs +135 -0
  27. package/src/i18n/messages/it.mjs +135 -0
  28. package/src/i18n/messages/ja.mjs +135 -0
  29. package/src/i18n/messages/ko.mjs +135 -0
  30. package/src/i18n/messages/pt.mjs +135 -0
  31. package/src/i18n/messages/zh-Hant.mjs +135 -0
  32. package/src/i18n/messages/zh.mjs +135 -0
  33. package/src/i18n/preferences.mjs +34 -0
  34. package/src/i18n/screen/es.mjs +967 -0
  35. package/src/i18n/screen/ja.mjs +967 -0
  36. package/src/i18n/screen/pt.mjs +967 -0
  37. package/src/i18n/screen/zh.mjs +967 -0
  38. package/src/importers/apple-music-library/index.mjs +0 -16
  39. package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
  40. package/src/integrations/apple-music/catalog.mjs +166 -14
  41. package/src/integrations/apple-music/music-app.mjs +232 -0
  42. package/src/integrations/apple-music/storefront.mjs +28 -0
  43. package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
  44. package/src/integrations/lastfm/history.mjs +221 -0
  45. package/src/integrations/lastfm/settings.mjs +75 -0
  46. package/src/integrations/netease/ncm-cli-player.mjs +328 -0
  47. package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
  48. package/src/integrations/public-playlists.mjs +2 -1
  49. package/src/integrations/spotify/account-data-profile.mjs +14 -6
  50. package/src/integrations/spotify/authentication.mjs +149 -15
  51. package/src/integrations/spotify/catalog-resolver.mjs +9 -6
  52. package/src/integrations/spotify/connection.mjs +2 -1
  53. package/src/integrations/spotify/credential-store.mjs +4 -0
  54. package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
  55. package/src/integrations/spotify/history-archive.mjs +1 -1
  56. package/src/integrations/spotify/library-snapshot.mjs +227 -0
  57. package/src/integrations/spotify/recent-activity.mjs +2 -0
  58. package/src/integrations/spotify/service.mjs +873 -69
  59. package/src/integrations/spotify/web-api-client.mjs +410 -127
  60. package/src/integrations/web/brave-web.mjs +45 -0
  61. package/src/integrations/web/codex-web.mjs +15 -82
  62. package/src/integrations/web/deepseek-web.mjs +81 -0
  63. package/src/integrations/web/web-research.mjs +119 -0
  64. package/src/integrations/web/web-result.mjs +215 -0
  65. package/src/integrations/youtube-music/takeout.mjs +3 -1
  66. package/src/memory/local-memory-store.mjs +542 -57
  67. package/src/profile/history-import.mjs +42 -6
  68. package/src/profile/listener-corrections.mjs +22 -0
  69. package/src/profile/listener-profile-build.mjs +461 -0
  70. package/src/profile/listener-profile-store.mjs +130 -0
  71. package/src/profile/listener-profile-verification.mjs +68 -0
  72. package/src/profile/listener-profile-worker.mjs +179 -0
  73. package/src/profile/listening-history-store.mjs +640 -17
  74. package/src/profile/listening-profile-projection.mjs +454 -86
  75. package/src/profile/local-music-data.mjs +1 -1
  76. package/src/profile/moondog-suggestions.mjs +184 -0
  77. package/src/profile/music-providers.mjs +11 -7
  78. package/src/profile/profile-exploration.mjs +92 -0
  79. package/src/profile/recorded-measures.mjs +24 -0
  80. package/src/profile/spotify-archive-taste.mjs +0 -4
  81. package/src/runtime/pi/agent-runtime.mjs +2536 -692
  82. package/src/runtime/pi/authentication.mjs +34 -1
  83. package/src/runtime/pi/configured-runtime.mjs +22 -6
  84. package/src/runtime/pi/conversation-name.mjs +53 -0
  85. package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
  86. package/src/runtime/pi/model-catalog.mjs +28 -5
  87. package/src/runtime/pi/model-settings.mjs +202 -0
  88. package/src/runtime/pi/model-transport.mjs +10 -0
  89. package/src/runtime/pi/persistent-credential-store.mjs +67 -31
  90. package/src/runtime/pi/profile-build-runtime.mjs +161 -0
  91. package/src/runtime/pi/public-model-catalog.mjs +288 -0
  92. package/src/runtime/pi/runtime-settings.mjs +10 -2
  93. package/src/runtime/pi/skills/listener-profile/SKILL.md +48 -0
  94. package/src/runtime/pi/tool-trace.mjs +37 -0
  95. package/src/surfaces/cli/auth-command.mjs +12 -4
  96. package/src/surfaces/cli/brand-components.mjs +310 -132
  97. package/src/surfaces/cli/brand-theme.mjs +51 -28
  98. package/src/surfaces/cli/command-completions.mjs +58 -42
  99. package/src/surfaces/cli/format-output.mjs +199 -388
  100. package/src/surfaces/cli/history-import-view.mjs +292 -139
  101. package/src/surfaces/cli/import-guides.mjs +2 -0
  102. package/src/surfaces/cli/listener-profile-command.mjs +92 -0
  103. package/src/surfaces/cli/listening-menu.mjs +7 -4
  104. package/src/surfaces/cli/profile-command.mjs +30 -21
  105. package/src/surfaces/cli/seasons-chart.mjs +87 -0
  106. package/src/surfaces/cli/spotify-command.mjs +54 -6
  107. package/src/surfaces/cli/taste-profile-model.mjs +78 -79
  108. package/src/surfaces/cli/taste-profile-view.mjs +33 -26
  109. package/src/surfaces/cli/terminal-art.mjs +140 -35
  110. package/src/surfaces/cli/tui.mjs +1066 -357
  111. package/src/surfaces/cli/update-command.mjs +32 -0
  112. package/src/surfaces/cli/web-command.mjs +53 -14
  113. package/src/surfaces/html/tasteprint.mjs +43 -70
  114. package/contracts/v1/capability-policy.schema.json +0 -572
  115. package/contracts/v1/profile-evidence.schema.json +0 -237
  116. package/contracts/v1/tool-invocation.schema.json +0 -318
  117. package/contracts/v1/tool-receipt.schema.json +0 -1066
  118. package/scripts/demo-studio.mjs +0 -58
  119. package/src/core/apple-data-migration.mjs +0 -50
  120. package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
  121. package/src/demo/moondog-tasteprint-demo.mjs +0 -631
  122. package/src/integrations/spotify/index.mjs +0 -80
  123. package/src/surfaces/web/showcase.mjs +0 -272
  124. package/src/surfaces/web/studio-page.mjs +0 -4148
  125. package/src/surfaces/web/studio.mjs +0 -2288
@@ -1,58 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import {
4
- MOONDOG_STUDIO_MAXIMUM_ARCHIVES,
5
- runMoondogStudio,
6
- } from "../src/surfaces/web/studio.mjs";
7
-
8
- const usage = `Usage:
9
- npm run demo:studio
10
- npm run demo:studio -- --from <spotify-history.zip> [--from <spotify-history.zip>]
11
-
12
- Without --from, Moondog opens the fictional production-importer tour.
13
- With one or two explicitly supplied ZIPs, it opens a session-only private profile without installing dependencies or writing persistent profile state.`;
14
-
15
- function terminalText(value) {
16
- return String(value ?? "Unknown failure")
17
- .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, " ")
18
- .replace(/[\u202a-\u202e\u2066-\u2069]/gu, "")
19
- .slice(0, 1_000);
20
- }
21
-
22
- async function main() {
23
- const values = process.argv.slice(2);
24
- if (values.length === 1 && new Set(["--help", "-h"]).has(values[0])) {
25
- process.stdout.write(`${usage}\n`);
26
- return;
27
- }
28
- if (values.length === 0) {
29
- await runMoondogStudio({ demoOnly: true });
30
- return;
31
- }
32
-
33
- const archivePaths = [];
34
- for (let index = 0; index < values.length; index += 2) {
35
- const option = values[index];
36
- const archivePath = values[index + 1];
37
- if (
38
- option !== "--from" ||
39
- !archivePath ||
40
- archivePath.startsWith("--")
41
- ) {
42
- throw new Error(usage);
43
- }
44
- archivePaths.push(archivePath);
45
- }
46
- if (
47
- archivePaths.length < 1 ||
48
- archivePaths.length > MOONDOG_STUDIO_MAXIMUM_ARCHIVES
49
- ) {
50
- throw new Error(usage);
51
- }
52
- await runMoondogStudio({ archivePaths });
53
- }
54
-
55
- main().catch((error) => {
56
- process.stderr.write(`moondog demo studio: ${terminalText(error?.message)}\n`);
57
- process.exitCode = 1;
58
- });
@@ -1,50 +0,0 @@
1
- import { cp, lstat, mkdir } from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- import { legacyAppleMusicImportsRoot, resolveAppleMusicImportsRoot } from "./apple-library-source-status.mjs";
5
- import { resolveAppleMusicProjectionPath } from "./apple-projection-domain-services.mjs";
6
- import { legacyAppleMusicProjectionPath } from "../importers/apple-music-library/index.mjs";
7
-
8
- async function kind(target) {
9
- try {
10
- const metadata = await lstat(target);
11
- return metadata.isSymbolicLink() ? "link" : metadata.isDirectory() ? "directory" : "file";
12
- } catch (error) {
13
- if (error?.code === "ENOENT") return null;
14
- throw error;
15
- }
16
- }
17
-
18
- async function copyOnce(from, to, expected) {
19
- if ((await kind(from)) !== expected || (await kind(to)) !== null) return false;
20
- await mkdir(path.dirname(to), { recursive: true, mode: 0o700 });
21
- await cp(from, to, { recursive: true, errorOnExist: true, force: false, preserveTimestamps: true });
22
- return true;
23
- }
24
-
25
- /**
26
- * Earlier versions kept Apple Music data inside the source checkout. Copy it
27
- * into the default private state directory once, leaving the original in
28
- * place. Runs with an explicit state or Apple location are never touched, so
29
- * isolated runs such as tests cannot pull private data into their directories.
30
- */
31
- export async function migrateLegacyAppleMusicData(environment = process.env) {
32
- const explicit = [
33
- "MOONDOG_STATE_HOME",
34
- "MOONDOG_CONFIG_HOME",
35
- "MOONDOG_APPLE_IMPORTS_ROOT",
36
- "MOONDOG_APPLE_PROJECTION_PATH",
37
- ].some((name) => environment[name]?.trim());
38
- if (explicit) return { copied: [] };
39
- const copied = [];
40
- if (await copyOnce(legacyAppleMusicImportsRoot, resolveAppleMusicImportsRoot(environment), "directory")) {
41
- copied.push("imports");
42
- }
43
- if (await copyOnce(legacyAppleMusicProjectionPath, resolveAppleMusicProjectionPath(environment), "file")) {
44
- copied.push("projection");
45
- for (const suffix of ["-wal", "-shm"]) {
46
- await copyOnce(`${legacyAppleMusicProjectionPath}${suffix}`, `${resolveAppleMusicProjectionPath(environment)}${suffix}`, "file");
47
- }
48
- }
49
- return { copied };
50
- }
@@ -1,293 +0,0 @@
1
- import {
2
- createListenerCorrection,
3
- createListenerCorrectionRetraction,
4
- listenerCorrectionTargetKey,
5
- } from "../profile/listener-corrections.mjs";
6
- import { createPublicTasteprintDemoProfile } from "./moondog-tasteprint-demo.mjs";
7
-
8
- const DEMO_SUBJECT_ID = "70000000-0000-4000-8000-000000000001";
9
-
10
- function demoCorrectionError(code, message) {
11
- const error = new Error(message);
12
- error.name = "ListenerCorrectionError";
13
- error.code = code;
14
- return error;
15
- }
16
-
17
- function activeAssertions(records) {
18
- const retracted = new Set(
19
- records
20
- .filter((record) => record.operation === "retract")
21
- .map((record) => record.retracts_taste_event_id),
22
- );
23
- const superseded = new Set(
24
- records
25
- .filter((record) => record.operation === "assert")
26
- .map((record) => record.supersedes_taste_event_id)
27
- .filter(Boolean),
28
- );
29
- return records
30
- .filter(
31
- (record) =>
32
- record.operation === "assert" &&
33
- !retracted.has(record.taste_event_id) &&
34
- !superseded.has(record.taste_event_id),
35
- )
36
- .sort((left, right) => {
37
- const time = right.recorded_at.localeCompare(left.recorded_at);
38
- return time || right.taste_event_id.localeCompare(left.taste_event_id);
39
- });
40
- }
41
-
42
- function publicCorrection(record) {
43
- const artistCredit = record.extensions?.["moondog.artist_credit"];
44
- return {
45
- correction_id: record.taste_event_id,
46
- state: "active",
47
- entity_type: record.target.entity_type,
48
- label: record.target.label,
49
- ...(artistCredit ? { artist_credit: artistCredit } : {}),
50
- stance: record.signal_type === "avoidance" ? "avoid" : "like",
51
- occurred_at: record.occurred_at,
52
- ...(record.note ? { note: record.note } : {}),
53
- };
54
- }
55
-
56
- function projectedAssertion(record) {
57
- const correction = publicCorrection(record);
58
- return {
59
- correction_id: correction.correction_id,
60
- evidence_id: correction.correction_id,
61
- entity_type: correction.entity_type,
62
- label: correction.label,
63
- ...(correction.artist_credit
64
- ? { artist_credit: correction.artist_credit }
65
- : {}),
66
- stance: correction.stance,
67
- strength: 1,
68
- asserted_at: correction.occurred_at,
69
- ...(correction.note ? { note: correction.note } : {}),
70
- };
71
- }
72
-
73
- function normalizedText(value) {
74
- return String(value).normalize("NFKC").toLocaleLowerCase("und").trim();
75
- }
76
-
77
- function strongPreferenceAvoided(item, avoids) {
78
- const label = normalizedText(item.label);
79
- return avoids.some((avoid) => {
80
- if (normalizedText(avoid.label) !== label) return false;
81
- if (avoid.entity_type === "track") return true;
82
- return /artist/iu.test(item.signal ?? "");
83
- });
84
- }
85
-
86
- function behaviorTrackAvoided(item, avoids) {
87
- const label = normalizedText(item?.label);
88
- const artist = normalizedText(item?.artist_credit);
89
- return avoids.some((avoid) => {
90
- if (avoid.entity_type === "artist") {
91
- return artist && artist === normalizedText(avoid.label);
92
- }
93
- if (avoid.entity_type !== "track" || label !== normalizedText(avoid.label)) {
94
- return false;
95
- }
96
- return !avoid.artist_credit ||
97
- artist === normalizedText(avoid.artist_credit);
98
- });
99
- }
100
-
101
- function directPreference(assertion, kind) {
102
- return {
103
- label: assertion.artist_credit
104
- ? `${assertion.label} / ${assertion.artist_credit}`
105
- : assertion.label,
106
- signal: `Direct ${kind === "demo" ? "demo" : "session"} ${assertion.entity_type} preference`,
107
- };
108
- }
109
-
110
- export class InteractiveTasteprintDemoSession {
111
- #records = [];
112
- #baseProfileFactory;
113
- #kind;
114
-
115
- constructor({
116
- baseProfileFactory = createPublicTasteprintDemoProfile,
117
- kind = "demo",
118
- } = {}) {
119
- if (typeof baseProfileFactory !== "function") {
120
- throw new TypeError("The interactive demo profile factory is invalid.");
121
- }
122
- if (!new Set(["demo", "private_session"]).has(kind)) {
123
- throw new TypeError("The interactive profile session kind is invalid.");
124
- }
125
- this.#baseProfileFactory = baseProfileFactory;
126
- this.#kind = kind;
127
- }
128
-
129
- get activeCount() {
130
- return activeAssertions(this.#records).length;
131
- }
132
-
133
- corrections() {
134
- return activeAssertions(this.#records).map(publicCorrection);
135
- }
136
-
137
- profile() {
138
- const profile = this.#baseProfileFactory();
139
- if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
140
- throw new TypeError("The interactive demo base profile is invalid.");
141
- }
142
- const active = activeAssertions(this.#records).map(projectedAssertion);
143
- const preferences = active.filter((item) => item.stance === "like");
144
- const avoids = active.filter((item) => item.stance === "avoid");
145
- const assertions = this.#records.filter(
146
- (record) => record.operation === "assert",
147
- ).length;
148
- const retractions = this.#records.filter(
149
- (record) => record.operation === "retract",
150
- ).length;
151
- profile.coverage = {
152
- ...profile.coverage,
153
- listener_assertion_events: assertions,
154
- active_listener_assertions: active.length,
155
- listener_retractions: retractions,
156
- };
157
- profile.listener_assertions = {
158
- active,
159
- preferences,
160
- avoids,
161
- retractions,
162
- };
163
- profile.strong_preferences = [
164
- ...preferences.map((item) => directPreference(item, this.#kind)),
165
- ...profile.strong_preferences.filter(
166
- (item) => !strongPreferenceAvoided(item, avoids),
167
- ),
168
- ].slice(0, 10);
169
- profile.curated_preferences = {
170
- ...profile.curated_preferences,
171
- avoids: avoids.map((item) => ({
172
- entity_type: item.entity_type,
173
- label: item.label,
174
- ...(item.artist_credit
175
- ? { artist_credit: item.artist_credit }
176
- : {}),
177
- stance: "avoid",
178
- })),
179
- };
180
- if (profile.listening_behavior) {
181
- // Refilter every listen-again selection from the original bounded profile.
182
- // Descriptive history remains evidence even when the listener avoids it.
183
- // Retraction restores the original candidates without inventing replacements.
184
- profile.listening_behavior = { ...profile.listening_behavior };
185
- for (const key of [
186
- "rediscovery_tracks",
187
- "time_capsule_tracks",
188
- "historical_return_tracks",
189
- "back_to_back_tracks",
190
- ]) {
191
- const candidates = profile.listening_behavior[key];
192
- if (Array.isArray(candidates)) {
193
- profile.listening_behavior[key] = candidates.filter(
194
- (item) => !behaviorTrackAvoided(item, avoids),
195
- );
196
- }
197
- }
198
- }
199
- profile.limitations = [
200
- this.#kind === "demo"
201
- ? "Interactive demo corrections exist only in this Studio process and disappear when it stops."
202
- : "Session-only corrections exist only in this Studio process and disappear when it stops.",
203
- ...profile.limitations,
204
- ].slice(0, 8);
205
- return profile;
206
- }
207
-
208
- record({
209
- entityType,
210
- label,
211
- artistCredit,
212
- stance,
213
- note,
214
- occurredAt,
215
- recordedAt = occurredAt,
216
- } = {}) {
217
- const initial = createListenerCorrection({
218
- subjectId: DEMO_SUBJECT_ID,
219
- entityType,
220
- label,
221
- artistCredit,
222
- stance,
223
- note,
224
- occurredAt,
225
- recordedAt,
226
- });
227
- const previous = activeAssertions(this.#records).find(
228
- (record) => listenerCorrectionTargetKey(record) === initial.targetKey,
229
- );
230
- const value = previous
231
- ? createListenerCorrection({
232
- subjectId: DEMO_SUBJECT_ID,
233
- entityType,
234
- label,
235
- artistCredit,
236
- stance,
237
- note,
238
- occurredAt,
239
- recordedAt,
240
- correctionId: initial.record.taste_event_id,
241
- supersedesCorrectionId: previous.taste_event_id,
242
- })
243
- : initial;
244
- this.#records.push(value.record);
245
- return publicCorrection(value.record);
246
- }
247
-
248
- retract({
249
- correctionId,
250
- occurredAt,
251
- recordedAt = occurredAt,
252
- } = {}) {
253
- const assertion = this.#records.find(
254
- (record) =>
255
- record.operation === "assert" &&
256
- record.taste_event_id === correctionId?.toLocaleLowerCase("en-US"),
257
- );
258
- if (!assertion) {
259
- throw demoCorrectionError(
260
- "listener_correction_not_found",
261
- this.#kind === "demo"
262
- ? "The demo listener correction was not found."
263
- : "The session-only listener correction was not found.",
264
- );
265
- }
266
- if (
267
- !activeAssertions(this.#records).some(
268
- (record) => record.taste_event_id === assertion.taste_event_id,
269
- )
270
- ) {
271
- throw demoCorrectionError(
272
- "listener_correction_inactive",
273
- this.#kind === "demo"
274
- ? "Only an active demo listener correction can be retracted."
275
- : "Only an active session-only listener correction can be retracted.",
276
- );
277
- }
278
- const retraction = createListenerCorrectionRetraction({
279
- subjectId: DEMO_SUBJECT_ID,
280
- retractsCorrectionId: assertion.taste_event_id,
281
- occurredAt,
282
- recordedAt,
283
- });
284
- this.#records.push(retraction);
285
- return {
286
- state: "retracted",
287
- correction_id: assertion.taste_event_id,
288
- retraction_id: retraction.taste_event_id,
289
- entity_type: assertion.target.entity_type,
290
- label: assertion.target.label,
291
- };
292
- }
293
- }