Browse Source
Splits the SvelteKit app into three workspaces per docs/MONOREPO_MIGRATION_BRIEF.md: - packages/core (@webavatar/webavatar-core): session management, auth tokens, provider adapters, and the WS relay, with curated entry points (., ./client, ./server, ./vite) and a browser-stub guard on the server-only ones. - packages/humanoid (@webavatar/webavatar-humanoid): avatar-state derivation and animation-channel composition, depending on core only via type-only imports (no runtime reference to core in its built output). - apps/template: the SvelteKit application template, now importing both packages by name, routed through a server-only wrapper and a build-time guard (server-only-guard.ts) for the server/vite entries. Also collapses the duplicated server.js/vite-plugin composition wiring into a single createRealtimeRuntime factory (exported from packages/core/server), adds ESLint boundary-enforcement rules and Gitea Actions workflows (verify.yml, publish.yml), externalizes both packages in the production SSR build for artifact parity with a real client install, and rewrites the project documentation to match. No runtime behavior or wire protocol changes. Registry publication stays blocked on Gitea host/org placeholders not yet resolved — see the brief's status banner for exactly what remains. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>main
129 changed files with 1506 additions and 503 deletions
@ -0,0 +1,30 @@ |
|||||
|
name: publish |
||||
|
on: |
||||
|
push: |
||||
|
tags: ['core-v*', 'humanoid-v*'] |
||||
|
|
||||
|
jobs: |
||||
|
publish: |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- uses: actions/checkout@v4 |
||||
|
- uses: actions/setup-node@v4 |
||||
|
with: |
||||
|
node-version: 22 |
||||
|
registry-url: https://<gitea-host>/api/packages/<owner>/npm/ |
||||
|
scope: '@webavatar' |
||||
|
- run: npm ci |
||||
|
- run: npm run build:packages |
||||
|
- name: Resolve the workspace from the tag and check its version |
||||
|
run: | |
||||
|
case "$GITHUB_REF_NAME" in |
||||
|
core-v*) WS=packages/core; VERSION="${GITHUB_REF_NAME#core-v}" ;; |
||||
|
humanoid-v*) WS=packages/humanoid; VERSION="${GITHUB_REF_NAME#humanoid-v}" ;; |
||||
|
*) echo "unexpected tag $GITHUB_REF_NAME"; exit 1 ;; |
||||
|
esac |
||||
|
test "$(node -p "require('./$WS/package.json').version")" = "$VERSION" |
||||
|
echo "WS=$WS" >> "$GITHUB_ENV" |
||||
|
- run: npm test -w "$WS" |
||||
|
- run: npm publish -w "$WS" |
||||
|
env: |
||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} |
||||
@ -0,0 +1,29 @@ |
|||||
|
name: verify |
||||
|
on: |
||||
|
push: |
||||
|
pull_request: |
||||
|
|
||||
|
jobs: |
||||
|
verify: |
||||
|
# A runner (or container image) with Node 22+, git-lfs, and Chromium's system |
||||
|
# dependencies for the browser-mode Vitest project. |
||||
|
runs-on: ubuntu-latest |
||||
|
steps: |
||||
|
- uses: actions/checkout@v4 |
||||
|
with: |
||||
|
lfs: true |
||||
|
- uses: actions/setup-node@v4 |
||||
|
with: |
||||
|
node-version: 22 |
||||
|
cache: npm |
||||
|
- run: npm ci # no registry token on purpose — see "Versions, ranges, and the 0.x window" |
||||
|
- run: npx playwright install --with-deps chromium |
||||
|
- run: npm run lint |
||||
|
- run: npm run check |
||||
|
- run: npm test |
||||
|
- run: npm run build |
||||
|
- run: npm publish --dry-run -w packages/core -w packages/humanoid |
||||
|
- name: Humanoid ships no runtime reference to core |
||||
|
run: if grep -rl "webavatar-core" packages/humanoid/dist --include="*.js"; then exit 1; fi |
||||
|
- name: The SvelteKit server build imports core by name |
||||
|
run: grep -rl "@webavatar/webavatar-core/server" apps/template/build/server |
||||
@ -1 +1,2 @@ |
|||||
engine-strict=true |
engine-strict=true |
||||
|
@webavatar:registry=https://<gitea-host>/api/packages/<owner>/npm/ |
||||
|
|||||
@ -1,34 +1,47 @@ |
|||||
# webavatar-core |
# webavatar |
||||
|
|
||||
A base SvelteKit project meant to be branched off for future webavatar client projects. It provides the reusable core infrastructure those projects would otherwise reimplement from scratch — session management, auth tokens, and the realtime model connection — as a foundation to branch from, not a finished product. |
An npm-workspaces monorepo providing reusable core infrastructure for webavatar client projects — session management, auth tokens, and the realtime model connection — as independently-versioned packages, plus a SvelteKit template to branch client projects from. |
||||
|
|
||||
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full architecture map, and [AGENTS.md](AGENTS.md) for project conventions and structure. `docs/` holds a deep-dive per subsystem. |
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the full architecture map, [AGENTS.md](AGENTS.md) for project conventions, and `docs/` for a deep-dive per subsystem. |
||||
|
|
||||
|
## Workspaces |
||||
|
|
||||
|
| Workspace | What it is | |
||||
|
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
|
| [`packages/core`](packages/core) | `@webavatar/webavatar-core` — session management, auth tokens, provider adapters, the WS relay. Every client project needs this. | |
||||
|
| [`packages/humanoid`](packages/humanoid) | `@webavatar/webavatar-humanoid` — avatar-state derivation and animation-channel composition for 3D humanoid avatars. Optional — only projects building a humanoid avatar need it. | |
||||
|
| [`apps/template`](apps/template) | The SvelteKit application template client projects branch from. See its own README for the client-project bootstrap procedure. | |
||||
|
|
||||
|
Both packages are privately distributed (not published to the public npm registry) and ESM-only. |
||||
|
|
||||
## Getting started |
## Getting started |
||||
|
|
||||
```sh |
```sh |
||||
npm install |
npm install |
||||
cp .env.example .env |
cp apps/template/.env.example apps/template/.env |
||||
# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in .env |
# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in apps/template/.env |
||||
npm run dev |
npm run dev |
||||
``` |
``` |
||||
|
|
||||
Open [`/demo/realtime`](http://localhost:5173/demo/realtime) for the realtime session reference wiring, or [`/demo/humanoid`](http://localhost:5173/demo/humanoid) for the humanoid avatar-state demo (works offline via `REALTIME_PROVIDER=mock`, no API key needed). |
The root `npm run dev` builds both packages once, then watches them alongside the template app's own dev server — this is what makes package-source hot reload work (see [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md)'s "Dev-mode reload behavior"). Open [`/demo/realtime`](http://localhost:5173/demo/realtime) or [`/demo/humanoid`](http://localhost:5173/demo/humanoid) (works offline via `REALTIME_PROVIDER=mock`, no API key needed). |
||||
|
|
||||
## Scripts |
## Scripts |
||||
|
|
||||
- `npm run dev` / `npm run build` / `npm run preview` — standard SvelteKit commands. |
Run from the repo root, these orchestrate all three workspaces: |
||||
- `npm run check` — type-check. `npm run lint` / `npm run format` — prettier + eslint. |
|
||||
- `npm run test:unit` — vitest. `npm run test:e2e` — Playwright. |
- `npm run dev` / `npm run build` / `npm run check` / `npm test` — build the packages first, then run the equivalent command across every workspace. |
||||
- `npm start` — run the production build via `server.js` (the persistent Node process the realtime WebSocket relay requires — see [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)). |
- `npm run lint` / `npm run format` — prettier + eslint across the whole repo. |
||||
|
|
||||
|
Run a single workspace's own scripts with `-w`, e.g. `npm run dev -w apps/template`, or `npm run test -w packages/core`. |
||||
|
|
||||
## Documentation |
## Documentation |
||||
|
|
||||
| Subsystem | Doc | |
| Subsystem | Doc | |
||||
| -------------------------------------------------------- | ---------------------------------------------------- | |
| --------------------------------------------------------------------- | -------------------------------------------------------------------- | |
||||
| Architecture overview and module layering | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | |
| Architecture overview and module layering | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | |
||||
| Realtime session management | [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md) | |
| Realtime session management | [docs/REALTIME_SESSION.md](docs/REALTIME_SESSION.md) | |
||||
| Tool calling (server- and client-executed) | [docs/TOOLS.md](docs/TOOLS.md) | |
| Tool calling (server- and client-executed) | [docs/TOOLS.md](docs/TOOLS.md) | |
||||
| Analytics recording | [docs/ANALYTICS.md](docs/ANALYTICS.md) | |
| Analytics recording | [docs/ANALYTICS.md](docs/ANALYTICS.md) | |
||||
| Configuration (env vars, persona) | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | |
| Configuration (env vars, persona) | [docs/CONFIGURATION.md](docs/CONFIGURATION.md) | |
||||
| `humanoid` package (avatar-state, animation composition) | [docs/HUMANOID.md](docs/HUMANOID.md) | |
| `@webavatar/webavatar-humanoid` (avatar-state, animation composition) | [docs/HUMANOID.md](docs/HUMANOID.md) | |
||||
|
| The monorepo migration itself — rationale, decisions, verification | [docs/MONOREPO_MIGRATION_BRIEF.md](docs/MONOREPO_MIGRATION_BRIEF.md) | |
||||
|
|||||
@ -0,0 +1,49 @@ |
|||||
|
# webavatar-template |
||||
|
|
||||
|
The SvelteKit application template webavatar client projects branch from. It composes `@webavatar/webavatar-core` and `@webavatar/webavatar-humanoid` with project-owned persona, tools, and deployment configuration — see the monorepo root's [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) for the full architecture map, and [AGENTS.md](../../AGENTS.md) for project conventions. `docs/` holds a deep-dive per subsystem. |
||||
|
|
||||
|
## Getting started (inside this monorepo) |
||||
|
|
||||
|
```sh |
||||
|
npm install # from the monorepo root |
||||
|
cp apps/template/.env.example apps/template/.env |
||||
|
# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in apps/template/.env |
||||
|
npm run dev # from the monorepo root — builds the packages, then watches them alongside this app |
||||
|
``` |
||||
|
|
||||
|
Open [`/demo/realtime`](http://localhost:5173/demo/realtime) for the realtime session reference wiring, or [`/demo/humanoid`](http://localhost:5173/demo/humanoid) for the humanoid avatar-state demo (works offline via `REALTIME_PROVIDER=mock`, no API key needed). |
||||
|
|
||||
|
Running `npm run dev` from _this_ directory alone starts the app but does **not** hot-reload package source changes — see [docs/REALTIME_SESSION.md](../../docs/REALTIME_SESSION.md)'s "Dev-mode reload behavior" for why, and use the root `npm run dev` instead when iterating on `packages/core` or `packages/humanoid`. |
||||
|
|
||||
|
## Scripts |
||||
|
|
||||
|
- `npm run dev` / `npm run build` / `npm run preview` — standard SvelteKit commands. |
||||
|
- `npm run check` — type-check. `npm run lint` / `npm run format` — prettier + eslint. |
||||
|
- `npm run test:unit` — vitest. `npm run test:e2e` — Playwright. |
||||
|
- `npm start` — run the production build via `server.js` (the persistent Node process the realtime WebSocket relay requires — see [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md)). |
||||
|
|
||||
|
## Documentation |
||||
|
|
||||
|
| Subsystem | Doc | |
||||
|
| --------------------------------------------------------------------- | ---------------------------------------------------------- | |
||||
|
| Architecture overview and module layering | [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) | |
||||
|
| Realtime session management | [docs/REALTIME_SESSION.md](../../docs/REALTIME_SESSION.md) | |
||||
|
| Tool calling (server- and client-executed) | [docs/TOOLS.md](../../docs/TOOLS.md) | |
||||
|
| Analytics recording | [docs/ANALYTICS.md](../../docs/ANALYTICS.md) | |
||||
|
| Configuration (env vars, persona) | [docs/CONFIGURATION.md](../../docs/CONFIGURATION.md) | |
||||
|
| `@webavatar/webavatar-humanoid` (avatar-state, animation composition) | [docs/HUMANOID.md](../../docs/HUMANOID.md) | |
||||
|
|
||||
|
## Starting a client project from this template |
||||
|
|
||||
|
The packages (`@webavatar/webavatar-core`, `@webavatar/webavatar-humanoid`) are what a client project upgrades; this template is what it starts from. A project installs versions of the packages — it does not merge package source from this repository. |
||||
|
|
||||
|
1. Copy this directory (`apps/template`) into a new repository. Do not copy the monorepo root, `packages/`, or the root lockfile. |
||||
|
2. The copied `package.json` becomes the project's own root manifest. It stays `"private": true`. Its `@webavatar/*` ranges no longer resolve to workspace links, so pin them to published versions. |
||||
|
3. Add the committed `.npmrc` scope mapping (`@webavatar:registry=...`), configure registry authentication, and run `npm install`. |
||||
|
4. Copy `.env.example` to `.env`, fill in the secrets, and run `npm run dev`. The demo routes stay dev-only gated and can be kept or deleted. |
||||
|
5. Project code goes under `src/lib/**/project`, exactly as in this template. |
||||
|
6. To upgrade, bump the package versions; a peer-range conflict between core and humanoid surfaces as an `npm install` error — that's the intended signal, not a bug. |
||||
|
|
||||
|
Changes to this template shell itself (`server.js`, `vite.config.ts`, routes, deployment files) don't arrive through package upgrades; a project applies them by hand. The composition-root factory (`createRealtimeRuntime`, from `@webavatar/webavatar-core/server`) is what keeps the shell thin enough that this stays rare — a change in how the relay is assembled ships inside a core release instead. |
||||
|
|
||||
|
**Current status:** steps 3 and 6 (installing from the registry, and the peer-range upgrade signal) require the private Gitea package registry, which isn't provisioned yet — see `docs/MONOREPO_MIGRATION_BRIEF.md`'s "Decisions required before handoff". Steps 1, 2, 4, and 5 work today; the packages themselves have been verified installable from packed tarballs in place of the registry (see the migration brief's verification requirements). |
||||
@ -0,0 +1,49 @@ |
|||||
|
{ |
||||
|
"name": "webavatar-template", |
||||
|
"private": true, |
||||
|
"version": "0.0.1", |
||||
|
"type": "module", |
||||
|
"scripts": { |
||||
|
"dev": "vite dev", |
||||
|
"build": "vite build && tsc -p tsconfig.server.json", |
||||
|
"preview": "vite preview", |
||||
|
"start": "node server.js", |
||||
|
"prepare": "svelte-kit sync || echo ''", |
||||
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", |
||||
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", |
||||
|
"lint": "prettier --check . && eslint .", |
||||
|
"format": "prettier --write .", |
||||
|
"test:unit": "vitest", |
||||
|
"test": "npm run test:unit -- --run && npm run test:e2e", |
||||
|
"test:e2e": "playwright install && playwright test" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@eslint/js": "^10.0.1", |
||||
|
"@playwright/test": "^1.60.0", |
||||
|
"@sveltejs/adapter-node": "^5.5.7", |
||||
|
"@sveltejs/kit": "^2.63.0", |
||||
|
"@sveltejs/vite-plugin-svelte": "^7.1.2", |
||||
|
"@types/node": "^24", |
||||
|
"@types/three": "^0.185.4", |
||||
|
"@vitest/browser-playwright": "^4.1.8", |
||||
|
"eslint": "^10.4.1", |
||||
|
"eslint-config-prettier": "^10.1.8", |
||||
|
"eslint-plugin-svelte": "^3.19.0", |
||||
|
"globals": "^17.6.0", |
||||
|
"playwright": "^1.60.0", |
||||
|
"prettier": "^3.8.3", |
||||
|
"prettier-plugin-svelte": "^4.1.0", |
||||
|
"svelte": "^5.56.1", |
||||
|
"svelte-check": "^4.6.0", |
||||
|
"three": "^0.185.1", |
||||
|
"typescript": "^6.0.3", |
||||
|
"typescript-eslint": "^8.60.1", |
||||
|
"vite": "^8.0.16", |
||||
|
"vitest": "^4.1.8", |
||||
|
"vitest-browser-svelte": "^2.1.1" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"@webavatar/webavatar-core": "^0.1.0", |
||||
|
"@webavatar/webavatar-humanoid": "^0.1.0" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
import type { Plugin } from 'vite'; |
||||
|
|
||||
|
const SERVER_ONLY = /^@webavatar\/webavatar-core\/(server|vite)(\/|$)/; |
||||
|
|
||||
|
/** |
||||
|
* Build-time backstop for the server-only boundary: SvelteKit's own |
||||
|
* `$lib/server` guard is path-based and doesn't reach into `node_modules`, so |
||||
|
* once core's server code is a package this is what makes a client-side |
||||
|
* import of `@webavatar/webavatar-core/server` (or `/vite`) fail the build |
||||
|
* rather than only failing at runtime via the package's `browser` stub. See |
||||
|
* docs/ARCHITECTURE.md's "Server-only safety" section. |
||||
|
*/ |
||||
|
export function serverOnlyGuard(): Plugin { |
||||
|
return { |
||||
|
name: 'webavatar-server-only-guard', |
||||
|
enforce: 'pre', |
||||
|
resolveId(id, importer, options) { |
||||
|
const isClient = this.environment ? this.environment.name === 'client' : !options?.ssr; |
||||
|
if (isClient && SERVER_ONLY.test(id)) { |
||||
|
throw new Error( |
||||
|
`${id} is server-only; import it through $lib/server instead (importer: ${importer}).` |
||||
|
); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// Custom production server: wraps adapter-node's generated request handler in
|
||||
|
// a plain http.Server so we can also intercept WebSocket upgrade requests,
|
||||
|
// which SvelteKit itself has no hook for (see @webavatar/webavatar-core's
|
||||
|
// server/ws-handler.ts).
|
||||
|
//
|
||||
|
// `.env` is not auto-loaded here (SvelteKit's own adapter-node doesn't in
|
||||
|
// production either) — run with `node --env-file=.env server.js` locally, or
|
||||
|
// provide OPENAI_API_KEY / REALTIME_TOKEN_SECRET via your deployment's real
|
||||
|
// secret mechanism.
|
||||
|
import { createServer } from 'node:http'; |
||||
|
import { createRealtimeRuntime } from '@webavatar/webavatar-core/server'; |
||||
|
import { handler } from './build/handler.js'; |
||||
|
import { SESSION_PERSONA } from './build-server/project/persona.js'; |
||||
|
import { toolRegistry } from './build-server/server/project/tools/index.js'; |
||||
|
|
||||
|
const runtime = createRealtimeRuntime({ persona: SESSION_PERSONA, toolRegistry, isDev: false }); |
||||
|
const server = createServer(handler); |
||||
|
|
||||
|
server.on('upgrade', runtime.handleUpgrade); |
||||
|
|
||||
|
server.listen(runtime.config.port, () => { |
||||
|
console.log(`listening on port ${runtime.config.port}`); |
||||
|
}); |
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@ -1,12 +1,17 @@ |
|||||
<script lang="ts"> |
<script lang="ts"> |
||||
import type { LogLevel } from '../../core/logger.js'; |
import type { LogLevel } from '@webavatar/webavatar-core'; |
||||
import { clearLogs, debugLogStore, type DisplayLogEntry } from './logger.js'; |
import { |
||||
import type { ServerLogForwarder, ServerLogForwarderState } from './server-log-forwarder.js'; |
clearLogs, |
||||
|
debugLogStore, |
||||
|
type DisplayLogEntry, |
||||
|
type ServerLogForwarder, |
||||
|
type ServerLogForwarderState |
||||
|
} from '@webavatar/webavatar-core/client'; |
||||
|
|
||||
/** |
/** |
||||
* Production-shippable debug panel — lives in `$lib/client/core` (not |
* Production-shippable debug panel — lives in `$lib/client` (not `/demo`, |
||||
* `/demo`, which is dev-only-gated scaffolding) since the key-based auth |
* which is dev-only-gated scaffolding) since the key-based auth on |
||||
* on `forwarder`, not route removal, is what protects it in prod. See |
* `forwarder`, not route removal, is what protects it in prod. See |
||||
* docs/LOGGING.md. |
* docs/LOGGING.md. |
||||
* |
* |
||||
* `forwarder` is optional: with none given (or on a page with no realtime |
* `forwarder` is optional: with none given (or on a page with no realtime |
||||
@ -1,6 +1,6 @@ |
|||||
import { beforeEach, describe, expect, it } from 'vitest'; |
import { beforeEach, describe, expect, it } from 'vitest'; |
||||
import { render } from 'vitest-browser-svelte'; |
import { render } from 'vitest-browser-svelte'; |
||||
import { clearLogs, clientLogger } from './logger.js'; |
import { clearLogs, clientLogger } from '@webavatar/webavatar-core/client'; |
||||
import DebugPanel from './DebugPanel.svelte'; |
import DebugPanel from './DebugPanel.svelte'; |
||||
|
|
||||
beforeEach(() => { |
beforeEach(() => { |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ClientToolHandler } from '../../core/tool-dispatcher.js'; |
import type { ClientToolHandler } from '@webavatar/webavatar-core/client'; |
||||
|
|
||||
/** Briefly flashes the page background — proves the client tool relay round-trip visibly. */ |
/** Briefly flashes the page background — proves the client tool relay round-trip visibly. */ |
||||
export const flashPage: ClientToolHandler = async () => { |
export const flashPage: ClientToolHandler = async () => { |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ClientToolHandler } from '../../core/tool-dispatcher.js'; |
import type { ClientToolHandler } from '@webavatar/webavatar-core/client'; |
||||
import { flashPage } from './flash-page.js'; |
import { flashPage } from './flash-page.js'; |
||||
|
|
||||
/** This project's client-side tool handlers, keyed by tool name. */ |
/** This project's client-side tool handlers, keyed by tool name. */ |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ToolDefinition } from '../core/tools.js'; |
import type { ToolDefinition } from '@webavatar/webavatar-core'; |
||||
|
|
||||
/** |
/** |
||||
* Project-owned tool definitions — isomorphic (needed by the server at |
* Project-owned tool definitions — isomorphic (needed by the server at |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ToolExecutor } from '../../core/tools.js'; |
import type { ToolExecutor } from '../../webavatar-core.js'; |
||||
|
|
||||
export const getCurrentTime: ToolExecutor = async () => { |
export const getCurrentTime: ToolExecutor = async () => { |
||||
return { iso: new Date().toISOString() }; |
return { iso: new Date().toISOString() }; |
||||
@ -1,4 +1,4 @@ |
|||||
import { createToolRegistry } from '../../core/tools.js'; |
import { createToolRegistry } from '../../webavatar-core.js'; |
||||
import { TOOL_DEFINITIONS } from '../../../project/tools.js'; |
import { TOOL_DEFINITIONS } from '../../../project/tools.js'; |
||||
import { getCurrentTime } from './get-current-time.js'; |
import { getCurrentTime } from './get-current-time.js'; |
||||
|
|
||||
@ -0,0 +1 @@ |
|||||
|
export * from '@webavatar/webavatar-core/server'; |
||||
@ -1,6 +1,5 @@ |
|||||
import { json } from '@sveltejs/kit'; |
import { json } from '@sveltejs/kit'; |
||||
import { loadConfig } from '$lib/server/core/config.js'; |
import { loadConfig, mintSessionToken } from '$lib/server/webavatar-core.js'; |
||||
import { mintSessionToken } from '$lib/server/core/tokens.js'; |
|
||||
import type { RequestHandler } from './$types'; |
import type { RequestHandler } from './$types'; |
||||
|
|
||||
export const POST: RequestHandler = () => { |
export const POST: RequestHandler = () => { |
||||
@ -1,7 +1,6 @@ |
|||||
<script lang="ts"> |
<script lang="ts"> |
||||
import type { AvatarState } from '$lib/humanoid/state.js'; |
import type { AvatarState, AnimationFrame } from '@webavatar/webavatar-humanoid'; |
||||
import type { AnimationFrame } from '$lib/humanoid/animation.js'; |
import type { EyeGazeState } from '@webavatar/webavatar-humanoid/eye'; |
||||
import type { EyeGazeState } from '$lib/humanoid/eye/gaze.js'; |
|
||||
|
|
||||
interface Vec2 { |
interface Vec2 { |
||||
x: number; |
x: number; |
||||
@ -1,15 +1,25 @@ |
|||||
{ |
{ |
||||
"extends": "./.svelte-kit/tsconfig.json", |
"extends": "./.svelte-kit/tsconfig.json", |
||||
"compilerOptions": { |
"compilerOptions": { |
||||
|
// Inlined rather than extending the monorepo's tsconfig.base.json: this |
||||
|
// app is meant to be copied out and started as a standalone client |
||||
|
// project (see "Starting a client project from the template" in the |
||||
|
// template README), which explicitly does not bring the monorepo root |
||||
|
// along — a relative `extends` up into it would break the moment it's |
||||
|
// copied. packages/core and packages/humanoid don't have this |
||||
|
// constraint (they're consumed via npm, never copied) and keep |
||||
|
// extending tsconfig.base.json normally. |
||||
|
"strict": true, |
||||
|
"esModuleInterop": true, |
||||
|
"forceConsistentCasingInFileNames": true, |
||||
|
"skipLibCheck": true, |
||||
|
"isolatedModules": true, |
||||
|
"verbatimModuleSyntax": true, |
||||
"rewriteRelativeImportExtensions": true, |
"rewriteRelativeImportExtensions": true, |
||||
"allowJs": true, |
"allowJs": true, |
||||
"checkJs": true, |
"checkJs": true, |
||||
"esModuleInterop": true, |
|
||||
"forceConsistentCasingInFileNames": true, |
|
||||
"resolveJsonModule": true, |
"resolveJsonModule": true, |
||||
"skipLibCheck": true, |
|
||||
"sourceMap": true, |
"sourceMap": true, |
||||
"strict": true, |
|
||||
"moduleResolution": "bundler" |
"moduleResolution": "bundler" |
||||
} |
} |
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias |
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias |
||||
@ -1,51 +1,33 @@ |
|||||
{ |
{ |
||||
"name": "webavatar-base", |
"name": "webavatar-monorepo", |
||||
"private": true, |
"private": true, |
||||
"version": "0.0.1", |
|
||||
"type": "module", |
"type": "module", |
||||
|
"license": "UNLICENSED", |
||||
|
"engines": { |
||||
|
"node": ">=22.12" |
||||
|
}, |
||||
|
"workspaces": [ |
||||
|
"packages/*", |
||||
|
"apps/*" |
||||
|
], |
||||
"scripts": { |
"scripts": { |
||||
"dev": "vite dev", |
"build:packages": "npm run build -w packages/core && npm run build -w packages/humanoid", |
||||
"build": "vite build && tsc -p tsconfig.server.json", |
"build": "npm run build:packages && npm run build -w apps/template", |
||||
"preview": "vite preview", |
"dev": "npm run build:packages && concurrently \"npm run watch -w packages/core\" \"npm run watch -w packages/humanoid\" \"npm run dev -w apps/template\"", |
||||
"start": "node server.js", |
"check": "npm run build:packages && npm run check --workspaces --if-present", |
||||
"prepare": "svelte-kit sync || echo ''", |
"test": "npm run build:packages && npm run test -w packages/core -w packages/humanoid && npm run test:unit -w apps/template -- --run", |
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", |
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", |
|
||||
"lint": "prettier --check . && eslint .", |
"lint": "prettier --check . && eslint .", |
||||
"format": "prettier --write .", |
"format": "prettier --write ." |
||||
"test:unit": "vitest", |
|
||||
"test": "npm run test:unit -- --run && npm run test:e2e", |
|
||||
"test:e2e": "playwright install && playwright test" |
|
||||
}, |
}, |
||||
"devDependencies": { |
"devDependencies": { |
||||
"@eslint/js": "^10.0.1", |
"@eslint/js": "^10.0.1", |
||||
"@playwright/test": "^1.60.0", |
"concurrently": "^9.1.0", |
||||
"@sveltejs/adapter-node": "^5.5.7", |
|
||||
"@sveltejs/kit": "^2.63.0", |
|
||||
"@sveltejs/vite-plugin-svelte": "^7.1.2", |
|
||||
"@types/better-sqlite3": "^9.6.0", |
|
||||
"@types/node": "^24", |
|
||||
"@types/three": "^0.185.4", |
|
||||
"@types/ws": "^8.18.1", |
|
||||
"@vitest/browser-playwright": "^4.1.8", |
|
||||
"eslint": "^10.4.1", |
"eslint": "^10.4.1", |
||||
"eslint-config-prettier": "^10.1.8", |
"eslint-config-prettier": "^10.1.8", |
||||
"eslint-plugin-svelte": "^3.19.0", |
"eslint-plugin-svelte": "^3.19.0", |
||||
"globals": "^17.6.0", |
"globals": "^17.6.0", |
||||
"playwright": "^1.60.0", |
|
||||
"prettier": "^3.8.3", |
"prettier": "^3.8.3", |
||||
"prettier-plugin-svelte": "^4.1.0", |
"prettier-plugin-svelte": "^4.1.0", |
||||
"svelte": "^5.56.1", |
"typescript-eslint": "^8.60.1" |
||||
"svelte-check": "^4.6.0", |
|
||||
"three": "^0.185.1", |
|
||||
"typescript": "^6.0.3", |
|
||||
"typescript-eslint": "^8.60.1", |
|
||||
"vite": "^8.0.16", |
|
||||
"vitest": "^4.1.8", |
|
||||
"vitest-browser-svelte": "^2.1.1" |
|
||||
}, |
|
||||
"dependencies": { |
|
||||
"better-sqlite3": "^13.0.3", |
|
||||
"ws": "^8.21.3" |
|
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,40 @@ |
|||||
|
# @webavatar/webavatar-core |
||||
|
|
||||
|
Reusable realtime-session infrastructure for webavatar client projects — session management, auth tokens, provider adapters, and the WebSocket relay — so a client project doesn't reimplement the basics from scratch. Privately distributed (not published to the public npm registry); see the monorepo root's `docs/ARCHITECTURE.md` for the full picture and `docs/REALTIME_SESSION.md` for how the session layer works. |
||||
|
|
||||
|
**ESM-only.** No CommonJS build; `require()` is unsupported by design. |
||||
|
|
||||
|
## Entry points |
||||
|
|
||||
|
| Entry | Contains | |
||||
|
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
||||
|
| `@webavatar/webavatar-core` | Isomorphic contracts: the `ClientEvent`/`ServerEvent` protocol, `Logger`/`LogEntry`/`LogLevel`/`NOOP_LOGGER`, `ToolDefinition`/`formatToolError`, `SessionPersona`. Safe for both browser and server code. | |
||||
|
| `@webavatar/webavatar-core/client` | Browser transports and wrappers: `RealtimeClient`, `ResilientRealtimeClient`, client-side logging (`clientLogger`, `debugLogStore`), `createToolDispatcher`. Transport only, no business logic. | |
||||
|
| `@webavatar/webavatar-core/server` | Server-only implementations: `loadConfig`, `selectProvider`, the analytics recorders, `resolveSessionDefaults`, `handleRealtimeUpgrade`, token mint/verify, `createToolRegistry`, and the `createRealtimeRuntime` composition-root factory. Resolves to a throwing stub in any consumer's browser bundle. | |
||||
|
| `@webavatar/webavatar-core/vite` | `realtimeDevWsPlugin` — the Vite dev-server integration that wires the same WS upgrade handling into `npm run dev`. Kept separate from `./server` so importing the package root never pulls in Vite. | |
||||
|
|
||||
|
## The composition-root factory |
||||
|
|
||||
|
Both a production entrypoint and a Vite dev-server plugin need the same wiring — load config, pick a provider, choose an analytics recorder, merge session defaults with your persona and tool registry: |
||||
|
|
||||
|
```ts |
||||
|
import { createRealtimeRuntime } from '@webavatar/webavatar-core/server'; |
||||
|
|
||||
|
const runtime = createRealtimeRuntime({ persona, toolRegistry, isDev: false }); |
||||
|
server.on('upgrade', runtime.handleUpgrade); |
||||
|
server.listen(runtime.config.port); |
||||
|
``` |
||||
|
|
||||
|
`isDev` is the only thing that differs per entrypoint. See `docs/ARCHITECTURE.md`'s "Composition roots" for why this exists — it's what keeps a consuming app's `server.js`/`vite.config.ts` thin. |
||||
|
|
||||
|
## Server-only safety |
||||
|
|
||||
|
`./server` and `./vite` map to a stub that throws when evaluated in a browser bundle (this package's `browser` export condition), for every consumer, automatically. A consuming SvelteKit app should still route through its own `$lib/server` wrapper and a build-time guard for defense in depth — see `docs/ARCHITECTURE.md`'s "Server-only safety" section for the full layered approach the template app uses. |
||||
|
|
||||
|
## Compatibility |
||||
|
|
||||
|
Caret-ranged (`^0.1.0`) during the `0.x` window — a core minor is a coordinated release with `@webavatar/webavatar-humanoid`'s peer range. See the monorepo root's `docs/MONOREPO_MIGRATION_BRIEF.md` ("Versions, ranges, and the `0.x` window") for the full policy. |
||||
|
|
||||
|
## Peer dependencies |
||||
|
|
||||
|
`vite` is an **optional** peer — only needed if you import `./vite`. Consumers that never touch that entry point don't need Vite installed at all. |
||||
@ -0,0 +1,60 @@ |
|||||
|
{ |
||||
|
"name": "@webavatar/webavatar-core", |
||||
|
"version": "0.1.0", |
||||
|
"license": "UNLICENSED", |
||||
|
"type": "module", |
||||
|
"engines": { |
||||
|
"node": ">=22.12" |
||||
|
}, |
||||
|
"files": [ |
||||
|
"dist" |
||||
|
], |
||||
|
"exports": { |
||||
|
".": { |
||||
|
"types": "./dist/index.d.ts", |
||||
|
"import": "./dist/index.js" |
||||
|
}, |
||||
|
"./client": { |
||||
|
"types": "./dist/client/index.d.ts", |
||||
|
"import": "./dist/client/index.js" |
||||
|
}, |
||||
|
"./server": { |
||||
|
"types": "./dist/server/index.d.ts", |
||||
|
"browser": "./dist/server/browser-stub.js", |
||||
|
"import": "./dist/server/index.js" |
||||
|
}, |
||||
|
"./vite": { |
||||
|
"types": "./dist/vite/index.d.ts", |
||||
|
"browser": "./dist/server/browser-stub.js", |
||||
|
"import": "./dist/vite/index.js" |
||||
|
} |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"better-sqlite3": "^13.0.3", |
||||
|
"ws": "^8.21.3", |
||||
|
"@types/better-sqlite3": "^9.6.0", |
||||
|
"@types/ws": "^8.18.1" |
||||
|
}, |
||||
|
"peerDependencies": { |
||||
|
"vite": "^8.0.0" |
||||
|
}, |
||||
|
"peerDependenciesMeta": { |
||||
|
"vite": { |
||||
|
"optional": true |
||||
|
} |
||||
|
}, |
||||
|
"publishConfig": { |
||||
|
"registry": "https://<gitea-host>/api/packages/<owner>/npm/" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/node": "^24", |
||||
|
"typescript": "^6.0.3", |
||||
|
"vitest": "^4.1.8" |
||||
|
}, |
||||
|
"scripts": { |
||||
|
"build": "tsc -p tsconfig.build.json", |
||||
|
"watch": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", |
||||
|
"check": "tsc -p tsconfig.json --noEmit", |
||||
|
"test": "vitest run" |
||||
|
} |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
import { REALTIME_WS_PATH, type ClientEvent, type ServerEvent } from '../../core/protocol.js'; |
import { REALTIME_WS_PATH, type ClientEvent, type ServerEvent } from '../protocol.js'; |
||||
|
|
||||
export interface RealtimeClientOptions { |
export interface RealtimeClientOptions { |
||||
/** Origin to connect to. Defaults to the current page's origin. */ |
/** Origin to connect to. Defaults to the current page's origin. */ |
||||
@ -0,0 +1,22 @@ |
|||||
|
export { RealtimeClient } from './client.js'; |
||||
|
export type { RealtimeClientOptions } from './client.js'; |
||||
|
|
||||
|
export { ResilientRealtimeClient } from './resilient-client.js'; |
||||
|
export type { |
||||
|
ConnectionState, |
||||
|
RealtimeTransport, |
||||
|
ResilientRealtimeClientOptions |
||||
|
} from './resilient-client.js'; |
||||
|
|
||||
|
export { clientLogger, debugLogStore, pushLogEntry, clearLogs } from './logger.js'; |
||||
|
export type { DisplayLogEntry } from './logger.js'; |
||||
|
|
||||
|
export { attachServerLogForwarding } from './server-log-forwarder.js'; |
||||
|
export type { |
||||
|
ServerLogForwarder, |
||||
|
ServerLogForwarderState, |
||||
|
ServerLogForwardingClient |
||||
|
} from './server-log-forwarder.js'; |
||||
|
|
||||
|
export { createToolDispatcher } from './tool-dispatcher.js'; |
||||
|
export type { ToolDispatchClient, ClientToolHandler } from './tool-dispatcher.js'; |
||||
@ -1,4 +1,4 @@ |
|||||
import type { LogEntry, LogLevel, Logger } from '../../core/logger.js'; |
import type { LogEntry, LogLevel, Logger } from '../logger.js'; |
||||
|
|
||||
/** A `LogEntry` tagged with where it came from, for display in `DebugPanel.svelte`. */ |
/** A `LogEntry` tagged with where it came from, for display in `DebugPanel.svelte`. */ |
||||
export interface DisplayLogEntry extends LogEntry { |
export interface DisplayLogEntry extends LogEntry { |
||||
@ -1,5 +1,5 @@ |
|||||
import { describe, expect, it, vi } from 'vitest'; |
import { describe, expect, it, vi } from 'vitest'; |
||||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
||||
import { ResilientRealtimeClient, type RealtimeTransport } from './resilient-client.js'; |
import { ResilientRealtimeClient, type RealtimeTransport } from './resilient-client.js'; |
||||
|
|
||||
/** Minimal stand-in for RealtimeClient — only what ResilientRealtimeClient touches. */ |
/** Minimal stand-in for RealtimeClient — only what ResilientRealtimeClient touches. */ |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
||||
import { RealtimeClient, type RealtimeClientOptions } from './client.js'; |
import { RealtimeClient, type RealtimeClientOptions } from './client.js'; |
||||
|
|
||||
export type ConnectionState = 'connecting' | 'open' | 'reconnecting' | 'closed'; |
export type ConnectionState = 'connecting' | 'open' | 'reconnecting' | 'closed'; |
||||
@ -1,5 +1,5 @@ |
|||||
import { beforeEach, describe, expect, it } from 'vitest'; |
import { beforeEach, describe, expect, it } from 'vitest'; |
||||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
||||
import { clearLogs, debugLogStore } from './logger.js'; |
import { clearLogs, debugLogStore } from './logger.js'; |
||||
import { |
import { |
||||
attachServerLogForwarding, |
attachServerLogForwarding, |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
||||
import { pushLogEntry } from './logger.js'; |
import { pushLogEntry } from './logger.js'; |
||||
|
|
||||
/** |
/** |
||||
@ -1,5 +1,5 @@ |
|||||
import { describe, expect, it, vi } from 'vitest'; |
import { describe, expect, it, vi } from 'vitest'; |
||||
import type { ServerEvent } from '../../core/protocol.js'; |
import type { ServerEvent } from '../protocol.js'; |
||||
import type { RealtimeClient } from './client.js'; |
import type { RealtimeClient } from './client.js'; |
||||
import { createToolDispatcher } from './tool-dispatcher.js'; |
import { createToolDispatcher } from './tool-dispatcher.js'; |
||||
|
|
||||
@ -1,5 +1,5 @@ |
|||||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
||||
import { formatToolError } from '../../core/tools.js'; |
import { formatToolError } from '../tools.js'; |
||||
|
|
||||
export type ClientToolHandler = (args: Record<string, unknown>) => Promise<unknown>; |
export type ClientToolHandler = (args: Record<string, unknown>) => Promise<unknown>; |
||||
|
|
||||
@ -0,0 +1,16 @@ |
|||||
|
export type { |
||||
|
SessionStartConfig, |
||||
|
ClientEvent, |
||||
|
BrowserClientEvent, |
||||
|
RealtimeErrorCode, |
||||
|
ServerEvent |
||||
|
} from './protocol.js'; |
||||
|
export { REALTIME_WS_PATH, isBrowserClientEvent } from './protocol.js'; |
||||
|
|
||||
|
export type { LogLevel, LogEntry, Logger } from './logger.js'; |
||||
|
export { NOOP_LOGGER } from './logger.js'; |
||||
|
|
||||
|
export type { ToolDefinition } from './tools.js'; |
||||
|
export { formatToolError } from './tools.js'; |
||||
|
|
||||
|
export type { SessionPersona } from './persona.js'; |
||||
@ -0,0 +1,11 @@ |
|||||
|
/** |
||||
|
* Portable session-persona contract. A branched project supplies a concrete |
||||
|
* value satisfying this interface (see `apps/template/src/lib/project/persona.ts`) |
||||
|
* for `resolveSessionDefaults` to take as a parameter — core never imports a |
||||
|
* project's own persona value, only this contract (see docs/TOOLS.md for the |
||||
|
* same core/project ownership split applied to tools). |
||||
|
*/ |
||||
|
export interface SessionPersona { |
||||
|
model: string; |
||||
|
instructions: string; |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
throw new Error( |
||||
|
'@webavatar/webavatar-core/server is server-only and was imported into browser code.' |
||||
|
); |
||||
@ -1,4 +1,4 @@ |
|||||
import type { SessionPersona } from '../../project/persona.js'; |
import type { SessionPersona } from '../persona.js'; |
||||
import type { SessionDefaults } from './session.js'; |
import type { SessionDefaults } from './session.js'; |
||||
|
|
||||
/** |
/** |
||||
@ -0,0 +1,96 @@ |
|||||
|
import type { IncomingMessage } from 'node:http'; |
||||
|
import type { Duplex } from 'node:stream'; |
||||
|
import type { SessionPersona } from '../persona.js'; |
||||
|
import { |
||||
|
NoopAnalyticsRecorder, |
||||
|
SqliteAnalyticsRecorder, |
||||
|
type AnalyticsRecorder |
||||
|
} from './analytics.js'; |
||||
|
import { loadConfig } from './config.js'; |
||||
|
import { resolveSessionDefaults } from './defaults.js'; |
||||
|
import { handleRealtimeUpgrade } from './ws-handler.js'; |
||||
|
import { selectProvider } from './providers/index.js'; |
||||
|
import type { RealtimeProvider } from './provider.js'; |
||||
|
import type { SessionDefaults } from './session.js'; |
||||
|
import type { ToolRegistry } from './tools.js'; |
||||
|
import type { AppConfig } from './config.js'; |
||||
|
|
||||
|
export type { AppConfig, AnalyticsConfig, SessionRuntimeConfig, DebugConfig } from './config.js'; |
||||
|
export { loadConfig } from './config.js'; |
||||
|
|
||||
|
export type { AnalyticsCategoryConfig, AnalyticsRecorder } from './analytics.js'; |
||||
|
export { SqliteAnalyticsRecorder, NoopAnalyticsRecorder } from './analytics.js'; |
||||
|
|
||||
|
export type { RealtimeProvider, ProviderSession, ResolvedSessionConfig } from './provider.js'; |
||||
|
export { selectProvider } from './providers/index.js'; |
||||
|
|
||||
|
export type { SessionDefaults, RealtimeSessionState } from './session.js'; |
||||
|
export { createRealtimeSession, resolveSessionConfig } from './session.js'; |
||||
|
|
||||
|
export { resolveSessionDefaults } from './defaults.js'; |
||||
|
|
||||
|
export type { RealtimeUpgradeDeps } from './ws-handler.js'; |
||||
|
export { handleRealtimeUpgrade } from './ws-handler.js'; |
||||
|
|
||||
|
export type { ToolExecutor, ToolRegistry } from './tools.js'; |
||||
|
export { createToolRegistry } from './tools.js'; |
||||
|
|
||||
|
export type { MintedToken, TokenVerification } from './tokens.js'; |
||||
|
export { mintSessionToken, verifySessionToken, timingSafeCompare } from './tokens.js'; |
||||
|
|
||||
|
export type { CreateLoggerOptions } from './logger.js'; |
||||
|
export { createLogger, serverLogger, safeSerialize, verifyDebugAccessKey } from './logger.js'; |
||||
|
|
||||
|
export interface RealtimeRuntimeOptions { |
||||
|
persona: SessionPersona; |
||||
|
toolRegistry: ToolRegistry; |
||||
|
/** Literal per entrypoint: dev auto-forwards debug logs, prod requires DEBUG_ACCESS_KEY. */ |
||||
|
isDev: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface RealtimeRuntime { |
||||
|
config: AppConfig; |
||||
|
provider: RealtimeProvider; |
||||
|
recorder: AnalyticsRecorder; |
||||
|
defaults: SessionDefaults; |
||||
|
/** Attach to an http.Server 'upgrade' event. A closure, so it can be passed unbound. */ |
||||
|
handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): void; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Collapses the composition wiring both entrypoints (production `server.js`, |
||||
|
* the dev-mode `./vite` plugin) need: load/validate config, pick a provider, |
||||
|
* choose an analytics recorder, and merge session defaults with the caller's |
||||
|
* persona and tool registry. A verbatim move of what each entrypoint used to |
||||
|
* duplicate — not new behavior. Neither a service locator nor global state: |
||||
|
* returns a fresh instance per call, called once per process by whichever |
||||
|
* composition root is running. |
||||
|
*/ |
||||
|
export function createRealtimeRuntime(options: RealtimeRuntimeOptions): RealtimeRuntime { |
||||
|
const config = loadConfig(); |
||||
|
const provider = selectProvider(config); |
||||
|
const recorder = config.analytics.enabled |
||||
|
? new SqliteAnalyticsRecorder(config.analytics.dbPath, config.analytics) |
||||
|
: new NoopAnalyticsRecorder(); |
||||
|
const defaults: SessionDefaults = { |
||||
|
...resolveSessionDefaults(options.persona, config.session), |
||||
|
toolRegistry: options.toolRegistry |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
config, |
||||
|
provider, |
||||
|
recorder, |
||||
|
defaults, |
||||
|
handleUpgrade(req, socket, head) { |
||||
|
handleRealtimeUpgrade(req, socket, head, { |
||||
|
provider, |
||||
|
defaults, |
||||
|
recorder, |
||||
|
clientToolTimeoutMs: config.session.clientToolTimeoutMs, |
||||
|
isDev: options.isDev, |
||||
|
logMessages: config.debug.messages |
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
import type { LogEntry, LogLevel, Logger } from '../../core/logger.js'; |
import type { LogEntry, LogLevel, Logger } from '../logger.js'; |
||||
import { timingSafeCompare } from './tokens.js'; |
import { timingSafeCompare } from './tokens.js'; |
||||
|
|
||||
const CONSOLE_METHOD: Record<LogLevel, 'debug' | 'info' | 'warn' | 'error'> = { |
const CONSOLE_METHOD: Record<LogLevel, 'debug' | 'info' | 'warn' | 'error'> = { |
||||
@ -1,5 +1,5 @@ |
|||||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
||||
import type { ToolDefinition } from '../../core/tools.js'; |
import type { ToolDefinition } from '../tools.js'; |
||||
|
|
||||
/** |
/** |
||||
* Fully-resolved, server-trusted session config. Never expose this shape to |
* Fully-resolved, server-trusted session config. Never expose this shape to |
||||
@ -1,5 +1,5 @@ |
|||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
||||
import type { ServerEvent } from '../../../core/protocol.js'; |
import type { ServerEvent } from '../../protocol.js'; |
||||
import { MockRealtimeProvider, type MockScript } from './mock.js'; |
import { MockRealtimeProvider, type MockScript } from './mock.js'; |
||||
|
|
||||
const SCRIPT: MockScript = [ |
const SCRIPT: MockScript = [ |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ServerEvent } from '../../../core/protocol.js'; |
import type { ServerEvent } from '../../protocol.js'; |
||||
import type { ProviderSession, RealtimeProvider } from '../provider.js'; |
import type { ProviderSession, RealtimeProvider } from '../provider.js'; |
||||
|
|
||||
/** One scripted event, replayed `delayMs` after the previous step (or after `createSession` for the first step). */ |
/** One scripted event, replayed `delayMs` after the previous step (or after `createSession` for the first step). */ |
||||
@ -1,5 +1,5 @@ |
|||||
import { describe, expect, it } from 'vitest'; |
import { describe, expect, it } from 'vitest'; |
||||
import type { ToolDefinition } from '../../../core/tools.js'; |
import type { ToolDefinition } from '../../tools.js'; |
||||
import { fromOpenAiEvent, toOpenAiEvents, toOpenAiTool } from './openai.js'; |
import { fromOpenAiEvent, toOpenAiEvents, toOpenAiTool } from './openai.js'; |
||||
|
|
||||
const TOOL: ToolDefinition = { |
const TOOL: ToolDefinition = { |
||||
@ -1,6 +1,6 @@ |
|||||
import WebSocket from 'ws'; |
import WebSocket from 'ws'; |
||||
import type { ClientEvent, ServerEvent } from '../../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../../protocol.js'; |
||||
import type { ToolDefinition } from '../../../core/tools.js'; |
import type { ToolDefinition } from '../../tools.js'; |
||||
import type { ProviderSession, RealtimeProvider, ResolvedSessionConfig } from '../provider.js'; |
import type { ProviderSession, RealtimeProvider, ResolvedSessionConfig } from '../provider.js'; |
||||
|
|
||||
const REALTIME_URL = 'wss://api.openai.com/v1/realtime'; |
const REALTIME_URL = 'wss://api.openai.com/v1/realtime'; |
||||
@ -1,8 +1,8 @@ |
|||||
import { EventEmitter } from 'node:events'; |
import { EventEmitter } from 'node:events'; |
||||
import { afterEach, describe, expect, it, vi } from 'vitest'; |
import { afterEach, describe, expect, it, vi } from 'vitest'; |
||||
import type WebSocket from 'ws'; |
import type WebSocket from 'ws'; |
||||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
||||
import type { ToolDefinition } from '../../core/tools.js'; |
import type { ToolDefinition } from '../tools.js'; |
||||
import { SqliteAnalyticsRecorder, type AnalyticsRecorder } from './analytics.js'; |
import { SqliteAnalyticsRecorder, type AnalyticsRecorder } from './analytics.js'; |
||||
import type { ProviderSession, RealtimeProvider } from './provider.js'; |
import type { ProviderSession, RealtimeProvider } from './provider.js'; |
||||
import { createRealtimeSession, resolveSessionConfig } from './session.js'; |
import { createRealtimeSession, resolveSessionConfig } from './session.js'; |
||||
@ -1,14 +1,14 @@ |
|||||
import { randomUUID } from 'node:crypto'; |
import { randomUUID } from 'node:crypto'; |
||||
import WebSocket from 'ws'; |
import WebSocket from 'ws'; |
||||
import type { LogEntry, Logger } from '../../core/logger.js'; |
import type { LogEntry, Logger } from '../logger.js'; |
||||
import { |
import { |
||||
isBrowserClientEvent, |
isBrowserClientEvent, |
||||
type BrowserClientEvent, |
type BrowserClientEvent, |
||||
type ClientEvent, |
type ClientEvent, |
||||
type ServerEvent, |
type ServerEvent, |
||||
type SessionStartConfig |
type SessionStartConfig |
||||
} from '../../core/protocol.js'; |
} from '../protocol.js'; |
||||
import { formatToolError } from '../../core/tools.js'; |
import { formatToolError } from '../tools.js'; |
||||
import type { AnalyticsRecorder } from './analytics.js'; |
import type { AnalyticsRecorder } from './analytics.js'; |
||||
import { createLogger, safeSerialize, verifyDebugAccessKey } from './logger.js'; |
import { createLogger, safeSerialize, verifyDebugAccessKey } from './logger.js'; |
||||
import type { ProviderSession, RealtimeProvider, ResolvedSessionConfig } from './provider.js'; |
import type { ProviderSession, RealtimeProvider, ResolvedSessionConfig } from './provider.js'; |
||||
@ -1,5 +1,5 @@ |
|||||
import { describe, expect, it } from 'vitest'; |
import { describe, expect, it } from 'vitest'; |
||||
import type { ToolDefinition } from '../../core/tools.js'; |
import type { ToolDefinition } from '../tools.js'; |
||||
import { createToolRegistry } from './tools.js'; |
import { createToolRegistry } from './tools.js'; |
||||
|
|
||||
const SERVER_TOOL: ToolDefinition = { |
const SERVER_TOOL: ToolDefinition = { |
||||
@ -1,4 +1,4 @@ |
|||||
import type { ToolDefinition } from '../../core/tools.js'; |
import type { ToolDefinition } from '../tools.js'; |
||||
|
|
||||
export type ToolExecutor = (args: Record<string, unknown>) => Promise<unknown>; |
export type ToolExecutor = (args: Record<string, unknown>) => Promise<unknown>; |
||||
|
|
||||
@ -1,7 +1,7 @@ |
|||||
import type { IncomingMessage } from 'node:http'; |
import type { IncomingMessage } from 'node:http'; |
||||
import type { Duplex } from 'node:stream'; |
import type { Duplex } from 'node:stream'; |
||||
import { WebSocketServer } from 'ws'; |
import { WebSocketServer } from 'ws'; |
||||
import { REALTIME_WS_PATH } from '../../core/protocol.js'; |
import { REALTIME_WS_PATH } from '../protocol.js'; |
||||
import type { AnalyticsRecorder } from './analytics.js'; |
import type { AnalyticsRecorder } from './analytics.js'; |
||||
import type { RealtimeProvider } from './provider.js'; |
import type { RealtimeProvider } from './provider.js'; |
||||
import { createRealtimeSession, type SessionDefaults } from './session.js'; |
import { createRealtimeSession, type SessionDefaults } from './session.js'; |
||||
@ -0,0 +1,57 @@ |
|||||
|
import { loadEnv, type Plugin } from 'vite'; |
||||
|
import type { SessionPersona } from '../persona.js'; |
||||
|
import { createRealtimeRuntime } from '../server/index.js'; |
||||
|
import type { ToolRegistry } from '../server/tools.js'; |
||||
|
|
||||
|
export interface RealtimeDevWsPluginOptions { |
||||
|
/** |
||||
|
* Raw, env-independent pieces (project persona + tool registry) — taken as |
||||
|
* parameters rather than imported here so this module never has to import |
||||
|
* project code directly. See the composition root that supplies these |
||||
|
* (apps/template/vite.config.ts) and docs/TOOLS.md. |
||||
|
* |
||||
|
* Deliberately NOT pre-resolved against env vars by the caller: resolving |
||||
|
* them happens inside configureServer below, gated by the VITEST check — |
||||
|
* see the comment on that check for why. |
||||
|
*/ |
||||
|
persona: SessionPersona; |
||||
|
toolRegistry: ToolRegistry; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wires the same upgrade handling used in production (via |
||||
|
* `createRealtimeRuntime` from `./server`) into Vite's dev server, so |
||||
|
* `npm run dev` supports the realtime WS endpoint without a production build. |
||||
|
* |
||||
|
* Loads .env itself: this hook runs at dev-server startup, outside |
||||
|
* SvelteKit's own request/module graph, so `$env/...` isn't available here — |
||||
|
* see the note in `./server/tokens.ts` for why this whole module tree reads |
||||
|
* `process.env` directly instead. |
||||
|
*/ |
||||
|
export function realtimeDevWsPlugin(options: RealtimeDevWsPluginOptions): Plugin { |
||||
|
return { |
||||
|
name: 'webavatar-realtime-ws-dev', |
||||
|
configureServer(server) { |
||||
|
// vitest's browser-mode project boots its own Vite dev server for
|
||||
|
// component tests; this plugin has nothing to do there. Critically,
|
||||
|
// this guard is also what keeps loadConfig()'s fail-fast validation
|
||||
|
// (inside createRealtimeRuntime) from running during `npm run
|
||||
|
// test:unit` — that suite's `test.projects` entries both `extends`
|
||||
|
// the app's vite.config.ts, so this plugin's config gets evaluated
|
||||
|
// there too, and unit tests need no env vars set at all. Don't move
|
||||
|
// runtime creation above this line.
|
||||
|
if (process.env.VITEST) return; |
||||
|
|
||||
|
const loaded = loadEnv('development', process.cwd(), ''); |
||||
|
for (const [key, value] of Object.entries(loaded)) { |
||||
|
if (process.env[key] === undefined) process.env[key] = value; |
||||
|
} |
||||
|
|
||||
|
const runtime = createRealtimeRuntime({ ...options, isDev: true }); |
||||
|
|
||||
|
server.httpServer?.on('upgrade', (req, socket, head) => { |
||||
|
runtime.handleUpgrade(req, socket, head); |
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
{ |
||||
|
"extends": "./tsconfig.json", |
||||
|
"compilerOptions": { |
||||
|
"noEmit": false, |
||||
|
"outDir": "dist", |
||||
|
"rootDir": "src", |
||||
|
"declaration": true, |
||||
|
"declarationMap": true, |
||||
|
"sourceMap": true |
||||
|
}, |
||||
|
"exclude": ["src/**/*.spec.ts"] |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"extends": "../../tsconfig.base.json", |
||||
|
"compilerOptions": { |
||||
|
"module": "nodenext", |
||||
|
"moduleResolution": "nodenext", |
||||
|
"target": "es2023", |
||||
|
"types": ["node"], |
||||
|
"noEmit": true |
||||
|
}, |
||||
|
"include": ["src/**/*.ts"] |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
import { defineConfig } from 'vitest/config'; |
||||
|
|
||||
|
export default defineConfig({ |
||||
|
test: { |
||||
|
environment: 'node', |
||||
|
include: ['src/**/*.spec.ts'], |
||||
|
expect: { requireAssertions: true } |
||||
|
} |
||||
|
}); |
||||
@ -0,0 +1,28 @@ |
|||||
|
# @webavatar/webavatar-humanoid |
||||
|
|
||||
|
Renderer-agnostic avatar-state derivation and animation-channel composition for 3D humanoid avatars rigged with ARKit blend shapes. An **optional** package — infrastructure only some webavatar client projects need (one building a 2D avatar has no reason to depend on this at all). See the monorepo root's `docs/HUMANOID.md` for the full design writeup. |
||||
|
|
||||
|
**ESM-only.** No CommonJS build; `require()` is unsupported by design. |
||||
|
|
||||
|
## Entry points |
||||
|
|
||||
|
| Entry | Contains | |
||||
|
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | |
||||
|
| `@webavatar/webavatar-humanoid` | `AvatarState`/`AvatarSignal`/`nextAvatarState` (the pure state-transition reducer), `AnimationConfig`/`DEFAULT_ANIMATION_CONFIG`/`resolveAnimationConfig`, `ARKitBlendShape`/`AnimationFrame`/`AnimationLayer`/`compositeAnimationFrames`. | |
||||
|
| `@webavatar/webavatar-humanoid/client` | `deriveAvatarState` — the browser wrapper that folds a live `RealtimeClient`'s `ServerEvent`s through the pure reducer and owns the `idleActive`→`idlePassive` timer. | |
||||
|
| `@webavatar/webavatar-humanoid/eye` | The first animation library built as a consumer of the root contract: the Macro/Micro/Nano gaze system (`stepEyeGaze`, `EyeConfig`/`DEFAULT_EYE_CONFIG`/`resolveEyeConfig`, `gazeOffsetToBlendShapes`). | |
||||
|
| `@webavatar/webavatar-humanoid/eye/client` | `createEyeGazeController` — the browser wrapper that owns the gaze system's per-frame clock and `Math.random`. | |
||||
|
|
||||
|
Future animation libraries (blink, lip-sync, body/gesture) are expected to nest the same way, as `./<name>` / `./<name>/client` sub-packages, rather than becoming further top-level packages. |
||||
|
|
||||
|
## What this package is not |
||||
|
|
||||
|
No renderer knowledge (Three.js, Babylon, a game engine — none of it), no rig/mesh binding, no Svelte dependency (its hand-rolled stores satisfy the Svelte store contract structurally). Consuming code owns which `ARKitBlendShape` maps to which morph target on its own mesh. |
||||
|
|
||||
|
## Core dependency: type-only |
||||
|
|
||||
|
This package's only relationship to `@webavatar/webavatar-core` is a **type-only** import (`import type { ServerEvent }`) — its built JavaScript contains no runtime reference to core at all, verified as part of this package's own build (`grep -rl "webavatar-core" dist --include="*.js"` finds nothing). Core is declared as a `peerDependency` purely so npm reports a version-compatibility error at install time if a project's core and humanoid versions drift apart — not because this package calls into core at runtime. |
||||
|
|
||||
|
## Compatibility |
||||
|
|
||||
|
Caret-ranged (`^0.1.0`) during the `0.x` window — every humanoid release during this window bumps its peer/dev range on core in the same commit as any core minor it needs to track. See the monorepo root's `docs/MONOREPO_MIGRATION_BRIEF.md` ("Versions, ranges, and the `0.x` window") for the full policy. |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue