@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
@@ -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: "#eee7d5",
8
- foreground: "#242722",
9
- muted: "#606359",
10
- faint: "#65685d",
11
- accent: "#746044",
12
- success: "#50644b",
13
- warning: "#7c5d25",
14
- error: "#a04136",
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: "#222521",
18
- foreground: "#eee7d5",
19
- muted: "#a9ab9c",
20
- faint: "#96988d",
21
- accent: "#ccb995",
22
- success: "#a8b497",
23
- warning: "#d0ad73",
24
- error: "#d39b8c",
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: light ? 30 : 90,
152
- accent: light ? 30 : 33,
153
- success: light ? 32 : 92,
154
- warning: light ? 30 : 33,
155
- error: light ? 31 : 91,
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 = (name) => {
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 === "success") return style(32, 39);
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
- const selected = (value) => inverse(bold(text(value)));
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: selected,
188
- selectedText: selected,
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", "Warm ivory and charcoal ink"],
7
- ["charcoal", "A quiet room, warm light"],
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
- ["import-history", "Add a Spotify history ZIP to your profile"],
44
- ["configure", "Save your Spotify app's Client ID"],
45
- ["login", "Connect your Spotify account"],
46
- ["logout", "Disconnect your Spotify account"],
47
- ["help", "Spotify commands"],
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 available.map((provider) => ({ value: provider.id, label: provider.id, description: provider.name }));
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
  },