@alexkroman1/aai 0.7.7 → 0.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli.js +2410 -2412
  2. package/dist/sdk/_internal_types.d.ts +2 -1
  3. package/dist/sdk/_internal_types.d.ts.map +1 -1
  4. package/dist/sdk/_internal_types.js.map +1 -1
  5. package/dist/sdk/protocol.d.ts +0 -7
  6. package/dist/sdk/protocol.d.ts.map +1 -1
  7. package/dist/sdk/protocol.js +0 -6
  8. package/dist/sdk/protocol.js.map +1 -1
  9. package/dist/sdk/s2s.d.ts.map +1 -1
  10. package/dist/sdk/s2s.js +1 -1
  11. package/dist/sdk/s2s.js.map +1 -1
  12. package/dist/sdk/server.d.ts +2 -0
  13. package/dist/sdk/server.d.ts.map +1 -1
  14. package/dist/sdk/server.js +40 -1
  15. package/dist/sdk/server.js.map +1 -1
  16. package/dist/sdk/winterc_server.d.ts.map +1 -1
  17. package/dist/sdk/winterc_server.js +1 -2
  18. package/dist/sdk/winterc_server.js.map +1 -1
  19. package/dist/sdk/worker_shim.d.ts.map +1 -1
  20. package/dist/sdk/worker_shim.js +2 -3
  21. package/dist/sdk/worker_shim.js.map +1 -1
  22. package/dist/ui/session.d.ts.map +1 -1
  23. package/dist/ui/session.js +0 -13
  24. package/dist/ui/session.js.map +1 -1
  25. package/package.json +5 -3
  26. package/templates/_shared/CLAUDE.md +192 -38
  27. package/templates/_shared/biome.json +32 -0
  28. package/templates/_shared/index.html +16 -0
  29. package/templates/_shared/package.json +6 -5
  30. package/templates/code-interpreter/client.tsx +1 -0
  31. package/templates/dispatch-center/client.tsx +1 -0
  32. package/templates/embedded-assets/client.tsx +1 -0
  33. package/templates/health-assistant/client.tsx +1 -0
  34. package/templates/infocom-adventure/client.tsx +1 -0
  35. package/templates/math-buddy/client.tsx +1 -0
  36. package/templates/memory-agent/client.tsx +1 -0
  37. package/templates/night-owl/client.tsx +1 -0
  38. package/templates/personal-finance/client.tsx +1 -0
  39. package/templates/simple/client.tsx +1 -0
  40. package/templates/smart-research/client.tsx +1 -0
  41. package/templates/support/client.tsx +1 -0
  42. package/templates/travel-concierge/client.tsx +1 -0
  43. package/templates/web-researcher/client.tsx +1 -0
  44. package/ui/styles.css +73 -0
@@ -1,7 +1,6 @@
1
- # Build a voice agent with `aai`
1
+ # aai Voice Agent Project
2
2
 
3
- You are helping a user build a voice agent using the **aai** framework. Generate
4
- or update files based on the user's description in `$ARGUMENTS`.
3
+ You are helping a user build a voice agent using the **aai** framework.
5
4
 
6
5
  ## Workflow
7
6
 
@@ -14,16 +13,24 @@ or update files based on the user's description in `$ARGUMENTS`.
14
13
  4. **Iterate** — Make small, focused changes. Verify each change works before
15
14
  moving on.
16
15
 
17
- ## Getting started
16
+ ## Key rules
18
17
 
19
- ### Use the `aai` CLI
18
+ - Every agent lives in `agent.ts` and exports a default `defineAgent()` call
19
+ - Custom UI goes in `client.tsx` alongside `agent.ts`
20
+ - Optimize `instructions` for spoken conversation — short sentences, no visual
21
+ formatting, no exclamation points
22
+ - Never hardcode secrets — use `aai env add` and access via `ctx.env`
23
+ - Tool `execute` return values go into LLM context — filter and truncate large
24
+ API responses
25
+ - Agent code runs in a sandboxed worker — use `fetch` (proxied) for HTTP,
26
+ `ctx.env` for secrets
20
27
 
21
- Always use the `aai` CLI to scaffold, deploy, and manage agents:
28
+ ## CLI commands
22
29
 
23
30
  ```sh
24
- aai # Scaffold (if needed) + deploy
25
- aai new # Scaffold a new agent (interactive)
26
- aai new -t <template> # Scaffold from a specific template
31
+ aai init # Scaffold a new agent (uses simple template)
32
+ aai init -t <template> # Scaffold from a specific template
33
+ aai dev # Start local dev server
27
34
  aai deploy # Bundle and deploy to production
28
35
  aai deploy -y # Deploy without prompts
29
36
  aai deploy --dry-run # Validate and bundle without deploying
@@ -33,29 +40,10 @@ aai env ls # List environment variable names
33
40
  aai env pull # Pull env var names into .env for local dev
34
41
  ```
35
42
 
36
- Install: `curl -fsSL https://aai-agent.fly.dev/install | sh`
43
+ ## Templates
37
44
 
38
- ### Deploy a scaffolded project
39
-
40
- After scaffolding with `aai new`, deploy from the project directory:
41
-
42
- ```sh
43
- cd my-agent
44
- aai deploy # Bundle, check, and deploy
45
- aai deploy -y # Skip confirmation prompts
46
- ```
47
-
48
- The CLI auto-detects the server URL. When running via `aai-dev` (the local
49
- monorepo dev wrapper), it targets `http://localhost:3100` automatically.
50
-
51
- ### Start from a template
52
-
53
- Before writing an agent from scratch, **choose the closest template** and
54
- scaffold with `aai new -t <template_name>`. Ask the user which template fits, or
55
- recommend one based on their description. Fall back to `simple` if nothing else
56
- fits.
57
-
58
- Templates are in `templates/` relative to the CLI source:
45
+ Before writing an agent from scratch, choose the closest template and scaffold
46
+ with `aai init -t <template_name>`.
59
47
 
60
48
  | Template | Description |
61
49
  | ------------------- | ---------------------------------------------------------------------------------- |
@@ -75,7 +63,7 @@ Templates are in `templates/` relative to the CLI source:
75
63
  | `support` | RAG-powered support agent using vector_search (AssemblyAI docs example) |
76
64
  | `terminal` | STT-only mode for voice-driven kubectl commands |
77
65
 
78
- ### Minimal agent
66
+ ## Minimal agent
79
67
 
80
68
  Every agent lives in `agent.ts` and exports a default `defineAgent()` call:
81
69
 
@@ -104,9 +92,9 @@ import { z } from "zod"; // Tools with typed params (included in package.json)
104
92
  defineAgent({
105
93
  // Core
106
94
  name: string; // Required: display name
107
- instructions?: string; // System prompt (voice-first default provided)
108
- greeting?: string; // Spoken on connect
109
- voice?: Voice; // Cartesia voice UUID (default: Sarah)
95
+ instructions?: string; // System prompt (default: general voice assistant)
96
+ greeting?: string; // Spoken on connect (default: "Hey, how can I help you?")
97
+ voice?: Voice; // Cartesia voice UUID (default: Sarah 694f9389...)
110
98
 
111
99
  // Speech
112
100
  sttPrompt?: string; // STT guidance for jargon, names, acronyms
@@ -118,8 +106,6 @@ defineAgent({
118
106
  activeTools?: string[]; // Default active tools per turn (subset of all tools)
119
107
  maxSteps?: number | ((ctx: HookContext) => number);
120
108
 
121
- // Environment
122
-
123
109
  // State
124
110
  state?: () => S; // Factory for per-session state
125
111
 
@@ -407,6 +393,42 @@ onBeforeStep: (stepNumber, ctx) => {
407
393
  },
408
394
  ```
409
395
 
396
+ ### Tool choice
397
+
398
+ Control when the LLM uses tools:
399
+
400
+ ```ts
401
+ toolChoice: "auto", // Default — LLM decides when to use tools
402
+ toolChoice: "required", // Force a tool call every step (useful for research pipelines)
403
+ toolChoice: "none", // Disable all tool use
404
+ toolChoice: { type: "tool", toolName: "search" }, // Force a specific tool
405
+ ```
406
+
407
+ ### Phase-based tool filtering
408
+
409
+ Combine `state`, `onBeforeStep`, and `activeTools` for multi-phase workflows:
410
+
411
+ ```ts
412
+ state: () => ({ phase: "gather" as "gather" | "analyze" | "respond" }),
413
+ onBeforeStep: (_step, ctx) => {
414
+ const state = ctx.state as { phase: string };
415
+ if (state.phase === "gather") return { activeTools: ["web_search", "advance"] };
416
+ if (state.phase === "analyze") return { activeTools: ["summarize", "advance"] };
417
+ return { activeTools: [] }; // respond phase — LLM speaks freely
418
+ },
419
+ tools: {
420
+ advance: {
421
+ description: "Move to the next phase",
422
+ execute: (_args, ctx) => {
423
+ const state = ctx.state as { phase: string };
424
+ if (state.phase === "gather") state.phase = "analyze";
425
+ else if (state.phase === "analyze") state.phase = "respond";
426
+ return { phase: state.phase };
427
+ },
428
+ },
429
+ },
430
+ ```
431
+
410
432
  ### Static `activeTools`
411
433
 
412
434
  Restrict which tools the LLM can use by default, without writing a hook:
@@ -472,6 +494,7 @@ Add `client.tsx` alongside `agent.ts`. Define a Preact component and call
472
494
  `mount()` to render it. Use JSX syntax:
473
495
 
474
496
  ```tsx
497
+ import "aai/ui/styles.css";
475
498
  import { mount, useSession } from "aai/ui";
476
499
 
477
500
  function App() {
@@ -497,6 +520,8 @@ mount(App);
497
520
 
498
521
  **Rules:**
499
522
 
523
+ - Always import `"aai/ui/styles.css"` at the top — without it, default styles
524
+ won't load
500
525
  - Call `mount(YourComponent)` at the end of the file
501
526
  - Use `.tsx` file extension for JSX syntax
502
527
  - Import hooks from `preact/hooks` (`useEffect`, `useRef`, `useState`, etc.)
@@ -561,6 +586,7 @@ data lives on `session` (a `VoiceSession`); UI-only controls are top-level.
561
586
  ### Showing tool calls in custom UI
562
587
 
563
588
  ```tsx
589
+ import "aai/ui/styles.css";
564
590
  import { mount, ToolCallBlock, useSession } from "aai/ui";
565
591
 
566
592
  function App() {
@@ -590,6 +616,7 @@ mount(App);
590
616
  ### Reacting to agent state
591
617
 
592
618
  ```tsx
619
+ import "aai/ui/styles.css";
593
620
  import { useEffect } from "preact/hooks";
594
621
  import { mount, StateIndicator, useSession } from "aai/ui";
595
622
 
@@ -646,6 +673,92 @@ function App() {
646
673
  - `--color-aai-surface`, `--color-aai-border`
647
674
  - `--color-aai-state-{state}` — color for each `AgentState` value
648
675
 
676
+ ## Self-hosting with `createServer()`
677
+
678
+ Agents can run anywhere (Node, Deno, Docker) without the managed platform:
679
+
680
+ ```ts
681
+ import { defineAgent } from "aai";
682
+ import { createServer } from "aai/server";
683
+
684
+ const agent = defineAgent({
685
+ name: "My Agent",
686
+ instructions: "You are a helpful assistant.",
687
+ });
688
+
689
+ const server = createServer(agent, {
690
+ port: 3000, // default: 3000
691
+ staticDir: "public", // optional: serve static files
692
+ });
693
+
694
+ server.listen();
695
+ ```
696
+
697
+ Run with `node --experimental-strip-types server.ts` or bundle with your
698
+ preferred tool. The server handles WebSocket connections, STT/TTS, and the
699
+ agentic loop. Set `ASSEMBLYAI_API_KEY` as an environment variable.
700
+
701
+ ## Useful free API endpoints
702
+
703
+ These public APIs require no auth and work well in voice agents:
704
+
705
+ ```text
706
+ Weather (Open-Meteo):
707
+ Geocode: https://geocoding-api.open-meteo.com/v1/search?name={city}&count=1&language=en
708
+ Forecast: https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto&forecast_days=7
709
+
710
+ Currency (ExchangeRate):
711
+ Rates: https://open.er-api.com/v6/latest/{CODE} → { rates: { USD: 1.0, EUR: 0.85, ... } }
712
+
713
+ Crypto (CoinGecko):
714
+ Price: https://api.coingecko.com/api/v3/simple/price?ids={coin}&vs_currencies={cur}&include_24hr_change=true
715
+
716
+ Drug info (FDA):
717
+ Label: https://api.fda.gov/drug/label.json?search=openfda.generic_name:"{name}"&limit=1
718
+
719
+ Drug interactions (RxNorm):
720
+ RxCUI: https://rxnav.nlm.nih.gov/REST/rxcui.json?name={name}
721
+ Interactions: https://rxnav.nlm.nih.gov/REST/interaction/list.json?rxcuis={id1}+{id2}
722
+ ```
723
+
724
+ Use `fetch_json` builtin tool or `fetch` in custom tools to call these.
725
+
726
+ ## Partial custom UI with `ChatView`
727
+
728
+ For a custom start screen that transitions to the default chat interface:
729
+
730
+ ```tsx
731
+ import "aai/ui/styles.css";
732
+ import { ChatView, mount, useSession } from "aai/ui";
733
+
734
+ function MyAgent() {
735
+ const { started, start } = useSession();
736
+
737
+ if (!started.value) {
738
+ return (
739
+ <div class="flex items-center justify-center h-screen bg-aai-bg">
740
+ <div class="flex flex-col items-center gap-6">
741
+ <h1 class="text-xl text-aai-text">My Agent</h1>
742
+ <button
743
+ class="px-8 py-3 rounded-aai bg-aai-primary text-white border-none cursor-pointer"
744
+ onClick={start}
745
+ >
746
+ Start
747
+ </button>
748
+ </div>
749
+ </div>
750
+ );
751
+ }
752
+
753
+ return <ChatView />;
754
+ }
755
+
756
+ mount(MyAgent);
757
+ ```
758
+
759
+ This gives you full control over the start screen while reusing the built-in
760
+ chat UI once the session begins.
761
+
649
762
  ## Project structure
650
763
 
651
764
  After scaffolding, your project directory looks like:
@@ -661,12 +774,53 @@ my-agent/
661
774
  .env # Local dev secrets (gitignored)
662
775
  .gitignore # Ignores node_modules/, .aai/, .env, etc.
663
776
  README.md # Getting started guide
664
- CLAUDE.md # Agent API reference (auto-generated)
777
+ CLAUDE.md # Agent API reference (always loaded by Claude Code)
665
778
  .aai/ # Build output (managed by CLI, gitignored)
666
779
  project.json # Deploy target (slug, server URL)
667
780
  build/ # Bundle output
668
781
  ```
669
782
 
783
+ ## Instructions patterns from templates
784
+
785
+ Good instructions tell the LLM what it is, how to behave, and when to use each
786
+ tool. Study these patterns:
787
+
788
+ **Code execution agent** — force tool use for anything computational:
789
+ ```
790
+ You MUST use the run_code tool for ANY question involving math, counting,
791
+ string manipulation, or data processing. NEVER do mental math or estimate.
792
+ Use console.log() to output intermediate steps.
793
+ ```
794
+
795
+ **Research agent** — search before answering:
796
+ ```
797
+ Search first. Never guess or rely on memory for factual questions.
798
+ Use visit_webpage when search snippets aren't detailed enough.
799
+ For complex questions, search multiple times with different queries.
800
+ ```
801
+
802
+ **FAQ/support agent** — stay grounded in knowledge:
803
+ ```
804
+ Always use vector_search to find relevant documentation before answering.
805
+ Base your answers strictly on the retrieved documentation — don't guess.
806
+ If search results aren't relevant, say the docs don't cover that topic.
807
+ ```
808
+
809
+ **API-calling agent** — tell the LLM which endpoints to use:
810
+ ```
811
+ API endpoints (use fetch_json):
812
+ - Currency rates: https://open.er-api.com/v6/latest/{CODE}
813
+ Returns { rates: { USD: 1.0, EUR: 0.85, ... } }
814
+ - Weather: https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}...
815
+ ```
816
+
817
+ **Game/interactive agent** — establish world rules and voice style:
818
+ ```
819
+ You ARE the game. Maintain world state, describe rooms, handle puzzles.
820
+ Keep descriptions to two to four sentences. No visual formatting.
821
+ Use directional words naturally: "To the north you see..." not "N: forest"
822
+ ```
823
+
670
824
  ## Common pitfalls
671
825
 
672
826
  - **Writing `instructions` with visual formatting** — Bullets, bold, numbered
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
3
+ "files": {
4
+ "includes": ["**"],
5
+ "ignores": ["node_modules", "dist", ".aai"]
6
+ },
7
+ "linter": {
8
+ "enabled": true,
9
+ "rules": {
10
+ "recommended": true,
11
+ "correctness": {
12
+ "noUnusedImports": "error",
13
+ "noUnusedVariables": "error"
14
+ },
15
+ "a11y": {
16
+ "recommended": false
17
+ }
18
+ }
19
+ },
20
+ "formatter": {
21
+ "enabled": true,
22
+ "indentStyle": "space",
23
+ "indentWidth": 2,
24
+ "lineWidth": 100
25
+ },
26
+ "javascript": {
27
+ "formatter": {
28
+ "quoteStyle": "double",
29
+ "semicolons": "always"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, viewport-fit=cover"
8
+ />
9
+ <title>aai</title>
10
+ <link rel="icon" href="data:," />
11
+ </head>
12
+ <body>
13
+ <main id="app"></main>
14
+ <script type="module" src="./client.tsx"></script>
15
+ </body>
16
+ </html>
@@ -3,15 +3,16 @@
3
3
  "scripts": {
4
4
  "dev": "aai dev",
5
5
  "build": "aai deploy --dry-run",
6
- "deploy": "aai deploy"
6
+ "deploy": "aai deploy",
7
+ "lint": "biome check .",
8
+ "lint:fix": "biome check --write ."
7
9
  },
8
10
  "dependencies": {
9
- "@alexkroman1/aai": "*",
10
- "preact": "^10",
11
- "@preact/signals": "^2",
12
- "zod": "^4"
11
+ "@alexkroman1/aai": "*"
13
12
  },
14
13
  "devDependencies": {
14
+ "@biomejs/biome": "^2",
15
+ "@types/node": "^22",
15
16
  "typescript": "^5"
16
17
  }
17
18
  }
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,3 +1,4 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { mount, useSession } from "@alexkroman1/aai/ui";
2
3
  import type { Message } from "@alexkroman1/aai/ui";
3
4
  import { useEffect, useRef } from "preact/hooks";
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,3 +1,4 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { mount, useSession } from "@alexkroman1/aai/ui";
2
3
  import type { Message } from "@alexkroman1/aai/ui";
3
4
  import { useEffect, useRef } from "preact/hooks";
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,3 +1,4 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { ChatView, mount, useSession } from "@alexkroman1/aai/ui";
2
3
 
3
4
  function NightOwl() {
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
@@ -1,2 +1,3 @@
1
+ import "@alexkroman1/aai/ui/styles.css";
1
2
  import { App, mount } from "@alexkroman1/aai/ui";
2
3
  mount(App);
package/ui/styles.css ADDED
@@ -0,0 +1,73 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap");
2
+ @import "tailwindcss";
3
+ @source "./";
4
+
5
+ @theme {
6
+ --color-aai-bg: #101010;
7
+ --color-aai-surface: #151515;
8
+ --color-aai-surface-faint: rgba(255, 255, 255, 0.031);
9
+ --color-aai-surface-hover: rgba(255, 255, 255, 0.059);
10
+ --color-aai-border: #282828;
11
+ --color-aai-primary: #fab283;
12
+ --color-aai-text: rgba(255, 255, 255, 0.936);
13
+ --color-aai-text-secondary: rgba(255, 255, 255, 0.618);
14
+ --color-aai-text-muted: rgba(255, 255, 255, 0.284);
15
+ --color-aai-text-dim: rgba(255, 255, 255, 0.422);
16
+ --color-aai-error: #e06c75;
17
+ --color-aai-ring: #56b6c2;
18
+ --color-aai-state-disconnected: rgba(255, 255, 255, 0.422);
19
+ --color-aai-state-connecting: rgba(255, 255, 255, 0.422);
20
+ --color-aai-state-ready: #7fd88f;
21
+ --color-aai-state-listening: #56b6c2;
22
+ --color-aai-state-thinking: #f5a742;
23
+ --color-aai-state-speaking: #e06c75;
24
+ --color-aai-state-error: #e06c75;
25
+ --radius-aai: 6px;
26
+ --font-aai: "Inter", system-ui, -apple-system, sans-serif;
27
+ --font-aai-mono: "IBM Plex Mono", monospace;
28
+ }
29
+
30
+ @layer base {
31
+ html,
32
+ body {
33
+ margin: 0;
34
+ padding: 0;
35
+ background: var(--color-aai-bg);
36
+ }
37
+ }
38
+
39
+ @keyframes aai-bounce {
40
+ 0%,
41
+ 80%,
42
+ 100% {
43
+ opacity: 0.3;
44
+ transform: scale(0.8);
45
+ }
46
+ 40% {
47
+ opacity: 1;
48
+ transform: scale(1);
49
+ }
50
+ }
51
+
52
+ @keyframes aai-shimmer {
53
+ 0% {
54
+ background-position: -200% 0;
55
+ }
56
+ 100% {
57
+ background-position: 200% 0;
58
+ }
59
+ }
60
+
61
+ .tool-shimmer {
62
+ background: linear-gradient(
63
+ 90deg,
64
+ var(--color-aai-text) 25%,
65
+ var(--color-aai-text-dim) 50%,
66
+ var(--color-aai-text) 75%
67
+ );
68
+ background-size: 200% 100%;
69
+ -webkit-background-clip: text;
70
+ background-clip: text;
71
+ -webkit-text-fill-color: transparent;
72
+ animation: aai-shimmer 2s ease-in-out infinite;
73
+ }