@tpmjs/email-subject-score 0.1.0
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/LICENSE +21 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +246 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2025 TPMJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as ai from 'ai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Email Subject Score Tool for TPMJS
|
|
5
|
+
* Scores email subject lines for open rate potential
|
|
6
|
+
*
|
|
7
|
+
* This is a proper AI SDK v6 tool that can be used with streamText()
|
|
8
|
+
* Uses jsonSchema() to avoid Zod 4 JSON Schema conversion issues with OpenAI
|
|
9
|
+
*/
|
|
10
|
+
interface SubjectScore {
|
|
11
|
+
subject: string;
|
|
12
|
+
overallScore: number;
|
|
13
|
+
scores: {
|
|
14
|
+
length: {
|
|
15
|
+
score: number;
|
|
16
|
+
ideal: string;
|
|
17
|
+
current: number;
|
|
18
|
+
};
|
|
19
|
+
clarity: {
|
|
20
|
+
score: number;
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
urgency: {
|
|
24
|
+
score: number;
|
|
25
|
+
reason: string;
|
|
26
|
+
};
|
|
27
|
+
curiosity: {
|
|
28
|
+
score: number;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
personalization: {
|
|
32
|
+
score: number;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
suggestions: string[];
|
|
37
|
+
predictedOpenRate: 'low' | 'medium' | 'high';
|
|
38
|
+
}
|
|
39
|
+
interface SubjectScores {
|
|
40
|
+
scores: SubjectScore[];
|
|
41
|
+
bestSubject: string;
|
|
42
|
+
averageScore: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Input type for Email Subject Score Tool
|
|
46
|
+
*/
|
|
47
|
+
type EmailSubjectScoreInput = {
|
|
48
|
+
subjects: string[];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Email Subject Score Tool
|
|
52
|
+
* Scores email subject lines for open rate potential
|
|
53
|
+
*
|
|
54
|
+
* This is a proper AI SDK v6 tool that can be used with streamText()
|
|
55
|
+
*/
|
|
56
|
+
declare const emailSubjectScoreTool: ai.Tool<EmailSubjectScoreInput, {
|
|
57
|
+
scores: SubjectScore[];
|
|
58
|
+
bestSubject: string;
|
|
59
|
+
averageScore: number;
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
export { type SubjectScore, type SubjectScores, emailSubjectScoreTool as default, emailSubjectScoreTool };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { tool, jsonSchema } from 'ai';
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
function scoreLengthCriterion(subject) {
|
|
5
|
+
const length = subject.length;
|
|
6
|
+
if (length >= 40 && length <= 60) {
|
|
7
|
+
return { score: 1, ideal: "40-60 chars (optimal)", current: length };
|
|
8
|
+
} else if (length >= 30 && length < 40) {
|
|
9
|
+
return { score: 0.8, ideal: "40-60 chars (optimal)", current: length };
|
|
10
|
+
} else if (length > 60 && length <= 70) {
|
|
11
|
+
return { score: 0.7, ideal: "40-60 chars (optimal)", current: length };
|
|
12
|
+
} else if (length < 30) {
|
|
13
|
+
return { score: 0.5, ideal: "40-60 chars (optimal)", current: length };
|
|
14
|
+
} else {
|
|
15
|
+
return { score: 0.4, ideal: "40-60 chars (optimal)", current: length };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function scoreClarityCriterion(subject) {
|
|
19
|
+
let score = 0.7;
|
|
20
|
+
const reasons = [];
|
|
21
|
+
const vagueWords = ["thing", "stuff", "something", "various", "some"];
|
|
22
|
+
const hasVagueWords = vagueWords.some((word) => subject.toLowerCase().includes(word));
|
|
23
|
+
if (hasVagueWords) {
|
|
24
|
+
score -= 0.3;
|
|
25
|
+
reasons.push("Contains vague language");
|
|
26
|
+
} else {
|
|
27
|
+
reasons.push("Uses specific language");
|
|
28
|
+
}
|
|
29
|
+
if (/\d+/.test(subject)) {
|
|
30
|
+
score += 0.2;
|
|
31
|
+
reasons.push("Includes specific numbers");
|
|
32
|
+
}
|
|
33
|
+
if (/[!?]{2,}/.test(subject)) {
|
|
34
|
+
score -= 0.2;
|
|
35
|
+
reasons.push("Excessive punctuation reduces clarity");
|
|
36
|
+
}
|
|
37
|
+
if (subject === subject.toUpperCase() && subject.length > 5) {
|
|
38
|
+
score -= 0.3;
|
|
39
|
+
reasons.push("All caps reduces readability");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
score: Math.max(0, Math.min(1, score)),
|
|
43
|
+
reason: reasons.join("; ")
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function scoreUrgencyCriterion(subject) {
|
|
47
|
+
const urgencyWords = [
|
|
48
|
+
"today",
|
|
49
|
+
"now",
|
|
50
|
+
"urgent",
|
|
51
|
+
"limited",
|
|
52
|
+
"expires",
|
|
53
|
+
"deadline",
|
|
54
|
+
"last chance",
|
|
55
|
+
"ending soon",
|
|
56
|
+
"hurry",
|
|
57
|
+
"final",
|
|
58
|
+
"hours left",
|
|
59
|
+
"ends tonight"
|
|
60
|
+
];
|
|
61
|
+
const lowerSubject = subject.toLowerCase();
|
|
62
|
+
const urgencyCount = urgencyWords.filter((word) => lowerSubject.includes(word)).length;
|
|
63
|
+
if (urgencyCount === 0) {
|
|
64
|
+
return { score: 0.3, reason: "No urgency indicators" };
|
|
65
|
+
} else if (urgencyCount === 1) {
|
|
66
|
+
return { score: 0.8, reason: "Moderate urgency" };
|
|
67
|
+
} else {
|
|
68
|
+
return { score: 0.6, reason: "High urgency (may seem pushy)" };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function scoreCuriosityCriterion(subject) {
|
|
72
|
+
let score = 0.5;
|
|
73
|
+
const reasons = [];
|
|
74
|
+
if (subject.includes("?")) {
|
|
75
|
+
score += 0.3;
|
|
76
|
+
reasons.push("Question creates curiosity");
|
|
77
|
+
}
|
|
78
|
+
const curiosityWords = [
|
|
79
|
+
"secret",
|
|
80
|
+
"reveal",
|
|
81
|
+
"discover",
|
|
82
|
+
"unlock",
|
|
83
|
+
"insider",
|
|
84
|
+
"exclusive",
|
|
85
|
+
"surprising",
|
|
86
|
+
"you won't believe",
|
|
87
|
+
"what",
|
|
88
|
+
"why",
|
|
89
|
+
"how"
|
|
90
|
+
];
|
|
91
|
+
const curiosityCount = curiosityWords.filter(
|
|
92
|
+
(word) => subject.toLowerCase().includes(word)
|
|
93
|
+
).length;
|
|
94
|
+
if (curiosityCount > 0) {
|
|
95
|
+
score += 0.2 * Math.min(curiosityCount, 2);
|
|
96
|
+
reasons.push("Uses curiosity-inducing language");
|
|
97
|
+
}
|
|
98
|
+
const benefitWords = ["free", "save", "bonus", "gift", "win", "earn"];
|
|
99
|
+
const hasBenefit = benefitWords.some((word) => subject.toLowerCase().includes(word));
|
|
100
|
+
if (hasBenefit) {
|
|
101
|
+
score += 0.2;
|
|
102
|
+
reasons.push("Highlights clear benefit");
|
|
103
|
+
}
|
|
104
|
+
if (reasons.length === 0) {
|
|
105
|
+
reasons.push("Could be more intriguing");
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
score: Math.max(0, Math.min(1, score)),
|
|
109
|
+
reason: reasons.join("; ")
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function scorePersonalizationCriterion(subject) {
|
|
113
|
+
let score = 0.4;
|
|
114
|
+
const reasons = [];
|
|
115
|
+
const hasPersonalizationToken = /\{|\[|%/.test(subject);
|
|
116
|
+
if (hasPersonalizationToken) {
|
|
117
|
+
score += 0.4;
|
|
118
|
+
reasons.push("Uses personalization tokens");
|
|
119
|
+
}
|
|
120
|
+
const hasYou = /\b(you|your)\b/i.test(subject);
|
|
121
|
+
if (hasYou) {
|
|
122
|
+
score += 0.3;
|
|
123
|
+
reasons.push("Direct personal address");
|
|
124
|
+
}
|
|
125
|
+
const hasNamePlaceholder = /\{(first_?name|name)\}/i.test(subject);
|
|
126
|
+
if (hasNamePlaceholder) {
|
|
127
|
+
score += 0.3;
|
|
128
|
+
reasons.push("Includes name placeholder");
|
|
129
|
+
}
|
|
130
|
+
if (reasons.length === 0) {
|
|
131
|
+
reasons.push("No personalization detected");
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
score: Math.max(0, Math.min(1, score)),
|
|
135
|
+
reason: reasons.join("; ")
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function generateSuggestions(subject, scores) {
|
|
139
|
+
const suggestions = [];
|
|
140
|
+
if (scores.length.current < 30) {
|
|
141
|
+
suggestions.push("Add more context - subject is too short");
|
|
142
|
+
} else if (scores.length.current > 70) {
|
|
143
|
+
suggestions.push("Shorten subject line - may get truncated on mobile");
|
|
144
|
+
}
|
|
145
|
+
if (scores.clarity.score < 0.6) {
|
|
146
|
+
suggestions.push("Use more specific, concrete language");
|
|
147
|
+
}
|
|
148
|
+
if (scores.urgency.score < 0.5) {
|
|
149
|
+
suggestions.push("Consider adding time-sensitive language if appropriate");
|
|
150
|
+
}
|
|
151
|
+
if (scores.curiosity.score < 0.5) {
|
|
152
|
+
suggestions.push("Add intrigue or highlight a benefit to spark curiosity");
|
|
153
|
+
}
|
|
154
|
+
if (scores.personalization.score < 0.6) {
|
|
155
|
+
suggestions.push('Add personalization tokens like {firstName} or use "you/your"');
|
|
156
|
+
}
|
|
157
|
+
const spamWords = ["free", "click here", "act now", "limited time", "buy now", "!!!", "100%"];
|
|
158
|
+
const hasSpamWords = spamWords.some((word) => subject.toLowerCase().includes(word));
|
|
159
|
+
if (hasSpamWords) {
|
|
160
|
+
suggestions.push("Reduce spam-trigger words to avoid spam filters");
|
|
161
|
+
}
|
|
162
|
+
const hasEmoji = /[\u{1F300}-\u{1F9FF}]/u.test(subject);
|
|
163
|
+
if (!hasEmoji) {
|
|
164
|
+
suggestions.push("Consider adding a relevant emoji for visual appeal (test first)");
|
|
165
|
+
}
|
|
166
|
+
return suggestions;
|
|
167
|
+
}
|
|
168
|
+
function calculateOverallScore(scores) {
|
|
169
|
+
const weights = {
|
|
170
|
+
length: 0.2,
|
|
171
|
+
clarity: 0.25,
|
|
172
|
+
urgency: 0.15,
|
|
173
|
+
curiosity: 0.25,
|
|
174
|
+
personalization: 0.15
|
|
175
|
+
};
|
|
176
|
+
const overall = scores.length.score * weights.length + scores.clarity.score * weights.clarity + scores.urgency.score * weights.urgency + scores.curiosity.score * weights.curiosity + scores.personalization.score * weights.personalization;
|
|
177
|
+
let openRate;
|
|
178
|
+
if (overall >= 0.75) {
|
|
179
|
+
openRate = "high";
|
|
180
|
+
} else if (overall >= 0.55) {
|
|
181
|
+
openRate = "medium";
|
|
182
|
+
} else {
|
|
183
|
+
openRate = "low";
|
|
184
|
+
}
|
|
185
|
+
return { overall, openRate };
|
|
186
|
+
}
|
|
187
|
+
function scoreSubject(subject) {
|
|
188
|
+
const length = scoreLengthCriterion(subject);
|
|
189
|
+
const clarity = scoreClarityCriterion(subject);
|
|
190
|
+
const urgency = scoreUrgencyCriterion(subject);
|
|
191
|
+
const curiosity = scoreCuriosityCriterion(subject);
|
|
192
|
+
const personalization = scorePersonalizationCriterion(subject);
|
|
193
|
+
const scores = {
|
|
194
|
+
length,
|
|
195
|
+
clarity,
|
|
196
|
+
urgency,
|
|
197
|
+
curiosity,
|
|
198
|
+
personalization
|
|
199
|
+
};
|
|
200
|
+
const { overall, openRate } = calculateOverallScore(scores);
|
|
201
|
+
const suggestions = generateSuggestions(subject, scores);
|
|
202
|
+
return {
|
|
203
|
+
subject,
|
|
204
|
+
overallScore: Math.round(overall * 100) / 100,
|
|
205
|
+
scores,
|
|
206
|
+
suggestions,
|
|
207
|
+
predictedOpenRate: openRate
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
var emailSubjectScoreTool = tool({
|
|
211
|
+
description: "Scores email subject lines for open rate potential based on length, clarity, urgency, curiosity, and personalization. Provides detailed feedback and improvement suggestions for each subject line.",
|
|
212
|
+
inputSchema: jsonSchema({
|
|
213
|
+
type: "object",
|
|
214
|
+
properties: {
|
|
215
|
+
subjects: {
|
|
216
|
+
type: "array",
|
|
217
|
+
items: { type: "string" },
|
|
218
|
+
description: "Array of email subject lines to evaluate",
|
|
219
|
+
minItems: 1
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
required: ["subjects"],
|
|
223
|
+
additionalProperties: false
|
|
224
|
+
}),
|
|
225
|
+
async execute({ subjects }) {
|
|
226
|
+
if (!subjects || subjects.length === 0) {
|
|
227
|
+
throw new Error("At least one subject line is required");
|
|
228
|
+
}
|
|
229
|
+
if (subjects.some((s) => !s || s.trim().length === 0)) {
|
|
230
|
+
throw new Error("All subject lines must be non-empty strings");
|
|
231
|
+
}
|
|
232
|
+
const scoredSubjects = subjects.map(scoreSubject);
|
|
233
|
+
const averageScore = scoredSubjects.reduce((sum, s) => sum + s.overallScore, 0) / scoredSubjects.length;
|
|
234
|
+
const bestSubject = scoredSubjects.reduce(
|
|
235
|
+
(best, current) => current.overallScore > best.overallScore ? current : best
|
|
236
|
+
).subject;
|
|
237
|
+
return {
|
|
238
|
+
scores: scoredSubjects,
|
|
239
|
+
bestSubject,
|
|
240
|
+
averageScore: Math.round(averageScore * 100) / 100
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
var index_default = emailSubjectScoreTool;
|
|
245
|
+
|
|
246
|
+
export { index_default as default, emailSubjectScoreTool };
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tpmjs/email-subject-score",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Score email subject lines for open rate potential based on length, urgency, personalization",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"tpmjs",
|
|
8
|
+
"email",
|
|
9
|
+
"marketing",
|
|
10
|
+
"subject-line",
|
|
11
|
+
"ai"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.3.5",
|
|
24
|
+
"typescript": "^5.9.3",
|
|
25
|
+
"@tpmjs/tsconfig": "0.0.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/ajaxdavis/tpmjs.git",
|
|
33
|
+
"directory": "packages/tools/official/email-subject-score"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://tpmjs.com",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"tpmjs": {
|
|
38
|
+
"category": "marketing",
|
|
39
|
+
"frameworks": [
|
|
40
|
+
"vercel-ai"
|
|
41
|
+
],
|
|
42
|
+
"tools": [
|
|
43
|
+
{
|
|
44
|
+
"exportName": "emailSubjectScoreTool",
|
|
45
|
+
"description": "Scores email subject lines for open rate potential based on length, clarity, urgency, curiosity, and personalization. Provides detailed feedback and improvement suggestions.",
|
|
46
|
+
"parameters": [
|
|
47
|
+
{
|
|
48
|
+
"name": "subjects",
|
|
49
|
+
"type": "string[]",
|
|
50
|
+
"description": "Array of email subject lines to evaluate",
|
|
51
|
+
"required": true
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"returns": {
|
|
55
|
+
"type": "SubjectScores",
|
|
56
|
+
"description": "Detailed scores for each subject line with overall score, criterion breakdown, suggestions, and predicted open rate (low/medium/high)"
|
|
57
|
+
},
|
|
58
|
+
"aiAgent": {
|
|
59
|
+
"useCase": "Use this tool when users need to evaluate and compare email subject lines for effectiveness. Helps optimize email marketing campaigns by scoring subjects on multiple criteria.",
|
|
60
|
+
"limitations": "Scores are based on best practices and heuristics, not actual A/B testing data. Results are predictive and should be validated with real campaign data.",
|
|
61
|
+
"examples": [
|
|
62
|
+
"Score these subject lines: 'New Product Launch' vs 'You won't believe what we just released'",
|
|
63
|
+
"Evaluate my email subject for open rate potential",
|
|
64
|
+
"Compare multiple subject lines and recommend the best one"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"ai": "6.0.0-beta.124"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "tsup",
|
|
75
|
+
"dev": "tsup --watch",
|
|
76
|
+
"type-check": "tsc --noEmit",
|
|
77
|
+
"clean": "rm -rf dist .turbo"
|
|
78
|
+
}
|
|
79
|
+
}
|