From 3e1fa82bffb28c299f59836df9d630cc34913c08 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:33:06 -0400 Subject: [PATCH 1/6] feat: add recoup research command with 25 subcommands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full CLI for the research API — search, profile, metrics, audience, cities, similar, urls, instagram-posts, playlists, albums, tracks, career, insights, lookup, track, playlist, curator, discover, genres, festivals, web, report, people, extract, enrich. All subcommands support --json for agent consumption. Artist-scoped commands accept names or UUIDs. Tested E2E against local API with live Chartmetric data. Made-with: Cursor --- src/bin.ts | 2 + src/commands/research.ts | 467 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 469 insertions(+) create mode 100644 src/commands/research.ts diff --git a/src/bin.ts b/src/bin.ts index 5608963..5938040 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -10,6 +10,7 @@ import { notificationsCommand } from "./commands/notifications.js"; import { orgsCommand } from "./commands/orgs.js"; import { contentCommand } from "./commands/content.js"; import { tasksCommand } from "./commands/tasks.js"; +import { researchCommand } from "./commands/research.js"; const pkgPath = join(__dirname, "..", "package.json"); const { version } = JSON.parse(readFileSync(pkgPath, "utf-8")); @@ -30,5 +31,6 @@ program.addCommand(sandboxesCommand); program.addCommand(orgsCommand); program.addCommand(tasksCommand); program.addCommand(contentCommand); +program.addCommand(researchCommand); program.parse(); diff --git a/src/commands/research.ts b/src/commands/research.ts new file mode 100644 index 0000000..6e0fb6b --- /dev/null +++ b/src/commands/research.ts @@ -0,0 +1,467 @@ +import { Command } from "commander"; +import { get, post } from "../client.js"; +import { printJson, printTable, printError } from "../output.js"; + +const searchCommand = new Command("research") + .description("Music industry research — streaming metrics, audience, playlists, competitive analysis, web intelligence") + .argument("[query]", "Artist name to search for") + .option("--json", "Output as JSON") + .option("--limit ", "Max results", "10") + .option("--type ", "Entity type: artists, tracks, albums", "artists") + .action(async (query, opts) => { + if (!query) { + console.log("Usage: recoup research "); + console.log(" recoup research [options]"); + console.log("\nSubcommands: profile, metrics, audience, cities, similar, urls,"); + console.log(" instagram-posts, playlists, albums, tracks, career, insights,"); + console.log(" lookup, track, playlist, curator, discover, genres, festivals,"); + console.log(" web, deep, people, extract, enrich"); + return; + } + try { + const data = await get("/api/research", { q: query, type: opts.type, limit: opts.limit }); + const results = (data.results as Record[]) || []; + if (opts.json) return printJson(results); + printTable(results, [ + { key: "name", label: "NAME" }, + { key: "id", label: "ID" }, + { key: "sp_monthly_listeners", label: "LISTENERS" }, + { key: "sp_followers", label: "FOLLOWERS" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const profileCommand = new Command("profile") + .description("Full artist profile — bio, genres, social URLs, label") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/profile", { artist }); + if (opts.json) return printJson(data); + console.log(`${data.name} (${(data as Record).country_code || "unknown"})`); + const genres = data.genres as Record | undefined; + if (genres?.primary) console.log(`Genre: ${(genres.primary as Record).name}`); + console.log(`CM Score: ${data.cm_artist_score}`); + } catch (err) { printError((err as Error).message); } + }); + +const metricsCommand = new Command("metrics") + .description("Platform metrics over time (14 platforms)") + .argument("", "Artist name or Recoup ID") + .requiredOption("--source ", "Platform: spotify, instagram, tiktok, youtube_channel, etc.") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/metrics", { artist, source: opts.source }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const audienceCommand = new Command("audience") + .description("Audience demographics — age, gender, country") + .argument("", "Artist name or Recoup ID") + .option("--platform ", "instagram (default), tiktok, youtube", "instagram") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/audience", { artist, platform: opts.platform }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const citiesCommand = new Command("cities") + .description("Top cities by listener concentration") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/cities", { artist }); + const cities = (data.cities as Record[]) || []; + if (opts.json) return printJson(cities); + printTable(cities, [ + { key: "name", label: "CITY" }, + { key: "country", label: "COUNTRY" }, + { key: "listeners", label: "LISTENERS" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const similarCommand = new Command("similar") + .description("Find similar artists for competitive analysis") + .argument("", "Artist name or Recoup ID") + .option("--audience ", "high, medium, low") + .option("--genre ", "high, medium, low") + .option("--mood ", "high, medium, low") + .option("--musicality ", "high, medium, low") + .option("--limit ", "Max results", "10") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const params: Record = { artist, limit: opts.limit }; + if (opts.audience) params.audience = opts.audience; + if (opts.genre) params.genre = opts.genre; + if (opts.mood) params.mood = opts.mood; + if (opts.musicality) params.musicality = opts.musicality; + const data = await get("/api/research/similar", params); + const artists = (data.artists as Record[]) || []; + if (opts.json) return printJson(artists); + printTable(artists, [ + { key: "name", label: "NAME" }, + { key: "career_stage", label: "STAGE" }, + { key: "recent_momentum", label: "MOMENTUM" }, + { key: "sp_monthly_listeners", label: "LISTENERS" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const urlsCommand = new Command("urls") + .description("All social and streaming links") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/urls", { artist }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const instagramPostsCommand = new Command("instagram-posts") + .description("Top Instagram posts and reels by engagement") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/instagram-posts", { artist }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const playlistsCommand = new Command("playlists") + .description("Playlist placements across platforms") + .argument("", "Artist name or Recoup ID") + .option("--platform

", "spotify, applemusic, deezer, amazon, youtube", "spotify") + .option("--status ", "current or past", "current") + .option("--editorial", "Only editorial playlists") + .option("--since ", "Filter by date (YYYY-MM-DD)") + .option("--sort ", "Sort field (e.g., followers)") + .option("--limit ", "Max results", "20") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const params: Record = { artist, platform: opts.platform, status: opts.status, limit: opts.limit }; + if (opts.editorial) params.editorial = "true"; + if (opts.since) params.since = opts.since; + if (opts.sort) params.sort = opts.sort; + const data = await get("/api/research/playlists", params); + const placements = (data.placements as Record[]) || []; + if (opts.json) return printJson(placements); + printJson(placements); + } catch (err) { printError((err as Error).message); } + }); + +const albumsCommand = new Command("albums") + .description("Full discography") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/albums", { artist }); + const albums = (data.albums as Record[]) || []; + if (opts.json) return printJson(albums); + printTable(albums, [ + { key: "name", label: "ALBUM" }, + { key: "release_date", label: "RELEASED" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const tracksCommand = new Command("tracks") + .description("All tracks with popularity") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/tracks", { artist }); + const tracks = (data.tracks as Record[]) || []; + if (opts.json) return printJson(tracks); + printTable(tracks, [ + { key: "name", label: "TRACK" }, + { key: "id", label: "ID" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const careerCommand = new Command("career") + .description("Career timeline and milestones") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/career", { artist }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const insightsCommand = new Command("insights") + .description("AI-generated observations and trends") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .action(async (artist, opts) => { + try { + const data = await get("/api/research/insights", { artist }); + const insights = (data.insights as Record[]) || []; + if (opts.json) return printJson(insights); + for (const i of insights) { + const text = (i as Record).insight || (i as Record).text || JSON.stringify(i); + console.log(` • ${String(text).substring(0, 120)}`); + } + } catch (err) { printError((err as Error).message); } + }); + +const lookupCommand = new Command("lookup") + .description("Find artist by Spotify URL or platform ID") + .argument("", "Spotify URL or platform ID") + .option("--json", "Output as JSON") + .action(async (url, opts) => { + try { + const data = await get("/api/research/lookup", { url }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const trackCommand = new Command("track") + .description("Track metadata by name or Spotify URL") + .argument("", "Track name or Spotify URL") + .option("--json", "Output as JSON") + .action(async (query, opts) => { + try { + const data = await get("/api/research/track", { q: query }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const playlistCommand = new Command("playlist") + .description("Playlist metadata") + .argument("", "spotify, applemusic, deezer, amazon, youtube") + .argument("", "Playlist ID") + .option("--json", "Output as JSON") + .action(async (platform, id, opts) => { + try { + const data = await get("/api/research/playlist", { platform, id }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const curatorCommand = new Command("curator") + .description("Curator profile") + .argument("", "spotify, applemusic, deezer, amazon, youtube") + .argument("", "Curator ID") + .option("--json", "Output as JSON") + .action(async (platform, id, opts) => { + try { + const data = await get("/api/research/curator", { platform, id }); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const discoverCommand = new Command("discover") + .description("Discover artists by criteria") + .option("--country ", "ISO country code (US, BR, GB)") + .option("--genre ", "Genre ID (use 'recoup research genres' to list)") + .option("--spotify-listeners ", "min,max monthly listeners (e.g., 100000,500000)") + .option("--tiktok-followers ", "min,max followers") + .option("--sort ", "Sort field") + .option("--limit ", "Max results", "20") + .option("--json", "Output as JSON") + .action(async (opts) => { + try { + const params: Record = { limit: opts.limit }; + if (opts.country) params.country = opts.country; + if (opts.genre) params.genre = opts.genre; + if (opts.sort) params.sort = opts.sort; + if (opts.spotifyListeners) { + const [min, max] = opts.spotifyListeners.split(","); + if (min) params.sp_monthly_listeners_min = min; + if (max) params.sp_monthly_listeners_max = max; + } + const data = await get("/api/research/discover", params); + const artists = (data.artists as Record[]) || []; + if (opts.json) return printJson(artists); + printTable(artists, [ + { key: "name", label: "NAME" }, + { key: "sp_monthly_listeners", label: "LISTENERS" }, + { key: "country", label: "COUNTRY" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const genresCommand = new Command("genres") + .description("List all genre IDs and names") + .option("--json", "Output as JSON") + .action(async (opts) => { + try { + const data = await get("/api/research/genres"); + const genres = (data.genres as Record[]) || []; + if (opts.json) return printJson(genres); + printTable(genres, [ + { key: "name", label: "GENRE" }, + { key: "id", label: "ID" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const festivalsCommand = new Command("festivals") + .description("List music festivals") + .option("--json", "Output as JSON") + .action(async (opts) => { + try { + const data = await get("/api/research/festivals"); + const festivals = (data.festivals as Record[]) || []; + if (opts.json) return printJson(festivals); + printTable(festivals, [ + { key: "name", label: "FESTIVAL" }, + { key: "city", label: "CITY" }, + { key: "country", label: "COUNTRY" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const webCommand = new Command("web") + .description("Web search for real-time information") + .argument("", "Search query") + .option("--max-results ", "Max results", "10") + .option("--country ", "ISO country code") + .option("--json", "Output as JSON") + .action(async (query, opts) => { + try { + const body: Record = { query, max_results: parseInt(opts.maxResults) }; + if (opts.country) body.country = opts.country; + const data = await post("/api/research/web", body); + const results = (data.results as Record[]) || []; + if (opts.json) return printJson(results); + for (const r of results) { + console.log(` ${r.title}`); + console.log(` ${r.url}`); + console.log(` ${String(r.snippet || "").substring(0, 120)}`); + console.log(); + } + } catch (err) { printError((err as Error).message); } + }); + +const deepCommand = new Command("report") + .description("Deep research report with citations") + .argument("", "Research query") + .option("--json", "Output as JSON") + .action(async (query, opts) => { + try { + const data = await post("/api/research/deep", { query }); + if (opts.json) return printJson(data); + console.log(data.content); + const citations = (data.citations as string[]) || []; + if (citations.length > 0) { + console.log("\nCitations:"); + citations.forEach((c, i) => console.log(` [${i + 1}] ${c}`)); + } + } catch (err) { printError((err as Error).message); } + }); + +const peopleCommand = new Command("people") + .description("Search for people in the music industry") + .argument("", "Search query (e.g., 'A&R reps at Atlantic Records')") + .option("--num-results ", "Max results", "10") + .option("--json", "Output as JSON") + .action(async (query, opts) => { + try { + const data = await post("/api/research/people", { query, num_results: parseInt(opts.numResults) }); + const results = (data.results as Record[]) || []; + if (opts.json) return printJson(results); + for (const r of results) { + console.log(` ${r.title}`); + console.log(` ${r.url}`); + if (r.summary) console.log(` ${String(r.summary).substring(0, 120)}`); + console.log(); + } + } catch (err) { printError((err as Error).message); } + }); + +const extractCommand = new Command("extract") + .description("Extract clean markdown from URLs") + .argument("", "URLs to extract (max 10)") + .option("--objective ", "What information to focus on") + .option("--full-content", "Return full page instead of excerpts") + .option("--json", "Output as JSON") + .action(async (urls, opts) => { + try { + const body: Record = { urls }; + if (opts.objective) body.objective = opts.objective; + if (opts.fullContent) body.full_content = true; + const data = await post("/api/research/extract", body); + const results = (data.results as Record[]) || []; + if (opts.json) return printJson(results); + for (const r of results) { + console.log(`--- ${r.title || r.url} ---`); + const excerpts = (r.excerpts as string[]) || []; + const content = r.full_content as string | undefined; + if (content) { + console.log(content); + } else { + for (const e of excerpts) console.log(e); + } + console.log(); + } + } catch (err) { printError((err as Error).message); } + }); + +const enrichCommand = new Command("enrich") + .description("Structured data enrichment from web research") + .argument("", "What to research (e.g., 'Kaash Paige R&B artist')") + .requiredOption("--schema ", "JSON schema for output fields") + .option("--processor ", "base (fast), core (balanced), ultra (deep)", "base") + .option("--json", "Output as JSON") + .action(async (input, opts) => { + try { + let schema: Record; + try { schema = JSON.parse(opts.schema); } catch { printError("--schema must be valid JSON"); return; } + const data = await post("/api/research/enrich", { input, schema, processor: opts.processor }); + if (opts.json) return printJson(data); + printJson(data.output); + } catch (err) { printError((err as Error).message); } + }); + +// Register all subcommands +searchCommand.addCommand(profileCommand); +searchCommand.addCommand(metricsCommand); +searchCommand.addCommand(audienceCommand); +searchCommand.addCommand(citiesCommand); +searchCommand.addCommand(similarCommand); +searchCommand.addCommand(urlsCommand); +searchCommand.addCommand(instagramPostsCommand); +searchCommand.addCommand(playlistsCommand); +searchCommand.addCommand(albumsCommand); +searchCommand.addCommand(tracksCommand); +searchCommand.addCommand(careerCommand); +searchCommand.addCommand(insightsCommand); +searchCommand.addCommand(lookupCommand); +searchCommand.addCommand(trackCommand); +searchCommand.addCommand(playlistCommand); +searchCommand.addCommand(curatorCommand); +searchCommand.addCommand(discoverCommand); +searchCommand.addCommand(genresCommand); +searchCommand.addCommand(festivalsCommand); +searchCommand.addCommand(webCommand); +searchCommand.addCommand(deepCommand); +searchCommand.addCommand(peopleCommand); +searchCommand.addCommand(extractCommand); +searchCommand.addCommand(enrichCommand); + +export const researchCommand = searchCommand; From 5fe3a2338823de5cfee57978597c2b830bfc565f Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:40:50 -0400 Subject: [PATCH 2/6] fix: add examples to --help per CLI for Agents best practices Added Examples sections to --help for research (root), metrics, similar, playlists, discover, and enrich subcommands. Root command now shows help instead of manual usage text when called with no arguments. Follows CLI for Agents pattern: every --help includes real copy-pasteable invocations so agents can pattern-match. Made-with: Cursor --- src/commands/research.ts | 49 +++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/src/commands/research.ts b/src/commands/research.ts index 6e0fb6b..1e80d38 100644 --- a/src/commands/research.ts +++ b/src/commands/research.ts @@ -2,20 +2,29 @@ import { Command } from "commander"; import { get, post } from "../client.js"; import { printJson, printTable, printError } from "../output.js"; +const EXAMPLES = ` +Examples: + recoup research "Drake" + recoup research "Drake" --json + recoup research cities "Drake" + recoup research metrics "Drake" --source spotify + recoup research similar "Drake" --audience high --genre high + recoup research web "Drake brand partnerships" + recoup research report "Tell me about Drake" + recoup research people "A&R reps Atlantic Records" + recoup research extract "https://en.wikipedia.org/wiki/Drake_(musician)" + recoup research enrich "Drake" --schema '{"properties":{"label":{"type":"string"}}}'`; + const searchCommand = new Command("research") .description("Music industry research — streaming metrics, audience, playlists, competitive analysis, web intelligence") .argument("[query]", "Artist name to search for") .option("--json", "Output as JSON") .option("--limit ", "Max results", "10") .option("--type ", "Entity type: artists, tracks, albums", "artists") + .addHelpText("after", EXAMPLES) .action(async (query, opts) => { if (!query) { - console.log("Usage: recoup research "); - console.log(" recoup research [options]"); - console.log("\nSubcommands: profile, metrics, audience, cities, similar, urls,"); - console.log(" instagram-posts, playlists, albums, tracks, career, insights,"); - console.log(" lookup, track, playlist, curator, discover, genres, festivals,"); - console.log(" web, deep, people, extract, enrich"); + searchCommand.help(); return; } try { @@ -51,6 +60,14 @@ const metricsCommand = new Command("metrics") .argument("", "Artist name or Recoup ID") .requiredOption("--source ", "Platform: spotify, instagram, tiktok, youtube_channel, etc.") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research metrics "Drake" --source spotify + recoup research metrics "Drake" --source tiktok --json + recoup research metrics "Drake" --source youtube_channel + +Valid sources: spotify, instagram, tiktok, twitter, facebook, youtube_channel, + youtube_artist, soundcloud, deezer, twitch, line, melon, wikipedia, bandsintown`) .action(async (artist, opts) => { try { const data = await get("/api/research/metrics", { artist, source: opts.source }); @@ -98,6 +115,11 @@ const similarCommand = new Command("similar") .option("--musicality ", "high, medium, low") .option("--limit ", "Max results", "10") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research similar "Drake" + recoup research similar "Drake" --audience high --genre high --limit 20 + recoup research similar "Drake" --musicality high --json`) .action(async (artist, opts) => { try { const params: Record = { artist, limit: opts.limit }; @@ -151,6 +173,12 @@ const playlistsCommand = new Command("playlists") .option("--sort ", "Sort field (e.g., followers)") .option("--limit ", "Max results", "20") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research playlists "Drake" + recoup research playlists "Drake" --editorial --sort followers + recoup research playlists "Drake" --status past --since 2025-01-01 + recoup research playlists "Drake" --platform applemusic --json`) .action(async (artist, opts) => { try { const params: Record = { artist, platform: opts.platform, status: opts.status, limit: opts.limit }; @@ -283,6 +311,11 @@ const discoverCommand = new Command("discover") .option("--sort ", "Sort field") .option("--limit ", "Max results", "20") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research discover --country US --spotify-listeners 100000,500000 + recoup research discover --genre 86 --sort weekly_diff.sp_monthly_listeners + recoup research discover --tiktok-followers 1000000,10000000 --spotify-listeners 0,100000`) .action(async (opts) => { try { const params: Record = { limit: opts.limit }; @@ -428,6 +461,10 @@ const enrichCommand = new Command("enrich") .requiredOption("--schema ", "JSON schema for output fields") .option("--processor ", "base (fast), core (balanced), ultra (deep)", "base") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research enrich "Kaash Paige R&B artist" --schema '{"properties":{"real_name":{"type":"string"},"label":{"type":"string"},"hometown":{"type":"string"}}}' + recoup research enrich "Atlantic Records" --schema '{"properties":{"ceo":{"type":"string"},"artists":{"type":"array","items":{"type":"string"}}}}' --processor core`) .action(async (input, opts) => { try { let schema: Record; From 06adf594af1131ca85e4133b8e0c3e4a1da4eae8 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:43:49 -0400 Subject: [PATCH 3/6] fix: add examples to --help for ALL 25 subcommands Every subcommand now has copy-pasteable example invocations in its --help output per CLI for Agents best practices. Made-with: Cursor --- src/commands/research.ts | 80 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/commands/research.ts b/src/commands/research.ts index 1e80d38..373c78e 100644 --- a/src/commands/research.ts +++ b/src/commands/research.ts @@ -44,6 +44,10 @@ const profileCommand = new Command("profile") .description("Full artist profile — bio, genres, social URLs, label") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research profile "Drake" + recoup research profile "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/profile", { artist }); @@ -81,6 +85,11 @@ const audienceCommand = new Command("audience") .argument("", "Artist name or Recoup ID") .option("--platform ", "instagram (default), tiktok, youtube", "instagram") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research audience "Drake" + recoup research audience "Drake" --platform tiktok + recoup research audience "Drake" --platform youtube --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/audience", { artist, platform: opts.platform }); @@ -93,6 +102,10 @@ const citiesCommand = new Command("cities") .description("Top cities by listener concentration") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research cities "Drake" + recoup research cities "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/cities", { artist }); @@ -143,6 +156,10 @@ const urlsCommand = new Command("urls") .description("All social and streaming links") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research urls "Drake" + recoup research urls "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/urls", { artist }); @@ -155,6 +172,10 @@ const instagramPostsCommand = new Command("instagram-posts") .description("Top Instagram posts and reels by engagement") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research instagram-posts "Drake" + recoup research instagram-posts "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/instagram-posts", { artist }); @@ -196,6 +217,10 @@ const albumsCommand = new Command("albums") .description("Full discography") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research albums "Drake" + recoup research albums "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/albums", { artist }); @@ -212,6 +237,10 @@ const tracksCommand = new Command("tracks") .description("All tracks with popularity") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research tracks "Drake" + recoup research tracks "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/tracks", { artist }); @@ -228,6 +257,10 @@ const careerCommand = new Command("career") .description("Career timeline and milestones") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research career "Drake" + recoup research career "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/career", { artist }); @@ -240,6 +273,10 @@ const insightsCommand = new Command("insights") .description("AI-generated observations and trends") .argument("", "Artist name or Recoup ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research insights "Drake" + recoup research insights "Drake" --json`) .action(async (artist, opts) => { try { const data = await get("/api/research/insights", { artist }); @@ -256,6 +293,10 @@ const lookupCommand = new Command("lookup") .description("Find artist by Spotify URL or platform ID") .argument("", "Spotify URL or platform ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research lookup "https://open.spotify.com/artist/3TVXtAsR1Inumwj472S9r4" + recoup research lookup "3TVXtAsR1Inumwj472S9r4" --json`) .action(async (url, opts) => { try { const data = await get("/api/research/lookup", { url }); @@ -268,6 +309,10 @@ const trackCommand = new Command("track") .description("Track metadata by name or Spotify URL") .argument("", "Track name or Spotify URL") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research track "God's Plan" + recoup research track "https://open.spotify.com/track/..." --json`) .action(async (query, opts) => { try { const data = await get("/api/research/track", { q: query }); @@ -281,6 +326,10 @@ const playlistCommand = new Command("playlist") .argument("", "spotify, applemusic, deezer, amazon, youtube") .argument("", "Playlist ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research playlist spotify 1645080 + recoup research playlist spotify 1645080 --json`) .action(async (platform, id, opts) => { try { const data = await get("/api/research/playlist", { platform, id }); @@ -294,6 +343,10 @@ const curatorCommand = new Command("curator") .argument("", "spotify, applemusic, deezer, amazon, youtube") .argument("", "Curator ID") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research curator spotify 1 + recoup research curator spotify 1 --json`) .action(async (platform, id, opts) => { try { const data = await get("/api/research/curator", { platform, id }); @@ -341,6 +394,10 @@ Examples: const genresCommand = new Command("genres") .description("List all genre IDs and names") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research genres + recoup research genres --json`) .action(async (opts) => { try { const data = await get("/api/research/genres"); @@ -356,6 +413,10 @@ const genresCommand = new Command("genres") const festivalsCommand = new Command("festivals") .description("List music festivals") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research festivals + recoup research festivals --json`) .action(async (opts) => { try { const data = await get("/api/research/festivals"); @@ -375,6 +436,11 @@ const webCommand = new Command("web") .option("--max-results ", "Max results", "10") .option("--country ", "ISO country code") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research web "Drake brand partnerships sync licensing" + recoup research web "Kaash Paige fan community" --max-results 5 + recoup research web "indie R&B trends 2026" --json`) .action(async (query, opts) => { try { const body: Record = { query, max_results: parseInt(opts.maxResults) }; @@ -395,6 +461,10 @@ const deepCommand = new Command("report") .description("Deep research report with citations") .argument("", "Research query") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research report "Tell me everything about Kaash Paige" + recoup research report "Competitive landscape for independent R&B artists in 2026" --json`) .action(async (query, opts) => { try { const data = await post("/api/research/deep", { query }); @@ -413,6 +483,11 @@ const peopleCommand = new Command("people") .argument("", "Search query (e.g., 'A&R reps at Atlantic Records')") .option("--num-results ", "Max results", "10") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research people "A&R reps at Atlantic Records" + recoup research people "music managers in Los Angeles R&B" + recoup research people "Kaash Paige manager" --json`) .action(async (query, opts) => { try { const data = await post("/api/research/people", { query, num_results: parseInt(opts.numResults) }); @@ -433,6 +508,11 @@ const extractCommand = new Command("extract") .option("--objective ", "What information to focus on") .option("--full-content", "Return full page instead of excerpts") .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research extract "https://en.wikipedia.org/wiki/Drake_(musician)" + recoup research extract "https://example.com/page" --objective "funding and investors" + recoup research extract "https://a.com" "https://b.com" --full-content --json`) .action(async (urls, opts) => { try { const body: Record = { urls }; From 519558ff62d3bb6aecdc110cf0bd1a91ef643219 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:43:47 -0400 Subject: [PATCH 4/6] feat: add milestones, venues, rank, charts, radio subcommands 5 new CLI commands with examples in --help. Total: 30 subcommands. Made-with: Cursor --- src/commands/research.ts | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/src/commands/research.ts b/src/commands/research.ts index 373c78e..91245a9 100644 --- a/src/commands/research.ts +++ b/src/commands/research.ts @@ -555,6 +555,107 @@ Examples: } catch (err) { printError((err as Error).message); } }); +const milestonesCommand = new Command("milestones") + .description("Artist activity feed — playlist adds, chart entries, notable events") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research milestones "Drake" + recoup research milestones "Drake" --json`) + .action(async (artist, opts) => { + try { + const data = await get("/api/research/milestones", { artist }); + const milestones = (data.milestones as Record[]) || []; + if (opts.json) return printJson(milestones); + printTable(milestones, [ + { key: "date", label: "DATE" }, + { key: "summary", label: "EVENT" }, + { key: "platform", label: "PLATFORM" }, + { key: "track_name", label: "TRACK" }, + { key: "stars", label: "STARS" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const venuesCommand = new Command("venues") + .description("Venues the artist has performed at") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research venues "Drake" + recoup research venues "Drake" --json`) + .action(async (artist, opts) => { + try { + const data = await get("/api/research/venues", { artist }); + const venues = (data.venues as Record[]) || []; + if (opts.json) return printJson(venues); + printTable(venues, [ + { key: "venue_name", label: "VENUE" }, + { key: "venue_capacity", label: "CAPACITY" }, + { key: "city_name", label: "CITY" }, + { key: "country", label: "COUNTRY" }, + ]); + } catch (err) { printError((err as Error).message); } + }); + +const rankCommand = new Command("rank") + .description("Global artist ranking") + .argument("", "Artist name or Recoup ID") + .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research rank "Drake" + recoup research rank "Drake" --json`) + .action(async (artist, opts) => { + try { + const data = await get("/api/research/rank", { artist }); + if (opts.json) return printJson(data); + console.log(`Global rank: ${data.rank ?? "N/A"}`); + } catch (err) { printError((err as Error).message); } + }); + +const chartsCommand = new Command("charts") + .description("Global chart positions by platform") + .requiredOption("--platform ", "Chart platform: spotify, applemusic, tiktok, youtube, itunes, shazam") + .option("--country ", "ISO country code (US, GB, DE)") + .option("--interval ", "Time interval (daily, weekly)") + .option("--type ", "Chart type (varies by platform)") + .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research charts --platform spotify + recoup research charts --platform spotify --country US --json + recoup research charts --platform applemusic --country GB --interval weekly`) + .action(async (opts) => { + try { + const params: Record = { platform: opts.platform }; + if (opts.country) params.country = opts.country; + if (opts.interval) params.interval = opts.interval; + if (opts.type) params.type = opts.type; + const data = await get("/api/research/charts", params); + if (opts.json) return printJson(data); + printJson(data); + } catch (err) { printError((err as Error).message); } + }); + +const radioCommand = new Command("radio") + .description("List radio stations") + .option("--json", "Output as JSON") + .addHelpText("after", ` +Examples: + recoup research radio + recoup research radio --json`) + .action(async (opts) => { + try { + const data = await get("/api/research/radio"); + const stations = (data.stations as Record[]) || []; + if (opts.json) return printJson(stations); + printJson(stations); + } catch (err) { printError((err as Error).message); } + }); + // Register all subcommands searchCommand.addCommand(profileCommand); searchCommand.addCommand(metricsCommand); @@ -580,5 +681,10 @@ searchCommand.addCommand(deepCommand); searchCommand.addCommand(peopleCommand); searchCommand.addCommand(extractCommand); searchCommand.addCommand(enrichCommand); +searchCommand.addCommand(milestonesCommand); +searchCommand.addCommand(venuesCommand); +searchCommand.addCommand(rankCommand); +searchCommand.addCommand(chartsCommand); +searchCommand.addCommand(radioCommand); export const researchCommand = searchCommand; From 56ce3fc8b51daa4046f0b88ac6858a88f65046b8 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:08:33 -0400 Subject: [PATCH 5/6] fix: replace Kaash Paige examples with Drake Made-with: Cursor --- src/commands/research.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/research.ts b/src/commands/research.ts index 91245a9..f5271ee 100644 --- a/src/commands/research.ts +++ b/src/commands/research.ts @@ -439,7 +439,7 @@ const webCommand = new Command("web") .addHelpText("after", ` Examples: recoup research web "Drake brand partnerships sync licensing" - recoup research web "Kaash Paige fan community" --max-results 5 + recoup research web "Drake fan community" --max-results 5 recoup research web "indie R&B trends 2026" --json`) .action(async (query, opts) => { try { @@ -463,7 +463,7 @@ const deepCommand = new Command("report") .option("--json", "Output as JSON") .addHelpText("after", ` Examples: - recoup research report "Tell me everything about Kaash Paige" + recoup research report "Tell me everything about Drake" recoup research report "Competitive landscape for independent R&B artists in 2026" --json`) .action(async (query, opts) => { try { @@ -487,7 +487,7 @@ const peopleCommand = new Command("people") Examples: recoup research people "A&R reps at Atlantic Records" recoup research people "music managers in Los Angeles R&B" - recoup research people "Kaash Paige manager" --json`) + recoup research people "Drake manager" --json`) .action(async (query, opts) => { try { const data = await post("/api/research/people", { query, num_results: parseInt(opts.numResults) }); @@ -537,13 +537,13 @@ Examples: const enrichCommand = new Command("enrich") .description("Structured data enrichment from web research") - .argument("", "What to research (e.g., 'Kaash Paige R&B artist')") + .argument("", "What to research (e.g., 'Drake rapper')") .requiredOption("--schema ", "JSON schema for output fields") .option("--processor ", "base (fast), core (balanced), ultra (deep)", "base") .option("--json", "Output as JSON") .addHelpText("after", ` Examples: - recoup research enrich "Kaash Paige R&B artist" --schema '{"properties":{"real_name":{"type":"string"},"label":{"type":"string"},"hometown":{"type":"string"}}}' + recoup research enrich "Drake rapper" --schema '{"properties":{"real_name":{"type":"string"},"label":{"type":"string"},"hometown":{"type":"string"}}}' recoup research enrich "Atlantic Records" --schema '{"properties":{"ceo":{"type":"string"},"artists":{"type":"array","items":{"type":"string"}}}}' --processor core`) .action(async (input, opts) => { try { From 0945ee8094e46303055e6c5749a3d6391194c51b Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:23:43 -0400 Subject: [PATCH 6/6] fix: validate parseInt inputs for web and people commands Made-with: Cursor --- src/commands/research.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/commands/research.ts b/src/commands/research.ts index f5271ee..627f249 100644 --- a/src/commands/research.ts +++ b/src/commands/research.ts @@ -443,7 +443,12 @@ Examples: recoup research web "indie R&B trends 2026" --json`) .action(async (query, opts) => { try { - const body: Record = { query, max_results: parseInt(opts.maxResults) }; + const maxResults = parseInt(opts.maxResults, 10); + if (Number.isNaN(maxResults) || maxResults < 1) { + printError("--max-results must be a positive number"); + return; + } + const body: Record = { query, max_results: maxResults }; if (opts.country) body.country = opts.country; const data = await post("/api/research/web", body); const results = (data.results as Record[]) || []; @@ -490,7 +495,12 @@ Examples: recoup research people "Drake manager" --json`) .action(async (query, opts) => { try { - const data = await post("/api/research/people", { query, num_results: parseInt(opts.numResults) }); + const numResults = parseInt(opts.numResults, 10); + if (Number.isNaN(numResults) || numResults < 1) { + printError("--num-results must be a positive number"); + return; + } + const data = await post("/api/research/people", { query, num_results: numResults }); const results = (data.results as Record[]) || []; if (opts.json) return printJson(results); for (const r of results) {