@ever-works/whatsapp-channel-plugin 1.0.0 → 1.0.4

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/dist/index.js ADDED
@@ -0,0 +1,957 @@
1
+ // ../../../node_modules/.pnpm/@ever-co+connect-contracts@1.0.0-rc.2/node_modules/@ever-co/connect-contracts/dist/index.js
2
+ var PROBLEM_CODES = [
3
+ "validation_failed",
4
+ "not_found",
5
+ "unauthorized",
6
+ "forbidden_role",
7
+ "forbidden_scope",
8
+ "ambiguous_credentials",
9
+ "credential_revoked",
10
+ "instance_disconnected",
11
+ "instance_pending_approval",
12
+ "not_connection_owner",
13
+ "integration_disabled",
14
+ "integration_revoked",
15
+ "denied_by_policy",
16
+ "integration_not_available",
17
+ "entitlement_required",
18
+ "tier_required",
19
+ "limit_exceeded",
20
+ "rate_limited",
21
+ "idempotency_mismatch",
22
+ "idempotency_in_progress",
23
+ "stale_version",
24
+ "module_disabled",
25
+ "handle_taken",
26
+ "handle_reserved",
27
+ "handle_retired",
28
+ "handle_invalid",
29
+ "handle_held",
30
+ "handle_held_by_you",
31
+ "code_invalid",
32
+ "code_expired",
33
+ "product_mismatch",
34
+ "product_not_supported",
35
+ "key_mismatch",
36
+ "signature_invalid",
37
+ "unlinked_org",
38
+ "schema_violation",
39
+ "mirror_owned_field",
40
+ "illegal_transition",
41
+ "already_exists",
42
+ "already_completed",
43
+ "billing_active",
44
+ "sso_required",
45
+ "reauth_required",
46
+ "session_required",
47
+ "step_up_required",
48
+ "session_bound",
49
+ "authorization_pending",
50
+ "slow_down",
51
+ "expired_token",
52
+ "already_connected",
53
+ "already_linked",
54
+ "already_claimed",
55
+ "invalid_client",
56
+ "public_jwk_invalid",
57
+ "identifier_claimed",
58
+ "salt_version_unknown",
59
+ "salt_version_retired",
60
+ "hashes_invalid",
61
+ "lookup_key_required",
62
+ "required_component",
63
+ "resync_required",
64
+ "last_owner",
65
+ "last_auth_method",
66
+ "instances_linked",
67
+ "install_in_progress",
68
+ "slug_reserved",
69
+ "checklist_incomplete",
70
+ "scope_version_outdated",
71
+ "method_not_allowed",
72
+ "payload_too_large",
73
+ "unsupported_media_type",
74
+ "gone",
75
+ "unavailable",
76
+ "upstream",
77
+ "database_absent",
78
+ "database_unreachable",
79
+ "topic_private",
80
+ "keys_unavailable",
81
+ "internal_error"
82
+ ];
83
+ new Set(PROBLEM_CODES);
84
+
85
+ // ../../contracts/dist/index.js
86
+ var USER_SELECTABLE_WORK_KINDS = [
87
+ "website",
88
+ "landing-page",
89
+ "blog",
90
+ "directory",
91
+ "awesome-repo",
92
+ "repo",
93
+ "app"
94
+ ];
95
+ var WORK_KINDS = [...USER_SELECTABLE_WORK_KINDS, "company", "campaign", "default"];
96
+ new Set(WORK_KINDS);
97
+ var GOAL_KINDS = ["metric", "delivery"];
98
+ new Set(GOAL_KINDS);
99
+ var VERSION = "1.0.0";
100
+ var GTM_SKILLS = [
101
+ // ── research ────────────────────────────────────────────────────────
102
+ {
103
+ slug: "lead-research",
104
+ title: "Lead research",
105
+ description: "Build a candidate lead list from seed criteria and public sources, recording a supporting source for every contact.",
106
+ stage: "research",
107
+ tags: ["gtm", "sales", "research"],
108
+ version: VERSION,
109
+ inputs: [
110
+ { key: "icp", description: "Ideal-customer description: segment, size, geography, role titles." },
111
+ { key: "seed_sources", description: "Seed lists, domains, or saved searches to start from." },
112
+ { key: "max_leads", description: "Hard cap on how many candidates this run may collect." }
113
+ ],
114
+ outputs: [
115
+ { key: "contacts", description: "Candidate contacts with name, company, title, and a source reference." }
116
+ ],
117
+ allowedTools: ["search", "content-extractor"],
118
+ body: [
119
+ "Build a candidate lead list from the supplied criteria and seed sources.",
120
+ "",
121
+ "## Rules",
122
+ "- NEVER invent a contact detail. An email address, phone number, or title is recorded only",
123
+ " when a source supports it; otherwise leave the field empty.",
124
+ "- Record `source` on every contact \u2014 the URL or seed list the entry came from. A contact",
125
+ " without a source is not a lead, it is a guess.",
126
+ "- Respect `max_leads` exactly. Stop collecting when the cap is reached and say how many",
127
+ " candidates were left unexamined.",
128
+ "- De-duplicate by email when present, otherwise by (company, name).",
129
+ "- Exclude anyone who has asked not to be contacted, and any source that forbids collection.",
130
+ "",
131
+ "## Output",
132
+ "Write `contacts`. Each entry carries `name`, and any of `email`, `company`, `title`,",
133
+ "`source`, `notes` that a source supports. Report the counts examined, kept, and dropped,",
134
+ "with the reason for each drop category."
135
+ ].join("\n")
136
+ },
137
+ {
138
+ slug: "competitor-watch",
139
+ title: "Competitor watch",
140
+ description: "Monitor a watchlist of companies across public sources and emit dated, source-linked change signals.",
141
+ stage: "research",
142
+ tags: ["gtm", "marketing", "research"],
143
+ version: VERSION,
144
+ inputs: [
145
+ { key: "watchlist", description: "Companies to monitor: name, website, public repositories." },
146
+ { key: "focus_areas", description: "What to watch: pricing, features, positioning, hiring." },
147
+ { key: "since", description: "Only report changes observed after this date." }
148
+ ],
149
+ outputs: [{ key: "signals", description: "Dated observations with a source URL and the focus area they hit." }],
150
+ allowedTools: ["search", "content-extractor"],
151
+ body: [
152
+ "Monitor the watchlist across public sources and report what changed.",
153
+ "",
154
+ "## Rules",
155
+ "- Public sources only: published pages, public announcements, public repositories.",
156
+ "- Every signal carries a source URL and an observation date. No date or no URL means the",
157
+ " signal is not reportable.",
158
+ "- Separate FACT (what the source says) from ANALYSIS (what you infer). Never blend them in",
159
+ " the same sentence.",
160
+ "- Report only changes since `since`. If a focus area produced nothing, say so explicitly \u2014",
161
+ " an empty period is a finding, not a gap to fill.",
162
+ "- Do not speculate about unannounced plans, internal metrics, or private information.",
163
+ "",
164
+ "## Output",
165
+ "Write `signals`. Each entry carries `title`, `url`, `publishedDate`, the focus area, and a",
166
+ "one-line factual summary."
167
+ ].join("\n")
168
+ },
169
+ {
170
+ slug: "news-signal-detection",
171
+ title: "News signal detection",
172
+ description: "Scan configured topics for newsworthy events and rank them by relevance to the campaign, discarding noise.",
173
+ stage: "research",
174
+ tags: ["gtm", "marketing", "research"],
175
+ version: VERSION,
176
+ inputs: [
177
+ { key: "topics", description: "Topics, keywords, or accounts to scan." },
178
+ { key: "lookback_days", description: "How far back the scan reaches." },
179
+ { key: "relevance_threshold", description: "Minimum relevance (0-1) an item must clear to be kept." }
180
+ ],
181
+ outputs: [{ key: "signals", description: "Relevance-ranked news items with source URL and publish date." }],
182
+ allowedTools: ["search", "content-extractor"],
183
+ body: [
184
+ "Scan the configured topics and keep only what is genuinely newsworthy for this campaign.",
185
+ "",
186
+ "## Rules",
187
+ "- Score each candidate 0-1 for relevance to the campaign brief and drop anything below",
188
+ " `relevance_threshold`. State the score and the one-line reason for every item you keep.",
189
+ "- Prefer primary sources over aggregators; when both exist, link the primary.",
190
+ "- Collapse duplicate coverage of the same event into one signal with the best source.",
191
+ "- Nothing older than `lookback_days`. Undated items are dropped, not guessed at.",
192
+ "- Never restate a headline as fact when the source hedges it \u2014 carry the hedge through.",
193
+ "",
194
+ "## Output",
195
+ "Write `signals` ordered by descending relevance, each with `title`, `url`, `publishedDate`,",
196
+ "the relevance score, and the reason it matters here."
197
+ ].join("\n")
198
+ },
199
+ // ── qualify ─────────────────────────────────────────────────────────
200
+ {
201
+ slug: "lead-scoring",
202
+ title: "Lead scoring",
203
+ description: "Score contacts 0-100 against a declarative weight table and attach an explainable reason per point awarded.",
204
+ stage: "qualify",
205
+ tags: ["gtm", "sales", "scoring"],
206
+ version: VERSION,
207
+ inputs: [
208
+ { key: "contacts", description: "Candidate contacts to score." },
209
+ { key: "score_weights", description: "Weight table: signal name \u2192 points awarded." }
210
+ ],
211
+ outputs: [
212
+ { key: "scored_contacts", description: "Contacts with `score` (0-100) and `scoreReasons` per contact." }
213
+ ],
214
+ allowedTools: [],
215
+ body: [
216
+ "Score every contact against the supplied weight table.",
217
+ "",
218
+ "## Rules",
219
+ "- The weight table is the whole model. Award points ONLY for signals it names; never invent",
220
+ " a bonus, and never award points for a signal the contact data does not actually support.",
221
+ "- Clamp the final score to 0-100. Record one `scoreReasons` line per point awarded, in the",
222
+ " form `<signal> +<points>`, so any score can be re-derived by hand.",
223
+ "- A missing field is worth zero points, never an assumed value.",
224
+ "- Scoring is deterministic: the same contact and the same weight table must always produce",
225
+ " the same score. Do not break ties with judgement calls \u2014 leave ties tied.",
226
+ "",
227
+ "## Output",
228
+ "Write `scored_contacts` sorted by descending score, then report the score distribution and",
229
+ "the three signals that moved the list most."
230
+ ].join("\n")
231
+ },
232
+ {
233
+ slug: "risk-filter",
234
+ title: "Risk filter",
235
+ description: "Flag low-quality or unsafe contacts against a risk weight table and exclude those at or above the threshold.",
236
+ stage: "qualify",
237
+ tags: ["gtm", "sales", "safety"],
238
+ version: VERSION,
239
+ inputs: [
240
+ { key: "scored_contacts", description: "Contacts already carrying a priority score." },
241
+ { key: "risk_weights", description: "Risk weight table: risk signal \u2192 points." },
242
+ { key: "risk_threshold", description: "Risk score at or above which a contact is excluded." }
243
+ ],
244
+ outputs: [
245
+ {
246
+ key: "scored_contacts",
247
+ description: "The same contacts, now carrying `riskScore`, `riskReasons`, and an excluded flag."
248
+ }
249
+ ],
250
+ allowedTools: [],
251
+ body: [
252
+ "Assess each scored contact for risk and mark \u2014 never silently delete \u2014 the excluded ones.",
253
+ "",
254
+ "## Rules",
255
+ "- Apply the risk weight table exactly as given; record one `riskReasons` line per point.",
256
+ "- Contacts at or above `risk_threshold` are EXCLUDED from outbound, but they stay in the",
257
+ " data set with their reasons attached so a human can overturn the call.",
258
+ '- Never drop a contact without a recorded reason. "Looked wrong" is not a reason.',
259
+ "- Risk is about deliverability and safety (unverifiable identity, throwaway domains,",
260
+ " do-not-contact signals) \u2014 it is not a second opinion on fit. Fit is `lead-scoring`.",
261
+ "",
262
+ "## Output",
263
+ "Write the enriched `scored_contacts` and report how many were excluded, grouped by the",
264
+ "dominant risk reason."
265
+ ].join("\n")
266
+ },
267
+ // ── draft ───────────────────────────────────────────────────────────
268
+ {
269
+ slug: "outreach-personalization",
270
+ title: "Outreach personalization",
271
+ description: "Write one 80-120 word personalized outbound draft per qualified contact, grounded only in known facts.",
272
+ stage: "draft",
273
+ tags: ["gtm", "sales", "writing"],
274
+ version: VERSION,
275
+ inputs: [
276
+ { key: "scored_contacts", description: "Qualified, non-excluded contacts to write for." },
277
+ { key: "campaign_brief", description: "Offer, proof points, and the single ask." },
278
+ { key: "tone", description: "Configured tone for the campaign." }
279
+ ],
280
+ outputs: [{ key: "drafts", description: "One draft per contact with `ref`, `channel`, `subject`, `body`." }],
281
+ allowedTools: [],
282
+ body: [
283
+ "Write one outbound draft per qualified contact.",
284
+ "",
285
+ "## Rules",
286
+ "- HARD CONSTRAINT: drafts only. Nothing is sent, scheduled, or queued for delivery here.",
287
+ '- 80-120 words in the body. One ask. No multi-part asks, no "quick question" openers that',
288
+ " the rest of the message contradicts.",
289
+ "- Personalize ONLY from fields and signals actually present on the contact. If there is no",
290
+ " personalization material, write the generic version and say so \u2014 a fabricated detail is",
291
+ " worse than a generic opener.",
292
+ "- Write a subject line only for channels that carry one.",
293
+ "- No invented metrics, customer names, or mutual connections. Ever.",
294
+ "",
295
+ "## Output",
296
+ "Write `drafts`, ordered by descending contact score so the review gate sees the best",
297
+ "candidates first. Each draft carries a stable `ref` used later by review and the action log."
298
+ ].join("\n")
299
+ },
300
+ {
301
+ slug: "newsletter-drafting",
302
+ title: "Newsletter drafting",
303
+ description: "Turn briefs, notes, and collected signals into a scannable newsletter issue with one clear call to action.",
304
+ stage: "draft",
305
+ tags: ["gtm", "marketing", "content", "writing"],
306
+ version: VERSION,
307
+ inputs: [
308
+ { key: "campaign_brief", description: "Issue theme, audience, and the call to action." },
309
+ { key: "signals", description: "Collected source material for the issue." },
310
+ { key: "tone", description: "Configured editorial tone." }
311
+ ],
312
+ outputs: [{ key: "drafts", description: "A single newsletter draft: subject line plus sectioned body." }],
313
+ allowedTools: ["content-extractor"],
314
+ body: [
315
+ "Draft one newsletter issue from the brief and the collected material.",
316
+ "",
317
+ "## Rules",
318
+ "- Draft-first: the issue is produced for human review. Never send or schedule it.",
319
+ "- Structure: subject line, one-sentence hook, 3-5 short sections, one call to action.",
320
+ " Scannable beats comprehensive.",
321
+ "- Every claim traces to the brief, the knowledge base, or a supplied signal. Link the",
322
+ " source when the section rests on one.",
323
+ "- Never invent metrics, quotes, or customer names.",
324
+ "- When the source material is thin, say so and list what extra input would raise quality",
325
+ " rather than padding the issue.",
326
+ "",
327
+ "## Output",
328
+ "Write `drafts` with a single entry: `subject` plus the sectioned `body`."
329
+ ].join("\n")
330
+ },
331
+ {
332
+ slug: "social-scheduling",
333
+ title: "Social scheduling",
334
+ description: "Plan a channel-fit social calendar and stage each post for review, respecting cadence and angle variety.",
335
+ stage: "draft",
336
+ tags: ["gtm", "marketing", "social", "writing"],
337
+ version: VERSION,
338
+ inputs: [
339
+ { key: "campaign_brief", description: "Campaign themes, channels, and cadence." },
340
+ { key: "signals", description: "Source material and trends to build posts from." },
341
+ { key: "cadence", description: "Posts per channel per period." }
342
+ ],
343
+ outputs: [
344
+ { key: "drafts", description: "Channel-fit post drafts, each with its planned slot in the calendar." }
345
+ ],
346
+ allowedTools: [],
347
+ body: [
348
+ "Plan the social calendar for the cadence window and draft each post.",
349
+ "",
350
+ "## Rules",
351
+ "- Draft-first: posts are STAGED for review. Never publish, never schedule to a live channel.",
352
+ "- Fit each draft to its channel: length, hashtag conventions, and link placement differ and",
353
+ " a cross-posted body is a tell.",
354
+ "- Do not repeat the same angle inside one cadence window. Track the angle per slot and vary",
355
+ " it deliberately.",
356
+ "- Build only from the brief, the content Works, and supplied signals. Never invent product",
357
+ " claims or engagement numbers.",
358
+ "- Respect `cadence` exactly \u2014 proposing more slots than the operator configured is not",
359
+ " ambition, it is a spam risk.",
360
+ "",
361
+ "## Output",
362
+ "Write `drafts` with the planned slot (channel + relative day) on each entry, plus a compact",
363
+ "calendar view of the window."
364
+ ].join("\n")
365
+ },
366
+ {
367
+ slug: "digest-compilation",
368
+ title: "Digest compilation",
369
+ description: "Compile collected signals into a recurring digest that separates facts from analysis and highlights change.",
370
+ stage: "draft",
371
+ tags: ["gtm", "marketing", "reporting", "writing"],
372
+ version: VERSION,
373
+ inputs: [
374
+ { key: "signals", description: "Signals collected this period." },
375
+ { key: "previous_digest", description: "The prior issue, used to compute what changed." },
376
+ { key: "focus_areas", description: "Sections the digest must cover." }
377
+ ],
378
+ outputs: [{ key: "drafts", description: "A digest draft: sections, change highlights, and a trend view." }],
379
+ allowedTools: [],
380
+ body: [
381
+ "Compile the period digest from the collected signals.",
382
+ "",
383
+ "## Rules",
384
+ "- One section per focus area, in the configured order, every issue. A stable shape is what",
385
+ " makes a recurring digest readable.",
386
+ "- Every section splits FACTS (sourced, dated, linked) from ANALYSIS (your interpretation),",
387
+ " under those labels.",
388
+ "- Lead with what CHANGED since `previous_digest`. Unchanged areas get one line, not a",
389
+ " restatement of last issue.",
390
+ "- No fabrication and no padding: if a focus area produced no signal this period, write",
391
+ " exactly that.",
392
+ "- Close with a short trend view over the recent periods \u2014 direction, not prophecy.",
393
+ "",
394
+ "## Output",
395
+ "Write `drafts` with a single digest entry for review."
396
+ ].join("\n")
397
+ },
398
+ // ── act ─────────────────────────────────────────────────────────────
399
+ {
400
+ slug: "crm-sync-hygiene",
401
+ title: "CRM sync hygiene",
402
+ description: "Prepare reviewed records for a customer-record sync: normalized fields, no duplicates, no destructive overwrites.",
403
+ stage: "act",
404
+ tags: ["gtm", "sales", "operations"],
405
+ version: VERSION,
406
+ inputs: [
407
+ { key: "approved_drafts", description: "Records cleared by the review gate." },
408
+ { key: "field_map", description: "Local field \u2192 destination field mapping." }
409
+ ],
410
+ outputs: [
411
+ { key: "action_log", description: "Prepared record writes with the field diff and the reason for each." }
412
+ ],
413
+ allowedTools: ["connector"],
414
+ body: [
415
+ "Prepare reviewed records for the destination system without losing data.",
416
+ "",
417
+ "## Rules",
418
+ "- Only records that passed the review gate are eligible. Nothing else, no exceptions.",
419
+ "- NEVER overwrite a populated destination field with an empty local value. Blank does not",
420
+ ' mean "clear it", it means "we do not know".',
421
+ "- Match before you create: search by the destination key (email, then domain + name) and",
422
+ " update the existing record rather than minting a duplicate.",
423
+ "- Normalize on the way out \u2014 casing, phone format, domain form \u2014 per `field_map`.",
424
+ "- Record every intended write in `action_log` with its field-level diff BEFORE it is",
425
+ " applied, so the change is auditable and reversible.",
426
+ "",
427
+ "## Output",
428
+ "Write `action_log` with one entry per record, status `prepared` or `skipped`, and the",
429
+ "reason for every skip."
430
+ ].join("\n")
431
+ },
432
+ // ── follow-up ───────────────────────────────────────────────────────
433
+ {
434
+ slug: "follow-up-cadence",
435
+ title: "Follow-up cadence",
436
+ description: "Schedule timed re-engagement for quiet threads, with strict caps and an unconditional stop on any reply.",
437
+ stage: "follow-up",
438
+ tags: ["gtm", "sales", "cadence"],
439
+ version: VERSION,
440
+ inputs: [
441
+ { key: "action_log", description: "What was prepared, for whom, and when." },
442
+ { key: "reply_state", description: "Which threads have received a reply." },
443
+ { key: "cadence", description: "Follow-up spacing and the maximum number of touches." }
444
+ ],
445
+ outputs: [
446
+ { key: "follow_up_queue", description: "Queued follow-ups with a due offset and the rationale for each." }
447
+ ],
448
+ allowedTools: [],
449
+ body: [
450
+ "Queue follow-ups for threads that have gone quiet.",
451
+ "",
452
+ "## Rules",
453
+ "- HARD STOP on any reply, bounce, or opt-out: that thread leaves the cadence immediately and",
454
+ " permanently. This overrides every other rule here.",
455
+ "- Never exceed the configured maximum touches per contact, and never shorten the configured",
456
+ " spacing to fit more in.",
457
+ "- Each follow-up must add something \u2014 a new angle, a new proof point, a genuine deadline.",
458
+ ' "Bumping this to the top of your inbox" is not a follow-up.',
459
+ "- Follow-ups are drafts like any other outbound: they re-enter the review gate before",
460
+ " anything is prepared for delivery.",
461
+ "",
462
+ "## Output",
463
+ "Write `follow_up_queue` with `draftRef`, `channel`, `dueAfterDays`, and the rationale."
464
+ ].join("\n")
465
+ },
466
+ {
467
+ slug: "reply-detection",
468
+ title: "Reply detection",
469
+ description: "Classify inbound replies into interested / not-now / decline / opt-out and route each to the right next step.",
470
+ stage: "follow-up",
471
+ tags: ["gtm", "sales", "classification"],
472
+ version: VERSION,
473
+ inputs: [
474
+ { key: "inbound_messages", description: "Replies received against prepared outbound." },
475
+ { key: "action_log", description: "The outbound each reply responds to." }
476
+ ],
477
+ outputs: [
478
+ { key: "reply_state", description: "Per-thread classification, confidence, and the routing decision." }
479
+ ],
480
+ allowedTools: ["connector"],
481
+ body: [
482
+ "Classify each inbound reply and decide what happens to its thread.",
483
+ "",
484
+ "## Classes",
485
+ "`interested` \xB7 `not_now` \xB7 `decline` \xB7 `opt_out` \xB7 `auto_reply` \xB7 `unclear`",
486
+ "",
487
+ "## Rules",
488
+ "- `opt_out` and `decline` stop the cadence permanently. When in doubt between `opt_out` and",
489
+ " anything else, choose `opt_out` \u2014 the cost of a wrong stop is far below the cost of a",
490
+ " wrong send.",
491
+ "- `auto_reply` (out-of-office, autoresponder) does NOT count as a reply for cadence",
492
+ " purposes, but it does update the contact record if it names a new owner.",
493
+ "- Use `unclear` rather than guessing; unclear threads route to a human, they do not route",
494
+ " to the next touch.",
495
+ "- Record a confidence value and the phrase that drove the classification.",
496
+ "",
497
+ "## Output",
498
+ "Write `reply_state` with the class, confidence, evidence phrase, and routing decision per",
499
+ "thread."
500
+ ].join("\n")
501
+ },
502
+ // ── enrich ──────────────────────────────────────────────────────────
503
+ {
504
+ slug: "contact-enrichment",
505
+ title: "Contact enrichment",
506
+ description: "Backfill missing contact and account fields from verifiable sources, recording the evidence for each fill.",
507
+ stage: "enrich",
508
+ tags: ["gtm", "sales", "data"],
509
+ version: VERSION,
510
+ inputs: [
511
+ { key: "contacts", description: "Contacts with gaps to fill." },
512
+ { key: "enrichment_fields", description: "Which fields this run is allowed to fill." }
513
+ ],
514
+ outputs: [
515
+ { key: "enriched_contacts", description: "Contacts with filled fields and a source note per filled field." }
516
+ ],
517
+ allowedTools: ["search", "content-extractor", "connector"],
518
+ body: [
519
+ "Fill the requested gaps on each contact \u2014 and only those gaps.",
520
+ "",
521
+ "## Rules",
522
+ "- Evidence-bound: a field is filled ONLY when a source supports it, and the source goes in",
523
+ " the contact notes alongside the value. No source, no fill.",
524
+ "- NEVER guess or pattern-generate an email address. Constructing `first.last@domain` because",
525
+ " it usually works is fabrication, and it is the single most damaging thing this Skill can do.",
526
+ "- Never overwrite an existing non-empty value. Enrichment fills gaps; corrections are a",
527
+ " human decision.",
528
+ "- Only touch fields named in `enrichment_fields`.",
529
+ "- When two sources disagree, leave the field empty and flag the conflict.",
530
+ "",
531
+ "## Output",
532
+ "Write `enriched_contacts` and report fill rate per field plus every conflict flagged."
533
+ ].join("\n")
534
+ },
535
+ // ── measure ─────────────────────────────────────────────────────────
536
+ {
537
+ slug: "seo-audit",
538
+ title: "Search-visibility audit",
539
+ description: "Audit a site or content Work for search visibility and return a prioritized, evidence-cited fix list.",
540
+ stage: "measure",
541
+ tags: ["gtm", "marketing", "seo", "audit"],
542
+ version: VERSION,
543
+ inputs: [
544
+ { key: "pages", description: "Pages or posts in scope, with their current metadata and headings." },
545
+ { key: "target_keywords", description: "Keywords or topics the Work is meant to rank for." },
546
+ { key: "previous_audit", description: "The prior audit, used to report movement and regressions." }
547
+ ],
548
+ outputs: [
549
+ { key: "campaign_report", description: "Prioritized findings with page reference, impact, and fix." }
550
+ ],
551
+ allowedTools: ["content-extractor", "search"],
552
+ body: [
553
+ "Audit the in-scope pages for search visibility and propose fixes.",
554
+ "",
555
+ "## What to check",
556
+ "Title and description metadata \xB7 heading structure and hierarchy \xB7 internal linking and",
557
+ "orphan pages \xB7 keyword coverage against `target_keywords` and the gaps between them \xB7",
558
+ "duplicate or thin content \xB7 canonical and indexability signals.",
559
+ "",
560
+ "## Rules",
561
+ "- Every finding cites the specific page or setting it applies to and states the expected",
562
+ " impact. A finding without a page reference is an opinion.",
563
+ "- Return a prioritized top 5-10, quick wins first \u2014 not an exhaustive dump.",
564
+ "- Propose changes as reviewable edits or tasks. Never modify published pages directly.",
565
+ "- When `previous_audit` is present, report movement honestly, INCLUDING regressions. A",
566
+ " clean report that hides a regression is worse than no report.",
567
+ "- No traffic or ranking projections. Estimate effort and impact qualitatively instead.",
568
+ "",
569
+ "## Output",
570
+ "Write `campaign_report` with the prioritized findings and the movement summary."
571
+ ].join("\n")
572
+ },
573
+ {
574
+ slug: "campaign-reporting",
575
+ title: "Campaign reporting",
576
+ description: "Summarize a campaign period into counted totals, honest insights, and concrete hints for the next cycle.",
577
+ stage: "measure",
578
+ tags: ["gtm", "reporting"],
579
+ version: VERSION,
580
+ inputs: [
581
+ { key: "action_log", description: "What the campaign prepared and skipped." },
582
+ { key: "reply_state", description: "How recipients responded." },
583
+ { key: "scored_contacts", description: "The qualified population the period worked from." }
584
+ ],
585
+ outputs: [{ key: "campaign_report", description: "Totals, insights, and next-cycle variant hints." }],
586
+ allowedTools: [],
587
+ body: [
588
+ "Report the campaign period and close the loop into the next one.",
589
+ "",
590
+ "## Rules",
591
+ "- Totals are COUNTED from the supplied data, never estimated: contacts, qualified, excluded,",
592
+ " drafts, approved, prepared, follow-ups queued.",
593
+ "- State the denominator on every rate. A percentage without its base is not a metric.",
594
+ "- Below roughly 30 observations, report counts and refuse to draw a conclusion \u2014 small",
595
+ " samples produce confident nonsense.",
596
+ "- Insights must be falsifiable and tied to a number in the totals.",
597
+ "- End with `nextVariantHints`: concrete, testable changes for the next draft cycle, one per",
598
+ " line. This is the loop closing \u2014 a report that ends at description wasted the period.",
599
+ "",
600
+ "## Output",
601
+ "Write `campaign_report` with `summary`, `totals`, `insights`, and `nextVariantHints`."
602
+ ].join("\n")
603
+ },
604
+ {
605
+ slug: "engagement-analysis",
606
+ title: "Engagement analysis",
607
+ description: "Compare engagement across variants, channels, and segments, and say which differences are real.",
608
+ stage: "measure",
609
+ tags: ["gtm", "marketing", "reporting"],
610
+ version: VERSION,
611
+ inputs: [
612
+ { key: "engagement_events", description: "Per-item engagement observations for the period." },
613
+ { key: "drafts", description: "The variants the events belong to." },
614
+ { key: "comparison_window", description: "Prior period to compare against." }
615
+ ],
616
+ outputs: [
617
+ { key: "campaign_report", description: "Per-variant and per-segment engagement with a confidence call." }
618
+ ],
619
+ allowedTools: ["metrics"],
620
+ body: [
621
+ "Analyze engagement and report what actually differs.",
622
+ "",
623
+ "## Rules",
624
+ "- Compare like with like: same channel, same audience segment, comparable send window.",
625
+ " Cross-channel comparisons need an explicit caveat.",
626
+ "- Report absolute counts alongside every rate.",
627
+ "- Say plainly when a difference is within noise. Declaring a winner on a handful of",
628
+ " observations is the most common way this analysis misleads a campaign for months.",
629
+ "- Separate what changed (observation) from why (hypothesis), and label hypotheses as such.",
630
+ "- Surface the losing variants too \u2014 a variant that consistently underperforms is the",
631
+ " cheapest thing to stop doing.",
632
+ "",
633
+ "## Output",
634
+ "Write `campaign_report` with the per-variant table, the segment breakdown, and an explicit",
635
+ "confidence statement per comparison."
636
+ ].join("\n")
637
+ }
638
+ ];
639
+ GTM_SKILLS.map((skill) => skill.slug);
640
+ new Map(
641
+ GTM_SKILLS.map((skill) => [skill.slug, skill])
642
+ );
643
+ var FLEET_AGENT_CREDENTIAL_FAMILIES = [
644
+ {
645
+ cli: "claude-code",
646
+ // CLAUDE_CODE_OAUTH_TOKEN (a Claude plan) before ANTHROPIC_API_KEY
647
+ // (per-token Console billing) — deliberately the REVERSE of the CLI's
648
+ // own precedence, so the cheaper credential wins rather than the one
649
+ // that silently spends money.
650
+ envNames: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
651
+ },
652
+ {
653
+ cli: "codex",
654
+ // CODEX_ACCESS_TOKEN draws the ChatGPT workspace entitlement;
655
+ // OPENAI_API_KEY is per-token platform billing.
656
+ envNames: ["CODEX_ACCESS_TOKEN", "OPENAI_API_KEY"]
657
+ }
658
+ ];
659
+ FLEET_AGENT_CREDENTIAL_FAMILIES.flatMap(
660
+ (family) => family.envNames
661
+ );
662
+ var EMAIL_INBOX_CAP_FIELDS = [
663
+ "inboxDailySends",
664
+ "inboxBurstSends",
665
+ "inboxBurstRecipients",
666
+ "recipientsPerMessage"
667
+ ];
668
+ new Set(EMAIL_INBOX_CAP_FIELDS);
669
+ var BACKUP_TRIM_POLICIES = Object.freeze({
670
+ activity: Object.freeze({ field: "createdAt", defaultDays: 365, fullHistoryDays: 1095 }),
671
+ runLogs: Object.freeze({ field: "createdAt", defaultDays: 90, fullHistoryDays: 1095 }),
672
+ terminalTranscripts: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 }),
673
+ notifications: Object.freeze({ field: "createdAt", defaultDays: 180, fullHistoryDays: 1095 }),
674
+ // `occurredAt`, not `createdAt`: `PluginUsageEvent` names its
675
+ // `@CreateDateColumn` `occurredAt` and has no `createdAt` at all. The trim
676
+ // resolver fails OPEN on an unknown column, so the wrong name here did not
677
+ // error — it silently exported every plugin usage event for the lifetime
678
+ // of the workspace, from the archive's single largest history table, while
679
+ // the docs page published a 180-day window.
680
+ pluginUsageEvents: Object.freeze({ field: "occurredAt", defaultDays: 180, fullHistoryDays: 1095 }),
681
+ deliveryLogs: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 }),
682
+ // `firedAt`, for the same reason: `InboundTriggerFire`'s creation
683
+ // timestamp is `firedAt` and it has no `createdAt`, so the declared
684
+ // 90-day window was never applied to `data/schedules/trigger-fires.jsonl`
685
+ // and the `schedules` domain reported `complete` where the docs promise a
686
+ // trim.
687
+ triggerFires: Object.freeze({ field: "firedAt", defaultDays: 90, fullHistoryDays: 1095 }),
688
+ retrievalTrail: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 }),
689
+ fleetJobs: Object.freeze({ field: "createdAt", defaultDays: 30, fullHistoryDays: 365 })
690
+ });
691
+ var BACKUP_DOMAINS = Object.freeze([
692
+ Object.freeze({
693
+ key: "account",
694
+ restorability: "restorable",
695
+ collectorId: "account",
696
+ dataDir: "account"
697
+ }),
698
+ Object.freeze({
699
+ key: "organizations",
700
+ restorability: "restorable",
701
+ collectorId: "organizations",
702
+ dataDir: "organizations"
703
+ }),
704
+ Object.freeze({
705
+ key: "agents",
706
+ restorability: "restorable",
707
+ collectorId: "agents",
708
+ dataDir: "agents"
709
+ }),
710
+ Object.freeze({
711
+ key: "missions",
712
+ restorability: "restorable",
713
+ collectorId: "missions",
714
+ dataDir: "missions"
715
+ }),
716
+ Object.freeze({
717
+ key: "tasks",
718
+ restorability: "restorable",
719
+ collectorId: "tasks",
720
+ dataDir: "tasks"
721
+ }),
722
+ Object.freeze({
723
+ key: "works",
724
+ restorability: "restorable",
725
+ collectorId: "works",
726
+ dataDir: "works"
727
+ }),
728
+ Object.freeze({
729
+ key: "knowledge",
730
+ restorability: "restorable",
731
+ collectorId: "knowledge",
732
+ dataDir: "knowledge"
733
+ }),
734
+ Object.freeze({
735
+ key: "schedules",
736
+ restorability: "restorable",
737
+ collectorId: "schedules",
738
+ dataDir: "schedules"
739
+ }),
740
+ Object.freeze({
741
+ key: "runs",
742
+ restorability: "record-only",
743
+ collectorId: "runs",
744
+ dataDir: "runs"
745
+ }),
746
+ Object.freeze({
747
+ key: "decisions",
748
+ restorability: "record-only",
749
+ collectorId: "decisions",
750
+ dataDir: "decisions"
751
+ }),
752
+ Object.freeze({
753
+ key: "communication",
754
+ restorability: "partly-restorable",
755
+ collectorId: "communication",
756
+ dataDir: "communication"
757
+ }),
758
+ Object.freeze({
759
+ key: "connections",
760
+ restorability: "restorable",
761
+ collectorId: "connections",
762
+ dataDir: "connections"
763
+ }),
764
+ Object.freeze({
765
+ key: "fleet",
766
+ restorability: "partly-restorable",
767
+ collectorId: "fleet",
768
+ dataDir: "fleet"
769
+ }),
770
+ Object.freeze({
771
+ key: "billing",
772
+ restorability: "record-only",
773
+ collectorId: "billing",
774
+ dataDir: "billing"
775
+ }),
776
+ Object.freeze({
777
+ key: "activity",
778
+ restorability: "record-only",
779
+ collectorId: "activity",
780
+ dataDir: "activity",
781
+ trim: BACKUP_TRIM_POLICIES.activity
782
+ })
783
+ ]);
784
+ Object.freeze(BACKUP_DOMAINS.map((domain) => domain.key));
785
+ BACKUP_DOMAINS.length;
786
+ var ACTION_CATEGORIES = Object.freeze([
787
+ "read.internal",
788
+ "read.external",
789
+ "write.internal",
790
+ "write.destructive",
791
+ "message.internal",
792
+ "message.external",
793
+ "publish.external",
794
+ "spend.metered",
795
+ "spend.commitment",
796
+ "access.grant",
797
+ "machine.run",
798
+ "machine.admin",
799
+ "agent.fanout"
800
+ ]);
801
+ Object.freeze(
802
+ ACTION_CATEGORIES.filter((c) => c !== "read.internal")
803
+ );
804
+ var PLUGIN_CAPABILITIES = {
805
+ // Notifications v2 (sibling of EW-650) — Notification Channels.
806
+ // Plugins declare `NOTIFICATION_CHANNEL` (umbrella) plus the
807
+ // channel-specific constant for plugin discovery + UI grouping.
808
+ // See `capabilities/notification-channel.interface.ts`.
809
+ NOTIFICATION_CHANNEL: "notification-channel",
810
+ NOTIFICATION_CHANNEL_WHATSAPP: "notification-channel-whatsapp"};
811
+
812
+ // src/whatsapp-channel-plugin.ts
813
+ var GRAPH_API_BASE = "https://graph.facebook.com";
814
+ var DEFAULT_API_VERSION = "v21.0";
815
+ var PHONE_NUMBER_ID_PATTERN = /^\d{1,32}$/;
816
+ var API_VERSION_PATTERN = /^v\d+\.\d+$/;
817
+ function isValidPhoneNumberId(value) {
818
+ return PHONE_NUMBER_ID_PATTERN.test(value);
819
+ }
820
+ function getTarget(config) {
821
+ const accessToken = config.accessToken;
822
+ const phoneNumberId = config.phoneNumberId;
823
+ const to = config.to;
824
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
825
+ throw new Error("whatsapp-channel: targetConfig.accessToken is required");
826
+ }
827
+ if (typeof phoneNumberId !== "string" || phoneNumberId.length === 0) {
828
+ throw new Error("whatsapp-channel: targetConfig.phoneNumberId is required");
829
+ }
830
+ if (!isValidPhoneNumberId(phoneNumberId)) {
831
+ throw new Error("whatsapp-channel: targetConfig.phoneNumberId must be a numeric id");
832
+ }
833
+ if (typeof to !== "string" || to.length === 0) {
834
+ throw new Error("whatsapp-channel: targetConfig.to (recipient) is required");
835
+ }
836
+ return { accessToken, phoneNumberId, to };
837
+ }
838
+ var WhatsappChannelPlugin = class {
839
+ constructor() {
840
+ this.id = "whatsapp-channel";
841
+ this.name = "WhatsApp";
842
+ this.version = "1.0.0";
843
+ this.category = "notification-channel";
844
+ this.capabilities = [
845
+ PLUGIN_CAPABILITIES.NOTIFICATION_CHANNEL,
846
+ PLUGIN_CAPABILITIES.NOTIFICATION_CHANNEL_WHATSAPP
847
+ ];
848
+ this.shape = "direct";
849
+ this.settingsSchema = {
850
+ type: "object",
851
+ properties: {
852
+ apiVersion: { type: "string" }
853
+ }
854
+ };
855
+ this.idempotencyCache = /* @__PURE__ */ new Map();
856
+ }
857
+ async onLoad() {
858
+ }
859
+ async onUnload() {
860
+ this.idempotencyCache.clear();
861
+ }
862
+ apiVersion(options) {
863
+ const v = options.settings?.apiVersion;
864
+ return typeof v === "string" && API_VERSION_PATTERN.test(v) ? v : DEFAULT_API_VERSION;
865
+ }
866
+ async verifyTarget(config, options) {
867
+ const accessToken = config.accessToken;
868
+ const phoneNumberId = config.phoneNumberId;
869
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
870
+ return { valid: false, message: "accessToken is required" };
871
+ }
872
+ if (typeof phoneNumberId !== "string" || phoneNumberId.length === 0) {
873
+ return { valid: false, message: "phoneNumberId is required" };
874
+ }
875
+ if (!isValidPhoneNumberId(phoneNumberId)) {
876
+ return { valid: false, message: "phoneNumberId must be a numeric id" };
877
+ }
878
+ if (typeof config.to !== "string" || config.to.length === 0) {
879
+ return { valid: false, message: "to (recipient) is required" };
880
+ }
881
+ try {
882
+ const response = await fetch(`${GRAPH_API_BASE}/${this.apiVersion(options)}/${phoneNumberId}`, {
883
+ method: "GET",
884
+ headers: { Authorization: `Bearer ${accessToken}` }
885
+ });
886
+ const data = await response.json();
887
+ if (!response.ok || data.error) {
888
+ return {
889
+ valid: false,
890
+ message: `WhatsApp number check failed: ${data.error?.message ?? response.status}`
891
+ };
892
+ }
893
+ return {
894
+ valid: true,
895
+ details: { phoneNumberId: data.id, displayPhoneNumber: data.display_phone_number }
896
+ };
897
+ } catch (err) {
898
+ return { valid: false, message: err instanceof Error ? err.message : String(err) };
899
+ }
900
+ }
901
+ async send(payload, options) {
902
+ const { accessToken, phoneNumberId, to } = getTarget(payload.target ?? {});
903
+ const cacheKey = `${options.channelId ?? ""} ${phoneNumberId} ${payload.messageRef}`;
904
+ const cached = this.idempotencyCache.get(cacheKey);
905
+ if (cached) return cached;
906
+ const body = payload.rich?.kind === "whatsapp-template" ? {
907
+ messaging_product: "whatsapp",
908
+ to,
909
+ type: "template",
910
+ template: payload.rich.payload
911
+ } : {
912
+ messaging_product: "whatsapp",
913
+ to,
914
+ type: "text",
915
+ text: { body: payload.text }
916
+ };
917
+ const response = await fetch(`${GRAPH_API_BASE}/${this.apiVersion(options)}/${phoneNumberId}/messages`, {
918
+ method: "POST",
919
+ headers: {
920
+ Authorization: `Bearer ${accessToken}`,
921
+ "Content-Type": "application/json"
922
+ },
923
+ body: JSON.stringify(body)
924
+ });
925
+ const data = await response.json();
926
+ if (!response.ok || data.error || !data.messages?.length) {
927
+ throw new Error(
928
+ `WhatsApp send failed (${response.status}): ${data.error?.message ?? "no message id returned"}`
929
+ );
930
+ }
931
+ const result = {
932
+ provider: this.id,
933
+ providerMessageId: data.messages[0].id,
934
+ deliveredAt: /* @__PURE__ */ new Date()
935
+ };
936
+ this.idempotencyCache.set(cacheKey, result);
937
+ if (this.idempotencyCache.size > 500) {
938
+ const firstKey = this.idempotencyCache.keys().next().value;
939
+ if (firstKey) this.idempotencyCache.delete(firstKey);
940
+ }
941
+ return result;
942
+ }
943
+ };
944
+ var whatsappChannelPlugin = new WhatsappChannelPlugin();
945
+ /**
946
+ * One header finding of the licence scan (plan §2.6:324-325, FR-54): the
947
+ * `SPDX-License-Identifier:` / `@license` tag a file declares in its first
948
+ * `LICENSE_HEADER_SCAN_BYTES` bytes.
949
+ *
950
+ * **Gap, named rather than guessed:** plan §3.1:437 writes the column's third
951
+ * field as `headerFindings: …` without a shape. `path` is certain — it is the
952
+ * first field of the scan's input — and the detected identifier is the second
953
+ * fact the scan produces, so both are declared; anything further an
954
+ * implementation adds is additive.
955
+ */
956
+
957
+ export { WhatsappChannelPlugin, whatsappChannelPlugin };