thumbgate 1.14.1 → 1.16.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.
Files changed (150) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/.well-known/llms.txt +5 -5
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +60 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +217 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +211 -8
  12. package/config/enforcement.json +59 -7
  13. package/config/evals/agent-safety-eval.json +338 -22
  14. package/config/gates/default.json +33 -0
  15. package/config/gates/routine.json +43 -0
  16. package/config/github-about.json +3 -3
  17. package/config/mcp-allowlists.json +4 -0
  18. package/config/merge-quality-checks.json +2 -1
  19. package/config/model-candidates.json +131 -0
  20. package/openapi/openapi.yaml +118 -2
  21. package/package.json +70 -51
  22. package/public/blog.html +7 -7
  23. package/public/codex-plugin.html +13 -7
  24. package/public/compare.html +29 -23
  25. package/public/dashboard.html +105 -12
  26. package/public/guide.html +28 -28
  27. package/public/index.html +233 -97
  28. package/public/learn.html +87 -20
  29. package/public/lessons.html +26 -2
  30. package/public/numbers.html +271 -0
  31. package/public/pro.html +89 -19
  32. package/scripts/agent-audit-trace.js +55 -0
  33. package/scripts/agent-memory-lifecycle.js +96 -0
  34. package/scripts/agent-readiness-plan.js +118 -0
  35. package/scripts/agentic-data-pipeline.js +21 -1
  36. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  37. package/scripts/ai-org-governance.js +98 -0
  38. package/scripts/ai-search-distribution.js +43 -0
  39. package/scripts/artifact-agent-plan.js +81 -0
  40. package/scripts/billing.js +27 -8
  41. package/scripts/cli-feedback.js +2 -1
  42. package/scripts/cli-schema.js +60 -5
  43. package/scripts/code-mode-mcp-plan.js +71 -0
  44. package/scripts/commercial-offer.js +1 -1
  45. package/scripts/context-engine.js +1 -2
  46. package/scripts/context-manager.js +4 -1
  47. package/scripts/contextfs.js +214 -32
  48. package/scripts/dashboard-render-spec.js +1 -1
  49. package/scripts/dashboard.js +275 -9
  50. package/scripts/decision-journal.js +13 -3
  51. package/scripts/document-workflow-governance.js +62 -0
  52. package/scripts/enterprise-agent-rollout.js +34 -0
  53. package/scripts/experience-replay-governance.js +69 -0
  54. package/scripts/export-hf-dataset.js +1 -1
  55. package/scripts/feedback-loop.js +141 -9
  56. package/scripts/feedback-to-rules.js +17 -23
  57. package/scripts/gates-engine.js +4 -6
  58. package/scripts/growth-campaigns.js +49 -0
  59. package/scripts/harness-selector.js +145 -1
  60. package/scripts/hybrid-supervisor-agent.js +64 -0
  61. package/scripts/inference-cache-policy.js +72 -0
  62. package/scripts/inference-economics.js +53 -0
  63. package/scripts/internal-agent-bootstrap.js +12 -2
  64. package/scripts/knowledge-layer-plan.js +108 -0
  65. package/scripts/lesson-canonical.js +181 -0
  66. package/scripts/lesson-db.js +71 -10
  67. package/scripts/lesson-inference.js +183 -44
  68. package/scripts/lesson-search.js +4 -1
  69. package/scripts/lesson-synthesis.js +23 -2
  70. package/scripts/llm-client.js +157 -26
  71. package/scripts/mailer/resend-mailer.js +112 -1
  72. package/scripts/mcp-transport-strategy.js +66 -0
  73. package/scripts/memory-store-governance.js +60 -0
  74. package/scripts/meta-agent-loop.js +7 -13
  75. package/scripts/model-access-eligibility.js +38 -0
  76. package/scripts/model-migration-readiness.js +55 -0
  77. package/scripts/native-messaging-audit.js +514 -0
  78. package/scripts/operational-integrity.js +96 -3
  79. package/scripts/otel-declarative-config.js +56 -0
  80. package/scripts/perplexity-client.js +1 -1
  81. package/scripts/post-training-governance.js +34 -0
  82. package/scripts/pr-manager.js +47 -7
  83. package/scripts/private-core-boundary.js +72 -0
  84. package/scripts/production-agent-readiness.js +40 -0
  85. package/scripts/profile-router.js +16 -1
  86. package/scripts/prompt-eval.js +564 -32
  87. package/scripts/prompt-programs.js +93 -0
  88. package/scripts/provider-action-normalizer.js +585 -0
  89. package/scripts/rule-validator.js +285 -0
  90. package/scripts/scaling-law-claims.js +60 -0
  91. package/scripts/security-scanner.js +1 -1
  92. package/scripts/self-distill-agent.js +7 -32
  93. package/scripts/seo-gsd.js +400 -43
  94. package/scripts/skill-rag-router.js +53 -0
  95. package/scripts/spec-gate.js +1 -1
  96. package/scripts/student-consistent-training.js +73 -0
  97. package/scripts/synthetic-data-provenance.js +98 -0
  98. package/scripts/task-context-result.js +81 -0
  99. package/scripts/telemetry-analytics.js +149 -0
  100. package/scripts/thompson-sampling.js +2 -2
  101. package/scripts/token-savings.js +7 -6
  102. package/scripts/token-tco.js +46 -0
  103. package/scripts/tool-registry.js +75 -3
  104. package/scripts/verification-loop.js +10 -1
  105. package/scripts/verifier-scoring.js +71 -0
  106. package/scripts/workflow-sentinel.js +284 -28
  107. package/scripts/workspace-agent-routines.js +118 -0
  108. package/skills/thumbgate/SKILL.md +1 -1
  109. package/src/api/server.js +434 -120
  110. package/.claude-plugin/README.md +0 -170
  111. package/adapters/README.md +0 -12
  112. package/scripts/analytics-report.js +0 -328
  113. package/scripts/autonomous-workflow.js +0 -377
  114. package/scripts/billing-setup.js +0 -109
  115. package/scripts/creator-campaigns.js +0 -239
  116. package/scripts/cross-encoder-reranker.js +0 -235
  117. package/scripts/daemon-manager.js +0 -108
  118. package/scripts/decision-trace.js +0 -354
  119. package/scripts/delegation-runtime.js +0 -896
  120. package/scripts/dispatch-brief.js +0 -159
  121. package/scripts/distribution-surfaces.js +0 -110
  122. package/scripts/feedback-history-distiller.js +0 -382
  123. package/scripts/funnel-analytics.js +0 -35
  124. package/scripts/history-distiller.js +0 -200
  125. package/scripts/hosted-job-launcher.js +0 -256
  126. package/scripts/intent-router.js +0 -392
  127. package/scripts/lesson-reranker.js +0 -263
  128. package/scripts/lesson-retrieval.js +0 -148
  129. package/scripts/managed-lesson-agent.js +0 -183
  130. package/scripts/operational-dashboard.js +0 -103
  131. package/scripts/operational-summary.js +0 -129
  132. package/scripts/operator-artifacts.js +0 -608
  133. package/scripts/optimize-context.js +0 -17
  134. package/scripts/org-dashboard.js +0 -206
  135. package/scripts/partner-orchestration.js +0 -146
  136. package/scripts/predictive-insights.js +0 -356
  137. package/scripts/pulse.js +0 -80
  138. package/scripts/reflector-agent.js +0 -221
  139. package/scripts/sales-pipeline.js +0 -681
  140. package/scripts/session-episode-store.js +0 -329
  141. package/scripts/session-health-sensor.js +0 -242
  142. package/scripts/session-report.js +0 -120
  143. package/scripts/swarm-coordinator.js +0 -81
  144. package/scripts/tool-kpi-tracker.js +0 -12
  145. package/scripts/webhook-delivery.js +0 -62
  146. package/scripts/workflow-sprint-intake.js +0 -475
  147. package/skills/agent-memory/SKILL.md +0 -97
  148. package/skills/solve-architecture-autonomy/SKILL.md +0 -17
  149. package/skills/solve-architecture-autonomy/tool.js +0 -33
  150. package/skills/thumbgate-feedback/SKILL.md +0 -49
@@ -1,681 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const crypto = require('node:crypto');
5
- const fs = require('node:fs');
6
- const path = require('node:path');
7
-
8
- const { getFeedbackPaths } = require('./feedback-paths');
9
- const { appendJsonl, ensureParentDir, readJsonl } = require('./fs-utils');
10
-
11
- const SALES_PIPELINE_FILE = 'sales-pipeline.jsonl';
12
- const SALES_STAGE_FLOW = [
13
- 'targeted',
14
- 'contacted',
15
- 'replied',
16
- 'call_booked',
17
- 'checkout_started',
18
- 'sprint_intake',
19
- 'paid',
20
- 'lost',
21
- ];
22
-
23
- const SALES_STAGE_TRANSITIONS = {
24
- targeted: ['contacted', 'lost'],
25
- contacted: ['replied', 'lost'],
26
- replied: ['call_booked', 'checkout_started', 'sprint_intake', 'lost'],
27
- call_booked: ['checkout_started', 'sprint_intake', 'paid', 'lost'],
28
- checkout_started: ['paid', 'lost'],
29
- sprint_intake: ['paid', 'lost'],
30
- paid: [],
31
- lost: [],
32
- };
33
-
34
- function normalizeText(value, maxLength = 1000) {
35
- if (value === undefined || value === null) return null;
36
- const text = String(value).trim();
37
- if (!text) return null;
38
- return text.slice(0, maxLength);
39
- }
40
-
41
- function normalizeUrl(value) {
42
- const text = normalizeText(value, 1000);
43
- if (!text) return null;
44
- try {
45
- return new URL(text).toString();
46
- } catch {
47
- return text;
48
- }
49
- }
50
-
51
- function normalizeSalesStage(value, fallback = null) {
52
- const normalized = normalizeText(value, 80);
53
- if (!normalized) return fallback;
54
- return SALES_STAGE_FLOW.includes(normalized) ? normalized : fallback;
55
- }
56
-
57
- function normalizeInteger(value, fallback = 0) {
58
- const parsed = Number.parseInt(String(value || '').trim(), 10);
59
- return Number.isFinite(parsed) ? parsed : fallback;
60
- }
61
-
62
- function slugify(value, fallback = 'lead') {
63
- const normalized = normalizeText(value, 320);
64
- if (!normalized) return fallback;
65
- let slug = '';
66
- let pendingSeparator = false;
67
- for (const char of normalized.toLowerCase()) {
68
- const code = char.codePointAt(0);
69
- const alphaNumeric = (code >= 97 && code <= 122) || (code >= 48 && code <= 57);
70
- if (alphaNumeric) {
71
- if (pendingSeparator && slug) slug += '_';
72
- slug += char;
73
- pendingSeparator = false;
74
- } else {
75
- pendingSeparator = true;
76
- }
77
- }
78
- return slug || fallback;
79
- }
80
-
81
- function shortHash(value) {
82
- return crypto.createHash('sha256').update(String(value || '')).digest('hex').slice(0, 10);
83
- }
84
-
85
- function buildSalesLeadId(entry = {}) {
86
- const explicit = normalizeText(entry.leadId, 160);
87
- if (explicit) return explicit;
88
-
89
- const source = normalizeText(entry.source, 80) || 'manual';
90
- const username = normalizeText(entry.contact?.username, 160)
91
- || normalizeText(entry.username, 160);
92
- const repoName = normalizeText(entry.account?.repoName, 200)
93
- || normalizeText(entry.repoName, 200);
94
- const accountName = normalizeText(entry.account?.name, 200)
95
- || normalizeText(entry.company, 200);
96
- const stableKey = [source, username, repoName || accountName].filter(Boolean).join(':');
97
-
98
- if (stableKey) {
99
- return slugify(stableKey, `lead_${shortHash(JSON.stringify(entry))}`);
100
- }
101
- return `lead_${shortHash(JSON.stringify(entry))}`;
102
- }
103
-
104
- function buildHistoryEntry({
105
- fromStage = null,
106
- toStage,
107
- actor = null,
108
- channel = null,
109
- note = null,
110
- url = null,
111
- timestamp = new Date().toISOString(),
112
- } = {}) {
113
- return {
114
- fromStage: normalizeSalesStage(fromStage, null),
115
- toStage: normalizeSalesStage(toStage, 'targeted'),
116
- at: normalizeText(timestamp, 64) || new Date().toISOString(),
117
- actor: normalizeText(actor, 160),
118
- channel: normalizeText(channel, 80),
119
- note: normalizeText(note, 2000),
120
- url: normalizeUrl(url),
121
- };
122
- }
123
-
124
- function normalizeLeadHistory(entry, stage, updatedAt) {
125
- const hasHistory = Array.isArray(entry.history) ? entry.history.length > 0 : false;
126
- return hasHistory
127
- ? entry.history.map((item) => buildHistoryEntry(item))
128
- : [buildHistoryEntry({
129
- toStage: stage,
130
- actor: entry.actor || 'sales-pipeline',
131
- channel: entry.channel || entry.source || 'manual',
132
- note: entry.note || 'Lead entered pipeline.',
133
- timestamp: updatedAt,
134
- })];
135
- }
136
-
137
- function normalizeLeadContact(entry = {}) {
138
- const contact = entry.contact || {};
139
- return {
140
- username: normalizeText(contact.username, 160),
141
- name: normalizeText(contact.name, 160),
142
- email: normalizeText(contact.email, 320),
143
- url: normalizeUrl(contact.url),
144
- };
145
- }
146
-
147
- function normalizeLeadAccount(entry = {}) {
148
- const account = entry.account || {};
149
- return {
150
- name: normalizeText(account.name, 200),
151
- repoName: normalizeText(account.repoName, 200),
152
- repoUrl: normalizeUrl(account.repoUrl),
153
- description: normalizeText(account.description, 1000),
154
- stars: normalizeInteger(account.stars, 0),
155
- updatedAt: normalizeText(account.updatedAt, 64),
156
- };
157
- }
158
-
159
- function normalizeLeadQualification(entry = {}) {
160
- const qualification = entry.qualification || {};
161
- return {
162
- painHypothesis: normalizeText(qualification.painHypothesis, 1200),
163
- concreteOffer: normalizeText(qualification.concreteOffer, 400)
164
- || 'I will harden one AI-agent workflow for you.',
165
- proofTiming: normalizeText(qualification.proofTiming, 240)
166
- || 'Use proof pack only after the buyer confirms pain.',
167
- };
168
- }
169
-
170
- function normalizeLeadOutbound(entry = {}) {
171
- const outbound = entry.outbound || {};
172
- return {
173
- draft: normalizeText(outbound.draft, 2000),
174
- cta: normalizeUrl(outbound.cta),
175
- lastSentAt: normalizeText(outbound.lastSentAt, 64),
176
- lastSentUrl: normalizeUrl(outbound.lastSentUrl),
177
- };
178
- }
179
-
180
- function normalizeLeadRevenue(entry = {}) {
181
- const revenue = entry.revenue || {};
182
- return {
183
- amountCents: Math.max(0, normalizeInteger(revenue.amountCents, 0)),
184
- currency: normalizeText(revenue.currency, 16) || 'usd',
185
- paidAt: normalizeText(revenue.paidAt, 64),
186
- };
187
- }
188
-
189
- function normalizeLeadAttribution(entry = {}) {
190
- const attribution = entry.attribution || {};
191
- return {
192
- sourceReport: normalizeText(attribution.sourceReport, 1000),
193
- campaign: normalizeText(attribution.campaign, 160),
194
- utmSource: normalizeText(attribution.utmSource, 120),
195
- utmMedium: normalizeText(attribution.utmMedium, 120),
196
- utmCampaign: normalizeText(attribution.utmCampaign, 160),
197
- };
198
- }
199
-
200
- function sanitizeSalesLead(entry = {}) {
201
- const createdAt = normalizeText(entry.createdAt, 64) || new Date().toISOString();
202
- const updatedAt = normalizeText(entry.updatedAt, 64) || createdAt;
203
- const stage = normalizeSalesStage(entry.stage, 'targeted');
204
- const source = normalizeText(entry.source, 80) || 'manual';
205
-
206
- return {
207
- leadId: buildSalesLeadId(entry),
208
- createdAt,
209
- updatedAt,
210
- stage,
211
- source,
212
- channel: normalizeText(entry.channel, 80) || source,
213
- offer: normalizeText(entry.offer, 120) || 'workflow_hardening_sprint',
214
- contact: normalizeLeadContact(entry),
215
- account: normalizeLeadAccount(entry),
216
- qualification: normalizeLeadQualification(entry),
217
- outbound: normalizeLeadOutbound(entry),
218
- revenue: normalizeLeadRevenue(entry),
219
- attribution: normalizeLeadAttribution(entry),
220
- history: normalizeLeadHistory(entry, stage, updatedAt),
221
- };
222
- }
223
-
224
- function getSalesPipelinePath({ statePath = null, feedbackDir = null } = {}) {
225
- if (statePath) return path.resolve(statePath);
226
- const baseDir = feedbackDir || getFeedbackPaths().FEEDBACK_DIR;
227
- return path.join(baseDir, SALES_PIPELINE_FILE);
228
- }
229
-
230
- function appendSalesLeadSnapshot(lead = {}, options = {}) {
231
- const sanitized = sanitizeSalesLead(lead);
232
- appendJsonl(getSalesPipelinePath(options), sanitized);
233
- return sanitized;
234
- }
235
-
236
- function loadSalesLeadSnapshots(options = {}) {
237
- return readJsonl(getSalesPipelinePath(options))
238
- .map((entry) => {
239
- try {
240
- return sanitizeSalesLead(entry);
241
- } catch {
242
- return null;
243
- }
244
- })
245
- .filter(Boolean);
246
- }
247
-
248
- function loadSalesLeads(options = {}) {
249
- const latestByLeadId = new Map();
250
- for (const snapshot of loadSalesLeadSnapshots(options)) {
251
- const existing = latestByLeadId.get(snapshot.leadId);
252
- if (!existing || String(snapshot.updatedAt || '') >= String(existing.updatedAt || '')) {
253
- latestByLeadId.set(snapshot.leadId, snapshot);
254
- }
255
- }
256
- return Array.from(latestByLeadId.values())
257
- .sort((a, b) => String(a.updatedAt || '').localeCompare(String(b.updatedAt || '')));
258
- }
259
-
260
- function buildLeadFromRevenueTarget(target = {}, { sourcePath = null } = {}) {
261
- const username = normalizeText(target.username, 160);
262
- const repoName = normalizeText(target.repoName, 200);
263
- const repoUrl = normalizeUrl(target.repoUrl);
264
- return sanitizeSalesLead({
265
- source: 'github',
266
- channel: 'github',
267
- stage: 'targeted',
268
- offer: 'workflow_hardening_sprint',
269
- contact: {
270
- username,
271
- url: username ? `https://github.com/${username}` : null,
272
- },
273
- account: {
274
- name: username,
275
- repoName,
276
- repoUrl,
277
- description: target.description,
278
- stars: target.stars,
279
- updatedAt: target.updatedAt,
280
- },
281
- qualification: {
282
- painHypothesis: target.motionReason || target.description,
283
- concreteOffer: 'I will harden one AI-agent workflow for you.',
284
- proofTiming: 'Use proof pack only after the buyer confirms pain.',
285
- },
286
- outbound: {
287
- draft: target.message,
288
- cta: target.cta,
289
- },
290
- attribution: {
291
- sourceReport: sourcePath,
292
- campaign: 'workflow_hardening_sprint_outbound',
293
- utmSource: 'github',
294
- utmMedium: 'direct_outbound',
295
- utmCampaign: 'workflow_hardening_sprint',
296
- },
297
- });
298
- }
299
-
300
- function importRevenueLoopReport(report = {}, options = {}) {
301
- const existing = new Map(loadSalesLeads(options).map((lead) => [lead.leadId, lead]));
302
- const targets = Array.isArray(report.targets) ? report.targets : [];
303
- const imported = [];
304
- const skipped = [];
305
-
306
- for (const target of targets) {
307
- const candidate = buildLeadFromRevenueTarget(target, { sourcePath: options.sourcePath || null });
308
- if (existing.has(candidate.leadId)) {
309
- skipped.push(candidate.leadId);
310
- continue;
311
- }
312
- imported.push(appendSalesLeadSnapshot(candidate, options));
313
- }
314
-
315
- return {
316
- imported,
317
- skipped,
318
- };
319
- }
320
-
321
- function addSalesLead(payload = {}, options = {}) {
322
- const lead = sanitizeSalesLead({
323
- leadId: payload.leadId,
324
- source: payload.source || 'manual',
325
- channel: payload.channel || payload.source || 'manual',
326
- stage: payload.stage || 'targeted',
327
- offer: payload.offer || 'workflow_hardening_sprint',
328
- contact: {
329
- username: payload.username,
330
- name: payload.name,
331
- email: payload.email,
332
- url: payload.contactUrl,
333
- },
334
- account: {
335
- name: payload.account,
336
- repoName: payload.repo,
337
- repoUrl: payload.repoUrl,
338
- description: payload.description,
339
- stars: payload.stars,
340
- },
341
- qualification: {
342
- painHypothesis: payload.pain || payload.description,
343
- concreteOffer: payload.concreteOffer || 'I will harden one AI-agent workflow for you.',
344
- proofTiming: payload.proofTiming || 'Use proof pack only after the buyer confirms pain.',
345
- },
346
- outbound: {
347
- draft: payload.draft,
348
- cta: payload.cta,
349
- },
350
- attribution: {
351
- campaign: payload.campaign || 'workflow_hardening_sprint_outbound',
352
- utmSource: payload.utmSource || payload.source || 'manual',
353
- utmMedium: payload.utmMedium || 'direct_outbound',
354
- utmCampaign: payload.utmCampaign || 'workflow_hardening_sprint',
355
- },
356
- });
357
-
358
- const existing = loadSalesLeads(options).find((entry) => entry.leadId === lead.leadId);
359
- if (existing && !payload.force) {
360
- throw new Error(`Sales lead already exists: ${lead.leadId}`);
361
- }
362
-
363
- return appendSalesLeadSnapshot(lead, options);
364
- }
365
-
366
- function readRevenueLoopReport(sourcePath) {
367
- const resolved = path.resolve(sourcePath || '');
368
- const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
369
- return {
370
- report: parsed,
371
- sourcePath: resolved,
372
- };
373
- }
374
-
375
- function validateStageTransition(currentStage, nextStage, { force = false } = {}) {
376
- if (force || currentStage === nextStage) return;
377
- const allowed = SALES_STAGE_TRANSITIONS[currentStage] || [];
378
- if (!allowed.includes(nextStage)) {
379
- throw new Error(`Invalid sales pipeline transition: ${currentStage} -> ${nextStage}`);
380
- }
381
- }
382
-
383
- function advanceSalesLead(payload = {}, options = {}) {
384
- const leadId = normalizeText(payload.leadId || payload.lead, 160);
385
- const nextStage = normalizeSalesStage(payload.stage, null);
386
- if (!leadId) throw new Error('leadId is required.');
387
- if (!nextStage) throw new Error(`stage must be one of: ${SALES_STAGE_FLOW.join(', ')}`);
388
-
389
- const currentLead = loadSalesLeads(options).find((lead) => lead.leadId === leadId);
390
- if (!currentLead) throw new Error(`Unknown sales lead: ${leadId}`);
391
- validateStageTransition(currentLead.stage, nextStage, { force: Boolean(payload.force) });
392
-
393
- if (currentLead.stage === nextStage) {
394
- return {
395
- lead: currentLead,
396
- unchanged: true,
397
- };
398
- }
399
-
400
- const updatedAt = normalizeText(payload.timestamp, 64) || new Date().toISOString();
401
- const revenueAmount = normalizeInteger(payload.amountCents, currentLead.revenue.amountCents || 0);
402
- const updatedLead = appendSalesLeadSnapshot({
403
- ...currentLead,
404
- updatedAt,
405
- stage: nextStage,
406
- outbound: {
407
- ...currentLead.outbound,
408
- lastSentAt: nextStage === 'contacted' ? updatedAt : currentLead.outbound.lastSentAt,
409
- lastSentUrl: nextStage === 'contacted'
410
- ? normalizeUrl(payload.url) || currentLead.outbound.lastSentUrl
411
- : currentLead.outbound.lastSentUrl,
412
- },
413
- revenue: {
414
- ...currentLead.revenue,
415
- amountCents: nextStage === 'paid' ? revenueAmount : currentLead.revenue.amountCents,
416
- currency: normalizeText(payload.currency, 16) || currentLead.revenue.currency,
417
- paidAt: nextStage === 'paid' ? updatedAt : currentLead.revenue.paidAt,
418
- },
419
- history: currentLead.history.concat(buildHistoryEntry({
420
- fromStage: currentLead.stage,
421
- toStage: nextStage,
422
- actor: payload.actor || 'operator',
423
- channel: payload.channel || currentLead.channel,
424
- note: payload.note,
425
- url: payload.url,
426
- timestamp: updatedAt,
427
- })),
428
- }, options);
429
-
430
- return {
431
- lead: updatedLead,
432
- unchanged: false,
433
- };
434
- }
435
-
436
- function summarizeSalesPipeline(leads = []) {
437
- const byStage = Object.fromEntries(SALES_STAGE_FLOW.map((stage) => [stage, 0]));
438
- let bookedRevenueCents = 0;
439
- for (const lead of leads) {
440
- byStage[lead.stage] = (byStage[lead.stage] || 0) + 1;
441
- if (lead.stage === 'paid') {
442
- bookedRevenueCents += lead.revenue.amountCents || 0;
443
- }
444
- }
445
-
446
- return {
447
- total: leads.length,
448
- byStage,
449
- active: leads.filter((lead) => lead.stage !== 'paid' && lead.stage !== 'lost').length,
450
- contacted: byStage.contacted + byStage.replied + byStage.call_booked
451
- + byStage.checkout_started + byStage.sprint_intake + byStage.paid,
452
- replies: byStage.replied + byStage.call_booked + byStage.checkout_started + byStage.sprint_intake + byStage.paid,
453
- callsBooked: byStage.call_booked + byStage.checkout_started + byStage.sprint_intake + byStage.paid,
454
- paid: byStage.paid,
455
- bookedRevenueCents,
456
- };
457
- }
458
-
459
- function formatLeadContact(contact = {}) {
460
- return contact.username ? `@${contact.username}` : (contact.email || 'n/a');
461
- }
462
-
463
- function renderLeadQueueEntry(lead) {
464
- const repo = lead.account.repoUrl || lead.account.repoName || lead.account.name || 'n/a';
465
- return [
466
- `### ${lead.leadId}`,
467
- `- Stage: ${lead.stage}`,
468
- `- Offer: ${lead.offer}`,
469
- `- Repo/account: ${repo}`,
470
- `- Contact: ${formatLeadContact(lead.contact)}`,
471
- `- Concrete offer: ${lead.qualification.concreteOffer}`,
472
- `- Proof rule: ${lead.qualification.proofTiming}`,
473
- `- Outreach draft: ${lead.outbound.draft || 'n/a'}`,
474
- '',
475
- ];
476
- }
477
-
478
- function renderSalesPipelineMarkdown({ leads = [], generatedAt = new Date().toISOString() } = {}) {
479
- const summary = summarizeSalesPipeline(leads);
480
- const leadQueueLines = leads.length
481
- ? leads.flatMap(renderLeadQueueEntry)
482
- : ['- No leads tracked yet. Import a GTM revenue loop JSON report first.'];
483
- const lines = [
484
- '# Sales Pipeline',
485
- '',
486
- `Updated: ${generatedAt}`,
487
- '',
488
- 'This is the first-dollar truth table. Posts are not sales; only stage movement counts.',
489
- '',
490
- '## Summary',
491
- `- Total leads: ${summary.total}`,
492
- `- Active leads: ${summary.active}`,
493
- `- Contacted: ${summary.contacted}`,
494
- `- Replied: ${summary.replies}`,
495
- `- Calls booked: ${summary.callsBooked}`,
496
- `- Paid: ${summary.paid}`,
497
- `- Booked revenue: $${(summary.bookedRevenueCents / 100).toFixed(2)}`,
498
- '',
499
- '## Stage Counts',
500
- ...SALES_STAGE_FLOW.map((stage) => `- ${stage}: ${summary.byStage[stage] || 0}`),
501
- '',
502
- '## Lead Queue',
503
- ...leadQueueLines,
504
- ];
505
- return `${lines.join('\n').trim()}\n`;
506
- }
507
-
508
- function writeSalesPipelineReport({ outPath, leads }) {
509
- if (!outPath) return null;
510
- const resolved = path.resolve(outPath);
511
- ensureParentDir(resolved);
512
- fs.writeFileSync(resolved, renderSalesPipelineMarkdown({ leads }), 'utf8');
513
- return resolved;
514
- }
515
-
516
- function parseArgs(argv = []) {
517
- const firstArg = argv[0];
518
- const hasCommand = firstArg ? !firstArg.startsWith('--') : false;
519
- const command = hasCommand ? firstArg : 'report';
520
- const args = hasCommand ? argv.slice(1) : argv;
521
- const options = { command };
522
-
523
- for (let index = 0; index < args.length; index += 1) {
524
- const arg = args[index];
525
- if (!arg.startsWith('--')) continue;
526
- const eqIndex = arg.indexOf('=', 2);
527
- const rawKey = eqIndex === -1 ? arg.slice(2) : arg.slice(2, eqIndex);
528
- const key = rawKey.replaceAll(/-([a-z])/g, (_, letter) => letter.toUpperCase());
529
- if (eqIndex !== -1) {
530
- options[key] = arg.slice(eqIndex + 1);
531
- continue;
532
- }
533
- const nextArg = args[index + 1];
534
- if (nextArg && !nextArg.startsWith('--')) {
535
- options[key] = nextArg;
536
- index += 1;
537
- continue;
538
- }
539
- options[key] = true;
540
- }
541
-
542
- return options;
543
- }
544
-
545
- function runCli(argv = process.argv.slice(2)) {
546
- const options = parseArgs(argv);
547
- const stateOptions = {
548
- statePath: options.state,
549
- feedbackDir: options.feedbackDir,
550
- };
551
-
552
- switch (options.command) {
553
- case 'import':
554
- case 'import-gtm': {
555
- if (!options.source) throw new Error('--source is required for import.');
556
- const { report, sourcePath } = readRevenueLoopReport(options.source);
557
- const result = importRevenueLoopReport(report, { ...stateOptions, sourcePath });
558
- const leads = loadSalesLeads(stateOptions);
559
- const reportPath = writeSalesPipelineReport({ outPath: options.out, leads });
560
- return {
561
- command: options.command,
562
- imported: result.imported.length,
563
- skipped: result.skipped.length,
564
- statePath: getSalesPipelinePath(stateOptions),
565
- reportPath,
566
- };
567
- }
568
-
569
- case 'advance': {
570
- const result = advanceSalesLead({
571
- leadId: options.lead || options.leadId,
572
- stage: options.stage,
573
- actor: options.actor,
574
- channel: options.channel,
575
- note: options.note,
576
- url: options.url,
577
- amountCents: options.amountCents,
578
- currency: options.currency,
579
- force: options.force,
580
- }, stateOptions);
581
- const leads = loadSalesLeads(stateOptions);
582
- const reportPath = writeSalesPipelineReport({ outPath: options.out, leads });
583
- return {
584
- command: options.command,
585
- leadId: result.lead.leadId,
586
- stage: result.lead.stage,
587
- unchanged: result.unchanged,
588
- statePath: getSalesPipelinePath(stateOptions),
589
- reportPath,
590
- };
591
- }
592
-
593
- case 'add': {
594
- const lead = addSalesLead({
595
- leadId: options.lead || options.leadId,
596
- source: options.source,
597
- channel: options.channel,
598
- stage: options.stage,
599
- offer: options.offer,
600
- username: options.username,
601
- name: options.name,
602
- email: options.email,
603
- contactUrl: options.contactUrl,
604
- account: options.account,
605
- repo: options.repo,
606
- repoUrl: options.repoUrl,
607
- description: options.description,
608
- stars: options.stars,
609
- pain: options.pain,
610
- concreteOffer: options.concreteOffer,
611
- proofTiming: options.proofTiming,
612
- draft: options.draft,
613
- cta: options.cta,
614
- campaign: options.campaign,
615
- utmSource: options.utmSource,
616
- utmMedium: options.utmMedium,
617
- utmCampaign: options.utmCampaign,
618
- force: options.force,
619
- }, stateOptions);
620
- const leads = loadSalesLeads(stateOptions);
621
- const reportPath = writeSalesPipelineReport({ outPath: options.out, leads });
622
- return {
623
- command: options.command,
624
- leadId: lead.leadId,
625
- stage: lead.stage,
626
- statePath: getSalesPipelinePath(stateOptions),
627
- reportPath,
628
- };
629
- }
630
-
631
- case 'report': {
632
- const leads = loadSalesLeads(stateOptions);
633
- const reportPath = writeSalesPipelineReport({ outPath: options.out, leads });
634
- return {
635
- command: options.command,
636
- summary: summarizeSalesPipeline(leads),
637
- statePath: getSalesPipelinePath(stateOptions),
638
- reportPath,
639
- };
640
- }
641
-
642
- default:
643
- throw new Error(`Unknown sales pipeline command: ${options.command}`);
644
- }
645
- }
646
-
647
- function isCliInvocation(argv = process.argv) {
648
- const invokedPath = argv[1];
649
- return Boolean(invokedPath) && !path.relative(path.resolve(invokedPath), __filename);
650
- }
651
-
652
- if (isCliInvocation()) {
653
- try {
654
- const result = runCli();
655
- console.log(JSON.stringify(result, null, 2));
656
- } catch (err) {
657
- console.error(err?.message || err);
658
- process.exit(1);
659
- }
660
- }
661
-
662
- module.exports = {
663
- SALES_PIPELINE_FILE,
664
- SALES_STAGE_FLOW,
665
- SALES_STAGE_TRANSITIONS,
666
- addSalesLead,
667
- advanceSalesLead,
668
- appendSalesLeadSnapshot,
669
- buildLeadFromRevenueTarget,
670
- getSalesPipelinePath,
671
- importRevenueLoopReport,
672
- isCliInvocation,
673
- loadSalesLeads,
674
- loadSalesLeadSnapshots,
675
- normalizeSalesStage,
676
- parseArgs,
677
- renderSalesPipelineMarkdown,
678
- runCli,
679
- sanitizeSalesLead,
680
- summarizeSalesPipeline,
681
- };