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 |
|||
@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 |
|||
|
|||
```sh |
|||
npm install |
|||
cp .env.example .env |
|||
# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in .env |
|||
cp apps/template/.env.example apps/template/.env |
|||
# fill in OPENAI_API_KEY and REALTIME_TOKEN_SECRET in apps/template/.env |
|||
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 |
|||
|
|||
- `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)). |
|||
Run from the repo root, these orchestrate all three workspaces: |
|||
|
|||
- `npm run dev` / `npm run build` / `npm run check` / `npm test` — build the packages first, then run the equivalent command across every workspace. |
|||
- `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 |
|||
|
|||
| 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) | |
|||
| `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"> |
|||
import type { LogLevel } from '../../core/logger.js'; |
|||
import { clearLogs, debugLogStore, type DisplayLogEntry } from './logger.js'; |
|||
import type { ServerLogForwarder, ServerLogForwarderState } from './server-log-forwarder.js'; |
|||
import type { LogLevel } from '@webavatar/webavatar-core'; |
|||
import { |
|||
clearLogs, |
|||
debugLogStore, |
|||
type DisplayLogEntry, |
|||
type ServerLogForwarder, |
|||
type ServerLogForwarderState |
|||
} from '@webavatar/webavatar-core/client'; |
|||
|
|||
/** |
|||
* Production-shippable debug panel — lives in `$lib/client/core` (not |
|||
* `/demo`, which is dev-only-gated scaffolding) since the key-based auth |
|||
* on `forwarder`, not route removal, is what protects it in prod. See |
|||
* Production-shippable debug panel — lives in `$lib/client` (not `/demo`, |
|||
* which is dev-only-gated scaffolding) since the key-based auth on |
|||
* `forwarder`, not route removal, is what protects it in prod. See |
|||
* docs/LOGGING.md. |
|||
* |
|||
* `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 { render } from 'vitest-browser-svelte'; |
|||
import { clearLogs, clientLogger } from './logger.js'; |
|||
import { clearLogs, clientLogger } from '@webavatar/webavatar-core/client'; |
|||
import DebugPanel from './DebugPanel.svelte'; |
|||
|
|||
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. */ |
|||
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'; |
|||
|
|||
/** 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 |
|||
@ -1,4 +1,4 @@ |
|||
import type { ToolExecutor } from '../../core/tools.js'; |
|||
import type { ToolExecutor } from '../../webavatar-core.js'; |
|||
|
|||
export const getCurrentTime: ToolExecutor = async () => { |
|||
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 { 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 { loadConfig } from '$lib/server/core/config.js'; |
|||
import { mintSessionToken } from '$lib/server/core/tokens.js'; |
|||
import { loadConfig, mintSessionToken } from '$lib/server/webavatar-core.js'; |
|||
import type { RequestHandler } from './$types'; |
|||
|
|||
export const POST: RequestHandler = () => { |
|||
@ -1,7 +1,6 @@ |
|||
<script lang="ts"> |
|||
import type { AvatarState } from '$lib/humanoid/state.js'; |
|||
import type { AnimationFrame } from '$lib/humanoid/animation.js'; |
|||
import type { EyeGazeState } from '$lib/humanoid/eye/gaze.js'; |
|||
import type { AvatarState, AnimationFrame } from '@webavatar/webavatar-humanoid'; |
|||
import type { EyeGazeState } from '@webavatar/webavatar-humanoid/eye'; |
|||
|
|||
interface Vec2 { |
|||
x: number; |
|||
@ -1,15 +1,25 @@ |
|||
{ |
|||
"extends": "./.svelte-kit/tsconfig.json", |
|||
"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, |
|||
"allowJs": true, |
|||
"checkJs": true, |
|||
"esModuleInterop": true, |
|||
"forceConsistentCasingInFileNames": true, |
|||
"resolveJsonModule": true, |
|||
"skipLibCheck": true, |
|||
"sourceMap": true, |
|||
"strict": true, |
|||
"moduleResolution": "bundler" |
|||
} |
|||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias |
|||
@ -1,51 +1,33 @@ |
|||
{ |
|||
"name": "webavatar-base", |
|||
"name": "webavatar-monorepo", |
|||
"private": true, |
|||
"version": "0.0.1", |
|||
"type": "module", |
|||
"license": "UNLICENSED", |
|||
"engines": { |
|||
"node": ">=22.12" |
|||
}, |
|||
"workspaces": [ |
|||
"packages/*", |
|||
"apps/*" |
|||
], |
|||
"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", |
|||
"build:packages": "npm run build -w packages/core && npm run build -w packages/humanoid", |
|||
"build": "npm run build:packages && npm run build -w apps/template", |
|||
"dev": "npm run build:packages && concurrently \"npm run watch -w packages/core\" \"npm run watch -w packages/humanoid\" \"npm run dev -w apps/template\"", |
|||
"check": "npm run build:packages && npm run check --workspaces --if-present", |
|||
"test": "npm run build:packages && npm run test -w packages/core -w packages/humanoid && npm run test:unit -w apps/template -- --run", |
|||
"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" |
|||
"format": "prettier --write ." |
|||
}, |
|||
"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/better-sqlite3": "^9.6.0", |
|||
"@types/node": "^24", |
|||
"@types/three": "^0.185.4", |
|||
"@types/ws": "^8.18.1", |
|||
"@vitest/browser-playwright": "^4.1.8", |
|||
"concurrently": "^9.1.0", |
|||
"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": { |
|||
"better-sqlite3": "^13.0.3", |
|||
"ws": "^8.21.3" |
|||
"typescript-eslint": "^8.60.1" |
|||
} |
|||
} |
|||
|
|||
@ -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 { |
|||
/** 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`. */ |
|||
export interface DisplayLogEntry extends LogEntry { |
|||
@ -1,5 +1,5 @@ |
|||
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'; |
|||
|
|||
/** 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'; |
|||
|
|||
export type ConnectionState = 'connecting' | 'open' | 'reconnecting' | 'closed'; |
|||
@ -1,5 +1,5 @@ |
|||
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 { |
|||
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'; |
|||
|
|||
/** |
|||
@ -1,5 +1,5 @@ |
|||
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 { createToolDispatcher } from './tool-dispatcher.js'; |
|||
|
|||
@ -1,5 +1,5 @@ |
|||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
|||
import { formatToolError } from '../../core/tools.js'; |
|||
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
|||
import { formatToolError } from '../tools.js'; |
|||
|
|||
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'; |
|||
|
|||
/** |
|||
@ -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'; |
|||
|
|||
const CONSOLE_METHOD: Record<LogLevel, 'debug' | 'info' | 'warn' | 'error'> = { |
|||
@ -1,5 +1,5 @@ |
|||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
|||
import type { ToolDefinition } from '../../core/tools.js'; |
|||
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
|||
import type { ToolDefinition } from '../tools.js'; |
|||
|
|||
/** |
|||
* 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 type { ServerEvent } from '../../../core/protocol.js'; |
|||
import type { ServerEvent } from '../../protocol.js'; |
|||
import { MockRealtimeProvider, type MockScript } from './mock.js'; |
|||
|
|||
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'; |
|||
|
|||
/** 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 type { ToolDefinition } from '../../../core/tools.js'; |
|||
import type { ToolDefinition } from '../../tools.js'; |
|||
import { fromOpenAiEvent, toOpenAiEvents, toOpenAiTool } from './openai.js'; |
|||
|
|||
const TOOL: ToolDefinition = { |
|||
@ -1,6 +1,6 @@ |
|||
import WebSocket from 'ws'; |
|||
import type { ClientEvent, ServerEvent } from '../../../core/protocol.js'; |
|||
import type { ToolDefinition } from '../../../core/tools.js'; |
|||
import type { ClientEvent, ServerEvent } from '../../protocol.js'; |
|||
import type { ToolDefinition } from '../../tools.js'; |
|||
import type { ProviderSession, RealtimeProvider, ResolvedSessionConfig } from '../provider.js'; |
|||
|
|||
const REALTIME_URL = 'wss://api.openai.com/v1/realtime'; |
|||
@ -1,8 +1,8 @@ |
|||
import { EventEmitter } from 'node:events'; |
|||
import { afterEach, describe, expect, it, vi } from 'vitest'; |
|||
import type WebSocket from 'ws'; |
|||
import type { ClientEvent, ServerEvent } from '../../core/protocol.js'; |
|||
import type { ToolDefinition } from '../../core/tools.js'; |
|||
import type { ClientEvent, ServerEvent } from '../protocol.js'; |
|||
import type { ToolDefinition } from '../tools.js'; |
|||
import { SqliteAnalyticsRecorder, type AnalyticsRecorder } from './analytics.js'; |
|||
import type { ProviderSession, RealtimeProvider } from './provider.js'; |
|||
import { createRealtimeSession, resolveSessionConfig } from './session.js'; |
|||
@ -1,14 +1,14 @@ |
|||
import { randomUUID } from 'node:crypto'; |
|||
import WebSocket from 'ws'; |
|||
import type { LogEntry, Logger } from '../../core/logger.js'; |
|||
import type { LogEntry, Logger } from '../logger.js'; |
|||
import { |
|||
isBrowserClientEvent, |
|||
type BrowserClientEvent, |
|||
type ClientEvent, |
|||
type ServerEvent, |
|||
type SessionStartConfig |
|||
} from '../../core/protocol.js'; |
|||
import { formatToolError } from '../../core/tools.js'; |
|||
} from '../protocol.js'; |
|||
import { formatToolError } from '../tools.js'; |
|||
import type { AnalyticsRecorder } from './analytics.js'; |
|||
import { createLogger, safeSerialize, verifyDebugAccessKey } from './logger.js'; |
|||
import type { ProviderSession, RealtimeProvider, ResolvedSessionConfig } from './provider.js'; |
|||
@ -1,5 +1,5 @@ |
|||
import { describe, expect, it } from 'vitest'; |
|||
import type { ToolDefinition } from '../../core/tools.js'; |
|||
import type { ToolDefinition } from '../tools.js'; |
|||
import { createToolRegistry } from './tools.js'; |
|||
|
|||
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>; |
|||
|
|||
@ -1,7 +1,7 @@ |
|||
import type { IncomingMessage } from 'node:http'; |
|||
import type { Duplex } from 'node:stream'; |
|||
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 { RealtimeProvider } from './provider.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