thumbgate 0.9.10

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 (364) hide show
  1. package/.claude-plugin/README.md +134 -0
  2. package/.claude-plugin/bundle/icon.png +0 -0
  3. package/.claude-plugin/bundle/icon.svg +18 -0
  4. package/.claude-plugin/bundle/server/index.js +24 -0
  5. package/.claude-plugin/marketplace.json +36 -0
  6. package/.claude-plugin/plugin.json +21 -0
  7. package/.well-known/mcp/server-card.json +231 -0
  8. package/LICENSE +21 -0
  9. package/README.md +375 -0
  10. package/adapters/README.md +9 -0
  11. package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
  12. package/adapters/chatgpt/INSTALL.md +83 -0
  13. package/adapters/chatgpt/openapi.yaml +1281 -0
  14. package/adapters/claude/.mcp.json +14 -0
  15. package/adapters/codex/config.toml +9 -0
  16. package/adapters/gemini/function-declarations.json +224 -0
  17. package/adapters/mcp/server-stdio.js +788 -0
  18. package/adapters/opencode/opencode.json +15 -0
  19. package/bin/cli.js +1484 -0
  20. package/bin/memory.sh +64 -0
  21. package/bin/obsidian-sync.sh +20 -0
  22. package/bin/postinstall.js +37 -0
  23. package/config/build-metadata.json +4 -0
  24. package/config/e2e-critical-flows.json +45 -0
  25. package/config/gate-templates.json +77 -0
  26. package/config/gates/claim-verification.json +29 -0
  27. package/config/gates/computer-use.json +39 -0
  28. package/config/gates/default.json +117 -0
  29. package/config/github-about.json +25 -0
  30. package/config/mcp-allowlists.json +135 -0
  31. package/config/model-tiers.json +33 -0
  32. package/config/partner-routing.json +132 -0
  33. package/config/policy-bundles/constrained-v1.json +64 -0
  34. package/config/policy-bundles/default-v1.json +91 -0
  35. package/config/rubrics/default-v1.json +52 -0
  36. package/config/skill-packs/react-testing.json +23 -0
  37. package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
  38. package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
  39. package/config/skill-specs/pr-reviewer.json +9 -0
  40. package/config/skill-specs/release-status.json +9 -0
  41. package/config/skill-specs/ticket-triage.json +9 -0
  42. package/config/subagent-profiles.json +32 -0
  43. package/config/tessl-tiles.json +29 -0
  44. package/config/thumbgate-settings.managed.json +12 -0
  45. package/openapi/openapi.yaml +1281 -0
  46. package/package.json +283 -0
  47. package/plugins/amp-skill/INSTALL.md +52 -0
  48. package/plugins/amp-skill/SKILL.md +64 -0
  49. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
  50. package/plugins/claude-codex-bridge/.mcp.json +12 -0
  51. package/plugins/claude-codex-bridge/INSTALL.md +43 -0
  52. package/plugins/claude-codex-bridge/README.md +46 -0
  53. package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
  54. package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
  55. package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
  56. package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
  57. package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
  58. package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
  59. package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
  60. package/plugins/claude-skill/INSTALL.md +55 -0
  61. package/plugins/claude-skill/SKILL.md +46 -0
  62. package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
  63. package/plugins/codex-profile/.mcp.json +12 -0
  64. package/plugins/codex-profile/AGENTS.md +20 -0
  65. package/plugins/codex-profile/INSTALL.md +66 -0
  66. package/plugins/codex-profile/README.md +37 -0
  67. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
  68. package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
  69. package/plugins/cursor-marketplace/LICENSE +21 -0
  70. package/plugins/cursor-marketplace/README.md +124 -0
  71. package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
  72. package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
  73. package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
  74. package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
  75. package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
  76. package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
  77. package/plugins/cursor-marketplace/mcp.json +12 -0
  78. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
  79. package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
  80. package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
  81. package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
  82. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
  83. package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
  84. package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
  85. package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
  86. package/plugins/gemini-extension/INSTALL.md +92 -0
  87. package/plugins/gemini-extension/gemini_prompt.txt +14 -0
  88. package/plugins/gemini-extension/tool_contract.json +45 -0
  89. package/plugins/opencode-profile/INSTALL.md +57 -0
  90. package/public/assets/instagram-card.png +0 -0
  91. package/public/assets/tiktok-agent-memory.mp4 +0 -0
  92. package/public/blog.html +400 -0
  93. package/public/dashboard.html +1093 -0
  94. package/public/guide.html +317 -0
  95. package/public/index.html +1014 -0
  96. package/public/learn/agent-harness-pattern.html +180 -0
  97. package/public/learn/ai-agent-persistent-memory.html +202 -0
  98. package/public/learn/learn.css +45 -0
  99. package/public/learn/mcp-pre-action-gates-explained.html +172 -0
  100. package/public/learn/stop-ai-agent-force-push.html +134 -0
  101. package/public/learn/vibe-coding-safety-net.html +142 -0
  102. package/public/learn.html +213 -0
  103. package/public/lessons.html +650 -0
  104. package/public/vercel.json +8 -0
  105. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  106. package/scripts/a2ui-engine.js +73 -0
  107. package/scripts/access-anomaly-detector.js +12 -0
  108. package/scripts/adk-consolidator.js +266 -0
  109. package/scripts/agent-readiness.js +220 -0
  110. package/scripts/agent-security-hardening.js +227 -0
  111. package/scripts/agentic-data-pipeline.js +847 -0
  112. package/scripts/analytics-report.js +328 -0
  113. package/scripts/analytics-window.js +158 -0
  114. package/scripts/async-job-runner.js +1001 -0
  115. package/scripts/audit-trail.js +398 -0
  116. package/scripts/auto-promote-gates.js +299 -0
  117. package/scripts/auto-wire-hooks.js +312 -0
  118. package/scripts/autonomous-sales-agent.js +39 -0
  119. package/scripts/autoresearch-runner.js +216 -0
  120. package/scripts/background-agent-governance.js +237 -0
  121. package/scripts/behavioral-extraction.js +97 -0
  122. package/scripts/belief-update.js +84 -0
  123. package/scripts/billing.js +2438 -0
  124. package/scripts/bot-detector.js +50 -0
  125. package/scripts/budget-guard.js +173 -0
  126. package/scripts/build-claude-mcpb.js +189 -0
  127. package/scripts/build-metadata.js +97 -0
  128. package/scripts/check-congruence.js +322 -0
  129. package/scripts/cli-feedback.js +135 -0
  130. package/scripts/cli-telemetry.js +87 -0
  131. package/scripts/cloudflare-dynamic-sandbox.js +315 -0
  132. package/scripts/code-reasoning.js +350 -0
  133. package/scripts/codegraph-context.js +466 -0
  134. package/scripts/commercial-offer.js +56 -0
  135. package/scripts/computer-use-firewall.js +250 -0
  136. package/scripts/context-engine.js +694 -0
  137. package/scripts/contextfs.js +1287 -0
  138. package/scripts/conversation-context.js +119 -0
  139. package/scripts/creator-campaigns.js +239 -0
  140. package/scripts/daemon-manager.js +108 -0
  141. package/scripts/daily-digest.js +11 -0
  142. package/scripts/dashboard-render-spec.js +395 -0
  143. package/scripts/dashboard.js +1058 -0
  144. package/scripts/data-governance.js +173 -0
  145. package/scripts/delegation-runtime.js +900 -0
  146. package/scripts/deploy-gcp.sh +44 -0
  147. package/scripts/deploy-policy.js +263 -0
  148. package/scripts/disagreement-mining.js +315 -0
  149. package/scripts/dispatch-brief.js +159 -0
  150. package/scripts/distribution-surfaces.js +44 -0
  151. package/scripts/dpo-optimizer.js +209 -0
  152. package/scripts/ephemeral-agent-store.js +219 -0
  153. package/scripts/eval-harness.js +56 -0
  154. package/scripts/evolution-state.js +241 -0
  155. package/scripts/experiment-tracker.js +267 -0
  156. package/scripts/export-databricks-bundle.js +242 -0
  157. package/scripts/export-dpo-pairs.js +345 -0
  158. package/scripts/export-kto-pairs.js +310 -0
  159. package/scripts/export-training.js +448 -0
  160. package/scripts/failure-diagnostics.js +558 -0
  161. package/scripts/feedback-attribution.js +313 -0
  162. package/scripts/feedback-fallback.js +111 -0
  163. package/scripts/feedback-history-distiller.js +391 -0
  164. package/scripts/feedback-inbox-read.js +162 -0
  165. package/scripts/feedback-loop.js +1887 -0
  166. package/scripts/feedback-paths.js +145 -0
  167. package/scripts/feedback-quality.js +139 -0
  168. package/scripts/feedback-root-consolidator.js +238 -0
  169. package/scripts/feedback-schema.js +426 -0
  170. package/scripts/feedback-session.js +286 -0
  171. package/scripts/feedback-to-memory.js +185 -0
  172. package/scripts/feedback-to-rules.js +163 -0
  173. package/scripts/filesystem-search.js +404 -0
  174. package/scripts/funnel-analytics.js +35 -0
  175. package/scripts/gate-satisfy.js +42 -0
  176. package/scripts/gate-stats.js +116 -0
  177. package/scripts/gate-templates.js +70 -0
  178. package/scripts/gates-engine.js +816 -0
  179. package/scripts/generate-paperbanana-diagrams.sh +99 -0
  180. package/scripts/generate-pretool-hook.sh +40 -0
  181. package/scripts/github-about.js +350 -0
  182. package/scripts/github-outreach.js +65 -0
  183. package/scripts/gtm-revenue-loop.js +520 -0
  184. package/scripts/hallucination-detector.js +226 -0
  185. package/scripts/hf-papers.js +317 -0
  186. package/scripts/history-distiller.js +200 -0
  187. package/scripts/hook-auto-capture.sh +95 -0
  188. package/scripts/hook-stop-pr-thread-check.sh +68 -0
  189. package/scripts/hook-stop-self-score.sh +51 -0
  190. package/scripts/hook-stop-verify-deploy.sh +31 -0
  191. package/scripts/hook-thumbgate-cache-updater.js +48 -0
  192. package/scripts/hook-verify-before-done.sh +20 -0
  193. package/scripts/hosted-config.js +170 -0
  194. package/scripts/hybrid-feedback-context.js +676 -0
  195. package/scripts/install-mcp.js +159 -0
  196. package/scripts/intent-router.js +392 -0
  197. package/scripts/internal-agent-bootstrap.js +490 -0
  198. package/scripts/jsonl-watcher.js +155 -0
  199. package/scripts/lesson-db.js +613 -0
  200. package/scripts/lesson-inference.js +315 -0
  201. package/scripts/lesson-retrieval.js +95 -0
  202. package/scripts/lesson-rotation.js +137 -0
  203. package/scripts/lesson-search.js +644 -0
  204. package/scripts/lesson-synthesis.js +196 -0
  205. package/scripts/license.js +50 -0
  206. package/scripts/local-model-profile.js +383 -0
  207. package/scripts/markdown-escape.js +12 -0
  208. package/scripts/marketing-experiment.js +671 -0
  209. package/scripts/mcp-config.js +149 -0
  210. package/scripts/mcp-policy.js +99 -0
  211. package/scripts/memalign-recall.js +111 -0
  212. package/scripts/memory-firewall.js +222 -0
  213. package/scripts/memory-migration.js +296 -0
  214. package/scripts/meta-policy.js +194 -0
  215. package/scripts/metered-billing.js +16 -0
  216. package/scripts/model-tier-router.js +301 -0
  217. package/scripts/money-watcher.js +71 -0
  218. package/scripts/multi-hop-recall.js +240 -0
  219. package/scripts/natural-language-harness.js +330 -0
  220. package/scripts/obsidian-export.js +712 -0
  221. package/scripts/operational-dashboard.js +103 -0
  222. package/scripts/operational-summary.js +93 -0
  223. package/scripts/optimize-context.js +17 -0
  224. package/scripts/org-dashboard.js +201 -0
  225. package/scripts/partner-orchestration.js +146 -0
  226. package/scripts/per-step-scoring.js +165 -0
  227. package/scripts/perplexity-marketing.js +466 -0
  228. package/scripts/pii-scanner.js +153 -0
  229. package/scripts/plan-gate.js +154 -0
  230. package/scripts/post-everywhere.js +308 -0
  231. package/scripts/post-to-x-retry.sh +22 -0
  232. package/scripts/post-to-x.js +369 -0
  233. package/scripts/pr-manager.js +236 -0
  234. package/scripts/predictive-insights.js +356 -0
  235. package/scripts/principle-extractor.js +162 -0
  236. package/scripts/pro-features.js +40 -0
  237. package/scripts/pro-local-dashboard.js +174 -0
  238. package/scripts/problem-detail.js +53 -0
  239. package/scripts/product-feedback.js +134 -0
  240. package/scripts/profile-router.js +245 -0
  241. package/scripts/prompt-dlp.js +221 -0
  242. package/scripts/prompt-guard.js +83 -0
  243. package/scripts/prove-adapters.js +863 -0
  244. package/scripts/prove-attribution.js +365 -0
  245. package/scripts/prove-automation.js +653 -0
  246. package/scripts/prove-autoresearch.js +304 -0
  247. package/scripts/prove-claim-verification.js +277 -0
  248. package/scripts/prove-cloudflare-sandbox.js +163 -0
  249. package/scripts/prove-data-pipeline.js +410 -0
  250. package/scripts/prove-data-quality.js +227 -0
  251. package/scripts/prove-evolution.js +352 -0
  252. package/scripts/prove-harnesses.js +287 -0
  253. package/scripts/prove-intelligence.js +259 -0
  254. package/scripts/prove-lancedb.js +371 -0
  255. package/scripts/prove-local-intelligence.js +342 -0
  256. package/scripts/prove-loop-closure.js +263 -0
  257. package/scripts/prove-predictive-insights.js +357 -0
  258. package/scripts/prove-runtime.js +350 -0
  259. package/scripts/prove-seo-gsd.js +234 -0
  260. package/scripts/prove-settings.js +279 -0
  261. package/scripts/prove-subway-upgrades.js +277 -0
  262. package/scripts/prove-tessl.js +229 -0
  263. package/scripts/prove-training-export.js +327 -0
  264. package/scripts/prove-workflow-contract.js +116 -0
  265. package/scripts/prove-xmemory.js +332 -0
  266. package/scripts/publish-decision.js +133 -0
  267. package/scripts/pulse.js +80 -0
  268. package/scripts/rate-limiter.js +125 -0
  269. package/scripts/reddit-dm-outreach.js +182 -0
  270. package/scripts/reddit-monitor-cron.sh +26 -0
  271. package/scripts/reflector-agent.js +221 -0
  272. package/scripts/reminder-engine.js +132 -0
  273. package/scripts/revenue-status.js +472 -0
  274. package/scripts/risk-scorer.js +458 -0
  275. package/scripts/rlaif-self-audit.js +129 -0
  276. package/scripts/rubric-engine.js +230 -0
  277. package/scripts/schedule-manager.js +251 -0
  278. package/scripts/secret-scanner.js +414 -0
  279. package/scripts/self-heal.js +147 -0
  280. package/scripts/self-healing-check.js +188 -0
  281. package/scripts/semantic-layer.js +98 -0
  282. package/scripts/seo-gsd.js +1153 -0
  283. package/scripts/settings-hierarchy.js +214 -0
  284. package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
  285. package/scripts/skill-exporter.js +262 -0
  286. package/scripts/skill-generator.js +446 -0
  287. package/scripts/skill-materializer.js +134 -0
  288. package/scripts/skill-packs.js +136 -0
  289. package/scripts/skill-proposer.js +99 -0
  290. package/scripts/skill-quality-tracker.js +284 -0
  291. package/scripts/slo-alert-engine.js +14 -0
  292. package/scripts/slow-loop.js +72 -0
  293. package/scripts/social-analytics/db/schema.sql +32 -0
  294. package/scripts/social-analytics/digest.js +256 -0
  295. package/scripts/social-analytics/generate-instagram-card.js +97 -0
  296. package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
  297. package/scripts/social-analytics/mcp-server.js +289 -0
  298. package/scripts/social-analytics/normalizer.js +580 -0
  299. package/scripts/social-analytics/notify.js +162 -0
  300. package/scripts/social-analytics/poll-all.js +107 -0
  301. package/scripts/social-analytics/pollers/github.js +195 -0
  302. package/scripts/social-analytics/pollers/instagram.js +253 -0
  303. package/scripts/social-analytics/pollers/linkedin.js +330 -0
  304. package/scripts/social-analytics/pollers/plausible.js +247 -0
  305. package/scripts/social-analytics/pollers/reddit.js +306 -0
  306. package/scripts/social-analytics/pollers/threads.js +233 -0
  307. package/scripts/social-analytics/pollers/tiktok.js +203 -0
  308. package/scripts/social-analytics/pollers/x.js +227 -0
  309. package/scripts/social-analytics/pollers/youtube.js +304 -0
  310. package/scripts/social-analytics/pollers/zernio.js +180 -0
  311. package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
  312. package/scripts/social-analytics/publishers/devto.js +122 -0
  313. package/scripts/social-analytics/publishers/instagram.js +317 -0
  314. package/scripts/social-analytics/publishers/linkedin.js +294 -0
  315. package/scripts/social-analytics/publishers/reddit.js +390 -0
  316. package/scripts/social-analytics/publishers/threads.js +275 -0
  317. package/scripts/social-analytics/publishers/tiktok.js +217 -0
  318. package/scripts/social-analytics/publishers/x.js +259 -0
  319. package/scripts/social-analytics/publishers/youtube.js +223 -0
  320. package/scripts/social-analytics/publishers/zernio.js +209 -0
  321. package/scripts/social-analytics/run-digest.js +34 -0
  322. package/scripts/social-analytics/store.js +257 -0
  323. package/scripts/social-analytics/utm.js +143 -0
  324. package/scripts/social-pipeline.js +2628 -0
  325. package/scripts/social-quality-gate.js +18 -0
  326. package/scripts/social-reply-monitor.js +445 -0
  327. package/scripts/status-dashboard.js +155 -0
  328. package/scripts/statusline-lesson.js +16 -0
  329. package/scripts/statusline-tower.js +8 -0
  330. package/scripts/statusline.sh +116 -0
  331. package/scripts/stripe-live-status.js +115 -0
  332. package/scripts/subagent-profiles.js +79 -0
  333. package/scripts/sync-gh-secrets-from-env.sh +70 -0
  334. package/scripts/sync-github-about.js +52 -0
  335. package/scripts/sync-version.js +451 -0
  336. package/scripts/synthetic-dpo.js +234 -0
  337. package/scripts/telemetry-analytics.js +821 -0
  338. package/scripts/tessl-export.js +371 -0
  339. package/scripts/test-coverage.js +120 -0
  340. package/scripts/thompson-sampling.js +417 -0
  341. package/scripts/thumbgate-search.js +189 -0
  342. package/scripts/tool-kpi-tracker.js +12 -0
  343. package/scripts/tool-registry.js +811 -0
  344. package/scripts/train_from_feedback.py +910 -0
  345. package/scripts/user-profile.js +78 -0
  346. package/scripts/validate-feedback.js +580 -0
  347. package/scripts/validate-workflow-contract.js +287 -0
  348. package/scripts/vector-store.js +198 -0
  349. package/scripts/verification-loop.js +291 -0
  350. package/scripts/verify-obsidian-setup.sh +269 -0
  351. package/scripts/verify-run.js +269 -0
  352. package/scripts/webhook-delivery.js +62 -0
  353. package/scripts/weekly-auto-post.js +124 -0
  354. package/scripts/workflow-runs.js +154 -0
  355. package/scripts/workflow-sprint-intake.js +475 -0
  356. package/scripts/workspace-evolver.js +374 -0
  357. package/scripts/x-autonomous-marketing.js +139 -0
  358. package/scripts/xmemory-lite.js +405 -0
  359. package/skills/agent-memory/SKILL.md +97 -0
  360. package/skills/solve-architecture-autonomy/SKILL.md +17 -0
  361. package/skills/solve-architecture-autonomy/tool.js +33 -0
  362. package/skills/thumbgate/SKILL.md +114 -0
  363. package/skills/thumbgate-feedback/SKILL.md +49 -0
  364. package/src/api/server.js +4208 -0
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * PII Scanner — LogSentinel-inspired PII detection for ThumbGate.
6
+ *
7
+ * Scans feedback content, context packs, and DPO exports for PII patterns.
8
+ * Applies hierarchical sensitivity labels. Redacts or rejects as configured.
9
+ * Builds on secret-scanner.js patterns + adds PII-specific detectors.
10
+ */
11
+
12
+ const { SECRET_PATTERNS, redactText: redactSecrets } = require('./secret-scanner');
13
+
14
+ // PII patterns beyond secrets — emails, phone numbers, SSNs, card numbers, IP addresses
15
+ const PII_PATTERNS = [
16
+ { id: 'email', label: 'Email address', regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, sensitivity: 'sensitive' },
17
+ { id: 'phone_us', label: 'US phone number', regex: /\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, sensitivity: 'sensitive' },
18
+ { id: 'ssn', label: 'Social Security Number', regex: /\b\d{3}[-.\s]?\d{2}[-.\s]?\d{4}\b/g, sensitivity: 'restricted' },
19
+ { id: 'credit_card', label: 'Credit card number', regex: /\b(?:4\d{3}|5[1-5]\d{2}|3[47]\d{2}|6(?:011|5\d{2}))[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/g, sensitivity: 'restricted' },
20
+ { id: 'ip_address', label: 'IP address', regex: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g, sensitivity: 'internal' },
21
+ { id: 'aws_account', label: 'AWS account ID', regex: /\b\d{12}\b/g, sensitivity: 'internal' },
22
+ ];
23
+
24
+ const SENSITIVITY_LEVELS = ['public', 'internal', 'sensitive', 'restricted'];
25
+
26
+ function sensitivityRank(level) {
27
+ const idx = SENSITIVITY_LEVELS.indexOf(level);
28
+ return idx >= 0 ? idx : 0;
29
+ }
30
+
31
+ /**
32
+ * Scan text for PII and return findings with sensitivity labels.
33
+ */
34
+ function scanForPii(text) {
35
+ if (!text) return { findings: [], highestSensitivity: 'public', hasPii: false };
36
+ const str = String(text);
37
+ const findings = [];
38
+ let highest = 'public';
39
+
40
+ for (const pattern of PII_PATTERNS) {
41
+ // Reset regex lastIndex for global patterns
42
+ pattern.regex.lastIndex = 0;
43
+ const matches = str.match(pattern.regex);
44
+ if (matches && matches.length > 0) {
45
+ findings.push({
46
+ id: pattern.id,
47
+ label: pattern.label,
48
+ sensitivity: pattern.sensitivity,
49
+ matchCount: matches.length,
50
+ sample: matches[0].slice(0, 4) + '***',
51
+ });
52
+ if (sensitivityRank(pattern.sensitivity) > sensitivityRank(highest)) {
53
+ highest = pattern.sensitivity;
54
+ }
55
+ }
56
+ }
57
+
58
+ return { findings, highestSensitivity: highest, hasPii: findings.length > 0 };
59
+ }
60
+
61
+ /**
62
+ * Redact PII from text. Returns redacted string.
63
+ */
64
+ function redactPii(text) {
65
+ if (!text) return '';
66
+ let redacted = String(text);
67
+ // First redact secrets (API keys, tokens)
68
+ redacted = redactSecrets(redacted);
69
+ // Then redact PII
70
+ for (const pattern of PII_PATTERNS) {
71
+ pattern.regex.lastIndex = 0;
72
+ redacted = redacted.replace(pattern.regex, `[REDACTED:${pattern.id}]`);
73
+ }
74
+ return redacted;
75
+ }
76
+
77
+ /**
78
+ * Assign a sensitivity label to a feedback entry based on content scan.
79
+ * Returns: { sensitivity, findings, redactedContent }
80
+ */
81
+ function classifyFeedback(feedbackEntry) {
82
+ const content = [
83
+ feedbackEntry.context || '',
84
+ feedbackEntry.whatWentWrong || '',
85
+ feedbackEntry.whatToChange || '',
86
+ feedbackEntry.whatWorked || '',
87
+ ].join('\n');
88
+
89
+ const scan = scanForPii(content);
90
+
91
+ return {
92
+ sensitivity: scan.highestSensitivity,
93
+ findings: scan.findings,
94
+ hasPii: scan.hasPii,
95
+ redactedContent: scan.hasPii ? redactPii(content) : content,
96
+ originalContentHash: require('crypto').createHash('sha256').update(content).digest('hex').slice(0, 16),
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Scan a DPO pair for PII. Returns scan result with pass/fail.
102
+ */
103
+ function scanDpoPair(pair) {
104
+ const chosen = scanForPii(pair.chosen || '');
105
+ const rejected = scanForPii(pair.rejected || '');
106
+ const prompt = scanForPii(pair.prompt || '');
107
+ const allFindings = [...prompt.findings, ...chosen.findings, ...rejected.findings];
108
+ const highest = SENSITIVITY_LEVELS[Math.max(
109
+ sensitivityRank(prompt.highestSensitivity),
110
+ sensitivityRank(chosen.highestSensitivity),
111
+ sensitivityRank(rejected.highestSensitivity),
112
+ )];
113
+
114
+ return {
115
+ hasPii: allFindings.length > 0,
116
+ highestSensitivity: highest,
117
+ findings: allFindings,
118
+ safe: sensitivityRank(highest) < sensitivityRank('sensitive'),
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Gate a DPO export: filter out pairs containing PII above threshold.
124
+ * Returns { safePairs, blockedPairs, blockedCount, totalScanned }.
125
+ */
126
+ function gateDpoExport(pairs, { maxSensitivity = 'internal' } = {}) {
127
+ const maxRank = sensitivityRank(maxSensitivity);
128
+ const safePairs = [];
129
+ const blockedPairs = [];
130
+
131
+ for (const pair of pairs) {
132
+ const scan = scanDpoPair(pair);
133
+ if (sensitivityRank(scan.highestSensitivity) <= maxRank) {
134
+ safePairs.push(pair);
135
+ } else {
136
+ blockedPairs.push({ pair, scan });
137
+ }
138
+ }
139
+
140
+ return {
141
+ safePairs,
142
+ blockedPairs,
143
+ blockedCount: blockedPairs.length,
144
+ totalScanned: pairs.length,
145
+ passRate: pairs.length > 0 ? Math.round((safePairs.length / pairs.length) * 1000) / 10 : 100,
146
+ };
147
+ }
148
+
149
+ module.exports = {
150
+ PII_PATTERNS, SENSITIVITY_LEVELS,
151
+ scanForPii, redactPii, classifyFeedback,
152
+ scanDpoPair, gateDpoExport, sensitivityRank,
153
+ };
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Gate validators
9
+ // ---------------------------------------------------------------------------
10
+
11
+ function countTableRows(content, sectionHeading) {
12
+ const sectionRegex = new RegExp(
13
+ `#+\\s*${sectionHeading}[^\\n]*\\n([\\s\\S]*?)(?=\\n#+\\s|$)`,
14
+ );
15
+ const match = content.match(sectionRegex);
16
+ if (!match) return 0;
17
+
18
+ const lines = match[1].split('\n').filter((l) => l.trim().startsWith('|'));
19
+ // Subtract header row and separator row
20
+ const dataRows = lines.filter(
21
+ (l) => !/^\|\s*-+/.test(l.trim()) && !/^\|\s*:?-+/.test(l.trim()),
22
+ );
23
+ // First row is the header
24
+ return Math.max(0, dataRows.length - 1);
25
+ }
26
+
27
+ function countContracts(content) {
28
+ const sectionRegex = /#+\s*Contracts[^\n]*\n([\s\S]*?)(?=\n#+\s|$)/;
29
+ const match = content.match(sectionRegex);
30
+ if (!match) return 0;
31
+
32
+ const section = match[1];
33
+ // Find code blocks and look for interface/type keywords inside them
34
+ const codeBlockRegex = /```[\s\S]*?```/g;
35
+ let count = 0;
36
+ let blockMatch;
37
+ while ((blockMatch = codeBlockRegex.exec(section)) !== null) {
38
+ const block = blockMatch[0];
39
+ const interfaceMatches = block.match(/\b(interface|type)\s+\w+/g);
40
+ if (interfaceMatches) count += interfaceMatches.length;
41
+ }
42
+ return count;
43
+ }
44
+
45
+ function countValidationScenarios(content) {
46
+ const sectionRegex =
47
+ /#+\s*Validation\s+Checklist[^\n]*\n([\s\S]*?)(?=\n#+\s|$)/;
48
+ const match = content.match(sectionRegex);
49
+ if (!match) return 0;
50
+
51
+ const lines = match[1].split('\n');
52
+ return lines.filter((l) => /^\s*-\s*\[\s*\]/.test(l)).length;
53
+ }
54
+
55
+ function getStatus(content) {
56
+ const match = content.match(/#+\s*Status[^\n]*\n\s*(\S+)/);
57
+ return match ? match[1].trim() : null;
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Main
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function validatePlan(content) {
65
+ const questionCount = countTableRows(content, 'Clarifying Questions Resolved');
66
+ const contractCount = countContracts(content);
67
+ const scenarioCount = countValidationScenarios(content);
68
+ const status = getStatus(content);
69
+
70
+ const gates = [
71
+ {
72
+ name: 'Clarifying Questions',
73
+ pass: questionCount >= 3,
74
+ detail: `${questionCount} questions resolved`,
75
+ },
76
+ {
77
+ name: 'Contracts Defined',
78
+ pass: contractCount >= 1,
79
+ detail: `${contractCount} interface${contractCount !== 1 ? 's' : ''} found`,
80
+ },
81
+ {
82
+ name: 'Validation Checklist',
83
+ pass: scenarioCount >= 2,
84
+ detail: `${scenarioCount} scenarios defined`,
85
+ },
86
+ {
87
+ name: 'Status',
88
+ pass: status !== 'COMPLETE',
89
+ detail:
90
+ status === 'COMPLETE'
91
+ ? 'COMPLETE (already finished — cannot re-approve)'
92
+ : `${status || 'UNKNOWN'} (not COMPLETE)`,
93
+ },
94
+ ];
95
+
96
+ const allPass = gates.every((g) => g.pass);
97
+ return { gates, allPass };
98
+ }
99
+
100
+ function formatReport(result) {
101
+ const lines = result.gates.map(
102
+ (g) => `${g.pass ? '✅' : '❌'} ${g.name}: ${g.detail}`,
103
+ );
104
+ lines.push('');
105
+ lines.push(
106
+ result.allPass
107
+ ? 'RESULT: PASS — all gates satisfied'
108
+ : 'RESULT: BLOCKED — resolve issues above before spawning agents',
109
+ );
110
+ return lines.join('\n');
111
+ }
112
+
113
+ function run() {
114
+ const args = process.argv.slice(2);
115
+ const jsonFlag = args.includes('--json');
116
+ const filePath = args.find((a) => a !== '--json');
117
+
118
+ if (!filePath) {
119
+ console.error('Usage: node scripts/plan-gate.js <plan-file.md> [--json]');
120
+ process.exit(1);
121
+ }
122
+
123
+ const resolved = path.resolve(filePath);
124
+ if (!fs.existsSync(resolved)) {
125
+ console.error(`File not found: ${resolved}`);
126
+ process.exit(1);
127
+ }
128
+
129
+ const content = fs.readFileSync(resolved, 'utf-8');
130
+ const result = validatePlan(content);
131
+
132
+ if (jsonFlag) {
133
+ console.log(JSON.stringify(result, null, 2));
134
+ } else {
135
+ console.log(formatReport(result));
136
+ }
137
+
138
+ process.exit(result.allPass ? 0 : 1);
139
+ }
140
+
141
+ // Export for testing
142
+ module.exports = {
143
+ validatePlan,
144
+ formatReport,
145
+ countTableRows,
146
+ countContracts,
147
+ countValidationScenarios,
148
+ getStatus,
149
+ };
150
+
151
+ // Run only when executed directly
152
+ if (require.main === module) {
153
+ run();
154
+ }
@@ -0,0 +1,308 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * post-everywhere.js
5
+ * Unified CLI to post content to all social platforms from a single markdown post file.
6
+ *
7
+ * Usage:
8
+ * node scripts/post-everywhere.js docs/marketing/reddit-cursor-post.md
9
+ * node scripts/post-everywhere.js docs/marketing/reddit-cursor-post.md --dry-run
10
+ * node scripts/post-everywhere.js docs/marketing/reddit-cursor-post.md --platforms=reddit,x,devto
11
+ *
12
+ * Post file format (markdown with metadata):
13
+ * # Reddit Post: r/cursor
14
+ * **Subreddit:** r/cursor
15
+ * **Title:** ...
16
+ * **Body:** ...
17
+ * **Comment (post immediately after):** ...
18
+ *
19
+ * The script parses the markdown, extracts platform-specific fields, and dispatches to
20
+ * the appropriate publisher module.
21
+ *
22
+ * Env vars: see individual publisher modules for required credentials per platform.
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const { tagUrlsInText } = require('./social-analytics/utm');
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Publisher imports (lazy — only loaded when needed)
31
+ // ---------------------------------------------------------------------------
32
+
33
+ function getPublisher(platform) {
34
+ const publishers = {
35
+ reddit: () => require('./social-analytics/publishers/reddit.js'),
36
+ x: () => require('./post-to-x.js'),
37
+ linkedin: () => require('./social-analytics/publishers/linkedin.js'),
38
+ devto: () => require('./social-analytics/publishers/devto.js'),
39
+ threads: () => require('./social-analytics/publishers/threads.js'),
40
+ instagram: () => require('./social-analytics/publishers/instagram.js'),
41
+ tiktok: () => require('./social-analytics/publishers/tiktok.js'),
42
+ youtube: () => require('./social-analytics/publishers/youtube.js'),
43
+ };
44
+ const loader = publishers[platform];
45
+ if (!loader) throw new Error(`Unknown platform: ${platform}`);
46
+ return loader();
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Markdown parser
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Parse a marketing post markdown file into structured fields.
55
+ * Extracts: subreddit, title, body, comment, platform hints.
56
+ */
57
+ function parsePostFile(filePath) {
58
+ const raw = fs.readFileSync(filePath, 'utf8');
59
+ const lines = raw.split('\n');
60
+
61
+ const result = {
62
+ platform: null,
63
+ subreddit: null,
64
+ title: null,
65
+ body: null,
66
+ comment: null,
67
+ tags: [],
68
+ };
69
+
70
+ // Detect platform from header
71
+ const header = lines[0] || '';
72
+ if (/reddit/i.test(header)) result.platform = 'reddit';
73
+ else if (/obsidian/i.test(header)) result.platform = 'reddit'; // Obsidian posts go to Reddit
74
+ else if (/locallama/i.test(header)) result.platform = 'reddit';
75
+ else if (/programming/i.test(header)) result.platform = 'reddit';
76
+ else if (/twitter|x\.com/i.test(header)) result.platform = 'x';
77
+ else if (/linkedin/i.test(header)) result.platform = 'linkedin';
78
+ else if (/dev\.to/i.test(header)) result.platform = 'devto';
79
+
80
+ // Extract subreddit
81
+ const subLine = lines.find((l) => /^\*\*Subreddit:\*\*/i.test(l.trim()));
82
+ if (subLine) {
83
+ const match = subLine.match(/r\/(\w+)/);
84
+ if (match) result.subreddit = match[1];
85
+ }
86
+
87
+ // Extract title
88
+ const titleLine = lines.find((l) => /^\*\*Title:\*\*/i.test(l.trim()));
89
+ if (titleLine) {
90
+ result.title = titleLine.replace(/^\*\*Title:\*\*\s*/i, '').trim();
91
+ }
92
+
93
+ // Extract body — content between **Body:** and the next **Comment or --- separator
94
+ const bodyStartIdx = lines.findIndex((l) => /^\*\*Body:\*\*/i.test(l.trim()));
95
+ if (bodyStartIdx !== -1) {
96
+ const bodyLines = [];
97
+ for (let i = bodyStartIdx + 1; i < lines.length; i++) {
98
+ const line = lines[i];
99
+ // Stop at comment section or horizontal rule before comment
100
+ if (/^\*\*Comment/i.test(line.trim())) break;
101
+ if (line.trim() === '---' && i + 1 < lines.length && /^\*\*Comment/i.test(lines[i + 1].trim())) break;
102
+ bodyLines.push(line);
103
+ }
104
+ result.body = bodyLines.join('\n').trim();
105
+ }
106
+
107
+ // Extract comment
108
+ const commentStartIdx = lines.findIndex((l) => /^\*\*Comment/i.test(l.trim()));
109
+ if (commentStartIdx !== -1) {
110
+ const commentLines = [];
111
+ for (let i = commentStartIdx + 1; i < lines.length; i++) {
112
+ commentLines.push(lines[i]);
113
+ }
114
+ result.comment = commentLines.join('\n').trim();
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // Platform dispatchers
122
+ // ---------------------------------------------------------------------------
123
+
124
+ async function postToReddit(parsed, dryRun) {
125
+ const { subreddit, title, body, comment } = parsed;
126
+ if (!subreddit || !title || !body) {
127
+ throw new Error('Reddit post requires subreddit, title, and body');
128
+ }
129
+
130
+ if (dryRun) {
131
+ console.log(`[dry-run] Reddit r/${subreddit}: "${title}" (${body.length} chars)`);
132
+ if (comment) console.log(`[dry-run] Reddit follow-up comment: (${comment.length} chars)`);
133
+ return { dryRun: true };
134
+ }
135
+
136
+ const reddit = getPublisher('reddit');
137
+ const postData = await reddit.publishToReddit({ subreddit, title, text: body });
138
+
139
+ // Post the follow-up comment if we have one and got a post ID
140
+ if (comment && postData.name) {
141
+ console.log('[post-everywhere] Posting follow-up comment...');
142
+ const token = await reddit.getRedditToken(
143
+ process.env.REDDIT_CLIENT_ID,
144
+ process.env.REDDIT_CLIENT_SECRET,
145
+ process.env.REDDIT_USERNAME,
146
+ process.env.REDDIT_PASSWORD
147
+ );
148
+ const userAgent = process.env.REDDIT_USER_AGENT || `thumbgate/1.0 by ${process.env.REDDIT_USERNAME}`;
149
+ await reddit.submitComment(token, userAgent, { parentId: postData.name, text: comment });
150
+ }
151
+
152
+ return postData;
153
+ }
154
+
155
+ async function postToX(parsed, dryRun) {
156
+ const text = parsed.title ? `${parsed.title}\n\n${(parsed.body || '').slice(0, 240)}` : parsed.body;
157
+ if (!text) throw new Error('X post requires title or body');
158
+
159
+ if (dryRun) {
160
+ console.log(`[dry-run] X/Twitter: "${text.slice(0, 100)}..." (${text.length} chars)`);
161
+ return { dryRun: true };
162
+ }
163
+
164
+ const x = getPublisher('x');
165
+ return x.postTweet(text);
166
+ }
167
+
168
+ async function postToLinkedIn(parsed, dryRun) {
169
+ const text = parsed.body || '';
170
+ if (!text) throw new Error('LinkedIn post requires body');
171
+
172
+ if (dryRun) {
173
+ console.log(`[dry-run] LinkedIn: "${text.slice(0, 100)}..." (${text.length} chars)`);
174
+ return { dryRun: true };
175
+ }
176
+
177
+ const linkedin = getPublisher('linkedin');
178
+ return linkedin.publishPost({ text });
179
+ }
180
+
181
+ async function postToDevTo(parsed, dryRun) {
182
+ const { title, body } = parsed;
183
+ if (!title || !body) throw new Error('Dev.to post requires title and body');
184
+
185
+ if (dryRun) {
186
+ console.log(`[dry-run] Dev.to: "${title}" (${body.length} chars)`);
187
+ return { dryRun: true };
188
+ }
189
+
190
+ const devto = getPublisher('devto');
191
+ return devto.publishArticle({ title, body_markdown: body, tags: parsed.tags });
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Main orchestrator
196
+ // ---------------------------------------------------------------------------
197
+
198
+ const DISPATCHERS = {
199
+ reddit: postToReddit,
200
+ x: postToX,
201
+ linkedin: postToLinkedIn,
202
+ devto: postToDevTo,
203
+ };
204
+
205
+ async function postEverywhere(filePath, { platforms, dryRun } = {}) {
206
+ const parsed = parsePostFile(filePath);
207
+ console.log(`[post-everywhere] Parsed: platform=${parsed.platform}, subreddit=${parsed.subreddit}, title="${parsed.title}"`);
208
+
209
+ const qualityGate = require('./social-quality-gate');
210
+ const postText = [parsed.title, parsed.body, parsed.comment].filter(Boolean).join('\n');
211
+ const gateResult = qualityGate.gatePost(postText);
212
+ if (!gateResult.allowed) {
213
+ const reasons = gateResult.findings.map(f => f.reason).join(', ');
214
+ console.error(`[post-everywhere] BLOCKED by quality gate: ${reasons}`);
215
+ return { blocked: true, reasons: gateResult.findings };
216
+ }
217
+ console.log('[post-everywhere] Quality gate: PASSED');
218
+
219
+ // Determine which platforms to post to
220
+ const targetPlatforms = platforms || (parsed.platform ? [parsed.platform] : Object.keys(DISPATCHERS));
221
+
222
+ // Preserve original body/comment so each platform gets a fresh UTM tag
223
+ const originalBody = parsed.body;
224
+ const originalComment = parsed.comment;
225
+
226
+ // Tag trackable URLs with per-platform UTM parameters before dispatching
227
+ const results = {};
228
+ for (const platform of targetPlatforms) {
229
+ const utmOpts = { source: platform, medium: 'social', campaign: 'organic' };
230
+ parsed.body = originalBody ? tagUrlsInText(originalBody, utmOpts) : originalBody;
231
+ parsed.comment = originalComment ? tagUrlsInText(originalComment, utmOpts) : originalComment;
232
+
233
+ const dispatcher = DISPATCHERS[platform];
234
+ if (!dispatcher) {
235
+ console.warn(`[post-everywhere] No dispatcher for platform: ${platform}, skipping`);
236
+ continue;
237
+ }
238
+
239
+ try {
240
+ console.log(`\n[post-everywhere] Posting to ${platform}...`);
241
+ results[platform] = await dispatcher(parsed, dryRun);
242
+ console.log(`[post-everywhere] ${platform}: OK`);
243
+ } catch (err) {
244
+ console.error(`[post-everywhere] ${platform}: FAILED — ${err.message}`);
245
+ results[platform] = { error: err.message };
246
+ }
247
+ }
248
+
249
+ return results;
250
+ }
251
+
252
+ module.exports = { postEverywhere, parsePostFile };
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // CLI
256
+ // ---------------------------------------------------------------------------
257
+ if (require.main === module) {
258
+ const args = process.argv.slice(2);
259
+ const filePath = args.find((a) => !a.startsWith('--'));
260
+ const dryRun = args.includes('--dry-run');
261
+
262
+ function getArg(flag) {
263
+ const prefix = `${flag}=`;
264
+ const entry = args.find((a) => a.startsWith(prefix));
265
+ return entry ? entry.slice(prefix.length) : null;
266
+ }
267
+
268
+ const platformsArg = getArg('--platforms');
269
+ const platforms = platformsArg ? platformsArg.split(',').map((p) => p.trim()) : null;
270
+
271
+ if (!filePath) {
272
+ console.error('Usage: node scripts/post-everywhere.js <post-file.md> [--dry-run] [--platforms=reddit,x,devto]');
273
+ process.exit(1);
274
+ }
275
+
276
+ const resolved = path.resolve(filePath);
277
+ if (!fs.existsSync(resolved)) {
278
+ console.error(`File not found: ${resolved}`);
279
+ process.exit(1);
280
+ }
281
+
282
+ // Load .env if available
283
+ const envPath = path.resolve(__dirname, '..', '.env');
284
+ if (fs.existsSync(envPath)) {
285
+ const envContent = fs.readFileSync(envPath, 'utf8');
286
+ for (const line of envContent.split('\n')) {
287
+ const trimmed = line.trim();
288
+ if (!trimmed || trimmed.startsWith('#')) continue;
289
+ const eqIdx = trimmed.indexOf('=');
290
+ if (eqIdx > 0) {
291
+ const key = trimmed.slice(0, eqIdx);
292
+ const value = trimmed.slice(eqIdx + 1);
293
+ if (!process.env[key]) process.env[key] = value;
294
+ }
295
+ }
296
+ }
297
+
298
+ postEverywhere(resolved, { platforms, dryRun })
299
+ .then((results) => {
300
+ console.log('\n[post-everywhere] Results:', JSON.stringify(results, null, 2));
301
+ const failed = Object.values(results).filter((r) => r.error);
302
+ if (failed.length > 0) process.exit(1);
303
+ })
304
+ .catch((err) => {
305
+ console.error('[post-everywhere] Fatal:', err.message);
306
+ process.exit(1);
307
+ });
308
+ }
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ # Retry posting to X.com until it succeeds (X API v2 has frequent 503s)
3
+ # Usage: source .env && bash scripts/post-to-x-retry.sh
4
+
5
+ set -euo pipefail
6
+
7
+ MAX_RETRIES=10
8
+ RETRY_DELAY=30
9
+
10
+ for i in $(seq 1 $MAX_RETRIES); do
11
+ echo "Attempt $i/$MAX_RETRIES..."
12
+ if node scripts/post-to-x.js "$@" 2>&1 | grep -q "Posted tweet"; then
13
+ echo "✅ Tweet posted successfully!"
14
+ exit 0
15
+ fi
16
+ echo " Retrying in ${RETRY_DELAY}s..."
17
+ sleep $RETRY_DELAY
18
+ RETRY_DELAY=$((RETRY_DELAY * 2))
19
+ done
20
+
21
+ echo "❌ Failed after $MAX_RETRIES attempts. X API may be down."
22
+ exit 1