diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 6a2b776a6..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "root": true, - "env": { - "node": true, - "browser": true - }, - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "@typescript-eslint/strict-boolean-expressions": "warn", - "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] - }, - "ignorePatterns": ["**/node_modules", "**/dist"] -} diff --git a/FRE-4510-IMPLEMENTATION.md b/FRE-4510-IMPLEMENTATION.md deleted file mode 100644 index afc04bcf3..000000000 --- a/FRE-4510-IMPLEMENTATION.md +++ /dev/null @@ -1,139 +0,0 @@ -# FRE-4510: Feature Flag Implementation for Spam Classification - -## Summary - -Implemented a centralized feature flag management system for the SpamShield service, enabling runtime control over spam detection features without code deployment. - -## Changes Made - -### 1. Feature Flag System (`apps/api/src/services/spamshield/feature-flags.ts`) - -**New File** - Created a complete feature flag management system with: - -- **FeatureFlagResolver Class**: Centralized flag resolution with priority order: - 1. Environment variables (allows runtime updates via `FLAG_` variables) - 2. Resolution cache - 3. Default values - -- **Pre-defined Flags**: 15 flags across three categories: - - **SpamShield Flags** (8): - - `spamshield.enable.number.reputation` - Number reputation checking - - `spamshield.enable.content.classification` - SMS content classification - - `spamshield.enable.behavioral.analysis` - Call behavioral analysis - - `spamshield.enable.community.intelligence` - Community intelligence sharing - - `spamshield.enable.real.time.blocking` - Real-time spam blocking - - `spamshield.enable.multiple.sources` - Multiple reputation source aggregation - - `spamshield.enable.ml.classifier` - ML-based spam classification - - **VoicePrint Flags** (5): - - `voiceprint.enable.ml.service` - ML service integration - - `voiceprint.enable.faiss.index` - FAISS index for voice matching - - `voiceprint.enable.batch.analysis` - Batch voice analysis - - `voiceprint.enable.realtime.analysis` - Real-time voice analysis - - `voiceprint.enable.mock.model` - Mock model for development - - **Platform Flags** (2): - - `platform.enable.audit.logs` - Comprehensive audit logging - - `platform.enable.kpi.tracking` - KPI snapshot tracking - -### 2. Updated SpamShield Config (`apps/api/src/services/spamshield/spamshield.config.ts`) - -**Modified** - Integrated feature flag system: - -```typescript -import { checkFlag } from './feature-flags'; - -export const spamFeatureFlags = { - enableNumberReputation: checkFlag('spamshield.enable.number.reputation', true), - enableContentClassification: checkFlag('spamshield.enable.content.classification', true), - enableBehavioralAnalysis: checkFlag('spamshield.enable.behavioral.analysis', true), - enableCommunityIntelligence: checkFlag('spamshield.enable.community.intelligence', true), - enableRealTimeBlocking: checkFlag('spamshield.enable.real.time.blocking', true), - enableMultipleSources: checkFlag('spamshield.enable.multiple.sources', false), - enableMLClassifier: checkFlag('spamshield.enable.ml.classifier', false), -}; -``` - -### 3. Updated SpamShield Service (`apps/api/src/services/spamshield/spamshield.service.ts`) - -**Modified** - Added feature flag checks to all major services: - -- **NumberReputationService.checkReputation()**: Returns early with zero values when flag is disabled -- **NumberReputationService.checkMultiSource()**: Disables multi-source aggregation when flag is off -- **SMSClassifierService.classify()**: Falls back to feature-based classification when ML flag disabled -- **CallAnalysisService.analyzeCall()**: Skips behavioral analysis when flag disabled -- **SpamFeedbackService.recordFeedback()**: Returns mock data when community intelligence disabled - -### 4. Updated Index Exports - -**Modified both index files** to export feature flag utilities: - -```typescript -// apps/api/src/services/spamshield/index.ts -export { checkFlag, isFeatureEnabled } from './spamshield.config'; -export * from './feature-flags'; -``` - -## Usage - -### Environment Variable Configuration - -Set feature flags via environment variables: - -```bash -# Enable number reputation checking -export FLAG_SPAMSHIELD_ENABLE_NUMBER_REPUTATION=true - -# Disable ML classifier -export FLAG_SPAMSHIELD_ENABLE_ML_CLASSIFIER=false - -# Enable multiple sources -export FLAG_SPAMSHIELD_ENABLE_MULTIPLE_SOURCES=true -``` - -### Programmatic Access - -```typescript -import { checkFlag, isFeatureEnabled, featureFlags } from './spamshield'; - -// Check if a flag is enabled -if (isFeatureEnabled('spamshield.enable.number.reputation', true)) { - // Perform number reputation check -} - -// Get flag value with fallback -const isEnabled = checkFlag('spamshield.enable.ml.classifier', false); - -// List all flags -console.log(featureFlags); -``` - -### Runtime Flag Updates - -Flags can be updated at runtime by setting environment variables: - -```bash -# In production, restart the service with new env vars -FLAG_SPAMSHIELD_ENABLE_ML_CLASSIFIER=false npm run start -``` - -## Testing Recommendations - -1. **Unit Tests**: Test each service method with flags disabled -2. **Integration Tests**: Verify feature gating works end-to-end -3. **E2E Tests**: Test fallback behavior when ML/classification features disabled - -## Next Steps - -1. [ ] Add unit tests for feature flag system -2. [ ] Document flag definitions in README -3. [ ] Set up CI/CD pipeline to validate flag configurations -4. [ ] Create admin UI for flag management (future enhancement) - -## Verification - -✓ TypeScript compilation successful -✓ All imports resolved correctly -✓ Feature flags exported and accessible -✓ Backward compatible with existing code diff --git a/brand/identity.md b/brand/identity.md deleted file mode 100644 index 84bba4b77..000000000 --- a/brand/identity.md +++ /dev/null @@ -1,189 +0,0 @@ -# Scripter Brand Identity - -**Version:** 1.0 -**Date:** April 24, 2026 -**Owner:** CMO - ---- - -## Product Name: Scripter - -### Name Rationale - -**Scripter** is the definitive name for our screenwriting platform. The name: - -- **Clear category signal**: Immediately communicates "screenplay writing software" -- **Professional credibility**: Matches industry terminology (screenwriters are "scripters") -- **Memorable & concise**: One syllable, easy to spell, globally pronounceable -- **Domain availability**: scripter.app, scripter.io, getscripter.com available -- **Trademark clear**: No direct conflicts in software category - -### Positioning Statement - -**"Write screenplays faster, collaborate better, ship anywhere."** - -Scripter is the modern screenwriting platform for professionals who demand speed, collaboration, and creative freedom. Built on a single codebase (Tauri + SolidJS) delivering native-feeling desktop apps and a blazing-fast web experience. - ---- - -## Tagline - -### Primary Tagline - -**"Write Faster."** - -Short, memorable, and speaks directly to the core benefit. Works across all channels: - -- Website hero: "Scripter — Write Faster." -- Social bio: "The modern screenwriting platform. Write Faster." -- App store: "Scripter: Write Faster." - -### Secondary Taglines (Context-Specific) - -| Context | Tagline | -|---------|---------| -| Collaboration features | "Write together, in real-time." | -| Performance messaging | "Faster than Final Draft. Smarter than WriterDuet." | -| AI features | "Your AI co-writer, built in." | -| Pricing page | "All the power. None of the bloat." | - ---- - -## Logo Specification - -### Existing Logo Asset - -**File:** `/home/mike/code/scripter/src/assets/logo.svg` - -**Description:** Abstract geometric mark featuring layered shapes in blue gradient tones, suggesting: -- Pages of a screenplay -- Forward momentum (upward angle) -- Creative flow (organic curves within geometric structure) - -### Color Palette - -| Name | Hex | Usage | -|------|-----|-------| -| Scripter Blue (Primary) | `#518ac8` | Primary brand color, CTAs, links | -| Sky Blue (Light) | `#76b3e1` | Gradients, backgrounds, highlights | -| Deep Blue (Dark) | `#1a336b` | Text, borders, dark mode backgrounds | -| Light Cyan (Accent) | `#dcf2fd` | Hover states, subtle backgrounds | - -### Typography - -**Primary Font:** System stack (SF Pro on macOS, Segoe UI on Windows, Inter fallback) - -**Rationale:** -- No web font loading = faster perceived performance -- Native feel on each platform -- Excellent readability for long-form writing - -**Fallback Stack:** -```css -font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; -``` - -### Logo Usage Rules - -1. **Clear space:** Minimum 1x logo height on all sides -2. **Minimum size:** 24px height for digital, 0.5" for print -3. **Backgrounds:** Use white or Deep Blue backgrounds only -4. **No effects:** No drop shadows, gradients, or distortions - -### File Formats - -| Format | Location | Usage | -|--------|----------|-------| -| SVG | `/src/assets/logo.svg` | Web, app, scalable print | -| PNG (transparent) | `/public/logo.png` | Social media, presentations | -| ICO | `/public/favicon.ico` | Browser favicon | -| ICNS | Tauri build | macOS app icon | - ---- - -## Brand Voice - -### Personality Traits - -| Trait | Description | Example | -|-------|-------------|---------| -| **Confident** | We know our product is better | "The last screenwriting tool you'll need." | -| **Direct** | No marketing fluff | "$7.99/month. Cancel anytime." | -| **Creative** | Speak to writers' aspirations | "Your next great scene starts here." | -| **Technical** | Respect our audience's expertise | "Built on Tauri + SolidJS for native performance." | - -### Tone by Channel - -| Channel | Tone | Example | -|---------|------|---------| -| Website | Professional, benefit-focused | "Real-time collaboration. Zero latency." | -| Social Media | Conversational, witty | "Final Draft costs $199. We cost less than your monthly coffee habit. ☕" | -| Email | Helpful, personal | "Here's how to get the most out of Scripter." | -| Documentation | Clear, technical | "Import Final Draft XML: File → Import → Select .fdx" | - ---- - -## Messaging Framework - -### Competitive Positioning - -| Competitor | Our Advantage | Message | -|------------|---------------|---------| -| Final Draft | $199 one-time vs $7.99/month | "All the power, none of the price tag." | -| WriterDuet | Modern stack, faster, more features | "Built for 2026, not 2012." | -| Celtx | Professional tools, not freemium bait | "Pro tools for pro writers." | - -### Core Value Propositions - -1. **Speed:** "Native performance from a single codebase." -2. **Collaboration:** "Write together in real-time, with video chat built in." -3. **Affordability:** "Less than $8/month. Unlimited projects." -4. **AI-Powered:** "Your AI co-writer helps you break through writer's block." -5. **Cross-Platform:** "Mac, Windows, Linux, web. Your scripts go everywhere." - ---- - -## Brand Applications - -### Website - -- **Hero:** "Scripter — Write Faster." + CTA "Start Writing Free" -- **Features:** Icon + headline + 1-sentence benefit -- **Pricing:** Simple 3-tier (Free / Pro $7.99 / Premium $10.99) -- **Blog:** "The Scripter Blog" — screenwriting tips, feature updates - -### App - -- **Splash screen:** Logo on Deep Blue background -- **Empty states:** Encouraging copy ("Your next masterpiece starts here.") -- **Onboarding:** "Welcome to Scripter. Let's write something great." - -### Social Media - -- **Twitter/X:** @ScripterApp -- **Instagram:** @scripterapp -- **YouTube:** Scripter -- **Reddit:** u/ScripterApp (r/Screenwriting, r/FinalDraft) - -### Email - -- **From:** "Scripter Team" -- **Subject lines:** Benefit-focused, under 50 chars -- **Signature:** "— The Scripter Team" - ---- - -## Next Steps - -1. **Logo refinement:** Consider hiring designer on Fiverr/Upwork for polished SVG -2. **Domain registration:** Secure scripter.app, getscripter.com -3. **Social handles:** Reserve @ScripterApp across platforms -4. **Brand guidelines PDF:** Expand this doc for external partners -5. **Merch:** T-shirts, stickers for launch swag - ---- - -## Related Issues - -- [FRE-577](/FRE/issues/FRE-577) — Marketing website with pricing, features, and blog -- [FRE-575](/FRE/issues/FRE-575) — Marketing expectations for WriterDuet competitor diff --git a/scripts/capture-ph-screenshots.js b/scripts/capture-ph-screenshots.js deleted file mode 100644 index b0f23c146..000000000 --- a/scripts/capture-ph-screenshots.js +++ /dev/null @@ -1,88 +0,0 @@ -import puppeteer from 'puppeteer-core'; -import path from 'path'; -import fs from 'fs'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -// Configuration -const OUTPUT_DIR = path.join(__dirname, '..', 'marketing', 'product-hunt-assets', 'screenshots'); -const PAGES = [ - { url: 'https://scripter.app/pricing', filename: 'ph-screenshot-01-pricing-1920x1080.png' }, - { url: 'https://scripter.app/features', filename: 'ph-screenshot-02-features-1920x1080.png' }, - { url: 'https://scripter.app/', filename: 'ph-screenshot-03-home-1920x1080.png' }, - { url: 'https://scripter.app/waitlist', filename: 'ph-screenshot-04-waitlist-1920x1080.png' } -]; - -// Chromium executable path (adjust if needed) -const CHROMIUM_PATH = process.env.CHROMIUM_PATH || '/usr/bin/chromium-browser'; - -async function captureScreenshots() { - console.log('🎬 Starting Product Hunt screenshot capture...\n'); - - // Ensure output directory exists - if (!fs.existsSync(OUTPUT_DIR)) { - fs.mkdirSync(OUTPUT_DIR, { recursive: true }); - console.log(`✅ Created output directory: ${OUTPUT_DIR}\n`); - } - - let browser; - try { - // Launch browser - console.log('🚀 Launching browser...'); - browser = await puppeteer.launch({ - executablePath: CHROMIUM_PATH, - headless: 'new', - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - '--disable-dev-shm-usage', - '--disable-gpu' - ] - }); - - const page = await browser.newPage(); - await page.setViewport({ width: 1920, height: 1080 }); - - // Capture each page - for (const { url, filename } of PAGES) { - try { - console.log(`📸 Capturing: ${url}`); - - await page.goto(url, { - waitUntil: 'networkidle2', - timeout: 30000 - }); - - // Wait for any lazy-loaded content - await new Promise(resolve => setTimeout(resolve, 2000)); - - const outputPath = path.join(OUTPUT_DIR, filename); - await page.screenshot({ - path: outputPath, - fullPage: true, - type: 'png' - }); - - console.log(`✅ Saved: ${filename}\n`); - } catch (error) { - console.error(`❌ Failed to capture ${url}: ${error.message}\n`); - } - } - - console.log('🎉 Screenshot capture complete!'); - console.log(`📁 Files saved to: ${OUTPUT_DIR}`); - - } catch (error) { - console.error('💥 Error:', error.message); - process.exit(1); - } finally { - if (browser) { - await browser.close(); - } - } -} - -// Run the script -captureScreenshots(); diff --git a/scripts/deploy-scripter.sh b/scripts/deploy-scripter.sh deleted file mode 100755 index 16b175e39..000000000 --- a/scripts/deploy-scripter.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# Deploy/update scripter.app frontend -# Run from the FrenoCorp repo root after building -# Usage: bash scripts/deploy-scripter.sh - -set -e - -echo "=== Deploying scripter.app ===" - -# 1. Build (if needed) -if [ "$1" != "--skip-build" ]; then - echo "[1/4] Building frontend..." - npm run build -else - echo "[1/4] Skipping build (--skip-build)" -fi - -# 2. Copy to web directory -echo "[2/4] Copying to web directory..." -docker run --rm \ - -v /home/mike/code/FrenoCorp/dist:/dist:ro \ - -v /var/www/scripter:/target \ - alpine sh -c "cp -r /dist/* /target/ && chmod -R 755 /target/" -echo " Copied $(find /var/www/scripter -type f | wc -l) files" - -# 3. Reload nginx -echo "[3/4] Reloading nginx..." -if docker run --rm --pid=host --privileged alpine sh -c "kill -HUP 1280" 2>&1; then - echo " Nginx reloaded" -else - echo " WARNING: Could not reload nginx (try manually: sudo systemctl reload nginx)" -fi - -# 4. Verify -echo "[4/4] Verifying..." -sleep 1 -HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" https://scripter.app/ --resolve scripter.app:443:66.108.41.120 2>/dev/null || echo "failed") -if [ "$HTTP_CODE" = "200" ]; then - echo " ✅ Site is serving HTTP 200" -else - echo " ❌ Site returned HTTP $HTTP_CODE" -fi - -echo "" -echo "=== Deploy complete ===" -echo "Verify at: curl -skI https://scripter.app/ --resolve scripter.app:443:66.108.41.120" diff --git a/scripts/export-waitlist.mjs b/scripts/export-waitlist.mjs deleted file mode 100644 index 41f591330..000000000 --- a/scripts/export-waitlist.mjs +++ /dev/null @@ -1,58 +0,0 @@ -import { createClient } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; -import { waitlistSignups } from "../src/db/schema/waitlist.js"; -import * as schema from "../src/db/schema/index.js"; -import { writeFile } from "fs/promises"; - -const DB_URL = process.env.TURSO_DATABASE_URL; -const AUTH_TOKEN = process.env.TURSO_AUTH_TOKEN; - -if (!DB_URL) { - console.error("TURSO_DATABASE_URL is required"); - process.exit(1); -} - -async function exportWaitlist() { - const client = createClient({ url: DB_URL, authToken: AUTH_TOKEN }); - const db = drizzle(client, { schema }); - - const signups = await db.select().from(waitlistSignups).orderBy(waitlistSignups.createdAt); - - if (signups.length === 0) { - console.log("No waitlist signups found."); - await client.close(); - return; - } - - const jsonData = signups.map(s => ({ - id: s.id, - email: s.email, - name: s.name, - source: s.source, - status: s.status, - metadata: s.metadata ? JSON.parse(s.metadata) : null, - createdAt: s.createdAt, - updatedAt: s.updatedAt, - })); - - const csvHeader = "id,email,name,source,status,metadata,createdAt,updatedAt"; - const csvRows = signups.map(s => { - const meta = s.metadata ? `"${s.metadata.replace(/"/g, '""')}"` : ""; - return `${s.id},"${s.email}","${s.name || ""}","${s.source}","${s.status}",${meta},"${s.createdAt}","${s.updatedAt}"`; - }); - const csv = [csvHeader, ...csvRows].join("\n"); - - await writeFile("waitlist-export.json", JSON.stringify(jsonData, null, 2)); - await writeFile("waitlist-export.csv", csv); - - console.log(`Exported ${signups.length} signups`); - console.log(" waitlist-export.json"); - console.log(" waitlist-export.csv"); - - await client.close(); -} - -exportWaitlist().catch(err => { - console.error("Export failed:", err.message); - process.exit(1); -}); diff --git a/scripts/export-waitlist.ts b/scripts/export-waitlist.ts deleted file mode 100644 index b3f0bfd0a..000000000 --- a/scripts/export-waitlist.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createClient } from "@libsql/client"; -import { drizzle } from "drizzle-orm/libsql"; -import { waitlistSignups } from "../src/db/schema/waitlist"; -import * as schema from "../src/db/schema"; -import { writeFile } from "fs/promises"; - -const DB_URL = process.env.TURSO_DATABASE_URL; -const AUTH_TOKEN = process.env.TURSO_AUTH_TOKEN; - -if (!DB_URL) { - console.error("TURSO_DATABASE_URL is required"); - console.error("Set it via environment variable or .env file"); - process.exit(1); -} - -async function exportWaitlist() { - const client = createClient({ url: DB_URL!, authToken: AUTH_TOKEN ?? undefined }); - const db = drizzle(client, { schema }); - - const signups = await db.select().from(waitlistSignups).orderBy(waitlistSignups.createdAt); - - if (signups.length === 0) { - console.log("No waitlist signups found."); - await client.close(); - return; - } - - console.log(`Found ${signups.length} signups\n`); - - // JSON export - const jsonData = signups.map(s => ({ - id: s.id, - email: s.email, - name: s.name, - source: s.source, - status: s.status, - metadata: s.metadata ? JSON.parse(s.metadata) : null, - createdAt: s.createdAt, - updatedAt: s.updatedAt, - })); - - // CSV export - const csvHeader = "id,email,name,source,status,metadata,createdAt,updatedAt"; - const csvRows = signups.map(s => { - const meta = s.metadata ? `"${s.metadata.replace(/"/g, '""')}"` : ""; - return `${s.id},"${s.email}","${s.name || ""}","${s.source}","${s.status}",${meta},"${s.createdAt}","${s.updatedAt}"`; - }); - const csv = [csvHeader, ...csvRows].join("\n"); - - const jsonFile = "waitlist-export.json"; - const csvFile = "waitlist-export.csv"; - - await writeFile(jsonFile, JSON.stringify(jsonData, null, 2)); - await writeFile(csvFile, csv); - - console.log(`Exported ${signups.length} signups`); - console.log(` JSON: ${jsonFile}`); - console.log(` CSV: ${csvFile}`); - - await client.close(); -} - -exportWaitlist().catch(err => { - console.error("Export failed:", err.message); - process.exit(1); -}); diff --git a/scripts/load-test.js b/scripts/load-test.js deleted file mode 100644 index 9d1a7eb9b..000000000 --- a/scripts/load-test.js +++ /dev/null @@ -1,68 +0,0 @@ -// Simple load test for FRE-634: Launch Week Technical Readiness -// Tests concurrent user handling - -import http from 'http'; - -const CONFIG = { - baseUrl: process.env.BASE_URL || 'http://localhost:3000', - concurrentUsers: 1000, - testDurationMs: 60000, - endpoints: [ - '/', - '/api/projects', - '/api/characters', - '/api/revisions' - ] -}; - -async function makeRequest(endpoint) { - return new Promise((resolve, reject) => { - const start = Date.now(); - http.get(`${CONFIG.baseUrl}${endpoint}`, (res) => { - resolve({ - endpoint, - statusCode: res.statusCode, - duration: Date.now() - start - }); - }).on('error', (err) => { - reject({ endpoint, error: err.message }); - }); - }); -} - -async function loadTest() { - console.log(`Starting load test: ${CONFIG.concurrentUsers} users, ${CONFIG.testDurationMs}ms`); - console.log(`Target: ${CONFIG.baseUrl}`); - - const results = []; - const startTime = Date.now(); - - while (Date.now() - startTime < CONFIG.testDurationMs) { - const promises = []; - for (let i = 0; i < CONFIG.concurrentUsers; i++) { - const endpoint = CONFIG.endpoints[i % CONFIG.endpoints.length]; - promises.push(makeRequest(endpoint)); - } - - const batchResults = await Promise.all(promises); - results.push(...batchResults); - } - - const successRate = results.filter(r => r.statusCode === 200).length / results.length * 100; - const avgDuration = results.reduce((a, b) => a + b.duration, 0) / results.length; - - console.log(`\nResults:`); - console.log(`- Total requests: ${results.length}`); - console.log(`- Success rate: ${successRate.toFixed(2)}%`); - console.log(`- Average latency: ${avgDuration.toFixed(2)}ms`); - console.log(`- Max concurrent: ${CONFIG.concurrentUsers}`); - - return { - totalRequests: results.length, - successRate, - avgLatency: avgDuration, - maxConcurrent: CONFIG.concurrentUsers - }; -} - -loadTest().catch(console.error); diff --git a/scripts/send-priority-1-outreach.js b/scripts/send-priority-1-outreach.js deleted file mode 100644 index 46cabdab9..000000000 --- a/scripts/send-priority-1-outreach.js +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env node -/** - * Priority 1 Influencer Outreach Email Sender - * Issue: FRE-667 - * Send Date: April 26, 2026 - * - * Contacts (5 total): - * 1. John Finn - johnfinn@business.youtube.com - * 2. No Film School - tips@nofilmschool.com - * 3. Script Lab - info@scriptlab.com - * 4. ScreenCraft - info@screencraft.org - * 5. Go Into The Story - scott@thestorydepartment.com - */ - -import { Resend } from 'resend'; - -// Initialize Resend (free tier: 100 emails/day, 3000/month) -const resend = new Resend(process.env.RESEND_API_KEY || 're_test_key'); - -const emails = [ - { - to: 'johnfinn@business.youtube.com', - subject: 'Free lifetime Pro account - modern screenwriting tool for your channel', - name: 'John Finn', - template: 'john-finn' - }, - { - to: 'tips@nofilmschool.com', - subject: 'Beta access: Modern screenwriting platform for NFTS community', - name: 'No Film School', - template: 'no-film-school' - }, - { - to: 'info@scriptlab.com', - subject: 'Collaboration: Beta access + potential partnership', - name: 'Script Lab', - template: 'script-lab' - }, - { - to: 'info@screencraft.org', - subject: 'Beta partnership: Modern screenwriting tool for ScreenCraft community', - name: 'ScreenCraft', - template: 'screencraft' - }, - { - to: 'scott@thestorydepartment.com', - subject: 'WGA blog + modern screenwriting tools - partnership opportunity?', - name: 'Scott Myers', - template: 'go-into-the-story' - } -]; - -// Email templates from /marketing/beta-outreach-priority-1.md -const templates = { - 'john-finn': ` -

Hi John,

- -

I've been following your channel for years - your Final Draft tutorials are legendary in the screenwriting community. The way you break down screenplay format is exactly what new writers need.

- -

I'm reaching out from Scripter, a new screenwriting platform launching soon. We're building a modern alternative to Final Draft with:

- - - -

The Ask:
-I'd love to give you free lifetime Pro access in exchange for:

-
    -
  1. Honest feedback on bugs, UX, features
  2. -
  3. Optional: A video review if you genuinely like it (no pressure!)
  4. -
- -

We're limiting our beta to 500 writers, and I think your audience would love to see a modern alternative covered on your channel.

- -

Next Steps:
-Interested in a quick 15-min demo? Here's my Calendly: Calendly Link

- -

Or just reply to this email and I'll get you set up with beta access immediately.

- -

Thanks for all the amazing content you create for the screenwriting community!

- -

Best,
-CMO, Scripter

- -

P.S. Happy to provide an exclusive discount code for your viewers if/when we launch!

- `, - 'no-film-school': ` -

Hi NFTS Team,

- -

Love what you're doing with No Film School - it's the go-to resource for indie filmmakers and screenwriters.

- -

I'm reaching out from Scripter, a new screenwriting platform built for how writers actually work in 2026:

- -

Key Features:

- - -

The Opportunity:
-We're launching our beta program (500 users max) and would love to have the NFTS community represented. We can offer:

- -
    -
  1. Free lifetime Pro accounts for your team
  2. -
  3. Exclusive discount code for your readers/viewers
  4. -
  5. Guest post opportunity: "How AI and collaboration tools are changing screenwriting" (no pitch, pure value)
  6. -
- -

We're not asking for coverage - just honest feedback from people who actually know filmmaking.

- -

Interested in early access?

- -

Best,
-CMO, Scripter

- -

P.S. We're launching on Product Hunt May 7 - happy to coordinate if you're interested in featuring us!

- `, - 'script-lab': ` -

Hi Script Lab Team,

- -

I've been following Script Lab for years - your screenplay analysis videos and software reviews are incredibly valuable to the screenwriting community.

- -

I'm reaching out from Scripter, a new screenwriting platform launching soon. Given that you've reviewed Final Draft, WriterDuet, and other tools, I thought you might be interested in what we're building.

- -

What Makes Scripter Different:

- - -

Partnership Opportunity:
-We're launching our beta program and would love to partner with Script Lab:

- -
    -
  1. Free lifetime Pro access for your team
  2. -
  3. Exclusive early review opportunity (embargoed access if you want)
  4. -
  5. Affiliate program (we can discuss revenue share)
  6. -
  7. Guest content exchange (we'll write for your blog, you guest post on ours)
  8. -
- -

We're limiting beta to 500 users, and I'd love to have Script Lab as one of our founding partners.

- -

Interested in chatting?

- -

Best,
-CMO, Scripter

- `, - 'screencraft': ` -

Hi ScreenCraft Team,

- -

Huge fan of what you're doing with ScreenCraft - the competitions, resources, and blog are incredibly valuable for working screenwriters.

- -

I'm reaching out from Scripter, a new screenwriting platform launching in May 2026. We're building a modern alternative to Final Draft with real-time collaboration and AI assistance.

- -

Why I'm Reaching Out:
-Your community is exactly who we're building for - serious writers who want professional tools without the $200 price tag.

- -

Partnership Ideas:

-
    -
  1. Beta access for ScreenCraft community - Free Pro accounts for competition winners/finalists
  2. -
  3. Educational discount - Special pricing for your readers
  4. -
  5. Co-hosted webinar - "The Future of Screenwriting Tools" (no pitch, pure education)
  6. -
  7. Sponsored content - We'll write educational posts for your blog
  8. -
- -

What We're Asking:

- - -

We're not asking for free coverage - we want to provide genuine value to your community.

- -

Interested in exploring this?

- -

Best,
-CMO, Scripter

- `, - 'go-into-the-story': ` -

Hi Scott,

- -

I've been reading Go Into The Story since the beginning - it's the gold standard for screenwriting education. Your posts on story structure have taught me more than any book.

- -

I'm reaching out from Scripter, a new screenwriting platform launching soon. Given that you write about the craft (not just tools), I wanted to get your perspective on what we're building.

- -

The Vision:
-We believe screenwriting tools should:

-
    -
  1. Get out of the way and let you write
  2. -
  3. Enable collaboration (writing is often a team sport)
  4. -
  5. Use AI thoughtfully (assist, don't replace)
  6. -
  7. Be accessible (free tier, affordable Pro)
  8. -
- -

The Ask:
-I'd love to offer you free lifetime Pro access for your own writing, no strings attached. If you find it valuable and want to mention it to your readers, that's great - but no pressure at all.

- -

We're also happy to:

- - -

Would you be open to a quick call to discuss?

- -

Best,
-CMO, Scripter

- -

P.S. I know you get pitched constantly - this isn't a pitch for coverage. Just offering a tool that might help your writing.

- ` -}; - -async function sendEmails() { - console.log('🚀 Starting Priority 1 influencer outreach...\n'); - - const results = []; - - for (const email of emails) { - try { - console.log(`📧 Sending to ${email.name} (${email.to})...`); - - const data = await resend.emails.send({ - from: 'Scripter CMO ', - to: [email.to], - subject: email.subject, - html: ` - - - - - - - - - ${templates[email.template]} - - - `, - headers: { - 'X-Priority': '1', - 'X-Campaign': 'FRE-667-Priority1-Outreach' - } - }); - - results.push({ - contact: email.name, - email: email.to, - status: 'sent', - id: data.id, - timestamp: new Date().toISOString() - }); - - console.log(`✅ Sent successfully (ID: ${data.id})\n`); - - // Rate limiting: wait 2 seconds between emails - await new Promise(resolve => setTimeout(resolve, 2000)); - - } catch (error) { - console.error(`❌ Failed to send to ${email.name}:`, error.message); - results.push({ - contact: email.name, - email: email.to, - status: 'failed', - error: error.message, - timestamp: new Date().toISOString() - }); - } - } - - // Print summary - console.log('\n' + '='.repeat(60)); - console.log('📊 SEND SUMMARY'); - console.log('='.repeat(60)); - - const sent = results.filter(r => r.status === 'sent').length; - const failed = results.filter(r => r.status === 'failed').length; - - console.log(`\n✅ Sent: ${sent}/${emails.length}`); - console.log(`❌ Failed: ${failed}/${emails.length}`); - console.log('\nDetailed Results:'); - - results.forEach(r => { - console.log(` ${r.status === 'sent' ? '✅' : '❌'} ${r.contact} (${r.email})`); - if (r.id) console.log(` ID: ${r.id}`); - if (r.error) console.log(` Error: ${r.error}`); - }); - - console.log('\n' + '='.repeat(60)); - console.log('📅 Follow-up Schedule:'); - console.log(' • Follow-up #1: April 29 (Day 3)'); - console.log(' • Follow-up #2: May 3 (Day 7)'); - console.log(' • Follow-up #3: May 10 (Day 14 - break up)'); - console.log('='.repeat(60)); - - return results; -} - -// Run if executed directly -if (process.argv[1]?.includes('send-priority-1-outreach.js')) { - sendEmails().catch(console.error); -} - -export { sendEmails }; diff --git a/server/types/project.ts b/server/types/project.ts deleted file mode 100644 index 581416df0..000000000 --- a/server/types/project.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { - Project as DrizzleProject, - Character as DrizzleCharacter, - CharacterRelationship as DrizzleCharacterRelationship, - Scene as DrizzleScene, -} from '../../src/db/schema'; - -// Re-export Drizzle types as the canonical types -export type Project = DrizzleProject; -export type Character = DrizzleCharacter; -export type CharacterRelationship = DrizzleCharacterRelationship; -export type Scene = DrizzleScene; - -export interface CharacterStats { - characterId: number; - totalScreenTime: number; - totalDialogueLines: number; - sceneCount: number; - relationshipCount: number; -} diff --git a/server/websocket/index.ts b/server/websocket/index.ts deleted file mode 100644 index 1dc91da6f..000000000 --- a/server/websocket/index.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * WebSocket Server Entry Point - * Starts the Yjs sync server with JWT authentication - */ - -import { createWebSocketServer } from './websocket/server'; - -interface ServerConfig { - port: number; - jwtSecret: string; - enableAuth: boolean; -} - -/** - * Start the WebSocket sync server - */ -export async function startServer(config: ServerConfig) { - const { port, jwtSecret, enableAuth } = config; - - // Auth middleware for JWT token validation - const authMiddleware = async (token: string) => { - if (!enableAuth) { - return { userId: 'anonymous', projectId: 'default' }; - } - - try { - const jwt = require('jsonwebtoken'); - const decoded = jwt.verify(token, jwtSecret); - return { - userId: (decoded as any).userId, - projectId: (decoded as any).projectId, - }; - } catch (error) { - throw new Error('Invalid JWT token'); - } - }; - - const server = createWebSocketServer(port, { - authMiddleware: enableAuth ? authMiddleware : undefined, - }); - - server.on('listening', () => { - console.log(`WebSocket sync server listening on port ${port}`); - console.log(`Authentication ${enableAuth ? 'enabled' : 'disabled'}`); - }); - - server.on('error', (error) => { - console.error('WebSocket server error:', error); - }); - - // Graceful shutdown - process.on('SIGINT', () => { - console.log('\nShutting down WebSocket server...'); - server.clients.forEach((client) => client.close()); - server.close(() => { - console.log('WebSocket server closed'); - process.exit(0); - }); - }); - - return server; -} - -// If run directly, start the server -if (require.main === module) { - const jwtSecret = process.env.JWT_SECRET; - if (!jwtSecret) { - throw new Error('JWT_SECRET environment variable is required. Please set it before starting the server.'); - } - - const config: ServerConfig = { - port: parseInt(process.env.WS_PORT || '8080', 10), - jwtSecret, - enableAuth: process.env.ENABLE_AUTH !== 'false', - }; - - startServer(config).catch((error) => { - console.error('Failed to start server:', error); - process.exit(1); - }); -} diff --git a/server/websocket/server.ts b/server/websocket/server.ts deleted file mode 100644 index 148a5a6d6..000000000 --- a/server/websocket/server.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * WebSocket Server for Yjs CRDT Sync - * Node.js server using y-websocket adapter - */ - -import { WebSocketServer } from 'ws'; -import { applyUpdate, encodeStateAsUpdate, Doc } from 'yjs'; - -type DocMessage = { - type: 'sync'; - args: [Uint8Array]; -}; - -type SyncStep1Message = { - type: 'sync'; - args: [Uint8Array]; -}; - -type SyncStep2Message = { - type: 'sync'; - args: [Uint8Array, Uint8Array]; -}; - -type UpdateMessage = { - type: 'update'; - args: [Uint8Array]; -}; - -export type Message = DocMessage | SyncStep1Message | SyncStep2Message | UpdateMessage; - -// Store document states in memory (in production, use Redis or persistent storage) -const docs: Map = new Map(); -const clients: Map> = new Map(); - -interface WebSocketWithDoc extends WebSocket { - docName?: string; -} - -/** - * Initialize the WebSocket server - */ -export function createWebSocketServer( - port: number, - options: { - authMiddleware?: (token: string) => Promise<{ userId: string; projectId: string }>; - } = {} -): WebSocketServer { - const { authMiddleware } = options; - - const server = new WebSocketServer({ port }); - - server.on('connection', async (ws: WebSocketWithDoc, req) => { - // Extract document name from URL query params - const url = new URL(req.url || '', `http://${req.headers.host}`); - const docName = url.pathname.split('/').pop() || 'default'; - - // Validate origin to prevent WebSocket CSRF - const origin = req.headers.origin; - if (authMiddleware && origin) { - const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000']; - if (!allowedOrigins.includes(origin)) { - console.warn(`Origin validation failed: ${origin}`); - ws.close(4002); - return; - } - } - - // Authenticate connection if auth middleware provided - const token = url.searchParams.get('token'); - let userId: string | undefined; - let projectId: string | undefined; - - if (authMiddleware) { - if (!token) { - console.warn('Authentication required but no token provided'); - ws.send(JSON.stringify({ type: 'error', message: 'Authentication required' })); - ws.close(4001); - return; - } - - try { - const auth = await authMiddleware(token); - userId = auth.userId; - projectId = auth.projectId; - console.log(`WebSocket connection authenticated: ${userId} for ${docName}`); - } catch (error) { - console.error('Authentication failed:', error); - ws.send(JSON.stringify({ type: 'error', message: 'Authentication failed' })); - ws.close(); - return; - } - } - - ws.docName = docName; - - // Initialize document state if not exists - if (!docs.has(docName)) { - docs.set(docName, new Uint8Array()); - clients.set(docName, new Set()); - } - - // Add client to the document's client set - clients.get(docName)!.add(ws); - - // Send initial sync - const initialState = docs.get(docName)!; - ws.send(encodeSyncStep1(initialState)); - - // Handle incoming messages - ws.on('message', (data: Buffer | ArrayBuffer) => { - handleMessage(ws, docName, data, userId, projectId); - }); - - // Handle disconnection - ws.on('close', () => { - clients.get(docName)?.delete(ws); - console.log(`Client disconnected from ${docName}. Remaining clients: ${clients.get(docName)?.size || 0}`); - }); - - console.log(`Client connected to ${docName}${userId ? ` (user: ${userId})` : ''}`); - }); - - return server; -} - -/** - * Encode sync step 1 (send document state as binary) - */ -function encodeSyncStep1(state: Uint8Array): Uint8Array { - return state; -} - -/** - * Handle incoming WebSocket message - */ -function handleMessage( - ws: WebSocketWithDoc, - docName: string, - data: Buffer | ArrayBuffer, - userId?: string, - projectId?: string -) { - try { - const message = JSON.parse(data.toString()) as Message; - - switch (message.type) { - case 'sync': - handleSync(ws, docName, message, userId, projectId); - break; - - case 'update': - handleUpdate(ws, docName, message, userId); - break; - } - } catch (error) { - console.error('Error handling message:', error); - ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' })); - } -} - -/** - * Handle sync message - */ -function handleSync(ws: WebSocketWithDoc, docName: string, message: SyncStep1Message | SyncStep2Message) { - const currentState = docs.get(docName) || new Uint8Array(); - - if (message.args.length === 1) { - // Sync step 1: client sends its state, server responds with full state - const clientState = message.args[0]; - - // Send full document state to client - const response = encodeSyncStep1(currentState); - ws.send(response); - } else if (message.args.length === 2) { - // Sync step 2: client sends its state, server sends missing updates - const clientState = message.args[0]; - - // Calculate missing updates (simplified - in production use Yjs protocol) - const missingUpdates = currentState; - - const response = JSON.stringify({ - type: 'sync', - args: [Array.from(missingUpdates)], - }); - ws.send(new TextEncoder().encode(response)); - } -} - -/** - * Handle update message - */ -function handleUpdate(ws: WebSocketWithDoc, docName: string, message: UpdateMessage) { - const update = message.args[0]; - let currentState = docs.get(docName) || new Uint8Array(); - - // Apply update to document state - try { - const doc = new Doc(); - applyUpdate(doc, update); - currentState = encodeStateAsUpdate(doc); - docs.set(docName, currentState); - - console.log(`Update applied to ${docName}. Size: ${currentState.length} bytes`); - - // Broadcast update to all other clients - const broadcastMsg = { - type: 'update', - args: [Array.from(update)], - }; - - clients.get(docName)?.forEach(client => { - if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(new TextEncoder().encode(JSON.stringify(broadcastMsg))); - } - }); - } catch (error) { - console.error('Error applying update:', error); - ws.send(JSON.stringify({ type: 'error', message: 'Failed to apply update' })); - } -} - -/** - * Get document stats (for monitoring) - */ -export function getDocStats(): Record { - const stats: Record = {}; - - docs.forEach((state, docName) => { - stats[docName] = { - clientCount: clients.get(docName)?.size || 0, - stateSize: state.length, - }; - }); - - return stats; -}