@audiofool/moondog 0.1.0 → 0.2.0-beta.2

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 (121) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +153 -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 +168 -36
  10. package/src/core/application-update.mjs +320 -0
  11. package/src/core/capability-catalog.mjs +278 -55
  12. package/src/core/listening-now.mjs +108 -0
  13. package/src/core/listening-profile-domain-services.mjs +153 -41
  14. package/src/core/listening-upkeep.mjs +31 -0
  15. package/src/core/moondog-application.mjs +1908 -104
  16. package/src/core/playback-preferences.mjs +97 -0
  17. package/src/core/spotify-listening-intent.mjs +121 -0
  18. package/src/core/synthetic-domain-services.mjs +24 -3
  19. package/src/demo/moondog-demo.mjs +4 -22
  20. package/src/i18n/index.mjs +285 -0
  21. package/src/i18n/messages/de.mjs +128 -0
  22. package/src/i18n/messages/en.mjs +129 -0
  23. package/src/i18n/messages/es.mjs +128 -0
  24. package/src/i18n/messages/fr.mjs +128 -0
  25. package/src/i18n/messages/it.mjs +128 -0
  26. package/src/i18n/messages/ja.mjs +128 -0
  27. package/src/i18n/messages/ko.mjs +128 -0
  28. package/src/i18n/messages/pt.mjs +128 -0
  29. package/src/i18n/messages/zh-Hant.mjs +128 -0
  30. package/src/i18n/messages/zh.mjs +128 -0
  31. package/src/i18n/preferences.mjs +34 -0
  32. package/src/i18n/screen/es.mjs +966 -0
  33. package/src/i18n/screen/ja.mjs +966 -0
  34. package/src/i18n/screen/pt.mjs +966 -0
  35. package/src/i18n/screen/zh.mjs +966 -0
  36. package/src/importers/apple-music-library/index.mjs +0 -16
  37. package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
  38. package/src/integrations/apple-music/catalog.mjs +123 -14
  39. package/src/integrations/apple-music/music-app.mjs +232 -0
  40. package/src/integrations/apple-music/storefront.mjs +28 -0
  41. package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
  42. package/src/integrations/lastfm/history.mjs +221 -0
  43. package/src/integrations/lastfm/settings.mjs +75 -0
  44. package/src/integrations/netease/ncm-cli-player.mjs +321 -0
  45. package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
  46. package/src/integrations/public-playlists.mjs +2 -1
  47. package/src/integrations/spotify/account-data-profile.mjs +14 -6
  48. package/src/integrations/spotify/authentication.mjs +149 -15
  49. package/src/integrations/spotify/catalog-resolver.mjs +8 -4
  50. package/src/integrations/spotify/connection.mjs +2 -1
  51. package/src/integrations/spotify/credential-store.mjs +4 -0
  52. package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
  53. package/src/integrations/spotify/history-archive.mjs +1 -1
  54. package/src/integrations/spotify/library-snapshot.mjs +227 -0
  55. package/src/integrations/spotify/recent-activity.mjs +2 -0
  56. package/src/integrations/spotify/service.mjs +873 -69
  57. package/src/integrations/spotify/web-api-client.mjs +407 -127
  58. package/src/integrations/web/brave-web.mjs +45 -0
  59. package/src/integrations/web/codex-web.mjs +15 -82
  60. package/src/integrations/web/deepseek-web.mjs +81 -0
  61. package/src/integrations/web/web-research.mjs +119 -0
  62. package/src/integrations/web/web-result.mjs +215 -0
  63. package/src/integrations/youtube-music/takeout.mjs +3 -1
  64. package/src/memory/local-memory-store.mjs +469 -41
  65. package/src/profile/history-import.mjs +42 -6
  66. package/src/profile/listener-profile-build.mjs +378 -0
  67. package/src/profile/listener-profile-store.mjs +130 -0
  68. package/src/profile/listener-profile-verification.mjs +68 -0
  69. package/src/profile/listener-profile-worker.mjs +172 -0
  70. package/src/profile/listening-history-store.mjs +424 -8
  71. package/src/profile/listening-profile-projection.mjs +242 -76
  72. package/src/profile/local-music-data.mjs +1 -1
  73. package/src/profile/music-providers.mjs +11 -7
  74. package/src/profile/profile-exploration.mjs +84 -0
  75. package/src/profile/recorded-measures.mjs +24 -0
  76. package/src/profile/spotify-archive-taste.mjs +0 -4
  77. package/src/runtime/pi/agent-runtime.mjs +2204 -605
  78. package/src/runtime/pi/authentication.mjs +34 -1
  79. package/src/runtime/pi/configured-runtime.mjs +22 -6
  80. package/src/runtime/pi/conversation-name.mjs +53 -0
  81. package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
  82. package/src/runtime/pi/model-catalog.mjs +28 -5
  83. package/src/runtime/pi/model-settings.mjs +202 -0
  84. package/src/runtime/pi/model-transport.mjs +10 -0
  85. package/src/runtime/pi/persistent-credential-store.mjs +67 -31
  86. package/src/runtime/pi/profile-build-runtime.mjs +160 -0
  87. package/src/runtime/pi/public-model-catalog.mjs +288 -0
  88. package/src/runtime/pi/runtime-settings.mjs +10 -2
  89. package/src/runtime/pi/skills/listener-profile/SKILL.md +46 -0
  90. package/src/runtime/pi/tool-trace.mjs +37 -0
  91. package/src/surfaces/cli/auth-command.mjs +12 -4
  92. package/src/surfaces/cli/brand-components.mjs +310 -132
  93. package/src/surfaces/cli/brand-theme.mjs +51 -28
  94. package/src/surfaces/cli/command-completions.mjs +58 -42
  95. package/src/surfaces/cli/format-output.mjs +199 -388
  96. package/src/surfaces/cli/history-import-view.mjs +292 -139
  97. package/src/surfaces/cli/import-guides.mjs +2 -0
  98. package/src/surfaces/cli/listener-profile-command.mjs +92 -0
  99. package/src/surfaces/cli/listening-menu.mjs +7 -4
  100. package/src/surfaces/cli/profile-command.mjs +30 -21
  101. package/src/surfaces/cli/seasons-chart.mjs +87 -0
  102. package/src/surfaces/cli/spotify-command.mjs +54 -6
  103. package/src/surfaces/cli/taste-profile-model.mjs +78 -79
  104. package/src/surfaces/cli/taste-profile-view.mjs +33 -26
  105. package/src/surfaces/cli/terminal-art.mjs +140 -35
  106. package/src/surfaces/cli/tui.mjs +1066 -357
  107. package/src/surfaces/cli/update-command.mjs +32 -0
  108. package/src/surfaces/cli/web-command.mjs +53 -14
  109. package/src/surfaces/html/tasteprint.mjs +43 -70
  110. package/contracts/v1/capability-policy.schema.json +0 -572
  111. package/contracts/v1/profile-evidence.schema.json +0 -237
  112. package/contracts/v1/tool-invocation.schema.json +0 -318
  113. package/contracts/v1/tool-receipt.schema.json +0 -1066
  114. package/scripts/demo-studio.mjs +0 -58
  115. package/src/core/apple-data-migration.mjs +0 -50
  116. package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
  117. package/src/demo/moondog-tasteprint-demo.mjs +0 -631
  118. package/src/integrations/spotify/index.mjs +0 -80
  119. package/src/surfaces/web/showcase.mjs +0 -272
  120. package/src/surfaces/web/studio-page.mjs +0 -4148
  121. package/src/surfaces/web/studio.mjs +0 -2288
@@ -673,7 +673,7 @@ async function checkpointSqlite(filePath) {
673
673
  } catch (error) {
674
674
  fail(
675
675
  "local_music_data_busy",
676
- "Local music data is busy. Close Moondog Studio and other Moondog processes before reset.",
676
+ "Local music data is busy. Close other Moondog windows and processes before reset.",
677
677
  { cause: error },
678
678
  );
679
679
  } finally {
@@ -1,8 +1,10 @@
1
1
  export const MUSIC_PROVIDER_LABELS = Object.freeze({
2
2
  spotify_account_data: "Spotify",
3
+ spotify_web_api: "Spotify",
3
4
  youtube_music: "YouTube Music",
5
+ lastfm: "Last.fm",
4
6
  qq_music: "QQ Music",
5
- netease: "NetEase Cloud Music",
7
+ netease: "NetEase Music",
6
8
  });
7
9
 
8
10
  export function musicProviderLabel(system) {
@@ -10,16 +12,18 @@ export function musicProviderLabel(system) {
10
12
  }
11
13
 
12
14
  export function collectionCoverage(coverage) {
13
- const spotify = coverage.collection_sources?.find((source) => source.system === "spotify_account_data");
15
+ // An export and a live library read are both Spotify; evidence is already deduplicated across them.
16
+ const spotify = (coverage.collection_sources ?? []).filter((source) => ["spotify_account_data", "spotify_web_api"].includes(source.system));
17
+ const total = (key) => spotify.reduce((sum, source) => sum + (source[key] ?? 0), 0);
14
18
  return {
15
19
  collection_tracks: coverage.collection_tracks ?? 0,
16
20
  collection_sources: coverage.collection_sources ?? [],
17
21
  saved_tracks: coverage.saved_tracks,
18
22
  playlist_memberships: coverage.playlist_memberships,
19
- spotify_profile_evidence: spotify?.evidence_records ?? 0,
20
- spotify_saved_tracks: spotify?.saved_tracks ?? 0,
21
- spotify_saved_albums: spotify?.saved_albums ?? 0,
22
- spotify_followed_artists: spotify?.followed_artists ?? 0,
23
- spotify_playlist_memberships: spotify?.playlist_memberships ?? 0,
23
+ spotify_profile_evidence: total("evidence_records"),
24
+ spotify_saved_tracks: total("saved_tracks"),
25
+ spotify_saved_albums: total("saved_albums"),
26
+ spotify_followed_artists: total("followed_artists"),
27
+ spotify_playlist_memberships: total("playlist_memberships"),
24
28
  };
25
29
  }
@@ -0,0 +1,84 @@
1
+ // The limits below apply to delivery, never to the corpus being analyzed.
2
+ export const PROFILE_SECTIONS = Object.freeze([
3
+ "overview", "history_artists", "history_tracks", "recent_artists", "recent_tracks",
4
+ "history_years", "history_releases", "saved_tracks", "playlist_tracks", "saved_albums",
5
+ "followed_artists", "provider_genres", "listener_preferences", "listener_avoids",
6
+ "provider_evidence", "apple_tracks", "apple_artists", "apple_genres",
7
+ ]);
8
+
9
+ const normalize = (value) => String(value ?? "").normalize("NFKC").toLocaleLowerCase("und");
10
+
11
+ export function profileExplorationInput(value = {}) {
12
+ if (!value || typeof value !== "object" || Array.isArray(value) ||
13
+ Object.keys(value).some((key) => !["section", "query", "offset", "limit", "sort"].includes(key))) {
14
+ throw new TypeError("Profile exploration arguments are invalid");
15
+ }
16
+ const { section = "overview", query = "", offset = 0, limit = 12, sort = "ranked" } = value;
17
+ if (!PROFILE_SECTIONS.includes(section) || typeof query !== "string" || query.length > 256 ||
18
+ !Number.isSafeInteger(offset) || offset < 0 || !Number.isInteger(limit) || limit < 1 || limit > 20 ||
19
+ !["ranked", "least_played", "recent"].includes(sort)) {
20
+ throw new TypeError("Profile exploration selection is invalid");
21
+ }
22
+ if (sort !== "ranked" && !["history_tracks", "history_artists", "recent_tracks", "recent_artists", "apple_tracks"].includes(section)) {
23
+ throw new TypeError("This profile section supports ranked order only");
24
+ }
25
+ if (sort === "recent" && section === "apple_tracks") {
26
+ throw new TypeError("Apple library play counts have no event chronology");
27
+ }
28
+ return { section, query: query.trim(), offset, limit, sort };
29
+ }
30
+
31
+ export function exploreProfileCatalog({ listening, apple, input }) {
32
+ const { section, query, offset, limit, sort } = profileExplorationInput(input);
33
+ const catalog = { ...listening?.sections, ...apple?.sections };
34
+ const all = catalog[section] ?? [];
35
+ const needle = normalize(query);
36
+ let matches = all.filter((row) => !needle || [
37
+ row.name, row.label, row.artist_credit, row.release, row.genre, row.year, row.evidence_kind,
38
+ row.source_label, row.period, row.text, row.playlist_name, ...(row.playlist_names ?? []),
39
+ ]
40
+ .some((text) => normalize(text).includes(needle)));
41
+ if (sort !== "ranked") {
42
+ matches = matches.map((row, index) => ({ row, index })).sort((left, right) => {
43
+ const order = sort === "least_played"
44
+ ? (left.row.play_count ?? Number.MAX_SAFE_INTEGER) - (right.row.play_count ?? Number.MAX_SAFE_INTEGER)
45
+ : String(right.row.last_played_at ?? "").localeCompare(String(left.row.last_played_at ?? ""));
46
+ return order || left.index - right.index;
47
+ }).map(({ row }) => row);
48
+ }
49
+ const items = matches.slice(offset, offset + limit).map((row) => {
50
+ if (!needle || !row.playlist_names) return row;
51
+ // Search every membership, and keep matching names in the bounded display.
52
+ return { ...row, playlist_names: [...row.playlist_names].sort((left, right) =>
53
+ Number(normalize(right).includes(needle)) - Number(normalize(left).includes(needle))) };
54
+ });
55
+ return structuredClone({
56
+ schema_version: "profile-exploration/1",
57
+ analysis_scope: "all_retained_supported_data",
58
+ section,
59
+ coverage: { ...listening?.coverage, ...apple?.coverage },
60
+ sources: listening?.sources ?? [],
61
+ context: listening?.context ?? {},
62
+ section_note: section.startsWith("recent_")
63
+ ? "Recent rankings use the retained-history reference date and recent_window_days, not today's date."
64
+ : section === "history_releases"
65
+ ? "Release depth includes releases meeting release_minimum_distinct_tracks. It does not prove album completion."
66
+ : section === "provider_genres"
67
+ ? "Named provider genre labels only. Unresolved labels remain in provider_evidence."
68
+ : "Complete selected evidence section, before search and pagination.",
69
+ sections: Object.entries(catalog).map(([name, rows]) => ({ name, total: rows.length })),
70
+ total: all.length,
71
+ matched: matches.length,
72
+ offset,
73
+ next_offset: offset + items.length < matches.length ? offset + items.length : null,
74
+ items,
75
+ limitations: [
76
+ "All retained supported records contribute before search, sorting, or pagination. A page is a selection, not the complete profile.",
77
+ "History describes attention; library membership, saved tracks, ratings, provider labels, and explicit listener choices are separate signals.",
78
+ "Spotify private-session plays are excluded from behavioral analysis. Superseded export overlaps are not counted again.",
79
+ "Apple Music is a library snapshot. Its aggregate play counts are not dated plays and must not be added to Spotify event totals.",
80
+ "Apple preferred-track counts exclude active listener Avoids. Original library membership, ratings, and love states remain visible.",
81
+ "Provider genre labels describe catalog metadata, not measured sound. Absence from an export is not a dislike.",
82
+ ],
83
+ });
84
+ }
@@ -0,0 +1,24 @@
1
+ // Sources record different measures for each play. Spotify's extended history
2
+ // records how long a play lasted and, for most plays, whether it was skipped.
3
+ // Spotify account data records only the length. Last.fm and most ListenBrainz
4
+ // listens record neither, and an Apple library counts plays for some tracks
5
+ // only. A measure that no play recorded is unknown: it never becomes a zero
6
+ // that reads as no listening time, never skipped, or unplayed.
7
+
8
+ /** Whether all, some or none of `total` items recorded a measure. */
9
+ export function recordedShare(total, recorded, atLeast = 1) {
10
+ if (recorded >= total * atLeast) return "all";
11
+ return recorded === 0 ? "none" : "some";
12
+ }
13
+
14
+ /**
15
+ * What a set of plays recorded: its listening time and its skips, each "all",
16
+ * "some" or "none". `timedPlayCount` counts plays with a recorded length, and
17
+ * `skipKnownCount` counts plays that say whether they were skipped.
18
+ */
19
+ export function recordedMeasures({ playCount, timedPlayCount, skipKnownCount }) {
20
+ return {
21
+ time: recordedShare(playCount, timedPlayCount),
22
+ skips: recordedShare(playCount, skipKnownCount),
23
+ };
24
+ }
@@ -235,10 +235,6 @@ export function createSpotifyListeningProfileProjection(listening, options) {
235
235
  return createListeningProfileProjection(listening, options);
236
236
  }
237
237
 
238
- export function createSpotifyArchiveProfileProjection(listening, options) {
239
- return createSpotifyListeningProfileProjection(listening, options);
240
- }
241
-
242
238
  function validatedArchivePaths(value) {
243
239
  if (
244
240
  !Array.isArray(value) ||