@coppica/persuasion-mcp 1.0.1
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/README.md +108 -0
- package/data/LICENSE.md +8 -0
- package/data/catalog.json +1 -0
- package/data/categories.json +1 -0
- package/data/trait-evidence.json +187 -0
- package/dist/data.d.ts +81 -0
- package/dist/data.js +207 -0
- package/dist/format.d.ts +28 -0
- package/dist/format.js +50 -0
- package/dist/hookcheck/index.d.ts +98 -0
- package/dist/hookcheck/index.js +294 -0
- package/dist/http.d.ts +12 -0
- package/dist/http.js +62 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/roles.d.ts +68 -0
- package/dist/roles.js +188 -0
- package/dist/schemas.d.ts +7 -0
- package/dist/schemas.js +13 -0
- package/dist/scorers/claim-framework.d.ts +52 -0
- package/dist/scorers/claim-framework.js +363 -0
- package/dist/scorers/flow-framework.d.ts +69 -0
- package/dist/scorers/flow-framework.js +708 -0
- package/dist/scorers/hook-framework.d.ts +58 -0
- package/dist/scorers/hook-framework.js +447 -0
- package/dist/scorers/string-utils.d.ts +1 -0
- package/dist/scorers/string-utils.js +10 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +71 -0
- package/dist/stats.d.ts +22 -0
- package/dist/stats.js +112 -0
- package/dist/tools/catalog.d.ts +2 -0
- package/dist/tools/catalog.js +84 -0
- package/dist/tools/craft.d.ts +2 -0
- package/dist/tools/craft.js +129 -0
- package/dist/tools/diagnose.d.ts +5 -0
- package/dist/tools/diagnose.js +269 -0
- package/dist/tools/plan.d.ts +4 -0
- package/dist/tools/plan.js +99 -0
- package/dist/tools/testing.d.ts +2 -0
- package/dist/tools/testing.js +145 -0
- package/dist/tools/why.d.ts +2 -0
- package/dist/tools/why.js +63 -0
- package/dist/voice.d.ts +33 -0
- package/dist/voice.js +139 -0
- package/package.json +73 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOOK & HEADLINE FRAMEWORK — single source of truth.
|
|
3
|
+
*
|
|
4
|
+
* Distilled from six independent research passes (classic direct response masters,
|
|
5
|
+
* modern practitioners, and our internal Hook Lab framework). Encodes the universal
|
|
6
|
+
* craft laws every writing bot inherits.
|
|
7
|
+
*
|
|
8
|
+
* This file owns:
|
|
9
|
+
* - The doctrine block injected into every writing bot's system prompt
|
|
10
|
+
* - The 7-gate evaluation function (scoreHook) called by the evaluate_hook tool
|
|
11
|
+
* - The AI-smell detection (banned words + patterns) used as a quality filter
|
|
12
|
+
* - The headline-vs-hook-vs-subhead distinction
|
|
13
|
+
*
|
|
14
|
+
* Pure module — no DB calls, no I/O. The diagnostic tool wraps these functions.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Detect AI-smell in any text. Returns the violations (if any).
|
|
18
|
+
* Used by both scoreHook and as a standalone filter.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectAISmell(text: string): {
|
|
21
|
+
violations: string[];
|
|
22
|
+
hasSmells: boolean;
|
|
23
|
+
};
|
|
24
|
+
type GateResult = 'pass' | 'warn' | 'fail';
|
|
25
|
+
interface GateOutcome {
|
|
26
|
+
name: string;
|
|
27
|
+
result: GateResult;
|
|
28
|
+
detail: string;
|
|
29
|
+
}
|
|
30
|
+
export type HookFormat = 'ad_headline' | 'email_subject' | 'first_line' | 'video_hook' | 'landing_page_h1' | 'social_post_open' | 'vsl_opening' | 'sales_letter_lead';
|
|
31
|
+
export type AwarenessLevel = 'unaware' | 'problem_aware' | 'solution_aware' | 'product_aware' | 'most_aware';
|
|
32
|
+
export interface HookScore {
|
|
33
|
+
score: number;
|
|
34
|
+
scoreLabel: 'fail' | 'weak' | 'passable' | 'strong' | 'excellent';
|
|
35
|
+
gates: GateOutcome[];
|
|
36
|
+
violations: string[];
|
|
37
|
+
suggestions: string[];
|
|
38
|
+
format?: HookFormat;
|
|
39
|
+
awarenessLevel?: AwarenessLevel;
|
|
40
|
+
}
|
|
41
|
+
export interface ScoreHookOptions {
|
|
42
|
+
format?: HookFormat;
|
|
43
|
+
awarenessLevel?: AwarenessLevel;
|
|
44
|
+
}
|
|
45
|
+
export declare function scoreHook(hook: string, opts?: ScoreHookOptions): HookScore;
|
|
46
|
+
export type Tactic = 'confession' | 'bold_claim' | 'relatability' | 'contrast' | 'curiosity';
|
|
47
|
+
/**
|
|
48
|
+
* Map a hook to one of the Hook Lab 5 tactics, by best-guess heuristic.
|
|
49
|
+
* Used by the persistence layer for aggregation. Order matters: more specific
|
|
50
|
+
* patterns are checked first so they aren't shadowed by broader ones.
|
|
51
|
+
*/
|
|
52
|
+
export declare function inferTactic(hook: string): Tactic;
|
|
53
|
+
/**
|
|
54
|
+
* Returns the doctrine block for injection into bot system prompts.
|
|
55
|
+
* Wrapped in delimiters so it's visually distinct from the bot's own prompt.
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildHookFrameworkBlock(): string;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// Copied from persuasion-ecosystem lib/hook-framework.ts on 2026-10-03. Pure module: no I/O, no AI calls.
|
|
2
|
+
// Keep in sync with the source until both import a shared package.
|
|
3
|
+
/**
|
|
4
|
+
* HOOK & HEADLINE FRAMEWORK — single source of truth.
|
|
5
|
+
*
|
|
6
|
+
* Distilled from six independent research passes (classic direct response masters,
|
|
7
|
+
* modern practitioners, and our internal Hook Lab framework). Encodes the universal
|
|
8
|
+
* craft laws every writing bot inherits.
|
|
9
|
+
*
|
|
10
|
+
* This file owns:
|
|
11
|
+
* - The doctrine block injected into every writing bot's system prompt
|
|
12
|
+
* - The 7-gate evaluation function (scoreHook) called by the evaluate_hook tool
|
|
13
|
+
* - The AI-smell detection (banned words + patterns) used as a quality filter
|
|
14
|
+
* - The headline-vs-hook-vs-subhead distinction
|
|
15
|
+
*
|
|
16
|
+
* Pure module — no DB calls, no I/O. The diagnostic tool wraps these functions.
|
|
17
|
+
*/
|
|
18
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// AI-SMELL: vocabulary and constructions that mark text as AI-generated
|
|
20
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
21
|
+
/**
|
|
22
|
+
* Single words/phrases that became AI tells in 2024-2026.
|
|
23
|
+
* These were "power words" pre-2023; now they actively reduce trust.
|
|
24
|
+
* Match is case-insensitive on word boundaries.
|
|
25
|
+
*/
|
|
26
|
+
const AI_SMELL_BANNED_WORDS = [
|
|
27
|
+
// Generic action verbs that signal "AI broadcast voice"
|
|
28
|
+
'unlock', 'unlocks', 'unlocking',
|
|
29
|
+
'leverage', 'leverages', 'leveraging',
|
|
30
|
+
'harness', 'harnesses', 'harnessing',
|
|
31
|
+
'elevate', 'elevates', 'elevating',
|
|
32
|
+
'empower', 'empowers', 'empowering',
|
|
33
|
+
'embark on', 'embarking',
|
|
34
|
+
'delve', 'delves', 'delving',
|
|
35
|
+
'navigate', 'navigates', 'navigating', // when used as "navigate the landscape"
|
|
36
|
+
// Hype phrases
|
|
37
|
+
'game-changer', 'game changer', 'game-changing',
|
|
38
|
+
'dive in', 'dive into', 'diving into',
|
|
39
|
+
'buckle up',
|
|
40
|
+
"let's be real",
|
|
41
|
+
"let's dive",
|
|
42
|
+
'tapestry',
|
|
43
|
+
'testament to',
|
|
44
|
+
'nestled in',
|
|
45
|
+
'a journey through',
|
|
46
|
+
// Clichéd opener phrases
|
|
47
|
+
"in today's fast-paced world",
|
|
48
|
+
"in today's digital age",
|
|
49
|
+
'in an era where',
|
|
50
|
+
'in a world where',
|
|
51
|
+
'in the world of',
|
|
52
|
+
'ever-evolving',
|
|
53
|
+
'fast-paced',
|
|
54
|
+
'cutting-edge',
|
|
55
|
+
'state-of-the-art',
|
|
56
|
+
'paradigm shift',
|
|
57
|
+
'seamless',
|
|
58
|
+
'seamlessly',
|
|
59
|
+
'revolutionize',
|
|
60
|
+
'revolutionizes',
|
|
61
|
+
'revolutionary',
|
|
62
|
+
// Empty intensifiers
|
|
63
|
+
'absolutely critical',
|
|
64
|
+
'truly unique',
|
|
65
|
+
'truly remarkable',
|
|
66
|
+
'utterly transformative',
|
|
67
|
+
'profoundly',
|
|
68
|
+
];
|
|
69
|
+
/**
|
|
70
|
+
* Constructions that betray ChatGPT-era output.
|
|
71
|
+
* These are matched as RegExp against the full text.
|
|
72
|
+
*/
|
|
73
|
+
const AI_SMELL_PATTERNS = [
|
|
74
|
+
{
|
|
75
|
+
name: 'em_dash_reversal',
|
|
76
|
+
// Tightened: only matches the ChatGPT signature pattern (short claim + em-dash + "it's" reversal).
|
|
77
|
+
// The previous regex [^—.!?]{2,40} was too greedy and matched legitimate parallel dash constructions
|
|
78
|
+
// like "The method — which I've tested over 6 months — it's working" (parallel asides, not reversal).
|
|
79
|
+
regex: /\b\w[\w\s]{0,20}\s—\s*it'?s\s+\w/i,
|
|
80
|
+
reason: 'Em-dash "X — it\'s Y" reversal is a ChatGPT signature construction. Use a period or restructure.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'not_just_but',
|
|
84
|
+
regex: /\bit'?s\s+not\s+(just\s+)?\w[\w\s]{1,40},?\s+it'?s\s+\w/i,
|
|
85
|
+
reason: '"It\'s not just X, it\'s Y" is a ChatGPT signature. Restructure as one assertion.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'more_than_just',
|
|
89
|
+
regex: /\bmore\s+than\s+just\b/i,
|
|
90
|
+
reason: '"More than just" is filler. Cut it and name what the thing actually is.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'in_the_world_of',
|
|
94
|
+
regex: /\bin\s+the\s+world\s+of\b/i,
|
|
95
|
+
reason: '"In the world of [X]" is throat-clearing. Skip the setup, make the claim.',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'tripled_adjectives',
|
|
99
|
+
regex: /\b(\w+ly\s+)?\w+,\s+\w+,\s+and\s+\w+\s+(experience|approach|solution|platform|system)\b/i,
|
|
100
|
+
reason: 'Three-adjective stack ("X, Y, and Z platform") is corporate boilerplate. Pick one specific claim.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'hope_this_helps',
|
|
104
|
+
regex: /\bhope\s+this\s+helps\b/i,
|
|
105
|
+
reason: '"Hope this helps" is AI assistant filler. Cut it.',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'absolutely_great_question',
|
|
109
|
+
regex: /\b(absolutely|great\s+question|excellent\s+point|that'?s\s+a\s+great)\b/i,
|
|
110
|
+
reason: 'Sycophantic AI assistant filler. Get to the point.',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
/**
|
|
114
|
+
* Detect AI-smell in any text. Returns the violations (if any).
|
|
115
|
+
* Used by both scoreHook and as a standalone filter.
|
|
116
|
+
*/
|
|
117
|
+
export function detectAISmell(text) {
|
|
118
|
+
const violations = [];
|
|
119
|
+
const lower = text.toLowerCase();
|
|
120
|
+
for (const word of AI_SMELL_BANNED_WORDS) {
|
|
121
|
+
// Word-boundary match for single words; substring for multi-word phrases
|
|
122
|
+
const pattern = word.includes(' ') || word.includes("'")
|
|
123
|
+
? new RegExp(word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i')
|
|
124
|
+
: new RegExp(`\\b${word}\\b`, 'i');
|
|
125
|
+
if (pattern.test(lower)) {
|
|
126
|
+
violations.push(`Banned word/phrase: "${word}"`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
for (const pat of AI_SMELL_PATTERNS) {
|
|
130
|
+
if (pat.regex.test(text)) {
|
|
131
|
+
violations.push(`${pat.name}: ${pat.reason}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { violations, hasSmells: violations.length > 0 };
|
|
135
|
+
}
|
|
136
|
+
// Format-specific length and character constraints
|
|
137
|
+
const FORMAT_CONSTRAINTS = {
|
|
138
|
+
ad_headline: { idealMaxChars: 125, hardMaxChars: 200, idealMaxWords: 12, note: 'First line of paid social — must work before "See more" cutoff.' },
|
|
139
|
+
email_subject: { idealMaxChars: 40, hardMaxChars: 60, idealMaxWords: 7, note: 'Mobile inbox cuts at ~40 chars. Lowercase preferred.' },
|
|
140
|
+
first_line: { idealMaxChars: 80, hardMaxChars: 140, idealMaxWords: 12, note: 'Opening sentence of body copy. Must earn the second sentence.' },
|
|
141
|
+
video_hook: { idealMaxChars: 60, hardMaxChars: 100, idealMaxWords: 10, note: 'Spoken or text overlay in first 3 seconds. Must complete a thought.' },
|
|
142
|
+
landing_page_h1: { idealMaxChars: 90, hardMaxChars: 140, idealMaxWords: 14, note: 'Hero headline. Must promise the outcome in plain language.' },
|
|
143
|
+
social_post_open: { idealMaxChars: 100, hardMaxChars: 200, idealMaxWords: 15, note: 'First two lines before fold on LinkedIn/X. Identity-flag the reader.' },
|
|
144
|
+
vsl_opening: { idealMaxChars: 100, hardMaxChars: 160, idealMaxWords: 16, note: 'First spoken sentence. Disqualify wrong viewers, hook the right ones.' },
|
|
145
|
+
sales_letter_lead: { idealMaxChars: 120, hardMaxChars: 220, idealMaxWords: 18, note: 'Long-form lead. Sets up the unique mechanism of the problem.' },
|
|
146
|
+
};
|
|
147
|
+
// Gate 1: SPECIFICITY
|
|
148
|
+
function checkSpecificity(hook) {
|
|
149
|
+
const hasNumber = /\b\d+([,.]\d+)?[k%+]?\b/.test(hook) || /\$[\d,]+/.test(hook);
|
|
150
|
+
const hasNamedEntity = /\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/.test(hook.replace(/^[^a-z]*/i, '')); // proper noun not at sentence start
|
|
151
|
+
const hasTimeframe = /\b(day|days|week|weeks|month|months|year|years|hour|hours|minute|minutes|second|seconds|today|tomorrow|tonight|monday|tuesday|wednesday|thursday|friday|saturday|sunday|january|february|march|april|may|june|july|august|september|october|november|december|q[1-4]|2024|2025|2026)\b/i.test(hook);
|
|
152
|
+
const hasMechanismHint = /\b(method|system|formula|framework|protocol|technique|approach|process|secret|trick|hack|loophole|shortcut|trick|mistake)\b/i.test(hook);
|
|
153
|
+
const signals = [hasNumber, hasNamedEntity, hasTimeframe, hasMechanismHint].filter(Boolean).length;
|
|
154
|
+
if (signals >= 2) {
|
|
155
|
+
return { name: 'specificity', result: 'pass', detail: 'Has multiple specificity signals (number/name/timeframe/mechanism).' };
|
|
156
|
+
}
|
|
157
|
+
if (signals === 1) {
|
|
158
|
+
return { name: 'specificity', result: 'warn', detail: 'Has one specificity signal. Add a second (e.g. exact number AND timeframe) for credibility.' };
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
name: 'specificity',
|
|
162
|
+
result: 'fail',
|
|
163
|
+
detail: 'No number, named entity, timeframe, or named mechanism. Vague hooks get scrolled past. Add at least one specific.',
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// Gate 2: CURIOSITY (information gap)
|
|
167
|
+
function checkCuriosity(hook) {
|
|
168
|
+
// Strong curiosity signals: question, "the X that", "why", "how", "what nobody"
|
|
169
|
+
const hasQuestion = /\?/.test(hook);
|
|
170
|
+
const hasGapPhrase = /\b(the\s+\w+\s+(that|nobody|that\s+nobody|why|how|when))\b/i.test(hook);
|
|
171
|
+
const hasReveal = /\b(nobody|never|won't|no one|secret|hidden|the real|the actual|the truth)\b/i.test(hook);
|
|
172
|
+
const hasContradiction = /\b(but|until|except|despite|wrong|backward|opposite)\b/i.test(hook);
|
|
173
|
+
// Negative: starts with bland statement of fact
|
|
174
|
+
const isFlatStatement = /^(this is|here is|here's a|we are|we offer|our|introducing)/i.test(hook.trim());
|
|
175
|
+
if (isFlatStatement) {
|
|
176
|
+
return { name: 'curiosity', result: 'fail', detail: 'Opens with a flat statement of fact. No gap. Reframe as a question, contradiction, or hidden reveal.' };
|
|
177
|
+
}
|
|
178
|
+
if (hasQuestion || hasGapPhrase || hasReveal || hasContradiction) {
|
|
179
|
+
return { name: 'curiosity', result: 'pass', detail: 'Opens an information gap the body must close.' };
|
|
180
|
+
}
|
|
181
|
+
return { name: 'curiosity', result: 'warn', detail: 'No clear curiosity gap. The reader can predict what comes next. Add tension, reveal, or contradiction.' };
|
|
182
|
+
}
|
|
183
|
+
// Gate 3: PROOF WELDED TO CLAIM
|
|
184
|
+
function checkProofWelded(hook) {
|
|
185
|
+
// Detect "claim" verbs that need backing
|
|
186
|
+
const hasBigClaim = /\b(\d+x|doubles?|triples?|quadruples?|increases?|decreases?|cuts?|saves?|earns?|made|generates?|adds?|delivers?)\b/i.test(hook);
|
|
187
|
+
if (!hasBigClaim) {
|
|
188
|
+
return { name: 'proof_welded', result: 'pass', detail: 'No big claim made — proof not required for this hook type.' };
|
|
189
|
+
}
|
|
190
|
+
// If big claim made, look for proof element
|
|
191
|
+
const hasProofMarker = /\b\d+([,.]\d+)?[k%+]?\b/.test(hook) || // a number
|
|
192
|
+
/\[CLIENT PROOF/.test(hook) || // explicit placeholder
|
|
193
|
+
/\b(study|harvard|mit|stanford|research|tested|verified|tracked|measured)\b/i.test(hook) || // authority
|
|
194
|
+
/[""]/.test(hook); // a quote (testimonial style)
|
|
195
|
+
if (hasProofMarker) {
|
|
196
|
+
return { name: 'proof_welded', result: 'pass', detail: 'Claim is welded to a proof element (number, study, quote, or [CLIENT PROOF] placeholder).' };
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
name: 'proof_welded',
|
|
200
|
+
result: 'fail',
|
|
201
|
+
detail: 'Hook makes a quantifiable claim without an attached proof element. Either add a number/source/quote, or use [CLIENT PROOF: ...] placeholder.',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
// Gate 4: VOICE (sounds human, not broadcast)
|
|
205
|
+
function checkVoice(hook) {
|
|
206
|
+
const hasFiller = /\b(in today's|in a world|in an era|in the world of)\b/i.test(hook);
|
|
207
|
+
// Corporate verbs synced with AI_SMELL_BANNED_WORDS — these are the words that signal
|
|
208
|
+
// "broadcast voice" and should fail the voice gate even before they're caught by anti_smell.
|
|
209
|
+
const hasCorporateVerb = /\b(unlock|leverage|harness|elevate|empower|optimize|streamline|facilitate|utilize|embark|delve|navigate|revolutionize)\b/i.test(hook);
|
|
210
|
+
// Title case with five+ words is a red flag (sounds like a billboard)
|
|
211
|
+
const words = hook.split(/\s+/).filter(Boolean);
|
|
212
|
+
const titleCaseFraction = words.length >= 5 ? words.filter(w => /^[A-Z]/.test(w)).length / words.length : 0;
|
|
213
|
+
const isTitleCased = titleCaseFraction > 0.7;
|
|
214
|
+
if (hasFiller || hasCorporateVerb) {
|
|
215
|
+
return { name: 'voice_human', result: 'fail', detail: 'Contains broadcast/corporate vocabulary. Read it aloud — does a real person talk this way?' };
|
|
216
|
+
}
|
|
217
|
+
if (isTitleCased) {
|
|
218
|
+
return { name: 'voice_human', result: 'warn', detail: 'Title case throughout reads as broadcast advertising. Try sentence case or lowercase for higher trust.' };
|
|
219
|
+
}
|
|
220
|
+
return { name: 'voice_human', result: 'pass', detail: 'Sounds like a specific person, not a broadcast.' };
|
|
221
|
+
}
|
|
222
|
+
// Gate 5: CONTRACT (heuristic — structural integrity check)
|
|
223
|
+
function checkContract(hook, format) {
|
|
224
|
+
// Heuristic: a hook that promises something must reference a delivery medium
|
|
225
|
+
// Without the body, we can only flag obvious overreach
|
|
226
|
+
const hasGuarantee = /\b(guarantee|guaranteed|promise|always|forever|never fails?)\b/i.test(hook);
|
|
227
|
+
const hasUnverifiableSuperlative = /\b(best|greatest|most|world.?class|number\s*one|#1|undisputed)\b/i.test(hook) && !/\d+/.test(hook);
|
|
228
|
+
if (hasUnverifiableSuperlative) {
|
|
229
|
+
return { name: 'contract', result: 'warn', detail: 'Uses "best/most/#1" without quantification. Either add a specific metric or remove the superlative.' };
|
|
230
|
+
}
|
|
231
|
+
if (hasGuarantee) {
|
|
232
|
+
return { name: 'contract', result: 'warn', detail: 'Promises a guarantee — make sure the body delivers it explicitly with terms.' };
|
|
233
|
+
}
|
|
234
|
+
// Format-specific length check feeds into contract
|
|
235
|
+
if (format) {
|
|
236
|
+
const c = FORMAT_CONSTRAINTS[format];
|
|
237
|
+
if (hook.length > c.hardMaxChars) {
|
|
238
|
+
return { name: 'contract', result: 'fail', detail: `Too long for ${format} (${hook.length} chars vs ${c.hardMaxChars} hard max). Reader won't see the full promise. ${c.note}` };
|
|
239
|
+
}
|
|
240
|
+
if (hook.length > c.idealMaxChars) {
|
|
241
|
+
return { name: 'contract', result: 'warn', detail: `Length (${hook.length}) exceeds ideal (${c.idealMaxChars}) for ${format}. Tighten if possible. ${c.note}` };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { name: 'contract', result: 'pass', detail: 'Promise is bounded and deliverable.' };
|
|
245
|
+
}
|
|
246
|
+
// Gate 6: ANTI-AI-SMELL
|
|
247
|
+
function checkAntiSmell(hook) {
|
|
248
|
+
const { violations, hasSmells } = detectAISmell(hook);
|
|
249
|
+
if (!hasSmells) {
|
|
250
|
+
return { name: 'anti_smell', result: 'pass', detail: 'No banned vocabulary or patterns detected.' };
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
name: 'anti_smell',
|
|
254
|
+
result: 'fail',
|
|
255
|
+
detail: `AI-smell detected: ${violations.slice(0, 3).join(' | ')}${violations.length > 3 ? ` (+${violations.length - 3} more)` : ''}`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// Gate 7: AWARENESS MATCH (tactic appropriate to audience awareness level)
|
|
259
|
+
function checkAwarenessMatch(hook, awarenessLevel) {
|
|
260
|
+
if (!awarenessLevel) {
|
|
261
|
+
return { name: 'awareness_match', result: 'pass', detail: 'No awareness level provided — gate skipped. Pass awareness_level for tighter scoring.' };
|
|
262
|
+
}
|
|
263
|
+
// Heuristic checks per level
|
|
264
|
+
const isCold = awarenessLevel === 'unaware' || awarenessLevel === 'problem_aware';
|
|
265
|
+
const isWarm = awarenessLevel === 'solution_aware' || awarenessLevel === 'product_aware';
|
|
266
|
+
const isHot = awarenessLevel === 'most_aware';
|
|
267
|
+
const hasOfferLanguage = /\b(buy|order|get yours|sale|discount|deal|free shipping|order now)\b/i.test(hook);
|
|
268
|
+
const hasMechanismName = /\b(method|system|formula|framework|protocol)\b/i.test(hook);
|
|
269
|
+
const hasComparisonLanguage = /\b(vs|versus|better than|unlike|compared to)\b/i.test(hook);
|
|
270
|
+
if (isHot && !hasOfferLanguage) {
|
|
271
|
+
return { name: 'awareness_match', result: 'warn', detail: 'Most-aware audience — they want the deal. Lead with offer/price/urgency, not curiosity.' };
|
|
272
|
+
}
|
|
273
|
+
if (isCold && hasOfferLanguage) {
|
|
274
|
+
return { name: 'awareness_match', result: 'warn', detail: 'Cold/unaware audience — leading with offer/price feels premature. Build curiosity or relate to a pain first.' };
|
|
275
|
+
}
|
|
276
|
+
if (isWarm && !(hasMechanismName || hasComparisonLanguage)) {
|
|
277
|
+
return { name: 'awareness_match', result: 'warn', detail: 'Solution/product-aware audience — they already know solutions exist. Differentiate via mechanism or comparison.' };
|
|
278
|
+
}
|
|
279
|
+
return { name: 'awareness_match', result: 'pass', detail: 'Tactic appropriate for audience awareness level.' };
|
|
280
|
+
}
|
|
281
|
+
const GATE_WEIGHTS = {
|
|
282
|
+
specificity: 2.0,
|
|
283
|
+
curiosity: 2.0,
|
|
284
|
+
proof_welded: 1.5,
|
|
285
|
+
voice_human: 1.5,
|
|
286
|
+
contract: 1.0,
|
|
287
|
+
anti_smell: 1.5,
|
|
288
|
+
awareness_match: 0.5,
|
|
289
|
+
};
|
|
290
|
+
const TOTAL_WEIGHT = Object.values(GATE_WEIGHTS).reduce((a, b) => a + b, 0);
|
|
291
|
+
function gateValue(result) {
|
|
292
|
+
return result === 'pass' ? 1 : result === 'warn' ? 0.5 : 0;
|
|
293
|
+
}
|
|
294
|
+
export function scoreHook(hook, opts = {}) {
|
|
295
|
+
const trimmed = hook.trim();
|
|
296
|
+
const gates = [
|
|
297
|
+
checkSpecificity(trimmed),
|
|
298
|
+
checkCuriosity(trimmed),
|
|
299
|
+
checkProofWelded(trimmed),
|
|
300
|
+
checkVoice(trimmed),
|
|
301
|
+
checkContract(trimmed, opts.format),
|
|
302
|
+
checkAntiSmell(trimmed),
|
|
303
|
+
checkAwarenessMatch(trimmed, opts.awarenessLevel),
|
|
304
|
+
];
|
|
305
|
+
// Weighted score 0-10
|
|
306
|
+
let weighted = 0;
|
|
307
|
+
for (const g of gates) {
|
|
308
|
+
weighted += gateValue(g.result) * (GATE_WEIGHTS[g.name] ?? 1);
|
|
309
|
+
}
|
|
310
|
+
let score = Math.round((weighted / TOTAL_WEIGHT) * 100) / 10; // 0.0 - 10.0
|
|
311
|
+
// HARD CAPS — a single failed gate on a critical dimension is disqualifying.
|
|
312
|
+
// Other passing gates shouldn't inflate a hook that violates the non-negotiables.
|
|
313
|
+
// The caps below produce score labels that match severity:
|
|
314
|
+
// fail (< 4) — disqualifying, do not deliver
|
|
315
|
+
// weak (4-6) — revise required
|
|
316
|
+
// passable (6-7.5) — usable but suboptimal
|
|
317
|
+
const curiosityFailed = gates.find(g => g.name === 'curiosity')?.result === 'fail';
|
|
318
|
+
const specificityFailed = gates.find(g => g.name === 'specificity')?.result === 'fail';
|
|
319
|
+
const antiSmellFailed = gates.find(g => g.name === 'anti_smell')?.result === 'fail';
|
|
320
|
+
const voiceFailed = gates.find(g => g.name === 'voice_human')?.result === 'fail';
|
|
321
|
+
if (antiSmellFailed)
|
|
322
|
+
score = Math.min(score, 3.9); // banned vocab = catastrophic, label = fail
|
|
323
|
+
if (curiosityFailed)
|
|
324
|
+
score = Math.min(score, 4.5); // no curiosity = label, not a hook
|
|
325
|
+
if (specificityFailed)
|
|
326
|
+
score = Math.min(score, 4.5); // no specifics = generic, not a hook
|
|
327
|
+
if (voiceFailed)
|
|
328
|
+
score = Math.min(score, 4.5); // broadcast voice = ad-shape, scrolled past
|
|
329
|
+
if (curiosityFailed && specificityFailed)
|
|
330
|
+
score = Math.min(score, 2.5); // double failure = unusable
|
|
331
|
+
// Aggregate violations and suggestions
|
|
332
|
+
const violations = gates.filter(g => g.result === 'fail').map(g => `${g.name}: ${g.detail}`);
|
|
333
|
+
const warnings = gates.filter(g => g.result === 'warn').map(g => `${g.name}: ${g.detail}`);
|
|
334
|
+
const suggestions = [...violations, ...warnings];
|
|
335
|
+
let scoreLabel;
|
|
336
|
+
if (score < 4)
|
|
337
|
+
scoreLabel = 'fail';
|
|
338
|
+
else if (score < 6)
|
|
339
|
+
scoreLabel = 'weak';
|
|
340
|
+
else if (score < 7.5)
|
|
341
|
+
scoreLabel = 'passable';
|
|
342
|
+
else if (score < 9)
|
|
343
|
+
scoreLabel = 'strong';
|
|
344
|
+
else
|
|
345
|
+
scoreLabel = 'excellent';
|
|
346
|
+
return {
|
|
347
|
+
score,
|
|
348
|
+
scoreLabel,
|
|
349
|
+
gates,
|
|
350
|
+
violations,
|
|
351
|
+
suggestions,
|
|
352
|
+
format: opts.format,
|
|
353
|
+
awarenessLevel: opts.awarenessLevel,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Map a hook to one of the Hook Lab 5 tactics, by best-guess heuristic.
|
|
358
|
+
* Used by the persistence layer for aggregation. Order matters: more specific
|
|
359
|
+
* patterns are checked first so they aren't shadowed by broader ones.
|
|
360
|
+
*/
|
|
361
|
+
export function inferTactic(hook) {
|
|
362
|
+
// Confession is most specific: first-person vulnerability
|
|
363
|
+
if (/\b(i\s+(was|tried|failed|lost|spent|almost)|my\s+\w+\s+(was|told|said))\b/i.test(hook))
|
|
364
|
+
return 'confession';
|
|
365
|
+
// Contrast checked BEFORE bold_claim because contrast is structurally more specific.
|
|
366
|
+
// A hook like "We cut costs 40% BUT competitors charge 2x more" is fundamentally a
|
|
367
|
+
// contrast move; checking bold_claim first would mislabel it.
|
|
368
|
+
if (/\b(but|until|except|despite|wrong|backward|actually|really|truly|opposite)\b/i.test(hook))
|
|
369
|
+
return 'contrast';
|
|
370
|
+
// Bold claim: specific quantified assertion
|
|
371
|
+
if (/\b(\d+x|doubled?|tripled?|increased|cut|saved|earned|generated|made)\b/i.test(hook))
|
|
372
|
+
return 'bold_claim';
|
|
373
|
+
// Relatability: second-person mirror
|
|
374
|
+
if (/\b(if\s+you|when\s+you|ever\s+(tried|wondered|noticed|felt))\b/i.test(hook))
|
|
375
|
+
return 'relatability';
|
|
376
|
+
// Default: curiosity (the most universal tactic)
|
|
377
|
+
return 'curiosity';
|
|
378
|
+
}
|
|
379
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
380
|
+
// THE DOCTRINE — injected into every writing bot's system prompt
|
|
381
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
382
|
+
const HOOK_FRAMEWORK_DOCTRINE = `
|
|
383
|
+
HOOK & HEADLINE FRAMEWORK (canonical doctrine for all writing bots)
|
|
384
|
+
|
|
385
|
+
Hooks and headlines do 80%+ of the conversion work. A weak opener kills strong body copy. A strong opener earns the right to be read. These rules are not stylistic preferences — they are the convergent craft laws that every direct response master from Hopkins to Halbert to Hormozi independently arrived at.
|
|
386
|
+
|
|
387
|
+
THE THREE ARTIFACTS — never confuse them
|
|
388
|
+
- HEADLINE: the line on the page or screen that earns the click or first attention. Its job is selection + promise + pattern interrupt. It is read by people who haven't yet decided to read.
|
|
389
|
+
- HOOK: the first sentence (or first three sentences) of the body. Its job is to validate the headline's promise and start the slippery slide. It is read by people who have just decided to read and are looking for any reason to leave.
|
|
390
|
+
- SUBHEAD: not a label, not a topic marker. It is a second headline. Every subhead must do persuasion work — provoke, claim, or create curiosity. A reader who only reads your subheads should still want to buy. NEVER use placeholder section headers like "The Problem" or "How It Works" — those are dead.
|
|
391
|
+
|
|
392
|
+
THE 7 GATES (every hook must pass these)
|
|
393
|
+
1. SPECIFICITY — has at least one number, named entity, timeframe, or named mechanism. Vague hooks get scrolled past. "$47,329" beats "around $50K". "By Friday" beats "soon". Odd numbers feel audited; round numbers feel fabricated.
|
|
394
|
+
2. CURIOSITY — opens an information gap the body must close. Loewenstein's law: curiosity fires when the gap is perceivable but not yet closeable. Give the category, withhold the specific. The brain needs the resolution.
|
|
395
|
+
3. PROOF WELDED TO CLAIM — Bencivenga's rule: never trumpet a claim without joining proof at the hip. If you make a quantifiable claim, the proof element travels with it (a number, a study, a quote, a named source). If real proof doesn't exist yet, use [CLIENT PROOF: ...] placeholder. Never fabricate.
|
|
396
|
+
4. VOICE — sounds like one specific person talking to one specific person. NOT broadcast voice. NOT corporate. NOT an announcer. Lowercase often beats title case. Fragments beat full sentences. Read it aloud — would a real human say this at a dinner party? If no, rewrite.
|
|
397
|
+
5. CONTRACT — whatever the hook promises, the body delivers, in order. Welsh's rule: the hook is a contract. Clickbait that doesn't pay off destroys trust for every future hook from that source.
|
|
398
|
+
6. ANTI-AI-SMELL — no banned vocabulary. The 2024 power words are now AI tells: "unlock," "leverage," "harness," "dive in," "game-changer," "buckle up," "elevate," "empower," "in today's fast-paced world," "in a world where," em-dash "X — it's Y" reversal, "It's not just X, it's Y." These actively reduce trust now. Avoid them in hooks especially. Avoid them in body copy whenever possible.
|
|
399
|
+
7. AWARENESS MATCH — Schwartz's framework determines tactic selection.
|
|
400
|
+
- Unaware/Cold → curiosity, story, surprising truth (do not lead with offer or mechanism)
|
|
401
|
+
- Problem-Aware → relate to the pain in their language, name the failed solutions
|
|
402
|
+
- Solution-Aware → introduce the unique mechanism, contrast against alternatives
|
|
403
|
+
- Product-Aware → lead with proof, objection killers, urgency, offer details
|
|
404
|
+
- Most-Aware → lead with the deal itself; tactic matters less than price and access
|
|
405
|
+
|
|
406
|
+
THE ANTI-CLEVERNESS RULE (most violated, most important)
|
|
407
|
+
Visceral always beats clever. The masters all converged on this: Ogilvy hated puns, Hopkins said frivolity has no place, Burnett said sell the product not the ad, Bencivenga banned hype.
|
|
408
|
+
|
|
409
|
+
When writing a hook:
|
|
410
|
+
- Lead with ONE concrete physical image. Not three stacked clauses with qualifiers.
|
|
411
|
+
- Use ONE visceral verb (steal, inject, trap, bottle, force-feed, slam, shatter). Not "leverage" or "harness."
|
|
412
|
+
- The benefit closes the sentence — never leads it.
|
|
413
|
+
- If a sentence has two concepts, you have one too many. Cut.
|
|
414
|
+
- "Steal the brains of a master copywriter and inject them into every ad you write" beats "trap everything your best campaign ever learned and force-feed it into every campaign that comes after which is honestly the compounding loop you've been missing."
|
|
415
|
+
|
|
416
|
+
The first one is one image, two verbs, one promise. The second is three concepts pretending to be one sentence.
|
|
417
|
+
|
|
418
|
+
THE CONTRACT BETWEEN HOOK AND BODY
|
|
419
|
+
A hook that promises X must deliver X, in the order it was promised, in the body. The "hook is a contract" rule (Welsh) is the single most-violated modern rule. Don't promise a 7-step framework and then deliver 5 steps. Don't promise a story and then deliver advice. Don't promise a number and then never reference it again.
|
|
420
|
+
|
|
421
|
+
THE evaluate_hook TOOL
|
|
422
|
+
You have access to a tool called evaluate_hook. Use it before finalizing any critical opener — landing page H1, ad headline, email subject, video first frame, VSL opening line. Pass the hook + format + awareness_level. The tool returns a 0-10 score, named gate failures, and revision suggestions. If the score is below 7, revise and re-score before delivering. Do not announce this process to the user — just deliver hooks that have passed.
|
|
423
|
+
|
|
424
|
+
THE get_winning_hooks TOOL (when available)
|
|
425
|
+
Before drafting hooks for a client, call get_winning_hooks to learn what's already converted in their market. Use the returned tactics and patterns as starting weight, not as templates to copy. Hooks that have actually converted for this client or their niche outweigh framework defaults.
|
|
426
|
+
|
|
427
|
+
WHY THIS MATTERS
|
|
428
|
+
Every campaign you've ever run threw away everything it learned. This system is the first place where hook craft compounds — every generated hook is scored, every delivered hook is tagged, every winner feeds back into the framework. The doctrine above is the floor. The data is the ceiling. Use both.
|
|
429
|
+
`.trim();
|
|
430
|
+
/**
|
|
431
|
+
* Returns the doctrine block for injection into bot system prompts.
|
|
432
|
+
* Wrapped in delimiters so it's visually distinct from the bot's own prompt.
|
|
433
|
+
*/
|
|
434
|
+
export function buildHookFrameworkBlock() {
|
|
435
|
+
return `
|
|
436
|
+
|
|
437
|
+
================================================================
|
|
438
|
+
HOOK FRAMEWORK ALIGNMENT (shared across all writing bots)
|
|
439
|
+
================================================================
|
|
440
|
+
|
|
441
|
+
${HOOK_FRAMEWORK_DOCTRINE}
|
|
442
|
+
|
|
443
|
+
================================================================
|
|
444
|
+
END HOOK FRAMEWORK ALIGNMENT
|
|
445
|
+
================================================================
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function safeSlice(str: string, max: number): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Minimal copy of persuasion-ecosystem lib/string-utils.ts safeSlice (UTF-16 safe).
|
|
2
|
+
export function safeSlice(str, max) {
|
|
3
|
+
if (str.length <= max)
|
|
4
|
+
return str;
|
|
5
|
+
const sliced = str.slice(0, max);
|
|
6
|
+
const last = sliced.charCodeAt(sliced.length - 1);
|
|
7
|
+
if (last >= 0xd800 && last <= 0xdbff)
|
|
8
|
+
return sliced.slice(0, -1);
|
|
9
|
+
return sliced;
|
|
10
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
/**
|
|
3
|
+
* The first thing a connecting model reads. Like every other string this server returns, it is
|
|
4
|
+
* written the way we want people to write: plain words, sentences that lead into each other.
|
|
5
|
+
*/
|
|
6
|
+
export declare const INSTRUCTIONS: string;
|
|
7
|
+
export declare function buildServer(): McpServer;
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { ROLE_NAMES, ROLES } from './roles.js';
|
|
5
|
+
import { TECHNIQUES, CATALOG_META } from './data.js';
|
|
6
|
+
import { cap } from './voice.js';
|
|
7
|
+
import { registerPlanTool } from './tools/plan.js';
|
|
8
|
+
import { registerDiagnoseTool } from './tools/diagnose.js';
|
|
9
|
+
import { registerWhyTool } from './tools/why.js';
|
|
10
|
+
import { registerCatalogTools } from './tools/catalog.js';
|
|
11
|
+
import { registerCraftTools } from './tools/craft.js';
|
|
12
|
+
import { registerTestingTools } from './tools/testing.js';
|
|
13
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
14
|
+
const QUESTIONS = ROLE_NAMES.map((r) => ROLES[r].question).join(' ');
|
|
15
|
+
/**
|
|
16
|
+
* The first thing a connecting model reads. Like every other string this server returns, it is
|
|
17
|
+
* written the way we want people to write: plain words, sentences that lead into each other.
|
|
18
|
+
*/
|
|
19
|
+
export const INSTRUCTIONS = [
|
|
20
|
+
`This is the Persuasion Taxonomy, from Coppica. It gives you a working model of the reader, a catalog of ${TECHNIQUES.length} persuasion techniques documented from real advertising, ` +
|
|
21
|
+
'a way to check copy before anyone sees it, and the math behind A/B tests.',
|
|
22
|
+
`The model of the reader is simple. Everyone who reads an ad, a page or an email is silently asking nine questions: ${QUESTIONS} ` +
|
|
23
|
+
'Copy persuades when it answers the questions its goal needs, answers them truthfully, and the answers fit together. When one answer contradicts another, ' +
|
|
24
|
+
'the reader feels that something is off and trusts everything else a little less.',
|
|
25
|
+
'So reach for these tools whenever you write, review or improve marketing copy of any kind, from ads and landing pages to emails, subject lines, social posts, headlines and scripts, ' +
|
|
26
|
+
"and whenever someone asks why their marketing isn't converting or how to test it. Do it even when you think you already know the answer. " +
|
|
27
|
+
'Left to itself, a language model answers each question with the most expected move. That move is exactly what readers have learned to skim past.',
|
|
28
|
+
'Here is the usual order. Call plan_marketing_copy before you write a word. Then, before you show anyone a draft, or when someone asks what you think of theirs, ' +
|
|
29
|
+
'call diagnose_marketing_copy, fix what it finds, and present the work. One check and one revision is usually enough. When a line leans on the obvious move, ' +
|
|
30
|
+
'find_persuasion_techniques and get_persuasion_technique will find you a better one. For headlines, hooks and subject lines, check_headlines checks and compares them, ' +
|
|
31
|
+
"and check_marketing_claims does the same for claims and buttons. If a funnel isn't converting, start with explain_why_not_converting. " +
|
|
32
|
+
"And when it's time to test, plan_ab_test sizes the test and read_ab_test_result tells you what it found.",
|
|
33
|
+
'Whenever you use a technique, name it and link to it. The catalog is free under CC BY 4.0, and that link lets your reader see the real examples behind the advice.',
|
|
34
|
+
].join('\n\n');
|
|
35
|
+
function nineQuestionsDoc() {
|
|
36
|
+
const lines = [
|
|
37
|
+
'# The nine reader questions',
|
|
38
|
+
'',
|
|
39
|
+
'From The 18 Theses, Thesis VIII: https://taxonomy.coppica.com/the-18-theses',
|
|
40
|
+
'',
|
|
41
|
+
'Everyone who reads an ad, a page or an email is silently asking nine questions. Copy persuades when it answers the ones its goal needs, truthfully, and the answers hold together.',
|
|
42
|
+
'',
|
|
43
|
+
];
|
|
44
|
+
for (const r of ROLE_NAMES) {
|
|
45
|
+
const i = ROLES[r];
|
|
46
|
+
lines.push(`${i.num}. **${i.question}** (${r}) This one is about ${i.gloss}. Leave it unanswered and here is what happens: "${i.ifMissing}" https://taxonomy.coppica.com/${r.toLowerCase()}`);
|
|
47
|
+
}
|
|
48
|
+
lines.push('', 'Real persuasion happens when every question a piece needs is answered, in the same person, at the same time, and the answers agree with each other. ' +
|
|
49
|
+
'When one answer contradicts another, the reader feels that something is off, and they discount everything.');
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
export function buildServer() {
|
|
53
|
+
const server = new McpServer({ name: 'persuasion-taxonomy', version: VERSION, title: 'The Persuasion Taxonomy (Coppica)' }, { instructions: INSTRUCTIONS });
|
|
54
|
+
registerPlanTool(server);
|
|
55
|
+
registerDiagnoseTool(server);
|
|
56
|
+
registerWhyTool(server);
|
|
57
|
+
registerCatalogTools(server);
|
|
58
|
+
registerCraftTools(server);
|
|
59
|
+
registerTestingTools(server);
|
|
60
|
+
server.registerResource('nine-questions', 'persuasion://nine-questions', { title: 'The nine reader questions', description: 'The model of the reader behind every tool here.', mimeType: 'text/markdown' }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: nineQuestionsDoc() }] }));
|
|
61
|
+
server.registerResource('about', 'persuasion://about', { title: 'About this catalog snapshot', description: 'When the snapshot was taken, what it holds, and its license.', mimeType: 'application/json' }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(CATALOG_META, null, 2) }] }));
|
|
62
|
+
const prompt = (t) => ({ messages: [{ role: 'user', content: { type: 'text', text: t } }] });
|
|
63
|
+
server.registerPrompt('roast', { title: 'Roast my copy', description: 'A line-by-line read of your copy against the nine questions every reader asks.', argsSchema: { copy: z.string(), goal: z.string().optional() } }, ({ copy, goal }) => prompt(`Roast this marketing copy${goal ? `. Its job is ${goal}` : ''}. Go through it line by line and label each line with the reader question it answers, then run diagnose_marketing_copy. ` +
|
|
64
|
+
`Give me the three fixes that will matter most, with the rewritten lines, and don't soften it.\n\n${copy}`));
|
|
65
|
+
server.registerPrompt('brief', { title: 'Brief and draft', description: 'Plan the copy around the nine questions, write it, then check it before you see it.', argsSchema: { offer: z.string(), audience: z.string(), format: z.string().optional() } }, ({ offer, audience, format }) => prompt(`Write ${format ?? 'a landing page'} for this offer: ${offer}. The reader is ${audience}. Start with plan_marketing_copy, and make at least one of your moves a less expected one. ` +
|
|
66
|
+
'Then write the draft, run diagnose_marketing_copy on it, and fix what it finds before you show it to me.'));
|
|
67
|
+
server.registerPrompt('why-not-converting', { title: 'Why is this not converting?', description: 'Start from what you can see happening and work back to what the copy is missing.', argsSchema: { situation: z.string() } }, ({ situation }) => prompt(`My marketing isn't converting. ${cap(situation)}\n\nWork out the symptom, run explain_why_not_converting, and tell me what to check first and what to test.`));
|
|
68
|
+
server.registerPrompt('next-test', { title: 'What should I test next?', description: 'Propose a test that teaches you something: two different answers to the same reader question.', argsSchema: { copy: z.string(), traffic: z.string().optional() } }, ({ copy, traffic }) => prompt('Propose the next A/B test for this copy, framed as two different answers to the same reader question. Use diagnose_marketing_copy to find the weakest question, ' +
|
|
69
|
+
`find_persuasion_techniques to find the challenger, and plan_ab_test to size it${traffic ? `. Here is the traffic and conversion rate: ${traffic}` : ''}.\n\n${copy}`));
|
|
70
|
+
return server;
|
|
71
|
+
}
|
package/dist/stats.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Plain A/B test statistics. No dependencies, no network. */
|
|
2
|
+
/** Standard normal CDF (Abramowitz & Stegun 7.1.26 via erf, |error| < 1.5e-7). */
|
|
3
|
+
export declare function normCdf(z: number): number;
|
|
4
|
+
/** Inverse standard normal CDF (Acklam's rational approximation). */
|
|
5
|
+
export declare function normInv(p: number): number;
|
|
6
|
+
/** Visitors needed per version to detect a relative lift (two-sided, two-proportion z-test). */
|
|
7
|
+
export declare function sampleSizePerArm(baseline: number, relativeLift: number, alpha: number, power: number): number;
|
|
8
|
+
export interface Comparison {
|
|
9
|
+
rateA: number;
|
|
10
|
+
rateB: number;
|
|
11
|
+
diff: number;
|
|
12
|
+
relLift: number;
|
|
13
|
+
z: number;
|
|
14
|
+
p: number;
|
|
15
|
+
ciLow: number;
|
|
16
|
+
ciHigh: number;
|
|
17
|
+
probBBetter: number;
|
|
18
|
+
}
|
|
19
|
+
/** Two-proportion z-test (pooled SE for the test, unpooled SE for the interval). */
|
|
20
|
+
export declare function compare(cA: number, nA: number, cB: number, nB: number, alpha: number): Comparison;
|
|
21
|
+
/** Sample ratio mismatch: chi-square goodness of fit of observed visitors against the intended split. */
|
|
22
|
+
export declare function srmPValue(observed: number[], intendedShares: number[]): number;
|