@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.
- package/README.md +125 -0
- package/assets/brand/moondog-lunar-record/moondog-logo-1x1.png +0 -0
- package/contracts/v1/capability-policy.schema.json +572 -0
- package/contracts/v1/definitions.schema.json +365 -0
- package/contracts/v1/library-track-observation.schema.json +138 -0
- package/contracts/v1/listening-event.schema.json +111 -0
- package/contracts/v1/profile-evidence.schema.json +237 -0
- package/contracts/v1/taste-event.schema.json +258 -0
- package/contracts/v1/tool-invocation.schema.json +318 -0
- package/contracts/v1/tool-receipt.schema.json +1066 -0
- package/contracts/v1/track-ref.schema.json +275 -0
- package/contracts/v2/profile-evidence.schema.json +284 -0
- package/package.json +105 -0
- package/scripts/contract-lib.mjs +361 -0
- package/scripts/contract-semantics.mjs +2630 -0
- package/scripts/demo-studio.mjs +58 -0
- package/scripts/moondog +52 -0
- package/scripts/moondog.mjs +954 -0
- package/src/core/apple-data-migration.mjs +50 -0
- package/src/core/apple-library-source-status.mjs +220 -0
- package/src/core/apple-projection-domain-services.mjs +2356 -0
- package/src/core/capability-catalog.mjs +577 -0
- package/src/core/listening-profile-domain-services.mjs +1010 -0
- package/src/core/lyric-library.mjs +121 -0
- package/src/core/lyric-profile.mjs +50 -0
- package/src/core/lyric-service.mjs +41 -0
- package/src/core/moondog-application.mjs +1827 -0
- package/src/core/state-directory.mjs +19 -0
- package/src/core/synthetic-domain-services.mjs +1485 -0
- package/src/core/uuid-v5.mjs +42 -0
- package/src/demo/fictional-spotify-history.mjs +400 -0
- package/src/demo/moondog-demo.mjs +355 -0
- package/src/demo/moondog-interactive-tasteprint-demo.mjs +293 -0
- package/src/demo/moondog-tasteprint-demo.mjs +631 -0
- package/src/importers/apple-music-library/import-batch.mjs +290 -0
- package/src/importers/apple-music-library/index.mjs +76 -0
- package/src/importers/apple-music-library/normalize.mjs +1126 -0
- package/src/importers/apple-music-library/parse-plist.mjs +357 -0
- package/src/importers/apple-music-library/projection-records.mjs +557 -0
- package/src/importers/apple-music-library/read-batch.mjs +276 -0
- package/src/importers/apple-music-library/sqlite-projection.mjs +1307 -0
- package/src/importers/apple-music-library/stable-ids.mjs +19 -0
- package/src/importers/apple-music-library/track-snapshot.schema.json +136 -0
- package/src/integrations/apple-music/catalog.mjs +770 -0
- package/src/integrations/cross-catalog-artist-identity.mjs +222 -0
- package/src/integrations/listenbrainz/history-file.mjs +560 -0
- package/src/integrations/lyrics/lrclib.mjs +55 -0
- package/src/integrations/open-music-similarity/artist-radio.mjs +806 -0
- package/src/integrations/public-playlists.mjs +104 -0
- package/src/integrations/spotify/account-data-history.mjs +545 -0
- package/src/integrations/spotify/account-data-profile.mjs +918 -0
- package/src/integrations/spotify/authentication.mjs +791 -0
- package/src/integrations/spotify/catalog-resolver.mjs +657 -0
- package/src/integrations/spotify/connection.mjs +125 -0
- package/src/integrations/spotify/credential-store.mjs +26 -0
- package/src/integrations/spotify/extended-streaming-history.mjs +525 -0
- package/src/integrations/spotify/history-archive.mjs +59 -0
- package/src/integrations/spotify/index.mjs +80 -0
- package/src/integrations/spotify/recent-activity.mjs +198 -0
- package/src/integrations/spotify/resolution-cache.mjs +260 -0
- package/src/integrations/spotify/service.mjs +713 -0
- package/src/integrations/spotify/settings.mjs +89 -0
- package/src/integrations/spotify/web-api-client.mjs +761 -0
- package/src/integrations/web/codex-web.mjs +263 -0
- package/src/integrations/youtube-music/takeout.mjs +143 -0
- package/src/memory/local-memory-store.mjs +1705 -0
- package/src/memory/reflection-worker.mjs +87 -0
- package/src/profile/apple-library-import.mjs +28 -0
- package/src/profile/history-import.mjs +163 -0
- package/src/profile/listener-corrections.mjs +217 -0
- package/src/profile/listening-history-store.mjs +2055 -0
- package/src/profile/listening-profile-projection.mjs +2520 -0
- package/src/profile/local-music-data.mjs +812 -0
- package/src/profile/music-import-bundle.mjs +91 -0
- package/src/profile/music-providers.mjs +25 -0
- package/src/profile/spotify-archive-catalog-hints.mjs +210 -0
- package/src/profile/spotify-archive-taste.mjs +482 -0
- package/src/runtime/pi/agent-runtime.mjs +6444 -0
- package/src/runtime/pi/authentication.mjs +104 -0
- package/src/runtime/pi/configured-runtime.mjs +98 -0
- package/src/runtime/pi/memory-agent-runtime.mjs +337 -0
- package/src/runtime/pi/model-catalog.mjs +62 -0
- package/src/runtime/pi/model-transport.mjs +117 -0
- package/src/runtime/pi/persistent-credential-store.mjs +670 -0
- package/src/runtime/pi/provider-registry.mjs +74 -0
- package/src/runtime/pi/runtime-settings.mjs +81 -0
- package/src/surfaces/cli/auth-command.mjs +355 -0
- package/src/surfaces/cli/brand-components.mjs +390 -0
- package/src/surfaces/cli/brand-theme.mjs +231 -0
- package/src/surfaces/cli/catalog-command.mjs +509 -0
- package/src/surfaces/cli/command-completions.mjs +113 -0
- package/src/surfaces/cli/data-command.mjs +214 -0
- package/src/surfaces/cli/format-output.mjs +750 -0
- package/src/surfaces/cli/history-import-view.mjs +445 -0
- package/src/surfaces/cli/import-guides.mjs +9 -0
- package/src/surfaces/cli/listenbrainz-command.mjs +129 -0
- package/src/surfaces/cli/listening-menu.mjs +171 -0
- package/src/surfaces/cli/profile-command.mjs +290 -0
- package/src/surfaces/cli/spotify-command.mjs +849 -0
- package/src/surfaces/cli/taste-profile-model.mjs +306 -0
- package/src/surfaces/cli/taste-profile-view.mjs +272 -0
- package/src/surfaces/cli/terminal-art.mjs +295 -0
- package/src/surfaces/cli/tui.mjs +1714 -0
- package/src/surfaces/cli/web-command.mjs +52 -0
- package/src/surfaces/html/tasteprint-artifact.mjs +96 -0
- package/src/surfaces/html/tasteprint.mjs +3575 -0
- package/src/surfaces/web/showcase.mjs +272 -0
- package/src/surfaces/web/studio-page.mjs +4148 -0
- package/src/surfaces/web/studio.mjs +2288 -0
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
export function sanitizeTerminalText(value) {
|
|
2
|
+
return String(value)
|
|
3
|
+
.replaceAll("\r\n", "\n")
|
|
4
|
+
.replaceAll("\r", "\n")
|
|
5
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, "")
|
|
6
|
+
.replace(/[\u202a-\u202e\u2066-\u2069]/gi, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function words(value) {
|
|
10
|
+
return String(value ?? "unknown").replaceAll("_", " ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatSource(source) {
|
|
14
|
+
if (!source.latest) return "- Apple Music library: not imported";
|
|
15
|
+
return [
|
|
16
|
+
`- Apple Music library: ${formatInteger(source.latest.tracks)} tracks and ${formatInteger(source.latest.playlists)} playlists, as of ${String(source.latest.captured_at).slice(0, 10)}`,
|
|
17
|
+
source.semantics?.complete_listening_history_available
|
|
18
|
+
? "- Includes a full listening history"
|
|
19
|
+
: "- A library shows what you keep, not every play",
|
|
20
|
+
].join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const serviceNames = {
|
|
24
|
+
spotify: "Spotify",
|
|
25
|
+
listenbrainz: "ListenBrainz",
|
|
26
|
+
lastfm: "Last.fm",
|
|
27
|
+
apple_music: "Apple Music",
|
|
28
|
+
youtube_music: "YouTube Music",
|
|
29
|
+
qq_music: "QQ Music",
|
|
30
|
+
netease: "NetEase Cloud Music",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function formatProfile(profile) {
|
|
34
|
+
if (profile.state !== "ready") {
|
|
35
|
+
return `- Not ready yet${profile.reason ? `: ${profile.reason}` : ""}\n- \`/import\` brings in your history`;
|
|
36
|
+
}
|
|
37
|
+
const lines = [];
|
|
38
|
+
const listening = [
|
|
39
|
+
Number.isInteger(profile.effective_listening_events)
|
|
40
|
+
? `${formatInteger(profile.effective_listening_events)} ${profile.effective_listening_events === 1 ? "play" : "plays"}`
|
|
41
|
+
: null,
|
|
42
|
+
typeof profile.listening_hours === "number" && profile.listening_hours > 0 ? `${profile.listening_hours} h` : null,
|
|
43
|
+
Array.isArray(profile.listening_sources) && profile.listening_sources.length > 0
|
|
44
|
+
? `from ${profile.listening_sources.map((source) => serviceNames[source] ?? words(source)).join(", ")}`
|
|
45
|
+
: null,
|
|
46
|
+
].filter(Boolean);
|
|
47
|
+
if (listening.length > 0) lines.push(`- ${listening.join(" · ")}`);
|
|
48
|
+
if (Number.isInteger(profile.spotify_profile_evidence) && profile.spotify_profile_evidence > 0) {
|
|
49
|
+
lines.push(`- ${formatInteger(profile.spotify_profile_evidence)} saved songs, follows, and playlist entries from Spotify`);
|
|
50
|
+
}
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeInlineText(value) {
|
|
55
|
+
return sanitizeTerminalText(value ?? "")
|
|
56
|
+
.replace(/\\/gu, "\\\\")
|
|
57
|
+
.replace(/([`*_[\]<>])/gu, "\\$1")
|
|
58
|
+
.replace(/\s+/gu, " ")
|
|
59
|
+
.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function formatInteger(value) {
|
|
63
|
+
return Number.isInteger(value) ? value.toLocaleString("en-US") : "unknown";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatListeningTime(minutes) {
|
|
67
|
+
if (!Number.isFinite(minutes) || minutes < 0) return "unknown duration";
|
|
68
|
+
if (minutes < 120) return `${Math.round(minutes)} min`;
|
|
69
|
+
return `${Math.round((minutes / 60) * 10) / 10} h`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function plural(value, one, many = `${one}s`) {
|
|
73
|
+
return `${formatInteger(value)} ${value === 1 ? one : many}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function formatArtistSignal(item) {
|
|
77
|
+
const details = [];
|
|
78
|
+
if (Number.isInteger(item.play_count)) {
|
|
79
|
+
details.push(plural(item.play_count, "play"));
|
|
80
|
+
}
|
|
81
|
+
if (Number.isFinite(item.listening_minutes)) {
|
|
82
|
+
details.push(formatListeningTime(item.listening_minutes));
|
|
83
|
+
}
|
|
84
|
+
if (Number.isInteger(item.distinct_tracks)) {
|
|
85
|
+
details.push(plural(item.distinct_tracks, "track"));
|
|
86
|
+
}
|
|
87
|
+
return `- ${safeInlineText(item.name)}${
|
|
88
|
+
details.length > 0 ? ` · ${details.join(", ")}` : ""
|
|
89
|
+
}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function formatTrackSignal(item) {
|
|
93
|
+
const label = safeInlineText(item.label);
|
|
94
|
+
const artist = safeInlineText(item.artist_credit);
|
|
95
|
+
const details = [];
|
|
96
|
+
if (Number.isInteger(item.play_count)) {
|
|
97
|
+
details.push(plural(item.play_count, "play"));
|
|
98
|
+
}
|
|
99
|
+
if (Number.isFinite(item.listening_minutes)) {
|
|
100
|
+
details.push(formatListeningTime(item.listening_minutes));
|
|
101
|
+
}
|
|
102
|
+
return `- ${label}${artist ? ` - ${artist}` : ""}${
|
|
103
|
+
details.length > 0 ? ` · ${details.join(", ")}` : ""
|
|
104
|
+
}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Attention alone is the default reading, so only stronger evidence is worth naming.
|
|
108
|
+
const signalPhrases = {
|
|
109
|
+
"historical attention only": null,
|
|
110
|
+
"saved-library state": "saved in your library",
|
|
111
|
+
"private playlist curation": "on one of your playlists",
|
|
112
|
+
"explicit listener preference": "you said you like it",
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function signalPhrase(value) {
|
|
116
|
+
const text = safeInlineText(value);
|
|
117
|
+
if (!text) return null;
|
|
118
|
+
return Object.hasOwn(signalPhrases, text) ? signalPhrases[text] : text;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function withDetails(base, details) {
|
|
122
|
+
const present = details.filter(Boolean);
|
|
123
|
+
return `${base}${present.length > 0 ? ` · ${present.join(", ")}` : ""}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatRediscoveryTrack(item) {
|
|
127
|
+
return withDetails(formatTrackSignal(item), [
|
|
128
|
+
Number.isInteger(item.quiet_days)
|
|
129
|
+
? `quiet for ${plural(item.quiet_days, "day")}`
|
|
130
|
+
: null,
|
|
131
|
+
Number.isInteger(item.peak_year) ? `biggest in ${item.peak_year}` : null,
|
|
132
|
+
signalPhrase(item.rediscovery_signal),
|
|
133
|
+
]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatHistoricalReturnTrack(item) {
|
|
137
|
+
return withDetails(formatTrackSignal(item), [
|
|
138
|
+
Number.isInteger(item.return_count)
|
|
139
|
+
? `came back ${item.return_count === 1 ? "once" : `${formatInteger(item.return_count)} times`}`
|
|
140
|
+
: null,
|
|
141
|
+
Number.isInteger(item.longest_gap_days)
|
|
142
|
+
? `longest time away ${plural(item.longest_gap_days, "day")}`
|
|
143
|
+
: null,
|
|
144
|
+
signalPhrase(item.historical_return_signal),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatBackToBackTrack(item) {
|
|
149
|
+
return withDetails(formatTrackSignal(item), [
|
|
150
|
+
Number.isInteger(item.maximum_consecutive_plays)
|
|
151
|
+
? `up to ${formatInteger(item.maximum_consecutive_plays)} in a row`
|
|
152
|
+
: null,
|
|
153
|
+
Number.isInteger(item.burst_count) && item.burst_count > 1
|
|
154
|
+
? `${formatInteger(item.burst_count)} separate runs`
|
|
155
|
+
: null,
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function formatTimeCapsuleTrack(item) {
|
|
160
|
+
const label = safeInlineText(item.label);
|
|
161
|
+
const artist = safeInlineText(item.artist_credit);
|
|
162
|
+
return withDetails(
|
|
163
|
+
`- ${Number.isInteger(item.capsule_year) ? item.capsule_year : "Unknown year"} ${label}${artist ? ` - ${artist}` : ""}`,
|
|
164
|
+
[
|
|
165
|
+
Number.isInteger(item.year_play_count)
|
|
166
|
+
? `${plural(item.year_play_count, "play")} that year`
|
|
167
|
+
: null,
|
|
168
|
+
signalPhrase(item.representative_signal),
|
|
169
|
+
],
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatHistoryArc(item) {
|
|
174
|
+
const details = [];
|
|
175
|
+
if (Number.isFinite(item.listening_minutes)) {
|
|
176
|
+
details.push(formatListeningTime(item.listening_minutes));
|
|
177
|
+
}
|
|
178
|
+
if (Number.isInteger(item.event_count)) {
|
|
179
|
+
details.push(plural(item.event_count, "play"));
|
|
180
|
+
}
|
|
181
|
+
if (Number.isInteger(item.distinct_tracks)) {
|
|
182
|
+
details.push(plural(item.distinct_tracks, "track"));
|
|
183
|
+
}
|
|
184
|
+
if (Number.isInteger(item.first_observed_tracks) && item.first_observed_tracks > 0) {
|
|
185
|
+
details.push(`${formatInteger(item.first_observed_tracks)} new`);
|
|
186
|
+
}
|
|
187
|
+
const topArtist = safeInlineText(item.top_artist?.name);
|
|
188
|
+
if (topArtist) details.push(`mostly ${topArtist}`);
|
|
189
|
+
return `- ${Number.isInteger(item.year) ? item.year : "Unknown year"}${
|
|
190
|
+
details.length > 0 ? ` · ${details.join(", ")}` : ""
|
|
191
|
+
}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function formatListeningSeason(item) {
|
|
195
|
+
const key = safeInlineText(item.key).replace("-", " ");
|
|
196
|
+
if (!Number.isInteger(item.event_count) || item.event_count === 0) {
|
|
197
|
+
return `- ${key || "Unknown season"} · nothing in your history`;
|
|
198
|
+
}
|
|
199
|
+
const signature = safeInlineText(item.signature_track?.label);
|
|
200
|
+
const signatureArtist = safeInlineText(item.signature_track?.artist_credit);
|
|
201
|
+
return withDetails(`- ${key || "Unknown season"}`, [
|
|
202
|
+
Number.isFinite(item.listening_minutes)
|
|
203
|
+
? formatListeningTime(item.listening_minutes)
|
|
204
|
+
: null,
|
|
205
|
+
plural(item.event_count, "play"),
|
|
206
|
+
Number.isInteger(item.first_observed_tracks) && item.first_observed_tracks > 0
|
|
207
|
+
? `${formatInteger(item.first_observed_tracks)} new`
|
|
208
|
+
: null,
|
|
209
|
+
safeInlineText(item.leading_artist?.name)
|
|
210
|
+
? `mostly ${safeInlineText(item.leading_artist.name)}`
|
|
211
|
+
: null,
|
|
212
|
+
signature
|
|
213
|
+
? `top song ${signature}${signatureArtist ? ` - ${signatureArtist}` : ""}`
|
|
214
|
+
: null,
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function formatListenerAssertion(item) {
|
|
219
|
+
const label = safeInlineText(item.label);
|
|
220
|
+
const artist = safeInlineText(item.artist_credit);
|
|
221
|
+
const target = artist ? `${label} - ${artist}` : label;
|
|
222
|
+
const note = safeInlineText(item.note);
|
|
223
|
+
const entity = safeInlineText(item.entity_type);
|
|
224
|
+
return `- ${item.stance === "avoid" ? "Keep out" : "Like"}${entity ? ` (${entity})` : ""}: ${target}${
|
|
225
|
+
note ? ` · "${note}"` : ""
|
|
226
|
+
} · ${safeInlineText(item.correction_id)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function appendTasteSection(lines, title, values, formatter, subtitle) {
|
|
230
|
+
if (!Array.isArray(values) || values.length === 0) return;
|
|
231
|
+
lines.push("", `## ${title}`, "", ...(subtitle ? [`*${subtitle}*`, ""] : []), ...values.map(formatter));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function formatBehaviorContextLine(value, total, label) {
|
|
235
|
+
if (!Number.isInteger(value) || total === 0) return null;
|
|
236
|
+
const validTotal = Number.isInteger(total) && total >= value && total > 0;
|
|
237
|
+
if (!validTotal) return `- ${label}: ${formatInteger(value)}`;
|
|
238
|
+
const share = Math.round((value / total) * 100);
|
|
239
|
+
return `- ${label}: ${share}% (${formatInteger(value)} of ${formatInteger(total)})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatTaste(profile) {
|
|
243
|
+
const coverage = profile.coverage ?? {};
|
|
244
|
+
const durationUnavailable = coverage.events_with_played_duration === 0;
|
|
245
|
+
const listeningSignal = (formatter) => (item) => formatter(durationUnavailable ? { ...item, listening_minutes: undefined } : item);
|
|
246
|
+
const behavior = profile.listening_behavior ?? {};
|
|
247
|
+
const context = behavior.context ?? {};
|
|
248
|
+
const listeningRange = profile.listening_source?.listening_range;
|
|
249
|
+
const range =
|
|
250
|
+
typeof listeningRange?.earliest === "string" &&
|
|
251
|
+
typeof listeningRange?.latest === "string"
|
|
252
|
+
? `${listeningRange.earliest.slice(0, 10)} to ${listeningRange.latest.slice(0, 10)}`
|
|
253
|
+
: null;
|
|
254
|
+
const oneOff = profile.preview_source?.persistent_import === false;
|
|
255
|
+
const hours = !durationUnavailable && Number.isFinite(coverage.listening_hours)
|
|
256
|
+
? `${coverage.listening_hours.toLocaleString("en-US")} h`
|
|
257
|
+
: null;
|
|
258
|
+
const lines = [
|
|
259
|
+
"# Your listening, so far",
|
|
260
|
+
"",
|
|
261
|
+
oneOff
|
|
262
|
+
? "A one-off reading of the file you chose. Nothing was saved to your profile."
|
|
263
|
+
: "Read from your own files. It stays on this machine.",
|
|
264
|
+
"",
|
|
265
|
+
"## What I'm reading from",
|
|
266
|
+
"",
|
|
267
|
+
];
|
|
268
|
+
const summary = [
|
|
269
|
+
Number.isInteger(coverage.effective_listening_events) ? plural(coverage.effective_listening_events, "play") : null,
|
|
270
|
+
hours,
|
|
271
|
+
Number.isInteger(coverage.listening_tracks) ? plural(coverage.listening_tracks, "track") : null,
|
|
272
|
+
].filter(Boolean);
|
|
273
|
+
if (summary.length > 0) lines.push(`- ${summary.join(" · ")}`);
|
|
274
|
+
if (range) lines.push(`- ${range}`);
|
|
275
|
+
const saved = coverage.saved_tracks ?? coverage.spotify_saved_tracks;
|
|
276
|
+
if (Number.isInteger(saved) && saved > 0) lines.push(`- ${plural(saved, "saved track")}`);
|
|
277
|
+
const memberships = coverage.playlist_memberships ?? coverage.spotify_playlist_memberships;
|
|
278
|
+
if (Number.isInteger(memberships) && memberships > 0) lines.push(`- ${plural(memberships, "song", "songs")} on your playlists`);
|
|
279
|
+
for (const source of coverage.collection_sources ?? []) {
|
|
280
|
+
if (source.tracks > 0) lines.push(`- ${safeInlineText(source.label)}: ${plural(source.tracks, "track")}`);
|
|
281
|
+
}
|
|
282
|
+
if (!durationUnavailable && Number.isInteger(coverage.events_with_played_duration) &&
|
|
283
|
+
Number.isInteger(coverage.effective_listening_events) &&
|
|
284
|
+
coverage.events_with_played_duration < coverage.effective_listening_events) {
|
|
285
|
+
lines.push(`- Listening time is known for ${formatInteger(coverage.events_with_played_duration)} of those plays`);
|
|
286
|
+
}
|
|
287
|
+
if (Number.isInteger(coverage.cross_format_track_links) && coverage.cross_format_track_links > 0) {
|
|
288
|
+
lines.push(`- ${plural(coverage.cross_format_track_links, "track")} matched across your two Spotify exports`);
|
|
289
|
+
}
|
|
290
|
+
if (Number.isInteger(coverage.cross_format_ambiguous_tracks) && coverage.cross_format_ambiguous_tracks > 0) {
|
|
291
|
+
lines.push(`- ${plural(coverage.cross_format_ambiguous_tracks, "track")} kept apart because the match was unclear`);
|
|
292
|
+
}
|
|
293
|
+
if (Number.isInteger(context.incognito_events_excluded) && context.incognito_events_excluded > 0) {
|
|
294
|
+
lines.push(`- ${plural(context.incognito_events_excluded, "private-session play")} left out`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
appendTasteSection(
|
|
298
|
+
lines,
|
|
299
|
+
"What you told me",
|
|
300
|
+
profile.listener_assertions?.active,
|
|
301
|
+
formatListenerAssertion,
|
|
302
|
+
);
|
|
303
|
+
appendTasteSection(
|
|
304
|
+
lines,
|
|
305
|
+
"Shine On",
|
|
306
|
+
behavior.enduring_artists,
|
|
307
|
+
listeningSignal(formatArtistSignal),
|
|
308
|
+
"Artists who stayed with you across the years",
|
|
309
|
+
);
|
|
310
|
+
appendTasteSection(
|
|
311
|
+
lines,
|
|
312
|
+
"Lately",
|
|
313
|
+
behavior.recent_artists,
|
|
314
|
+
listeningSignal(formatArtistSignal),
|
|
315
|
+
Number.isInteger(context.recent_window_days)
|
|
316
|
+
? `Who you've played most in the last ${context.recent_window_days} days`
|
|
317
|
+
: "Who you've played most recently",
|
|
318
|
+
);
|
|
319
|
+
appendTasteSection(
|
|
320
|
+
lines,
|
|
321
|
+
"Year by year",
|
|
322
|
+
behavior.history_arc,
|
|
323
|
+
listeningSignal(formatHistoryArc),
|
|
324
|
+
);
|
|
325
|
+
const listeningSeasons = behavior.listening_seasons?.seasons;
|
|
326
|
+
appendTasteSection(
|
|
327
|
+
lines,
|
|
328
|
+
"Seasons",
|
|
329
|
+
Array.isArray(listeningSeasons) ? listeningSeasons.slice(-12) : [],
|
|
330
|
+
listeningSignal(formatListeningSeason),
|
|
331
|
+
"Your last twelve quarters. \"New\" means new to your history, not necessarily new to you.",
|
|
332
|
+
);
|
|
333
|
+
appendTasteSection(
|
|
334
|
+
lines,
|
|
335
|
+
"Time",
|
|
336
|
+
behavior.time_capsule_tracks,
|
|
337
|
+
formatTimeCapsuleTrack,
|
|
338
|
+
"The years, one track each",
|
|
339
|
+
);
|
|
340
|
+
appendTasteSection(
|
|
341
|
+
lines,
|
|
342
|
+
"Wish You Were Here",
|
|
343
|
+
behavior.rediscovery_tracks,
|
|
344
|
+
formatRediscoveryTrack,
|
|
345
|
+
Number.isInteger(context.rediscovery_quiet_days)
|
|
346
|
+
? `Songs you used to play a lot that have gone quiet for ${context.rediscovery_quiet_days}+ days`
|
|
347
|
+
: "Songs you used to play a lot that have gone quiet",
|
|
348
|
+
);
|
|
349
|
+
appendTasteSection(
|
|
350
|
+
lines,
|
|
351
|
+
"Coming Back to Life",
|
|
352
|
+
behavior.historical_return_tracks,
|
|
353
|
+
formatHistoricalReturnTrack,
|
|
354
|
+
Number.isInteger(context.historical_return_minimum_gap_days)
|
|
355
|
+
? `Songs that found their way back after ${context.historical_return_minimum_gap_days}+ days away`
|
|
356
|
+
: "Songs that found their way back after long gaps",
|
|
357
|
+
);
|
|
358
|
+
appendTasteSection(
|
|
359
|
+
lines,
|
|
360
|
+
"Echoes",
|
|
361
|
+
behavior.back_to_back_tracks,
|
|
362
|
+
formatBackToBackTrack,
|
|
363
|
+
"Songs you played again right away",
|
|
364
|
+
);
|
|
365
|
+
appendTasteSection(
|
|
366
|
+
lines,
|
|
367
|
+
"Most played",
|
|
368
|
+
behavior.repeat_tracks,
|
|
369
|
+
listeningSignal(formatTrackSignal),
|
|
370
|
+
);
|
|
371
|
+
appendTasteSection(
|
|
372
|
+
lines,
|
|
373
|
+
"Recently",
|
|
374
|
+
behavior.recent_tracks,
|
|
375
|
+
listeningSignal(formatTrackSignal),
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
const behaviorContextLines = [
|
|
379
|
+
formatBehaviorContextLine(context.direct_selection_starts, context.start_reason_events, "Songs you picked yourself"),
|
|
380
|
+
formatBehaviorContextLine(context.trackdone_starts, context.start_reason_events, "Songs that followed on"),
|
|
381
|
+
formatBehaviorContextLine(context.trackdone_endings, context.end_reason_events, "Played to the end"),
|
|
382
|
+
formatBehaviorContextLine(context.explicit_skips, context.skip_state_events, "Skipped"),
|
|
383
|
+
formatBehaviorContextLine(context.shuffle_events, context.shuffle_state_events, "On shuffle"),
|
|
384
|
+
formatBehaviorContextLine(context.offline_events, context.offline_state_events, "Offline"),
|
|
385
|
+
].filter(Boolean);
|
|
386
|
+
if (behaviorContextLines.length > 0) {
|
|
387
|
+
lines.push("", "## How you listen", "", ...behaviorContextLines);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (
|
|
391
|
+
Array.isArray(profile.strong_preferences) &&
|
|
392
|
+
profile.strong_preferences.length > 0
|
|
393
|
+
) {
|
|
394
|
+
lines.push(
|
|
395
|
+
"",
|
|
396
|
+
"## Kept on purpose",
|
|
397
|
+
"",
|
|
398
|
+
...profile.strong_preferences.map(
|
|
399
|
+
(item) =>
|
|
400
|
+
`- ${safeInlineText(item.label)} · ${safeInlineText(item.signal).toLowerCase()}`,
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
lines.push(
|
|
406
|
+
"",
|
|
407
|
+
"## The dark side of the moon",
|
|
408
|
+
"",
|
|
409
|
+
"*What this reading can't see*",
|
|
410
|
+
"",
|
|
411
|
+
"- Plays show attention, not love. A song can be on repeat because it was stuck in your head.",
|
|
412
|
+
...(behaviorContextLines.length > 0 ? ["- A skip is a moment, not a verdict."] : []),
|
|
413
|
+
...((behavior.history_arc ?? []).length > 0 || (listeningSeasons ?? []).length > 0
|
|
414
|
+
? ["- Years and seasons follow UTC, so a late night can land on the next day.",
|
|
415
|
+
"- A quiet stretch means no history was kept, not that you stopped listening."]
|
|
416
|
+
: []),
|
|
417
|
+
"- If something here is wrong, open the song or artist in /taste and tell me.",
|
|
418
|
+
"",
|
|
419
|
+
oneOff
|
|
420
|
+
? "Add `--json` to the same `--from` command for the full data."
|
|
421
|
+
: "For the full data with evidence IDs, run `moondog taste --json`.",
|
|
422
|
+
);
|
|
423
|
+
return lines.join("\n");
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function formatMemory(memory) {
|
|
427
|
+
const lines = [];
|
|
428
|
+
if (Number.isInteger(memory.sessions)) {
|
|
429
|
+
lines.push(
|
|
430
|
+
`- ${formatInteger(memory.sessions)} ${memory.sessions === 1 ? "conversation" : "conversations"} saved on this machine`,
|
|
431
|
+
`- ${formatInteger(memory.active_memories)} ${memory.active_memories === 1 ? "thing" : "things"} I remember about you`,
|
|
432
|
+
);
|
|
433
|
+
if (memory.reflection?.pending_episodes > 0) {
|
|
434
|
+
lines.push(`- ${formatInteger(memory.reflection.pending_episodes)} recent moments I haven't thought over yet`);
|
|
435
|
+
}
|
|
436
|
+
} else {
|
|
437
|
+
lines.push(`- ${words(memory.state)}`);
|
|
438
|
+
}
|
|
439
|
+
lines.push(
|
|
440
|
+
"",
|
|
441
|
+
"I keep what you tell me in conversation apart from your music profile.",
|
|
442
|
+
"I only remember something for good when you say it plainly, or when you ask with `/remember`.",
|
|
443
|
+
"`/forget <id>` removes it.",
|
|
444
|
+
);
|
|
445
|
+
if (Array.isArray(memory.memories) && memory.memories.length > 0) {
|
|
446
|
+
lines.push(
|
|
447
|
+
"",
|
|
448
|
+
"## What I remember",
|
|
449
|
+
"",
|
|
450
|
+
...memory.memories.map((entry) => `- ${entry.text} · ${entry.kind} · \`${entry.memory_id}\``),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
if (Array.isArray(memory.recent_episodes) && memory.recent_episodes.length > 0) {
|
|
454
|
+
lines.push(
|
|
455
|
+
"",
|
|
456
|
+
"## Lately",
|
|
457
|
+
"",
|
|
458
|
+
...memory.recent_episodes.map((episode) => `- ${String(episode.occurred_at).slice(0, 10)} · ${episode.summary}`),
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
if (Array.isArray(memory.recent_sessions) && memory.recent_sessions.length > 0) {
|
|
462
|
+
lines.push(
|
|
463
|
+
"",
|
|
464
|
+
"## Recent conversations",
|
|
465
|
+
"",
|
|
466
|
+
...memory.recent_sessions.map((session) => {
|
|
467
|
+
const userTurns = session.turns
|
|
468
|
+
.filter((turn) => turn.role === "user")
|
|
469
|
+
.map((turn) => turn.text)
|
|
470
|
+
.join(" / ");
|
|
471
|
+
return `- ${String(session.started_at).slice(0, 10)} · ${userTurns}`;
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return lines.join("\n");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function formatReflectionProposal(proposal) {
|
|
479
|
+
const evidence = proposal.episode_ids.map((id) => `\`${id}\``).join(", ");
|
|
480
|
+
if (proposal.action === "candidate_memory") {
|
|
481
|
+
return `- [memory/${proposal.assertion_mode}/${proposal.confidence}] ${proposal.statement}\n Evidence: ${evidence}\n Why: ${proposal.rationale}`;
|
|
482
|
+
}
|
|
483
|
+
if (proposal.action === "candidate_music_profile") {
|
|
484
|
+
return `- [music-profile/${proposal.direction}/${proposal.confidence}] ${proposal.dimension}: ${proposal.value}\n Evidence: ${evidence}\n Why: ${proposal.rationale}`;
|
|
485
|
+
}
|
|
486
|
+
return `- [ignore] ${proposal.reason}\n Episodes: ${evidence}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function formatMemoryReflection(value) {
|
|
490
|
+
if (value.state === "no_work") {
|
|
491
|
+
return "Memory Agent: no unprocessed episodes.";
|
|
492
|
+
}
|
|
493
|
+
if (value.state === "busy") {
|
|
494
|
+
return `Memory Agent: another reflection is running (${value.active_run?.run_id ?? "unknown"}).`;
|
|
495
|
+
}
|
|
496
|
+
const counts = value.counts ?? {};
|
|
497
|
+
const dryRun = value.state === "dry_run";
|
|
498
|
+
const lines = [
|
|
499
|
+
`# Memory reflection ${value.state}`,
|
|
500
|
+
"",
|
|
501
|
+
`- Run: \`${value.run_id}\``,
|
|
502
|
+
`- Proposals: ${counts.proposals ?? value.proposals?.length ?? 0}`,
|
|
503
|
+
`- ${dryRun ? "Would promote durable memories" : "Durable memories promoted"}: ${counts.promoted_memories ?? 0}`,
|
|
504
|
+
`- ${dryRun ? "Would create durable memories" : "New durable memories"}: ${counts.created_memories ?? 0}`,
|
|
505
|
+
`- ${dryRun ? "Would become candidate episodes" : "Candidate episodes"}: ${counts.candidate_episodes ?? 0}`,
|
|
506
|
+
`- ${dryRun ? "Would ignore episodes" : "Ignored episodes"}: ${counts.ignored_episodes ?? 0}`,
|
|
507
|
+
];
|
|
508
|
+
if (Array.isArray(value.proposals) && value.proposals.length > 0) {
|
|
509
|
+
lines.push(
|
|
510
|
+
"",
|
|
511
|
+
"## Decisions",
|
|
512
|
+
"",
|
|
513
|
+
...value.proposals.map(formatReflectionProposal),
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return lines.join("\n");
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function formatDemoResult(value) {
|
|
520
|
+
const lines = [
|
|
521
|
+
"# Moondog demo",
|
|
522
|
+
"",
|
|
523
|
+
"This walkthrough uses synthetic music data and has no music-service side effects.",
|
|
524
|
+
"",
|
|
525
|
+
"## Prompt",
|
|
526
|
+
"",
|
|
527
|
+
safeInlineText(value.prompt),
|
|
528
|
+
"",
|
|
529
|
+
"## Tool trace",
|
|
530
|
+
"",
|
|
531
|
+
];
|
|
532
|
+
if (Array.isArray(value.tool_trace) && value.tool_trace.length > 0) {
|
|
533
|
+
lines.push(
|
|
534
|
+
...value.tool_trace.map(
|
|
535
|
+
(entry) =>
|
|
536
|
+
`${entry.sequence}. ${safeInlineText(entry.capability)} - ${safeInlineText(entry.status)}`,
|
|
537
|
+
),
|
|
538
|
+
);
|
|
539
|
+
} else {
|
|
540
|
+
lines.push("No tools were called.");
|
|
541
|
+
}
|
|
542
|
+
lines.push("", "## Result", "", value.result?.text ?? "No result text.");
|
|
543
|
+
if (Array.isArray(value.limitations) && value.limitations.length > 0) {
|
|
544
|
+
lines.push(
|
|
545
|
+
"",
|
|
546
|
+
"## Boundaries",
|
|
547
|
+
"",
|
|
548
|
+
...value.limitations.map(
|
|
549
|
+
(limitation) => `- ${safeInlineText(limitation)}`,
|
|
550
|
+
),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
if (Array.isArray(value.next_steps) && value.next_steps.length > 0) {
|
|
554
|
+
lines.push("", "## Keep exploring", "");
|
|
555
|
+
for (const step of value.next_steps) {
|
|
556
|
+
lines.push(
|
|
557
|
+
`### ${safeInlineText(step.title)}`,
|
|
558
|
+
"",
|
|
559
|
+
`- From this checkout: \`${safeInlineText(step.checkout_command)}\``,
|
|
560
|
+
`- From an installed CLI: \`${safeInlineText(step.installed_command)}\``,
|
|
561
|
+
`- ${safeInlineText(step.data_boundary)}`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return lines.join("\n");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const capabilityStates = {
|
|
569
|
+
ready: "ready",
|
|
570
|
+
enabled: "ready",
|
|
571
|
+
available: "ready",
|
|
572
|
+
blocked: "not available yet",
|
|
573
|
+
unavailable: "not available",
|
|
574
|
+
disabled: "off",
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
function formatTools(value) {
|
|
578
|
+
const rows = value.capabilities.map((capability) => {
|
|
579
|
+
const label = capability.agent_tool?.label ?? capability.id;
|
|
580
|
+
const state = capabilityStates[capability.state] ?? words(capability.state);
|
|
581
|
+
return `- **${label}** · ${state}\n ${capability.description}${
|
|
582
|
+
capability.blocked_by ? `\n Waiting on: ${words(capability.blocked_by)}` : ""
|
|
583
|
+
}`;
|
|
584
|
+
});
|
|
585
|
+
return `# What I can do\n\n${rows.join("\n")}`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const doctorLabels = {
|
|
589
|
+
"web.codex_cli": "Web lookups (Codex CLI)",
|
|
590
|
+
"node.version": "Node.js",
|
|
591
|
+
"apple_music.source": "Apple Music library",
|
|
592
|
+
"profile.projection": "Listening profile",
|
|
593
|
+
"agent.runtime": "Model",
|
|
594
|
+
"music.discovery.open_similarity": "Finding similar music",
|
|
595
|
+
"external.effects": "Changes to your accounts",
|
|
596
|
+
"spotify.connection": "Spotify",
|
|
597
|
+
"spotify.connected_action_scopes": "Spotify permissions",
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
function formatDoctor(value) {
|
|
601
|
+
return [
|
|
602
|
+
"# Checkup",
|
|
603
|
+
"",
|
|
604
|
+
...value.checks.map((check) => {
|
|
605
|
+
const state = check.ok ? "ok" : check.optional_for_local_commands ? "not set up (optional)" : "not working";
|
|
606
|
+
const detail = words(check.actual ?? check.state ?? "unknown");
|
|
607
|
+
return `- ${doctorLabels[check.id] ?? check.id}: ${state} · ${detail}`;
|
|
608
|
+
}),
|
|
609
|
+
].join("\n");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function formatRuntime(runtime) {
|
|
613
|
+
if (runtime?.state === "configured") {
|
|
614
|
+
return `- Talking through ${runtime.provider && runtime.model ? `\`${runtime.provider}/${runtime.model}\`` : "your chosen model"}`;
|
|
615
|
+
}
|
|
616
|
+
return runtime?.reason === "provider_authentication_required"
|
|
617
|
+
? "- Not signed in yet. `/auth` fixes that."
|
|
618
|
+
: "- No model connected. `/model` picks one. Your profile works without it.";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function formatSpotifyStatus(spotify) {
|
|
622
|
+
const state = spotify?.state ?? "not_configured";
|
|
623
|
+
if (state === "ready" || state === "connected") return "- Connected";
|
|
624
|
+
return state === "not_configured"
|
|
625
|
+
? "- Not connected. `/import` > Spotify walks you through it."
|
|
626
|
+
: `- ${words(state)}`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export function formatLocalResult(command, value) {
|
|
630
|
+
switch (command) {
|
|
631
|
+
case "status":
|
|
632
|
+
case "sources":
|
|
633
|
+
return [
|
|
634
|
+
"# Where things stand",
|
|
635
|
+
"",
|
|
636
|
+
"## Your profile",
|
|
637
|
+
"",
|
|
638
|
+
formatProfile(value.profile),
|
|
639
|
+
formatSource(value.source),
|
|
640
|
+
"",
|
|
641
|
+
"## Talking",
|
|
642
|
+
"",
|
|
643
|
+
formatRuntime(value.runtime),
|
|
644
|
+
"",
|
|
645
|
+
"## Memory",
|
|
646
|
+
"",
|
|
647
|
+
formatMemory(value.memory),
|
|
648
|
+
"",
|
|
649
|
+
"## Spotify",
|
|
650
|
+
"",
|
|
651
|
+
formatSpotifyStatus(value.spotify),
|
|
652
|
+
"",
|
|
653
|
+
value.external_effects === "disabled" && (value.spotify?.external_effects ?? "disabled") === "disabled"
|
|
654
|
+
? "Right now I can't change anything in your music accounts."
|
|
655
|
+
: "I can make changes in your connected accounts, and I always ask first.",
|
|
656
|
+
].join("\n");
|
|
657
|
+
case "profile":
|
|
658
|
+
return `# Your profile\n\n${formatProfile(value)}`;
|
|
659
|
+
case "taste":
|
|
660
|
+
return formatTaste(value);
|
|
661
|
+
case "memory":
|
|
662
|
+
return `# What I remember\n\n${formatMemory(value)}`;
|
|
663
|
+
case "tools":
|
|
664
|
+
return formatTools(value);
|
|
665
|
+
case "doctor":
|
|
666
|
+
return formatDoctor(value);
|
|
667
|
+
default:
|
|
668
|
+
return JSON.stringify(value, null, 2);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export function helpText() {
|
|
673
|
+
return `# Every command
|
|
674
|
+
|
|
675
|
+
From your shell:
|
|
676
|
+
|
|
677
|
+
- \`moondog web status|search|read [arguments] [--json]\` - look things up on public music sites through the Codex CLI
|
|
678
|
+
- \`moondog studio\` - open the browser Studio for dropping in a Spotify ZIP
|
|
679
|
+
- \`moondog studio --demo\` - try the Studio with made-up history; your own data is never read
|
|
680
|
+
- \`moondog studio --from <spotify-history.zip>\` - look at one Spotify ZIP in the Studio without saving it
|
|
681
|
+
- \`moondog studio --from <account-data.zip> --from <extended-history.zip>\` - combine two Spotify ZIPs in the Studio without saving them
|
|
682
|
+
- \`moondog demo-history --output <absolute-file.zip> [--json]\` - make a fictional Spotify history ZIP to try the importer with
|
|
683
|
+
- \`moondog auth login <provider>\` - sign in to a model provider; the key is typed out of sight, and openai-codex uses your ChatGPT sign-in
|
|
684
|
+
- \`moondog auth status [provider]\` - see which sign-ins are saved
|
|
685
|
+
- \`moondog auth logout <provider>\` - sign out of a provider
|
|
686
|
+
- \`moondog spotify help\` - set up and control Spotify
|
|
687
|
+
- \`moondog spotify login [client-id]\` - connect Spotify in your browser
|
|
688
|
+
- \`moondog spotify now|devices|queue\` - see what Spotify is playing, where, and what's next
|
|
689
|
+
- \`moondog spotify recent\` - see what you played lately
|
|
690
|
+
- \`moondog spotify sync-recent\` - add your latest plays to your profile
|
|
691
|
+
- \`moondog spotify play|pause|next|previous\` - control Spotify playback
|
|
692
|
+
- \`moondog listenbrainz import-history <listen-history.json>\` - add a ListenBrainz listens file to your profile
|
|
693
|
+
- \`moondog catalog latest-single --artist <name> [--known-release <title> | --from <spotify-history.zip>]\` - find an artist's latest single on Apple Music (US), no model needed
|
|
694
|
+
- \`moondog status [--json]\` - see where things stand
|
|
695
|
+
- \`moondog taste [--json]\` - your listening report
|
|
696
|
+
- \`moondog taste --html [--output <file.html>]\` - save your report as a web page that works offline
|
|
697
|
+
- \`moondog taste --card [--output <file.html>]\` - save a short recap you can check before sharing
|
|
698
|
+
- \`moondog taste --from <spotify-history.zip> --html\` - a one-off report from a Spotify ZIP, without saving it
|
|
699
|
+
- \`moondog taste --from <account-data.zip> --from <extended-history.zip> --html\` - a one-off report from both Spotify exports together
|
|
700
|
+
- \`moondog taste --from <spotify-history.zip> --save --html\` - save the Spotify history, then show your full report
|
|
701
|
+
- \`moondog data inspect --scope <listening|apple|profile>\` - see what's stored, and get the code needed to reset it
|
|
702
|
+
- \`moondog data export --scope <listening|apple|profile> --output <directory>\` - export a copy, leaving the original alone
|
|
703
|
+
- \`moondog data reset --scope <listening|apple|profile> --confirm <token>\` - set stored data aside (it is archived, not deleted)
|
|
704
|
+
- \`moondog profile corrections [--all]\` - everything you've told me, including earlier choices
|
|
705
|
+
- \`moondog profile correct --artist <name> (--like|--avoid)\` - like an artist, or keep them out
|
|
706
|
+
- \`moondog profile correct --track <title> --by <artist> (--like|--avoid)\` - like a track, or keep it out
|
|
707
|
+
- \`moondog profile retract <correction-id>\` - undo one of your choices
|
|
708
|
+
- \`moondog demo --offline [--json]\` - a demo with made-up data, no sign-in needed
|
|
709
|
+
- \`moondog demo [prompt] [--json]\` - the same demo, talking through your model
|
|
710
|
+
- \`moondog ask <prompt>\` - ask one question and exit
|
|
711
|
+
- \`moondog remember <text>\` - ask me to remember something
|
|
712
|
+
- \`moondog forget <memory-id>\` - forget something
|
|
713
|
+
- \`moondog memory reflect [--dry-run] [--json]\` - let me think over recent conversations now
|
|
714
|
+
- \`moondog\` - open the listening room
|
|
715
|
+
|
|
716
|
+
Inside the listening room:
|
|
717
|
+
|
|
718
|
+
- \`/status\` or \`/sources\` - see where things stand
|
|
719
|
+
- \`/profile\` or \`/taste\` - your profile, and why each song or artist is there
|
|
720
|
+
- \`/taste report\` - the whole report on one page
|
|
721
|
+
- \`/profile corrections [--all]\` - everything you've told me
|
|
722
|
+
- \`/profile correct --track "<title>" --by "<artist>" (--like|--avoid)\` - like a track, or keep it out
|
|
723
|
+
- \`/profile correct --artist "<name>" (--like|--avoid)\` - like an artist, or keep them out
|
|
724
|
+
- \`/profile retract <correction-id>\` - undo a choice
|
|
725
|
+
- \`/memory\` - what I remember
|
|
726
|
+
- \`/tools\` - what I can do right now
|
|
727
|
+
- \`/doctor\` - check your setup
|
|
728
|
+
- \`/model\` - pick the model I talk through
|
|
729
|
+
- \`/model <provider> <model>\` - switch straight to a model
|
|
730
|
+
- \`/auth [provider]\` - sign in to a model provider
|
|
731
|
+
- \`/web status|search|read\` - look things up on public music sites
|
|
732
|
+
- \`/import [path]\` - bring in your history, library, or a playlist
|
|
733
|
+
- \`/spotify\` - Spotify commands
|
|
734
|
+
- \`/spotify login [client-id]\` - connect Spotify
|
|
735
|
+
- \`/spotify now|devices|queue\` - see what's playing, where, and what's next
|
|
736
|
+
- \`/spotify recent|sync-recent\` - see or save your latest plays
|
|
737
|
+
- \`/spotify import-history "/path/to/spotify-history.zip"\` - add a Spotify ZIP, no sign-in needed
|
|
738
|
+
- \`/reload\` - reload model settings and sign-ins
|
|
739
|
+
- \`/remember [kind] <text>\` - ask me to remember something
|
|
740
|
+
- \`/forget <memory-id>\` - forget something
|
|
741
|
+
- \`/resume\` - pick up a saved conversation
|
|
742
|
+
- \`/resume <session-id>\` - go straight to one
|
|
743
|
+
- \`/new\` - start fresh; the last one stays saved
|
|
744
|
+
- \`/quit\` - exit
|
|
745
|
+
|
|
746
|
+
Anything you type that isn't a command goes to the model you picked with \`/model\`, or the one set in \`MOONDOG_PROVIDER\` and \`MOONDOG_MODEL\`.
|
|
747
|
+
Sign in with \`/auth\`, or set the provider's API key in your environment. \`/auth openai-codex\` uses your ChatGPT sign-in.
|
|
748
|
+
Once your profile is ready, I can search your library, explain what I see, and plan playlists with you.
|
|
749
|
+
I can only control Spotify after \`moondog spotify login\`, and I never post, send messages, delete anything, or spend money.`;
|
|
750
|
+
}
|