asasvirtuais 0.1.0
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/README.md +78 -0
- package/actions/draw.ts +110 -0
- package/components/OAuthCard.tsx +346 -0
- package/components/icons.tsx +11 -0
- package/components/markdown.tsx +18 -0
- package/components/stack/list.tsx +21 -0
- package/components/stack/menu.tsx +40 -0
- package/components/stack/nav.tsx +39 -0
- package/components/table/fixed.tsx +59 -0
- package/components/table/key-value.tsx +19 -0
- package/components/ui/color-mode.tsx +108 -0
- package/components/ui/provider.tsx +15 -0
- package/components/ui/toaster.tsx +43 -0
- package/components/ui/tooltip.tsx +46 -0
- package/hooks/useBoolean.tsx +11 -0
- package/hooks/useForwardAs.tsx +29 -0
- package/hooks/useHash copy.tsx +27 -0
- package/hooks/useHash.tsx +27 -0
- package/hooks/useIsMobile.tsx +6 -0
- package/hooks/useOAuthTokens.ts +97 -0
- package/hooks/useOpenRouterModels.ts +80 -0
- package/lib/auth0.ts +11 -0
- package/lib/blob.ts +3 -0
- package/lib/client-token-storage.ts +216 -0
- package/lib/oauth-tokens.ts +85 -0
- package/lib/react/context.tsx +20 -0
- package/lib/react/index.ts +1 -0
- package/lib/tools.ts +375 -0
- package/next-env.d.ts +5 -0
- package/next.config.ts +23 -0
- package/package.json +72 -0
- package/packages/blob.ts +97 -0
- package/packages/chat/components/chat/feed/index.tsx +76 -0
- package/packages/chat/components/chat/feed/story.tsx +18 -0
- package/packages/chat/components/chat/index.tsx +16 -0
- package/packages/chat/components/chat/story.tsx +74 -0
- package/packages/chat/components/debug/index.tsx +54 -0
- package/packages/chat/components/header/index.tsx +14 -0
- package/packages/chat/components/header/menu/index.tsx +63 -0
- package/packages/chat/components/header/story.tsx +33 -0
- package/packages/chat/components/header/title/index.tsx +35 -0
- package/packages/chat/components/index.ts +13 -0
- package/packages/chat/components/input/index.tsx +17 -0
- package/packages/chat/components/input/menu/index.tsx +35 -0
- package/packages/chat/components/input/send.tsx +21 -0
- package/packages/chat/components/input/story.tsx +35 -0
- package/packages/chat/components/input/textarea/index.tsx +20 -0
- package/packages/chat/components/message/file.tsx +103 -0
- package/packages/chat/components/message/menu/index.tsx +26 -0
- package/packages/chat/components/message/story.tsx +49 -0
- package/packages/chat/components/messages/index.tsx +23 -0
- package/packages/chat/components/messages/story.tsx +11 -0
- package/packages/chat/components/ui/prose.tsx +263 -0
- package/packages/chat/edit-message.tsx +49 -0
- package/packages/chat/header.tsx +118 -0
- package/packages/chat/index.ts +14 -0
- package/packages/chat/input.tsx +89 -0
- package/packages/chat/message-menu.tsx +57 -0
- package/packages/chat/message.tsx +44 -0
- package/packages/chat/messages.tsx +44 -0
- package/packages/chat/model-selector.tsx +172 -0
- package/packages/chat/scenarios.tsx +68 -0
- package/packages/chat/settings.tsx +98 -0
- package/packages/chat/temperature-slider.tsx +67 -0
- package/packages/chat/tool-results.tsx +32 -0
- package/packages/crud/core.ts +75 -0
- package/packages/crud/fetcher.ts +64 -0
- package/packages/crud/index.ts +2 -0
- package/packages/crud/next.ts +128 -0
- package/packages/crud/react.tsx +365 -0
- package/packages/env.ts +8 -0
- package/packages/fields.tsx +157 -0
- package/packages/firebase.ts +13 -0
- package/packages/firestore.ts +51 -0
- package/packages/form.tsx +66 -0
- package/packages/next.ts +64 -0
- package/packages/openrouter.ts +4 -0
- package/packages/react/context.tsx +21 -0
- package/packages/react/crud.tsx +372 -0
- package/packages/react/hooks.ts +90 -0
- package/packages/react/store.tsx +20 -0
- package/packages/replit-db.ts +219 -0
- package/packages/wretch.ts +22 -0
- package/packages/yaml.ts +163 -0
- package/pnpm-workspace.yaml +4 -0
- package/server/db.ts +15 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Client-side token storage using IndexedDB for security
|
|
4
|
+
// IndexedDB is more secure than localStorage because:
|
|
5
|
+
// 1. Data is not automatically sent with every HTTP request
|
|
6
|
+
// 2. Larger storage capacity
|
|
7
|
+
// 3. Better performance for complex data
|
|
8
|
+
// 4. Data is structured and can be queried
|
|
9
|
+
|
|
10
|
+
const DB_NAME = 'OAuthTokensDB'
|
|
11
|
+
const DB_VERSION = 1
|
|
12
|
+
const STORE_NAME = 'tokens'
|
|
13
|
+
|
|
14
|
+
interface ClientToken {
|
|
15
|
+
id: string // platform id
|
|
16
|
+
platform: string
|
|
17
|
+
accessToken: string
|
|
18
|
+
expiresAt?: string
|
|
19
|
+
lastRefreshed: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class TokenStorage {
|
|
23
|
+
private db: IDBDatabase | null = null
|
|
24
|
+
|
|
25
|
+
async init(): Promise<void> {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
|
28
|
+
|
|
29
|
+
request.onerror = () => reject(request.error)
|
|
30
|
+
request.onsuccess = () => {
|
|
31
|
+
this.db = request.result
|
|
32
|
+
resolve()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
request.onupgradeneeded = (event) => {
|
|
36
|
+
const db = (event.target as IDBOpenDBRequest).result
|
|
37
|
+
|
|
38
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
39
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' })
|
|
40
|
+
store.createIndex('platform', 'platform', { unique: false })
|
|
41
|
+
store.createIndex('lastRefreshed', 'lastRefreshed', { unique: false })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private async ensureDB(): Promise<IDBDatabase> {
|
|
48
|
+
if (!this.db) {
|
|
49
|
+
await this.init()
|
|
50
|
+
}
|
|
51
|
+
if (!this.db) {
|
|
52
|
+
throw new Error('Failed to initialize IndexedDB')
|
|
53
|
+
}
|
|
54
|
+
return this.db
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async storeToken(token: ClientToken): Promise<void> {
|
|
58
|
+
const db = await this.ensureDB()
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
61
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
62
|
+
|
|
63
|
+
const request = store.put({
|
|
64
|
+
...token,
|
|
65
|
+
lastRefreshed: new Date().toISOString()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
request.onsuccess = () => resolve()
|
|
69
|
+
request.onerror = () => reject(request.error)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getToken(platformId: string): Promise<ClientToken | null> {
|
|
74
|
+
const db = await this.ensureDB()
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const transaction = db.transaction([STORE_NAME], 'readonly')
|
|
77
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
78
|
+
const request = store.get(platformId)
|
|
79
|
+
|
|
80
|
+
request.onsuccess = () => {
|
|
81
|
+
const token = request.result
|
|
82
|
+
if (token && token.expiresAt) {
|
|
83
|
+
// Check if token is expired
|
|
84
|
+
const expiryDate = new Date(token.expiresAt)
|
|
85
|
+
if (expiryDate < new Date()) {
|
|
86
|
+
// Token is expired, remove it
|
|
87
|
+
this.removeToken(platformId)
|
|
88
|
+
resolve(null)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
resolve(token || null)
|
|
93
|
+
}
|
|
94
|
+
request.onerror = () => reject(request.error)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async getAllTokens(): Promise<ClientToken[]> {
|
|
99
|
+
const db = await this.ensureDB()
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const transaction = db.transaction([STORE_NAME], 'readonly')
|
|
102
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
103
|
+
const request = store.getAll()
|
|
104
|
+
|
|
105
|
+
request.onsuccess = () => {
|
|
106
|
+
const tokens = request.result || []
|
|
107
|
+
// Filter out expired tokens
|
|
108
|
+
const validTokens = tokens.filter(token => {
|
|
109
|
+
if (token.expiresAt) {
|
|
110
|
+
const expiryDate = new Date(token.expiresAt)
|
|
111
|
+
return expiryDate > new Date()
|
|
112
|
+
}
|
|
113
|
+
return true
|
|
114
|
+
})
|
|
115
|
+
resolve(validTokens)
|
|
116
|
+
}
|
|
117
|
+
request.onerror = () => reject(request.error)
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async removeToken(platformId: string): Promise<void> {
|
|
122
|
+
const db = await this.ensureDB()
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
125
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
126
|
+
const request = store.delete(platformId)
|
|
127
|
+
|
|
128
|
+
request.onsuccess = () => resolve()
|
|
129
|
+
request.onerror = () => reject(request.error)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async clearAll(): Promise<void> {
|
|
134
|
+
const db = await this.ensureDB()
|
|
135
|
+
return new Promise((resolve, reject) => {
|
|
136
|
+
const transaction = db.transaction([STORE_NAME], 'readwrite')
|
|
137
|
+
const store = transaction.objectStore(STORE_NAME)
|
|
138
|
+
const request = store.clear()
|
|
139
|
+
|
|
140
|
+
request.onsuccess = () => resolve()
|
|
141
|
+
request.onerror = () => reject(request.error)
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Export a singleton instance
|
|
147
|
+
export const tokenStorage = new TokenStorage()
|
|
148
|
+
|
|
149
|
+
// Helper function to check if IndexedDB is available
|
|
150
|
+
export function isIndexedDBAvailable(): boolean {
|
|
151
|
+
try {
|
|
152
|
+
return typeof window !== 'undefined' && 'indexedDB' in window
|
|
153
|
+
} catch {
|
|
154
|
+
return false
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fallback to sessionStorage if IndexedDB is not available
|
|
159
|
+
// SessionStorage is safer than localStorage for sensitive data
|
|
160
|
+
export class FallbackStorage {
|
|
161
|
+
private prefix = 'oauth_token_'
|
|
162
|
+
|
|
163
|
+
storeToken(token: ClientToken): void {
|
|
164
|
+
if (typeof window !== 'undefined') {
|
|
165
|
+
sessionStorage.setItem(this.prefix + token.id, JSON.stringify(token))
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
getToken(platformId: string): ClientToken | null {
|
|
170
|
+
if (typeof window !== 'undefined') {
|
|
171
|
+
const data = sessionStorage.getItem(this.prefix + platformId)
|
|
172
|
+
return data ? JSON.parse(data) : null
|
|
173
|
+
}
|
|
174
|
+
return null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
getAllTokens(): ClientToken[] {
|
|
178
|
+
const tokens: ClientToken[] = []
|
|
179
|
+
if (typeof window !== 'undefined') {
|
|
180
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
181
|
+
const key = sessionStorage.key(i)
|
|
182
|
+
if (key?.startsWith(this.prefix)) {
|
|
183
|
+
const data = sessionStorage.getItem(key)
|
|
184
|
+
if (data) {
|
|
185
|
+
tokens.push(JSON.parse(data))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return tokens
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
removeToken(platformId: string): void {
|
|
194
|
+
if (typeof window !== 'undefined') {
|
|
195
|
+
sessionStorage.removeItem(this.prefix + platformId)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
clearAll(): void {
|
|
200
|
+
if (typeof window !== 'undefined') {
|
|
201
|
+
const keysToRemove: string[] = []
|
|
202
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
203
|
+
const key = sessionStorage.key(i)
|
|
204
|
+
if (key?.startsWith(this.prefix)) {
|
|
205
|
+
keysToRemove.push(key)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
keysToRemove.forEach(key => sessionStorage.removeItem(key))
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const fallbackStorage = new FallbackStorage()
|
|
214
|
+
|
|
215
|
+
// Main export that automatically uses the best available storage
|
|
216
|
+
export const clientTokenStorage = isIndexedDBAvailable() ? tokenStorage : fallbackStorage
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Token storage utilities
|
|
2
|
+
// In production, these should be stored in a secure database with encryption
|
|
3
|
+
|
|
4
|
+
interface StoredToken {
|
|
5
|
+
userId: string
|
|
6
|
+
platform: string
|
|
7
|
+
accessToken: string
|
|
8
|
+
refreshToken?: string
|
|
9
|
+
expiresAt?: Date
|
|
10
|
+
scopes: string[]
|
|
11
|
+
createdAt: Date
|
|
12
|
+
updatedAt: Date
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Temporary in-memory storage for development
|
|
16
|
+
// Replace with database in production
|
|
17
|
+
const tokenStore = new Map<string, StoredToken>()
|
|
18
|
+
|
|
19
|
+
export function getTokenKey(userId: string, platform: string): string {
|
|
20
|
+
return `${userId}:${platform}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function storeToken(token: Omit<StoredToken, 'createdAt' | 'updatedAt'>): Promise<void> {
|
|
24
|
+
const key = getTokenKey(token.userId, token.platform)
|
|
25
|
+
const storedToken: StoredToken = {
|
|
26
|
+
...token,
|
|
27
|
+
createdAt: new Date(),
|
|
28
|
+
updatedAt: new Date()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
tokenStore.set(key, storedToken)
|
|
32
|
+
|
|
33
|
+
// In production:
|
|
34
|
+
// - Encrypt the tokens
|
|
35
|
+
// - Store in database
|
|
36
|
+
// - Set up automatic refresh based on expiresAt
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function getToken(userId: string, platform: string): Promise<StoredToken | null> {
|
|
40
|
+
const key = getTokenKey(userId, platform)
|
|
41
|
+
const token = tokenStore.get(key)
|
|
42
|
+
|
|
43
|
+
if (!token) return null
|
|
44
|
+
|
|
45
|
+
// Check if token is expired
|
|
46
|
+
if (token.expiresAt && new Date() > token.expiresAt) {
|
|
47
|
+
// In production, attempt to refresh the token here
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return token
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function removeToken(userId: string, platform: string): Promise<boolean> {
|
|
55
|
+
const key = getTokenKey(userId, platform)
|
|
56
|
+
return tokenStore.delete(key)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getUserTokens(userId: string): Promise<StoredToken[]> {
|
|
60
|
+
const tokens: StoredToken[] = []
|
|
61
|
+
|
|
62
|
+
for (const [key, token] of tokenStore.entries()) {
|
|
63
|
+
if (key.startsWith(`${userId}:`)) {
|
|
64
|
+
tokens.push(token)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return tokens
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Utility to encrypt tokens before storage (implement in production)
|
|
72
|
+
export function encryptToken(token: string): string {
|
|
73
|
+
// In production, use proper encryption (e.g., AES-256)
|
|
74
|
+
// For now, return as-is
|
|
75
|
+
console.warn('Token encryption not implemented - use only in development')
|
|
76
|
+
return token
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Utility to decrypt tokens after retrieval (implement in production)
|
|
80
|
+
export function decryptToken(encryptedToken: string): string {
|
|
81
|
+
// In production, use proper decryption
|
|
82
|
+
// For now, return as-is
|
|
83
|
+
console.warn('Token decryption not implemented - use only in development')
|
|
84
|
+
return encryptedToken
|
|
85
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** This is one of the lines of code that I haven't had to change, keep it like that */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
export function createContextFromHook<Props, Result>(useHook: (props: Props) => Result) {
|
|
5
|
+
|
|
6
|
+
const Context = React.createContext<Result | undefined>(undefined)
|
|
7
|
+
|
|
8
|
+
function Provider( { children, ...props}: React.PropsWithChildren<Props> ) {
|
|
9
|
+
|
|
10
|
+
const value = useHook(props as Props) as Result
|
|
11
|
+
|
|
12
|
+
return <Context.Provider value={value}>{ children }</Context.Provider>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function useContext() {
|
|
16
|
+
return React.useContext(Context) as Result
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return [Provider, useContext] as const
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './context'
|
package/lib/tools.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export type ServerTool = {
|
|
4
|
+
description: string
|
|
5
|
+
parameters: any
|
|
6
|
+
execute: (args: any) => Promise<any> | any
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
import { server } from '@/app/server'
|
|
10
|
+
import { drawAction } from '@/actions/draw'
|
|
11
|
+
import { uploadBuffer, getPublicUrl } from '@/packages/blob'
|
|
12
|
+
import crypto from 'crypto'
|
|
13
|
+
|
|
14
|
+
export const serverTools: Record<string, ServerTool> = {
|
|
15
|
+
inner_monologue: {
|
|
16
|
+
description: 'Record thoughts text associated with a character. Provide thoughts and characterId (the CHARACTER ID, not the name).',
|
|
17
|
+
parameters: z.object({
|
|
18
|
+
thoughts: z.string().describe('Thoughts as a plain string'),
|
|
19
|
+
characterId: z.string().describe('The ID of the character these thoughts belong to (not the name)')
|
|
20
|
+
}),
|
|
21
|
+
execute: async ({ thoughts, characterId }: { thoughts: string; characterId: string }) => {
|
|
22
|
+
// First try to find by id
|
|
23
|
+
try {
|
|
24
|
+
const character = await server.characters.find({ id: characterId })
|
|
25
|
+
return { thoughts, characterId, character }
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// If not found, attempt to treat the provided characterId as a name and try to lookup by name
|
|
28
|
+
try {
|
|
29
|
+
const list = await server.characters.list({ query: { name: characterId } })
|
|
30
|
+
if (Array.isArray(list) && list.length > 0) {
|
|
31
|
+
const found = list[0] as Character
|
|
32
|
+
return { thoughts, characterId: found.id, character: found }
|
|
33
|
+
}
|
|
34
|
+
} catch (_) {}
|
|
35
|
+
// fallback: return what we have
|
|
36
|
+
return { thoughts, characterId }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
generate_image: {
|
|
41
|
+
description: 'Generate an image from a prompt',
|
|
42
|
+
parameters: z.object({ prompt: z.string(), orientation: z.enum(['portrait', 'landscape', 'square']).optional() }),
|
|
43
|
+
execute: async ({ prompt, orientation = 'square' }: { prompt: string, orientation?: 'portrait' | 'landscape' | 'square' }) => {
|
|
44
|
+
// Call the server action which returns a data URL (or throws)
|
|
45
|
+
const dataUrl = await drawAction({ prompt, orientation })
|
|
46
|
+
// dataUrl expected like: data:image/png;base64,<base64data>
|
|
47
|
+
const match = /^data:(.+);base64,(.+)$/.exec(dataUrl)
|
|
48
|
+
if (!match) throw new Error('Invalid image data returned')
|
|
49
|
+
const contentType = match[1]
|
|
50
|
+
const base64 = match[2]
|
|
51
|
+
const buffer = Buffer.from(base64, 'base64')
|
|
52
|
+
|
|
53
|
+
// generate a filename
|
|
54
|
+
const filename = `generated/${Date.now()}-${crypto.randomBytes(6).toString('hex')}.png`
|
|
55
|
+
const publicUrl = await uploadBuffer(buffer, filename, contentType)
|
|
56
|
+
|
|
57
|
+
// For LLMs only return a short confirmation string — no image bytes or base64
|
|
58
|
+
return {
|
|
59
|
+
confirmation: 'image_generated',
|
|
60
|
+
prompt,
|
|
61
|
+
orientation,
|
|
62
|
+
imageUrl: publicUrl,
|
|
63
|
+
filename,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
photograph: {
|
|
68
|
+
description: 'Generate a photograph of a character. Provide characterId and lists of image-board tags for position, background, viewpoint, and action.',
|
|
69
|
+
parameters: z.object({
|
|
70
|
+
characterId: z.string(),
|
|
71
|
+
position: z.string().describe('A couple of image-board tags for the character positiion'),
|
|
72
|
+
background: z.string().describe('A couple of image-board tags for the background scene'),
|
|
73
|
+
viewpoint: z.string().describe('A couple of image-board tags for the point of view of the photograph'),
|
|
74
|
+
action: z.string().describe('A couple of image-board tags for the action taken in the scene'),
|
|
75
|
+
nsfw: z.string().describe('A list of nsfw tags if applicable to the scene'),
|
|
76
|
+
orientation: z.enum(['portrait', 'landscape', 'square']).optional()
|
|
77
|
+
}),
|
|
78
|
+
execute: async ({ characterId, position, background, viewpoint, nsfw, action, orientation = 'portrait' }:
|
|
79
|
+
{ characterId: string, position?: string, background?: string, viewpoint?: string, action?: string, nsfw?: string, orientation?: 'portrait'|'landscape'|'square' }) => {
|
|
80
|
+
// Load character and assemble tag prompt
|
|
81
|
+
const character = await server.characters.find({ id: characterId })
|
|
82
|
+
// Assume `appearance` and `personality` fields are comma-separated image-board tags
|
|
83
|
+
const appearanceTags = (character?.appearance || '').split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
84
|
+
const personalityTags = (character?.personality || '').split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
85
|
+
|
|
86
|
+
const parts = [
|
|
87
|
+
...(appearanceTags),
|
|
88
|
+
...(personalityTags),
|
|
89
|
+
...(position ? [position] : []),
|
|
90
|
+
...(background ? [background] : []),
|
|
91
|
+
...(viewpoint ? [viewpoint] : []),
|
|
92
|
+
...(action ? [action] : []),
|
|
93
|
+
...(nsfw ? [nsfw] : []),
|
|
94
|
+
].filter(Boolean)
|
|
95
|
+
|
|
96
|
+
const prompt = parts.join(', ')
|
|
97
|
+
|
|
98
|
+
// Use drawAction to generate the image
|
|
99
|
+
const publicUrl = await drawAction({ prompt, orientation })
|
|
100
|
+
const filename = `photograph/${characterId}/${Date.now()}-${crypto.randomBytes(6).toString('hex')}.png`
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
characterId,
|
|
104
|
+
promptSummary: prompt.substring(0, 500),
|
|
105
|
+
imageUrl: publicUrl,
|
|
106
|
+
filename,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
create_character: {
|
|
111
|
+
description: 'Create a new character with name, personality, appearance, and definition. Returns the created character.',
|
|
112
|
+
parameters: z.object({
|
|
113
|
+
name: z.string().describe('Character name'),
|
|
114
|
+
personality: z.string().describe('Character personality description (image-board tags only)'),
|
|
115
|
+
appearance: z.string().describe('Character appearance description (image-board tags only)'),
|
|
116
|
+
definition: z.string().describe('Full character definition and background'),
|
|
117
|
+
}),
|
|
118
|
+
execute: async ({ name, personality, appearance, definition }: Character.Writable) => {
|
|
119
|
+
// Store personality and appearance exactly as provided (no normalization)
|
|
120
|
+
const created = await server.characters.create({ data: { name, personality, appearance, definition } })
|
|
121
|
+
return created
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// Page management tools for SEO content
|
|
126
|
+
create_page: {
|
|
127
|
+
description: 'Create a complete SEO-optimized page with all sections (hero, features, content, FAQ, testimonials, CTA). Perfect for generating landing pages from simple prompts.',
|
|
128
|
+
parameters: z.object({
|
|
129
|
+
slug: z.string().describe('URL slug for the page (e.g., "organic-dog-food", "premium-yoga-classes")'),
|
|
130
|
+
title: z.string().describe('SEO page title'),
|
|
131
|
+
description: z.string().describe('SEO meta description'),
|
|
132
|
+
keywords: z.array(z.string()).describe('SEO keywords array'),
|
|
133
|
+
author: z.string().describe('Content author name'),
|
|
134
|
+
canonical: z.string().describe('Canonical URL for the page'),
|
|
135
|
+
ogImage: z.string().describe('Open Graph image URL'),
|
|
136
|
+
hero: z.object({
|
|
137
|
+
headline: z.string().describe('Main hero headline'),
|
|
138
|
+
subheadline: z.string().describe('Supporting subheadline'),
|
|
139
|
+
description: z.string().describe('Hero description text'),
|
|
140
|
+
ctaText: z.string().describe('Hero CTA button text'),
|
|
141
|
+
ctaUrl: z.string().describe('Hero CTA button URL'),
|
|
142
|
+
}).describe('Hero section content'),
|
|
143
|
+
features: z.object({
|
|
144
|
+
heading: z.string().describe('Features section heading'),
|
|
145
|
+
items: z.array(z.object({
|
|
146
|
+
title: z.string(),
|
|
147
|
+
description: z.string(),
|
|
148
|
+
})).describe('Feature items list'),
|
|
149
|
+
}).describe('Features section'),
|
|
150
|
+
content: z.string().describe('Main content in markdown format'),
|
|
151
|
+
faq: z.object({
|
|
152
|
+
heading: z.string().describe('FAQ section heading'),
|
|
153
|
+
items: z.array(z.object({
|
|
154
|
+
question: z.string(),
|
|
155
|
+
answer: z.string(),
|
|
156
|
+
})).describe('FAQ items'),
|
|
157
|
+
}).describe('FAQ section'),
|
|
158
|
+
testimonials: z.object({
|
|
159
|
+
heading: z.string().describe('Testimonials section heading'),
|
|
160
|
+
items: z.array(z.object({
|
|
161
|
+
name: z.string(),
|
|
162
|
+
role: z.string(),
|
|
163
|
+
content: z.string(),
|
|
164
|
+
rating: z.number().min(1).max(5).optional(),
|
|
165
|
+
})).describe('Customer testimonials'),
|
|
166
|
+
}).describe('Testimonials section'),
|
|
167
|
+
cta: z.object({
|
|
168
|
+
heading: z.string().describe('Final CTA heading'),
|
|
169
|
+
description: z.string().describe('CTA description'),
|
|
170
|
+
buttonText: z.string().describe('CTA button text'),
|
|
171
|
+
buttonUrl: z.string().describe('CTA button URL'),
|
|
172
|
+
}).describe('Call-to-action section'),
|
|
173
|
+
}),
|
|
174
|
+
execute: async ({
|
|
175
|
+
slug, title, description, keywords, author, canonical, ogImage,
|
|
176
|
+
hero, features, content, faq, testimonials, cta
|
|
177
|
+
}: any) => {
|
|
178
|
+
// Build the complete page data structure with new schema
|
|
179
|
+
const pageData: Page.Writable = {
|
|
180
|
+
slug,
|
|
181
|
+
title,
|
|
182
|
+
description,
|
|
183
|
+
keywords,
|
|
184
|
+
author,
|
|
185
|
+
canonical,
|
|
186
|
+
og: {
|
|
187
|
+
title,
|
|
188
|
+
description,
|
|
189
|
+
image: ogImage,
|
|
190
|
+
},
|
|
191
|
+
hero,
|
|
192
|
+
features,
|
|
193
|
+
content,
|
|
194
|
+
faq,
|
|
195
|
+
testimonials,
|
|
196
|
+
cta,
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const created = await server.pages.create({ data: pageData })
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
page: created,
|
|
203
|
+
message: `Successfully created SEO page "${title}" with slug "${slug}"`,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
find_page: {
|
|
209
|
+
description: 'Find a specific SEO page by ID. Returns the complete page with all sections.',
|
|
210
|
+
parameters: z.object({
|
|
211
|
+
id: z.string().describe('The page ID to find'),
|
|
212
|
+
}),
|
|
213
|
+
execute: async ({ id }: { id: string }) => {
|
|
214
|
+
try {
|
|
215
|
+
const page = await server.pages.find({ id })
|
|
216
|
+
return {
|
|
217
|
+
success: true,
|
|
218
|
+
page,
|
|
219
|
+
message: `Found page: ${page.title}`,
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
error: `Page with ID ${id} not found`,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
list_pages: {
|
|
231
|
+
description: 'List all SEO pages. Returns an array of pages with basic info.',
|
|
232
|
+
parameters: z.object({
|
|
233
|
+
limit: z.number().optional().describe('Maximum number of pages to return'),
|
|
234
|
+
}),
|
|
235
|
+
execute: async ({ limit }: { limit?: number }) => {
|
|
236
|
+
try {
|
|
237
|
+
const query: any = {}
|
|
238
|
+
if (limit) query.$limit = limit
|
|
239
|
+
|
|
240
|
+
const pages = await server.pages.list({ query })
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
pages: pages.map(page => ({
|
|
244
|
+
id: page.id,
|
|
245
|
+
slug: page.slug,
|
|
246
|
+
title: page.title,
|
|
247
|
+
description: page.description,
|
|
248
|
+
createdAt: page.createdAt,
|
|
249
|
+
updatedAt: page.updatedAt,
|
|
250
|
+
})),
|
|
251
|
+
count: pages.length,
|
|
252
|
+
message: `Found ${pages.length} page(s)`,
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
error: 'Failed to retrieve pages',
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
update_page: {
|
|
264
|
+
description: 'Update an existing SEO page. Provide the page ID and any fields or sections to update.',
|
|
265
|
+
parameters: z.object({
|
|
266
|
+
id: z.string().describe('The page ID to update'),
|
|
267
|
+
slug: z.string().optional().describe('New URL slug'),
|
|
268
|
+
title: z.string().optional().describe('New page title'),
|
|
269
|
+
description: z.string().optional().describe('New meta description'),
|
|
270
|
+
keywords: z.array(z.string()).optional().describe('New keywords'),
|
|
271
|
+
author: z.string().optional().describe('New content author'),
|
|
272
|
+
canonical: z.string().optional().describe('New canonical URL'),
|
|
273
|
+
ogImage: z.string().optional().describe('New Open Graph image URL'),
|
|
274
|
+
hero: z.object({
|
|
275
|
+
headline: z.string().optional(),
|
|
276
|
+
subheadline: z.string().optional(),
|
|
277
|
+
description: z.string().optional(),
|
|
278
|
+
ctaText: z.string().optional(),
|
|
279
|
+
ctaUrl: z.string().optional(),
|
|
280
|
+
}).optional().describe('Hero section updates'),
|
|
281
|
+
features: z.object({
|
|
282
|
+
heading: z.string().optional(),
|
|
283
|
+
items: z.array(z.object({
|
|
284
|
+
title: z.string(),
|
|
285
|
+
description: z.string(),
|
|
286
|
+
})).optional(),
|
|
287
|
+
}).optional().describe('Features section updates'),
|
|
288
|
+
content: z.string().optional().describe('Main content updates'),
|
|
289
|
+
faq: z.object({
|
|
290
|
+
heading: z.string().optional(),
|
|
291
|
+
items: z.array(z.object({
|
|
292
|
+
question: z.string(),
|
|
293
|
+
answer: z.string(),
|
|
294
|
+
})).optional(),
|
|
295
|
+
}).optional().describe('FAQ updates'),
|
|
296
|
+
testimonials: z.object({
|
|
297
|
+
heading: z.string().optional(),
|
|
298
|
+
items: z.array(z.object({
|
|
299
|
+
name: z.string(),
|
|
300
|
+
role: z.string(),
|
|
301
|
+
content: z.string(),
|
|
302
|
+
rating: z.number().min(1).max(5).optional(),
|
|
303
|
+
})).optional(),
|
|
304
|
+
}).optional().describe('Testimonials updates'),
|
|
305
|
+
cta: z.object({
|
|
306
|
+
heading: z.string().optional(),
|
|
307
|
+
description: z.string().optional(),
|
|
308
|
+
buttonText: z.string().optional(),
|
|
309
|
+
buttonUrl: z.string().optional(),
|
|
310
|
+
}).optional().describe('CTA updates'),
|
|
311
|
+
}),
|
|
312
|
+
execute: async ({ id, ogImage, ...updates }: any) => {
|
|
313
|
+
try {
|
|
314
|
+
// First fetch the existing page to preserve required fields
|
|
315
|
+
const existingPage = await server.pages.find({ id })
|
|
316
|
+
|
|
317
|
+
const updateData: any = {}
|
|
318
|
+
|
|
319
|
+
// Handle the Open Graph object - preserve existing values and update as needed
|
|
320
|
+
if (ogImage || updates.title || updates.description) {
|
|
321
|
+
updateData.og = {
|
|
322
|
+
title: updates.title || existingPage.og.title,
|
|
323
|
+
description: updates.description || existingPage.og.description,
|
|
324
|
+
image: ogImage || existingPage.og.image,
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Copy over any other provided updates (excluding title/description since they're handled in og)
|
|
329
|
+
Object.keys(updates).forEach(key => {
|
|
330
|
+
if (updates[key] !== undefined && key !== 'title' && key !== 'description') {
|
|
331
|
+
updateData[key] = updates[key]
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// Handle title and description separately (they go to both root and og)
|
|
336
|
+
if (updates.title) updateData.title = updates.title
|
|
337
|
+
if (updates.description) updateData.description = updates.description
|
|
338
|
+
|
|
339
|
+
const updated = await server.pages.update({ id, data: updateData })
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
page: updated,
|
|
343
|
+
message: `Successfully updated page "${updated.title}"`,
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
return {
|
|
347
|
+
success: false,
|
|
348
|
+
error: `Failed to update page with ID ${id}: ${error.message || error}`,
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
delete_page: {
|
|
355
|
+
description: 'Delete an SEO page by ID. This action cannot be undone.',
|
|
356
|
+
parameters: z.object({
|
|
357
|
+
id: z.string().describe('The page ID to delete'),
|
|
358
|
+
}),
|
|
359
|
+
execute: async ({ id }: { id: string }) => {
|
|
360
|
+
try {
|
|
361
|
+
const deleted = await server.pages.remove({ id })
|
|
362
|
+
return {
|
|
363
|
+
success: true,
|
|
364
|
+
deletedPage: deleted,
|
|
365
|
+
message: `Successfully deleted page: ${deleted.title}`,
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
error: `Failed to delete page with ID ${id}`,
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|