thumbgate 1.15.0 → 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 (129) 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 +59 -35
  6. package/adapters/chatgpt/openapi.yaml +118 -2
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/mcp/server-stdio.js +210 -84
  9. package/adapters/opencode/opencode.json +1 -1
  10. package/bench/prompt-eval-suite.json +5 -1
  11. package/bin/cli.js +157 -8
  12. package/config/evals/agent-safety-eval.json +338 -22
  13. package/config/gates/routine.json +43 -0
  14. package/config/github-about.json +3 -3
  15. package/config/model-candidates.json +131 -0
  16. package/openapi/openapi.yaml +118 -2
  17. package/package.json +55 -48
  18. package/public/blog.html +7 -7
  19. package/public/codex-plugin.html +6 -6
  20. package/public/compare.html +29 -23
  21. package/public/dashboard.html +82 -10
  22. package/public/guide.html +28 -28
  23. package/public/index.html +216 -98
  24. package/public/learn.html +50 -22
  25. package/public/lessons.html +1 -1
  26. package/public/numbers.html +17 -17
  27. package/public/pro.html +82 -18
  28. package/scripts/agent-audit-trace.js +55 -0
  29. package/scripts/agent-memory-lifecycle.js +96 -0
  30. package/scripts/agent-readiness-plan.js +118 -0
  31. package/scripts/agentic-data-pipeline.js +21 -1
  32. package/scripts/agents-sdk-sandbox-plan.js +57 -0
  33. package/scripts/ai-org-governance.js +98 -0
  34. package/scripts/ai-search-distribution.js +43 -0
  35. package/scripts/artifact-agent-plan.js +81 -0
  36. package/scripts/billing.js +27 -8
  37. package/scripts/cli-schema.js +18 -2
  38. package/scripts/code-mode-mcp-plan.js +71 -0
  39. package/scripts/context-engine.js +1 -2
  40. package/scripts/context-manager.js +4 -1
  41. package/scripts/dashboard-render-spec.js +1 -1
  42. package/scripts/dashboard.js +275 -9
  43. package/scripts/decision-journal.js +13 -3
  44. package/scripts/document-workflow-governance.js +62 -0
  45. package/scripts/enterprise-agent-rollout.js +34 -0
  46. package/scripts/experience-replay-governance.js +69 -0
  47. package/scripts/export-hf-dataset.js +1 -1
  48. package/scripts/feedback-loop.js +92 -4
  49. package/scripts/feedback-to-rules.js +17 -23
  50. package/scripts/gates-engine.js +4 -6
  51. package/scripts/growth-campaigns.js +49 -0
  52. package/scripts/harness-selector.js +16 -4
  53. package/scripts/hybrid-supervisor-agent.js +64 -0
  54. package/scripts/inference-cache-policy.js +72 -0
  55. package/scripts/inference-economics.js +53 -0
  56. package/scripts/internal-agent-bootstrap.js +12 -2
  57. package/scripts/knowledge-layer-plan.js +108 -0
  58. package/scripts/lesson-inference.js +183 -44
  59. package/scripts/lesson-search.js +4 -1
  60. package/scripts/llm-client.js +157 -26
  61. package/scripts/mailer/resend-mailer.js +112 -1
  62. package/scripts/mcp-transport-strategy.js +66 -0
  63. package/scripts/memory-store-governance.js +60 -0
  64. package/scripts/meta-agent-loop.js +7 -13
  65. package/scripts/model-access-eligibility.js +38 -0
  66. package/scripts/model-migration-readiness.js +55 -0
  67. package/scripts/operational-integrity.js +96 -3
  68. package/scripts/otel-declarative-config.js +56 -0
  69. package/scripts/perplexity-client.js +1 -1
  70. package/scripts/post-training-governance.js +34 -0
  71. package/scripts/private-core-boundary.js +72 -0
  72. package/scripts/production-agent-readiness.js +40 -0
  73. package/scripts/prompt-eval.js +564 -32
  74. package/scripts/prompt-programs.js +93 -0
  75. package/scripts/provider-action-normalizer.js +585 -0
  76. package/scripts/scaling-law-claims.js +60 -0
  77. package/scripts/security-scanner.js +1 -1
  78. package/scripts/self-distill-agent.js +7 -32
  79. package/scripts/seo-gsd.js +232 -55
  80. package/scripts/skill-rag-router.js +53 -0
  81. package/scripts/spec-gate.js +1 -1
  82. package/scripts/student-consistent-training.js +73 -0
  83. package/scripts/synthetic-data-provenance.js +98 -0
  84. package/scripts/task-context-result.js +81 -0
  85. package/scripts/telemetry-analytics.js +149 -0
  86. package/scripts/thompson-sampling.js +2 -2
  87. package/scripts/token-savings.js +7 -6
  88. package/scripts/token-tco.js +46 -0
  89. package/scripts/tool-registry.js +63 -3
  90. package/scripts/verification-loop.js +10 -1
  91. package/scripts/verifier-scoring.js +71 -0
  92. package/scripts/workflow-sentinel.js +284 -28
  93. package/scripts/workspace-agent-routines.js +118 -0
  94. package/src/api/server.js +381 -120
  95. package/scripts/analytics-report.js +0 -328
  96. package/scripts/autonomous-workflow.js +0 -377
  97. package/scripts/billing-setup.js +0 -109
  98. package/scripts/creator-campaigns.js +0 -239
  99. package/scripts/cross-encoder-reranker.js +0 -235
  100. package/scripts/daemon-manager.js +0 -108
  101. package/scripts/decision-trace.js +0 -354
  102. package/scripts/delegation-runtime.js +0 -896
  103. package/scripts/dispatch-brief.js +0 -159
  104. package/scripts/distribution-surfaces.js +0 -110
  105. package/scripts/feedback-history-distiller.js +0 -382
  106. package/scripts/funnel-analytics.js +0 -35
  107. package/scripts/history-distiller.js +0 -200
  108. package/scripts/hosted-job-launcher.js +0 -256
  109. package/scripts/intent-router.js +0 -392
  110. package/scripts/lesson-reranker.js +0 -263
  111. package/scripts/lesson-retrieval.js +0 -148
  112. package/scripts/managed-lesson-agent.js +0 -183
  113. package/scripts/operational-dashboard.js +0 -103
  114. package/scripts/operational-summary.js +0 -129
  115. package/scripts/operator-artifacts.js +0 -608
  116. package/scripts/optimize-context.js +0 -17
  117. package/scripts/org-dashboard.js +0 -206
  118. package/scripts/partner-orchestration.js +0 -146
  119. package/scripts/predictive-insights.js +0 -356
  120. package/scripts/pulse.js +0 -80
  121. package/scripts/reflector-agent.js +0 -221
  122. package/scripts/sales-pipeline.js +0 -681
  123. package/scripts/session-episode-store.js +0 -329
  124. package/scripts/session-health-sensor.js +0 -242
  125. package/scripts/session-report.js +0 -120
  126. package/scripts/swarm-coordinator.js +0 -81
  127. package/scripts/tool-kpi-tracker.js +0 -12
  128. package/scripts/webhook-delivery.js +0 -62
  129. package/scripts/workflow-sprint-intake.js +0 -475
@@ -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
- };