@audiofool/moondog 0.3.0-beta.1 → 0.3.0-beta.10
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 +2 -1
- package/package.json +1 -1
- package/scripts/moondog.mjs +138 -10
- package/src/core/capability-catalog.mjs +49 -0
- package/src/core/library-store.mjs +500 -0
- package/src/core/listening-now.mjs +80 -6
- package/src/core/moondog-application.mjs +570 -32
- package/src/i18n/index.mjs +6 -3
- package/src/i18n/messages/de.mjs +1 -0
- package/src/i18n/messages/en.mjs +2 -0
- package/src/i18n/messages/es.mjs +1 -0
- package/src/i18n/messages/fr.mjs +1 -0
- package/src/i18n/messages/it.mjs +1 -0
- package/src/i18n/messages/ja.mjs +1 -0
- package/src/i18n/messages/ko.mjs +1 -0
- package/src/i18n/messages/pt.mjs +1 -0
- package/src/i18n/messages/zh-Hant.mjs +1 -0
- package/src/i18n/messages/zh.mjs +1 -0
- package/src/i18n/preferences.mjs +24 -0
- package/src/i18n/screen/es.mjs +92 -0
- package/src/i18n/screen/ja.mjs +92 -0
- package/src/i18n/screen/pt.mjs +92 -0
- package/src/i18n/screen/zh.mjs +92 -0
- package/src/integrations/apple-music/music-app.mjs +9 -3
- package/src/integrations/apple-music/terminal-preview-player.mjs +31 -1
- package/src/integrations/netease/ncm-cli-player.mjs +12 -6
- package/src/integrations/playlist-links.mjs +96 -0
- package/src/integrations/public-playlists.mjs +10 -4
- package/src/integrations/spotify/service.mjs +22 -1
- package/src/integrations/stable-audio/cover.mjs +324 -0
- package/src/integrations/stable-audio/local-generator.mjs +259 -87
- package/src/integrations/stable-audio/setup.mjs +653 -0
- package/src/integrations/stable-audio/sources.mjs +106 -0
- package/src/profile/listening-history-store.mjs +1 -1
- package/src/profile/listening-profile-projection.mjs +1 -1
- package/src/profile/music-import-bundle.mjs +3 -1
- package/src/profile/music-providers.mjs +3 -0
- package/src/profile/taste-compiler.mjs +14 -5
- package/src/runtime/pi/agent-runtime.mjs +324 -31
- package/src/runtime/pi/model-switch.mjs +26 -0
- package/src/runtime/pi/model-transport.mjs +34 -4
- package/src/runtime/pi/record-name.mjs +80 -0
- package/src/runtime/pi/reply-language.mjs +45 -0
- package/src/runtime/pi/tool-arguments.mjs +89 -0
- package/src/surfaces/cli/command-completions.mjs +15 -1
- package/src/surfaces/cli/tui.mjs +404 -18
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Moondog</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center">Source candidate 0.3.0-beta.
|
|
7
|
+
<p align="center">Source candidate 0.3.0-beta.10</p>
|
|
8
8
|
|
|
9
9
|
<p align="center"><strong>A listening room that shows its work.</strong></p>
|
|
10
10
|
|
|
@@ -203,6 +203,7 @@ Your profile, and anything Moondog remembers, carry over.
|
|
|
203
203
|
- [Product charter](docs/PRODUCT_CHARTER.md) and [public roadmap](docs/ROADMAP.md): where Moondog is headed, and what it will not do.
|
|
204
204
|
- [Voice](docs/VOICE.md): how Moondog talks, and where Pink Floyd shows up.
|
|
205
205
|
- [The listening loop](docs/LISTENING_LOOP.md): how the agent keeps your choices, your recent listening, and its own picks apart.
|
|
206
|
+
- [The agent harness](docs/AGENT_HARNESS.md): how the tools keep the model's part small, so every supported model can play what you ask, and how each model is measured.
|
|
206
207
|
- [Runtime architecture](docs/ADR_0001_AGENT_RUNTIME_AND_CLI.md) and [data contracts](contracts/README.md): how the pieces fit together.
|
|
207
208
|
- [Contribution guide](CONTRIBUTING.md): local setup, checks, and how private data is kept out.
|
|
208
209
|
- [Release status](docs/PUBLIC_RELEASE_READINESS.md): what is published, and what the checks cover.
|
package/package.json
CHANGED
package/scripts/moondog.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { runUpdateCommand } from "../src/surfaces/cli/update-command.mjs";
|
|
|
7
7
|
|
|
8
8
|
import { MoondogApplication } from "../src/core/moondog-application.mjs";
|
|
9
9
|
import { openLocalMemoryStore } from "../src/memory/local-memory-store.mjs";
|
|
10
|
+
import { openLibraryStore } from "../src/core/library-store.mjs";
|
|
10
11
|
import {
|
|
11
12
|
openAppleProjectionDomainServices,
|
|
12
13
|
resolveAppleMusicSubjectId,
|
|
@@ -34,6 +35,7 @@ import { runListenerProfileCommand, savedProfileActions } from "../src/surfaces/
|
|
|
34
35
|
import { createWebResearch } from "../src/integrations/web/web-research.mjs";
|
|
35
36
|
import { runWebCommand } from "../src/surfaces/cli/web-command.mjs";
|
|
36
37
|
import { openSpotifyConnection } from "../src/integrations/spotify/connection.mjs";
|
|
38
|
+
import { preferredIdleDevice, thisComputerName } from "../src/integrations/spotify/service.mjs";
|
|
37
39
|
import {
|
|
38
40
|
openSpotifyResolutionCache,
|
|
39
41
|
} from "../src/integrations/spotify/resolution-cache.mjs";
|
|
@@ -43,6 +45,7 @@ import {
|
|
|
43
45
|
import { runSpotifyCommand } from "../src/surfaces/cli/spotify-command.mjs";
|
|
44
46
|
import { runListenBrainzCommand } from "../src/surfaces/cli/listenbrainz-command.mjs";
|
|
45
47
|
import { openListeningHistoryStore } from "../src/profile/listening-history-store.mjs";
|
|
48
|
+
import { musicHash, prepareMusicBundle } from "../src/profile/music-import-bundle.mjs";
|
|
46
49
|
import { commitSpotifyQuickImport, prepareHistoryImport, prepareSpotifyQuickImport } from "../src/profile/history-import.mjs";
|
|
47
50
|
import { persistAppleLibraryImport, refreshAppleLibraryImport } from "../src/profile/apple-library-import.mjs";
|
|
48
51
|
import { IMPORT_GUIDE_URLS } from "../src/surfaces/cli/import-guides.mjs";
|
|
@@ -54,6 +57,7 @@ import { createAppleMusicCatalog } from "../src/integrations/apple-music/catalog
|
|
|
54
57
|
import { createTerminalPreviewPlayer } from "../src/integrations/apple-music/terminal-preview-player.mjs";
|
|
55
58
|
import { createNeteasePlayer } from "../src/integrations/netease/ncm-cli-player.mjs";
|
|
56
59
|
import { createAtmosphereStream, createLocalGenerator } from "../src/integrations/stable-audio/local-generator.mjs";
|
|
60
|
+
import { createStableAudioSetup } from "../src/integrations/stable-audio/setup.mjs";
|
|
57
61
|
import { createMusicAppPlayer } from "../src/integrations/apple-music/music-app.mjs";
|
|
58
62
|
import {
|
|
59
63
|
createOpenMusicSimilarity,
|
|
@@ -69,7 +73,8 @@ import {
|
|
|
69
73
|
SPOTIFY_ARCHIVE_SET_MAXIMUM,
|
|
70
74
|
} from "../src/profile/spotify-archive-taste.mjs";
|
|
71
75
|
import { deriveArtistReleaseHintsFromSpotifyArchive } from "../src/profile/spotify-archive-catalog-hints.mjs";
|
|
72
|
-
import { resolveListenerLocale, writeLanguagePreference } from "../src/i18n/preferences.mjs";
|
|
76
|
+
import { createLanguageSelection, resolveListenerLocale, writeLanguagePreference } from "../src/i18n/preferences.mjs";
|
|
77
|
+
import { createModelSwitch } from "../src/runtime/pi/model-switch.mjs";
|
|
73
78
|
import { createPlaybackSelection, resolvePlaybackService, watchPlaybackPreference, writePlaybackPreference } from "../src/core/playback-preferences.mjs";
|
|
74
79
|
import { createFirstRun } from "../src/core/first-run.mjs";
|
|
75
80
|
|
|
@@ -146,6 +151,28 @@ function parseArguments(argv) {
|
|
|
146
151
|
};
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
// A playlist imported into the library adds its songs to the listener's taste evidence, as a kept playlist, the
|
|
155
|
+
// same way a public playlist imported from the Import guide does. Its source names where it came from.
|
|
156
|
+
const PLAYLIST_TASTE_SOURCES = { spotify: "spotify_playlist", apple_music: "apple_music_playlist", netease: "netease", qq_music: "qq_music" };
|
|
157
|
+
async function addPlaylistToTaste(found) {
|
|
158
|
+
const provider = PLAYLIST_TASTE_SOURCES[found.provider];
|
|
159
|
+
if (!provider) throw new Error("This playlist's service can't shape the profile.");
|
|
160
|
+
const historyStore = await openListeningHistoryStore();
|
|
161
|
+
try {
|
|
162
|
+
const appleSubjectId = await optionalAppleMusicSubjectId();
|
|
163
|
+
const subjectId = historyStore.localSubjectId({ ...(appleSubjectId ? { preferredSubjectId: appleSubjectId } : {}), create: true });
|
|
164
|
+
const rows = found.tracks.map((track, index) => ({ id: track.id, title: track.title, artist: track.artist, ...(track.album ? { album: track.album } : {}),
|
|
165
|
+
playlistId: found.id, playlistName: found.name, position: index + 1, member: "playlist-link.json", listenerMade: found.owned === true }));
|
|
166
|
+
const bytes = Buffer.from(JSON.stringify({ provider, id: found.id, name: found.name, rows }));
|
|
167
|
+
const prepared = prepareMusicBundle({ provider, subjectId, capturedAt: new Date().toISOString(), rows, digest: musicHash(bytes), size: bytes.length,
|
|
168
|
+
members: ["playlist-link.json"], fileName: found.name,
|
|
169
|
+
scopeNote: "A playlist the listener imported into their library. It shows what they keep, not what they played." });
|
|
170
|
+
return historyStore.ingestImport(prepared.bundle);
|
|
171
|
+
} finally {
|
|
172
|
+
historyStore.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
149
176
|
async function optionalAppleMusicSubjectId() {
|
|
150
177
|
const source = await readAppleMusicSourceStatus();
|
|
151
178
|
if (source.state === "missing") return null;
|
|
@@ -452,6 +479,76 @@ async function emitTasteprintArtifact(
|
|
|
452
479
|
return result;
|
|
453
480
|
}
|
|
454
481
|
|
|
482
|
+
// While the terminal's stream runs, the Music app is read this often. Only a change from not playing
|
|
483
|
+
// to playing ends the stream, and only in a read that began after the stream last changed and past its
|
|
484
|
+
// start: the song the first track pauses keeps playing while that track is made.
|
|
485
|
+
const STREAM_MUSIC_APP_MS = 5_000;
|
|
486
|
+
function watchMusicAppForStream(stream, { musicApp, interval = STREAM_MUSIC_APP_MS }) {
|
|
487
|
+
if (musicApp?.available !== true) return () => {};
|
|
488
|
+
let timer = null;
|
|
489
|
+
let reading = false;
|
|
490
|
+
let playing = null;
|
|
491
|
+
let phase = null;
|
|
492
|
+
let changedAt = 0;
|
|
493
|
+
const read = async () => {
|
|
494
|
+
if (reading) return;
|
|
495
|
+
reading = true;
|
|
496
|
+
const began = Date.now();
|
|
497
|
+
try {
|
|
498
|
+
const now = await musicApp.nowPlaying().catch(() => null);
|
|
499
|
+
const view = stream.view();
|
|
500
|
+
if (!view || began <= changedAt) return;
|
|
501
|
+
const playingNow = now?.state === "playing";
|
|
502
|
+
if (playingNow && playing === false && view.phase !== "starting") stream.stop("other_music");
|
|
503
|
+
playing = playingNow;
|
|
504
|
+
} finally {
|
|
505
|
+
reading = false;
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
const unsubscribe = stream.subscribe(({ view }) => {
|
|
509
|
+
changedAt = Date.now();
|
|
510
|
+
// The first track just paused the Music app, so playing at a later read is news.
|
|
511
|
+
if (phase === "starting" && view && view.phase !== "starting") playing = false;
|
|
512
|
+
phase = view?.phase ?? null;
|
|
513
|
+
if (view && !timer) {
|
|
514
|
+
playing = null;
|
|
515
|
+
timer = setInterval(() => void read(), interval);
|
|
516
|
+
timer.unref?.();
|
|
517
|
+
} else if (!view && timer) {
|
|
518
|
+
clearInterval(timer);
|
|
519
|
+
timer = null;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
return () => {
|
|
523
|
+
unsubscribe();
|
|
524
|
+
clearInterval(timer);
|
|
525
|
+
timer = null;
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Whether a /spotify command starts Spotify on this Mac: play on the device it resumes on (the active one,
|
|
530
|
+
// or with none active the one it would pick), or a move onto this Mac with --play or while Spotify plays (a
|
|
531
|
+
// move keeps playing what plays). A command its own checks refuse, or reads that fail, start nothing here.
|
|
532
|
+
async function spotifyStartsHere(application, args, { signal } = {}) {
|
|
533
|
+
if (!application.spotifyReady?.()) return false;
|
|
534
|
+
// A trailing --json changes only how the result is written.
|
|
535
|
+
if (args.at(-1) === "--json") args = args.slice(0, -1);
|
|
536
|
+
const play = args[0] === "play" && args.length <= 2;
|
|
537
|
+
const transfer = args[0] === "transfer" && args.length >= 2 && args.length <= 3 && (args.length === 2 || args[2] === "--play");
|
|
538
|
+
if (!play && !transfer) return false;
|
|
539
|
+
const service = application.requireSpotifyService();
|
|
540
|
+
const [listed, player] = await Promise.all([service.devices({ signal }).catch(() => null), service.currentPlayer({ signal }).catch(() => null)]);
|
|
541
|
+
const devices = listed?.devices ?? [];
|
|
542
|
+
if (transfer) {
|
|
543
|
+
const device = devices.find((entry) => entry.id === args[1]);
|
|
544
|
+
const playing = player?.state === "available" && player.is_playing === true;
|
|
545
|
+
return Boolean(device) && application.spotifyOnThisMac(device) && (args[2] === "--play" || playing);
|
|
546
|
+
}
|
|
547
|
+
const active = player?.device?.is_active === true ? player.device : devices.find((device) => device.is_active === true);
|
|
548
|
+
const device = active ?? preferredIdleDevice(devices, { thisComputer: await thisComputerName() });
|
|
549
|
+
return Boolean(device) && application.spotifyOnThisMac(device);
|
|
550
|
+
}
|
|
551
|
+
|
|
455
552
|
async function main() {
|
|
456
553
|
const updater = createApplicationUpdater();
|
|
457
554
|
const argv = process.argv.slice(2);
|
|
@@ -830,6 +927,9 @@ async function main() {
|
|
|
830
927
|
|
|
831
928
|
const domainState = await loadDomainServices();
|
|
832
929
|
const memoryStore = await openLocalMemoryStore();
|
|
930
|
+
// The listener's library: Saved, their playlists and folders, on one file the terminal and the window share.
|
|
931
|
+
// A library that can't open leaves the rest of Moondog working, without it.
|
|
932
|
+
const libraryStore = await openLibraryStore().catch(() => null);
|
|
833
933
|
const spotifyConnection = await loadSpotifyConnection();
|
|
834
934
|
const musicCatalog = createAppleMusicCatalog();
|
|
835
935
|
const artistIdentityResolver = createWikidataArtistResolver();
|
|
@@ -851,10 +951,14 @@ async function main() {
|
|
|
851
951
|
const neteasePlayer = createNeteasePlayer();
|
|
852
952
|
const playbackService = await resolvePlaybackService();
|
|
853
953
|
if (playbackService === "netease") await neteasePlayer.check();
|
|
854
|
-
// Moondog makes music on this Mac with
|
|
855
|
-
//
|
|
856
|
-
//
|
|
857
|
-
|
|
954
|
+
// Moondog makes music on this Mac with Stable Audio 3 once the listener turns it on, in a room
|
|
955
|
+
// that can play what it makes: Moondog's own install, downloaded when they ask, or their folder.
|
|
956
|
+
// The terminal and the window each keep one generator, so each makes one track at a time; a
|
|
957
|
+
// choice saved on one side is checked again on the other.
|
|
958
|
+
const localGenerator = previewPlayer ? createLocalGenerator({ keeps: (id) => libraryStore?.holdsMade(id) === true }) : null;
|
|
959
|
+
const generationSetup = localGenerator ? createStableAudioSetup() : null;
|
|
960
|
+
// A model set up or removed, here or on the other side, is checked again.
|
|
961
|
+
const unwatchGenerationSetup = generationSetup?.subscribe(() => { void localGenerator.check().catch(() => {}); }) ?? (() => {});
|
|
858
962
|
await localGenerator?.check();
|
|
859
963
|
void localGenerator?.sweep().catch(() => {});
|
|
860
964
|
const unwatchGeneration = localGenerator?.watch() ?? (() => {});
|
|
@@ -869,11 +973,20 @@ async function main() {
|
|
|
869
973
|
const unobserveStream = atmosphereStream ? previewPlayer.subscribe?.((event) => atmosphereStream.observe(event)) : null;
|
|
870
974
|
// Music making turned off, here or on the other surface, ends the stream.
|
|
871
975
|
const unwatchStreamGeneration = atmosphereStream ? localGenerator.subscribe((status) => { if (status.state !== "ready") atmosphereStream.stop("generation_off"); }) : null;
|
|
976
|
+
// The window watches the Music app for its own stream; the terminal has no window, so while its
|
|
977
|
+
// stream runs it reads the Music app itself, and music the listener starts there ends the stream.
|
|
978
|
+
const unwatchStreamMusicApp = atmosphereStream && !host ? watchMusicAppForStream(atmosphereStream, { musicApp }) : null;
|
|
872
979
|
const playbackSelection = createPlaybackSelection({ service: playbackService, save: writePlaybackPreference });
|
|
980
|
+
// A reply language the listener asks for in words is saved like the picker's and reaches every surface.
|
|
981
|
+
const languageSelection = createLanguageSelection({ save: (locale) => writeLanguagePreference(locale) });
|
|
982
|
+
// The listener's model menu, shared by the window, the terminal and a model asked for in words.
|
|
983
|
+
const modelSettings = createModelSettings({ environment: process.env });
|
|
984
|
+
const modelSwitch = createModelSwitch({ list: () => modelSettings.list() });
|
|
873
985
|
// The terminal and the window each run this core on one playback.json. A service chosen on the
|
|
874
|
-
// other side reaches this side's picker and agents, and stops the
|
|
986
|
+
// other side reaches this side's picker and agents, and stops the Apple Music songs this side plays.
|
|
987
|
+
// Moondog's stream is not a service: its made tracks play on.
|
|
875
988
|
const unwatchPlayback = watchPlaybackPreference(playbackSelection, {
|
|
876
|
-
onChange: (service) => { if (service !== "apple_music"
|
|
989
|
+
onChange: (service) => { if (service !== "apple_music" && previewPlayer?.holdsCatalogSongs?.() !== false) previewPlayer?.stop?.(); },
|
|
877
990
|
});
|
|
878
991
|
// The main application and every conversation the window opens beside it are built from
|
|
879
992
|
// this one list, so they cannot drift apart. Each passes in what may change after launch:
|
|
@@ -881,9 +994,11 @@ async function main() {
|
|
|
881
994
|
// Spotify's four-week top artists, as the listening upkeep last read them.
|
|
882
995
|
const spotifyTopArtists = { read_at: null, artists: [] };
|
|
883
996
|
const applicationOptions = ({ domainServices, domainServicesError, spotifyConnection, locale, playbackService }) => ({
|
|
884
|
-
domainServices, domainServicesError, spotifyConnection, locale, playbackService, playbackSelection,
|
|
997
|
+
domainServices, domainServicesError, spotifyConnection, locale, playbackService, playbackSelection, languageSelection, modelSwitch,
|
|
885
998
|
webResearch, memoryStore, musicCatalog, musicPreviewPlayer: previewPlayer, neteasePlayer, localGenerator, atmosphereStream, musicSimilarity, artistIdentityResolver,
|
|
886
|
-
spotifyTopArtists, musicApp,
|
|
999
|
+
spotifyTopArtists, musicApp, libraryStore,
|
|
1000
|
+
// A playlist that shapes the taste reloads the profile services at once, so the evidence counts in this session.
|
|
1001
|
+
playlistTaste: async (found) => { const receipt = await addPlaylistToTaste(found); replaceDomainServices(await loadDomainServices()); return receipt; },
|
|
887
1002
|
});
|
|
888
1003
|
const application = new MoondogApplication(applicationOptions({
|
|
889
1004
|
...domainState, spotifyConnection, locale: await resolveListenerLocale(), playbackService,
|
|
@@ -1083,7 +1198,12 @@ async function main() {
|
|
|
1083
1198
|
status: () => localGenerator.status(),
|
|
1084
1199
|
check: () => localGenerator.check(),
|
|
1085
1200
|
save: (settings) => localGenerator.save(settings),
|
|
1201
|
+
// Uses an installed model of Moondog's own install.
|
|
1202
|
+
use: async (model) => (generationSetup?.status()?.models?.[model]?.state === "ready"
|
|
1203
|
+
? localGenerator.save({ source: "managed", model }) : localGenerator.check()),
|
|
1086
1204
|
subscribe: (listener) => localGenerator.subscribe(listener),
|
|
1205
|
+
// Download, cancel and remove, shared by the window and the terminal.
|
|
1206
|
+
setup: generationSetup,
|
|
1087
1207
|
},
|
|
1088
1208
|
// The room asks whether a Last.fm key is set, and saves one the listener pastes.
|
|
1089
1209
|
lastfm: {
|
|
@@ -1113,7 +1233,7 @@ async function main() {
|
|
|
1113
1233
|
return createConfiguredRuntime(target, process.env, { selection, toolTrace });
|
|
1114
1234
|
},
|
|
1115
1235
|
// The desktop window lists models and signs in here, with no terminal.
|
|
1116
|
-
modelSettings
|
|
1236
|
+
modelSettings,
|
|
1117
1237
|
// A model chosen or a sign-in made on the other surface. Model variables fix the model at
|
|
1118
1238
|
// launch, so a saved choice has nothing to change then.
|
|
1119
1239
|
watchModelSettings: (onChange) => (process.env.MOONDOG_PROVIDER !== undefined || process.env.MOONDOG_MODEL !== undefined
|
|
@@ -1167,6 +1287,9 @@ async function main() {
|
|
|
1167
1287
|
return json ? `\`\`\`json\n${output.trim()}\n\`\`\`` : output.trim();
|
|
1168
1288
|
},
|
|
1169
1289
|
runSpotify: async (args, { signal, stderr } = {}) => {
|
|
1290
|
+
// /spotify play, and a move onto this Mac that plays there, start Spotify here: the other players go
|
|
1291
|
+
// quiet first. A command its own checks will refuse, or a move to another device, quiets nothing.
|
|
1292
|
+
if (await spotifyStartsHere(application, args, { signal })) await application.quietOtherPlayers("spotify", { signal });
|
|
1170
1293
|
let output = "";
|
|
1171
1294
|
const capture = {
|
|
1172
1295
|
write(value) {
|
|
@@ -1192,6 +1315,10 @@ async function main() {
|
|
|
1192
1315
|
} finally {
|
|
1193
1316
|
unwatchPlayback();
|
|
1194
1317
|
unwatchGeneration();
|
|
1318
|
+
unwatchGenerationSetup();
|
|
1319
|
+
// A download or uv run this process owns would keep it alive after the room closes; it stops and
|
|
1320
|
+
// resumes next time. One owned by the other window or terminal runs on.
|
|
1321
|
+
await generationSetup?.close?.();
|
|
1195
1322
|
// A made track still playing is stopped while the generator listens, so its outcome is kept.
|
|
1196
1323
|
if (localGenerator) {
|
|
1197
1324
|
previewPlayer?.stop?.();
|
|
@@ -1200,6 +1327,7 @@ async function main() {
|
|
|
1200
1327
|
unobservePlayer?.();
|
|
1201
1328
|
unobserveStream?.();
|
|
1202
1329
|
unwatchStreamGeneration?.();
|
|
1330
|
+
unwatchStreamMusicApp?.();
|
|
1203
1331
|
atmosphereStream?.stop("closed");
|
|
1204
1332
|
listeningUpkeep?.stop();
|
|
1205
1333
|
lyrics?.close();
|
|
@@ -7,6 +7,11 @@ const catalog = [
|
|
|
7
7
|
description: "Inspect or change the current music service through the same saved selection as the host's service picker.",
|
|
8
8
|
agent_tool: { name: "moondog_playback_service", label: N_("Choose a music service") },
|
|
9
9
|
},
|
|
10
|
+
{
|
|
11
|
+
id: "settings", version: "1", state: "enabled", effect: "write_local",
|
|
12
|
+
description: "Read Moondog's own setup (model, music service, reply language) and change the settings the listener may change by asking.",
|
|
13
|
+
agent_tool: { name: "moondog_settings", label: N_("Read or change Moondog's settings") },
|
|
14
|
+
},
|
|
10
15
|
{
|
|
11
16
|
id: "spotify.discovery.search", version: "1", state: "enabled", effect: "read_external", requires_spotify: true,
|
|
12
17
|
description: "Verify open-world music hypotheses with bounded Spotify queries, retaining a shared candidate pool.",
|
|
@@ -601,6 +606,50 @@ const catalog = [
|
|
|
601
606
|
label: N_("Save the playlist to Spotify"),
|
|
602
607
|
},
|
|
603
608
|
},
|
|
609
|
+
{
|
|
610
|
+
id: "collection.list",
|
|
611
|
+
version: "1",
|
|
612
|
+
state: "enabled",
|
|
613
|
+
effect: "read_local",
|
|
614
|
+
description: "Find playlists in the listener's own library on this Mac, by name, with what each holds.",
|
|
615
|
+
agent_tool: {
|
|
616
|
+
name: "moondog_collection_list",
|
|
617
|
+
label: N_("Look in the library"),
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
id: "collection.save",
|
|
622
|
+
version: "1",
|
|
623
|
+
state: "enabled",
|
|
624
|
+
effect: "write_local",
|
|
625
|
+
description: "Keep songs or tracks Moondog made in the listener's library on this Mac: in Saved, in one of their playlists, or in a new playlist.",
|
|
626
|
+
agent_tool: {
|
|
627
|
+
name: "moondog_collection_save",
|
|
628
|
+
label: N_("Save to the library"),
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
id: "collection.import",
|
|
633
|
+
version: "1",
|
|
634
|
+
state: "enabled",
|
|
635
|
+
effect: "write_local",
|
|
636
|
+
description: "Import a playlist link from Spotify, Apple Music, NetEase Music or QQ Music into the listener's library on this Mac.",
|
|
637
|
+
agent_tool: {
|
|
638
|
+
name: "moondog_collection_import",
|
|
639
|
+
label: N_("Import a playlist"),
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
id: "collection.play",
|
|
644
|
+
version: "1",
|
|
645
|
+
state: "enabled",
|
|
646
|
+
effect: "play_library",
|
|
647
|
+
description: "Play a playlist from the listener's library on the music service they chose.",
|
|
648
|
+
agent_tool: {
|
|
649
|
+
name: "moondog_collection_play",
|
|
650
|
+
label: N_("Play from the library"),
|
|
651
|
+
},
|
|
652
|
+
},
|
|
604
653
|
{
|
|
605
654
|
id: "music.generate.local",
|
|
606
655
|
version: "1",
|