Compare commits

..

5 Commits

Author SHA1 Message Date
a0f67bbb41 feat(backend): bypass auth in development
Use mockAuthSessionMiddleware with a fully populated dev
user when NODE_ENV is not production. Auth handlers are
only registered in production.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-11 00:19:15 +00:00
863c298bd3 refactor: rename aris to aelis (#59)
Rename all references across the codebase: package names,
imports, source IDs, directory names, docs, and configs.

Co-authored-by: Ona <no-reply@ona.com>
2026-03-10 19:19:23 +00:00
230116d9f7 fix(waitlist): add delay before email to avoid rate limit (#61)
Co-authored-by: Ona <no-reply@ona.com>
2026-03-08 03:35:58 +00:00
0a08706cf9 feat: init waitlist website (#60)
* feat: init waitlist website

Co-authored-by: Ona <no-reply@ona.com>

* feat[waitlist]: tweak copy

Co-authored-by: Ona <no-reply@ona.com>

* fix[waitlist]: reminify lottie json

Co-authored-by: Ona <no-reply@ona.com>

* feat[waitlist]: seo and preview stuff

* chore[waitlist]: clean up

* build[waitlist]: add fly.io config

* feat(waitlist): add time-of-day greeting and duplicate email message

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): handle duplicate emails and send confirmation

Co-authored-by: Ona <no-reply@ona.com>

* chore: remove stray console.log

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add privacy policy page

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add footer with bottom progressive blur

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add trouble message and improve error handling

Co-authored-by: Ona <no-reply@ona.com>

* fix(waitlist): fix timeOfDay logic, typo, and add audienceId

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add .ico fallback favicon and style error page

Co-authored-by: Ona <no-reply@ona.com>

* chore(waitlist): add robots.txt, sitemap, clean dockerignore

Co-authored-by: Ona <no-reply@ona.com>

* feat(waitlist): add footer to privacy policy page

Co-authored-by: Ona <no-reply@ona.com>

* fix(waitlist): use segments instead of audienceId

Co-authored-by: Ona <no-reply@ona.com>

* fix[waitlist]: remove segmentId from dup check

Co-authored-by: Ona <no-reply@ona.com>

* fix(waitlist): reset logo animation on mouse leave

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
2026-03-08 02:54:56 +00:00
badc00c43b feat(backend): add LLM-powered feed enhancement (#58)
* feat(backend): add LLM-powered feed enhancement

Add enhancement harness that fills feed item slots and
generates synthetic items via OpenRouter.

- LLM client with 30s timeout, reusable SDK instance
- Prompt builder with mini calendar and week overview
- arktype schema validation + JSON Schema for structured output
- Pure merge function with clock injection
- Defensive fallback in feed endpoint on enhancement failure
- Skips LLM call when no unfilled slots or no API key

Co-authored-by: Ona <no-reply@ona.com>

* refactor: move feed enhancement into UserSession

Move enhancement logic from HTTP handler into UserSession so the
transport layer has no knowledge of enhancement. UserSession.feed()
handles refresh, enhancement, and caching in one place.

- UserSession subscribes to engine updates and re-enhances eagerly
- Enhancement cache tracks source identity to prevent stale results
- UserSessionManager accepts config object with optional enhancer
- HTTP handler simplified to just call session.feed()

Co-authored-by: Ona <no-reply@ona.com>

* test: add schema sync tests for arktype/JSON Schema drift

Validates reference payloads against both the arktype schema
(parseEnhancementResult) and the OpenRouter JSON Schema structure.
Catches field additions/removals or type changes in either schema.

Co-authored-by: Ona <no-reply@ona.com>

* refactor: rename arktype schemas to match types

Co-authored-by: Ona <no-reply@ona.com>

---------

Co-authored-by: Ona <no-reply@ona.com>
2026-03-05 02:01:30 +00:00
233 changed files with 3273 additions and 619 deletions

View File

@@ -1,8 +1,8 @@
services: services:
expo: expo:
name: Expo Dev Server name: Expo Dev Server
description: Expo development server for aris-client description: Expo development server for aelis-client
triggeredBy: triggeredBy:
- postDevcontainerStart - postDevcontainerStart
commands: commands:
start: cd apps/aris-client && ./scripts/run-dev-server.sh start: cd apps/aelis-client && ./scripts/run-dev-server.sh

View File

@@ -2,7 +2,7 @@
## Project ## Project
ARIS is an AI-powered personal assistant that aggregates data from various sources into a contextual feed. Monorepo with `packages/` (shared libraries) and `apps/` (applications). AELIS is an AI-powered personal assistant that aggregates data from various sources into a contextual feed. Monorepo with `packages/` (shared libraries) and `apps/` (applications).
## Commands ## Commands

View File

@@ -1,4 +1,4 @@
# aris # aelis
To install dependencies: To install dependencies:
@@ -8,14 +8,14 @@ bun install
## Packages ## Packages
### @aris/source-tfl ### @aelis/source-tfl
TfL (Transport for London) feed source for tube, overground, and Elizabeth line alerts. TfL (Transport for London) feed source for tube, overground, and Elizabeth line alerts.
#### Testing #### Testing
```bash ```bash
cd packages/aris-source-tfl cd packages/aelis-source-tfl
bun run test bun run test
``` ```

View File

@@ -1,5 +1,5 @@
{ {
"name": "@aris/backend", "name": "@aelis/backend",
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"main": "src/server.ts", "main": "src/server.ts",
@@ -9,12 +9,12 @@
"test": "bun test src/" "test": "bun test src/"
}, },
"dependencies": { "dependencies": {
"@aris/core": "workspace:*", "@aelis/core": "workspace:*",
"@aris/source-caldav": "workspace:*", "@aelis/source-caldav": "workspace:*",
"@aris/source-google-calendar": "workspace:*", "@aelis/source-google-calendar": "workspace:*",
"@aris/source-location": "workspace:*", "@aelis/source-location": "workspace:*",
"@aris/source-tfl": "workspace:*", "@aelis/source-tfl": "workspace:*",
"@aris/source-weatherkit": "workspace:*", "@aelis/source-weatherkit": "workspace:*",
"@openrouter/sdk": "^0.9.11", "@openrouter/sdk": "^0.9.11",
"arktype": "^2.1.29", "arktype": "^2.1.29",
"better-auth": "^1", "better-auth": "^1",

View File

@@ -61,7 +61,7 @@ export async function getSessionFromHeaders(
} }
/** /**
* Test-only middleware that injects a fake user and session. * Dev/test middleware that injects a fake user and session.
* Pass userId to simulate an authenticated request, or omit to get 401. * Pass userId to simulate an authenticated request, or omit to get 401.
*/ */
export function mockAuthSessionMiddleware(userId?: string): AuthSessionMiddleware { export function mockAuthSessionMiddleware(userId?: string): AuthSessionMiddleware {
@@ -69,8 +69,34 @@ export function mockAuthSessionMiddleware(userId?: string): AuthSessionMiddlewar
if (!userId) { if (!userId) {
return c.json({ error: "Unauthorized" }, 401) return c.json({ error: "Unauthorized" }, 401)
} }
c.set("user", { id: userId } as AuthUser)
c.set("session", { id: "mock-session" } as AuthSession) const now = new Date()
const expiresAt = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
const user: AuthUser = {
id: "k7Gx2mPqRvNwYs9TdLfA4bHcJeUo1iZn",
name: "Dev User",
email: "dev@aelis.local",
emailVerified: true,
image: null,
createdAt: now,
updatedAt: now,
}
const session: AuthSession = {
id: "Wt3FvBpXaQrMhD8sKjE6LcYn0gUz5iRo",
userId: "k7Gx2mPqRvNwYs9TdLfA4bHcJeUo1iZn",
token: "Vb9CxNfRm2KwQs7TjPeA5dLhYg0UoZi4",
expiresAt,
ipAddress: "127.0.0.1",
userAgent: "aelis-dev",
createdAt: now,
updatedAt: now,
}
c.set("user", user)
c.set("session", session)
await next() await next()
} }
} }

View File

@@ -1,4 +1,4 @@
import type { FeedItem } from "@aris/core" import type { FeedItem } from "@aelis/core"
import type { LlmClient } from "./llm-client.ts" import type { LlmClient } from "./llm-client.ts"

View File

@@ -1,4 +1,4 @@
import type { FeedItem } from "@aris/core" import type { FeedItem } from "@aelis/core"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"

View File

@@ -1,4 +1,4 @@
import type { FeedItem } from "@aris/core" import type { FeedItem } from "@aelis/core"
import type { EnhancementResult } from "./schema.ts" import type { EnhancementResult } from "./schema.ts"

View File

@@ -1,4 +1,4 @@
import type { FeedItem } from "@aris/core" import type { FeedItem } from "@aelis/core"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"

View File

@@ -1,7 +1,7 @@
import type { FeedItem } from "@aris/core" import type { FeedItem } from "@aelis/core"
import { CalDavFeedItemType } from "@aris/source-caldav" import { CalDavFeedItemType } from "@aelis/source-caldav"
import { CalendarFeedItemType } from "@aris/source-google-calendar" import { CalendarFeedItemType } from "@aelis/source-google-calendar"
import systemPromptBase from "./prompts/system.txt" import systemPromptBase from "./prompts/system.txt"

View File

@@ -1,4 +1,4 @@
You are ARIS, a personal assistant. You enhance a user's feed by filling slots and optionally generating synthetic items. You are AELIS, a personal assistant. You enhance a user's feed by filling slots and optionally generating synthetic items.
The user message is a JSON object with: The user message is a JSON object with:
- "items": feed items with data and named slots to fill. Each slot has a description of what to write. - "items": feed items with data and named slots to fill. Each slot has a description of what to write.

View File

@@ -1,4 +1,4 @@
import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@aris/core" import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@aelis/core"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { Hono } from "hono" import { Hono } from "hono"

View File

@@ -45,7 +45,7 @@ async function handleUpdateLocation(c: Context<Env>) {
const user = c.get("user")! const user = c.get("user")!
const sessionManager = c.get("sessionManager") const sessionManager = c.get("sessionManager")
const session = sessionManager.getOrCreate(user.id) const session = sessionManager.getOrCreate(user.id)
await session.engine.executeAction("aris.location", "update-location", { await session.engine.executeAction("aelis.location", "update-location", {
lat: result.lat, lat: result.lat,
lng: result.lng, lng: result.lng,
accuracy: result.accuracy, accuracy: result.accuracy,

View File

@@ -1,8 +1,8 @@
import { LocationSource } from "@aris/source-location" import { LocationSource } from "@aelis/source-location"
import { Hono } from "hono" import { Hono } from "hono"
import { registerAuthHandlers } from "./auth/http.ts" import { registerAuthHandlers } from "./auth/http.ts"
import { requireSession } from "./auth/session-middleware.ts" import { mockAuthSessionMiddleware, requireSession } from "./auth/session-middleware.ts"
import { createFeedEnhancer } from "./enhancement/enhance-feed.ts" import { createFeedEnhancer } from "./enhancement/enhance-feed.ts"
import { createLlmClient } from "./enhancement/llm-client.ts" import { createLlmClient } from "./enhancement/llm-client.ts"
import { registerFeedHttpHandlers } from "./feed/http.ts" import { registerFeedHttpHandlers } from "./feed/http.ts"
@@ -43,10 +43,16 @@ function main() {
app.get("/health", (c) => c.json({ status: "ok" })) app.get("/health", (c) => c.json({ status: "ok" }))
const isDev = process.env.NODE_ENV !== "production"
const authSessionMiddleware = isDev ? mockAuthSessionMiddleware("dev-user") : requireSession
if (!isDev) {
registerAuthHandlers(app) registerAuthHandlers(app)
}
registerFeedHttpHandlers(app, { registerFeedHttpHandlers(app, {
sessionManager, sessionManager,
authSessionMiddleware: requireSession, authSessionMiddleware,
}) })
registerLocationHttpHandlers(app, { sessionManager }) registerLocationHttpHandlers(app, { sessionManager })

View File

@@ -1,4 +1,4 @@
import type { FeedSource } from "@aris/core" import type { FeedSource } from "@aelis/core"
export interface FeedSourceProvider { export interface FeedSourceProvider {
feedSourceForUser(userId: string): FeedSource feedSourceForUser(userId: string): FeedSource

View File

@@ -1,6 +1,6 @@
import type { WeatherKitClient, WeatherKitResponse } from "@aris/source-weatherkit" import type { WeatherKitClient, WeatherKitResponse } from "@aelis/source-weatherkit"
import { LocationSource } from "@aris/source-location" import { LocationSource } from "@aelis/source-location"
import { describe, expect, mock, test } from "bun:test" import { describe, expect, mock, test } from "bun:test"
import { WeatherSourceProvider } from "../weather/provider.ts" import { WeatherSourceProvider } from "../weather/provider.ts"
@@ -44,8 +44,8 @@ describe("UserSessionManager", () => {
const session1 = manager.getOrCreate("user-1") const session1 = manager.getOrCreate("user-1")
const session2 = manager.getOrCreate("user-2") const session2 = manager.getOrCreate("user-2")
const source1 = session1.getSource<LocationSource>("aris.location") const source1 = session1.getSource<LocationSource>("aelis.location")
const source2 = session2.getSource<LocationSource>("aris.location") const source2 = session2.getSource<LocationSource>("aelis.location")
expect(source1).not.toBe(source2) expect(source1).not.toBe(source2)
}) })
@@ -83,7 +83,7 @@ describe("UserSessionManager", () => {
const session = manager.getOrCreate("user-1") const session = manager.getOrCreate("user-1")
expect(session.getSource("aris.weather")).toBeDefined() expect(session.getSource("aelis.weather")).toBeDefined()
}) })
test("accepts mixed providers", () => { test("accepts mixed providers", () => {
@@ -94,8 +94,8 @@ describe("UserSessionManager", () => {
const session = manager.getOrCreate("user-1") const session = manager.getOrCreate("user-1")
expect(session.getSource("aris.location")).toBeDefined() expect(session.getSource("aelis.location")).toBeDefined()
expect(session.getSource("aris.weather")).toBeDefined() expect(session.getSource("aelis.weather")).toBeDefined()
}) })
test("refresh returns feed result through session", async () => { test("refresh returns feed result through session", async () => {
@@ -114,14 +114,14 @@ describe("UserSessionManager", () => {
const manager = new UserSessionManager({ providers: [() => new LocationSource()] }) const manager = new UserSessionManager({ providers: [() => new LocationSource()] })
const session = manager.getOrCreate("user-1") const session = manager.getOrCreate("user-1")
await session.engine.executeAction("aris.location", "update-location", { await session.engine.executeAction("aelis.location", "update-location", {
lat: 51.5074, lat: 51.5074,
lng: -0.1278, lng: -0.1278,
accuracy: 10, accuracy: 10,
timestamp: new Date(), timestamp: new Date(),
}) })
const source = session.getSource<LocationSource>("aris.location") const source = session.getSource<LocationSource>("aelis.location")
expect(source?.lastLocation?.lat).toBe(51.5074) expect(source?.lastLocation?.lat).toBe(51.5074)
}) })
@@ -132,7 +132,7 @@ describe("UserSessionManager", () => {
const session = manager.getOrCreate("user-1") const session = manager.getOrCreate("user-1")
session.engine.subscribe(callback) session.engine.subscribe(callback)
await session.engine.executeAction("aris.location", "update-location", { await session.engine.executeAction("aelis.location", "update-location", {
lat: 51.5074, lat: 51.5074,
lng: -0.1278, lng: -0.1278,
accuracy: 10, accuracy: 10,
@@ -156,7 +156,7 @@ describe("UserSessionManager", () => {
// Create new session and push location — old callback should not fire // Create new session and push location — old callback should not fire
const session2 = manager.getOrCreate("user-1") const session2 = manager.getOrCreate("user-1")
await session2.engine.executeAction("aris.location", "update-location", { await session2.engine.executeAction("aelis.location", "update-location", {
lat: 51.5074, lat: 51.5074,
lng: -0.1278, lng: -0.1278,
accuracy: 10, accuracy: 10,

View File

@@ -1,6 +1,6 @@
import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@aris/core" import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@aelis/core"
import { LocationSource } from "@aris/source-location" import { LocationSource } from "@aelis/source-location"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { UserSession } from "./user-session.ts" import { UserSession } from "./user-session.ts"
@@ -36,7 +36,7 @@ describe("UserSession", () => {
const location = new LocationSource() const location = new LocationSource()
const session = new UserSession([location]) const session = new UserSession([location])
const result = session.getSource<LocationSource>("aris.location") const result = session.getSource<LocationSource>("aelis.location")
expect(result).toBe(location) expect(result).toBe(location)
}) })
@@ -59,7 +59,7 @@ describe("UserSession", () => {
const location = new LocationSource() const location = new LocationSource()
const session = new UserSession([location]) const session = new UserSession([location])
await session.engine.executeAction("aris.location", "update-location", { await session.engine.executeAction("aelis.location", "update-location", {
lat: 51.5, lat: 51.5,
lng: -0.1, lng: -0.1,
accuracy: 10, accuracy: 10,

View File

@@ -1,4 +1,4 @@
import { FeedEngine, type FeedItem, type FeedResult, type FeedSource } from "@aris/core" import { FeedEngine, type FeedItem, type FeedResult, type FeedSource } from "@aelis/core"
import type { FeedEnhancer } from "../enhancement/enhance-feed.ts" import type { FeedEnhancer } from "../enhancement/enhance-feed.ts"

View File

@@ -1,4 +1,4 @@
import { TflSource, type ITflApi } from "@aris/source-tfl" import { TflSource, type ITflApi } from "@aelis/source-tfl"
import type { FeedSourceProvider } from "../session/feed-source-provider.ts" import type { FeedSourceProvider } from "../session/feed-source-provider.ts"

View File

@@ -1,4 +1,4 @@
import { WeatherSource, type WeatherSourceOptions } from "@aris/source-weatherkit" import { WeatherSource, type WeatherSourceOptions } from "@aelis/source-weatherkit"
import type { FeedSourceProvider } from "../session/feed-source-provider.ts" import type { FeedSourceProvider } from "../session/feed-source-provider.ts"

View File

@@ -1,11 +1,11 @@
{ {
"expo": { "expo": {
"name": "Aris", "name": "Aelis",
"slug": "aris-client", "slug": "aelis-client",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "aris", "scheme": "aelis",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": true, "newArchEnabled": true,
"ios": { "ios": {
@@ -15,7 +15,7 @@
}, },
"ITSAppUsesNonExemptEncryption": false "ITSAppUsesNonExemptEncryption": false
}, },
"bundleIdentifier": "sh.nym.aris" "bundleIdentifier": "sh.nym.aelis"
}, },
"android": { "android": {
"adaptiveIcon": { "adaptiveIcon": {
@@ -26,7 +26,7 @@
}, },
"edgeToEdgeEnabled": true, "edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false, "predictiveBackGestureEnabled": false,
"package": "sh.nym.aris" "package": "sh.nym.aelis"
}, },
"web": { "web": {
"output": "static", "output": "static",

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 384 KiB

After

Width:  |  Height:  |  Size: 384 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,5 +1,5 @@
{ {
"name": "aris-client", "name": "aelis-client",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"main": "expo-router/entry", "main": "expo-router/entry",

Some files were not shown because too many files have changed in this diff Show More