Hermy HQ: self-hostable mission-control template for your Hermes agent
A Next.js 16 + Prisma/Postgres dashboard that pairs with a local Hermes agent over a Postgres message bus: dispatch work, approve side-effecting actions, browse the agent's memory, and watch it run. Ships with an agent-onboarding prompt (ONBOARDING.md) so your Hermes can install it for you step by step. All secrets are env-configured; nothing sensitive is bundled. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,519 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
binaryTargets = ["native", "linux-arm64-openssl-3.0.x"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String?
|
||||
access_token String?
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String?
|
||||
session_state String?
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
model Draft {
|
||||
id String @id @default(uuid())
|
||||
text String
|
||||
title String?
|
||||
model String?
|
||||
status String @default("pending")
|
||||
type String?
|
||||
category String?
|
||||
hookType String?
|
||||
feedbackRating String?
|
||||
feedbackReason String?
|
||||
viralScore Json?
|
||||
impressionScore Json?
|
||||
scheduledDate String?
|
||||
scheduledTime String?
|
||||
visualUrl String?
|
||||
variantGroup String?
|
||||
postedAt DateTime?
|
||||
postedUrl String?
|
||||
tweetUrl String?
|
||||
tweetId String?
|
||||
metrics Json?
|
||||
editHistory Json? @default("[]")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
quoteUrl String?
|
||||
quoteText String?
|
||||
|
||||
// Content intelligence fields
|
||||
source String? // "sage" | "organic" | "csv_import"
|
||||
engagementScore Float? // time-weighted composite score (updated by scoring engine)
|
||||
engagementTier String? // "top20" | "bottom20" | null (cleared/set on rescore)
|
||||
|
||||
@@index([status])
|
||||
@@index([createdAt])
|
||||
@@index([tweetId])
|
||||
@@index([source])
|
||||
@@index([engagementTier])
|
||||
}
|
||||
|
||||
model TweetMetric {
|
||||
id String @id @default(cuid())
|
||||
draftId String
|
||||
tweetId String?
|
||||
url String?
|
||||
postedAt DateTime?
|
||||
checkpoints Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([draftId])
|
||||
}
|
||||
|
||||
model Idea {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
description String?
|
||||
category String?
|
||||
type String?
|
||||
model String?
|
||||
status String?
|
||||
timestamp DateTime @default(now())
|
||||
|
||||
@@index([category])
|
||||
@@index([type])
|
||||
}
|
||||
|
||||
model ContentCalendar {
|
||||
id String @id @default(cuid())
|
||||
week String @unique
|
||||
data Json
|
||||
}
|
||||
|
||||
model YoutubeIdea {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
hook String?
|
||||
angle String?
|
||||
hookType String?
|
||||
funnelStage String?
|
||||
status String @default("pending")
|
||||
rejectedReason String?
|
||||
sourceUrl String?
|
||||
sourceSummary String?
|
||||
competitionScore String? // "gap" | "moderate" | "crowded"
|
||||
competitionData Json? // { resultCount, avgViews, topVideos[], scoredAt }
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model YoutubeScript {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
hook String?
|
||||
storySetup String?
|
||||
conflict String?
|
||||
insight String?
|
||||
cta String?
|
||||
caption String?
|
||||
onScreenText String?
|
||||
hookType String?
|
||||
funnelStage String?
|
||||
factCheck Json?
|
||||
fullScript String?
|
||||
status String @default("draft")
|
||||
rejectedReason String?
|
||||
// SEO Package
|
||||
seoTitle String?
|
||||
seoDescription String?
|
||||
seoTags Json?
|
||||
seoChapters Json?
|
||||
titleVariants Json?
|
||||
thumbnailConcept String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model YoutubeFeedback {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
reason String?
|
||||
type String?
|
||||
date DateTime @default(now())
|
||||
}
|
||||
|
||||
model LongformScript {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
type String?
|
||||
status String @default("draft")
|
||||
platform String?
|
||||
platforms Json?
|
||||
description String?
|
||||
hook String?
|
||||
outline String?
|
||||
fullScript String?
|
||||
targetLength String?
|
||||
factCheck Json?
|
||||
notes String?
|
||||
rejectedReason String?
|
||||
youtubeUrl String?
|
||||
thumbnailUrl String?
|
||||
tweetUrl String?
|
||||
youtubeViews Int?
|
||||
youtubeLikes Int?
|
||||
youtubeComments Int?
|
||||
tweetViews Int?
|
||||
tweetLikes Int?
|
||||
tweetBookmarks Int?
|
||||
tweetRetweets Int?
|
||||
tweetReplies Int?
|
||||
postedAt DateTime?
|
||||
competitionScore String? // "gap" | "moderate" | "crowded"
|
||||
competitionData Json? // { resultCount, avgViews, topVideos[], scoredAt }
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model AgentState {
|
||||
id String @id
|
||||
name String
|
||||
emoji String?
|
||||
role String?
|
||||
status String @default("offline")
|
||||
lastActive DateTime?
|
||||
tasksCompleted Int @default(0)
|
||||
totalCost Float @default(0)
|
||||
currentTask String?
|
||||
recentActivity Json? @default("[]")
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model AgentBusMessage {
|
||||
id String @id @default(uuid())
|
||||
fromAgent String
|
||||
toAgent String
|
||||
type String
|
||||
content String
|
||||
metadata Json?
|
||||
read Boolean @default(false)
|
||||
timestamp DateTime @default(now())
|
||||
|
||||
@@index([toAgent, read])
|
||||
@@index([fromAgent])
|
||||
}
|
||||
|
||||
model BattleRoyaleBot {
|
||||
id String @id
|
||||
state Json
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model ContentRequest {
|
||||
id String @id @default(uuid())
|
||||
prompt String
|
||||
status String @default("pending")
|
||||
resultDraftIds Json?
|
||||
createdAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
}
|
||||
|
||||
// ─── Generic Key-Value Store ────────────────────────────────────
|
||||
|
||||
model DataStore {
|
||||
key String @id
|
||||
data Json
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ─── Daily Briefs ───────────────────────────────────────────────
|
||||
|
||||
model Brief {
|
||||
id String @id @default(cuid())
|
||||
date String @unique
|
||||
content String @db.Text
|
||||
source String @default("brief")
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// ─── Missions ───────────────────────────────────────────────────
|
||||
|
||||
model Mission {
|
||||
id String @id @default(uuid())
|
||||
agentId String
|
||||
title String
|
||||
description String @db.Text
|
||||
status String @default("pending") // pending, active, completed, failed
|
||||
priority String @default("medium") // high, medium, low
|
||||
result String? @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
completedAt DateTime?
|
||||
|
||||
@@index([agentId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// ─── Articles ───────────────────────────────────────────────────
|
||||
|
||||
model Article {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
body String @db.Text
|
||||
track String // "mega-viral" | "local-viral" | "icp-viral"
|
||||
status String @default("idea") // "idea" | "draft" | "ready" | "posted"
|
||||
qtTweet String? @db.Text
|
||||
qtUrl String?
|
||||
heroImageUrl String?
|
||||
heroImageHtml String? @db.Text
|
||||
inspirationUrls String? @db.Text // JSON array of URLs
|
||||
themes String? // JSON array of themes
|
||||
scheduledDate String?
|
||||
postedAt DateTime?
|
||||
postedUrl String?
|
||||
impressions Int?
|
||||
likes Int?
|
||||
bookmarks Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status])
|
||||
@@index([track])
|
||||
@@index([scheduledDate])
|
||||
}
|
||||
|
||||
model SavedTitle {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
track String
|
||||
themes String? // JSON array
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([track])
|
||||
}
|
||||
|
||||
// ─── Client Pulse Telegram Bot ─────────────────────────────────
|
||||
|
||||
model ClientPulseClient {
|
||||
id String @id @default(cuid())
|
||||
notionPageId String? @unique
|
||||
clientName String
|
||||
status String?
|
||||
services Json @default("[]")
|
||||
productManagerName String?
|
||||
renewalDate DateTime?
|
||||
endDate DateTime?
|
||||
expectedCheckinHours Int @default(72)
|
||||
sourceUpdatedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
chats ClientPulseChat[]
|
||||
analyses ClientPulseAnalysis[]
|
||||
alerts ClientPulseAlert[]
|
||||
|
||||
@@index([status])
|
||||
@@index([clientName])
|
||||
@@index([productManagerName])
|
||||
}
|
||||
|
||||
model ClientPulseChat {
|
||||
id String @id @default(cuid())
|
||||
telegramChatId String @unique
|
||||
title String
|
||||
linkedClientId String?
|
||||
status String @default("active")
|
||||
internalUserIds Json @default("[]")
|
||||
lastSeenAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
linkedClient ClientPulseClient? @relation(fields: [linkedClientId], references: [id], onDelete: SetNull)
|
||||
messages ClientPulseMessage[]
|
||||
alerts ClientPulseAlert[]
|
||||
|
||||
@@index([linkedClientId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model ClientPulseMessage {
|
||||
id String @id @default(cuid())
|
||||
telegramMessageId String
|
||||
telegramChatId String
|
||||
chatId String?
|
||||
senderTelegramId String
|
||||
senderName String
|
||||
isInternal Boolean @default(false)
|
||||
isBot Boolean @default(false)
|
||||
text String @db.Text
|
||||
sentAt DateTime
|
||||
intent String @default("other")
|
||||
sentiment String @default("neutral")
|
||||
needsReply Boolean @default(false)
|
||||
rawJson Json?
|
||||
createdAt DateTime @default(now())
|
||||
chat ClientPulseChat? @relation(fields: [chatId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([telegramChatId, telegramMessageId])
|
||||
@@index([telegramChatId, sentAt])
|
||||
@@index([chatId, sentAt])
|
||||
@@index([needsReply])
|
||||
}
|
||||
|
||||
model ClientPulseAnalysis {
|
||||
id String @id @default(cuid())
|
||||
clientId String
|
||||
windowStart DateTime
|
||||
windowEnd DateTime
|
||||
sentimentScore Int
|
||||
responseScore Int
|
||||
cadenceScore Int
|
||||
renewalRiskScore Int
|
||||
positiveSignalScore Int @default(70)
|
||||
overallScore Int
|
||||
category String
|
||||
reasons Json @default("[]")
|
||||
evidence Json @default("[]")
|
||||
recommendedAction String? @db.Text
|
||||
llmSummary String? @db.Text
|
||||
analysisSource String @default("fallback")
|
||||
analyzedAt DateTime @default(now())
|
||||
client ClientPulseClient @relation(fields: [clientId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([clientId, analyzedAt])
|
||||
@@index([category])
|
||||
@@index([overallScore])
|
||||
}
|
||||
|
||||
model ClientPulseAlert {
|
||||
id String @id @default(cuid())
|
||||
clientId String?
|
||||
chatId String?
|
||||
type String
|
||||
severity String
|
||||
evidence Json @default("[]")
|
||||
reason String @db.Text
|
||||
recommendedAction String? @db.Text
|
||||
sentTo String?
|
||||
sentAt DateTime?
|
||||
acknowledgedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
client ClientPulseClient? @relation(fields: [clientId], references: [id], onDelete: SetNull)
|
||||
chat ClientPulseChat? @relation(fields: [chatId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([clientId, createdAt])
|
||||
@@index([severity])
|
||||
@@index([sentAt])
|
||||
@@index([acknowledgedAt])
|
||||
}
|
||||
|
||||
// ─── Hermes bus (Hermy HQ website ↔ Hermes on the Mac mini) ─────
|
||||
// Shared Postgres is the message bus. A bridge on the mini polls
|
||||
// AgentRequest, runs approved/safe ones via the `hermes` CLI, and
|
||||
// mirrors kanban tasks + activity back into HermesTask / AgentEvent.
|
||||
|
||||
model AgentRequest {
|
||||
id String @id @default(cuid())
|
||||
origin String @default("web") // "web" | "hermes"
|
||||
kind String @default("oneshot") // "oneshot" | "kanban" | "chat"
|
||||
title String
|
||||
prompt String?
|
||||
sideEffecting Boolean @default(false)
|
||||
status String @default("queued") // queued | awaiting_approval | approved | running | done | failed | rejected
|
||||
result String?
|
||||
error String?
|
||||
hermesTaskId String?
|
||||
decidedAt DateTime?
|
||||
startedAt DateTime?
|
||||
finishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model AgentEvent {
|
||||
id String @id @default(cuid())
|
||||
kind String @default("activity") // activity | run | status | memory
|
||||
title String
|
||||
detail String?
|
||||
agent String?
|
||||
level String @default("info") // info | up | warn | down
|
||||
meta Json?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model HermesTask {
|
||||
id String @id // Hermes task id
|
||||
board String @default("default")
|
||||
title String
|
||||
assignee String?
|
||||
status String @default("todo")
|
||||
priority Int?
|
||||
result String?
|
||||
updatedAt DateTime @default(now())
|
||||
syncedAt DateTime @updatedAt
|
||||
|
||||
@@index([board])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// ─── Hermes Memory Wiki (warm tier — git-tracked markdown, mirrored) ─────
|
||||
model HermesMemory {
|
||||
id String @id // wiki entry slug (relative path w/o .md)
|
||||
path String // file path relative to wiki root
|
||||
type String @default("fact")
|
||||
title String
|
||||
status String @default("active")
|
||||
confidence String?
|
||||
provenance String?
|
||||
tags String[]
|
||||
links String[]
|
||||
body String @default("")
|
||||
validFrom DateTime?
|
||||
validTo DateTime?
|
||||
updatedAt DateTime @default(now())
|
||||
syncedAt DateTime @updatedAt
|
||||
|
||||
@@index([type])
|
||||
@@index([status])
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
const DATA_DIR = './data'
|
||||
|
||||
function readJson(filename: string): unknown {
|
||||
const p = path.join(DATA_DIR, filename)
|
||||
if (!fs.existsSync(p)) { console.warn(`⚠️ Missing: ${filename}`); return null }
|
||||
return JSON.parse(fs.readFileSync(p, 'utf-8'))
|
||||
}
|
||||
|
||||
function safeDate(val: unknown, fallback = new Date()): Date {
|
||||
if (!val) return fallback
|
||||
const d = new Date(val as string)
|
||||
return isNaN(d.getTime()) ? fallback : d
|
||||
}
|
||||
|
||||
async function seedDrafts() {
|
||||
const raw = readJson('drafts.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`📝 Seeding ${raw.length} drafts...`)
|
||||
|
||||
for (const d of raw) {
|
||||
const feedback = d.feedback ?? {}
|
||||
await prisma.draft.upsert({
|
||||
where: { id: d.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: d.id,
|
||||
text: d.text ?? '',
|
||||
title: d.title ?? null,
|
||||
model: d.model ?? null,
|
||||
status: d.status ?? 'pending',
|
||||
type: d.type ?? null,
|
||||
category: d.category ?? null,
|
||||
hookType: d.hookType ?? null,
|
||||
feedbackRating: feedback.rating ?? null,
|
||||
feedbackReason: feedback.reason ?? null,
|
||||
viralScore: d.viralScore ?? undefined,
|
||||
impressionScore: d.impressionScore ?? undefined,
|
||||
scheduledDate: d.scheduledDate ?? null,
|
||||
scheduledTime: d.scheduledTime ?? null,
|
||||
visualUrl: d.visualUrl ?? null,
|
||||
variantGroup: d.variantGroup ?? null,
|
||||
postedAt: d.postedAt ? safeDate(d.postedAt) : null,
|
||||
postedUrl: d.postedUrl ?? null,
|
||||
tweetUrl: d.tweetUrl ?? null,
|
||||
tweetId: d.tweetId ?? null,
|
||||
metrics: d.metrics ?? undefined,
|
||||
editHistory: d.editHistory ?? [],
|
||||
createdAt: safeDate(d.createdAt),
|
||||
updatedAt: safeDate(d.updatedAt),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} drafts done`)
|
||||
}
|
||||
|
||||
async function seedIdeas() {
|
||||
const raw = readJson('ideas.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`💡 Seeding ${raw.length} ideas...`)
|
||||
|
||||
for (const d of raw) {
|
||||
await prisma.idea.upsert({
|
||||
where: { id: d.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: d.id,
|
||||
title: d.title ?? '',
|
||||
description: d.description ?? null,
|
||||
category: d.category ?? null,
|
||||
type: d.type ?? null,
|
||||
model: d.model ?? null,
|
||||
status: d.status ?? null,
|
||||
timestamp: safeDate(d.timestamp),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} ideas done`)
|
||||
}
|
||||
|
||||
async function seedLongformScripts() {
|
||||
const raw = readJson('longform-scripts.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`📄 Seeding ${raw.length} longform scripts...`)
|
||||
|
||||
for (const d of raw) {
|
||||
await prisma.longformScript.upsert({
|
||||
where: { id: d.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: d.id,
|
||||
title: d.title ?? '',
|
||||
type: d.type ?? null,
|
||||
status: d.status ?? 'draft',
|
||||
platform: d.platform ?? null,
|
||||
platforms: d.platforms ?? undefined,
|
||||
description: d.description ?? null,
|
||||
hook: d.hook ?? null,
|
||||
outline: d.outline ?? null,
|
||||
fullScript: d.fullScript ?? null,
|
||||
targetLength: d.targetLength ?? null,
|
||||
factCheck: d.factCheck ?? undefined,
|
||||
notes: d.notes ?? null,
|
||||
createdAt: safeDate(d.createdAt),
|
||||
updatedAt: safeDate(d.updatedAt),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} longform scripts done`)
|
||||
}
|
||||
|
||||
async function seedYoutubeScripts() {
|
||||
const raw = readJson('youtube-scripts.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`🎬 Seeding ${raw.length} YouTube scripts...`)
|
||||
|
||||
for (const d of raw) {
|
||||
await prisma.youtubeScript.upsert({
|
||||
where: { id: d.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: d.id,
|
||||
title: d.title ?? '',
|
||||
hook: d.hook ?? null,
|
||||
storySetup: d.storySetup ?? null,
|
||||
conflict: d.conflict ?? null,
|
||||
insight: d.insight ?? null,
|
||||
cta: d.cta ?? null,
|
||||
caption: d.caption ?? null,
|
||||
onScreenText: d.onScreenText ?? null,
|
||||
hookType: d.hookType ?? null,
|
||||
funnelStage: d.funnelStage ?? null,
|
||||
factCheck: d.factCheck ?? undefined,
|
||||
status: d.status ?? 'draft',
|
||||
rejectedReason: d.rejectedReason ?? null,
|
||||
createdAt: safeDate(d.createdAt),
|
||||
updatedAt: safeDate(d.updatedAt),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} YouTube scripts done`)
|
||||
}
|
||||
|
||||
async function seedYoutubeIdeas() {
|
||||
const raw = readJson('youtube-ideas.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`🎯 Seeding ${raw.length} YouTube ideas...`)
|
||||
|
||||
// Delete and re-insert (no stable IDs in source file)
|
||||
await prisma.youtubeIdea.deleteMany()
|
||||
for (const d of raw) {
|
||||
await prisma.youtubeIdea.create({
|
||||
data: {
|
||||
title: d.title ?? '',
|
||||
hook: d.hook ?? null,
|
||||
angle: d.angle ?? null,
|
||||
hookType: d.hookType ?? null,
|
||||
funnelStage: d.funnelStage ?? null,
|
||||
status: d.status ?? 'pending',
|
||||
rejectedReason: d.rejectedReason ?? null,
|
||||
createdAt: safeDate(d.createdAt),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} YouTube ideas done`)
|
||||
}
|
||||
|
||||
async function seedYoutubeFeedback() {
|
||||
const raw = readJson('youtube-feedback.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`💬 Seeding ${raw.length} YouTube feedback...`)
|
||||
|
||||
await prisma.youtubeFeedback.deleteMany()
|
||||
for (const d of raw) {
|
||||
await prisma.youtubeFeedback.create({
|
||||
data: {
|
||||
title: d.title ?? '',
|
||||
reason: d.reason ?? null,
|
||||
type: d.type ?? null,
|
||||
date: safeDate(d.date),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} YouTube feedback done`)
|
||||
}
|
||||
|
||||
async function seedTweetMetrics() {
|
||||
const raw = readJson('tweet-metrics.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`📊 Seeding ${raw.length} tweet metrics...`)
|
||||
|
||||
await prisma.tweetMetric.deleteMany()
|
||||
for (const d of raw) {
|
||||
await prisma.tweetMetric.create({
|
||||
data: {
|
||||
draftId: d.draftId ?? '',
|
||||
tweetId: d.tweetId ?? null,
|
||||
url: d.url ?? null,
|
||||
postedAt: d.postedAt ? safeDate(d.postedAt) : null,
|
||||
checkpoints: d.checkpoints ?? undefined,
|
||||
createdAt: safeDate(d.createdAt),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} tweet metrics done`)
|
||||
}
|
||||
|
||||
async function seedAgentState() {
|
||||
const raw = readJson('agent-state.json') as any[]
|
||||
if (!raw) return
|
||||
console.log(`🤖 Seeding ${raw.length} agent states...`)
|
||||
|
||||
for (const d of raw) {
|
||||
await prisma.agentState.upsert({
|
||||
where: { id: d.id },
|
||||
update: {
|
||||
name: d.name,
|
||||
emoji: d.emoji ?? null,
|
||||
role: d.role ?? null,
|
||||
status: d.status ?? 'offline',
|
||||
lastActive: d.lastActive ? safeDate(d.lastActive) : null,
|
||||
tasksCompleted: d.tasksCompleted ?? 0,
|
||||
totalCost: d.totalCost ?? 0,
|
||||
currentTask: d.currentTask ?? null,
|
||||
recentActivity: d.recentActivity ?? [],
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
emoji: d.emoji ?? null,
|
||||
role: d.role ?? null,
|
||||
status: d.status ?? 'offline',
|
||||
lastActive: d.lastActive ? safeDate(d.lastActive) : null,
|
||||
tasksCompleted: d.tasksCompleted ?? 0,
|
||||
totalCost: d.totalCost ?? 0,
|
||||
currentTask: d.currentTask ?? null,
|
||||
recentActivity: d.recentActivity ?? [],
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} agent states done`)
|
||||
}
|
||||
|
||||
async function seedContentCalendar() {
|
||||
const raw = readJson('content-calendar.json') as any
|
||||
if (!raw) return
|
||||
// File is { week: "...", days: [...] }
|
||||
const week = raw.week
|
||||
if (!week) { console.warn('⚠️ content-calendar.json missing week field'); return }
|
||||
console.log(`📅 Seeding content calendar (week ${week})...`)
|
||||
|
||||
await prisma.contentCalendar.upsert({
|
||||
where: { week },
|
||||
update: { data: raw },
|
||||
create: { week, data: raw },
|
||||
})
|
||||
console.log(` ✅ Calendar done`)
|
||||
}
|
||||
|
||||
async function seedContentRequests() {
|
||||
const raw = readJson('content-requests.json') as any[]
|
||||
if (!raw || !raw.length) return
|
||||
console.log(`📬 Seeding ${raw.length} content requests...`)
|
||||
|
||||
for (const d of raw) {
|
||||
await prisma.contentRequest.upsert({
|
||||
where: { id: d.id },
|
||||
update: {},
|
||||
create: {
|
||||
id: d.id,
|
||||
prompt: d.prompt ?? '',
|
||||
status: d.status ?? 'pending',
|
||||
resultDraftIds: d.resultDraftIds ?? undefined,
|
||||
createdAt: safeDate(d.createdAt),
|
||||
completedAt: d.completedAt ? safeDate(d.completedAt) : null,
|
||||
},
|
||||
})
|
||||
}
|
||||
console.log(` ✅ ${raw.length} content requests done`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🌱 Starting Max HQ data seed...\n')
|
||||
|
||||
await seedAgentState()
|
||||
await seedDrafts()
|
||||
await seedIdeas()
|
||||
await seedLongformScripts()
|
||||
await seedYoutubeScripts()
|
||||
await seedYoutubeIdeas()
|
||||
await seedYoutubeFeedback()
|
||||
await seedTweetMetrics()
|
||||
await seedContentCalendar()
|
||||
await seedContentRequests()
|
||||
|
||||
console.log('\n✅ All done! Max HQ database is fully seeded.')
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => { console.error('❌ Seed failed:', e); process.exit(1) })
|
||||
.finally(() => prisma.$disconnect())
|
||||
Reference in New Issue
Block a user