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.
- package/.claude-plugin/README.md +134 -0
- package/.claude-plugin/bundle/icon.png +0 -0
- package/.claude-plugin/bundle/icon.svg +18 -0
- package/.claude-plugin/bundle/server/index.js +24 -0
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.well-known/mcp/server-card.json +231 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/adapters/README.md +9 -0
- package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
- package/adapters/chatgpt/INSTALL.md +83 -0
- package/adapters/chatgpt/openapi.yaml +1281 -0
- package/adapters/claude/.mcp.json +14 -0
- package/adapters/codex/config.toml +9 -0
- package/adapters/gemini/function-declarations.json +224 -0
- package/adapters/mcp/server-stdio.js +788 -0
- package/adapters/opencode/opencode.json +15 -0
- package/bin/cli.js +1484 -0
- package/bin/memory.sh +64 -0
- package/bin/obsidian-sync.sh +20 -0
- package/bin/postinstall.js +37 -0
- package/config/build-metadata.json +4 -0
- package/config/e2e-critical-flows.json +45 -0
- package/config/gate-templates.json +77 -0
- package/config/gates/claim-verification.json +29 -0
- package/config/gates/computer-use.json +39 -0
- package/config/gates/default.json +117 -0
- package/config/github-about.json +25 -0
- package/config/mcp-allowlists.json +135 -0
- package/config/model-tiers.json +33 -0
- package/config/partner-routing.json +132 -0
- package/config/policy-bundles/constrained-v1.json +64 -0
- package/config/policy-bundles/default-v1.json +91 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/skill-packs/react-testing.json +23 -0
- package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
- package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
- package/config/skill-specs/pr-reviewer.json +9 -0
- package/config/skill-specs/release-status.json +9 -0
- package/config/skill-specs/ticket-triage.json +9 -0
- package/config/subagent-profiles.json +32 -0
- package/config/tessl-tiles.json +29 -0
- package/config/thumbgate-settings.managed.json +12 -0
- package/openapi/openapi.yaml +1281 -0
- package/package.json +283 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +64 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
- package/plugins/claude-codex-bridge/.mcp.json +12 -0
- package/plugins/claude-codex-bridge/INSTALL.md +43 -0
- package/plugins/claude-codex-bridge/README.md +46 -0
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
- package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
- package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
- package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
- package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
- package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
- package/plugins/codex-profile/.mcp.json +12 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +66 -0
- package/plugins/codex-profile/README.md +37 -0
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
- package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
- package/plugins/cursor-marketplace/LICENSE +21 -0
- package/plugins/cursor-marketplace/README.md +124 -0
- package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
- package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
- package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
- package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
- package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
- package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
- package/plugins/cursor-marketplace/mcp.json +12 -0
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
- package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
- package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
- package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
- package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
- package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
- package/plugins/gemini-extension/INSTALL.md +92 -0
- package/plugins/gemini-extension/gemini_prompt.txt +14 -0
- package/plugins/gemini-extension/tool_contract.json +45 -0
- package/plugins/opencode-profile/INSTALL.md +57 -0
- package/public/assets/instagram-card.png +0 -0
- package/public/assets/tiktok-agent-memory.mp4 +0 -0
- package/public/blog.html +400 -0
- package/public/dashboard.html +1093 -0
- package/public/guide.html +317 -0
- package/public/index.html +1014 -0
- package/public/learn/agent-harness-pattern.html +180 -0
- package/public/learn/ai-agent-persistent-memory.html +202 -0
- package/public/learn/learn.css +45 -0
- package/public/learn/mcp-pre-action-gates-explained.html +172 -0
- package/public/learn/stop-ai-agent-force-push.html +134 -0
- package/public/learn/vibe-coding-safety-net.html +142 -0
- package/public/learn.html +213 -0
- package/public/lessons.html +650 -0
- package/public/vercel.json +8 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/access-anomaly-detector.js +12 -0
- package/scripts/adk-consolidator.js +266 -0
- package/scripts/agent-readiness.js +220 -0
- package/scripts/agent-security-hardening.js +227 -0
- package/scripts/agentic-data-pipeline.js +847 -0
- package/scripts/analytics-report.js +328 -0
- package/scripts/analytics-window.js +158 -0
- package/scripts/async-job-runner.js +1001 -0
- package/scripts/audit-trail.js +398 -0
- package/scripts/auto-promote-gates.js +299 -0
- package/scripts/auto-wire-hooks.js +312 -0
- package/scripts/autonomous-sales-agent.js +39 -0
- package/scripts/autoresearch-runner.js +216 -0
- package/scripts/background-agent-governance.js +237 -0
- package/scripts/behavioral-extraction.js +97 -0
- package/scripts/belief-update.js +84 -0
- package/scripts/billing.js +2438 -0
- package/scripts/bot-detector.js +50 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/build-claude-mcpb.js +189 -0
- package/scripts/build-metadata.js +97 -0
- package/scripts/check-congruence.js +322 -0
- package/scripts/cli-feedback.js +135 -0
- package/scripts/cli-telemetry.js +87 -0
- package/scripts/cloudflare-dynamic-sandbox.js +315 -0
- package/scripts/code-reasoning.js +350 -0
- package/scripts/codegraph-context.js +466 -0
- package/scripts/commercial-offer.js +56 -0
- package/scripts/computer-use-firewall.js +250 -0
- package/scripts/context-engine.js +694 -0
- package/scripts/contextfs.js +1287 -0
- package/scripts/conversation-context.js +119 -0
- package/scripts/creator-campaigns.js +239 -0
- package/scripts/daemon-manager.js +108 -0
- package/scripts/daily-digest.js +11 -0
- package/scripts/dashboard-render-spec.js +395 -0
- package/scripts/dashboard.js +1058 -0
- package/scripts/data-governance.js +173 -0
- package/scripts/delegation-runtime.js +900 -0
- package/scripts/deploy-gcp.sh +44 -0
- package/scripts/deploy-policy.js +263 -0
- package/scripts/disagreement-mining.js +315 -0
- package/scripts/dispatch-brief.js +159 -0
- package/scripts/distribution-surfaces.js +44 -0
- package/scripts/dpo-optimizer.js +209 -0
- package/scripts/ephemeral-agent-store.js +219 -0
- package/scripts/eval-harness.js +56 -0
- package/scripts/evolution-state.js +241 -0
- package/scripts/experiment-tracker.js +267 -0
- package/scripts/export-databricks-bundle.js +242 -0
- package/scripts/export-dpo-pairs.js +345 -0
- package/scripts/export-kto-pairs.js +310 -0
- package/scripts/export-training.js +448 -0
- package/scripts/failure-diagnostics.js +558 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-fallback.js +111 -0
- package/scripts/feedback-history-distiller.js +391 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +1887 -0
- package/scripts/feedback-paths.js +145 -0
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-root-consolidator.js +238 -0
- package/scripts/feedback-schema.js +426 -0
- package/scripts/feedback-session.js +286 -0
- package/scripts/feedback-to-memory.js +185 -0
- package/scripts/feedback-to-rules.js +163 -0
- package/scripts/filesystem-search.js +404 -0
- package/scripts/funnel-analytics.js +35 -0
- package/scripts/gate-satisfy.js +42 -0
- package/scripts/gate-stats.js +116 -0
- package/scripts/gate-templates.js +70 -0
- package/scripts/gates-engine.js +816 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/generate-pretool-hook.sh +40 -0
- package/scripts/github-about.js +350 -0
- package/scripts/github-outreach.js +65 -0
- package/scripts/gtm-revenue-loop.js +520 -0
- package/scripts/hallucination-detector.js +226 -0
- package/scripts/hf-papers.js +317 -0
- package/scripts/history-distiller.js +200 -0
- package/scripts/hook-auto-capture.sh +95 -0
- package/scripts/hook-stop-pr-thread-check.sh +68 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/hook-stop-verify-deploy.sh +31 -0
- package/scripts/hook-thumbgate-cache-updater.js +48 -0
- package/scripts/hook-verify-before-done.sh +20 -0
- package/scripts/hosted-config.js +170 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/install-mcp.js +159 -0
- package/scripts/intent-router.js +392 -0
- package/scripts/internal-agent-bootstrap.js +490 -0
- package/scripts/jsonl-watcher.js +155 -0
- package/scripts/lesson-db.js +613 -0
- package/scripts/lesson-inference.js +315 -0
- package/scripts/lesson-retrieval.js +95 -0
- package/scripts/lesson-rotation.js +137 -0
- package/scripts/lesson-search.js +644 -0
- package/scripts/lesson-synthesis.js +196 -0
- package/scripts/license.js +50 -0
- package/scripts/local-model-profile.js +383 -0
- package/scripts/markdown-escape.js +12 -0
- package/scripts/marketing-experiment.js +671 -0
- package/scripts/mcp-config.js +149 -0
- package/scripts/mcp-policy.js +99 -0
- package/scripts/memalign-recall.js +111 -0
- package/scripts/memory-firewall.js +222 -0
- package/scripts/memory-migration.js +296 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/metered-billing.js +16 -0
- package/scripts/model-tier-router.js +301 -0
- package/scripts/money-watcher.js +71 -0
- package/scripts/multi-hop-recall.js +240 -0
- package/scripts/natural-language-harness.js +330 -0
- package/scripts/obsidian-export.js +712 -0
- package/scripts/operational-dashboard.js +103 -0
- package/scripts/operational-summary.js +93 -0
- package/scripts/optimize-context.js +17 -0
- package/scripts/org-dashboard.js +201 -0
- package/scripts/partner-orchestration.js +146 -0
- package/scripts/per-step-scoring.js +165 -0
- package/scripts/perplexity-marketing.js +466 -0
- package/scripts/pii-scanner.js +153 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/post-everywhere.js +308 -0
- package/scripts/post-to-x-retry.sh +22 -0
- package/scripts/post-to-x.js +369 -0
- package/scripts/pr-manager.js +236 -0
- package/scripts/predictive-insights.js +356 -0
- package/scripts/principle-extractor.js +162 -0
- package/scripts/pro-features.js +40 -0
- package/scripts/pro-local-dashboard.js +174 -0
- package/scripts/problem-detail.js +53 -0
- package/scripts/product-feedback.js +134 -0
- package/scripts/profile-router.js +245 -0
- package/scripts/prompt-dlp.js +221 -0
- package/scripts/prompt-guard.js +83 -0
- package/scripts/prove-adapters.js +863 -0
- package/scripts/prove-attribution.js +365 -0
- package/scripts/prove-automation.js +653 -0
- package/scripts/prove-autoresearch.js +304 -0
- package/scripts/prove-claim-verification.js +277 -0
- package/scripts/prove-cloudflare-sandbox.js +163 -0
- package/scripts/prove-data-pipeline.js +410 -0
- package/scripts/prove-data-quality.js +227 -0
- package/scripts/prove-evolution.js +352 -0
- package/scripts/prove-harnesses.js +287 -0
- package/scripts/prove-intelligence.js +259 -0
- package/scripts/prove-lancedb.js +371 -0
- package/scripts/prove-local-intelligence.js +342 -0
- package/scripts/prove-loop-closure.js +263 -0
- package/scripts/prove-predictive-insights.js +357 -0
- package/scripts/prove-runtime.js +350 -0
- package/scripts/prove-seo-gsd.js +234 -0
- package/scripts/prove-settings.js +279 -0
- package/scripts/prove-subway-upgrades.js +277 -0
- package/scripts/prove-tessl.js +229 -0
- package/scripts/prove-training-export.js +327 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/prove-xmemory.js +332 -0
- package/scripts/publish-decision.js +133 -0
- package/scripts/pulse.js +80 -0
- package/scripts/rate-limiter.js +125 -0
- package/scripts/reddit-dm-outreach.js +182 -0
- package/scripts/reddit-monitor-cron.sh +26 -0
- package/scripts/reflector-agent.js +221 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/revenue-status.js +472 -0
- package/scripts/risk-scorer.js +458 -0
- package/scripts/rlaif-self-audit.js +129 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/schedule-manager.js +251 -0
- package/scripts/secret-scanner.js +414 -0
- package/scripts/self-heal.js +147 -0
- package/scripts/self-healing-check.js +188 -0
- package/scripts/semantic-layer.js +98 -0
- package/scripts/seo-gsd.js +1153 -0
- package/scripts/settings-hierarchy.js +214 -0
- package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
- package/scripts/skill-exporter.js +262 -0
- package/scripts/skill-generator.js +446 -0
- package/scripts/skill-materializer.js +134 -0
- package/scripts/skill-packs.js +136 -0
- package/scripts/skill-proposer.js +99 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/slo-alert-engine.js +14 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/social-analytics/db/schema.sql +32 -0
- package/scripts/social-analytics/digest.js +256 -0
- package/scripts/social-analytics/generate-instagram-card.js +97 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
- package/scripts/social-analytics/mcp-server.js +289 -0
- package/scripts/social-analytics/normalizer.js +580 -0
- package/scripts/social-analytics/notify.js +162 -0
- package/scripts/social-analytics/poll-all.js +107 -0
- package/scripts/social-analytics/pollers/github.js +195 -0
- package/scripts/social-analytics/pollers/instagram.js +253 -0
- package/scripts/social-analytics/pollers/linkedin.js +330 -0
- package/scripts/social-analytics/pollers/plausible.js +247 -0
- package/scripts/social-analytics/pollers/reddit.js +306 -0
- package/scripts/social-analytics/pollers/threads.js +233 -0
- package/scripts/social-analytics/pollers/tiktok.js +203 -0
- package/scripts/social-analytics/pollers/x.js +227 -0
- package/scripts/social-analytics/pollers/youtube.js +304 -0
- package/scripts/social-analytics/pollers/zernio.js +180 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
- package/scripts/social-analytics/publishers/devto.js +122 -0
- package/scripts/social-analytics/publishers/instagram.js +317 -0
- package/scripts/social-analytics/publishers/linkedin.js +294 -0
- package/scripts/social-analytics/publishers/reddit.js +390 -0
- package/scripts/social-analytics/publishers/threads.js +275 -0
- package/scripts/social-analytics/publishers/tiktok.js +217 -0
- package/scripts/social-analytics/publishers/x.js +259 -0
- package/scripts/social-analytics/publishers/youtube.js +223 -0
- package/scripts/social-analytics/publishers/zernio.js +209 -0
- package/scripts/social-analytics/run-digest.js +34 -0
- package/scripts/social-analytics/store.js +257 -0
- package/scripts/social-analytics/utm.js +143 -0
- package/scripts/social-pipeline.js +2628 -0
- package/scripts/social-quality-gate.js +18 -0
- package/scripts/social-reply-monitor.js +445 -0
- package/scripts/status-dashboard.js +155 -0
- package/scripts/statusline-lesson.js +16 -0
- package/scripts/statusline-tower.js +8 -0
- package/scripts/statusline.sh +116 -0
- package/scripts/stripe-live-status.js +115 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +70 -0
- package/scripts/sync-github-about.js +52 -0
- package/scripts/sync-version.js +451 -0
- package/scripts/synthetic-dpo.js +234 -0
- package/scripts/telemetry-analytics.js +821 -0
- package/scripts/tessl-export.js +371 -0
- package/scripts/test-coverage.js +120 -0
- package/scripts/thompson-sampling.js +417 -0
- package/scripts/thumbgate-search.js +189 -0
- package/scripts/tool-kpi-tracker.js +12 -0
- package/scripts/tool-registry.js +811 -0
- package/scripts/train_from_feedback.py +910 -0
- package/scripts/user-profile.js +78 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +198 -0
- package/scripts/verification-loop.js +291 -0
- package/scripts/verify-obsidian-setup.sh +269 -0
- package/scripts/verify-run.js +269 -0
- package/scripts/webhook-delivery.js +62 -0
- package/scripts/weekly-auto-post.js +124 -0
- package/scripts/workflow-runs.js +154 -0
- package/scripts/workflow-sprint-intake.js +475 -0
- package/scripts/workspace-evolver.js +374 -0
- package/scripts/x-autonomous-marketing.js +139 -0
- package/scripts/xmemory-lite.js +405 -0
- package/skills/agent-memory/SKILL.md +97 -0
- package/skills/solve-architecture-autonomy/SKILL.md +17 -0
- package/skills/solve-architecture-autonomy/tool.js +33 -0
- package/skills/thumbgate/SKILL.md +114 -0
- package/skills/thumbgate-feedback/SKILL.md +49 -0
- 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
|