repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
The Living OS cockpit
stars
latest
clone command
git clone gitlawb://did:key:z6Mku78K...XywC/living-os-cockp...git clone gitlawb://did:key:z6Mku78K.../living-os-cockp...59751530feat: surface worker supervisor health in live work8h ago| #1 | import { NextRequest, NextResponse } from 'next/server'; |
| #2 | import { mkdir, writeFile } from 'fs/promises'; |
| #3 | import { join } from 'path'; |
| #4 | import { authErrorResponse, CANONICAL_KING_ID, getUserContext } from '@/lib/user-context'; |
| #5 | import { executeToolCall } from '@/lib/agentic-tools'; |
| #6 | |
| #7 | export const runtime = 'nodejs'; |
| #8 | export const maxDuration = 900; |
| #9 | |
| #10 | const INBOX_ROOT = '/home/kingbau/video_intake/inbox'; |
| #11 | |
| #12 | function safeName(name: string) { |
| #13 | return name.replace(/[^a-zA-Z0-9._ -]/g, '_').replace(/\s+/g, ' ').trim() || 'upload'; |
| #14 | } |
| #15 | |
| #16 | function parseUrls(value: FormDataEntryValue | null) { |
| #17 | if (!value) return []; |
| #18 | try { |
| #19 | const parsed = JSON.parse(String(value)); |
| #20 | return Array.isArray(parsed) ? parsed.map(String).filter(Boolean) : []; |
| #21 | } catch { |
| #22 | return String(value).split(/[\n,]+/g).map(item => item.trim()).filter(Boolean); |
| #23 | } |
| #24 | } |
| #25 | |
| #26 | // Hosts we accept for content intake (matched as host === h or *.h). Generous — |
| #27 | // covers the platforms King actually shares from. yt-dlp does the real extraction. |
| #28 | const SOCIAL_HOSTS = [ |
| #29 | 'instagram.com', 'youtube.com', 'youtu.be', 'tiktok.com', 'x.com', 'twitter.com', |
| #30 | 'fxtwitter.com', 'vxtwitter.com', 'facebook.com', 'fb.watch', 'fb.com', |
| #31 | 'vimeo.com', 'rumble.com', 'twitch.tv', 'reddit.com', 'redd.it', 'v.redd.it', |
| #32 | 'dailymotion.com', 'dai.ly', 'streamable.com', 'soundcloud.com', 't.me', |
| #33 | 'odysee.com', 'bitchute.com', 'kick.com', |
| #34 | ]; |
| #35 | |
| #36 | // Tracking/share params to strip everywhere. The Instagram share suffix (?igsh=) |
| #37 | // is the one that breaks intake; the rest are the usual analytics noise. |
| #38 | const TRACKING_PARAM = /^(igsh|igshid|si|feature|utm_[a-z_]+|fbclid|gclid|gbraid|wbraid|mc_[a-z]+|ref|ref_src|ref_url|source|spm|_branch_match_id|__tn__|__cft__.*|cmpid|ncid)$/i; |
| #39 | // X/Twitter share links use ?s=&t= for tracking — strip only on those hosts. |
| #40 | const X_TRACKING_PARAM = /^(s|t|cxt|ref_url|ref_src)$/i; |
| #41 | |
| #42 | function hostMatches(host: string, allowed: string): boolean { |
| #43 | return host === allowed || host.endsWith('.' + allowed); |
| #44 | } |
| #45 | |
| #46 | // Validate + canonicalize one share URL. Returns the clean URL (tracking params |
| #47 | // stripped) or an explicit error — never a cryptic "did not match the expected |
| #48 | // pattern". Accepts only real http(s) URLs from an expected social/video host. |
| #49 | function normalizeSocialUrl(raw: string): { url?: string; error?: string } { |
| #50 | let candidate = String(raw || '').trim(); |
| #51 | if (!candidate) return { error: 'empty value' }; |
| #52 | // Allow bare host/path (e.g. "instagram.com/reel/..") by assuming https. |
| #53 | if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(candidate)) { |
| #54 | if (/^[\w-]+(\.[\w-]+)+\//.test(candidate) || /^[\w-]+(\.[\w-]+)+$/.test(candidate)) { |
| #55 | candidate = 'https://' + candidate; |
| #56 | } else { |
| #57 | return { error: `not an http(s) URL: ${raw}` }; |
| #58 | } |
| #59 | } |
| #60 | let u: URL; |
| #61 | try { |
| #62 | u = new URL(candidate); |
| #63 | } catch { |
| #64 | return { error: `invalid URL: ${raw}` }; |
| #65 | } |
| #66 | if (u.protocol !== 'http:' && u.protocol !== 'https:') { |
| #67 | return { error: `unsupported scheme (${u.protocol}): ${raw}` }; |
| #68 | } |
| #69 | const host = u.hostname.replace(/^www\./i, '').toLowerCase(); |
| #70 | if (!host.includes('.')) return { error: `not a public host: ${raw}` }; |
| #71 | const isSocial = SOCIAL_HOSTS.some(h => hostMatches(host, h)); |
| #72 | if (!isSocial) { |
| #73 | return { error: `host not supported for content intake: ${host} (expected Instagram, YouTube, TikTok, X, etc.)` }; |
| #74 | } |
| #75 | const isX = hostMatches(host, 'x.com') || hostMatches(host, 'twitter.com'); |
| #76 | for (const key of [...u.searchParams.keys()]) { |
| #77 | if (TRACKING_PARAM.test(key) || (isX && X_TRACKING_PARAM.test(key))) { |
| #78 | u.searchParams.delete(key); |
| #79 | } |
| #80 | } |
| #81 | u.hash = ''; |
| #82 | let out = u.toString().replace(/\?$/, ''); |
| #83 | return { url: out }; |
| #84 | } |
| #85 | |
| #86 | export async function POST(req: NextRequest) { |
| #87 | try { |
| #88 | const ctx = await getUserContext(); |
| #89 | if (ctx.canonicalMemberId !== CANONICAL_KING_ID) { |
| #90 | return NextResponse.json({ ok: false, error: 'content_intake is limited to King admin identity in v1' }, { status: 403 }); |
| #91 | } |
| #92 | |
| #93 | const form = await req.formData(); |
| #94 | const stamp = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15) + 'Z'; |
| #95 | const inbox = join(INBOX_ROOT, stamp); |
| #96 | await mkdir(inbox, { recursive: true }); |
| #97 | |
| #98 | const files = form.getAll('files').filter((item): item is File => item instanceof File); |
| #99 | const uploadedPaths: string[] = []; |
| #100 | for (const file of files) { |
| #101 | const path = join(inbox, `${Date.now()}_${safeName(file.name)}`); |
| #102 | await writeFile(path, Buffer.from(await file.arrayBuffer())); |
| #103 | uploadedPaths.push(path); |
| #104 | } |
| #105 | |
| #106 | // Normalize + validate share URLs at the boundary: strip tracking params |
| #107 | // (?igsh=, ?si=, utm_*, fbclid, …) so the clean canonical URL is what gets |
| #108 | // ingested, and reject non-URLs with an explicit message instead of a cryptic |
| #109 | // "did not match the expected pattern". |
| #110 | const rawUrls = parseUrls(form.get('urls')); |
| #111 | const urls: string[] = []; |
| #112 | const urlErrors: string[] = []; |
| #113 | for (const raw of rawUrls) { |
| #114 | const { url, error } = normalizeSocialUrl(raw); |
| #115 | if (url) urls.push(url); |
| #116 | else if (error) urlErrors.push(error); |
| #117 | } |
| #118 | if (urlErrors.length) { |
| #119 | return NextResponse.json( |
| #120 | { ok: false, error: `Rejected ${urlErrors.length} URL(s): ${urlErrors.join('; ')}` }, |
| #121 | { status: 400 }, |
| #122 | ); |
| #123 | } |
| #124 | const mode = String(form.get('mode') || 'auto'); |
| #125 | const focus = String(form.get('focus') || ''); |
| #126 | const targetCollection = String(form.get('target_collection') || 'clip_bank'); |
| #127 | const input = [...uploadedPaths, ...urls]; |
| #128 | if (!input.length) { |
| #129 | return NextResponse.json({ ok: false, error: 'No files or URLs supplied.' }, { status: 400 }); |
| #130 | } |
| #131 | |
| #132 | const call = { |
| #133 | tool: 'content_intake', |
| #134 | parameters: { |
| #135 | input, |
| #136 | mode, |
| #137 | focus, |
| #138 | target_collection: targetCollection, |
| #139 | }, |
| #140 | requiresConfirmation: false, |
| #141 | reason: 'Approved from the cockpit Content Intake panel.', |
| #142 | }; |
| #143 | const result = await executeToolCall(ctx, call); |
| #144 | return NextResponse.json({ ...result, ok: Boolean(result.ok), inbox, uploaded_paths: uploadedPaths }); |
| #145 | } catch (error) { |
| #146 | return authErrorResponse(error); |
| #147 | } |
| #148 | } |
| #149 |