Compare commits

..

1 Commits

Author SHA1 Message Date
21a078ac40 feat: add CalDAV source provider
Wire CalDavSourceProvider into the backend to support CalDAV
calendar sources (e.g. iCloud) with basic auth. Config accepts
serverUrl, username, lookAheadDays, and timeZone. Credentials
(app-specific password) are stored encrypted via the existing
credential storage infrastructure.

Co-authored-by: Ona <no-reply@ona.com>
2026-04-11 15:29:29 +00:00
60 changed files with 453 additions and 2206 deletions

View File

@@ -1 +0,0 @@
bunx lint-staged

View File

@@ -8,5 +8,5 @@
"ignoreCase": true,
"newlinesBetween": true
},
"ignorePatterns": [".claude", ".ona", "drizzle", "fixtures"]
"ignorePatterns": [".claude", "fixtures"]
}

View File

@@ -1,3 +0,0 @@
{
"js/ts.experimental.useTsgo": true
}

View File

@@ -34,7 +34,7 @@
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"typescript": "^6",
"typescript": "~5.9.3",
"vite": "^7.2.4"
}
}

View File

@@ -74,24 +74,19 @@ export function SourceConfigPanel({ source, onUpdate }: SourceConfigPanelProps)
const saveMutation = useMutation({
mutationFn: async () => {
const promises: Promise<void>[] = [
replaceSource(source.id, { enabled, config: getUserConfig() }),
]
const credentialFields = getCredentialFields()
const hasCredentials = Object.values(credentialFields).some(
(v) => typeof v === "string" && v.length > 0,
)
if (hasCredentials) {
promises.push(updateProviderConfig(source.id, { credentials: credentialFields }))
}
const body: Parameters<typeof replaceSource>[1] = {
enabled,
config: getUserConfig(),
}
if (hasCredentials && source.perUserCredentials) {
body.credentials = credentialFields
}
await replaceSource(source.id, body)
// For non-per-user credentials (provider-level), still use the admin endpoint.
if (hasCredentials && !source.perUserCredentials) {
await updateProviderConfig(source.id, { credentials: credentialFields })
}
await Promise.all(promises)
},
onSuccess() {
setDirty({})

View File

@@ -23,8 +23,6 @@ export interface SourceDefinition {
name: string
description: string
alwaysEnabled?: boolean
/** When true, secret fields are stored as per-user credentials via /api/sources/:id/credentials. */
perUserCredentials?: boolean
fields: Record<string, ConfigFieldDef>
}
@@ -80,44 +78,6 @@ const sourceDefinitions: SourceDefinition[] = [
},
},
},
{
id: "aelis.caldav",
name: "CalDAV",
description: "Calendar events from any CalDAV server (Nextcloud, Radicale, Baikal, etc.).",
perUserCredentials: true,
fields: {
serverUrl: {
type: "string",
label: "Server URL",
required: true,
secret: false,
description: "CalDAV server URL (e.g. https://nextcloud.example.com/remote.php/dav)",
},
username: {
type: "string",
label: "Username",
required: true,
secret: false,
},
password: {
type: "string",
label: "Password",
required: true,
secret: true,
},
lookAheadDays: {
type: "number",
label: "Look-ahead Days",
defaultValue: 0,
description: "Number of additional days beyond today to fetch events for",
},
timeZone: {
type: "string",
label: "Timezone",
description: 'IANA timezone for determining "today" (e.g. Europe/London). Defaults to UTC.',
},
},
},
{
id: "aelis.tfl",
name: "TfL",
@@ -174,7 +134,7 @@ export async function fetchConfigs(): Promise<SourceConfig[]> {
export async function replaceSource(
sourceId: string,
body: { enabled: boolean; config: unknown; credentials?: Record<string, unknown> },
body: { enabled: boolean; config: unknown },
): Promise<void> {
const res = await fetch(`${serverBase()}/sources/${sourceId}`, {
method: "PUT",
@@ -204,22 +164,6 @@ export async function updateProviderConfig(
}
}
export async function updateSourceCredentials(
sourceId: string,
credentials: Record<string, unknown>,
): Promise<void> {
const res = await fetch(`${serverBase()}/sources/${sourceId}/credentials`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(credentials),
})
if (!res.ok) {
const data = (await res.json()) as { error?: string }
throw new Error(data.error ?? `Failed to update credentials: ${res.status}`)
}
}
export interface LocationInput {
lat: number
lng: number

View File

@@ -3,11 +3,12 @@
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM"],
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
@@ -15,12 +16,14 @@
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}

View File

@@ -2,6 +2,7 @@
"files": [],
"references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}

View File

@@ -7,12 +7,14 @@
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { Hono } from "hono"
import { describe, expect, test } from "bun:test"
import type { Auth } from "./index.ts"
import type { AuthSession, AuthUser } from "./session.ts"

View File

@@ -1,12 +1,9 @@
import type { PgDatabase } from "drizzle-orm/pg-core"
import { SQL } from "bun"
import { drizzle, type BunSQLQueryResultHKT } from "drizzle-orm/bun-sql"
import { drizzle, type BunSQLDatabase } from "drizzle-orm/bun-sql"
import * as schema from "./schema.ts"
/** Covers both the top-level drizzle instance and transaction handles. */
export type Database = PgDatabase<BunSQLQueryResultHKT, typeof schema>
export type Database = BunSQLDatabase<typeof schema>
export interface DatabaseConnection {
db: Database

View File

@@ -47,3 +47,5 @@ export function createFeedEnhancer(config: FeedEnhancerConfig): FeedEnhancer {
return mergeEnhancement(items, result, currentTime)
}
}

View File

@@ -36,7 +36,8 @@ export function buildPrompt(
for (const item of items) {
const hasUnfilledSlots =
item.slots && Object.values(item.slots).some((slot) => slot.content === null)
item.slots &&
Object.values(item.slots).some((slot) => slot.content === null)
if (hasUnfilledSlots) {
enhanceItems.push({
@@ -78,7 +79,9 @@ export function buildPrompt(
*/
export function hasUnfilledSlots(items: FeedItem[]): boolean {
return items.some(
(item) => item.slots && Object.values(item.slots).some((slot) => slot.content === null),
(item) =>
item.slots &&
Object.values(item.slots).some((slot) => slot.content === null),
)
}
@@ -126,20 +129,7 @@ function extractCalendarEntry(item: FeedItem): CalendarEntry | null {
}
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const
const MONTHS = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec",
] as const
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const
function pad2(n: number): string {
return n.toString().padStart(2, "0")
@@ -154,11 +144,7 @@ function formatDayShort(date: Date): string {
}
function formatDayLabel(date: Date, currentTime: Date): string {
const currentDay = Date.UTC(
currentTime.getUTCFullYear(),
currentTime.getUTCMonth(),
currentTime.getUTCDate(),
)
const currentDay = Date.UTC(currentTime.getUTCFullYear(), currentTime.getUTCMonth(), currentTime.getUTCDate())
const targetDay = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
const diffDays = Math.round((targetDay - currentDay) / (1000 * 60 * 60 * 24))

View File

@@ -135,7 +135,9 @@ describe("schema sync", () => {
// JSON Schema structure matches
const jsonSchema = enhancementResultJsonSchema
expect(Object.keys(jsonSchema.properties).sort()).toEqual(Object.keys(payload).sort())
expect(Object.keys(jsonSchema.properties).sort()).toEqual(
Object.keys(payload).sort(),
)
expect([...jsonSchema.required].sort()).toEqual(Object.keys(payload).sort())
// syntheticItems item schema has the right required fields
@@ -165,7 +167,11 @@ describe("schema sync", () => {
// JSON Schema only allows string or null for slot values
const slotValueSchema =
enhancementResultJsonSchema.properties.slotFills.additionalProperties.additionalProperties
expect(slotValueSchema.anyOf).toEqual([{ type: "string" }, { type: "null" }])
enhancementResultJsonSchema.properties.slotFills.additionalProperties
.additionalProperties
expect(slotValueSchema.anyOf).toEqual([
{ type: "string" },
{ type: "null" },
])
})
})

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test"
import { randomBytes } from "node:crypto"
import { describe, expect, test } from "bun:test"
import { CredentialEncryptor } from "./crypto.ts"

View File

@@ -81,27 +81,6 @@ mock.module("../sources/user-sources.ts", () => ({
updatedAt: now,
}
},
async findForUpdate(sourceId: string) {
// Delegates to find — row locking is a no-op in tests.
if (mockFindResult !== undefined) return mockFindResult
const now = new Date()
return {
id: crypto.randomUUID(),
userId,
sourceId,
enabled: true,
config: {},
credentials: null,
createdAt: now,
updatedAt: now,
}
},
async updateConfig(_sourceId: string, _update: { enabled?: boolean; config?: unknown }) {
// no-op for tests
},
async upsertConfig(_sourceId: string, _data: { enabled: boolean; config: unknown }) {
// no-op for tests
},
async updateCredentials(sourceId: string, credentials: Buffer) {
if (mockUpdateCredentialsError) {
throw mockUpdateCredentialsError
@@ -111,9 +90,7 @@ mock.module("../sources/user-sources.ts", () => ({
}),
}))
const fakeDb = {
transaction: <T>(fn: (tx: unknown) => Promise<T>) => fn(fakeDb),
} as unknown as Database
const fakeDb = {} as Database
function createStubSource(id: string, items: FeedItem[] = []): FeedSource {
return {
@@ -847,121 +824,3 @@ describe("UserSessionManager.updateSourceCredentials", () => {
expect(factory).not.toHaveBeenCalled()
})
})
describe("UserSessionManager.saveSourceConfig", () => {
test("upserts config without credentials (existing behavior)", async () => {
setEnabledSources(["test"])
const factory = mock(async () => createStubSource("test"))
const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory }
const manager = new UserSessionManager({
db: fakeDb,
providers: [provider],
credentialEncryptor: testEncryptor,
})
// Create a session first so we can verify the source is refreshed
await manager.getOrCreate("user-1")
await manager.saveSourceConfig("user-1", "test", {
enabled: true,
config: { key: "value" },
})
// feedSourceForUser called once for session creation, once for upsert refresh
expect(factory).toHaveBeenCalledTimes(2)
// No credentials should have been persisted
expect(mockUpdateCredentialsCalls).toHaveLength(0)
})
test("upserts config with credentials — persists both and passes credentials to source", async () => {
setEnabledSources(["test"])
let receivedCredentials: unknown = null
const factory = mock(async (_userId: string, _config: unknown, creds: unknown) => {
receivedCredentials = creds
return createStubSource("test")
})
const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory }
const manager = new UserSessionManager({
db: fakeDb,
providers: [provider],
credentialEncryptor: testEncryptor,
})
// Create a session so the source refresh path runs
await manager.getOrCreate("user-1")
const creds = { username: "alice", password: "s3cret" }
await manager.saveSourceConfig("user-1", "test", {
enabled: true,
config: { serverUrl: "https://example.com" },
credentials: creds,
})
// Credentials were encrypted and persisted
expect(mockUpdateCredentialsCalls).toHaveLength(1)
const decrypted = JSON.parse(testEncryptor.decrypt(mockUpdateCredentialsCalls[0]!.credentials))
expect(decrypted).toEqual(creds)
// feedSourceForUser received the provided credentials (not null)
expect(receivedCredentials).toEqual(creds)
})
test("upserts config with credentials adds source to session when not already present", async () => {
// Start with no enabled sources so the session is empty
setEnabledSources([])
const factory = mock(async () => createStubSource("test"))
const provider: FeedSourceProvider = { sourceId: "test", feedSourceForUser: factory }
const manager = new UserSessionManager({
db: fakeDb,
providers: [provider],
credentialEncryptor: testEncryptor,
})
const session = await manager.getOrCreate("user-1")
expect(session.hasSource("test")).toBe(false)
// Set mockFindResult to undefined so find() returns a row (simulating the row was just created by upsertConfig)
await manager.saveSourceConfig("user-1", "test", {
enabled: true,
config: {},
credentials: { token: "abc" },
})
// Source should now be in the session
expect(session.hasSource("test")).toBe(true)
expect(mockUpdateCredentialsCalls).toHaveLength(1)
})
test("throws CredentialStorageUnavailableError when credentials provided without encryptor", async () => {
setEnabledSources(["test"])
const provider = createStubProvider("test")
const manager = new UserSessionManager({
db: fakeDb,
providers: [provider],
// No credentialEncryptor
})
await expect(
manager.saveSourceConfig("user-1", "test", {
enabled: true,
config: {},
credentials: { token: "abc" },
}),
).rejects.toBeInstanceOf(CredentialStorageUnavailableError)
})
test("throws SourceNotFoundError for unknown provider", async () => {
const manager = new UserSessionManager({
db: fakeDb,
providers: [],
credentialEncryptor: testEncryptor,
})
await expect(
manager.saveSourceConfig("user-1", "unknown", {
enabled: true,
config: {},
}),
).rejects.toBeInstanceOf(SourceNotFoundError)
})
})

View File

@@ -126,29 +126,27 @@ export class UserSessionManager {
return
}
// Use a transaction with SELECT FOR UPDATE to prevent lost updates
// when concurrent PATCH requests merge config against the same base.
const { existingRow, mergedConfig } = await this.db.transaction(async (tx) => {
const existingRow = await sources(tx, userId).findForUpdate(sourceId)
// Fetch the existing row for config merging and credential access.
// NOTE: find + updateConfig is not atomic. A concurrent update could
// read stale config. Use SELECT FOR UPDATE or atomic jsonb merge if
// this becomes a problem.
const existingRow = await sources(this.db, userId).find(sourceId)
let mergedConfig: Record<string, unknown> | undefined
if (update.config !== undefined && provider.configSchema) {
const existingConfig = (existingRow?.config ?? {}) as Record<string, unknown>
mergedConfig = merge({}, existingConfig, update.config)
let mergedConfig: Record<string, unknown> | undefined
if (update.config !== undefined && provider.configSchema) {
const existingConfig = (existingRow?.config ?? {}) as Record<string, unknown>
mergedConfig = merge({}, existingConfig, update.config)
const validated = provider.configSchema(mergedConfig)
if (validated instanceof type.errors) {
throw new InvalidSourceConfigError(sourceId, validated.summary)
}
const validated = provider.configSchema(mergedConfig)
if (validated instanceof type.errors) {
throw new InvalidSourceConfigError(sourceId, validated.summary)
}
}
// Throws SourceNotFoundError if the row doesn't exist
await sources(tx, userId).updateConfig(sourceId, {
enabled: update.enabled,
config: mergedConfig,
})
return { existingRow, mergedConfig }
// Throws SourceNotFoundError if the row doesn't exist
await sources(this.db, userId).updateConfig(sourceId, {
enabled: update.enabled,
config: mergedConfig,
})
// Refresh the specific source in the active session instead of
@@ -173,18 +171,13 @@ export class UserSessionManager {
* inserts a new row if one doesn't exist and fully replaces config
* (no merge).
*
* When `credentials` is provided, they are encrypted and persisted
* alongside the config in the same flow, avoiding the race condition
* of separate config + credential requests.
*
* @throws {SourceNotFoundError} if the sourceId has no registered provider
* @throws {InvalidSourceConfigError} if config fails schema validation
* @throws {CredentialStorageUnavailableError} if credentials are provided but no encryptor is configured
*/
async saveSourceConfig(
async upsertSourceConfig(
userId: string,
sourceId: string,
data: { enabled: boolean; config?: unknown; credentials?: unknown },
data: { enabled: boolean; config?: unknown },
): Promise<void> {
const provider = this.providers.get(sourceId)
if (!provider) {
@@ -198,28 +191,15 @@ export class UserSessionManager {
}
}
if (data.credentials !== undefined && !this.encryptor) {
throw new CredentialStorageUnavailableError()
}
const config = data.config ?? {}
// Run the upsert + credential update atomically so a failure in
// either step doesn't leave the row in an inconsistent state.
const existingRow = await this.db.transaction(async (tx) => {
const existing = await sources(tx, userId).find(sourceId)
// Fetch existing row before upsert to capture credentials for session refresh.
// For new rows this will be undefined — credentials will be null.
const existingRow = await sources(this.db, userId).find(sourceId)
await sources(tx, userId).upsertConfig(sourceId, {
enabled: data.enabled,
config,
})
if (data.credentials !== undefined && this.encryptor) {
const encrypted = this.encryptor.encrypt(JSON.stringify(data.credentials))
await sources(tx, userId).updateCredentials(sourceId, encrypted)
}
return existing
await sources(this.db, userId).upsertConfig(sourceId, {
enabled: data.enabled,
config,
})
const session = this.sessions.get(userId)
@@ -227,13 +207,9 @@ export class UserSessionManager {
if (!data.enabled) {
session.removeSource(sourceId)
} else {
// Prefer the just-provided credentials over what was in the DB.
let credentials: unknown = null
if (data.credentials !== undefined) {
credentials = data.credentials
} else if (existingRow?.credentials) {
credentials = this.decryptCredentials(existingRow.credentials)
}
const credentials = existingRow?.credentials
? this.decryptCredentials(existingRow.credentials)
: null
const source = await provider.feedSourceForUser(userId, config, credentials)
if (session.hasSource(sourceId)) {
session.replaceSource(sourceId, source)

View File

@@ -80,9 +80,6 @@ function createInMemoryStore() {
async find(sourceId: string) {
return rows.get(key(userId, sourceId))
},
async findForUpdate(sourceId: string) {
return rows.get(key(userId, sourceId))
},
async updateConfig(sourceId: string, update: { enabled?: boolean; config?: unknown }) {
const existing = rows.get(key(userId, sourceId))
if (!existing) {
@@ -128,9 +125,7 @@ mock.module("../sources/user-sources.ts", () => ({
},
}))
const fakeDb = {
transaction: <T>(fn: (tx: unknown) => Promise<T>) => fn(fakeDb),
} as unknown as Database
const fakeDb = {} as Database
function createApp(providers: FeedSourceProvider[], userId?: string) {
const sessionManager = new UserSessionManager({ providers, db: fakeDb })
@@ -743,42 +738,6 @@ describe("PUT /api/sources/:sourceId", () => {
expect(res.status).toBe(204)
})
test("returns 204 when credentials are included alongside config", async () => {
activeStore = createInMemoryStore()
const { app } = createAppWithEncryptor(
[createStubProvider("aelis.weather", weatherConfig)],
MOCK_USER_ID,
)
const res = await put(app, "aelis.weather", {
enabled: true,
config: { units: "metric" },
credentials: { apiKey: "secret123" },
})
expect(res.status).toBe(204)
const row = activeStore.rows.get(`${MOCK_USER_ID}:aelis.weather`)
expect(row).toBeDefined()
expect(row!.enabled).toBe(true)
expect(row!.config).toEqual({ units: "metric" })
})
test("returns 503 when credentials are provided but no encryptor is configured", async () => {
activeStore = createInMemoryStore()
// createApp does NOT configure an encryptor
const { app } = createApp([createStubProvider("aelis.weather", weatherConfig)], MOCK_USER_ID)
const res = await put(app, "aelis.weather", {
enabled: true,
config: { units: "metric" },
credentials: { apiKey: "secret123" },
})
expect(res.status).toBe(503)
const body = (await res.json()) as { error: string }
expect(body.error).toContain("not configured")
})
})
describe("PUT /api/sources/:sourceId/credentials", () => {

View File

@@ -34,13 +34,11 @@ const ReplaceSourceConfigRequestBody = type({
"+": "reject",
enabled: "boolean",
config: "unknown",
"credentials?": "unknown",
})
const ReplaceSourceConfigNoConfigRequestBody = type({
"+": "reject",
enabled: "boolean",
"credentials?": "unknown",
})
export function registerSourcesHttpHandlers(
@@ -163,15 +161,14 @@ async function handleReplaceSource(c: Context<Env>) {
return c.json({ error: parsed.summary }, 400)
}
const { enabled, credentials } = parsed
const { enabled } = parsed
const config = "config" in parsed ? parsed.config : undefined
const user = c.get("user")!
try {
await sessionManager.saveSourceConfig(user.id, sourceId, {
await sessionManager.upsertSourceConfig(user.id, sourceId, {
enabled,
config,
credentials,
})
} catch (err) {
if (err instanceof SourceNotFoundError) {
@@ -180,9 +177,6 @@ async function handleReplaceSource(c: Context<Env>) {
if (err instanceof InvalidSourceConfigError) {
return c.json({ error: err.message }, 400)
}
if (err instanceof CredentialStorageUnavailableError) {
return c.json({ error: err.message }, 503)
}
throw err
}

View File

@@ -26,18 +26,6 @@ export function sources(db: Database, userId: string) {
return rows[0]
},
/** Like find(), but acquires a row lock to prevent concurrent modifications. Must be called inside a transaction. */
async findForUpdate(sourceId: string) {
const rows = await db
.select()
.from(userSources)
.where(and(eq(userSources.userId, userId), eq(userSources.sourceId, sourceId)))
.limit(1)
.for("update")
return rows[0]
},
/** Enables a source for the user. Throws if the source row doesn't exist. */
async enableSource(sourceId: string) {
const rows = await db

View File

@@ -55,112 +55,44 @@
"fontFamily": "Inter",
"fontDefinitions": [
{ "path": "./assets/fonts/Inter_100Thin.ttf", "weight": 100 },
{
"path": "./assets/fonts/Inter_100Thin_Italic.ttf",
"weight": 100,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_100Thin_Italic.ttf", "weight": 100, "style": "italic" },
{ "path": "./assets/fonts/Inter_200ExtraLight.ttf", "weight": 200 },
{
"path": "./assets/fonts/Inter_200ExtraLight_Italic.ttf",
"weight": 200,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_200ExtraLight_Italic.ttf", "weight": 200, "style": "italic" },
{ "path": "./assets/fonts/Inter_300Light.ttf", "weight": 300 },
{
"path": "./assets/fonts/Inter_300Light_Italic.ttf",
"weight": 300,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_300Light_Italic.ttf", "weight": 300, "style": "italic" },
{ "path": "./assets/fonts/Inter_400Regular.ttf", "weight": 400 },
{
"path": "./assets/fonts/Inter_400Regular_Italic.ttf",
"weight": 400,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_400Regular_Italic.ttf", "weight": 400, "style": "italic" },
{ "path": "./assets/fonts/Inter_500Medium.ttf", "weight": 500 },
{
"path": "./assets/fonts/Inter_500Medium_Italic.ttf",
"weight": 500,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_500Medium_Italic.ttf", "weight": 500, "style": "italic" },
{ "path": "./assets/fonts/Inter_600SemiBold.ttf", "weight": 600 },
{
"path": "./assets/fonts/Inter_600SemiBold_Italic.ttf",
"weight": 600,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_600SemiBold_Italic.ttf", "weight": 600, "style": "italic" },
{ "path": "./assets/fonts/Inter_700Bold.ttf", "weight": 700 },
{
"path": "./assets/fonts/Inter_700Bold_Italic.ttf",
"weight": 700,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_700Bold_Italic.ttf", "weight": 700, "style": "italic" },
{ "path": "./assets/fonts/Inter_800ExtraBold.ttf", "weight": 800 },
{
"path": "./assets/fonts/Inter_800ExtraBold_Italic.ttf",
"weight": 800,
"style": "italic"
},
{ "path": "./assets/fonts/Inter_800ExtraBold_Italic.ttf", "weight": 800, "style": "italic" },
{ "path": "./assets/fonts/Inter_900Black.ttf", "weight": 900 },
{
"path": "./assets/fonts/Inter_900Black_Italic.ttf",
"weight": 900,
"style": "italic"
}
{ "path": "./assets/fonts/Inter_900Black_Italic.ttf", "weight": 900, "style": "italic" }
]
},
{
"fontFamily": "Source Serif 4",
"fontDefinitions": [
{ "path": "./assets/fonts/SourceSerif4_200ExtraLight.ttf", "weight": 200 },
{
"path": "./assets/fonts/SourceSerif4_200ExtraLight_Italic.ttf",
"weight": 200,
"style": "italic"
},
{ "path": "./assets/fonts/SourceSerif4_200ExtraLight_Italic.ttf", "weight": 200, "style": "italic" },
{ "path": "./assets/fonts/SourceSerif4_300Light.ttf", "weight": 300 },
{
"path": "./assets/fonts/SourceSerif4_300Light_Italic.ttf",
"weight": 300,
"style": "italic"
},
{ "path": "./assets/fonts/SourceSerif4_300Light_Italic.ttf", "weight": 300, "style": "italic" },
{ "path": "./assets/fonts/SourceSerif4_400Regular.ttf", "weight": 400 },
{
"path": "./assets/fonts/SourceSerif4_400Regular_Italic.ttf",
"weight": 400,
"style": "italic"
},
{ "path": "./assets/fonts/SourceSerif4_400Regular_Italic.ttf", "weight": 400, "style": "italic" },
{ "path": "./assets/fonts/SourceSerif4_500Medium.ttf", "weight": 500 },
{
"path": "./assets/fonts/SourceSerif4_500Medium_Italic.ttf",
"weight": 500,
"style": "italic"
},
{ "path": "./assets/fonts/SourceSerif4_500Medium_Italic.ttf", "weight": 500, "style": "italic" },
{ "path": "./assets/fonts/SourceSerif4_600SemiBold.ttf", "weight": 600 },
{
"path": "./assets/fonts/SourceSerif4_600SemiBold_Italic.ttf",
"weight": 600,
"style": "italic"
},
{ "path": "./assets/fonts/SourceSerif4_600SemiBold_Italic.ttf", "weight": 600, "style": "italic" },
{ "path": "./assets/fonts/SourceSerif4_700Bold.ttf", "weight": 700 },
{
"path": "./assets/fonts/SourceSerif4_700Bold_Italic.ttf",
"weight": 700,
"style": "italic"
},
{ "path": "./assets/fonts/SourceSerif4_700Bold_Italic.ttf", "weight": 700, "style": "italic" },
{ "path": "./assets/fonts/SourceSerif4_800ExtraBold.ttf", "weight": 800 },
{
"path": "./assets/fonts/SourceSerif4_800ExtraBold_Italic.ttf",
"weight": 800,
"style": "italic"
},
{ "path": "./assets/fonts/SourceSerif4_800ExtraBold_Italic.ttf", "weight": 800, "style": "italic" },
{ "path": "./assets/fonts/SourceSerif4_900Black.ttf", "weight": 900 },
{
"path": "./assets/fonts/SourceSerif4_900Black_Italic.ttf",
"weight": 900,
"style": "italic"
}
{ "path": "./assets/fonts/SourceSerif4_900Black_Italic.ttf", "weight": 900, "style": "italic" }
]
}
]

View File

@@ -55,6 +55,6 @@
"eas-cli": "^18.0.1",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "^6"
"typescript": "~5.9.2"
}
}

View File

@@ -27,8 +27,8 @@ export class ApiClient {
(prevInit, middleware) => middleware(url, prevInit),
init,
)
return fetch(this.baseUrl ? new URL(url.toString(), this.baseUrl) : url, finalInit).then(
(res) => Promise.all([Promise.resolve(res), res.json()]),
return fetch(this.baseUrl ? new URL(url.toString(), this.baseUrl) : url, finalInit).then((res) =>
Promise.all([Promise.resolve(res), res.json()]),
)
}
}

View File

@@ -3,13 +3,13 @@ import { useEffect } from "react"
import { ScrollView, View } from "react-native"
import tw from "twrnc"
import { type Showcase } from "@/components/showcase"
import { buttonShowcase } from "@/components/ui/button.showcase"
import { feedCardShowcase } from "@/components/ui/feed-card.showcase"
import { monospaceTextShowcase } from "@/components/ui/monospace-text.showcase"
import { SansSerifText } from "@/components/ui/sans-serif-text"
import { sansSerifTextShowcase } from "@/components/ui/sans-serif-text.showcase"
import { serifTextShowcase } from "@/components/ui/serif-text.showcase"
import { type Showcase } from "@/components/showcase"
import { SansSerifText } from "@/components/ui/sans-serif-text"
const showcases: Record<string, Showcase> = {
button: buttonShowcase,
@@ -41,10 +41,7 @@ export default function ComponentDetailScreen() {
const ShowcaseComponent = showcase.component
return (
<ScrollView
style={tw`bg-stone-100 dark:bg-stone-900 flex-1`}
contentContainerStyle={tw`px-5 pb-10 pt-4 gap-6`}
>
<ScrollView style={tw`bg-stone-100 dark:bg-stone-900 flex-1`} contentContainerStyle={tw`px-5 pb-10 pt-4 gap-6`}>
<ShowcaseComponent />
</ScrollView>
)

View File

@@ -15,9 +15,7 @@ const components = [
export default function ComponentsScreen() {
return (
<View style={tw`flex-1`}>
<View
style={tw`mx-4 mt-4 rounded-xl border border-stone-200 dark:border-stone-800 overflow-hidden`}
>
<View style={tw`mx-4 mt-4 rounded-xl border border-stone-200 dark:border-stone-800 overflow-hidden`}>
<FlatList
data={components}
keyExtractor={(item) => item.name}

View File

@@ -1,8 +1,8 @@
import { View } from "react-native"
import tw from "twrnc"
import { type Showcase, Section } from "../showcase"
import { Button } from "./button"
import { type Showcase, Section } from "../showcase"
function ButtonShowcase() {
return (
@@ -11,7 +11,11 @@ function ButtonShowcase() {
<Button style={tw`self-start`} label="Press me" />
</Section>
<Section title="Leading icon">
<Button style={tw`self-start`} label="Add item" leadingIcon={<Button.Icon name="plus" />} />
<Button
style={tw`self-start`}
label="Add item"
leadingIcon={<Button.Icon name="plus" />}
/>
</Section>
<Section title="Trailing icon">
<Button

View File

@@ -23,11 +23,7 @@ type ButtonProps = Omit<PressableProps, "children"> & {
export function Button({ style, label, leadingIcon, trailingIcon, ...props }: ButtonProps) {
const hasIcons = leadingIcon != null || trailingIcon != null
const textElement = (
<SansSerifText style={tw`text-stone-100 dark:text-stone-200 font-medium`}>
{label}
</SansSerifText>
)
const textElement = <SansSerifText style={tw`text-stone-100 dark:text-stone-200 font-medium`}>{label}</SansSerifText>
return (
<Pressable style={[tw`rounded-full bg-teal-600 px-4 py-3 w-fit`, style]} {...props}>

View File

@@ -1,11 +1,11 @@
import { View } from "react-native"
import tw from "twrnc"
import { type Showcase, Section } from "../showcase"
import { Button } from "./button"
import { FeedCard } from "./feed-card"
import { SansSerifText } from "./sans-serif-text"
import { SerifText } from "./serif-text"
import { type Showcase, Section } from "../showcase"
function FeedCardShowcase() {
return (

View File

@@ -2,10 +2,5 @@ import { View, type ViewProps } from "react-native"
import tw from "twrnc"
export function FeedCard({ style, ...props }: ViewProps) {
return (
<View
style={[tw`border border-stone-200 dark:border-stone-800 rounded-lg`, style]}
{...props}
/>
)
return <View style={[tw`border border-stone-200 dark:border-stone-800 rounded-lg`, style]} {...props} />
}

View File

@@ -1,8 +1,8 @@
import { View } from "react-native"
import tw from "twrnc"
import { type Showcase, Section } from "../showcase"
import { MonospaceText } from "./monospace-text"
import { type Showcase, Section } from "../showcase"
function MonospaceTextShowcase() {
return (

View File

@@ -3,10 +3,7 @@ import tw from "twrnc"
export function MonospaceText({ children, style, ...props }: TextProps) {
return (
<Text
style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Menlo" }, style]}
{...props}
>
<Text style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Menlo" }, style]} {...props}>
{children}
</Text>
)

View File

@@ -1,8 +1,8 @@
import { View } from "react-native"
import tw from "twrnc"
import { type Showcase, Section } from "../showcase"
import { SansSerifText } from "./sans-serif-text"
import { type Showcase, Section } from "../showcase"
function SansSerifTextShowcase() {
return (

View File

@@ -3,10 +3,7 @@ import tw from "twrnc"
export function SansSerifText({ children, style, ...props }: TextProps) {
return (
<Text
style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Inter" }, style]}
{...props}
>
<Text style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Inter" }, style]} {...props}>
{children}
</Text>
)

View File

@@ -1,8 +1,8 @@
import { View } from "react-native"
import tw from "twrnc"
import { type Showcase, Section } from "../showcase"
import { SerifText } from "./serif-text"
import { type Showcase, Section } from "../showcase"
function SerifTextShowcase() {
return (

View File

@@ -3,10 +3,7 @@ import tw from "twrnc"
export function SerifText({ children, style, ...props }: TextProps) {
return (
<Text
style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Source Serif 4" }, style]}
{...props}
>
<Text style={[tw`text-stone-800 dark:text-stone-200`, { fontFamily: "Source Serif 4" }, style]} {...props}>
{children}
</Text>
)

View File

@@ -30,8 +30,7 @@ export const catalog = defineCatalog(schema, {
style: z.string().nullable(),
}),
slots: ["default"],
description:
"Bordered card container for feed content. The style prop accepts a twrnc class string.",
description: "Bordered card container for feed content. The style prop accepts a twrnc class string.",
example: { style: "p-4 gap-2" },
},
SansSerifText: {

View File

@@ -14,20 +14,12 @@ type ButtonIconName = React.ComponentProps<typeof Button.Icon>["name"]
export const { registry } = defineRegistry(catalog, {
components: {
View: ({ props, children }) => (
<View style={props.style ? tw`${props.style}` : undefined}>{children}</View>
),
View: ({ props, children }) => <View style={props.style ? tw`${props.style}` : undefined}>{children}</View>,
Button: ({ props, emit }) => (
<Button
label={props.label}
leadingIcon={
props.leadingIcon ? <Button.Icon name={props.leadingIcon as ButtonIconName} /> : undefined
}
trailingIcon={
props.trailingIcon ? (
<Button.Icon name={props.trailingIcon as ButtonIconName} />
) : undefined
}
leadingIcon={props.leadingIcon ? <Button.Icon name={props.leadingIcon as ButtonIconName} /> : undefined}
trailingIcon={props.trailingIcon ? <Button.Icon name={props.trailingIcon as ButtonIconName} /> : undefined}
onPress={() => emit("press")}
/>
),
@@ -35,17 +27,13 @@ export const { registry } = defineRegistry(catalog, {
<FeedCard style={props.style ? tw`${props.style}` : undefined}>{children}</FeedCard>
),
SansSerifText: ({ props }) => (
<SansSerifText style={props.style ? tw`${props.style}` : undefined}>
{props.text}
</SansSerifText>
<SansSerifText style={props.style ? tw`${props.style}` : undefined}>{props.text}</SansSerifText>
),
SerifText: ({ props }) => (
<SerifText style={props.style ? tw`${props.style}` : undefined}>{props.text}</SerifText>
),
MonospaceText: ({ props }) => (
<MonospaceText style={props.style ? tw`${props.style}` : undefined}>
{props.text}
</MonospaceText>
<MonospaceText style={props.style ? tw`${props.style}` : undefined}>{props.text}</MonospaceText>
),
},
})

View File

@@ -1,131 +1 @@
{
"fr": 60,
"h": 400,
"ip": 0,
"layers": [
{
"ind": 3,
"ty": 4,
"parent": 2,
"ks": {},
"ip": 0,
"op": 7,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 53] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 122] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
{
"ty": "st",
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 2,
"ty": 3,
"parent": 1,
"ks": {
"a": { "a": 0, "k": [160, 53] },
"p": { "a": 0, "k": [200.5, 200] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 6, "s": [-10], "h": 1 }
]
}
},
"ip": 0,
"op": 7,
"st": 0
},
{
"ind": 5,
"ty": 4,
"parent": 4,
"ks": {},
"ip": 0,
"op": 7,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 53] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 122] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
{
"ty": "st",
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 4,
"ty": 3,
"parent": 1,
"ks": {
"a": { "a": 0, "k": [160, 53] },
"p": { "a": 0, "k": [200.594, 200.176] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 6, "s": [10], "h": 1 }
]
}
},
"ip": 0,
"op": 7,
"st": 0
},
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 7, "st": 0 },
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 7, "st": 0 }
],
"meta": { "g": "https://jitter.video" },
"op": 6,
"v": "5.7.4",
"w": 400
}
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[-10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":7,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":7,"st":0}],"meta":{"g":"https://jitter.video"},"op":6,"v":"5.7.4","w":400}

View File

@@ -1,131 +1 @@
{
"fr": 60,
"h": 400,
"ip": 0,
"layers": [
{
"ind": 3,
"ty": 4,
"parent": 2,
"ks": {},
"ip": 0,
"op": 7,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 53] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 122] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
{
"ty": "st",
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 2,
"ty": 3,
"parent": 1,
"ks": {
"a": { "a": 0, "k": [160, 53] },
"p": { "a": 0, "k": [200.5, 200] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 6, "s": [-10], "h": 1 }
]
}
},
"ip": 0,
"op": 7,
"st": 0
},
{
"ind": 5,
"ty": 4,
"parent": 4,
"ks": {},
"ip": 0,
"op": 7,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 53] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 122] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{ "ty": "el", "p": { "a": 0, "k": [160, 53] }, "s": { "a": 0, "k": [320, 106] } },
{
"ty": "st",
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 4,
"ty": 3,
"parent": 1,
"ks": {
"a": { "a": 0, "k": [160, 53] },
"p": { "a": 0, "k": [200.594, 200.176] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [30], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 6, "s": [10], "h": 1 }
]
}
},
"ip": 0,
"op": 7,
"st": 0
},
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 7, "st": 0 },
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 7, "st": 0 }
],
"meta": { "g": "https://jitter.video" },
"op": 6,
"v": "5.7.4",
"w": 400
}
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[-10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":7,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,53]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,122]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":0,"k":[160,53]},"s":{"a":0,"k":[320,106]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":0,"k":[160,53]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":6,"s":[10],"h":1}]}},"ip":0,"op":7,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":7,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":7,"st":0}],"meta":{"g":"https://jitter.video"},"op":6,"v":"5.7.4","w":400}

View File

@@ -1,281 +1 @@
{
"fr": 60,
"h": 400,
"ip": 0,
"layers": [
{
"ind": 3,
"ty": 4,
"parent": 2,
"ks": {},
"ip": 0,
"op": 61,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 8.4,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 37.8,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 1],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 8.4,
"s": [320, 1],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [320, 106],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 37.8,
"s": [320, 106],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 2,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 8.4,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 37.8,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200, 200.014] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-90], "h": 1 },
{ "t": 8.4, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 30, "s": [0], "h": 1 },
{ "t": 37.8, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 60, "s": [90], "h": 1 }
]
}
},
"ip": 0,
"op": 61,
"st": 0
},
{
"ind": 5,
"ty": 4,
"parent": 4,
"ks": {},
"ip": 0,
"op": 61,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 1],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [320, 106],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 4,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200.094, 200.19] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 30, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 60, "s": [90], "h": 1 }
]
}
},
"ip": 0,
"op": 61,
"st": 0
},
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 61, "st": 0 },
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 61, "st": 0 }
],
"meta": { "g": "https://jitter.video" },
"op": 60,
"v": "5.7.4",
"w": 400
}
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200,200.014]},"r":{"a":1,"k":[{"t":0,"s":[-90],"h":1},{"t":8.4,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"h":1},{"t":37.8,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.094,200.19]},"r":{"a":1,"k":[{"t":0,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":61,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":61,"st":0}],"meta":{"g":"https://jitter.video"},"op":60,"v":"5.7.4","w":400}

View File

@@ -1,281 +1 @@
{
"fr": 60,
"h": 400,
"ip": 0,
"layers": [
{
"ind": 3,
"ty": 4,
"parent": 2,
"ks": {},
"ip": 0,
"op": 61,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 8.4,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 37.8,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 1],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 8.4,
"s": [320, 1],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [320, 106],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 37.8,
"s": [320, 106],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 2,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 8.4,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 37.8,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200, 200.014] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-90], "h": 1 },
{ "t": 8.4, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 30, "s": [0], "h": 1 },
{ "t": 37.8, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 60, "s": [90], "h": 1 }
]
}
},
"ip": 0,
"op": 61,
"st": 0
},
{
"ind": 5,
"ty": 4,
"parent": 4,
"ks": {},
"ip": 0,
"op": 61,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 1],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [320, 106],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 4,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 0.5],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{
"t": 30,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 1] },
"o": { "x": [0, 0.5], "y": [0, 0] }
},
{ "t": 60, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200.094, 200.19] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-90], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 30, "s": [0], "i": { "x": 0, "y": 1 }, "o": { "x": 0.5, "y": 0 } },
{ "t": 60, "s": [90], "h": 1 }
]
}
},
"ip": 0,
"op": 61,
"st": 0
},
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 61, "st": 0 },
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 61, "st": 0 }
],
"meta": { "g": "https://jitter.video" },
"op": 60,
"v": "5.7.4",
"w": 400
}
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":8.4,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":37.8,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200,200.014]},"r":{"a":1,"k":[{"t":0,"s":[-90],"h":1},{"t":8.4,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"h":1},{"t":37.8,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":61,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,1],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[320,106],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,0.5],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":30,"s":[160,53],"i":{"x":[1,0],"y":[1,1]},"o":{"x":[0,0.5],"y":[0,0]}},{"t":60,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.094,200.19]},"r":{"a":1,"k":[{"t":0,"s":[-90],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":30,"s":[0],"i":{"x":0,"y":1},"o":{"x":0.5,"y":0}},{"t":60,"s":[90],"h":1}]}},"ip":0,"op":61,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":61,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":61,"st":0}],"meta":{"g":"https://jitter.video"},"op":60,"v":"5.7.4","w":400}

View File

@@ -1,224 +1 @@
{
"fr": 60,
"h": 400,
"ip": 0,
"layers": [
{
"ind": 3,
"ty": 4,
"parent": 2,
"ks": {},
"ip": 0,
"op": 31,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 5.28,
"s": [160, 53],
"i": { "x": [1, 0.001], "y": [1, 0.998] },
"o": { "x": [0, 0.349], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 106],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 5.28,
"s": [320, 106],
"i": { "x": [1, 0.001], "y": [1, 0.998] },
"o": { "x": [0, 0.349], "y": [0, 0] }
},
{ "t": 30, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 2,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 5.28,
"s": [160, 53],
"i": { "x": [1, 0.001], "y": [1, 0.998] },
"o": { "x": [0, 0.349], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200.5, 200] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-30], "h": 1 },
{ "t": 5.28, "s": [-30], "i": { "x": 0.001, "y": 0.998 }, "o": { "x": 0.349, "y": 0 } },
{ "t": 30, "s": [-90], "h": 1 }
]
}
},
"ip": 0,
"op": 31,
"st": 0
},
{
"ind": 5,
"ty": 4,
"parent": 4,
"ks": {},
"ip": 0,
"op": 31,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 0.999] },
"o": { "x": [0, 0.348], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 106],
"i": { "x": [1, 0], "y": [1, 0.999] },
"o": { "x": [0, 0.348], "y": [0, 0] }
},
{ "t": 30, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.906, 0.898, 0.894] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 4,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 0.999] },
"o": { "x": [0, 0.348], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200.594, 200.176] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [30], "i": { "x": 0, "y": 0.999 }, "o": { "x": 0.348, "y": 0 } },
{ "t": 30, "s": [90], "h": 1 }
]
}
},
"ip": 0,
"op": 31,
"st": 0
},
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 31, "st": 0 },
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 31, "st": 0 }
],
"meta": { "g": "https://jitter.video" },
"op": 30,
"v": "5.7.4",
"w": 400
}
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[320,106],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"h":1},{"t":5.28,"s":[-30],"i":{"x":0.001,"y":0.998},"o":{"x":0.349,"y":0}},{"t":30,"s":[-90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.906,0.898,0.894]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":0.999},"o":{"x":0.348,"y":0}},{"t":30,"s":[90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":31,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":31,"st":0}],"meta":{"g":"https://jitter.video"},"op":30,"v":"5.7.4","w":400}

View File

@@ -1,224 +1 @@
{
"fr": 60,
"h": 400,
"ip": 0,
"layers": [
{
"ind": 3,
"ty": 4,
"parent": 2,
"ks": {},
"ip": 0,
"op": 31,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 5.28,
"s": [160, 53],
"i": { "x": [1, 0.001], "y": [1, 0.998] },
"o": { "x": [0, 0.349], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 106],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 5.28,
"s": [320, 106],
"i": { "x": [1, 0.001], "y": [1, 0.998] },
"o": { "x": [0, 0.349], "y": [0, 0] }
},
{ "t": 30, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 2,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 1], "y": [1, 1] },
"o": { "x": [0, 0], "y": [0, 0] }
},
{
"t": 5.28,
"s": [160, 53],
"i": { "x": [1, 0.001], "y": [1, 0.998] },
"o": { "x": [0, 0.349], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200.5, 200] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [-30], "h": 1 },
{ "t": 5.28, "s": [-30], "i": { "x": 0.001, "y": 0.998 }, "o": { "x": 0.349, "y": 0 } },
{ "t": 30, "s": [-90], "h": 1 }
]
}
},
"ip": 0,
"op": 31,
"st": 0
},
{
"ind": 5,
"ty": 4,
"parent": 4,
"ks": {},
"ip": 0,
"op": 31,
"st": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ty": "rc",
"p": { "a": 0, "k": [160, 26.75] },
"r": { "a": 0, "k": 0 },
"s": { "a": 0, "k": [336, 174.5] }
},
{ "ty": "fl", "c": { "a": 0, "k": [0, 0, 0, 0] }, "o": { "a": 0, "k": 0 } },
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
},
{
"ty": "gr",
"it": [
{
"ty": "el",
"p": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 0.999] },
"o": { "x": [0, 0.348], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"s": {
"a": 1,
"k": [
{
"t": 0,
"s": [320, 106],
"i": { "x": [1, 0], "y": [1, 0.999] },
"o": { "x": [0, 0.348], "y": [0, 0] }
},
{ "t": 30, "s": [320, 1], "h": 1 }
]
}
},
{
"ty": "st",
"c": { "a": 0, "k": [0.11, 0.098, 0.09] },
"lc": 1,
"lj": 1,
"ml": 10,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 16 }
},
{ "ty": "tr", "o": { "a": 0, "k": 100 } }
]
}
]
},
{
"ind": 4,
"ty": 3,
"parent": 1,
"ks": {
"a": {
"a": 1,
"k": [
{
"t": 0,
"s": [160, 53],
"i": { "x": [1, 0], "y": [1, 0.999] },
"o": { "x": [0, 0.348], "y": [0, 0] }
},
{ "t": 30, "s": [160, 0.5], "h": 1 }
]
},
"p": { "a": 0, "k": [200.594, 200.176] },
"r": {
"a": 1,
"k": [
{ "t": 0, "s": [30], "i": { "x": 0, "y": 0.999 }, "o": { "x": 0.348, "y": 0 } },
{ "t": 30, "s": [90], "h": 1 }
]
}
},
"ip": 0,
"op": 31,
"st": 0
},
{ "ind": 1, "ty": 3, "parent": 0, "ks": {}, "ip": 0, "op": 31, "st": 0 },
{ "ind": 0, "ty": 3, "ks": {}, "ip": 0, "op": 31, "st": 0 }
],
"meta": { "g": "https://jitter.video" },
"op": 30,
"v": "5.7.4",
"w": 400
}
{"fr":60,"h":400,"ip":0,"layers":[{"ind":3,"ty":4,"parent":2,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[320,106],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":2,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,1],"y":[1,1]},"o":{"x":[0,0],"y":[0,0]}},{"t":5.28,"s":[160,53],"i":{"x":[1,0.001],"y":[1,0.998]},"o":{"x":[0,0.349],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.5,200]},"r":{"a":1,"k":[{"t":0,"s":[-30],"h":1},{"t":5.28,"s":[-30],"i":{"x":0.001,"y":0.998},"o":{"x":0.349,"y":0}},{"t":30,"s":[-90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":5,"ty":4,"parent":4,"ks":{},"ip":0,"op":31,"st":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","p":{"a":0,"k":[160,26.75]},"r":{"a":0,"k":0},"s":{"a":0,"k":[336,174.5]}},{"ty":"fl","c":{"a":0,"k":[0,0,0,0]},"o":{"a":0,"k":0}},{"ty":"tr","o":{"a":0,"k":100}}]},{"ty":"gr","it":[{"ty":"el","p":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"s":{"a":1,"k":[{"t":0,"s":[320,106],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[320,1],"h":1}]}},{"ty":"st","c":{"a":0,"k":[0.11,0.098,0.09]},"lc":1,"lj":1,"ml":10,"o":{"a":0,"k":100},"w":{"a":0,"k":16}},{"ty":"tr","o":{"a":0,"k":100}}]}]},{"ind":4,"ty":3,"parent":1,"ks":{"a":{"a":1,"k":[{"t":0,"s":[160,53],"i":{"x":[1,0],"y":[1,0.999]},"o":{"x":[0,0.348],"y":[0,0]}},{"t":30,"s":[160,0.5],"h":1}]},"p":{"a":0,"k":[200.594,200.176]},"r":{"a":1,"k":[{"t":0,"s":[30],"i":{"x":0,"y":0.999},"o":{"x":0.348,"y":0}},{"t":30,"s":[90],"h":1}]}},"ip":0,"op":31,"st":0},{"ind":1,"ty":3,"parent":0,"ks":{},"ip":0,"op":31,"st":0},{"ind":0,"ty":3,"ks":{},"ip":0,"op":31,"st":0}],"meta":{"g":"https://jitter.video"},"op":30,"v":"5.7.4","w":400}

View File

@@ -9,14 +9,14 @@ primary_region = 'lhr'
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
internal_port = 3000
force_https = true
auto_stop_machines = 'stop'
auto_start_machines = true
min_machines_running = 0
processes = ['app']
[[vm]]
memory = '1gb'
cpus = 1
memory_mb = 1024
memory = '1gb'
cpus = 1
memory_mb = 1024

View File

@@ -30,7 +30,7 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.13",
"typescript": "^6",
"typescript": "^5.9.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4"
}

View File

@@ -1,16 +1,18 @@
{
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
"compilerOptions": {
"lib": ["DOM", "ES2022"],
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"]
},
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,

View File

@@ -8,14 +8,11 @@
"@json-render/core": "^0.12.1",
"@nym.sh/jrx": "^0.2.0",
"@types/bun": "latest",
"@typescript/native-preview": "^7.0.0-dev.20260412.1",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"oxfmt": "^0.24.0",
"oxlint": "^1.39.0",
},
"peerDependencies": {
"typescript": "^6",
"typescript": "^5",
},
},
"apps/admin-dashboard": {
@@ -44,7 +41,7 @@
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"typescript": "^6",
"typescript": "~5.9.3",
"vite": "^7.2.4",
},
},
@@ -114,7 +111,7 @@
"eas-cli": "^18.0.1",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "^6",
"typescript": "~5.9.2",
},
},
"apps/waitlist-website": {
@@ -141,7 +138,7 @@
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.1.13",
"typescript": "^6",
"typescript": "^5.9.2",
"vite": "^7.1.7",
"vite-tsconfig-paths": "^5.1.4",
},
@@ -1455,22 +1452,6 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260412.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260412.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260412.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260412.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260412.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260412.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260412.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260412.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-tDw3XZt2BkjAlt/MJmnFGmbe9lgKmc5wezmrMoBIEvJcqz+/KVpVBVvjbkZoaiABnJmuG3G3b6IUFrEveTw6UQ=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260412.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sSkFG+hjtRWffg6FddF3dEkk7N3TRMEqfiUpixwcWhXgyocMdPw8wutTvQRBxQdgxeL9y01M2SO8A8YPPiEgVg=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260412.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2BTeaLkrHEEDg0D9snigddy01qTY+wgx+W+GpXAfx36PPvW4xWuGXNVWfSaB8bqAC9C8NeLnT/C9/G/rJ5v2w=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260412.1", "", { "os": "linux", "cpu": "arm" }, "sha512-wDLekbfsfmKMWORg7CTnEnpKj8oXpU/6AEBrtVN9CEUCiQAe6yH878nZHhJNzWQXHtrtFf3lY49Yplqmdxja3w=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260412.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-JAdsG6MlVV1hoAUKPy8zxAL7xLeNxz8JgCbLCJVqW8EyH29R9FD4cFTqr7CSIRTNUEDzDTrgnXUyoRtDe1gr+w=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260412.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gYgppiQIqid3jZ7D8THh4k3Q+4bwidrQH6SL9Xgbk1qfP6/jwv8twuPqDOfZ+cK2OD55lQHp15fOh2lMNAC40Q=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260412.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-TOh7rH5H3jisHJqRXJSjmUGMzcbNBocS/hufhXPQIv+g3pdG5IKZoSnv3SV62I5d12FFDSS5KQon5MQPnOKAHg=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260412.1", "", { "os": "win32", "cpu": "x64" }, "sha512-u+70wL89wspN1wKoX6FVNUATRGCG3BpleByP3H/UqOZvlwuMm8N7Gy8hEbM0U8bDyAuyP/daUfTBVkqXjjv9mA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
@@ -1749,8 +1730,6 @@
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
"cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="],
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
@@ -1771,8 +1750,6 @@
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
@@ -1957,8 +1934,6 @@
"envinfo": ["envinfo@7.11.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
"error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="],
@@ -2033,8 +2008,6 @@
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
@@ -2309,8 +2282,6 @@
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
"hyperlinker": ["hyperlinker@1.0.0", "", {}, "sha512-Ty8UblRWFEcfSuIaajM34LdPXIhbs1ajEX/BBPv24J+enSVaEVY63xQ6lTO9VRYS5LAoghIG0IDJ+p+IPzKUQQ=="],
"hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="],
@@ -2565,10 +2536,6 @@
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"lint-staged": ["lint-staged@16.4.0", "", { "dependencies": { "commander": "^14.0.3", "listr2": "^9.0.5", "picomatch": "^4.0.3", "string-argv": "^0.3.2", "tinyexec": "^1.0.4", "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw=="],
"listr2": ["listr2@9.0.5", "", { "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="],
@@ -2585,8 +2552,6 @@
"log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
"log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
@@ -3181,8 +3146,6 @@
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
@@ -3271,7 +3234,7 @@
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
@@ -3323,8 +3286,6 @@
"strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -3401,7 +3362,7 @@
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="],
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@@ -3467,7 +3428,7 @@
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
"typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ua-parser-js": ["ua-parser-js@1.0.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug=="],
@@ -3617,7 +3578,7 @@
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
@@ -3827,8 +3788,6 @@
"@expo/steps/joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="],
"@expo/steps/yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"@expo/xcpretty/@babel/code-frame": ["@babel/code-frame@7.23.5", "", { "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" } }, "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA=="],
"@formatjs/ecma402-abstract/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -3945,6 +3904,8 @@
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"aelis-client/@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
"aelis-client/@types/react": ["@types/react@19.1.17", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA=="],
"aelis-client/react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
@@ -3993,8 +3954,6 @@
"cli-progress/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cli-truncate/string-width": ["string-width@8.2.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -4025,8 +3984,6 @@
"eas-cli/ora": ["ora@5.1.0", "", { "dependencies": { "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.4.0", "is-interactive": "^1.0.0", "log-symbols": "^4.0.0", "mute-stream": "0.0.8", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w=="],
"eas-cli/yaml": ["yaml@2.6.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ=="],
"eciesjs/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"eciesjs/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
@@ -4137,16 +4094,8 @@
"lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"listr2/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"log-symbols/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
"log-update/ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
@@ -4167,6 +4116,8 @@
"metro-config/metro-runtime": ["metro-runtime@0.83.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw=="],
"metro-config/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
"metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA=="],
"metro-source-map/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
@@ -4201,8 +4152,6 @@
"nypm/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"nypm/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
"ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
"ora/log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="],
@@ -4281,10 +4230,6 @@
"simple-swizzle/is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
@@ -4491,16 +4436,12 @@
"@expo/plugin-help/@oclif/core/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
"@expo/plugin-help/@oclif/core/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
"@expo/plugin-help/@oclif/core/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@expo/plugin-help/@oclif/core/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@expo/plugin-warn-if-update-available/@oclif/core/js-yaml": ["js-yaml@3.14.2", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg=="],
"@expo/plugin-warn-if-update-available/@oclif/core/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
"@expo/plugin-warn-if-update-available/@oclif/core/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@expo/plugin-warn-if-update-available/@oclif/core/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -4603,6 +4544,8 @@
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
"aelis-client/@tanstack/react-query/@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
"aelis-client/react-dom/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"better-opn/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
@@ -4755,14 +4698,6 @@
"lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"listr2/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.32.0", "", {}, "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ=="],
"metro-symbolicate/metro-source-map/ob1": ["ob1@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA=="],

View File

@@ -41,7 +41,7 @@ Sources → Source Graph → FeedEngine
### One harness, not many agents
The "agents" in this doc describe _behaviors_, not separate running processes. A human PA is one person — they don't have a "calendar agent" and a "follow-up agent" in their head. They look at your whole situation and act on whatever matters.
The "agents" in this doc describe *behaviors*, not separate running processes. A human PA is one person — they don't have a "calendar agent" and a "follow-up agent" in their head. They look at your whole situation and act on whatever matters.
AELIS works the same way. One LLM harness receives all feed items, all context, all user memory, and all available tools. It returns a single `FeedEnhancement`. Every behavior (preparation, follow-up, anomaly detection, tone adjustment, cross-source reasoning) is an instruction in the system prompt, not a separate agent.
@@ -50,21 +50,20 @@ The advantage: the LLM sees everything at once. It doesn't need agent-to-agent c
The only separate LLM call is the **Query Agent** — because it's user-initiated and synchronous. But it uses the same system prompt and context. It's the same "person," just responding to a question instead of proactively enhancing the feed.
Everything else is either:
- **Rule-based post-processors** — pure functions, no LLM, run on every refresh
- **The single LLM harness** — runs periodically, produces cached `FeedEnhancement`
- **Background jobs** — daily summary compression, weekly pattern discovery
### Component categories
| Component | What it is | Examples |
| ------------------------------ | ----------------------------------------- | --------------------------------------------------------------------- |
| **FeedSource nodes** | Graph participants that produce items | Briefing, Preparation, Anomaly Detection, Follow-up, Social Awareness |
| **Rule-based post-processors** | Pure functions that rerank/filter/group | TimeOfDay, CalendarGrouping, Deduplication, UserAffinity |
| **LLM enhancement harness** | Single background LLM call, cached output | Card rewriting, cross-source synthesis, tone, narrative arcs |
| **Query interface** | Synchronous LLM call, user-initiated | Conversational Q&A, web search, delegation, actions |
| **Background jobs** | Periodic data processing | Daily summary compression, weekly pattern discovery |
| **Persistence** | Stored state that feeds into everything | Memory store, affinity model, conversation history, feed snapshots |
| Component | What it is | Examples |
|---|---|---|
| **FeedSource nodes** | Graph participants that produce items | Briefing, Preparation, Anomaly Detection, Follow-up, Social Awareness |
| **Rule-based post-processors** | Pure functions that rerank/filter/group | TimeOfDay, CalendarGrouping, Deduplication, UserAffinity |
| **LLM enhancement harness** | Single background LLM call, cached output | Card rewriting, cross-source synthesis, tone, narrative arcs |
| **Query interface** | Synchronous LLM call, user-initiated | Conversational Q&A, web search, delegation, actions |
| **Background jobs** | Periodic data processing | Daily summary compression, weekly pattern discovery |
| **Persistence** | Stored state that feeds into everything | Memory store, affinity model, conversation history, feed snapshots |
### AgentContext
@@ -72,32 +71,32 @@ The LLM harness and post-processors need a unified view of the user's world: cur
`AgentContext` is **not** on the engine. The engine's job is source orchestration — running sources in dependency order, accumulating context, collecting items. It shouldn't know about user preferences, conversation history, or feed snapshots. Those are separate concerns.
`AgentContext` is a separate object that _reads from_ the engine and composes its output with other data stores:
`AgentContext` is a separate object that *reads from* the engine and composes its output with other data stores:
```typescript
interface AgentContext {
/** Current accumulated context from all sources */
context: Context
/** Current accumulated context from all sources */
context: Context
/** Recent feed items (last N refreshes or time window) */
recentItems: FeedItem[]
/** Recent feed items (last N refreshes or time window) */
recentItems: FeedItem[]
/** Query items from a specific source */
itemsFrom(sourceId: string): FeedItem[]
/** Query items from a specific source */
itemsFrom(sourceId: string): FeedItem[]
/** User preference and memory store */
preferences: UserPreferences
/** User preference and memory store */
preferences: UserPreferences
/** Conversation history */
conversationHistory: ConversationEntry[]
/** Conversation history */
conversationHistory: ConversationEntry[]
}
// Constructed by composing the engine with persistence layers
const agentContext = new AgentContext({
engine, // reads current context + items
memoryStore, // reads/writes user preferences, discovered patterns
snapshotStore, // reads feed history for pattern discovery
conversationStore, // reads conversation history
engine, // reads current context + items
memoryStore, // reads/writes user preferences, discovered patterns
snapshotStore, // reads feed history for pattern discovery
conversationStore, // reads conversation history
})
```
@@ -136,20 +135,20 @@ The enhancement output:
```typescript
interface FeedEnhancement {
/** New items to inject (briefings, nudges, suggestions) */
syntheticItems: FeedItem[]
/** New items to inject (briefings, nudges, suggestions) */
syntheticItems: FeedItem[]
/** Annotations attached to existing items, keyed by item ID */
annotations: Record<string, string>
/** Annotations attached to existing items, keyed by item ID */
annotations: Record<string, string>
/** Items to group together with a summary card */
groups: Array<{ itemIds: string[]; summary: string }>
/** Items to group together with a summary card */
groups: Array<{ itemIds: string[], summary: string }>
/** Item IDs to suppress or deprioritize */
suppress: string[]
/** Item IDs to suppress or deprioritize */
suppress: string[]
/** Ranking hints: item ID → relative importance (0-1) */
rankingHints: Record<string, number>
/** Ranking hints: item ID → relative importance (0-1) */
rankingHints: Record<string, number>
}
```
@@ -186,7 +185,6 @@ These run on every refresh. Fast, deterministic, and cover most of the ranking q
**Anomaly detection.** Compare event start times against the user's historical distribution. A 6am meeting when the user never has meetings before 9am is a statistical outlier — flag it.
**User affinity scoring.** Track implicit signals per source type per time-of-day bucket:
- Dismissals: user swipes away weather cards → decay affinity for weather
- Taps: user taps calendar items frequently → boost affinity for calendar
- Dwell time: user reads TfL alerts carefully → boost
@@ -195,9 +193,9 @@ No LLM needed. A simple decay/boost model:
```typescript
interface UserAffinityModel {
affinities: Record<string, Record<TimeBucket, number>>
dismissalDecay: number
tapBoost: number
affinities: Record<string, Record<TimeBucket, number>>
dismissalDecay: number
tapBoost: number
}
```
@@ -311,7 +309,7 @@ There are three layers:
None of these have `if` statements. The LLM reads the feed, reads the user's memory, and decides what to say. Add a new source (Spotify, email, tasks) and the LLM automatically incorporates it — no new behavior code needed.
**Infrastructure (plumbing needed, but logic is emergent).** These need tables, APIs, and background jobs. But the _decision-making_ — what to extract, when to surface, how to phrase — is all LLM.
**Infrastructure (plumbing needed, but logic is emergent).** These need tables, APIs, and background jobs. But the *decision-making* — what to extract, when to surface, how to phrase — is all LLM.
- Gentle Follow-up — needs: extraction pipeline after each conversation turn, `commitments` table. The LLM decides what counts as a commitment and when to remind.
- Memory — needs: `memories` table, read/write API. The LLM decides what to remember and how to use it.
@@ -323,7 +321,7 @@ None of these have `if` statements. The LLM reads the feed, reads the user's mem
- Delegation — needs: confirmation flow, write-back infrastructure. The LLM decides what the user wants done.
- Financial Awareness — needs: `financial_events` table, email extraction. The LLM decides what financial events matter.
**Hardcoded rules (fast path, must be deterministic).** These run on every refresh in <10ms. They _should_ be rules because they need to be fast and predictable.
**Hardcoded rules (fast path, must be deterministic).** These run on every refresh in <10ms. They *should* be rules because they need to be fast and predictable.
- User affinity scoring decay/boost math on tap/dismiss events
- Deduplication title + time matching across sources
@@ -417,38 +415,39 @@ One per user, living in the `FeedEngineManager` on the backend:
```typescript
class EnhancementManager {
private cache: FeedEnhancement | null = null
private lastInputHash: string | null = null
private running = false
private cache: FeedEnhancement | null = null
private lastInputHash: string | null = null
private running = false
async enhance(items: FeedItem[], context: AgentContext): Promise<FeedEnhancement> {
const hash = computeHash(items, context)
async enhance(
items: FeedItem[],
context: AgentContext,
): Promise<FeedEnhancement> {
const hash = computeHash(items, context)
// Nothing changed — return cache
if (hash === this.lastInputHash && this.cache) {
return this.cache
}
// Nothing changed — return cache
if (hash === this.lastInputHash && this.cache) {
return this.cache
}
// Already running — return stale cache
if (this.running) {
return this.cache ?? emptyEnhancement()
}
// Already running — return stale cache
if (this.running) {
return this.cache ?? emptyEnhancement()
}
// Run in background, update cache when done
this.running = true
this.runHarness(items, context, hash)
.then((enhancement) => {
this.cache = enhancement
this.lastInputHash = hash
this.notifySubscribers(enhancement)
})
.finally(() => {
this.running = false
})
// Run in background, update cache when done
this.running = true
this.runHarness(items, context, hash)
.then(enhancement => {
this.cache = enhancement
this.lastInputHash = hash
this.notifySubscribers(enhancement)
})
.finally(() => { this.running = false })
// Return stale cache immediately
return this.cache ?? emptyEnhancement()
}
// Return stale cache immediately
return this.cache ?? emptyEnhancement()
}
}
```
@@ -523,7 +522,7 @@ These are `FeedSource` nodes that depend on calendar, tasks, weather, and other
#### Anticipatory Logistics
Works backward from events to tell you what you need to _do_ to be ready.
Works backward from events to tell you what you need to *do* to be ready.
- Flight at 6am → "You need to leave by 4am, which means waking at 3:30. I'd suggest packing tonight."
- Dinner at a new restaurant → "It's a 25-minute walk or 8-minute Uber. Street parking is difficult — there's a car park on the next street."
@@ -580,7 +579,7 @@ Tracks loose ends — things you said but never wrote down as tasks.
- "You told James you'd review his PR — it's been 3 days"
- "You promised to call your mom this weekend"
The key difference from task tracking: this catches things that fell through the cracks _because_ they were never formalized.
The key difference from task tracking: this catches things that fell through the cracks *because* they were never formalized.
**How intent extraction works:**
@@ -615,14 +614,12 @@ Long-term memory of interactions and preferences. Feeds into every other agent.
A persistent profile that builds over time. Not an agent itself — a system that makes every other agent smarter.
Learns from:
- Explicit statements: "I prefer morning meetings"
- Implicit behavior: user always dismisses evening suggestions
- Feedback: user rates suggestions as helpful/not
- Cross-source patterns: always books aisle seats, always picks the cheaper option
Used by:
- Proactive Agent suggests restaurants the user would actually like
- Delegation Agent books the right kind of hotel room
- Summary Agent uses the user's preferred level of detail
@@ -651,30 +648,27 @@ Passive observation. The patterns aren't hardcoded — the LLM discovers them fr
```typescript
interface DailySummary {
date: string
feedCheckTimes: string[] // when the user opened the feed
itemTypeCounts: Record<string, number> // how many of each type appeared
interactions: Array<{
// what the user tapped/dismissed
itemType: string
action: "tap" | "dismiss" | "dwell"
time: string
}>
locations: Array<{
// where the user was throughout the day
lat: number
lng: number
time: string
}>
calendarSummary: Array<{
// what events happened
title: string
startTime: string
endTime: string
location?: string
attendees?: string[]
}>
weatherConditions: string[] // conditions seen throughout the day
date: string
feedCheckTimes: string[] // when the user opened the feed
itemTypeCounts: Record<string, number> // how many of each type appeared
interactions: Array<{ // what the user tapped/dismissed
itemType: string
action: "tap" | "dismiss" | "dwell"
time: string
}>
locations: Array<{ // where the user was throughout the day
lat: number
lng: number
time: string
}>
calendarSummary: Array<{ // what events happened
title: string
startTime: string
endTime: string
location?: string
attendees?: string[]
}>
weatherConditions: string[] // conditions seen throughout the day
}
```
@@ -684,20 +678,20 @@ interface DailySummary {
```typescript
interface DiscoveredPattern {
/** What the pattern is, in natural language */
description: string
/** How confident (0-1) */
confidence: number
/** When this pattern is relevant */
relevance: {
daysOfWeek?: number[]
timeRange?: { start: string; end: string }
conditions?: string[]
}
/** How this should affect the feed */
feedImplication: string
/** Suggested card to surface when pattern is relevant */
suggestedAction?: string
/** What the pattern is, in natural language */
description: string
/** How confident (0-1) */
confidence: number
/** When this pattern is relevant */
relevance: {
daysOfWeek?: number[]
timeRange?: { start: string, end: string }
conditions?: string[]
}
/** How this should affect the feed */
feedImplication: string
/** Suggested card to surface when pattern is relevant */
suggestedAction?: string
}
```
@@ -723,9 +717,9 @@ Maintains awareness of relationships and surfaces timely nudges.
Needs: contacts with birthday/anniversary data, calendar history for meeting frequency, email/message signals, optionally social media.
This is what makes an assistant feel like it _cares_. Most tools are transactional. This one remembers the people in your life.
This is what makes an assistant feel like it *cares*. Most tools are transactional. This one remembers the people in your life.
Beyond frequency, the assistant can understand relationship _dynamics_:
Beyond frequency, the assistant can understand relationship *dynamics*:
- "You and Sarah always have productive meetings. You and Alex tend to go off-track — maybe set a tighter agenda."
- "You've cancelled on Tom three times — he might be feeling deprioritized."
@@ -791,7 +785,7 @@ This is where the source graph pays off. All the data is already there — the a
#### Tone & Timing
Controls _when_ and _how_ information is delivered. The difference between useful and annoying.
Controls *when* and *how* information is delivered. The difference between useful and annoying.
- Bad news before morning coffee? Hold it.
- Three notifications in a row? Batch them.
@@ -855,7 +849,6 @@ The primary interface. This isn't a feed query tool — it's the person you talk
The user should be able to ask AELIS anything they'd ask a knowledgeable friend. Some questions are about their data. Most aren't.
**About their life (reads from the source graph):**
- "What's on my calendar tomorrow?"
- "When's my next flight?"
- "Do I have any conflicts this week?"
@@ -863,7 +856,6 @@ The user should be able to ask AELIS anything they'd ask a knowledgeable friend.
- "Tell me more about this" (anchored to a feed item)
**About the world (falls through to web search):**
- "How do I unclog a drain?"
- "What should I make with chicken and broccoli?"
- "What's the best way to get from King's Cross to Heathrow?"
@@ -872,7 +864,6 @@ The user should be able to ask AELIS anything they'd ask a knowledgeable friend.
- "What are some good date night restaurants in Shoreditch?"
**Contextual blend (graph + web):**
- "What's the dress code for The Ivy?" (calendar shows dinner there tonight)
- "Will I need an umbrella?" (location + weather, but could also web-search venue for indoor/outdoor)
- "What should I know before my meeting with Acme Corp?" (calendar + web search for company info)
@@ -888,12 +879,10 @@ This is also where intent extraction happens for the Gentle Follow-up Agent. Eve
The backbone for general knowledge. Makes AELIS a person you can ask things, not just a dashboard you look at.
**Reactive (user asks):**
- Recipe ideas, how-to questions, factual lookups, recommendations
- Anything the source graph can't answer
**Proactive (agents trigger):**
- Contextual Preparation enriches calendar events: venue info, attendee backgrounds, parking
- Feed shows a concert → pre-fetches setlist, venue details
- Ambient Context checks for disruptions, closures, news
@@ -960,7 +949,7 @@ Handles tasks the user delegates via natural language.
Requires write access to sources. Confirmation UX for anything destructive or costly.
**Implementation:** Extends the Query Agent. When the LLM determines the user wants to _do_ something (not just ask), it calls a delegation tool with structured output: `{ action: "create_reminder" | "schedule_meeting" | "add_task", params: {...} }`. The backend maps this to `executeAction()` on the relevant source. For "find a time that works for both me and Sarah," the agent queries both calendars (requires Sarah to be a known contact with calendar access — or the agent asks the user to share availability). All write actions go through a confirmation step: the backend sends a `delegation.confirm` notification with the proposed action, and the client shows a confirmation UI. The user approves or modifies before execution. Store delegation history for the Follow-up Agent.
**Implementation:** Extends the Query Agent. When the LLM determines the user wants to *do* something (not just ask), it calls a delegation tool with structured output: `{ action: "create_reminder" | "schedule_meeting" | "add_task", params: {...} }`. The backend maps this to `executeAction()` on the relevant source. For "find a time that works for both me and Sarah," the agent queries both calendars (requires Sarah to be a known contact with calendar access — or the agent asks the user to share availability). All write actions go through a confirmation step: the backend sends a `delegation.confirm` notification with the proposed action, and the client shows a confirmation UI. The user approves or modifies before execution. Store delegation history for the Follow-up Agent.
#### Actions

View File

@@ -131,19 +131,19 @@ Feed items carry an optional `ui` field containing a json-render tree, and an op
```typescript
interface FeedItem<TType, TData> {
id: string
type: TType
timestamp: Date
data: TData
ui?: JsonRenderNode
slots?: Record<string, Slot>
id: string
type: TType
timestamp: Date
data: TData
ui?: JsonRenderNode
slots?: Record<string, Slot>
}
interface Slot {
/** Tells the LLM what this slot wants — the source writes this */
description: string
/** LLM-filled text content, null until enhanced */
content: string | null
/** Tells the LLM what this slot wants — the source writes this */
description: string
/** LLM-filled text content, null until enhanced */
content: string | null
}
```
@@ -238,23 +238,28 @@ The user never waits for the LLM. They see the feed instantly with the previous
The harness serializes items with their unfilled slots into a single prompt. Items without slots are excluded. The LLM sees everything at once and fills whatever slots are relevant.
```typescript
function buildHarnessInput(items: FeedItem[], context: AgentContext): HarnessInput {
const itemsWithSlots = items
.filter((item) => item.slots && Object.keys(item.slots).length > 0)
.map((item) => ({
id: item.id,
type: item.type,
data: item.data,
slots: Object.fromEntries(
Object.entries(item.slots!).map(([name, slot]) => [name, slot.description]),
),
}))
function buildHarnessInput(
items: FeedItem[],
context: AgentContext,
): HarnessInput {
const itemsWithSlots = items
.filter(item => item.slots && Object.keys(item.slots).length > 0)
.map(item => ({
id: item.id,
type: item.type,
data: item.data,
slots: Object.fromEntries(
Object.entries(item.slots!).map(
([name, slot]) => [name, slot.description]
)
),
}))
return {
items: itemsWithSlots,
userMemory: context.preferences,
currentTime: new Date().toISOString(),
}
return {
items: itemsWithSlots,
userMemory: context.preferences,
currentTime: new Date().toISOString(),
}
}
```
@@ -262,33 +267,29 @@ The LLM sees:
```json
{
"items": [
{
"id": "weather-current-123",
"type": "weather-current",
"data": { "temperature": 18, "condition": "cloudy" },
"slots": {
"insight": "A short contextual insight about the current weather and how it affects the user's day",
"cross-source": "Connection between weather and the user's calendar events or plans"
}
},
{
"id": "calendar-event-456",
"type": "calendar-event",
"data": {
"title": "Dinner at The Ivy",
"startTime": "19:00",
"location": "The Ivy, West St"
},
"slots": {
"context": "Background on this event, attendees, or previous meetings with these people",
"logistics": "Travel time, parking, directions to the venue",
"weather": "Weather conditions relevant to this event's time and location"
}
}
],
"userMemory": { "commute": "victoria-line", "preference.walking_distance": "1 mile" },
"currentTime": "2025-02-26T14:30:00Z"
"items": [
{
"id": "weather-current-123",
"type": "weather-current",
"data": { "temperature": 18, "condition": "cloudy" },
"slots": {
"insight": "A short contextual insight about the current weather and how it affects the user's day",
"cross-source": "Connection between weather and the user's calendar events or plans"
}
},
{
"id": "calendar-event-456",
"type": "calendar-event",
"data": { "title": "Dinner at The Ivy", "startTime": "19:00", "location": "The Ivy, West St" },
"slots": {
"context": "Background on this event, attendees, or previous meetings with these people",
"logistics": "Travel time, parking, directions to the venue",
"weather": "Weather conditions relevant to this event's time and location"
}
}
],
"userMemory": { "commute": "victoria-line", "preference.walking_distance": "1 mile" },
"currentTime": "2025-02-26T14:30:00Z"
}
```
@@ -298,30 +299,27 @@ A flat map of item ID → slot name → text content. Slots left null are unfill
```json
{
"slotFills": {
"weather-current-123": {
"insight": "Rain after 3pm — grab a jacket before your walk",
"cross-source": "Should be dry by 7pm for your dinner at The Ivy"
},
"calendar-event-456": {
"context": null,
"logistics": "20-minute walk from home — leave by 18:40",
"weather": "Rain clears by evening, you'll be fine"
}
},
"syntheticItems": [
{
"id": "briefing-morning",
"type": "briefing",
"data": {},
"ui": {
"component": "Text",
"props": { "text": "Light afternoon — just your dinner at 7. Rain clears by then." }
}
}
],
"suppress": [],
"rankingHints": {}
"slotFills": {
"weather-current-123": {
"insight": "Rain after 3pm — grab a jacket before your walk",
"cross-source": "Should be dry by 7pm for your dinner at The Ivy"
},
"calendar-event-456": {
"context": null,
"logistics": "20-minute walk from home — leave by 18:40",
"weather": "Rain clears by evening, you'll be fine"
}
},
"syntheticItems": [
{
"id": "briefing-morning",
"type": "briefing",
"data": {},
"ui": { "component": "Text", "props": { "text": "Light afternoon — just your dinner at 7. Rain clears by then." } }
}
],
"suppress": [],
"rankingHints": {}
}
```
@@ -331,41 +329,42 @@ One per user, living in the `FeedEngineManager` on the backend:
```typescript
class EnhancementManager {
private cache: EnhancementResult | null = null
private lastInputHash: string | null = null
private running = false
private cache: EnhancementResult | null = null
private lastInputHash: string | null = null
private running = false
async enhance(items: FeedItem[], context: AgentContext): Promise<EnhancementResult> {
const hash = computeHash(items, context)
async enhance(
items: FeedItem[],
context: AgentContext,
): Promise<EnhancementResult> {
const hash = computeHash(items, context)
if (hash === this.lastInputHash && this.cache) {
return this.cache
}
if (hash === this.lastInputHash && this.cache) {
return this.cache
}
if (this.running) {
return this.cache ?? emptyResult()
}
if (this.running) {
return this.cache ?? emptyResult()
}
this.running = true
this.runHarness(items, context)
.then((result) => {
this.cache = result
this.lastInputHash = hash
this.notifySubscribers(result)
})
.finally(() => {
this.running = false
})
this.running = true
this.runHarness(items, context)
.then(result => {
this.cache = result
this.lastInputHash = hash
this.notifySubscribers(result)
})
.finally(() => { this.running = false })
return this.cache ?? emptyResult()
}
return this.cache ?? emptyResult()
}
}
interface EnhancementResult {
slotFills: Record<string, Record<string, string | null>>
syntheticItems: FeedItem[]
suppress: string[]
rankingHints: Record<string, number>
slotFills: Record<string, Record<string, string | null>>
syntheticItems: FeedItem[]
suppress: string[]
rankingHints: Record<string, number>
}
```
@@ -374,20 +373,23 @@ interface EnhancementResult {
After the harness runs, the engine merges slot fills into items:
```typescript
function mergeEnhancement(items: FeedItem[], result: EnhancementResult): FeedItem[] {
return items.map((item) => {
const fills = result.slotFills[item.id]
if (!fills || !item.slots) return item
function mergeEnhancement(
items: FeedItem[],
result: EnhancementResult,
): FeedItem[] {
return items.map(item => {
const fills = result.slotFills[item.id]
if (!fills || !item.slots) return item
const mergedSlots = { ...item.slots }
for (const [name, content] of Object.entries(fills)) {
if (name in mergedSlots && content !== null) {
mergedSlots[name] = { ...mergedSlots[name], content }
}
}
const mergedSlots = { ...item.slots }
for (const [name, content] of Object.entries(fills)) {
if (name in mergedSlots && content !== null) {
mergedSlots[name] = { ...mergedSlots[name], content }
}
}
return { ...item, slots: mergedSlots }
})
return { ...item, slots: mergedSlots }
})
}
```

View File

@@ -24,16 +24,16 @@ The backend uses a raw `pg` Pool for Better Auth and has no ORM. We need a persi
A `user_sources` table stores per-user source state:
| Column | Type | Description |
| ------------- | --------------------- | -------------------------------------------------------------- |
| `id` | `uuid` PK | Row ID |
| `user_id` | `text` FK → `user.id` | Owner |
| `source_id` | `text` | Source identifier (e.g., `aelis.tfl`, `aelis.weather`) |
| `enabled` | `boolean` | Whether this source is active in the user's feed |
| `config` | `jsonb` | Source-specific configuration (validated by source at runtime) |
| `credentials` | `bytea` | Encrypted OAuth tokens / secrets (AES-256-GCM) |
| `created_at` | `timestamp with tz` | Row creation time |
| `updated_at` | `timestamp with tz` | Last modification time |
| Column | Type | Description |
| ------------ | ------------------------ | ------------------------------------------------------------ |
| `id` | `uuid` PK | Row ID |
| `user_id` | `text` FK → `user.id` | Owner |
| `source_id` | `text` | Source identifier (e.g., `aelis.tfl`, `aelis.weather`) |
| `enabled` | `boolean` | Whether this source is active in the user's feed |
| `config` | `jsonb` | Source-specific configuration (validated by source at runtime)|
| `credentials`| `bytea` | Encrypted OAuth tokens / secrets (AES-256-GCM) |
| `created_at` | `timestamp with tz` | Row creation time |
| `updated_at` | `timestamp with tz` | Last modification time |
- Unique constraint on `(user_id, source_id)` — one config row per source per user.
- `config` is a generic `jsonb` column. Each source package exports an arktype schema; the backend provider validates the JSON at source construction time.
@@ -50,11 +50,11 @@ A `user_sources` table stores per-user source state:
When a new user is created, seed `user_sources` rows for default sources:
| Source | Default config |
| ---------------- | ----------------------------------------------------------- |
| `aelis.location` | `{}` |
| `aelis.weather` | `{ "units": "metric", "hourlyLimit": 12, "dailyLimit": 7 }` |
| `aelis.tfl` | `{ "lines": <all default lines> }` |
| Source | Default config |
| ------------------ | --------------------------------------------------------------- |
| `aelis.location` | `{}` |
| `aelis.weather` | `{ "units": "metric", "hourlyLimit": 12, "dailyLimit": 7 }` |
| `aelis.tfl` | `{ "lines": <all default lines> }` |
- Seeding happens via a Better Auth `after` hook on user creation, or via application-level logic after signup.
- Sources requiring credentials (Google Calendar, CalDAV) are **not** enabled by default — they require the user to connect an account first.
@@ -67,35 +67,29 @@ Each provider receives the Drizzle DB instance and queries `user_sources` intern
```typescript
class TflSourceProvider implements FeedSourceProvider {
constructor(
private db: DrizzleDb,
private apiKey: string,
) {}
constructor(private db: DrizzleDb, private apiKey: string) {}
async feedSourceForUser(userId: string): Promise<TflSource> {
const row = await this.db
.select()
.from(userSources)
.where(
and(
eq(userSources.userId, userId),
eq(userSources.sourceId, "aelis.tfl"),
eq(userSources.enabled, true),
),
)
.limit(1)
async feedSourceForUser(userId: string): Promise<TflSource> {
const row = await this.db.select()
.from(userSources)
.where(and(
eq(userSources.userId, userId),
eq(userSources.sourceId, "aelis.tfl"),
eq(userSources.enabled, true),
))
.limit(1)
if (!row[0]) {
throw new SourceDisabledError("aelis.tfl", userId)
}
if (!row[0]) {
throw new SourceDisabledError("aelis.tfl", userId)
}
const config = tflSourceConfig(row[0].config ?? {})
if (config instanceof type.errors) {
throw new Error(`Invalid TFL config for user ${userId}: ${config.summary}`)
}
const config = tflSourceConfig(row[0].config ?? {})
if (config instanceof type.errors) {
throw new Error(`Invalid TFL config for user ${userId}: ${config.summary}`)
}
return new TflSource({ ...config, apiKey: this.apiKey })
}
return new TflSource({ ...config, apiKey: this.apiKey })
}
}
```
@@ -216,19 +210,16 @@ _`feed-source-provider.ts`, `user-session-manager.ts`, `engine/http.ts`, and `lo
## Dependencies
**Add:**
- `drizzle-orm`
- `drizzle-kit` (dev)
**Remove:**
- `pg`
- `@types/pg` (dev)
## Environment Variables
**Add to `.env.example`:**
- `CREDENTIALS_ENCRYPTION_KEY` — 32-byte hex or base64 key for AES-256-GCM
## Open Questions (Deferred)

View File

@@ -39,14 +39,14 @@ Source IDs use reverse domain notation. Built-in sources use `aelis.<name>`. Thi
Action IDs are descriptive verb-noun pairs in kebab-case, scoped to their source. The globally unique form is `<sourceId>/<actionId>`.
| Source ID | Action IDs |
| ---------------- | -------------------------------------------------------------- |
| Source ID | Action IDs |
| --------------- | -------------------------------------------------------------- |
| `aelis.location` | `update-location` (migrated from `pushLocation()`) |
| `aelis.tfl` | `set-lines-of-interest` (migrated from `setLinesOfInterest()`) |
| `aelis.weather` | _(none)_ |
| `com.spotify` | `play-track`, `pause-playback`, `skip-track`, `like-track` |
| `com.spotify` | `play-track`, `pause-playback`, `skip-track`, `like-track` |
| `aelis.calendar` | `rsvp`, `create-event` |
| `com.todoist` | `complete-task`, `snooze-task` |
| `com.todoist` | `complete-task`, `snooze-task` |
This means existing source packages need their `id` updated (e.g., `"location"``"aelis.location"`).
@@ -241,16 +241,8 @@ class SpotifySource implements FeedSource<SpotifyFeedItem> {
type: "View",
className: "flex-1",
children: [
{
type: "Text",
className: "font-semibold text-black dark:text-white",
text: track.name,
},
{
type: "Text",
className: "text-sm text-gray-500 dark:text-gray-400",
text: track.artist,
},
{ type: "Text", className: "font-semibold text-black dark:text-white", text: track.name },
{ type: "Text", className: "text-sm text-gray-500 dark:text-gray-400", text: track.artist },
],
},
],
@@ -269,8 +261,8 @@ class SpotifySource implements FeedSource<SpotifyFeedItem> {
4. `FeedSource.listActions()` is a required method returning `Record<string, ActionDefinition>` (empty record if no actions)
5. `FeedSource.executeAction()` is a required method (no-op for sources without actions)
6. `FeedItem.actions` is an optional readonly array of `ItemAction`
6b. `FeedItem.ui` is an optional json-render tree describing server-driven UI
6c. `FeedItem.slots` is an optional record of named LLM-fillable slots
6b. `FeedItem.ui` is an optional json-render tree describing server-driven UI
6c. `FeedItem.slots` is an optional record of named LLM-fillable slots
7. `FeedEngine.executeAction()` routes to correct source, returns `ActionResult`
8. `FeedEngine.listActions()` aggregates actions from all sources
9. Existing tests pass unchanged (all changes are additive)

View File

@@ -11,23 +11,16 @@
"lint": "oxlint .",
"lint:fix": "oxlint --fix .",
"format": "oxfmt --write .",
"format:check": "oxfmt --check .",
"prepare": "husky"
"format:check": "oxfmt --check ."
},
"devDependencies": {
"@json-render/core": "^0.12.1",
"@nym.sh/jrx": "^0.2.0",
"@types/bun": "latest",
"@typescript/native-preview": "^7.0.0-dev.20260412.1",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"oxfmt": "^0.24.0",
"oxlint": "^1.39.0"
},
"peerDependencies": {
"typescript": "^6"
},
"lint-staged": {
"*.{ts,tsx,js,jsx,json,css,md}": "oxfmt --write"
"typescript": "^5"
}
}

View File

@@ -7,11 +7,11 @@
"scripts": {
"test": "bun test ."
},
"peerDependencies": {
"@nym.sh/jrx": "*",
"@json-render/core": "*"
},
"dependencies": {
"@standard-schema/spec": "^1.1.0"
},
"peerDependencies": {
"@json-render/core": "*",
"@nym.sh/jrx": "*"
}
}

View File

@@ -1,14 +1,17 @@
import type { Context, FeedEnhancement, FeedItem, FeedPostProcessor } from "@aelis/core"
import { TimeRelevance } from "@aelis/core"
import type { CalDavEventData } from "@aelis/source-caldav"
import type { CalendarEventData } from "@aelis/source-google-calendar"
import type { CurrentWeatherData } from "@aelis/source-weatherkit"
import { TimeRelevance } from "@aelis/core"
import { CalDavFeedItemType } from "@aelis/source-caldav"
import { CalendarFeedItemType } from "@aelis/source-google-calendar"
import { TflFeedItemType } from "@aelis/source-tfl"
import { WeatherFeedItemType } from "@aelis/source-weatherkit"
export const TimePeriod = {
Morning: "morning",
Afternoon: "afternoon",
@@ -25,6 +28,7 @@ export const DayType = {
export type DayType = (typeof DayType)[keyof typeof DayType]
const PRE_MEETING_WINDOW_MS = 30 * 60 * 1000
const TRANSITION_WINDOW_MS = 30 * 60 * 1000
@@ -140,6 +144,7 @@ export function createTimeOfDayEnhancer(options?: TimeOfDayEnhancerOptions): Fee
return timeOfDayEnhancer
}
export function getTimePeriod(date: Date): TimePeriod {
const hour = date.getHours()
if (hour >= 22 || hour < 6) return TimePeriod.Night
@@ -177,9 +182,7 @@ function getNextPeriodBoundary(date: Date): { period: TimePeriod; msUntil: numbe
* Google Calendar uses `startTime`, CalDAV uses `startDate`.
*/
function getEventStartTime(data: CalendarEventData | CalDavEventData): Date {
return "startTime" in data
? (data as CalendarEventData).startTime
: (data as CalDavEventData).startDate
return "startTime" in data ? (data as CalendarEventData).startTime : (data as CalDavEventData).startDate
}
/**
@@ -193,6 +196,7 @@ function hasPrecipitationOrExtreme(item: FeedItem): boolean {
return false
}
interface PreMeetingInfo {
/** IDs of calendar items starting within the pre-meeting window */
upcomingMeetingIds: Set<string>
@@ -221,6 +225,7 @@ function detectPreMeetingItems(items: FeedItem[], now: Date): PreMeetingInfo {
return { upcomingMeetingIds, hasLocationMeeting }
}
function findFirstEventOfDay(items: FeedItem[], now: Date): string | null {
let earliest: { id: string; time: number } | null = null
@@ -247,6 +252,7 @@ function findFirstEventOfDay(items: FeedItem[], now: Date): string | null {
return earliest?.id ?? null
}
function applyMorningWeekday(
items: FeedItem[],
boost: Record<string, number>,
@@ -409,6 +415,7 @@ function applyNight(items: FeedItem[], boost: Record<string, number>, suppress:
}
}
function applyPreMeetingOverrides(
items: FeedItem[],
preMeeting: PreMeetingInfo,
@@ -480,6 +487,7 @@ function applyWindDown(
}
}
function applyTransitionLookahead(
items: FeedItem[],
now: Date,
@@ -536,6 +544,7 @@ function getNextPeriodBoostTargets(period: TimePeriod, dayType: DayType): Readon
return targets
}
function applyWeatherTimeCorrelation(
items: FeedItem[],
period: TimePeriod,
@@ -553,11 +562,7 @@ function applyWeatherTimeCorrelation(
break
}
case WeatherFeedItemType.Current:
if (
period === TimePeriod.Morning &&
dayType === DayType.Weekday &&
hasPrecipitationOrExtreme(item)
) {
if (period === TimePeriod.Morning && dayType === DayType.Weekday && hasPrecipitationOrExtreme(item)) {
boost[item.id] = (boost[item.id] ?? 0) + 0.1
}
if (period === TimePeriod.Evening && hasEveningEventWithLocation) {
@@ -586,3 +591,5 @@ function hasEveningCalendarEventWithLocation(items: FeedItem[], now: Date): bool
return false
}

View File

@@ -7,10 +7,11 @@
* Writes feed items (with slots) to scripts/.cache/feed-items.json for inspection.
*/
import { Context } from "@aelis/core"
import { mkdirSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { Context } from "@aelis/core"
import { CalDavSource } from "../src/index.ts"
const serverUrl = prompt("CalDAV server URL:")

View File

@@ -9,8 +9,8 @@
"fetch-fixtures": "bun run scripts/fetch-fixtures.ts"
},
"dependencies": {
"@aelis/components": "workspace:*",
"@aelis/core": "workspace:*",
"@aelis/components": "workspace:*",
"@aelis/source-location": "workspace:*",
"arktype": "^2.1.0"
},

View File

@@ -9,14 +9,15 @@
* Usage: bun packages/aelis-source-weatherkit/scripts/query.ts
*/
import { Context } from "@aelis/core"
import { LocationKey } from "@aelis/source-location"
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
import { join } from "node:path"
import { createInterface } from "node:readline/promises"
import { WeatherSource, Units } from "../src/weather-source"
import { Context } from "@aelis/core"
import { LocationKey } from "@aelis/source-location"
import { DefaultWeatherKitClient } from "../src/weatherkit"
import { WeatherSource, Units } from "../src/weather-source"
const SCRIPT_DIR = import.meta.dirname
const CACHE_DIR = join(SCRIPT_DIR, ".cache")

View File

@@ -4,12 +4,7 @@ import { Context } from "@aelis/core"
import { LocationKey } from "@aelis/source-location"
import { describe, expect, test } from "bun:test"
import type {
WeatherKitClient,
WeatherKitResponse,
HourlyForecast,
DailyForecast,
} from "./weatherkit"
import type { WeatherKitClient, WeatherKitResponse, HourlyForecast, DailyForecast } from "./weatherkit"
import fixture from "../fixtures/san-francisco.json"
import { WeatherFeedItemType, type DailyWeatherData, type HourlyWeatherData } from "./feed-items"

View File

@@ -3,12 +3,7 @@ import type { ActionDefinition, ContextEntry, FeedItemSignals, FeedSource } from
import { Context, TimeRelevance, UnknownActionError } from "@aelis/core"
import { LocationKey } from "@aelis/source-location"
import {
WeatherFeedItemType,
type DailyWeatherEntry,
type HourlyWeatherEntry,
type WeatherFeedItem,
} from "./feed-items"
import { WeatherFeedItemType, type DailyWeatherEntry, type HourlyWeatherEntry, type WeatherFeedItem } from "./feed-items"
import currentWeatherInsightPrompt from "./prompts/current-weather-insight.txt"
import { WeatherKey, type Weather } from "./weather-context"
import {

View File

@@ -1,5 +1,6 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
@@ -7,19 +8,20 @@
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"types": ["bun"],
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false