Skip to content
Merged

Test #59

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
39e3f4c
MCP - get_spotify_search
sweetmantech Dec 13, 2025
67cbcce
Merge pull request #47 from Recoupable-com/sweetmantech/myc-3731-mcp-…
sweetmantech Dec 13, 2025
4948213
Merge branch 'main' of github.com:Recoupable-com/Recoup-API into test
sweetmantech Dec 13, 2025
8171aad
MCP - get_spotify_artist_top_tracks
sweetmantech Dec 13, 2025
3e0c70f
Merge pull request #48 from Recoupable-com/sweetmantech/myc-3732-mcp-…
sweetmantech Dec 13, 2025
cc9bc91
MCP - get_spotify_artist_albums
sweetmantech Dec 13, 2025
309eb4d
Merge pull request #49 from Recoupable-com/sweetmantech/myc-3733-mcp-…
sweetmantech Dec 13, 2025
8410bef
MCP - get_spotify_album
sweetmantech Dec 13, 2025
f2337fe
refactor helper libs to match type name.
sweetmantech Dec 13, 2025
1ad1b2f
DRY - getCallToolResult
sweetmantech Dec 13, 2025
4cc3100
Merge pull request #50 from Recoupable-com/sweetmantech/myc-3734-mcp-…
sweetmantech Dec 13, 2025
148b628
MCP - spotify_deep_research
sweetmantech Dec 13, 2025
2ed5698
Merge pull request #51 from Recoupable-com/sweetmantech/myc-3735-mcp-…
sweetmantech Dec 13, 2025
c333d4b
MCP - update_account_info
sweetmantech Dec 13, 2025
0dba6de
Merge pull request #52 from Recoupable-com/sweetmantech/myc-3736-mcp-…
sweetmantech Dec 13, 2025
3bff7f3
MCP - update_artist_socials
sweetmantech Dec 13, 2025
b71b130
rollback lib/socials/getUsernameFromProfileUrl.ts
sweetmantech Dec 13, 2025
01a1962
Merge pull request #53 from Recoupable-com/sweetmantech/myc-3737-mcp-…
sweetmantech Dec 13, 2025
ed808bf
MCP - search_web
sweetmantech Dec 13, 2025
249a5d6
Merge pull request #54 from Recoupable-com/sweetmantech/myc-3738-mcp-…
sweetmantech Dec 13, 2025
40a0d26
MCP - create_knowledge_base
sweetmantech Dec 13, 2025
59182b3
Merge pull request #55 from Recoupable-com/sweetmantech/myc-3739-mcp-…
sweetmantech Dec 13, 2025
346e5b8
MCP - generate_txt_file
sweetmantech Dec 13, 2025
9eda450
refactor the generate function location
sweetmantech Dec 13, 2025
94cd34d
Merge pull request #56 from Recoupable-com/sweetmantech/myc-3740-mcp-…
sweetmantech Dec 13, 2025
4bef0ae
MCP - create_segments
sweetmantech Dec 13, 2025
96f1ed1
Merge pull request #57 from Recoupable-com/sweetmantech/myc-3741-mcp-…
sweetmantech Dec 13, 2025
5a6a4a0
MCP - youtube_login
sweetmantech Dec 13, 2025
a967d38
Merge pull request #58 from Recoupable-com/sweetmantech/myc-3742-mcp-…
sweetmantech Dec 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions lib/ai/generateArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { generateObject } from "ai";
import { DEFAULT_MODEL } from "@/lib/const";
import { z } from "zod";

export interface GenerateArrayResult {
segmentName: string;
fans: string[];
}

const generateArray = async ({
system,
prompt,
}: {
system?: string;
prompt: string;
}): Promise<GenerateArrayResult[]> => {
const result = await generateObject({
model: DEFAULT_MODEL,
system,
prompt,
output: "array",
schema: z.object({
segmentName: z.string().describe("Segment name."),
fans: z
.array(z.string())
.describe(
"For each Segment Name return an array of fan_social_id included in the segment. Do not make these up. Only use the actual fan_social_id provided in the fan data prompt input.",
),
}),
});

return result.object;
};

export default generateArray;
68 changes: 68 additions & 0 deletions lib/artist/createKnowledgeBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { uploadTextToArweave } from "@/lib/arweave/uploadTextToArweave";
import { arweaveGatewayUrl } from "@/lib/arweave/arweaveGatewayUrl";
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
import { updateAccountInfo } from "@/lib/supabase/account_info/updateAccountInfo";
import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
import type { Knowledge } from "./updateArtistProfile";

/**
* Creates a knowledge base entry for an artist by uploading text to Arweave
* and appending it to the artist's knowledges array.
*
* @param artistId - The artist account ID
* @param knowledgeBaseText - The text content to add to the knowledge base
* @returns The created knowledge entry with Arweave URL
*/
export async function createKnowledgeBase(
artistId: string,
knowledgeBaseText: string,
): Promise<Knowledge> {
if (!knowledgeBaseText || knowledgeBaseText.trim().length === 0) {
throw new Error("Knowledge base text is required");
}

// Upload text to Arweave
const arweaveUrl = await uploadTextToArweave(knowledgeBaseText);
const fetchableUrl = arweaveGatewayUrl(arweaveUrl);

// Generate a name from the first line or a default name
const firstLine = knowledgeBaseText.split("\n")[0].trim();
const name = firstLine.length > 0 && firstLine.length <= 100 ? firstLine : "Knowledge Base Entry";

// Create the knowledge entry
const newKnowledge: Knowledge = {
url: fetchableUrl || arweaveUrl,
name,
type: "text/plain",
};

// Get current account_info
const accountInfo = await selectAccountInfo(artistId);

// Get existing knowledges or initialize empty array
const existingKnowledges: Knowledge[] = accountInfo?.knowledges
? (accountInfo.knowledges as unknown as Knowledge[])
: [];

// Append new knowledge (deduplicate by URL)
const knowledgeMap = new Map<string, Knowledge>();
existingKnowledges.forEach(k => {
knowledgeMap.set(k.url, k);
});
knowledgeMap.set(newKnowledge.url, newKnowledge);
const updatedKnowledges = Array.from(knowledgeMap.values());

// Update account_info
if (accountInfo) {
await updateAccountInfo(artistId, {
knowledges: updatedKnowledges as unknown as typeof accountInfo.knowledges,
});
} else {
await insertAccountInfo({
account_id: artistId,
knowledges: updatedKnowledges as unknown as typeof accountInfo.knowledges,
});
}

return newKnowledge;
}
4 changes: 2 additions & 2 deletions lib/artist/getArtistSegments.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { selectArtistSegmentsCount } from "@/lib/supabase/artist_segments/selectArtistSegmentsCount";
import { selectArtistSegments } from "@/lib/supabase/artist_segments/selectArtistSegments";
import { selectArtistSegmentsWithDetails } from "@/lib/supabase/artist_segments/selectArtistSegmentsWithDetails";
import type { ArtistSegmentsQuery } from "@/lib/artist/validateArtistSegmentsQuery";
import { mapArtistSegments, type MappedArtistSegment } from "@/lib/artist/mapArtistSegments";

Expand Down Expand Up @@ -38,7 +38,7 @@ export const getArtistSegments = async ({
};
}

const data = await selectArtistSegments(artist_account_id, offset, limit);
const data = await selectArtistSegmentsWithDetails(artist_account_id, offset, limit);

if (!data) {
return {
Expand Down
2 changes: 1 addition & 1 deletion lib/artist/mapArtistSegments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SegmentQueryResult } from "@/lib/supabase/artist_segments/selectArtistSegments";
import type { SegmentQueryResult } from "@/lib/supabase/artist_segments/selectArtistSegmentsWithDetails";

export interface MappedArtistSegment {
id: string;
Expand Down
98 changes: 98 additions & 0 deletions lib/artist/updateArtistProfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { selectAccounts } from "@/lib/supabase/accounts/selectAccounts";
import { updateAccount } from "@/lib/supabase/accounts/updateAccount";
import { selectAccountInfo } from "@/lib/supabase/account_info/selectAccountInfo";
import { updateAccountInfo } from "@/lib/supabase/account_info/updateAccountInfo";
import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
import type { Tables } from "@/types/database.types";

export type Knowledge = {
url: string;
name: string;
type: string;
};

export type ArtistProfile = Tables<"accounts"> & Tables<"account_info">;

/**
* Updates an artist profile (account and account_info).
* All fields are optional except artistId.
*
* @param artistId - The artist account ID
* @param image - Optional profile image URL
* @param name - Optional display name
* @param instruction - Optional custom instructions
* @param label - Optional label/role
* @param knowledges - Optional array of knowledge objects
* @returns The updated artist profile
*/
export async function updateArtistProfile(
artistId: string,
image: string,
name: string,
instruction: string,
label: string,
knowledges: Knowledge[] | null,
): Promise<ArtistProfile> {
// Fetch current account
const currentAccounts = await selectAccounts(artistId);
const currentAccount = currentAccounts[0];
if (!currentAccount) {
throw new Error("Artist does not exist");
}

// Update account name if provided
if (name) {
const accountUpdate = { name };
const updatedAccount = await updateAccount(artistId, accountUpdate);
if (!updatedAccount) {
throw new Error("Failed to update account");
}
}

// Get or create account_info
const accountInfo = await selectAccountInfo(artistId);

if (accountInfo) {
// Update existing account_info
const infoUpdate: Partial<typeof accountInfo> = {};
if (image) infoUpdate.image = image;
if (instruction) infoUpdate.instruction = instruction;

if (knowledges !== null && knowledges !== undefined) {
// Deduplicate knowledges by URL
const uniqueMap = new Map(knowledges.map(k => [k.url, k]));
infoUpdate.knowledges = Array.from(uniqueMap.values());
}

if (label !== undefined) {
infoUpdate.label = label === "" ? null : label;
}

await updateAccountInfo(artistId, infoUpdate);
} else {
// Create new account_info
await insertAccountInfo({
image: image || null,
instruction: instruction || null,
knowledges: knowledges || null,
label: label === "" ? null : label || null,
account_id: artistId,
});
}

// Fetch and return the latest account and account_info
const [updatedAccounts, updatedAccountInfo] = await Promise.all([
selectAccounts(artistId),
selectAccountInfo(artistId),
]);

const account = updatedAccounts[0];
if (!account || !updatedAccountInfo) {
throw new Error("Failed to fetch updated artist profile");
}

return {
...updatedAccountInfo,
...account,
};
}
68 changes: 68 additions & 0 deletions lib/artist/updateArtistSocials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { getSocialPlatformByLink } from "@/lib/artists/getSocialPlatformByLink";
import { getUsernameFromProfileUrl } from "@/lib/socials/getUsernameFromProfileUrl";
import { selectAccountSocials } from "@/lib/supabase/account_socials/selectAccountSocials";
import { deleteAccountSocial } from "@/lib/supabase/account_socials/deleteAccountSocial";
import { insertAccountSocial } from "@/lib/supabase/account_socials/insertAccountSocial";
import { selectSocials } from "@/lib/supabase/socials/selectSocials";
import { insertSocials } from "@/lib/supabase/socials/insertSocials";
import type { AccountSocialWithSocial } from "@/lib/supabase/account_socials/selectAccountSocials";

/**
* Updates artist socials by replacing existing socials for each platform type.
*
* @param artistId - The artist account ID
* @param profileUrls - Record mapping platform types to profile URLs
* @returns Array of updated account socials with joined social data
*/
export async function updateArtistSocials(
artistId: string,
profileUrls: Record<string, string>,
): Promise<AccountSocialWithSocial[]> {
// Get current account socials (with no limit to get all)
const accountSocials = await selectAccountSocials(artistId, 0, 10000);

// Process each platform type
const profilePromises = Object.entries(profileUrls).map(async ([type, value]) => {
const socials = value ? await selectSocials({ profile_url: value }) : null;
const social = socials && socials.length > 0 ? socials[0] : null;
const existingSocial = (accountSocials || []).find(
(account_social: AccountSocialWithSocial) =>
getSocialPlatformByLink(account_social.social?.profile_url || "") === type,
);

// Delete existing social for this platform type if it exists
if (existingSocial && existingSocial.social?.id) {
await deleteAccountSocial(artistId, existingSocial.social.id);
}

// Insert new social if URL provided
if (value) {
if (social) {
// Social already exists, check if account_social relationship exists
const existing = (accountSocials || []).find(
(as: AccountSocialWithSocial) => as.social_id === social.id,
);
if (!existing) {
await insertAccountSocial(artistId, social.id);
}
} else {
// Create new social record
const newSocials = await insertSocials([
{
username: getUsernameFromProfileUrl(value),
profile_url: value,
},
]);
if (newSocials.length > 0) {
await insertAccountSocial(artistId, newSocials[0].id);
}
}
}
});

await Promise.all(profilePromises);

// Return the updated account socials
const updated = await selectAccountSocials(artistId, 0, 10000);
return updated || [];
}
13 changes: 13 additions & 0 deletions lib/arweave/uploadTextToArweave.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { uploadToArweave } from "./uploadToArweave";

/**
* Uploads text content to Arweave as a text/plain file.
*
* @param text - The text content to upload
* @returns The Arweave URL (ar://...)
*/
export async function uploadTextToArweave(text: string): Promise<string> {
const textBuffer = Buffer.from(text, "utf-8");
const transaction = await uploadToArweave(textBuffer, "text/plain");
return `ar://${transaction.id}`;
}
68 changes: 68 additions & 0 deletions lib/files/generateAndStoreTxtFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { uploadTextToArweave } from "@/lib/arweave/uploadTextToArweave";
import uploadJsonToArweave from "@/lib/arweave/uploadJsonToArweave";

export interface GeneratedTxtResponse {
txt: {
base64Data: string;
mimeType: string;
};
arweave?: string | null;
metadataArweave?: string | null;
}

/**
* Generates and stores a TXT file by uploading it to Arweave.
* Creates both the text file and metadata JSON on Arweave.
*
* @param contents - The text contents to store
* @returns The generated TXT response with Arweave URLs
*/
export async function generateAndStoreTxtFile(contents: string): Promise<GeneratedTxtResponse> {
if (!contents) {
throw new Error("Contents are required");
}

const mimeType = "text/plain";

// Upload the TXT file to Arweave
let txtFile: string | null = null;
try {
txtFile = await uploadTextToArweave(contents);
} catch (arweaveError) {
console.error("Error uploading TXT to Arweave:", arweaveError);
// Continue even if Arweave upload fails
}

const image = "ar://EXwe2peizXKxjUMop6W-JPflC5sWyeQR1y0JiRDwUB0";

// Upload metadata JSON to Arweave
let metadataArweave: string | null = null;
try {
const transaction = await uploadJsonToArweave({
image,
animation_url: txtFile || undefined,
content: {
mime: mimeType,
uri: txtFile || "",
},
description: contents,
name: contents.substring(0, 100), // Limit name length
});
metadataArweave = `ar://${transaction.id}`;
} catch (metadataError) {
console.error("Error uploading metadata to Arweave:", metadataError);
}

// Encode contents to base64 for response
const base64Data = Buffer.from(contents, "utf-8").toString("base64");

return {
txt: {
base64Data,
mimeType,
},
arweave: txtFile,
metadataArweave,
};
}

Loading