@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.
- package/LICENSE +202 -0
- package/README.md +153 -68
- package/contracts/v1/definitions.schema.json +1 -169
- package/package.json +7 -23
- package/scripts/contract-lib.mjs +0 -7
- package/scripts/contract-semantics.mjs +0 -1904
- package/scripts/moondog.mjs +285 -74
- package/src/core/apple-library-source-status.mjs +0 -16
- package/src/core/apple-projection-domain-services.mjs +168 -36
- package/src/core/application-update.mjs +320 -0
- package/src/core/capability-catalog.mjs +278 -55
- package/src/core/listening-now.mjs +108 -0
- package/src/core/listening-profile-domain-services.mjs +153 -41
- package/src/core/listening-upkeep.mjs +31 -0
- package/src/core/moondog-application.mjs +1908 -104
- package/src/core/playback-preferences.mjs +97 -0
- package/src/core/spotify-listening-intent.mjs +121 -0
- package/src/core/synthetic-domain-services.mjs +24 -3
- package/src/demo/moondog-demo.mjs +4 -22
- package/src/i18n/index.mjs +285 -0
- package/src/i18n/messages/de.mjs +128 -0
- package/src/i18n/messages/en.mjs +129 -0
- package/src/i18n/messages/es.mjs +128 -0
- package/src/i18n/messages/fr.mjs +128 -0
- package/src/i18n/messages/it.mjs +128 -0
- package/src/i18n/messages/ja.mjs +128 -0
- package/src/i18n/messages/ko.mjs +128 -0
- package/src/i18n/messages/pt.mjs +128 -0
- package/src/i18n/messages/zh-Hant.mjs +128 -0
- package/src/i18n/messages/zh.mjs +128 -0
- package/src/i18n/preferences.mjs +34 -0
- package/src/i18n/screen/es.mjs +966 -0
- package/src/i18n/screen/ja.mjs +966 -0
- package/src/i18n/screen/pt.mjs +966 -0
- package/src/i18n/screen/zh.mjs +966 -0
- package/src/importers/apple-music-library/index.mjs +0 -16
- package/src/importers/apple-music-library/sqlite-projection.mjs +51 -9
- package/src/integrations/apple-music/catalog.mjs +123 -14
- package/src/integrations/apple-music/music-app.mjs +232 -0
- package/src/integrations/apple-music/storefront.mjs +28 -0
- package/src/integrations/apple-music/terminal-preview-player.mjs +221 -0
- package/src/integrations/lastfm/history.mjs +221 -0
- package/src/integrations/lastfm/settings.mjs +75 -0
- package/src/integrations/netease/ncm-cli-player.mjs +321 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +26 -18
- package/src/integrations/public-playlists.mjs +2 -1
- package/src/integrations/spotify/account-data-profile.mjs +14 -6
- package/src/integrations/spotify/authentication.mjs +149 -15
- package/src/integrations/spotify/catalog-resolver.mjs +8 -4
- package/src/integrations/spotify/connection.mjs +2 -1
- package/src/integrations/spotify/credential-store.mjs +4 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +11 -11
- package/src/integrations/spotify/history-archive.mjs +1 -1
- package/src/integrations/spotify/library-snapshot.mjs +227 -0
- package/src/integrations/spotify/recent-activity.mjs +2 -0
- package/src/integrations/spotify/service.mjs +873 -69
- package/src/integrations/spotify/web-api-client.mjs +407 -127
- package/src/integrations/web/brave-web.mjs +45 -0
- package/src/integrations/web/codex-web.mjs +15 -82
- package/src/integrations/web/deepseek-web.mjs +81 -0
- package/src/integrations/web/web-research.mjs +119 -0
- package/src/integrations/web/web-result.mjs +215 -0
- package/src/integrations/youtube-music/takeout.mjs +3 -1
- package/src/memory/local-memory-store.mjs +469 -41
- package/src/profile/history-import.mjs +42 -6
- package/src/profile/listener-profile-build.mjs +378 -0
- package/src/profile/listener-profile-store.mjs +130 -0
- package/src/profile/listener-profile-verification.mjs +68 -0
- package/src/profile/listener-profile-worker.mjs +172 -0
- package/src/profile/listening-history-store.mjs +424 -8
- package/src/profile/listening-profile-projection.mjs +242 -76
- package/src/profile/local-music-data.mjs +1 -1
- package/src/profile/music-providers.mjs +11 -7
- package/src/profile/profile-exploration.mjs +84 -0
- package/src/profile/recorded-measures.mjs +24 -0
- package/src/profile/spotify-archive-taste.mjs +0 -4
- package/src/runtime/pi/agent-runtime.mjs +2204 -605
- package/src/runtime/pi/authentication.mjs +34 -1
- package/src/runtime/pi/configured-runtime.mjs +22 -6
- package/src/runtime/pi/conversation-name.mjs +53 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +2 -2
- package/src/runtime/pi/model-catalog.mjs +28 -5
- package/src/runtime/pi/model-settings.mjs +202 -0
- package/src/runtime/pi/model-transport.mjs +10 -0
- package/src/runtime/pi/persistent-credential-store.mjs +67 -31
- package/src/runtime/pi/profile-build-runtime.mjs +160 -0
- package/src/runtime/pi/public-model-catalog.mjs +288 -0
- package/src/runtime/pi/runtime-settings.mjs +10 -2
- package/src/runtime/pi/skills/listener-profile/SKILL.md +46 -0
- package/src/runtime/pi/tool-trace.mjs +37 -0
- package/src/surfaces/cli/auth-command.mjs +12 -4
- package/src/surfaces/cli/brand-components.mjs +310 -132
- package/src/surfaces/cli/brand-theme.mjs +51 -28
- package/src/surfaces/cli/command-completions.mjs +58 -42
- package/src/surfaces/cli/format-output.mjs +199 -388
- package/src/surfaces/cli/history-import-view.mjs +292 -139
- package/src/surfaces/cli/import-guides.mjs +2 -0
- package/src/surfaces/cli/listener-profile-command.mjs +92 -0
- package/src/surfaces/cli/listening-menu.mjs +7 -4
- package/src/surfaces/cli/profile-command.mjs +30 -21
- package/src/surfaces/cli/seasons-chart.mjs +87 -0
- package/src/surfaces/cli/spotify-command.mjs +54 -6
- package/src/surfaces/cli/taste-profile-model.mjs +78 -79
- package/src/surfaces/cli/taste-profile-view.mjs +33 -26
- package/src/surfaces/cli/terminal-art.mjs +140 -35
- package/src/surfaces/cli/tui.mjs +1066 -357
- package/src/surfaces/cli/update-command.mjs +32 -0
- package/src/surfaces/cli/web-command.mjs +53 -14
- package/src/surfaces/html/tasteprint.mjs +43 -70
- package/contracts/v1/capability-policy.schema.json +0 -572
- package/contracts/v1/profile-evidence.schema.json +0 -237
- package/contracts/v1/tool-invocation.schema.json +0 -318
- package/contracts/v1/tool-receipt.schema.json +0 -1066
- package/scripts/demo-studio.mjs +0 -58
- package/src/core/apple-data-migration.mjs +0 -50
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +0 -293
- package/src/demo/moondog-tasteprint-demo.mjs +0 -631
- package/src/integrations/spotify/index.mjs +0 -80
- package/src/surfaces/web/showcase.mjs +0 -272
- package/src/surfaces/web/studio-page.mjs +0 -4148
- 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
|
|
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
|
|
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
|
-
|
|
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:
|
|
20
|
-
spotify_saved_tracks:
|
|
21
|
-
spotify_saved_albums:
|
|
22
|
-
spotify_followed_artists:
|
|
23
|
-
spotify_playlist_memberships:
|
|
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) ||
|