thumbgate 1.4.3 → 1.4.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/README.md +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/mcp/server-stdio.js +1 -1
- package/adapters/opencode/opencode.json +1 -1
- package/package.json +157 -9
- package/scripts/statusline.sh +1 -0
- package/src/api/server.js +113 -16
- package/src/index.js +3 -0
- package/.claude-plugin/bundle/icon.png +0 -0
- package/.claude-plugin/bundle/icon.svg +0 -18
- package/.claude-plugin/bundle/server/index.js +0 -24
- package/adapters/chatgpt/INSTALL.md +0 -158
- package/adapters/perplexity/.mcp.json +0 -36
- package/adapters/perplexity/config.toml +0 -16
- package/adapters/perplexity/opencode.json +0 -29
- package/bin/memory.sh +0 -64
- package/bin/obsidian-sync.sh +0 -20
- package/plugins/amp-skill/INSTALL.md +0 -52
- package/plugins/amp-skill/SKILL.md +0 -64
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +0 -22
- package/plugins/claude-codex-bridge/.mcp.json +0 -14
- package/plugins/claude-codex-bridge/INSTALL.md +0 -43
- package/plugins/claude-codex-bridge/README.md +0 -46
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +0 -286
- package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +0 -24
- package/plugins/claude-codex-bridge/skills/result/SKILL.md +0 -22
- package/plugins/claude-codex-bridge/skills/review/SKILL.md +0 -28
- package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +0 -27
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +0 -21
- package/plugins/claude-codex-bridge/skills/status/SKILL.md +0 -19
- package/plugins/claude-skill/INSTALL.md +0 -55
- package/plugins/claude-skill/SKILL.md +0 -46
- package/plugins/codex-profile/.codex-plugin/plugin.json +0 -43
- package/plugins/codex-profile/.mcp.json +0 -14
- package/plugins/codex-profile/AGENTS.md +0 -20
- package/plugins/codex-profile/INSTALL.md +0 -89
- package/plugins/codex-profile/README.md +0 -61
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +0 -23
- package/plugins/cursor-marketplace/CHANGELOG.md +0 -30
- package/plugins/cursor-marketplace/LICENSE +0 -21
- package/plugins/cursor-marketplace/README.md +0 -124
- package/plugins/cursor-marketplace/agents/reliability-reviewer.md +0 -31
- package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
- package/plugins/cursor-marketplace/commands/capture-feedback.md +0 -33
- package/plugins/cursor-marketplace/commands/check-gates.md +0 -25
- package/plugins/cursor-marketplace/commands/show-lessons.md +0 -27
- package/plugins/cursor-marketplace/hooks/hooks.json +0 -10
- package/plugins/cursor-marketplace/mcp.json +0 -14
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +0 -34
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +0 -30
- package/plugins/cursor-marketplace/rules/session-continuity.mdc +0 -28
- package/plugins/cursor-marketplace/scripts/gate-check.sh +0 -21
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +0 -48
- package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +0 -31
- package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +0 -30
- package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +0 -33
- package/plugins/gemini-extension/INSTALL.md +0 -92
- package/plugins/gemini-extension/gemini_prompt.txt +0 -14
- package/plugins/gemini-extension/tool_contract.json +0 -45
- package/plugins/opencode-profile/INSTALL.md +0 -57
- package/public/assets/instagram-card.png +0 -0
- package/public/assets/tiktok-agent-memory.mp4 +0 -0
- package/public/blog.html +0 -474
- package/public/compare/mem0.html +0 -189
- package/public/compare/speclock.html +0 -180
- package/public/compare.html +0 -310
- package/public/dashboard.html +0 -1100
- package/public/guide.html +0 -317
- package/public/guides/claude-code-prevent-repeated-mistakes.html +0 -161
- package/public/guides/codex-cli-guardrails.html +0 -158
- package/public/guides/cursor-prevent-repeated-mistakes.html +0 -161
- package/public/guides/pre-action-gates.html +0 -162
- package/public/guides/stop-repeated-ai-agent-mistakes.html +0 -159
- package/public/index.html +0 -1225
- package/public/js/buyer-intent.js +0 -252
- package/public/learn/agent-harness-pattern.html +0 -180
- package/public/learn/ai-agent-persistent-memory.html +0 -203
- package/public/learn/learn.css +0 -45
- package/public/learn/mcp-pre-action-gates-explained.html +0 -172
- package/public/learn/stop-ai-agent-force-push.html +0 -134
- package/public/learn/vibe-coding-safety-net.html +0 -142
- package/public/learn.html +0 -274
- package/public/lessons.html +0 -967
- package/public/llm-context.md +0 -156
- package/public/pro.html +0 -1087
- package/public/vercel.json +0 -8
- package/scripts/a2ui-engine.js +0 -73
- package/scripts/adk-consolidator.js +0 -274
- package/scripts/agent-security-hardening.js +0 -225
- package/scripts/ai-search-visibility.js +0 -116
- package/scripts/autonomous-sales-agent.js +0 -39
- package/scripts/autoresearch-runner.js +0 -216
- package/scripts/background-agent-governance.js +0 -229
- package/scripts/behavioral-extraction.js +0 -93
- package/scripts/budget-enforcer.js +0 -173
- package/scripts/budget-guard.js +0 -173
- package/scripts/build-claude-mcpb.js +0 -255
- package/scripts/build-codex-plugin.js +0 -152
- package/scripts/capture-railway-diagnostics.sh +0 -97
- package/scripts/changeset-check.js +0 -372
- package/scripts/check-congruence.js +0 -443
- package/scripts/computer-use-firewall.js +0 -280
- package/scripts/content-engine/linkedin-content-generator.js +0 -154
- package/scripts/content-engine/output/linkedin-memento-validation.md +0 -17
- package/scripts/content-engine/output/linkedin-posts-2026-04-09.md +0 -175
- package/scripts/content-engine/reddit-thread-finder.js +0 -154
- package/scripts/context-engine.js +0 -710
- package/scripts/daily-digest.js +0 -11
- package/scripts/data-governance.js +0 -173
- package/scripts/deploy-gcp.sh +0 -44
- package/scripts/deploy-policy.js +0 -249
- package/scripts/disagreement-mining.js +0 -315
- package/scripts/dpo-optimizer.js +0 -206
- package/scripts/ensure-repo-bootstrap.js +0 -130
- package/scripts/ephemeral-agent-store.js +0 -212
- package/scripts/eval-harness.js +0 -56
- package/scripts/export-kto-pairs.js +0 -309
- package/scripts/export-training.js +0 -446
- package/scripts/feedback-fallback.js +0 -111
- package/scripts/feedback-inbox-read.js +0 -162
- package/scripts/feedback-root-consolidator.js +0 -233
- package/scripts/feedback-to-memory.js +0 -185
- package/scripts/gate-satisfy.js +0 -42
- package/scripts/generate-paperbanana-diagrams.sh +0 -99
- package/scripts/generate-pretool-hook.sh +0 -40
- package/scripts/github-about.js +0 -430
- package/scripts/github-outreach.js +0 -65
- package/scripts/gtm-revenue-loop.js +0 -535
- package/scripts/hallucination-detector.js +0 -226
- package/scripts/hf-papers.js +0 -317
- package/scripts/hook-auto-capture.sh +0 -100
- package/scripts/hook-stop-pr-thread-check.sh +0 -68
- package/scripts/hook-stop-self-score.sh +0 -51
- package/scripts/hook-stop-verify-deploy.sh +0 -31
- package/scripts/hook-verify-before-done.sh +0 -20
- package/scripts/managed-dpo-export.js +0 -91
- package/scripts/markdown-escape.js +0 -12
- package/scripts/marketing-experiment.js +0 -657
- package/scripts/memalign-recall.js +0 -111
- package/scripts/memory-migration.js +0 -296
- package/scripts/meta-policy.js +0 -190
- package/scripts/metered-billing.js +0 -16
- package/scripts/model-tier-router.js +0 -310
- package/scripts/money-watcher.js +0 -218
- package/scripts/multi-hop-recall.js +0 -240
- package/scripts/per-step-scoring.js +0 -163
- package/scripts/perplexity-command-center.js +0 -644
- package/scripts/perplexity-marketing.js +0 -454
- package/scripts/pii-scanner.js +0 -153
- package/scripts/plan-gate.js +0 -154
- package/scripts/post-everywhere.js +0 -341
- package/scripts/post-to-x-retry.sh +0 -22
- package/scripts/post-to-x.js +0 -369
- package/scripts/pr-manager.js +0 -421
- package/scripts/principle-extractor.js +0 -162
- package/scripts/pro-features.js +0 -41
- package/scripts/prompt-dlp.js +0 -222
- package/scripts/prove-adapters.js +0 -860
- package/scripts/prove-attribution.js +0 -361
- package/scripts/prove-automation.js +0 -651
- package/scripts/prove-autoresearch.js +0 -304
- package/scripts/prove-claim-verification.js +0 -277
- package/scripts/prove-cloudflare-sandbox.js +0 -161
- package/scripts/prove-data-pipeline.js +0 -408
- package/scripts/prove-data-quality.js +0 -227
- package/scripts/prove-evolution.js +0 -352
- package/scripts/prove-harnesses.js +0 -287
- package/scripts/prove-intelligence.js +0 -257
- package/scripts/prove-lancedb.js +0 -425
- package/scripts/prove-local-intelligence.js +0 -340
- package/scripts/prove-loop-closure.js +0 -263
- package/scripts/prove-packaged-runtime.js +0 -327
- package/scripts/prove-predictive-insights.js +0 -355
- package/scripts/prove-runtime.js +0 -363
- package/scripts/prove-seo-gsd.js +0 -234
- package/scripts/prove-settings.js +0 -279
- package/scripts/prove-subway-upgrades.js +0 -277
- package/scripts/prove-tessl.js +0 -229
- package/scripts/prove-training-export.js +0 -325
- package/scripts/prove-workflow-contract.js +0 -112
- package/scripts/prove-xmemory.js +0 -332
- package/scripts/publish-decision.js +0 -159
- package/scripts/ralph-loop.js +0 -376
- package/scripts/ralph-mode-ci.js +0 -434
- package/scripts/reddit-dm-outreach.js +0 -192
- package/scripts/reddit-monitor-cron.sh +0 -26
- package/scripts/reminder-engine.js +0 -132
- package/scripts/revenue-status.js +0 -472
- package/scripts/rotate-stripe-webhook-secret.js +0 -314
- package/scripts/schedule-manager.js +0 -249
- package/scripts/self-healing-check.js +0 -193
- package/scripts/session-analyzer.js +0 -533
- package/scripts/shieldcortex-memory-firewall-runner.mjs +0 -53
- package/scripts/skill-exporter.js +0 -260
- package/scripts/skill-materializer.js +0 -134
- package/scripts/skill-packs.js +0 -136
- package/scripts/skill-proposer.js +0 -99
- package/scripts/skill-quality-tracker.js +0 -282
- package/scripts/slow-loop.js +0 -72
- package/scripts/social-analytics/db/marketing-db.js +0 -179
- package/scripts/social-analytics/db/schema.sql +0 -55
- package/scripts/social-analytics/digest.js +0 -256
- package/scripts/social-analytics/engagement-audit.js +0 -185
- package/scripts/social-analytics/generate-instagram-card.js +0 -123
- package/scripts/social-analytics/generate-slides.js +0 -268
- package/scripts/social-analytics/instagram-thumbgate-post.js +0 -111
- package/scripts/social-analytics/install-growth-automation.js +0 -114
- package/scripts/social-analytics/load-env.js +0 -77
- package/scripts/social-analytics/mcp-server.js +0 -289
- package/scripts/social-analytics/normalizer.js +0 -580
- package/scripts/social-analytics/notify.js +0 -162
- package/scripts/social-analytics/poll-all.js +0 -107
- package/scripts/social-analytics/pollers/github.js +0 -195
- package/scripts/social-analytics/pollers/instagram.js +0 -253
- package/scripts/social-analytics/pollers/linkedin.js +0 -340
- package/scripts/social-analytics/pollers/plausible.js +0 -245
- package/scripts/social-analytics/pollers/reddit.js +0 -306
- package/scripts/social-analytics/pollers/threads.js +0 -233
- package/scripts/social-analytics/pollers/tiktok.js +0 -203
- package/scripts/social-analytics/pollers/x.js +0 -227
- package/scripts/social-analytics/pollers/youtube.js +0 -304
- package/scripts/social-analytics/pollers/zernio.js +0 -183
- package/scripts/social-analytics/post-video.js +0 -316
- package/scripts/social-analytics/publish-instagram-thumbgate.js +0 -104
- package/scripts/social-analytics/publish-thumbgate-launch.js +0 -322
- package/scripts/social-analytics/publishers/devto.js +0 -122
- package/scripts/social-analytics/publishers/instagram.js +0 -317
- package/scripts/social-analytics/publishers/linkedin.js +0 -294
- package/scripts/social-analytics/publishers/reddit.js +0 -385
- package/scripts/social-analytics/publishers/threads.js +0 -275
- package/scripts/social-analytics/publishers/tiktok.js +0 -217
- package/scripts/social-analytics/publishers/x.js +0 -259
- package/scripts/social-analytics/publishers/youtube.js +0 -223
- package/scripts/social-analytics/publishers/zernio.js +0 -568
- package/scripts/social-analytics/reconcile-thumbgate-campaign.js +0 -165
- package/scripts/social-analytics/run-digest.js +0 -34
- package/scripts/social-analytics/schedule-thumbgate-campaign.js +0 -275
- package/scripts/social-analytics/store.js +0 -455
- package/scripts/social-analytics/sync-launch-assets.js +0 -185
- package/scripts/social-analytics/utm.js +0 -143
- package/scripts/social-pipeline.js +0 -2626
- package/scripts/social-post-hourly.js +0 -228
- package/scripts/social-quality-gate.js +0 -134
- package/scripts/social-reply-monitor.js +0 -592
- package/scripts/status-dashboard.js +0 -155
- package/scripts/stripe-live-status.js +0 -115
- package/scripts/subagent-profiles.js +0 -79
- package/scripts/sync-branch-protection.js +0 -340
- package/scripts/sync-gh-secrets-from-env.sh +0 -70
- package/scripts/sync-github-about.js +0 -55
- package/scripts/sync-version.js +0 -479
- package/scripts/synthetic-dpo.js +0 -234
- package/scripts/tessl-export.js +0 -369
- package/scripts/test-coverage.js +0 -128
- package/scripts/thumbgate-bench.js +0 -494
- package/scripts/thumbgate_session_start.sh +0 -32
- package/scripts/train_from_feedback.py +0 -929
- package/scripts/validate-feedback.js +0 -581
- package/scripts/verify-obsidian-setup.sh +0 -269
- package/scripts/verify-run.js +0 -269
- package/scripts/weekly-auto-post.js +0 -124
- package/scripts/x-autonomous-marketing.js +0 -139
|
@@ -1,455 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
let Database = null;
|
|
6
|
-
try {
|
|
7
|
-
Database = require('better-sqlite3');
|
|
8
|
-
} catch (_) {
|
|
9
|
-
Database = null;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const DEFAULT_DB_PATH = path.join(__dirname, 'db', 'social-analytics.db');
|
|
13
|
-
const SCHEMA_PATH = path.join(__dirname, 'db', 'schema.sql');
|
|
14
|
-
|
|
15
|
-
class MemoryStatement {
|
|
16
|
-
constructor(handler) {
|
|
17
|
-
this.handler = handler;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
run(params) {
|
|
21
|
-
return this.handler.run(params);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
all(params) {
|
|
25
|
-
return this.handler.all(params);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
class MemoryDatabase {
|
|
30
|
-
constructor() {
|
|
31
|
-
this.tables = {
|
|
32
|
-
engagement_metrics: [],
|
|
33
|
-
follower_snapshots: [],
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
pragma() {}
|
|
38
|
-
|
|
39
|
-
exec() {}
|
|
40
|
-
|
|
41
|
-
close() {}
|
|
42
|
-
|
|
43
|
-
prepare(sql) {
|
|
44
|
-
const normalized = sql.replace(/\s+/g, ' ').trim();
|
|
45
|
-
|
|
46
|
-
if (normalized.includes('INSERT OR REPLACE INTO engagement_metrics')) {
|
|
47
|
-
return new MemoryStatement({
|
|
48
|
-
run: (params) => {
|
|
49
|
-
const index = this.tables.engagement_metrics.findIndex(
|
|
50
|
-
(row) =>
|
|
51
|
-
row.platform === params.platform &&
|
|
52
|
-
row.post_id === params.post_id &&
|
|
53
|
-
row.metric_date === params.metric_date
|
|
54
|
-
);
|
|
55
|
-
const row = { ...params };
|
|
56
|
-
if (index >= 0) {
|
|
57
|
-
this.tables.engagement_metrics[index] = row;
|
|
58
|
-
} else {
|
|
59
|
-
this.tables.engagement_metrics.push(row);
|
|
60
|
-
}
|
|
61
|
-
return { changes: 1 };
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (normalized.includes('INSERT OR REPLACE INTO follower_snapshots')) {
|
|
67
|
-
return new MemoryStatement({
|
|
68
|
-
run: (params) => {
|
|
69
|
-
const index = this.tables.follower_snapshots.findIndex(
|
|
70
|
-
(row) => row.platform === params.platform && row.snapshot_date === params.snapshot_date
|
|
71
|
-
);
|
|
72
|
-
const row = { ...params };
|
|
73
|
-
if (index >= 0) {
|
|
74
|
-
this.tables.follower_snapshots[index] = row;
|
|
75
|
-
} else {
|
|
76
|
-
this.tables.follower_snapshots.push(row);
|
|
77
|
-
}
|
|
78
|
-
return { changes: 1 };
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (normalized.includes('SELECT * FROM engagement_metrics WHERE platform = ?')) {
|
|
84
|
-
return new MemoryStatement({
|
|
85
|
-
all: (platform) =>
|
|
86
|
-
this.tables.engagement_metrics.filter((row) => row.platform === platform),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const literalPlatformMatch = normalized.match(
|
|
91
|
-
/^SELECT \* FROM engagement_metrics WHERE platform = '([^']+)'$/i
|
|
92
|
-
);
|
|
93
|
-
if (literalPlatformMatch) {
|
|
94
|
-
const [, platform] = literalPlatformMatch;
|
|
95
|
-
return new MemoryStatement({
|
|
96
|
-
all: () =>
|
|
97
|
-
this.tables.engagement_metrics.filter((row) => row.platform === platform),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (normalized.includes('FROM engagement_metrics') && normalized.includes('GROUP BY platform')) {
|
|
102
|
-
return new MemoryStatement({
|
|
103
|
-
all: (params = {}) => {
|
|
104
|
-
const rows = this.tables.engagement_metrics.filter((row) => {
|
|
105
|
-
if (row.metric_date < params.cutoff) return false;
|
|
106
|
-
if (params.platform && row.platform !== params.platform) return false;
|
|
107
|
-
return true;
|
|
108
|
-
});
|
|
109
|
-
const grouped = new Map();
|
|
110
|
-
for (const row of rows) {
|
|
111
|
-
const bucket = grouped.get(row.platform) || {
|
|
112
|
-
platform: row.platform,
|
|
113
|
-
post_count: 0,
|
|
114
|
-
total_impressions: 0,
|
|
115
|
-
total_reach: 0,
|
|
116
|
-
total_likes: 0,
|
|
117
|
-
total_comments: 0,
|
|
118
|
-
total_shares: 0,
|
|
119
|
-
total_saves: 0,
|
|
120
|
-
total_clicks: 0,
|
|
121
|
-
total_video_views: 0,
|
|
122
|
-
_postIds: new Set(),
|
|
123
|
-
_rows: 0,
|
|
124
|
-
};
|
|
125
|
-
bucket._postIds.add(row.post_id);
|
|
126
|
-
bucket.total_impressions += row.impressions || 0;
|
|
127
|
-
bucket.total_reach += row.reach || 0;
|
|
128
|
-
bucket.total_likes += row.likes || 0;
|
|
129
|
-
bucket.total_comments += row.comments || 0;
|
|
130
|
-
bucket.total_shares += row.shares || 0;
|
|
131
|
-
bucket.total_saves += row.saves || 0;
|
|
132
|
-
bucket.total_clicks += row.clicks || 0;
|
|
133
|
-
bucket.total_video_views += row.video_views || 0;
|
|
134
|
-
bucket._rows += 1;
|
|
135
|
-
grouped.set(row.platform, bucket);
|
|
136
|
-
}
|
|
137
|
-
return [...grouped.values()]
|
|
138
|
-
.map((bucket) => ({
|
|
139
|
-
platform: bucket.platform,
|
|
140
|
-
post_count: bucket._postIds.size,
|
|
141
|
-
total_impressions: bucket.total_impressions,
|
|
142
|
-
total_reach: bucket.total_reach,
|
|
143
|
-
total_likes: bucket.total_likes,
|
|
144
|
-
total_comments: bucket.total_comments,
|
|
145
|
-
total_shares: bucket.total_shares,
|
|
146
|
-
total_saves: bucket.total_saves,
|
|
147
|
-
total_clicks: bucket.total_clicks,
|
|
148
|
-
total_video_views: bucket.total_video_views,
|
|
149
|
-
avg_impressions: Number((bucket.total_impressions / bucket._rows).toFixed(2)),
|
|
150
|
-
avg_likes: Number((bucket.total_likes / bucket._rows).toFixed(2)),
|
|
151
|
-
}))
|
|
152
|
-
.sort((a, b) => b.total_impressions - a.total_impressions);
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (normalized.includes('FROM engagement_metrics') && normalized.includes('GROUP BY platform, post_id')) {
|
|
158
|
-
return new MemoryStatement({
|
|
159
|
-
all: ({ cutoff, limit }) => {
|
|
160
|
-
const rows = this.tables.engagement_metrics.filter((row) => row.metric_date >= cutoff);
|
|
161
|
-
const grouped = new Map();
|
|
162
|
-
for (const row of rows) {
|
|
163
|
-
const key = `${row.platform}::${row.post_id}`;
|
|
164
|
-
const bucket = grouped.get(key) || {
|
|
165
|
-
platform: row.platform,
|
|
166
|
-
content_type: row.content_type,
|
|
167
|
-
post_id: row.post_id,
|
|
168
|
-
post_url: row.post_url ?? null,
|
|
169
|
-
published_at: row.published_at ?? null,
|
|
170
|
-
total_engagement: 0,
|
|
171
|
-
total_impressions: 0,
|
|
172
|
-
total_likes: 0,
|
|
173
|
-
total_comments: 0,
|
|
174
|
-
total_shares: 0,
|
|
175
|
-
total_saves: 0,
|
|
176
|
-
total_video_views: 0,
|
|
177
|
-
};
|
|
178
|
-
bucket.total_engagement +=
|
|
179
|
-
(row.likes || 0) + (row.comments || 0) + (row.shares || 0) + (row.saves || 0);
|
|
180
|
-
bucket.total_impressions += row.impressions || 0;
|
|
181
|
-
bucket.total_likes += row.likes || 0;
|
|
182
|
-
bucket.total_comments += row.comments || 0;
|
|
183
|
-
bucket.total_shares += row.shares || 0;
|
|
184
|
-
bucket.total_saves += row.saves || 0;
|
|
185
|
-
bucket.total_video_views += row.video_views || 0;
|
|
186
|
-
grouped.set(key, bucket);
|
|
187
|
-
}
|
|
188
|
-
return [...grouped.values()]
|
|
189
|
-
.sort((a, b) => b.total_engagement - a.total_engagement)
|
|
190
|
-
.slice(0, limit);
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (normalized.includes('FROM follower_snapshots')) {
|
|
196
|
-
return new MemoryStatement({
|
|
197
|
-
all: ({ platform, cutoff }) =>
|
|
198
|
-
this.tables.follower_snapshots
|
|
199
|
-
.filter((row) => row.platform === platform && row.snapshot_date >= cutoff)
|
|
200
|
-
.sort((a, b) => a.snapshot_date.localeCompare(b.snapshot_date)),
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
throw new Error(`MemoryDatabase does not support query: ${normalized}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Opens the SQLite database, applies the schema, and returns the db instance.
|
|
210
|
-
* Idempotent — safe to call multiple times; schema uses IF NOT EXISTS guards.
|
|
211
|
-
*
|
|
212
|
-
* @param {string} [dbPath] - Absolute path to the .db file. Defaults to DEFAULT_DB_PATH.
|
|
213
|
-
* @returns {import('better-sqlite3').Database}
|
|
214
|
-
*/
|
|
215
|
-
function initDb(dbPath = DEFAULT_DB_PATH) {
|
|
216
|
-
const isInMemoryDatabase = dbPath === ':memory:';
|
|
217
|
-
const resolvedPath = isInMemoryDatabase ? dbPath : path.resolve(dbPath);
|
|
218
|
-
const dir = isInMemoryDatabase ? null : path.dirname(resolvedPath);
|
|
219
|
-
|
|
220
|
-
if (dir && !fs.existsSync(dir)) {
|
|
221
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const db = Database ? new Database(resolvedPath) : new MemoryDatabase();
|
|
225
|
-
db.pragma('busy_timeout = 3000');
|
|
226
|
-
|
|
227
|
-
// Enable WAL mode for better concurrent read performance.
|
|
228
|
-
if (!isInMemoryDatabase) {
|
|
229
|
-
db.pragma('journal_mode = WAL');
|
|
230
|
-
}
|
|
231
|
-
db.pragma('foreign_keys = ON');
|
|
232
|
-
|
|
233
|
-
const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
|
|
234
|
-
db.exec(schema);
|
|
235
|
-
|
|
236
|
-
return db;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Upserts a single engagement metric record.
|
|
241
|
-
* Uses INSERT OR REPLACE to handle the UNIQUE(platform, post_id, metric_date) constraint.
|
|
242
|
-
*
|
|
243
|
-
* @param {import('better-sqlite3').Database} db
|
|
244
|
-
* @param {object} record - Fields matching the engagement_metrics schema.
|
|
245
|
-
* @param {string} record.platform
|
|
246
|
-
* @param {string} record.content_type
|
|
247
|
-
* @param {string} record.post_id
|
|
248
|
-
* @param {string} [record.post_url]
|
|
249
|
-
* @param {string} [record.published_at]
|
|
250
|
-
* @param {string} record.metric_date
|
|
251
|
-
* @param {number} [record.impressions]
|
|
252
|
-
* @param {number} [record.reach]
|
|
253
|
-
* @param {number} [record.likes]
|
|
254
|
-
* @param {number} [record.comments]
|
|
255
|
-
* @param {number} [record.shares]
|
|
256
|
-
* @param {number} [record.saves]
|
|
257
|
-
* @param {number} [record.clicks]
|
|
258
|
-
* @param {number} [record.video_views]
|
|
259
|
-
* @param {number} [record.followers_delta]
|
|
260
|
-
* @param {string|object} [record.extra_json]
|
|
261
|
-
* @param {string} record.fetched_at
|
|
262
|
-
* @returns {import('better-sqlite3').RunResult}
|
|
263
|
-
*/
|
|
264
|
-
function upsertMetric(db, record) {
|
|
265
|
-
if (!record.platform) throw new Error('upsertMetric: record.platform is required');
|
|
266
|
-
if (!record.content_type) throw new Error('upsertMetric: record.content_type is required');
|
|
267
|
-
if (!record.post_id) throw new Error('upsertMetric: record.post_id is required');
|
|
268
|
-
if (!record.metric_date) throw new Error('upsertMetric: record.metric_date is required');
|
|
269
|
-
if (!record.fetched_at) throw new Error('upsertMetric: record.fetched_at is required');
|
|
270
|
-
|
|
271
|
-
const extraJson =
|
|
272
|
-
record.extra_json && typeof record.extra_json === 'object'
|
|
273
|
-
? JSON.stringify(record.extra_json)
|
|
274
|
-
: record.extra_json ?? null;
|
|
275
|
-
|
|
276
|
-
const stmt = db.prepare(`
|
|
277
|
-
INSERT OR REPLACE INTO engagement_metrics (
|
|
278
|
-
platform, content_type, post_id, post_url, published_at, metric_date,
|
|
279
|
-
impressions, reach, likes, comments, shares, saves, clicks,
|
|
280
|
-
video_views, followers_delta, extra_json, fetched_at
|
|
281
|
-
) VALUES (
|
|
282
|
-
@platform, @content_type, @post_id, @post_url, @published_at, @metric_date,
|
|
283
|
-
@impressions, @reach, @likes, @comments, @shares, @saves, @clicks,
|
|
284
|
-
@video_views, @followers_delta, @extra_json, @fetched_at
|
|
285
|
-
)
|
|
286
|
-
`);
|
|
287
|
-
|
|
288
|
-
return stmt.run({
|
|
289
|
-
platform: record.platform,
|
|
290
|
-
content_type: record.content_type,
|
|
291
|
-
post_id: record.post_id,
|
|
292
|
-
post_url: record.post_url ?? null,
|
|
293
|
-
published_at: record.published_at ?? null,
|
|
294
|
-
metric_date: record.metric_date,
|
|
295
|
-
impressions: record.impressions ?? 0,
|
|
296
|
-
reach: record.reach ?? 0,
|
|
297
|
-
likes: record.likes ?? 0,
|
|
298
|
-
comments: record.comments ?? 0,
|
|
299
|
-
shares: record.shares ?? 0,
|
|
300
|
-
saves: record.saves ?? 0,
|
|
301
|
-
clicks: record.clicks ?? 0,
|
|
302
|
-
video_views: record.video_views ?? 0,
|
|
303
|
-
followers_delta: record.followers_delta ?? 0,
|
|
304
|
-
extra_json: extraJson,
|
|
305
|
-
fetched_at: record.fetched_at,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Upserts a follower snapshot.
|
|
311
|
-
*
|
|
312
|
-
* @param {import('better-sqlite3').Database} db
|
|
313
|
-
* @param {{ platform: string, follower_count: number, snapshot_date: string }} snapshot
|
|
314
|
-
* @returns {import('better-sqlite3').RunResult}
|
|
315
|
-
*/
|
|
316
|
-
function upsertFollowerSnapshot(db, { platform, follower_count, snapshot_date }) {
|
|
317
|
-
if (!platform) throw new Error('upsertFollowerSnapshot: platform is required');
|
|
318
|
-
if (follower_count == null) throw new Error('upsertFollowerSnapshot: follower_count is required');
|
|
319
|
-
if (!snapshot_date) throw new Error('upsertFollowerSnapshot: snapshot_date is required');
|
|
320
|
-
|
|
321
|
-
const stmt = db.prepare(`
|
|
322
|
-
INSERT OR REPLACE INTO follower_snapshots (platform, follower_count, snapshot_date)
|
|
323
|
-
VALUES (@platform, @follower_count, @snapshot_date)
|
|
324
|
-
`);
|
|
325
|
-
|
|
326
|
-
return stmt.run({ platform, follower_count, snapshot_date });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Returns aggregated engagement metrics grouped by platform for the last N days.
|
|
331
|
-
*
|
|
332
|
-
* @param {import('better-sqlite3').Database} db
|
|
333
|
-
* @param {{ platform?: string, days?: number }} options
|
|
334
|
-
* @returns {object[]}
|
|
335
|
-
*/
|
|
336
|
-
function queryMetrics(db, { platform, days = 30 } = {}) {
|
|
337
|
-
if (days <= 0) throw new Error('queryMetrics: days must be a positive integer');
|
|
338
|
-
|
|
339
|
-
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
|
|
340
|
-
.toISOString()
|
|
341
|
-
.slice(0, 10);
|
|
342
|
-
|
|
343
|
-
const params = { cutoff };
|
|
344
|
-
let platformClause = '';
|
|
345
|
-
|
|
346
|
-
if (platform) {
|
|
347
|
-
platformClause = 'AND platform = @platform';
|
|
348
|
-
params.platform = platform;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return db
|
|
352
|
-
.prepare(
|
|
353
|
-
`
|
|
354
|
-
SELECT
|
|
355
|
-
platform,
|
|
356
|
-
COUNT(DISTINCT post_id) AS post_count,
|
|
357
|
-
SUM(impressions) AS total_impressions,
|
|
358
|
-
SUM(reach) AS total_reach,
|
|
359
|
-
SUM(likes) AS total_likes,
|
|
360
|
-
SUM(comments) AS total_comments,
|
|
361
|
-
SUM(shares) AS total_shares,
|
|
362
|
-
SUM(saves) AS total_saves,
|
|
363
|
-
SUM(clicks) AS total_clicks,
|
|
364
|
-
SUM(video_views) AS total_video_views,
|
|
365
|
-
ROUND(AVG(impressions), 2) AS avg_impressions,
|
|
366
|
-
ROUND(AVG(likes), 2) AS avg_likes
|
|
367
|
-
FROM engagement_metrics
|
|
368
|
-
WHERE metric_date >= @cutoff
|
|
369
|
-
${platformClause}
|
|
370
|
-
GROUP BY platform
|
|
371
|
-
ORDER BY total_impressions DESC
|
|
372
|
-
`
|
|
373
|
-
)
|
|
374
|
-
.all(params);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Returns top content by total engagement (likes + comments + shares + saves)
|
|
379
|
-
* for the last N days.
|
|
380
|
-
*
|
|
381
|
-
* @param {import('better-sqlite3').Database} db
|
|
382
|
-
* @param {{ days?: number, limit?: number }} options
|
|
383
|
-
* @returns {object[]}
|
|
384
|
-
*/
|
|
385
|
-
function topContent(db, { days = 30, limit = 10 } = {}) {
|
|
386
|
-
if (days <= 0) throw new Error('topContent: days must be a positive integer');
|
|
387
|
-
if (limit <= 0) throw new Error('topContent: limit must be a positive integer');
|
|
388
|
-
|
|
389
|
-
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
|
|
390
|
-
.toISOString()
|
|
391
|
-
.slice(0, 10);
|
|
392
|
-
|
|
393
|
-
return db
|
|
394
|
-
.prepare(
|
|
395
|
-
`
|
|
396
|
-
SELECT
|
|
397
|
-
platform,
|
|
398
|
-
content_type,
|
|
399
|
-
post_id,
|
|
400
|
-
post_url,
|
|
401
|
-
published_at,
|
|
402
|
-
SUM(likes + comments + shares + saves) AS total_engagement,
|
|
403
|
-
SUM(impressions) AS total_impressions,
|
|
404
|
-
SUM(likes) AS total_likes,
|
|
405
|
-
SUM(comments) AS total_comments,
|
|
406
|
-
SUM(shares) AS total_shares,
|
|
407
|
-
SUM(saves) AS total_saves,
|
|
408
|
-
SUM(video_views) AS total_video_views
|
|
409
|
-
FROM engagement_metrics
|
|
410
|
-
WHERE metric_date >= @cutoff
|
|
411
|
-
GROUP BY platform, post_id
|
|
412
|
-
ORDER BY total_engagement DESC
|
|
413
|
-
LIMIT @limit
|
|
414
|
-
`
|
|
415
|
-
)
|
|
416
|
-
.all({ cutoff, limit });
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
/**
|
|
420
|
-
* Returns follower snapshots for a platform for the last N days.
|
|
421
|
-
*
|
|
422
|
-
* @param {import('better-sqlite3').Database} db
|
|
423
|
-
* @param {{ platform: string, days?: number }} options
|
|
424
|
-
* @returns {object[]}
|
|
425
|
-
*/
|
|
426
|
-
function getFollowerHistory(db, { platform, days = 30 } = {}) {
|
|
427
|
-
if (!platform) throw new Error('getFollowerHistory: platform is required');
|
|
428
|
-
if (days <= 0) throw new Error('getFollowerHistory: days must be a positive integer');
|
|
429
|
-
|
|
430
|
-
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
|
|
431
|
-
.toISOString()
|
|
432
|
-
.slice(0, 10);
|
|
433
|
-
|
|
434
|
-
return db
|
|
435
|
-
.prepare(
|
|
436
|
-
`
|
|
437
|
-
SELECT platform, follower_count, snapshot_date
|
|
438
|
-
FROM follower_snapshots
|
|
439
|
-
WHERE platform = @platform
|
|
440
|
-
AND snapshot_date >= @cutoff
|
|
441
|
-
ORDER BY snapshot_date ASC
|
|
442
|
-
`
|
|
443
|
-
)
|
|
444
|
-
.all({ platform, cutoff });
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
module.exports = {
|
|
448
|
-
DEFAULT_DB_PATH,
|
|
449
|
-
initDb,
|
|
450
|
-
upsertMetric,
|
|
451
|
-
upsertFollowerSnapshot,
|
|
452
|
-
queryMetrics,
|
|
453
|
-
topContent,
|
|
454
|
-
getFollowerHistory,
|
|
455
|
-
};
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const {
|
|
7
|
-
listPosts,
|
|
8
|
-
} = require('./publishers/zernio');
|
|
9
|
-
|
|
10
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
-
const DEFAULT_STATE_PATH = path.join(REPO_ROOT, '.thumbgate', 'social-launch-assets.json');
|
|
12
|
-
|
|
13
|
-
const LAUNCH_MARKERS = {
|
|
14
|
-
twitter: 'launch_post_twitter',
|
|
15
|
-
linkedin: 'launch_post_linkedin',
|
|
16
|
-
instagram: 'launch_post_instagram',
|
|
17
|
-
reddit: 'launch_post_reddit',
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const CAMPAIGN_MARKERS = {
|
|
21
|
-
proof_pack: 'campaign_proof_pack',
|
|
22
|
-
free_local: 'campaign_free_local',
|
|
23
|
-
checkout_path: 'campaign_checkout_path',
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function parseArgs(argv = []) {
|
|
27
|
-
const options = {
|
|
28
|
-
limit: 50,
|
|
29
|
-
statePath: DEFAULT_STATE_PATH,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
33
|
-
const token = String(argv[index] || '').trim();
|
|
34
|
-
if (token.startsWith('--limit=')) {
|
|
35
|
-
options.limit = Number.parseInt(token.slice('--limit='.length), 10) || options.limit;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (token === '--limit' && argv[index + 1]) {
|
|
39
|
-
options.limit = Number.parseInt(String(argv[index + 1]), 10) || options.limit;
|
|
40
|
-
index += 1;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (token.startsWith('--state-path=')) {
|
|
44
|
-
options.statePath = token.slice('--state-path='.length).trim() || DEFAULT_STATE_PATH;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (token === '--state-path' && argv[index + 1]) {
|
|
48
|
-
options.statePath = String(argv[index + 1]).trim() || DEFAULT_STATE_PATH;
|
|
49
|
-
index += 1;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return options;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizePlatform(post = {}) {
|
|
57
|
-
return String(post?.platforms?.[0]?.platform || '').trim().toLowerCase();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function extractMarker(post = {}) {
|
|
61
|
-
const content = String(post.content || '');
|
|
62
|
-
|
|
63
|
-
for (const marker of Object.values(LAUNCH_MARKERS)) {
|
|
64
|
-
if (content.includes(`utm_content=${marker}`)) {
|
|
65
|
-
return marker;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const marker of Object.values(CAMPAIGN_MARKERS)) {
|
|
70
|
-
if (content.includes(`utm_content=${marker}`)) {
|
|
71
|
-
return marker;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return '';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function toTimestamp(post = {}) {
|
|
79
|
-
return new Date(post.createdAt || post.updatedAt || post.scheduledFor || 0).getTime();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function selectNewestPost(posts = []) {
|
|
83
|
-
return [...posts].sort((left, right) => toTimestamp(right) - toTimestamp(left))[0] || null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function summarizePost(post = {}, marker = '') {
|
|
87
|
-
return {
|
|
88
|
-
id: post._id || post.id || null,
|
|
89
|
-
platform: normalizePlatform(post),
|
|
90
|
-
status: post.status || null,
|
|
91
|
-
marker,
|
|
92
|
-
createdAt: post.createdAt || null,
|
|
93
|
-
updatedAt: post.updatedAt || null,
|
|
94
|
-
scheduledFor: post.scheduledFor || null,
|
|
95
|
-
content: post.content || '',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function buildLaunchAssetState(posts = []) {
|
|
100
|
-
const state = {
|
|
101
|
-
updatedAt: new Date().toISOString(),
|
|
102
|
-
launchPosts: {},
|
|
103
|
-
campaignPosts: {},
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
for (const [platform, marker] of Object.entries(LAUNCH_MARKERS)) {
|
|
107
|
-
const matching = posts.filter((post) => extractMarker(post) === marker);
|
|
108
|
-
const selected = selectNewestPost(matching);
|
|
109
|
-
if (selected) {
|
|
110
|
-
state.launchPosts[platform] = summarizePost(selected, marker);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
for (const [slug, marker] of Object.entries(CAMPAIGN_MARKERS)) {
|
|
115
|
-
const matching = posts.filter((post) => extractMarker(post) === marker);
|
|
116
|
-
if (matching.length === 0) {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const byPlatform = {};
|
|
121
|
-
for (const post of matching) {
|
|
122
|
-
const platform = normalizePlatform(post);
|
|
123
|
-
if (!platform) continue;
|
|
124
|
-
const existing = byPlatform[platform];
|
|
125
|
-
if (!existing || toTimestamp(post) > toTimestamp(existing)) {
|
|
126
|
-
byPlatform[platform] = post;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
state.campaignPosts[slug] = {};
|
|
131
|
-
for (const [platform, post] of Object.entries(byPlatform)) {
|
|
132
|
-
state.campaignPosts[slug][platform] = summarizePost(post, marker);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return state;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function writeLaunchAssetState(statePath = DEFAULT_STATE_PATH, state = {}) {
|
|
140
|
-
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
141
|
-
fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
142
|
-
return statePath;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function syncLaunchAssets(options = {}, api = {}) {
|
|
146
|
-
const zernio = {
|
|
147
|
-
listPosts: api.listPosts || listPosts,
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const posts = await zernio.listPosts({ limit: options.limit || 50 });
|
|
151
|
-
const state = buildLaunchAssetState(Array.isArray(posts) ? posts : []);
|
|
152
|
-
const statePath = options.statePath || DEFAULT_STATE_PATH;
|
|
153
|
-
writeLaunchAssetState(statePath, state);
|
|
154
|
-
return {
|
|
155
|
-
statePath,
|
|
156
|
-
launchCount: Object.keys(state.launchPosts).length,
|
|
157
|
-
campaignCount: Object.keys(state.campaignPosts).length,
|
|
158
|
-
state,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (require.main === module) {
|
|
163
|
-
syncLaunchAssets(parseArgs(process.argv.slice(2)))
|
|
164
|
-
.then((result) => {
|
|
165
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
166
|
-
})
|
|
167
|
-
.catch((error) => {
|
|
168
|
-
console.error(error && error.message ? error.message : error);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
module.exports = {
|
|
174
|
-
CAMPAIGN_MARKERS,
|
|
175
|
-
DEFAULT_STATE_PATH,
|
|
176
|
-
LAUNCH_MARKERS,
|
|
177
|
-
buildLaunchAssetState,
|
|
178
|
-
extractMarker,
|
|
179
|
-
normalizePlatform,
|
|
180
|
-
parseArgs,
|
|
181
|
-
selectNewestPost,
|
|
182
|
-
summarizePost,
|
|
183
|
-
syncLaunchAssets,
|
|
184
|
-
writeLaunchAssetState,
|
|
185
|
-
};
|