mango-lollipop 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/CLAUDE.md +69 -0
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/bin/mango-lollipop.js +385 -0
- package/dist/excel.d.ts +4 -0
- package/dist/excel.js +342 -0
- package/dist/html.d.ts +4 -0
- package/dist/html.js +938 -0
- package/dist/schema.d.ts +120 -0
- package/dist/schema.js +211 -0
- package/lib/excel.ts +433 -0
- package/lib/html.ts +993 -0
- package/lib/schema.ts +394 -0
- package/package.json +44 -0
- package/skills/audit/SKILL.md +248 -0
- package/skills/dev-handoff/SKILL.md +295 -0
- package/skills/generate-dashboard/SKILL.md +195 -0
- package/skills/generate-matrix/SKILL.md +374 -0
- package/skills/generate-messages/SKILL.md +262 -0
- package/skills/iterate/SKILL.md +242 -0
- package/skills/start/SKILL.md +310 -0
- package/templates/copywriting-guide.md +155 -0
- package/templates/dashboard.html +522 -0
- package/templates/events/saas-collaboration.yaml +50 -0
- package/templates/events/saas-document.yaml +44 -0
- package/templates/events/saas-general.yaml +38 -0
- package/templates/events/saas-marketplace.yaml +48 -0
- package/templates/overview.html +598 -0
- package/templates/saas-matrix.json +172 -0
package/lib/schema.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Mango Lollipop — TypeScript Schema & Validation
|
|
3
|
+
// =============================================================================
|
|
4
|
+
|
|
5
|
+
// -----------------------------------------------------------------------------
|
|
6
|
+
// Type Aliases
|
|
7
|
+
// -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
export type Channel = "email" | "sms" | "in-app" | "push";
|
|
10
|
+
|
|
11
|
+
export type AARRRStage = "AQ" | "AC" | "RV" | "RT" | "RF";
|
|
12
|
+
|
|
13
|
+
// -----------------------------------------------------------------------------
|
|
14
|
+
// Core Message Interfaces
|
|
15
|
+
// -----------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export interface Trigger {
|
|
18
|
+
event: string;
|
|
19
|
+
type: "event" | "scheduled" | "behavioral";
|
|
20
|
+
schedule?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Guard {
|
|
24
|
+
condition: string;
|
|
25
|
+
expression: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Suppression {
|
|
29
|
+
condition: string;
|
|
30
|
+
expression: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CTA {
|
|
34
|
+
text: string;
|
|
35
|
+
url?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Message {
|
|
39
|
+
id: string;
|
|
40
|
+
stage: AARRRStage | "TX";
|
|
41
|
+
name: string;
|
|
42
|
+
classification: "transactional" | "lifecycle";
|
|
43
|
+
trigger: Trigger;
|
|
44
|
+
wait: string;
|
|
45
|
+
guards: Guard[];
|
|
46
|
+
suppressions: Suppression[];
|
|
47
|
+
subject: string;
|
|
48
|
+
preheader?: string;
|
|
49
|
+
body: string;
|
|
50
|
+
cta: CTA;
|
|
51
|
+
channel: Channel;
|
|
52
|
+
format: "plain" | "rich";
|
|
53
|
+
from: string;
|
|
54
|
+
segment: string;
|
|
55
|
+
tags: string[];
|
|
56
|
+
goal: string;
|
|
57
|
+
comments: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// -----------------------------------------------------------------------------
|
|
61
|
+
// Voice & Persona Interfaces
|
|
62
|
+
// -----------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
export interface SenderPersona {
|
|
65
|
+
name: string;
|
|
66
|
+
role: string;
|
|
67
|
+
use_for: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface VoiceProfile {
|
|
71
|
+
tone: string;
|
|
72
|
+
formality: number; // 1-5
|
|
73
|
+
emoji_usage: "none" | "light" | "heavy";
|
|
74
|
+
signature_style: string;
|
|
75
|
+
sample_phrases: string[];
|
|
76
|
+
sender_personas: SenderPersona[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// -----------------------------------------------------------------------------
|
|
80
|
+
// Event Taxonomy
|
|
81
|
+
// -----------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export interface EventTaxonomy {
|
|
84
|
+
identity: string[];
|
|
85
|
+
activation: string[];
|
|
86
|
+
engagement: string[];
|
|
87
|
+
conversion: string[];
|
|
88
|
+
retention: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// -----------------------------------------------------------------------------
|
|
92
|
+
// Analysis (analysis.json output from the analyze skill)
|
|
93
|
+
// -----------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export interface AnalysisCompany {
|
|
96
|
+
name: string;
|
|
97
|
+
product_type: string;
|
|
98
|
+
target_audience: string;
|
|
99
|
+
key_value_prop: string;
|
|
100
|
+
aha_moment: string;
|
|
101
|
+
key_features: string[];
|
|
102
|
+
pricing_model: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface AnalysisTags {
|
|
106
|
+
sources: string[];
|
|
107
|
+
plans: string[];
|
|
108
|
+
segments: string[];
|
|
109
|
+
features: string[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ExistingPerformance {
|
|
113
|
+
open_rate_avg: string;
|
|
114
|
+
click_rate_avg: string;
|
|
115
|
+
problem_areas: string[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ExistingMessaging {
|
|
119
|
+
messages_count: number;
|
|
120
|
+
stages_covered: (AARRRStage | "TX")[];
|
|
121
|
+
stages_missing: (AARRRStage | "TX")[];
|
|
122
|
+
channels_used: Channel[];
|
|
123
|
+
performance: ExistingPerformance;
|
|
124
|
+
primary_goal: string;
|
|
125
|
+
messages: unknown[];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface Analysis {
|
|
129
|
+
path: "fresh" | "existing";
|
|
130
|
+
company: AnalysisCompany;
|
|
131
|
+
channels: Channel[];
|
|
132
|
+
voice: VoiceProfile;
|
|
133
|
+
events: EventTaxonomy;
|
|
134
|
+
tags: AnalysisTags;
|
|
135
|
+
existing?: ExistingMessaging;
|
|
136
|
+
recommendations: string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// -----------------------------------------------------------------------------
|
|
140
|
+
// Project Config (mango-lollipop.json)
|
|
141
|
+
// -----------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
export interface ProjectConfig {
|
|
144
|
+
name: string;
|
|
145
|
+
version: string;
|
|
146
|
+
created: string;
|
|
147
|
+
stage: string;
|
|
148
|
+
path: "fresh" | "existing" | null;
|
|
149
|
+
channels: Channel[];
|
|
150
|
+
analysis: Analysis | null;
|
|
151
|
+
matrix: { messages: Message[] } | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -----------------------------------------------------------------------------
|
|
155
|
+
// Constants
|
|
156
|
+
// -----------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
const VALID_CHANNELS: Channel[] = ["email", "sms", "in-app", "push"];
|
|
159
|
+
const VALID_STAGES: (AARRRStage | "TX")[] = ["AQ", "AC", "RV", "RT", "RF", "TX"];
|
|
160
|
+
const VALID_AARRR_STAGES: AARRRStage[] = ["AQ", "AC", "RV", "RT", "RF"];
|
|
161
|
+
|
|
162
|
+
// ISO 8601 duration pattern: P[nY][nM][nD][T[nH][nM][nS]] or PnW
|
|
163
|
+
const ISO_8601_DURATION_RE =
|
|
164
|
+
/^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$|^P(\d+)W$/;
|
|
165
|
+
|
|
166
|
+
// -----------------------------------------------------------------------------
|
|
167
|
+
// Type Guards
|
|
168
|
+
// -----------------------------------------------------------------------------
|
|
169
|
+
|
|
170
|
+
export function isValidChannel(ch: unknown): ch is Channel {
|
|
171
|
+
return typeof ch === "string" && VALID_CHANNELS.includes(ch as Channel);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function isValidStage(stage: unknown): stage is AARRRStage {
|
|
175
|
+
return typeof stage === "string" && VALID_AARRR_STAGES.includes(stage as AARRRStage);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// -----------------------------------------------------------------------------
|
|
179
|
+
// Validation Helpers
|
|
180
|
+
// -----------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
export function isValidWaitDuration(wait: unknown): boolean {
|
|
183
|
+
if (typeof wait !== "string") return false;
|
|
184
|
+
return ISO_8601_DURATION_RE.test(wait);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface ValidationResult {
|
|
188
|
+
valid: boolean;
|
|
189
|
+
errors: string[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function validateMessage(msg: unknown): ValidationResult {
|
|
193
|
+
const errors: string[] = [];
|
|
194
|
+
|
|
195
|
+
if (typeof msg !== "object" || msg === null) {
|
|
196
|
+
return { valid: false, errors: ["Message must be a non-null object"] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const m = msg as Record<string, unknown>;
|
|
200
|
+
|
|
201
|
+
// Required string fields
|
|
202
|
+
const requiredStrings: [string, string][] = [
|
|
203
|
+
["id", "id"],
|
|
204
|
+
["name", "name"],
|
|
205
|
+
["subject", "subject"],
|
|
206
|
+
["body", "body"],
|
|
207
|
+
["from", "from"],
|
|
208
|
+
["segment", "segment"],
|
|
209
|
+
["goal", "goal"],
|
|
210
|
+
["wait", "wait"],
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const [field, label] of requiredStrings) {
|
|
214
|
+
if (typeof m[field] !== "string" || (m[field] as string).length === 0) {
|
|
215
|
+
errors.push(`Missing or empty required field: ${label}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Stage
|
|
220
|
+
const validStagesAll = VALID_STAGES;
|
|
221
|
+
if (typeof m.stage !== "string" || !validStagesAll.includes(m.stage as AARRRStage | "TX")) {
|
|
222
|
+
errors.push(`Invalid stage: "${String(m.stage)}". Must be one of: ${validStagesAll.join(", ")}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Classification
|
|
226
|
+
if (m.classification !== "transactional" && m.classification !== "lifecycle") {
|
|
227
|
+
errors.push(`Invalid classification: "${String(m.classification)}". Must be "transactional" or "lifecycle"`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Wait duration
|
|
231
|
+
if (typeof m.wait === "string" && !isValidWaitDuration(m.wait)) {
|
|
232
|
+
errors.push(`Invalid wait duration: "${m.wait}". Must be ISO 8601 duration (e.g. "P0D", "PT5M", "P2D")`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Format
|
|
236
|
+
if (m.format !== "plain" && m.format !== "rich") {
|
|
237
|
+
errors.push(`Invalid format: "${String(m.format)}". Must be "plain" or "rich"`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Channel (singular)
|
|
241
|
+
if (!isValidChannel(m.channel)) {
|
|
242
|
+
errors.push(`Invalid channel: "${String(m.channel)}". Must be one of: ${VALID_CHANNELS.join(", ")}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Tags
|
|
246
|
+
if (!Array.isArray(m.tags)) {
|
|
247
|
+
errors.push("tags must be an array");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Guards
|
|
251
|
+
if (!Array.isArray(m.guards)) {
|
|
252
|
+
errors.push("guards must be an array");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Suppressions
|
|
256
|
+
if (!Array.isArray(m.suppressions)) {
|
|
257
|
+
errors.push("suppressions must be an array");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Trigger
|
|
261
|
+
if (typeof m.trigger !== "object" || m.trigger === null) {
|
|
262
|
+
errors.push("trigger must be a non-null object");
|
|
263
|
+
} else {
|
|
264
|
+
const t = m.trigger as Record<string, unknown>;
|
|
265
|
+
if (typeof t.event !== "string" || t.event.length === 0) {
|
|
266
|
+
errors.push("trigger.event is required");
|
|
267
|
+
}
|
|
268
|
+
if (t.type !== "event" && t.type !== "scheduled" && t.type !== "behavioral") {
|
|
269
|
+
errors.push(`Invalid trigger.type: "${String(t.type)}". Must be "event", "scheduled", or "behavioral"`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// CTA
|
|
274
|
+
if (typeof m.cta !== "object" || m.cta === null) {
|
|
275
|
+
errors.push("cta must be a non-null object");
|
|
276
|
+
} else {
|
|
277
|
+
const c = m.cta as Record<string, unknown>;
|
|
278
|
+
if (typeof c.text !== "string" || c.text.length === 0) {
|
|
279
|
+
errors.push("cta.text is required");
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { valid: errors.length === 0, errors };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function validateAnalysis(analysis: unknown): ValidationResult {
|
|
287
|
+
const errors: string[] = [];
|
|
288
|
+
|
|
289
|
+
if (typeof analysis !== "object" || analysis === null) {
|
|
290
|
+
return { valid: false, errors: ["Analysis must be a non-null object"] };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const a = analysis as Record<string, unknown>;
|
|
294
|
+
|
|
295
|
+
// Path
|
|
296
|
+
if (a.path !== "fresh" && a.path !== "existing") {
|
|
297
|
+
errors.push(`Invalid path: "${String(a.path)}". Must be "fresh" or "existing"`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Company
|
|
301
|
+
if (typeof a.company !== "object" || a.company === null) {
|
|
302
|
+
errors.push("company is required");
|
|
303
|
+
} else {
|
|
304
|
+
const c = a.company as Record<string, unknown>;
|
|
305
|
+
const companyFields = ["name", "product_type", "target_audience", "key_value_prop", "aha_moment", "pricing_model"];
|
|
306
|
+
for (const field of companyFields) {
|
|
307
|
+
if (typeof c[field] !== "string" || (c[field] as string).length === 0) {
|
|
308
|
+
errors.push(`Missing or empty company.${field}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!Array.isArray(c.key_features) || c.key_features.length === 0) {
|
|
312
|
+
errors.push("company.key_features must be a non-empty array");
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Channels
|
|
317
|
+
if (!Array.isArray(a.channels) || a.channels.length === 0) {
|
|
318
|
+
errors.push("Must have at least one channel");
|
|
319
|
+
} else {
|
|
320
|
+
for (const ch of a.channels) {
|
|
321
|
+
if (!isValidChannel(ch)) {
|
|
322
|
+
errors.push(`Invalid channel: "${String(ch)}". Must be one of: ${VALID_CHANNELS.join(", ")}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Voice
|
|
328
|
+
if (typeof a.voice !== "object" || a.voice === null) {
|
|
329
|
+
errors.push("voice profile is required");
|
|
330
|
+
} else {
|
|
331
|
+
const v = a.voice as Record<string, unknown>;
|
|
332
|
+
if (typeof v.tone !== "string" || (v.tone as string).length === 0) {
|
|
333
|
+
errors.push("voice.tone is required");
|
|
334
|
+
}
|
|
335
|
+
if (typeof v.formality !== "number" || v.formality < 1 || v.formality > 5) {
|
|
336
|
+
errors.push("voice.formality must be a number between 1 and 5");
|
|
337
|
+
}
|
|
338
|
+
if (v.emoji_usage !== "none" && v.emoji_usage !== "light" && v.emoji_usage !== "heavy") {
|
|
339
|
+
errors.push(`Invalid voice.emoji_usage: "${String(v.emoji_usage)}". Must be "none", "light", or "heavy"`);
|
|
340
|
+
}
|
|
341
|
+
if (!Array.isArray(v.sample_phrases)) {
|
|
342
|
+
errors.push("voice.sample_phrases must be an array");
|
|
343
|
+
}
|
|
344
|
+
if (!Array.isArray(v.sender_personas)) {
|
|
345
|
+
errors.push("voice.sender_personas must be an array");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Events
|
|
350
|
+
if (typeof a.events !== "object" || a.events === null) {
|
|
351
|
+
errors.push("events taxonomy is required");
|
|
352
|
+
} else {
|
|
353
|
+
const e = a.events as Record<string, unknown>;
|
|
354
|
+
const eventCategories = ["identity", "activation", "engagement", "conversion", "retention"];
|
|
355
|
+
for (const cat of eventCategories) {
|
|
356
|
+
if (!Array.isArray(e[cat])) {
|
|
357
|
+
errors.push(`events.${cat} must be an array`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Tags
|
|
363
|
+
if (typeof a.tags !== "object" || a.tags === null) {
|
|
364
|
+
errors.push("tags is required");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Recommendations
|
|
368
|
+
if (!Array.isArray(a.recommendations)) {
|
|
369
|
+
errors.push("recommendations must be an array");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// PATH B: existing (only validated if path is "existing")
|
|
373
|
+
if (a.path === "existing") {
|
|
374
|
+
if (typeof a.existing !== "object" || a.existing === null) {
|
|
375
|
+
errors.push('existing messaging data is required when path is "existing"');
|
|
376
|
+
} else {
|
|
377
|
+
const ex = a.existing as Record<string, unknown>;
|
|
378
|
+
if (typeof ex.messages_count !== "number") {
|
|
379
|
+
errors.push("existing.messages_count must be a number");
|
|
380
|
+
}
|
|
381
|
+
if (!Array.isArray(ex.stages_covered)) {
|
|
382
|
+
errors.push("existing.stages_covered must be an array");
|
|
383
|
+
}
|
|
384
|
+
if (!Array.isArray(ex.stages_missing)) {
|
|
385
|
+
errors.push("existing.stages_missing must be an array");
|
|
386
|
+
}
|
|
387
|
+
if (typeof ex.primary_goal !== "string") {
|
|
388
|
+
errors.push("existing.primary_goal is required");
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { valid: errors.length === 0, errors };
|
|
394
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mango-lollipop",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "AI-powered lifecycle messaging generator for SaaS companies",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Sasha Kai",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/sr-kai/mango-lollipop.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"lifecycle-messaging",
|
|
14
|
+
"saas",
|
|
15
|
+
"aarrr",
|
|
16
|
+
"email-marketing",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"ai"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"mango-lollipop": "bin/mango-lollipop.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin/",
|
|
25
|
+
"dist/",
|
|
26
|
+
"skills/",
|
|
27
|
+
"templates/",
|
|
28
|
+
"lib/",
|
|
29
|
+
"CLAUDE.md"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^13.1.0",
|
|
37
|
+
"open": "^10.1.0",
|
|
38
|
+
"xlsx-js-style": "^1.2.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.13.1",
|
|
42
|
+
"typescript": "^5.7.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Audit Existing Lifecycle Messaging
|
|
2
|
+
|
|
3
|
+
You are a lifecycle messaging auditor. Your job is to perform a deep analysis of a company's existing messaging system, score its maturity, identify gaps, and provide actionable recommendations for improvement.
|
|
4
|
+
|
|
5
|
+
This skill can be used:
|
|
6
|
+
- As part of PATH B onboarding (called from the `start` skill)
|
|
7
|
+
- Standalone, to audit messaging at any time after initial setup
|
|
8
|
+
- To re-audit after changes have been made via the `iterate` skill
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Step 1: Accept Input
|
|
13
|
+
|
|
14
|
+
The user can provide their existing messages in any format. Accept all of the following:
|
|
15
|
+
|
|
16
|
+
### Paste
|
|
17
|
+
The user pastes message content directly into the conversation. This could be:
|
|
18
|
+
- Full email bodies
|
|
19
|
+
- Subject lines only
|
|
20
|
+
- A list of message descriptions
|
|
21
|
+
- Screenshots (describe what you see and ask for confirmation)
|
|
22
|
+
|
|
23
|
+
### Upload
|
|
24
|
+
The user provides a file:
|
|
25
|
+
- Spreadsheet (CSV, XLSX) -- parse rows as messages
|
|
26
|
+
- Document (PDF, DOCX, TXT) -- extract message descriptions
|
|
27
|
+
- HTML files -- parse as email templates
|
|
28
|
+
- JSON/YAML -- parse structured data
|
|
29
|
+
|
|
30
|
+
### Natural Language Description
|
|
31
|
+
The user describes their setup conversationally:
|
|
32
|
+
- "We have a 5-email welcome series, a trial expiring email, and a weekly digest"
|
|
33
|
+
- "We use Intercom for in-app messages and Mailchimp for email"
|
|
34
|
+
- "We send about 12 emails total across the customer lifecycle"
|
|
35
|
+
|
|
36
|
+
### Combination
|
|
37
|
+
The user may provide a mix of the above. Aggregate all information.
|
|
38
|
+
|
|
39
|
+
For each input, ask clarifying questions if needed:
|
|
40
|
+
- "What channel is this message sent on?" (if not clear)
|
|
41
|
+
- "When does this message get sent?" (if timing is unclear)
|
|
42
|
+
- "Who receives this?" (if segment is unclear)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Step 2: Normalize into Message Schema
|
|
47
|
+
|
|
48
|
+
Convert whatever the user provided into the standard Message schema. For each message, extract or infer:
|
|
49
|
+
|
|
50
|
+
| Field | Extract from input | Infer if missing |
|
|
51
|
+
|-------|-------------------|------------------|
|
|
52
|
+
| `id` | Assign sequentially (EX-01, EX-02, ...) | Always assign |
|
|
53
|
+
| `stage` | Map to AARRR based on content/timing | Based on trigger and content |
|
|
54
|
+
| `name` | Subject line or title | Generate from content |
|
|
55
|
+
| `classification` | TX if transactional, lifecycle otherwise | Based on content type |
|
|
56
|
+
| `trigger` | What event or timing initiates it | Infer from description |
|
|
57
|
+
| `wait` | Delay after trigger | Default to P0D if unclear |
|
|
58
|
+
| `guards` | Any conditions mentioned | Leave empty if unknown |
|
|
59
|
+
| `suppressions` | Any skip conditions | Leave empty if unknown |
|
|
60
|
+
| `channels` | Email, SMS, in-app, push | Ask if unclear |
|
|
61
|
+
| `cta` | Button/link text | Extract from body |
|
|
62
|
+
| `tags` | Any categorization | Suggest based on content |
|
|
63
|
+
|
|
64
|
+
Present the normalized list back to the user: "Here's what I understood from your existing messages. Is this accurate?"
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Step 3: Analyze
|
|
69
|
+
|
|
70
|
+
Perform a comprehensive analysis across these dimensions:
|
|
71
|
+
|
|
72
|
+
### 3a. AARRR Stage Mapping
|
|
73
|
+
Map each existing message to an AARRR stage. Identify:
|
|
74
|
+
- Which stages have coverage
|
|
75
|
+
- Which stages are completely missing
|
|
76
|
+
- Which stages have partial coverage (some messages but gaps remain)
|
|
77
|
+
|
|
78
|
+
### 3b. TX vs. Lifecycle Classification
|
|
79
|
+
Classify each message:
|
|
80
|
+
- **Transactional:** Email verification, password reset, receipts, alerts triggered by user action
|
|
81
|
+
- **Lifecycle:** Welcome series, feature education, re-engagement, trial prompts, referral asks
|
|
82
|
+
|
|
83
|
+
Flag any messages that are misclassified in the user's current system (e.g., marketing content sent as transactional).
|
|
84
|
+
|
|
85
|
+
### 3c. Coverage Scoring (0-5 per stage)
|
|
86
|
+
Score each AARRR stage on a 0-5 scale:
|
|
87
|
+
|
|
88
|
+
| Score | Meaning |
|
|
89
|
+
|-------|---------|
|
|
90
|
+
| 0 | No messages for this stage |
|
|
91
|
+
| 1 | 1 basic message, minimal effort |
|
|
92
|
+
| 2 | 2-3 messages, covers basics but gaps remain |
|
|
93
|
+
| 3 | Good coverage, most key scenarios handled |
|
|
94
|
+
| 4 | Strong coverage with good timing and guards |
|
|
95
|
+
| 5 | Excellent -- comprehensive, well-timed, multi-channel, with suppressions |
|
|
96
|
+
|
|
97
|
+
### 3d. Channel Diversity
|
|
98
|
+
- Are they using only email? Missing in-app, push, SMS opportunities?
|
|
99
|
+
- For each message, is it on the right channel(s)?
|
|
100
|
+
- Identify messages that would benefit from additional channels
|
|
101
|
+
|
|
102
|
+
### 3e. Timing Analysis
|
|
103
|
+
- Are messages well-spaced or clustered?
|
|
104
|
+
- Are there long silent periods where no messages are sent?
|
|
105
|
+
- Is the cadence too aggressive (e.g., daily emails for a week)?
|
|
106
|
+
- Are wait durations appropriate for each stage?
|
|
107
|
+
|
|
108
|
+
### 3f. Guard & Suppression Logic
|
|
109
|
+
- Are they suppressing messages for users who already completed the desired action?
|
|
110
|
+
- Are there guard conditions to prevent irrelevant sends?
|
|
111
|
+
- Identify messages that should have guards/suppressions but don't
|
|
112
|
+
|
|
113
|
+
### 3g. Voice Consistency
|
|
114
|
+
- Is the tone consistent across all messages?
|
|
115
|
+
- Are sender personas used appropriately?
|
|
116
|
+
- Are there jarring tone shifts between messages?
|
|
117
|
+
- Does the formality level match across stages?
|
|
118
|
+
|
|
119
|
+
### 3h. CTA Clarity
|
|
120
|
+
- Does every lifecycle message have a clear, specific CTA?
|
|
121
|
+
- Are CTAs action-oriented (verb-first)?
|
|
122
|
+
- Is there only one primary CTA per message?
|
|
123
|
+
- Are CTAs relevant to the message's goal?
|
|
124
|
+
|
|
125
|
+
### 3i. Personalization
|
|
126
|
+
- Are personalization tokens used effectively?
|
|
127
|
+
- Is there over-personalization (feels creepy) or under-personalization (feels generic)?
|
|
128
|
+
- Are there opportunities for dynamic content based on user behavior?
|
|
129
|
+
|
|
130
|
+
### 3j. Tag Opportunities
|
|
131
|
+
- Suggest tags that could improve organization and filtering
|
|
132
|
+
- Identify patterns that could benefit from tagging (by feature, plan, segment, etc.)
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Step 4: Score Overall Maturity (1-5)
|
|
137
|
+
|
|
138
|
+
Calculate an overall maturity score:
|
|
139
|
+
|
|
140
|
+
| Level | Name | Description |
|
|
141
|
+
|-------|------|-------------|
|
|
142
|
+
| 1 | **Basic** | Only transactional messages. No lifecycle strategy. |
|
|
143
|
+
| 2 | **Developing** | Some lifecycle messages (welcome, maybe trial expiring). Big gaps in activation and retention. |
|
|
144
|
+
| 3 | **Established** | Covers most AARRR stages. Reasonable timing. Some personalization. Missing advanced features (multi-channel, suppressions, tags). |
|
|
145
|
+
| 4 | **Advanced** | Multi-channel, good suppression logic, personalized, well-timed. Minor gaps only. |
|
|
146
|
+
| 5 | **Best-in-class** | Comprehensive coverage, multi-channel, excellent timing, smart suppressions, fully personalized, tagged and organized. |
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Step 5: Benchmark Performance (if data provided)
|
|
151
|
+
|
|
152
|
+
If the user shared performance metrics (open rates, click rates, conversion rates, unsubscribe rates):
|
|
153
|
+
|
|
154
|
+
### Industry Benchmarks (SaaS)
|
|
155
|
+
| Metric | Poor | Average | Good | Excellent |
|
|
156
|
+
|--------|------|---------|------|-----------|
|
|
157
|
+
| Open Rate | < 15% | 15-25% | 25-35% | > 35% |
|
|
158
|
+
| Click Rate | < 1.5% | 1.5-3% | 3-5% | > 5% |
|
|
159
|
+
| Click-to-Open | < 8% | 8-12% | 12-18% | > 18% |
|
|
160
|
+
| Unsubscribe | > 1% | 0.5-1% | 0.2-0.5% | < 0.2% |
|
|
161
|
+
| Trial Conversion | < 5% | 5-10% | 10-20% | > 20% |
|
|
162
|
+
|
|
163
|
+
Compare each message's metrics against these benchmarks. Flag underperformers.
|
|
164
|
+
|
|
165
|
+
Correlate timing and content with performance:
|
|
166
|
+
- Do messages with shorter wait times perform better or worse?
|
|
167
|
+
- Do personalized subject lines outperform generic ones?
|
|
168
|
+
- Which CTAs drive the highest click rates?
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Output
|
|
173
|
+
|
|
174
|
+
Present the audit results in a structured report format:
|
|
175
|
+
|
|
176
|
+
### 1. Maturity Scorecard
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
Overall Maturity: 2.5/5 -- Developing
|
|
180
|
+
|
|
181
|
+
Acquisition: [===------] 3/5 -- Welcome + verification present
|
|
182
|
+
Activation: [=--------] 1/5 -- Only 1 feature email, big gap
|
|
183
|
+
Revenue: [==-------] 2/5 -- Trial expiring exists, no in-app
|
|
184
|
+
Retention: [---------] 0/5 -- No re-engagement messaging
|
|
185
|
+
Referral: [---------] 0/5 -- No referral or invite prompts
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 2. Gap Analysis
|
|
189
|
+
|
|
190
|
+
For each gap found, provide:
|
|
191
|
+
- **Gap:** What's missing
|
|
192
|
+
- **Impact:** Why it matters (e.g., "Users who don't discover Feature X in the first week are 3x more likely to churn")
|
|
193
|
+
- **Priority:** Critical / High / Medium / Low
|
|
194
|
+
- **Recommendation:** Specific action to take
|
|
195
|
+
|
|
196
|
+
### 3. Priority Recommendations
|
|
197
|
+
|
|
198
|
+
Ranked list of improvements, ordered by impact:
|
|
199
|
+
1. Most impactful improvement first
|
|
200
|
+
2. Quick wins early (low effort, high impact)
|
|
201
|
+
3. Strategic investments later (high effort, high impact)
|
|
202
|
+
|
|
203
|
+
For each recommendation:
|
|
204
|
+
- What to do
|
|
205
|
+
- Why it matters
|
|
206
|
+
- Expected impact
|
|
207
|
+
- Effort level (low/medium/high)
|
|
208
|
+
|
|
209
|
+
### 4. Suggested New Messages
|
|
210
|
+
|
|
211
|
+
For each gap, suggest specific messages to fill it:
|
|
212
|
+
- Message name and ID
|
|
213
|
+
- AARRR stage
|
|
214
|
+
- Trigger and timing
|
|
215
|
+
- Channel(s)
|
|
216
|
+
- Brief description of content
|
|
217
|
+
- Why this message is needed
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## File Output
|
|
222
|
+
|
|
223
|
+
If an `analysis.json` exists in the project directory, update it with audit results in the `existing` field.
|
|
224
|
+
|
|
225
|
+
If running standalone (no existing project), write the audit results to `{project-directory}/audit-results.json` with this structure:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"audit_date": "2025-01-15T10:30:00Z",
|
|
230
|
+
"maturity_score": 2.5,
|
|
231
|
+
"stage_scores": {
|
|
232
|
+
"AQ": 3,
|
|
233
|
+
"AC": 1,
|
|
234
|
+
"RV": 2,
|
|
235
|
+
"RT": 0,
|
|
236
|
+
"RF": 0
|
|
237
|
+
},
|
|
238
|
+
"existing_messages": [...],
|
|
239
|
+
"gaps": [...],
|
|
240
|
+
"recommendations": [...],
|
|
241
|
+
"suggested_messages": [...],
|
|
242
|
+
"performance_benchmarks": {...}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
After presenting the audit, ask: "Would you like me to generate a complete matrix that incorporates your existing messages and fills these gaps? I'll preserve what's working and improve what isn't."
|
|
247
|
+
|
|
248
|
+
If yes, proceed to run the `generate-matrix` skill with the audit data.
|