thumbgate 0.9.10 → 0.9.11

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 (113) hide show
  1. package/.claude-plugin/README.md +2 -2
  2. package/.claude-plugin/marketplace.json +4 -2
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +115 -312
  6. package/adapters/README.md +1 -1
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +4 -4
  9. package/adapters/mcp/server-stdio.js +61 -1
  10. package/adapters/opencode/opencode.json +4 -2
  11. package/bin/cli.js +156 -8
  12. package/bin/memory.sh +3 -3
  13. package/config/e2e-critical-flows.json +4 -0
  14. package/config/gates/default.json +74 -2
  15. package/config/github-about.json +1 -1
  16. package/config/mcp-allowlists.json +27 -0
  17. package/package.json +22 -5
  18. package/plugins/amp-skill/INSTALL.md +1 -0
  19. package/plugins/amp-skill/SKILL.md +1 -0
  20. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  21. package/plugins/claude-codex-bridge/.mcp.json +4 -2
  22. package/plugins/claude-skill/INSTALL.md +1 -0
  23. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  24. package/plugins/codex-profile/.mcp.json +4 -2
  25. package/plugins/codex-profile/INSTALL.md +1 -1
  26. package/plugins/codex-profile/README.md +1 -1
  27. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  28. package/plugins/cursor-marketplace/README.md +3 -3
  29. package/plugins/cursor-marketplace/mcp.json +3 -1
  30. package/plugins/cursor-marketplace/scripts/gate-check.sh +15 -5
  31. package/plugins/gemini-extension/INSTALL.md +3 -3
  32. package/plugins/opencode-profile/INSTALL.md +1 -1
  33. package/public/dashboard.html +15 -8
  34. package/public/index.html +125 -185
  35. package/public/js/buyer-intent.js +252 -0
  36. package/public/pro.html +1085 -0
  37. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  38. package/scripts/adk-consolidator.js +14 -2
  39. package/scripts/agent-readiness.js +3 -1
  40. package/scripts/agent-security-hardening.js +4 -4
  41. package/scripts/auto-promote-gates.js +2 -0
  42. package/scripts/auto-wire-hooks.js +105 -17
  43. package/scripts/behavioral-extraction.js +2 -6
  44. package/scripts/billing.js +107 -3
  45. package/scripts/budget-guard.js +2 -2
  46. package/scripts/build-metadata.js +14 -0
  47. package/scripts/context-engine.js +1 -0
  48. package/scripts/deploy-policy.js +3 -17
  49. package/scripts/dpo-optimizer.js +3 -6
  50. package/scripts/ensure-repo-bootstrap.js +129 -0
  51. package/scripts/export-dpo-pairs.js +2 -3
  52. package/scripts/export-kto-pairs.js +3 -4
  53. package/scripts/export-training.js +8 -6
  54. package/scripts/feedback-attribution.js +23 -11
  55. package/scripts/feedback-loop.js +40 -2
  56. package/scripts/feedback-to-rules.js +2 -1
  57. package/scripts/filesystem-search.js +3 -2
  58. package/scripts/gates-engine.js +760 -29
  59. package/scripts/generate-pretool-hook.sh +0 -0
  60. package/scripts/gtm-revenue-loop.js +20 -1
  61. package/scripts/hook-auto-capture.sh +8 -3
  62. package/scripts/hook-runtime.js +89 -0
  63. package/scripts/hook-stop-self-score.sh +3 -3
  64. package/scripts/hook-thumbgate-cache-updater.js +99 -38
  65. package/scripts/hosted-config.js +4 -16
  66. package/scripts/hybrid-feedback-context.js +54 -14
  67. package/scripts/install-mcp.js +13 -0
  68. package/scripts/intent-router.js +2 -2
  69. package/scripts/license.js +52 -14
  70. package/scripts/local-model-profile.js +3 -2
  71. package/scripts/mcp-config.js +68 -6
  72. package/scripts/meta-policy.js +4 -8
  73. package/scripts/money-watcher.js +166 -16
  74. package/scripts/obsidian-export.js +1 -0
  75. package/scripts/operational-integrity.js +480 -0
  76. package/scripts/post-everywhere.js +7 -12
  77. package/scripts/pr-manager.js +14 -11
  78. package/scripts/profile-router.js +2 -0
  79. package/scripts/prompt-dlp.js +1 -0
  80. package/scripts/publish-decision.js +10 -0
  81. package/scripts/published-cli.js +34 -0
  82. package/scripts/risk-scorer.js +3 -2
  83. package/scripts/rlhf_session_start.sh +32 -0
  84. package/scripts/skill-quality-tracker.js +3 -5
  85. package/scripts/social-analytics/db/social-analytics.db-shm +0 -0
  86. package/scripts/social-analytics/db/social-analytics.db-wal +0 -0
  87. package/scripts/social-analytics/engagement-audit.js +202 -0
  88. package/scripts/social-analytics/instagram-thumbgate-post.js +45 -7
  89. package/scripts/social-analytics/install-growth-automation.js +114 -0
  90. package/scripts/social-analytics/load-env.js +46 -0
  91. package/scripts/social-analytics/poll-all.js +3 -18
  92. package/scripts/social-analytics/pollers/zernio.js +3 -0
  93. package/scripts/social-analytics/publish-instagram-thumbgate.js +22 -3
  94. package/scripts/social-analytics/publish-thumbgate-launch.js +316 -0
  95. package/scripts/social-analytics/publishers/reddit.js +7 -12
  96. package/scripts/social-analytics/publishers/zernio.js +210 -22
  97. package/scripts/social-analytics/reconcile-thumbgate-campaign.js +165 -0
  98. package/scripts/social-analytics/schedule-thumbgate-campaign.js +275 -0
  99. package/scripts/social-analytics/sync-launch-assets.js +185 -0
  100. package/scripts/social-post-hourly.js +185 -0
  101. package/scripts/social-quality-gate.js +119 -3
  102. package/scripts/social-reply-monitor.js +148 -32
  103. package/scripts/statusline-cache-path.js +27 -0
  104. package/scripts/statusline-meta.js +22 -0
  105. package/scripts/statusline.sh +24 -32
  106. package/scripts/sync-version.js +11 -3
  107. package/scripts/test-coverage.js +20 -13
  108. package/scripts/tool-registry.js +97 -0
  109. package/scripts/train_from_feedback.py +32 -9
  110. package/scripts/validate-feedback.js +3 -2
  111. package/scripts/vector-store.js +2 -3
  112. package/scripts/verify-obsidian-setup.sh +3 -3
  113. package/src/api/server.js +281 -33
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const {
5
+ buildCampaignEntries,
6
+ defaultCampaignSchedule,
7
+ } = require('./publish-thumbgate-launch');
8
+ const {
9
+ deletePost,
10
+ listPosts,
11
+ } = require('./publishers/zernio');
12
+ const {
13
+ buildScheduleKey,
14
+ DEFAULT_STATE_PATH,
15
+ writeScheduleState,
16
+ } = require('./schedule-thumbgate-campaign');
17
+
18
+ const CAMPAIGN_MARKERS = {
19
+ proof_pack: 'campaign_proof_pack',
20
+ free_local: 'campaign_free_local',
21
+ checkout_path: 'campaign_checkout_path',
22
+ };
23
+
24
+ function parseArgs(argv = []) {
25
+ const options = {
26
+ cancelDuplicates: false,
27
+ limit: 50,
28
+ scheduleTimes: [],
29
+ statePath: DEFAULT_STATE_PATH,
30
+ };
31
+
32
+ for (let index = 0; index < argv.length; index += 1) {
33
+ const token = argv[index];
34
+ if (token === '--cancel-duplicates') {
35
+ options.cancelDuplicates = true;
36
+ continue;
37
+ }
38
+ if (token.startsWith('--limit=')) {
39
+ options.limit = Number(token.slice('--limit='.length)) || options.limit;
40
+ continue;
41
+ }
42
+ if (token.startsWith('--times=')) {
43
+ options.scheduleTimes = token.slice('--times='.length).split(',').map((value) => value.trim()).filter(Boolean);
44
+ continue;
45
+ }
46
+ if (token.startsWith('--state-path=')) {
47
+ options.statePath = token.slice('--state-path='.length).trim() || DEFAULT_STATE_PATH;
48
+ }
49
+ }
50
+
51
+ return options;
52
+ }
53
+
54
+ function normalizePlatform(post) {
55
+ return String(post?.platforms?.[0]?.platform || '').trim().toLowerCase();
56
+ }
57
+
58
+ function selectCanonicalPost(posts = []) {
59
+ return [...posts].sort((left, right) => {
60
+ const leftCreated = new Date(left.createdAt || 0).getTime();
61
+ const rightCreated = new Date(right.createdAt || 0).getTime();
62
+ return leftCreated - rightCreated;
63
+ })[0] || null;
64
+ }
65
+
66
+ async function reconcileCampaignState(options = {}, api = {}) {
67
+ const zernio = {
68
+ deletePost: api.deletePost || deletePost,
69
+ listPosts: api.listPosts || listPosts,
70
+ };
71
+ const scheduleTimes = options.scheduleTimes && options.scheduleTimes.length > 0
72
+ ? options.scheduleTimes
73
+ : defaultCampaignSchedule();
74
+ const posts = await zernio.listPosts({ limit: options.limit || 50 });
75
+ const scheduled = {};
76
+ const duplicates = [];
77
+ const kept = [];
78
+ const entries = buildCampaignEntries();
79
+
80
+ for (let index = 0; index < entries.length; index += 1) {
81
+ const entry = entries[index];
82
+ const scheduleLocal = scheduleTimes[index];
83
+ if (!scheduleLocal) {
84
+ continue;
85
+ }
86
+
87
+ const scheduledForUtc = new Date(scheduleLocal).toISOString();
88
+
89
+ for (const platform of Object.keys(entry.posts)) {
90
+ const marker = CAMPAIGN_MARKERS[entry.slug];
91
+ const matchingPosts = posts.filter((post) => (
92
+ post.status === 'scheduled' &&
93
+ normalizePlatform(post) === platform &&
94
+ String(post.content || '').includes(`utm_content=${marker}`) &&
95
+ String(post.scheduledFor || '') === scheduledForUtc
96
+ ));
97
+
98
+ if (matchingPosts.length === 0) {
99
+ continue;
100
+ }
101
+
102
+ const canonical = selectCanonicalPost(matchingPosts);
103
+ if (!canonical) {
104
+ continue;
105
+ }
106
+
107
+ const scheduleKey = buildScheduleKey({
108
+ slug: entry.slug,
109
+ platform,
110
+ scheduledFor: scheduleLocal,
111
+ });
112
+ scheduled[scheduleKey] = {
113
+ id: canonical._id,
114
+ scheduledFor: scheduleLocal,
115
+ slug: entry.slug,
116
+ platform,
117
+ recordedAt: new Date().toISOString(),
118
+ };
119
+ kept.push({
120
+ key: scheduleKey,
121
+ id: canonical._id,
122
+ });
123
+
124
+ const extras = matchingPosts.filter((post) => post._id !== canonical._id);
125
+ for (const duplicate of extras) {
126
+ const duplicateRecord = {
127
+ key: scheduleKey,
128
+ id: duplicate._id,
129
+ platform,
130
+ slug: entry.slug,
131
+ };
132
+ if (options.cancelDuplicates) {
133
+ duplicateRecord.cancelled = await zernio.deletePost(duplicate._id);
134
+ }
135
+ duplicates.push(duplicateRecord);
136
+ }
137
+ }
138
+ }
139
+
140
+ writeScheduleState(options.statePath || DEFAULT_STATE_PATH, { scheduled });
141
+ return {
142
+ duplicates,
143
+ kept,
144
+ statePath: options.statePath || DEFAULT_STATE_PATH,
145
+ };
146
+ }
147
+
148
+ if (require.main === module) {
149
+ reconcileCampaignState(parseArgs(process.argv.slice(2)))
150
+ .then((result) => {
151
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
152
+ })
153
+ .catch((error) => {
154
+ console.error(error && error.message ? error.message : error);
155
+ process.exit(1);
156
+ });
157
+ }
158
+
159
+ module.exports = {
160
+ CAMPAIGN_MARKERS,
161
+ normalizePlatform,
162
+ parseArgs,
163
+ reconcileCampaignState,
164
+ selectCanonicalPost,
165
+ };
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { publishInstagramThumbGate } = require('./publish-instagram-thumbgate');
7
+ const {
8
+ DEFAULT_TIMEZONE,
9
+ buildCampaignEntries,
10
+ defaultCampaignSchedule,
11
+ } = require('./publish-thumbgate-launch');
12
+ const {
13
+ getConnectedAccounts,
14
+ groupAccountsByPlatform,
15
+ schedulePost,
16
+ } = require('./publishers/zernio');
17
+
18
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
19
+ const DEFAULT_STATE_PATH = path.join(REPO_ROOT, '.thumbgate', 'social-campaign-schedule-state.json');
20
+
21
+ function parseArgs(argv = []) {
22
+ const options = {
23
+ dryRun: false,
24
+ platforms: [],
25
+ scheduleTimes: [],
26
+ statePath: DEFAULT_STATE_PATH,
27
+ timezone: DEFAULT_TIMEZONE,
28
+ };
29
+
30
+ for (let index = 0; index < argv.length; index += 1) {
31
+ const token = argv[index];
32
+ if (token === '--dry-run') {
33
+ options.dryRun = true;
34
+ continue;
35
+ }
36
+
37
+ if (token.startsWith('--platforms=')) {
38
+ options.platforms = token.slice('--platforms='.length).split(',').map((value) => value.trim()).filter(Boolean);
39
+ continue;
40
+ }
41
+
42
+ if (token === '--platforms' && argv[index + 1]) {
43
+ options.platforms = String(argv[index + 1]).split(',').map((value) => value.trim()).filter(Boolean);
44
+ index += 1;
45
+ continue;
46
+ }
47
+
48
+ if (token.startsWith('--times=')) {
49
+ options.scheduleTimes = token.slice('--times='.length).split(',').map((value) => value.trim()).filter(Boolean);
50
+ continue;
51
+ }
52
+
53
+ if (token === '--times' && argv[index + 1]) {
54
+ options.scheduleTimes = String(argv[index + 1]).split(',').map((value) => value.trim()).filter(Boolean);
55
+ index += 1;
56
+ continue;
57
+ }
58
+
59
+ if (token.startsWith('--timezone=')) {
60
+ options.timezone = token.slice('--timezone='.length).trim() || DEFAULT_TIMEZONE;
61
+ continue;
62
+ }
63
+
64
+ if (token.startsWith('--state-path=')) {
65
+ options.statePath = token.slice('--state-path='.length).trim() || DEFAULT_STATE_PATH;
66
+ continue;
67
+ }
68
+
69
+ if (token === '--timezone' && argv[index + 1]) {
70
+ options.timezone = String(argv[index + 1]).trim() || DEFAULT_TIMEZONE;
71
+ index += 1;
72
+ continue;
73
+ }
74
+
75
+ if (token === '--state-path' && argv[index + 1]) {
76
+ options.statePath = String(argv[index + 1]).trim() || DEFAULT_STATE_PATH;
77
+ index += 1;
78
+ }
79
+ }
80
+
81
+ return options;
82
+ }
83
+
84
+ function readScheduleState(statePath = DEFAULT_STATE_PATH) {
85
+ if (!fs.existsSync(statePath)) {
86
+ return { scheduled: {} };
87
+ }
88
+
89
+ try {
90
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
91
+ } catch {
92
+ return { scheduled: {} };
93
+ }
94
+ }
95
+
96
+ function writeScheduleState(statePath = DEFAULT_STATE_PATH, state = { scheduled: {} }) {
97
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
98
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
99
+ }
100
+
101
+ function buildScheduleKey({ slug, platform, scheduledFor }) {
102
+ return `${scheduledFor}::${slug}::${platform}`;
103
+ }
104
+
105
+ async function scheduleCampaign(options = {}, api = {}) {
106
+ const scheduleApi = {
107
+ getConnectedAccounts: api.getConnectedAccounts || getConnectedAccounts,
108
+ groupAccountsByPlatform: api.groupAccountsByPlatform || groupAccountsByPlatform,
109
+ publishInstagramThumbGate: api.publishInstagramThumbGate || publishInstagramThumbGate,
110
+ schedulePost: api.schedulePost || schedulePost,
111
+ };
112
+
113
+ const campaignEntries = buildCampaignEntries();
114
+ const scheduleTimes = options.scheduleTimes && options.scheduleTimes.length > 0
115
+ ? options.scheduleTimes
116
+ : defaultCampaignSchedule();
117
+ const platforms = options.platforms && options.platforms.length > 0
118
+ ? options.platforms
119
+ : ['twitter', 'linkedin', 'instagram'];
120
+ const statePath = options.statePath || DEFAULT_STATE_PATH;
121
+ const state = readScheduleState(statePath);
122
+ const scheduledState = state.scheduled && typeof state.scheduled === 'object' ? state.scheduled : {};
123
+ const accounts = await scheduleApi.getConnectedAccounts();
124
+ const groupedAccounts = scheduleApi.groupAccountsByPlatform(accounts);
125
+ const results = {
126
+ dryRun: options.dryRun === true,
127
+ statePath,
128
+ timezone: options.timezone || DEFAULT_TIMEZONE,
129
+ scheduleTimes,
130
+ platforms,
131
+ scheduled: [],
132
+ skipped: [],
133
+ errors: [],
134
+ };
135
+
136
+ for (let index = 0; index < campaignEntries.length; index += 1) {
137
+ const entry = campaignEntries[index];
138
+ const scheduledFor = scheduleTimes[index];
139
+ if (!scheduledFor) {
140
+ results.skipped.push({ slug: entry.slug, reason: 'missing_schedule_time' });
141
+ continue;
142
+ }
143
+
144
+ for (const platform of platforms) {
145
+ const normalizedPlatform = String(platform || '').trim().toLowerCase();
146
+ const platformAccounts = groupedAccounts.get(normalizedPlatform) || [];
147
+ if (platformAccounts.length === 0) {
148
+ results.skipped.push({ slug: entry.slug, platform: normalizedPlatform, reason: 'not_connected' });
149
+ continue;
150
+ }
151
+
152
+ const content = entry.posts[normalizedPlatform];
153
+ if (!content) {
154
+ results.skipped.push({ slug: entry.slug, platform: normalizedPlatform, reason: 'no_content' });
155
+ continue;
156
+ }
157
+
158
+ const scheduleKey = buildScheduleKey({
159
+ slug: entry.slug,
160
+ platform: normalizedPlatform,
161
+ scheduledFor,
162
+ });
163
+ if (!results.dryRun && scheduledState[scheduleKey]) {
164
+ results.skipped.push({
165
+ slug: entry.slug,
166
+ platform: normalizedPlatform,
167
+ reason: 'already_scheduled',
168
+ scheduledFor,
169
+ existing: scheduledState[scheduleKey],
170
+ });
171
+ continue;
172
+ }
173
+
174
+ if (results.dryRun) {
175
+ results.scheduled.push({
176
+ slug: entry.slug,
177
+ platform: normalizedPlatform,
178
+ scheduledFor,
179
+ dryRun: true,
180
+ content,
181
+ });
182
+ continue;
183
+ }
184
+
185
+ const utm = {
186
+ source: normalizedPlatform === 'twitter' ? 'x' : normalizedPlatform,
187
+ medium: 'organic_social',
188
+ campaign: 'first_customer_push',
189
+ };
190
+
191
+ try {
192
+ if (normalizedPlatform === 'instagram') {
193
+ const scheduledResult = await scheduleApi.publishInstagramThumbGate({
194
+ caption: content,
195
+ schedule: scheduledFor,
196
+ timezone: options.timezone || DEFAULT_TIMEZONE,
197
+ utm,
198
+ });
199
+ scheduledState[scheduleKey] = {
200
+ id: scheduledResult.postId || scheduledResult.id || null,
201
+ scheduledFor,
202
+ slug: entry.slug,
203
+ platform: normalizedPlatform,
204
+ recordedAt: new Date().toISOString(),
205
+ };
206
+ results.scheduled.push({
207
+ slug: entry.slug,
208
+ platform: normalizedPlatform,
209
+ scheduledFor,
210
+ result: scheduledResult,
211
+ });
212
+ continue;
213
+ }
214
+
215
+ const scheduledResult = await scheduleApi.schedulePost(
216
+ content,
217
+ platformAccounts,
218
+ scheduledFor,
219
+ options.timezone || DEFAULT_TIMEZONE,
220
+ { utm }
221
+ );
222
+ scheduledState[scheduleKey] = {
223
+ id: scheduledResult.id || scheduledResult.post?._id || null,
224
+ scheduledFor,
225
+ slug: entry.slug,
226
+ platform: normalizedPlatform,
227
+ recordedAt: new Date().toISOString(),
228
+ };
229
+ results.scheduled.push({
230
+ slug: entry.slug,
231
+ platform: normalizedPlatform,
232
+ scheduledFor,
233
+ result: scheduledResult,
234
+ });
235
+ } catch (error) {
236
+ results.errors.push({
237
+ slug: entry.slug,
238
+ platform: normalizedPlatform,
239
+ error: error && error.message ? error.message : String(error),
240
+ });
241
+ }
242
+ }
243
+ }
244
+
245
+ if (!results.dryRun) {
246
+ writeScheduleState(statePath, { scheduled: scheduledState });
247
+ }
248
+
249
+ return results;
250
+ }
251
+
252
+ async function main() {
253
+ const options = parseArgs(process.argv.slice(2));
254
+ const results = await scheduleCampaign(options);
255
+ process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
256
+ if (results.errors.length > 0) {
257
+ process.exitCode = 1;
258
+ }
259
+ }
260
+
261
+ if (require.main === module) {
262
+ main().catch((error) => {
263
+ console.error(error && error.message ? error.message : error);
264
+ process.exit(1);
265
+ });
266
+ }
267
+
268
+ module.exports = {
269
+ buildScheduleKey,
270
+ DEFAULT_STATE_PATH,
271
+ parseArgs,
272
+ readScheduleState,
273
+ scheduleCampaign,
274
+ writeScheduleState,
275
+ };
@@ -0,0 +1,185 @@
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
+ };