@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
|
@@ -2,28 +2,40 @@ import { stripVTControlCharacters } from "node:util";
|
|
|
2
2
|
|
|
3
3
|
const escape = "\u001b[";
|
|
4
4
|
const modes = new Set(["auto", "paper", "charcoal", "terminal"]);
|
|
5
|
+
// Moondog is black and white, like the moon. The six prism colors from The Dark Side of the Moon
|
|
6
|
+
// are the only hues, and they mark meaning rather than decoration: red fails, orange cautions,
|
|
7
|
+
// green confirms, and the whole spectrum turns while Moondog works.
|
|
8
|
+
// Charcoal uses the cover's colors exactly; paper darkens each hue until it reads on white.
|
|
9
|
+
const prismOrder = ["red", "orange", "yellow", "green", "blue", "violet"];
|
|
5
10
|
const palettes = {
|
|
6
11
|
paper: {
|
|
7
|
-
background: "#
|
|
8
|
-
foreground: "#
|
|
9
|
-
muted: "#
|
|
10
|
-
faint: "#
|
|
11
|
-
accent: "#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
background: "#f4f4f4",
|
|
13
|
+
foreground: "#141414",
|
|
14
|
+
muted: "#585858",
|
|
15
|
+
faint: "#8a8a8a",
|
|
16
|
+
accent: "#000000",
|
|
17
|
+
red: "#d33028",
|
|
18
|
+
orange: "#b25119",
|
|
19
|
+
yellow: "#787102",
|
|
20
|
+
green: "#3c7c3d",
|
|
21
|
+
blue: "#33768d",
|
|
22
|
+
violet: "#6e5c8c",
|
|
15
23
|
},
|
|
16
24
|
charcoal: {
|
|
17
|
-
background: "#
|
|
18
|
-
foreground: "#
|
|
19
|
-
muted: "#
|
|
20
|
-
faint: "#
|
|
21
|
-
accent: "#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
background: "#0c0c0c",
|
|
26
|
+
foreground: "#e4e4e4",
|
|
27
|
+
muted: "#9e9e9e",
|
|
28
|
+
faint: "#666666",
|
|
29
|
+
accent: "#ffffff",
|
|
30
|
+
red: "#d93e37",
|
|
31
|
+
orange: "#e27231",
|
|
32
|
+
yellow: "#fcf040",
|
|
33
|
+
green: "#52a953",
|
|
34
|
+
blue: "#4ea2bf",
|
|
35
|
+
violet: "#6e5c8c",
|
|
25
36
|
},
|
|
26
37
|
};
|
|
38
|
+
const semantic = { success: "green", warning: "orange", error: "red" };
|
|
27
39
|
const ansi16 = [
|
|
28
40
|
[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
|
|
29
41
|
[0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
|
|
@@ -148,19 +160,22 @@ export function createMoondogTheme({ mode = "auto", environment = process.env }
|
|
|
148
160
|
const fallback = {
|
|
149
161
|
foreground: light ? 30 : 37,
|
|
150
162
|
muted: light ? 30 : 37,
|
|
151
|
-
faint:
|
|
152
|
-
accent: light ? 30 :
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
163
|
+
faint: 90,
|
|
164
|
+
accent: light ? 30 : 97,
|
|
165
|
+
red: 31,
|
|
166
|
+
orange: 33,
|
|
167
|
+
yellow: 33,
|
|
168
|
+
green: 32,
|
|
169
|
+
blue: 36,
|
|
170
|
+
violet: 35,
|
|
156
171
|
};
|
|
157
|
-
const foreground = (
|
|
172
|
+
const foreground = (requested) => {
|
|
173
|
+
const name = semantic[requested] ?? requested;
|
|
158
174
|
if (depth === 0) return unstyled;
|
|
159
175
|
if (terminal) {
|
|
176
|
+
// The terminal keeps its own palette, including its own reading of each prism color.
|
|
160
177
|
if (name === "accent") return bold;
|
|
161
|
-
if (name
|
|
162
|
-
if (name === "warning") return style(33, 39);
|
|
163
|
-
if (name === "error") return style(31, 39);
|
|
178
|
+
if (Object.hasOwn(fallback, name) && prismOrder.includes(name)) return style(fallback[name], 39);
|
|
164
179
|
return plain;
|
|
165
180
|
}
|
|
166
181
|
if (depth === 24) return style(`38;2;${rgb(palette[name]).join(";")}`, 39);
|
|
@@ -174,6 +189,7 @@ export function createMoondogTheme({ mode = "auto", environment = process.env }
|
|
|
174
189
|
const success = foreground("success");
|
|
175
190
|
const warning = foreground("warning");
|
|
176
191
|
const error = foreground("error");
|
|
192
|
+
const prism = Object.freeze(prismOrder.map(foreground));
|
|
177
193
|
let background = plain;
|
|
178
194
|
if (depth === 0) background = unstyled;
|
|
179
195
|
else if (!terminal) {
|
|
@@ -182,10 +198,15 @@ export function createMoondogTheme({ mode = "auto", environment = process.env }
|
|
|
182
198
|
: depth === 8 ? `48;5;${nearestXterm(palette.background)}` : light ? 107 : 40;
|
|
183
199
|
background = style(code, 49);
|
|
184
200
|
}
|
|
185
|
-
|
|
201
|
+
// One selection language everywhere: a moon mark and full-strength ink, never an inverse bar.
|
|
202
|
+
const selected = (value) => bold(accent(value));
|
|
203
|
+
const selectedRow = (value) => {
|
|
204
|
+
const row = String(value ?? "");
|
|
205
|
+
return row.startsWith("→ ") ? accent("◉ ") + selected(row.slice(2)) : selected(row);
|
|
206
|
+
};
|
|
186
207
|
const selectListTheme = {
|
|
187
|
-
selectedPrefix:
|
|
188
|
-
selectedText:
|
|
208
|
+
selectedPrefix: selectedRow,
|
|
209
|
+
selectedText: selectedRow,
|
|
189
210
|
description: muted,
|
|
190
211
|
scrollInfo: muted,
|
|
191
212
|
noMatch: muted,
|
|
@@ -218,10 +239,12 @@ export function createMoondogTheme({ mode = "auto", environment = process.env }
|
|
|
218
239
|
success,
|
|
219
240
|
warning,
|
|
220
241
|
error,
|
|
242
|
+
prism,
|
|
221
243
|
bold,
|
|
222
244
|
italic,
|
|
223
245
|
underline,
|
|
224
246
|
inverse,
|
|
247
|
+
selected,
|
|
225
248
|
background,
|
|
226
249
|
editorTheme,
|
|
227
250
|
selectListTheme,
|
|
@@ -1,54 +1,59 @@
|
|
|
1
|
+
import { N_, screenTranslator } from "../../i18n/index.mjs";
|
|
2
|
+
|
|
3
|
+
// Descriptions are marked English and translated when shown, so /language applies at once.
|
|
4
|
+
let translate = screenTranslator("en");
|
|
1
5
|
function choices(entries) {
|
|
2
|
-
return entries.map(([value, description]) => ({ value, label: value, description }));
|
|
6
|
+
return entries.map(([value, description]) => ({ value, label: value, get description() { return translate.marked(description); } }));
|
|
3
7
|
}
|
|
4
8
|
|
|
5
9
|
const themeChoices = choices([
|
|
6
|
-
["paper", "
|
|
7
|
-
["charcoal", "
|
|
8
|
-
["terminal", "Keep your terminal's colors"],
|
|
9
|
-
["auto", "Follow MOONDOG_THEME, or guess from your terminal"],
|
|
10
|
+
["paper", N_("Black ink on white paper")],
|
|
11
|
+
["charcoal", N_("Moonlight on a black sky")],
|
|
12
|
+
["terminal", N_("Keep your terminal's colors")],
|
|
13
|
+
["auto", N_("Follow MOONDOG_THEME, or guess from your terminal")],
|
|
10
14
|
]);
|
|
11
15
|
const artChoices = choices([
|
|
12
|
-
["braille", "Character artwork with fine detail"],
|
|
13
|
-
["ascii", "Simple terminal characters"],
|
|
14
|
-
["off", "A quiet opening, no artwork"],
|
|
15
|
-
["auto", "Choose artwork for this terminal"],
|
|
16
|
+
["braille", N_("Character artwork with fine detail")],
|
|
17
|
+
["ascii", N_("Simple terminal characters")],
|
|
18
|
+
["off", N_("A quiet opening, no artwork")],
|
|
19
|
+
["auto", N_("Choose artwork for this terminal")],
|
|
16
20
|
]);
|
|
17
|
-
const motionChoices = choices([["on", "Animate the character artwork"], ["off", "Keep the artwork still"]]);
|
|
21
|
+
const motionChoices = choices([["on", N_("Animate the character artwork")], ["off", N_("Keep the artwork still")]]);
|
|
18
22
|
const webChoices = choices([
|
|
19
|
-
["search", "Search public music sites"],
|
|
20
|
-
["read", "Read a public page"],
|
|
21
|
-
["status", "Check whether web lookups work"],
|
|
22
|
-
["help", "Web lookup commands"],
|
|
23
|
+
["search", N_("Search public music sites")],
|
|
24
|
+
["read", N_("Read a public page")],
|
|
25
|
+
["status", N_("Check whether web lookups work")],
|
|
26
|
+
["help", N_("Web lookup commands")],
|
|
23
27
|
]);
|
|
24
28
|
const spotifyChoices = choices([
|
|
25
|
-
["status", "Check your Spotify connection"],
|
|
26
|
-
["now", "What's playing now"],
|
|
27
|
-
["devices", "Your Spotify devices"],
|
|
28
|
-
["queue", "What's queued up"],
|
|
29
|
-
["recent", "What you played lately"],
|
|
30
|
-
["account", "Which account is connected"],
|
|
31
|
-
["play", "Resume playback or play a Spotify URI"],
|
|
32
|
-
["pause", "Pause playback"],
|
|
33
|
-
["next", "Play the next track"],
|
|
34
|
-
["previous", "Play the previous track"],
|
|
35
|
-
["volume", "Set volume from 0 to 100"],
|
|
36
|
-
["seek", "Seek to a position in milliseconds"],
|
|
37
|
-
["shuffle", "Set shuffle on or off"],
|
|
38
|
-
["repeat", "Repeat off, one track, or the context"],
|
|
39
|
-
["transfer", "Move playback to another device"],
|
|
40
|
-
["queue-add", "Add a track or episode URI to the queue"],
|
|
41
|
-
["resolve", "Find a track by title and artist"],
|
|
42
|
-
["sync-recent", "Add your latest plays to your profile"],
|
|
43
|
-
["
|
|
44
|
-
["
|
|
45
|
-
["
|
|
46
|
-
["
|
|
47
|
-
["
|
|
29
|
+
["status", N_("Check your Spotify connection")],
|
|
30
|
+
["now", N_("What's playing now")],
|
|
31
|
+
["devices", N_("Your Spotify devices")],
|
|
32
|
+
["queue", N_("What's queued up")],
|
|
33
|
+
["recent", N_("What you played lately")],
|
|
34
|
+
["account", N_("Which account is connected")],
|
|
35
|
+
["play", N_("Resume playback or play a Spotify URI")],
|
|
36
|
+
["pause", N_("Pause playback")],
|
|
37
|
+
["next", N_("Play the next track")],
|
|
38
|
+
["previous", N_("Play the previous track")],
|
|
39
|
+
["volume", N_("Set volume from 0 to 100")],
|
|
40
|
+
["seek", N_("Seek to a position in milliseconds")],
|
|
41
|
+
["shuffle", N_("Set shuffle on or off")],
|
|
42
|
+
["repeat", N_("Repeat off, one track, or the context")],
|
|
43
|
+
["transfer", N_("Move playback to another device")],
|
|
44
|
+
["queue-add", N_("Add a track or episode URI to the queue")],
|
|
45
|
+
["resolve", N_("Find a track by title and artist")],
|
|
46
|
+
["sync-recent", N_("Add your latest plays to your profile")],
|
|
47
|
+
["sync-library", N_("Add your Spotify library and top artists to your profile")],
|
|
48
|
+
["import-history", N_("Add a Spotify history ZIP to your profile")],
|
|
49
|
+
["configure", N_("Save your Spotify app's Client ID")],
|
|
50
|
+
["login", N_("Connect your Spotify account")],
|
|
51
|
+
["logout", N_("Disconnect your Spotify account")],
|
|
52
|
+
["help", N_("Spotify commands")],
|
|
48
53
|
]);
|
|
49
54
|
const spotifyArguments = new Map([
|
|
50
|
-
["shuffle", choices([["on", "Enable shuffle"], ["off", "Disable shuffle"]])],
|
|
51
|
-
["repeat", choices([["off", "Disable repeat"], ["track", "Repeat this track"], ["context", "Repeat the album or playlist"]])],
|
|
55
|
+
["shuffle", choices([["on", N_("Enable shuffle")], ["off", N_("Disable shuffle")]])],
|
|
56
|
+
["repeat", choices([["off", N_("Disable repeat")], ["track", N_("Repeat this track")], ["context", N_("Repeat the album or playlist")]])],
|
|
52
57
|
]);
|
|
53
58
|
|
|
54
59
|
function completeArgument(prefix, getChoices) {
|
|
@@ -68,13 +73,21 @@ export function withCommandCompletions(commands, {
|
|
|
68
73
|
providers = () => [],
|
|
69
74
|
models = () => [],
|
|
70
75
|
authProviderIds = [],
|
|
76
|
+
tr,
|
|
71
77
|
} = {}) {
|
|
78
|
+
if (tr) translate = tr;
|
|
72
79
|
const firstArgument = (items) => (previous) => previous.length === 0 ? items : [];
|
|
73
80
|
const specifications = {
|
|
81
|
+
update: {
|
|
82
|
+
argumentHint: "[--check] [--channel latest|beta]",
|
|
83
|
+
choices: previous => previous.at(-1) === "--channel"
|
|
84
|
+
? choices([["latest", N_("Stable releases")], ["beta", N_("Preview releases")]])
|
|
85
|
+
: choices([["--check", N_("Check without installing")], ["--channel", N_("Choose the release channel")]]).filter(item => !previous.includes(item.value)),
|
|
86
|
+
},
|
|
74
87
|
theme: { argumentHint: "[paper|charcoal|terminal|auto]", choices: firstArgument(themeChoices) },
|
|
75
88
|
art: { argumentHint: "[braille|ascii|off|auto]", choices: firstArgument(artChoices) },
|
|
76
89
|
motion: { argumentHint: "[on|off]", choices: firstArgument(motionChoices) },
|
|
77
|
-
lyrics: { argumentHint: "[sync]", choices: firstArgument(choices([["sync", "Check profile songs for lyrics"]])) },
|
|
90
|
+
lyrics: { argumentHint: "[sync]", choices: firstArgument(choices([["sync", N_("Check profile songs for lyrics")]])) },
|
|
78
91
|
web: { argumentHint: "search <query> | read <url> | status", choices: firstArgument(webChoices) },
|
|
79
92
|
spotify: {
|
|
80
93
|
argumentHint: "<command> [arguments]",
|
|
@@ -88,14 +101,17 @@ export function withCommandCompletions(commands, {
|
|
|
88
101
|
.map((provider) => ({ value: provider.id, label: provider.id, description: provider.name })) : [],
|
|
89
102
|
},
|
|
90
103
|
model: {
|
|
91
|
-
argumentHint: "[provider] [model]",
|
|
104
|
+
argumentHint: "[provider] [model] | refresh [provider]",
|
|
92
105
|
choices: (previous) => {
|
|
93
106
|
if (previous.length > 1) return [];
|
|
94
107
|
const available = providers().filter((provider) => provider.modelCount > 0);
|
|
95
108
|
if (previous.length === 0) {
|
|
96
|
-
return
|
|
109
|
+
return [{ value: "refresh", label: "refresh", description: translate("Refresh public model metadata; keep selection") },
|
|
110
|
+
...available.map((provider) => ({ value: provider.id, label: provider.id, description: provider.name }))];
|
|
97
111
|
}
|
|
98
112
|
const providerId = previous[0].toLowerCase();
|
|
113
|
+
if (providerId === "refresh") return available.filter(provider => authProviderIds.includes(provider.id))
|
|
114
|
+
.map(provider => ({ value: provider.id, label: provider.id, description: provider.name }));
|
|
99
115
|
if (!available.some((provider) => provider.id === providerId)) return [];
|
|
100
116
|
return models(providerId).map((model) => ({ value: model.id, label: model.id, description: model.name }));
|
|
101
117
|
},
|