@audiofool/moondog 0.3.0-beta.10 → 0.3.0-beta.12

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 (31) hide show
  1. package/README.md +34 -9
  2. package/package.json +2 -2
  3. package/scripts/moondog.mjs +10 -4
  4. package/src/core/apple-projection-domain-services.mjs +4 -0
  5. package/src/core/capability-catalog.mjs +11 -0
  6. package/src/core/library-store.mjs +48 -4
  7. package/src/core/listening-profile-domain-services.mjs +4 -0
  8. package/src/core/moondog-application.mjs +105 -4
  9. package/src/i18n/screen/es.mjs +18 -4
  10. package/src/i18n/screen/ja.mjs +18 -4
  11. package/src/i18n/screen/pt.mjs +18 -4
  12. package/src/i18n/screen/zh.mjs +18 -4
  13. package/src/integrations/playlist-links.mjs +27 -9
  14. package/src/integrations/public-playlists.mjs +32 -5
  15. package/src/integrations/spotify/service.mjs +6 -1
  16. package/src/integrations/spotify/web-api-client.mjs +8 -2
  17. package/src/integrations/web/deepseek-web.mjs +1 -1
  18. package/src/integrations/web/web-research.mjs +84 -17
  19. package/src/integrations/web/web-result.mjs +60 -12
  20. package/src/profile/listening-history-store.mjs +54 -0
  21. package/src/runtime/pi/agent-runtime.mjs +73 -11
  22. package/src/runtime/pi/configured-runtime.mjs +3 -0
  23. package/src/runtime/pi/image-input.mjs +148 -0
  24. package/src/runtime/pi/model-roles.mjs +121 -0
  25. package/src/runtime/pi/model-settings.mjs +63 -2
  26. package/src/runtime/pi/runtime-settings.mjs +4 -2
  27. package/src/surfaces/cli/brand-components.mjs +1 -1
  28. package/src/surfaces/cli/command-completions.mjs +42 -3
  29. package/src/surfaces/cli/format-output.mjs +1 -1
  30. package/src/surfaces/cli/model-roles-command.mjs +60 -0
  31. package/src/surfaces/cli/tui.mjs +17 -3
@@ -38,6 +38,33 @@ const webChoices = choices([
38
38
  ["status", N_("Check whether web lookups work")],
39
39
  ["help", N_("Web lookup commands")],
40
40
  ]);
41
+ // The picker's own words. The scale matches EFFORT_SCALE; the test holds them together.
42
+ const effortChoices = choices([
43
+ ["default", N_("Moondog's default: thinking off where the model allows it")],
44
+ ["off", N_("No extra thinking")],
45
+ ["low", N_("Quick answers")],
46
+ ["medium", N_("Between speed and depth")],
47
+ ["high", N_("Thinks more before answering")],
48
+ ["xhigh", N_("Thinks longer; slower and costs more")],
49
+ ["max", N_("Thinks the most; slowest and costs the most")],
50
+ ]);
51
+ const libraryChoices = choices([
52
+ ["play", N_("Play a playlist by name")],
53
+ ["save", N_("Keep the song playing now")],
54
+ ["import", N_("Copy a playlist from a link")],
55
+ ]);
56
+ const modelRoleChoices = choices([
57
+ ["roles", N_("See what side tasks use")],
58
+ ["vision", N_("Choose the model that reads images")],
59
+ ["web", N_("Choose the web search service")],
60
+ ]);
61
+ const visionAutoChoices = choices([["auto", N_("Let the chat model read images when it can")]]);
62
+ const webServiceChoices = choices([
63
+ ["auto", N_("Take the first service that is set up")],
64
+ ["deepseek", N_("Search with DeepSeek")],
65
+ ["brave", N_("Search with Brave Search")],
66
+ ["codex", N_("Search with the Codex CLI")],
67
+ ]);
41
68
  const spotifyChoices = choices([
42
69
  ["status", N_("Check your Spotify connection")],
43
70
  ["now", N_("What's playing now")],
@@ -102,6 +129,8 @@ export function withCommandCompletions(commands, {
102
129
  art: { argumentHint: "[braille|ascii|off|auto]", choices: firstArgument(artChoices) },
103
130
  motion: { argumentHint: "[on|off]", choices: firstArgument(motionChoices) },
104
131
  service: { argumentHint: "[spotify|apple_music|netease]", choices: firstArgument(serviceChoices) },
132
+ library: { argumentHint: "[play <name> | save [playlist] | import <link>]", choices: firstArgument(libraryChoices) },
133
+ effort: { argumentHint: "[default|off|low|medium|high|xhigh|max]", choices: firstArgument(effortChoices) },
105
134
  // With the core's setup, /generation also downloads, uses and removes Moondog's own models.
106
135
  generation: generationSetup ? {
107
136
  argumentHint: "[on [folder]|off|download|use|remove|cancel]",
@@ -130,13 +159,23 @@ export function withCommandCompletions(commands, {
130
159
  .map((provider) => ({ value: provider.id, label: provider.id, description: provider.name })) : [],
131
160
  },
132
161
  model: {
133
- argumentHint: "[provider] [model] | refresh [provider]",
162
+ argumentHint: "[provider] [model] | refresh [provider] | roles | vision auto|<provider> <model> | web auto|deepseek|brave|codex",
134
163
  choices: (previous) => {
135
- if (previous.length > 1) return [];
136
164
  const available = providers().filter((provider) => provider.modelCount > 0);
165
+ const asChoice = (provider) => ({ value: provider.id, label: provider.id, description: provider.name });
166
+ const subcommand = previous[0]?.toLowerCase();
167
+ if (subcommand === "roles") return [];
168
+ if (subcommand === "web") return previous.length === 1 ? webServiceChoices : [];
169
+ if (subcommand === "vision") {
170
+ if (previous.length === 1) return [...visionAutoChoices, ...available.map(asChoice)];
171
+ const provider = previous[1].toLowerCase();
172
+ return previous.length === 2 && available.some((item) => item.id === provider)
173
+ ? models(provider).map((model) => ({ value: model.id, label: model.id, description: model.name })) : [];
174
+ }
175
+ if (previous.length > 1) return [];
137
176
  if (previous.length === 0) {
138
177
  return [{ value: "refresh", label: "refresh", description: translate("Refresh public model metadata; keep selection") },
139
- ...available.map((provider) => ({ value: provider.id, label: provider.id, description: provider.name }))];
178
+ ...modelRoleChoices, ...available.map(asChoice)];
140
179
  }
141
180
  const providerId = previous[0].toLowerCase();
142
181
  if (providerId === "refresh") return available.filter(provider => authProviderIds.includes(provider.id))
@@ -558,5 +558,5 @@ export function formatLocalResult(command, value, tr = screenTranslator("en")) {
558
558
  }
559
559
 
560
560
  export function helpText(tr = screenTranslator("en")) {
561
- return tr("# Every command\n\nFrom your shell:\n\n- `moondog --version` - show the installed version\n- `moondog update [--check] [--channel latest|beta]` - check for or install a published release\n- `moondog web status|search|read [arguments] [--json]` - look things up on public music sites through DeepSeek, Brave Search or the Codex CLI\n- `moondog demo-history --output <absolute-file.zip> [--json]` - make a fictional Spotify history ZIP to try the importer with\n- `moondog auth login <provider>` - sign in to a model provider; the key is typed out of sight; `openai --chatgpt` signs in with your ChatGPT subscription\n- `moondog auth status [provider]` - see which sign-ins are saved\n- `moondog auth logout <provider>` - sign out of a provider\n- `moondog spotify help` - set up and control Spotify\n- `moondog spotify login [client-id]` - connect Spotify in your browser\n- `moondog spotify now|devices|queue` - see what Spotify is playing, where, and what's next\n- `moondog spotify recent` - see what you played lately\n- `moondog spotify sync-recent` - add your latest plays to your profile\n- `moondog spotify sync-library` - add your Spotify library to your profile\n- `moondog spotify play|pause|next|previous` - control Spotify playback\n- `moondog listenbrainz import-history <listen-history.json>` - add a ListenBrainz listens file to your profile\n- `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\n- `moondog status [--json]` - see where things stand\n- `moondog taste [--json]` - your listening report\n- `moondog taste --html [--output <file.html>]` - save your report as a web page that works offline\n- `moondog taste --card [--output <file.html>]` - save a short recap you can check before sharing\n- `moondog taste --from <spotify-history.zip> --html` - a one-off report from a Spotify ZIP, without saving it\n- `moondog taste --from <account-data.zip> --from <extended-history.zip> --html` - a one-off report from both Spotify exports together\n- `moondog taste --from <spotify-history.zip> --save --html` - save the Spotify history, then show your full report\n- `moondog data inspect --scope <listening|apple|profile>` - see what's stored, and get the code needed to reset it\n- `moondog data export --scope <listening|apple|profile> --output <directory>` - export a copy, leaving the original alone\n- `moondog data reset --scope <listening|apple|profile> --confirm <token>` - set stored data aside (it is archived, not deleted)\n- `moondog profile build [--force]` - build or resume a saved profile with your connected model\n- `moondog profile saved [offset]` - read your saved profile\n- `moondog profile explain <number>` - trace a saved finding to its evidence\n- `moondog profile corrections [--all]` - everything you've told me, including earlier choices\n- `moondog profile correct --artist <name> (--like|--avoid)` - like an artist, or keep them out\n- `moondog profile correct --track <title> --by <artist> (--like|--avoid)` - like a track, or keep it out\n- `moondog profile retract <correction-id>` - undo one of your choices\n- `moondog demo --offline [--json]` - a demo with made-up data, no sign-in needed\n- `moondog demo [prompt] [--json]` - the same demo, talking through your model\n- `moondog ask <prompt>` - ask one question and exit\n- `moondog remember <text>` - ask me to remember something\n- `moondog forget <memory-id>` - forget something\n- `moondog memory reflect [--dry-run] [--json]` - let me think over recent conversations now\n- `moondog` - open the listening room\n\nInside the listening room:\n\n- `/update [--check] [--channel latest|beta]` - check for updates, or save and close this session to install one\n- `/status` or `/sources` - see where things stand\n- `/profile` or `/taste` - your profile, and why each song or artist is there\n- `/taste report` - the whole report on one page\n- `/profile build [--force]` - build or resume a saved reading; Ctrl+C keeps progress\n- `/profile saved [offset]` - read your saved profile\n- `/profile explain <number>` - inspect a saved finding's evidence\n- `/profile corrections [--all]` - everything you've told me\n- `/profile correct --track \"<title>\" --by \"<artist>\" (--like|--avoid)` - like a track, or keep it out\n- `/profile correct --artist \"<name>\" (--like|--avoid)` - like an artist, or keep them out\n- `/profile retract <correction-id>` - undo a choice\n- `/memory` - what I remember\n- `/tools` - what I can do right now\n- `/doctor` - check your setup\n- `/model` - pick the model I talk through\n- `/model <provider> <model>` - switch straight to a model\n- `/model refresh [provider]` - update public model choices; keep the selected model\n- `/auth [provider]` - sign in to a model provider\n- `/web status|search|read` - look things up on public music sites\n- `/import [path]` - bring in your history, library, or a playlist\n- `/language` - choose the language Moondog uses\n- `/service [spotify|apple_music|netease]` - choose where I play music\n- `/spotify` - Spotify commands\n- `/spotify login [client-id]` - connect Spotify\n- `/spotify now|devices|queue` - see what's playing, where, and what's next\n- `/spotify recent|sync-recent` - see or save your latest plays\n- `/spotify import-history \"/path/to/spotify-history.zip\"` - add a Spotify ZIP, no sign-in needed\n- `/skip` or `/stop` - move to the next song preview, or stop them\n- `/reload` - reload model settings and sign-ins\n- `/remember [kind] <text>` - ask me to remember something\n- `/forget <memory-id>` - forget something\n- `/rewind` or Alt+R - edit an earlier message in place; the messages after it are replaced\n- `/branch` - go on from here in a new conversation; this one stays saved\n- `/rewind latest` - edit the most recent message\n- `/resume` - pick up a saved conversation\n- `/resume <session-id>` - go straight to one\n- `/new` - start fresh; the last one stays saved\n- `/quit` - exit\n\nAnything 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`.\nSign in with `/auth`, or set the provider's API key in your environment. `/auth openai` signs in with your ChatGPT subscription or an API key.\nOnce your profile is ready, I can search your library, explain what I see, and plan playlists with you.\nI can only control Spotify after `moondog spotify login`, and I never post, send messages, delete anything, or spend money.\nAsk me to move playback onto a device by name, such as your iPhone.");
561
+ return tr("# Every command\n\nFrom your shell:\n\n- `moondog --version` - show the installed version\n- `moondog update [--check] [--channel latest|beta]` - check for or install a published release\n- `moondog web status|search|read [arguments] [--json]` - look things up on public music sites through DeepSeek, Brave Search or the Codex CLI\n- `moondog demo-history --output <absolute-file.zip> [--json]` - make a fictional Spotify history ZIP to try the importer with\n- `moondog auth login <provider>` - sign in to a model provider; the key is typed out of sight; `openai --chatgpt` signs in with your ChatGPT subscription\n- `moondog auth status [provider]` - see which sign-ins are saved\n- `moondog auth logout <provider>` - sign out of a provider\n- `moondog spotify help` - set up and control Spotify\n- `moondog spotify login [client-id]` - connect Spotify in your browser\n- `moondog spotify now|devices|queue` - see what Spotify is playing, where, and what's next\n- `moondog spotify recent` - see what you played lately\n- `moondog spotify sync-recent` - add your latest plays to your profile\n- `moondog spotify sync-library` - add your Spotify library to your profile\n- `moondog spotify play|pause|next|previous` - control Spotify playback\n- `moondog listenbrainz import-history <listen-history.json>` - add a ListenBrainz listens file to your profile\n- `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\n- `moondog status [--json]` - see where things stand\n- `moondog taste [--json]` - your listening report\n- `moondog taste --html [--output <file.html>]` - save your report as a web page that works offline\n- `moondog taste --card [--output <file.html>]` - save a short recap you can check before sharing\n- `moondog taste --from <spotify-history.zip> --html` - a one-off report from a Spotify ZIP, without saving it\n- `moondog taste --from <account-data.zip> --from <extended-history.zip> --html` - a one-off report from both Spotify exports together\n- `moondog taste --from <spotify-history.zip> --save --html` - save the Spotify history, then show your full report\n- `moondog data inspect --scope <listening|apple|profile>` - see what's stored, and get the code needed to reset it\n- `moondog data export --scope <listening|apple|profile> --output <directory>` - export a copy, leaving the original alone\n- `moondog data reset --scope <listening|apple|profile> --confirm <token>` - set stored data aside (it is archived, not deleted)\n- `moondog profile build [--force]` - build or resume a saved profile with your connected model\n- `moondog profile saved [offset]` - read your saved profile\n- `moondog profile explain <number>` - trace a saved finding to its evidence\n- `moondog profile corrections [--all]` - everything you've told me, including earlier choices\n- `moondog profile correct --artist <name> (--like|--avoid)` - like an artist, or keep them out\n- `moondog profile correct --track <title> --by <artist> (--like|--avoid)` - like a track, or keep it out\n- `moondog profile retract <correction-id>` - undo one of your choices\n- `moondog demo --offline [--json]` - a demo with made-up data, no sign-in needed\n- `moondog demo [prompt] [--json]` - the same demo, talking through your model\n- `moondog ask <prompt>` - ask one question and exit\n- `moondog remember <text>` - ask me to remember something\n- `moondog forget <memory-id>` - forget something\n- `moondog memory reflect [--dry-run] [--json]` - let me think over recent conversations now\n- `moondog` - open the listening room\n\nInside the listening room:\n\n- `/update [--check] [--channel latest|beta]` - check for updates, or save and close this session to install one\n- `/status` or `/sources` - see where things stand\n- `/profile` or `/taste` - your profile, and why each song or artist is there\n- `/taste report` - the whole report on one page\n- `/profile build [--force]` - build or resume a saved reading; Ctrl+C keeps progress\n- `/profile saved [offset]` - read your saved profile\n- `/profile explain <number>` - inspect a saved finding's evidence\n- `/profile corrections [--all]` - everything you've told me\n- `/profile correct --track \"<title>\" --by \"<artist>\" (--like|--avoid)` - like a track, or keep it out\n- `/profile correct --artist \"<name>\" (--like|--avoid)` - like an artist, or keep them out\n- `/profile retract <correction-id>` - undo a choice\n- `/memory` - what I remember\n- `/tools` - what I can do right now\n- `/doctor` - check your setup\n- `/model` - pick the model I talk through\n- `/model <provider> <model>` - switch straight to a model\n- `/model refresh [provider]` - update public model choices; keep the selected model\n- `/model roles` - see what side tasks use: the model that reads images and the web search service\n- `/model vision auto|<provider> <model>` - choose the model that reads images\n- `/model web auto|deepseek|brave|codex` - choose the web search service\n- `/effort` - choose how hard the model thinks\n- `/auth [provider]` - sign in to a model provider\n- `/endpoint add` or `/endpoint remove <id>` - add a model service by its address, or remove it\n- `/web status|search|read` - look things up on public music sites\n- `/import [path]` - bring in your history, library, or a playlist\n- `/library` - your saved songs and playlists\n- `/library play <name>` - play a playlist\n- `/library save [playlist]` - keep the song playing now\n- `/library import <link>` - copy a playlist from a link\n- `/generation on|off` - let Moondog make music on this Mac, or stop\n- `/generation download|use|remove|cancel` - set up a Stable Audio 3 model\n- `/onair` - go on air, or come off it\n- `/language` - choose the language Moondog uses\n- `/service [spotify|apple_music|netease]` - choose where I play music\n- `/spotify` - Spotify commands\n- `/spotify login [client-id]` - connect Spotify\n- `/spotify now|devices|queue` - see what's playing, where, and what's next\n- `/spotify recent|sync-recent` - see or save your latest plays\n- `/spotify import-history \"/path/to/spotify-history.zip\"` - add a Spotify ZIP, no sign-in needed\n- `/skip` or `/stop` - move to the next song preview or stream track, or stop them\n- `/home` - back to the record sleeve; the conversation stays\n- `/theme paper|charcoal|terminal|auto` - change the look\n- `/art braille|ascii|off` - change the artwork\n- `/motion on|off` - turn the animation on or off\n- `/lyrics` - the lyrics on the sleeve; `/lyrics sync` finds more\n- `/reload` - reload model settings and sign-ins\n- `/remember [kind] <text>` - ask me to remember something\n- `/forget <memory-id>` - forget something\n- `/rewind` or Alt+R - edit an earlier message in place; the messages after it are replaced\n- `/branch` - go on from here in a new conversation; this one stays saved\n- `/rewind latest` - edit the most recent message\n- `/resume` - pick up a saved conversation\n- `/resume <session-id>` - go straight to one\n- `/new` - start fresh; the last one stays saved\n- `/quit` - exit\n\nAnything 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`.\nSign in with `/auth`, or set the provider's API key in your environment. `/auth openai` signs in with your ChatGPT subscription or an API key.\nOnce your profile is ready, I can search your library, explain what I see, and plan playlists with you.\nI can only control Spotify after `moondog spotify login`. I ask before I remove anything from your library, and I never post, send messages, or spend money.\nAsk me to move playback onto a device by name, such as your iPhone.");
562
562
  }
@@ -0,0 +1,60 @@
1
+ import { MODEL_ROLES, WEB_SERVICES } from "../../runtime/pi/model-roles.mjs";
2
+
3
+ // /model roles, /model vision and /model web: the terminal's side of Settings' Auxiliary models.
4
+ // The chat model stays /model <provider> <model>.
5
+ export const MODEL_ROLE_WORDS = Object.freeze({ roles: null, vision: "vision", web: "web_search", audio: "audio" });
6
+
7
+ const titles = { chat: "Chat", vision: "Vision", web_search: "Web search", audio: "Audio" };
8
+ const usage = "Usage: /model roles, /model vision auto|<provider> <model>, or /model web auto|deepseek|brave|codex.";
9
+
10
+ const modelName = (choice) => `${choice.name ?? choice.model} (${choice.providerName ?? choice.provider})`;
11
+
12
+ function describe(role) {
13
+ if (role.state !== "enabled") return "not available yet; its place is kept for a later Moondog";
14
+ if (role.id === "chat") return role.choice ? modelName(role.choice) : "none chosen; /model picks one";
15
+ if (role.kind === "service") {
16
+ const service = (id) => role.services?.find((entry) => entry.id === id || entry.provider === id);
17
+ if (role.choice) {
18
+ const chosen = service(role.choice.service);
19
+ return `${chosen?.name ?? role.choice.service}${chosen && !chosen.ready ? " (not set up; falls back to the next service)" : ""}`;
20
+ }
21
+ const used = service(role.search);
22
+ return used?.ready ? `auto, now ${used.name}` : "auto, but no service is set up; /web status says how";
23
+ }
24
+ if (role.choice) return modelName(role.choice);
25
+ return role.auto ? `auto, now ${modelName(role.auto)}` : "auto, but the chat model can't see images";
26
+ }
27
+
28
+ export function formatModelRoles(roles) {
29
+ const lines = roles.map((role) => `- ${titles[role.id] ?? role.id}: ${describe(role)}`);
30
+ return `# Chat and auxiliary models\n\n${lines.join("\n")}\n\n` +
31
+ "`/model vision <provider> <model>` gives images a model of their own, and `/model vision auto` gives them back to the chat model when it can see.\n" +
32
+ "`/model web deepseek|brave|codex` picks the web search service, and `/model web auto` takes the first one set up.";
33
+ }
34
+
35
+ export async function runModelRolesCommand({ args = [], modelSettings, webResearch } = {}) {
36
+ if (!modelSettings?.roles) throw new Error("Model roles aren't available when Moondog is started this way.");
37
+ const [word, ...rest] = args;
38
+ const roleId = MODEL_ROLE_WORDS[String(word ?? "").toLowerCase()];
39
+ const show = async () => formatModelRoles(await modelSettings.roles({ web: await webResearch?.services?.().catch(() => null) ?? null }));
40
+ if (roleId === null) {
41
+ if (rest.length) throw new Error(usage);
42
+ return show();
43
+ }
44
+ if (roleId === undefined) throw new Error(usage);
45
+ const role = MODEL_ROLES.find((entry) => entry.id === roleId);
46
+ if (role.state !== "enabled") return `${titles[roleId]} isn't available yet. Its place is kept, so a later Moondog can add it.`;
47
+ if (!rest.length) return show();
48
+ let choice;
49
+ if (rest.length === 1 && rest[0].toLowerCase() === "auto") choice = null;
50
+ else if (role.kind === "service") {
51
+ if (rest.length !== 1 || !WEB_SERVICES.includes(rest[0].toLowerCase())) throw new Error(usage);
52
+ choice = { service: rest[0].toLowerCase() };
53
+ } else {
54
+ if (rest.length !== 2) throw new Error(usage);
55
+ choice = { provider: rest[0], model: rest[1] };
56
+ }
57
+ await modelSettings.setRole(roleId, choice);
58
+ if (roleId === "web_search") await webResearch?.refresh?.();
59
+ return show();
60
+ }
@@ -24,6 +24,8 @@ import { getPublicPiModelCatalog } from "../../runtime/pi/public-model-catalog.m
24
24
  import { serviceKeyProviders, supportedAuthProviderIds } from "../../runtime/pi/authentication.mjs";
25
25
  import { EFFORT_LEVELS, EFFORT_SCALE } from "../../runtime/pi/runtime-settings.mjs";
26
26
  import { runtimeBehindSelection } from "../../runtime/pi/runtime-selection.mjs";
27
+ import { MODEL_ROLE_WORDS } from "./model-roles-command.mjs";
28
+ import { imagesFromText } from "../../runtime/pi/image-input.mjs";
27
29
  import {
28
30
  formatLocalResult,
29
31
  helpText,
@@ -129,8 +131,8 @@ const slashCommands = [
129
131
  { name: "generation", description: N_("Let Moondog make music on this Mac") },
130
132
  { name: "onair", description: N_("Go on air: Moondog keeps making music from your taste") },
131
133
  { name: "spotify", description: N_("Connect and control Spotify") },
132
- { name: "skip", description: N_("Skip to the next song preview") },
133
- { name: "stop", description: N_("Stop the song previews") },
134
+ { name: "skip", description: N_("Skip to the next song preview or stream track") },
135
+ { name: "stop", description: N_("Stop the song previews or the stream") },
134
136
  { name: "reload", description: N_("Reload model settings and sign-ins") },
135
137
  { name: "remember", description: N_("Ask Moondog to remember something") },
136
138
  { name: "forget", description: N_("Forget one memory by ID") },
@@ -209,6 +211,7 @@ export async function runMoondogTui({
209
211
  runProfile,
210
212
  runProfileAction,
211
213
  runWeb,
214
+ runModelRoles,
212
215
  checkUpdates,
213
216
  runUpdate,
214
217
  lyrics,
@@ -1440,7 +1443,7 @@ export async function runMoondogTui({
1440
1443
  // The window's endpoints reach /model here; an agent on an endpoint that changed is rebuilt, since its
1441
1444
  // address or key may have. The window's menu list and its per-conversation choices don't move this one.
1442
1445
  const stopWatchingModel = watchModelSettings?.((selection, changed) => {
1443
- if (changed === "models") return;
1446
+ if (changed === "models" || changed === "roles") return;
1444
1447
  if (changed === "endpoints") void refreshEndpoints?.();
1445
1448
  const onEndpoint = changed === "endpoints" && String(runtimeStatus.provider ?? "").startsWith("endpoint-");
1446
1449
  if (!onEndpoint && !runtimeBehindSelection(runtimeStatus, selection)) return;
@@ -2550,6 +2553,13 @@ export async function runMoondogTui({
2550
2553
  return;
2551
2554
  }
2552
2555
  if (command === "model") {
2556
+ // /model roles, vision, web and audio set what other models are for; the rest pick the chat model.
2557
+ if (args[0] && Object.hasOwn(MODEL_ROLE_WORDS, args[0].toLowerCase())) {
2558
+ if (typeof runModelRoles !== "function") throw new Error(tr("Model roles aren't available when Moondog is started this way."));
2559
+ addMoondogMessage(await runModelRoles(args));
2560
+ setFooter(tr("Done."), success);
2561
+ return;
2562
+ }
2553
2563
  await selectModel(args);
2554
2564
  return;
2555
2565
  }
@@ -2827,8 +2837,12 @@ export async function runMoondogTui({
2827
2837
  };
2828
2838
 
2829
2839
  try {
2840
+ // An image path in the message goes with it, as Hermes Agent's terminal does; the words stay.
2841
+ const images = await imagesFromText(value);
2842
+ if (images.length) setFooter(tr("Looking at the attached images... Ctrl+C cancels."));
2830
2843
  const result = await activeRuntime.prompt(value, {
2831
2844
  ...(profileSeed ? { profileSeed } : {}),
2845
+ ...(images.length ? { images } : {}),
2832
2846
  onTextDelta: (delta) => {
2833
2847
  if (cleanedUp) return;
2834
2848
  work.quiet();