Compare commits

..

7 Commits

Author SHA1 Message Date
57c3275cce wip convo ui 2026-06-23 00:59:06 +01:00
25713ef614 chore: save wip changes 2026-06-20 16:48:08 +01:00
2e6cae4d02 chore: add zed settings (#151) 2026-06-20 16:46:39 +01:00
8cf38d609b feat: add upgrading expo skill (#150) 2026-06-20 16:46:30 +01:00
e6af1b7851 refactor: move conversation types to core (#149) 2026-06-18 20:47:36 +01:00
769fd5c77d feat: add conversation entries API (#148) 2026-06-18 17:19:47 +01:00
6cc0f7669a fix: upgrade client to expo 56 (#147)
Upgrade the React Native client through Expo SDK 56, align workspace React versions, switch Bun installs to the hoisted linker for Expo compatibility, and fix the Metro proxy to handle localhost/IPv6 loopback after the SDK upgrade.
2026-06-18 16:25:54 +01:00
60 changed files with 3075 additions and 370 deletions

View File

@@ -0,0 +1,134 @@
---
name: upgrading-expo
description: Guidelines for upgrading Expo SDK versions and fixing dependency issues
version: 1.0.0
license: MIT
---
## References
- ./references/react-19.md -- SDK +54: React 19 changes (useContext → use, Context.Provider → Context, forwardRef removal)
- ./references/new-architecture.md -- SDK +53: New Architecture migration guide
- ./references/react-compiler.md -- SDK +54: React Compiler setup and migration guide
- ./references/native-tabs.md -- SDK +55: Native tabs changes (Icon/Label/Badge now accessed via NativeTabs.Trigger.\*)
- ./references/expo-av-to-audio.md -- SDK +55: Migrate audio playback and recording from expo-av to expo-audio
- ./references/expo-av-to-video.md -- SDK +55: Migrate video playback from expo-av to expo-video
- ./references/react-navigation-to-expo-router.md -- SDK +56: Migrate `@react-navigation/*` imports to `expo-router` entry points (codemod + manual mapping)
## Beta/Preview Releases
Beta versions use `.preview` suffix (e.g., `55.0.0-preview.2`), published under `@next` tag.
Check if latest is beta: https://exp.host/--/api/v2/versions (look for `-preview` in `expoVersion`)
```bash
npx expo install expo@next --fix # install beta
```
## Step-by-Step Upgrade Process
1. Upgrade Expo and dependencies
```bash
npx expo install expo@latest
npx expo install --fix
```
2. Run diagnostics: `npx expo-doctor`
3. Clear caches and reinstall
```bash
npx expo export -p ios --clear
rm -rf node_modules .expo
watchman watch-del-all
```
## Breaking Changes Checklist
- Check for removed APIs in release notes
- Update import paths for moved modules
- Review native module changes requiring prebuild
- Test all camera, audio, and video features
- Verify navigation still works correctly
## Prebuild for Native Changes
**First check if `ios/` and `android/` directories exist in the project.** If neither directory exists, the project uses Continuous Native Generation (CNG) and native projects are regenerated at build time — skip this section and "Clear caches for bare workflow" entirely.
If upgrading requires native changes:
```bash
npx expo prebuild --clean
```
This regenerates the `ios` and `android` directories. Ensure the project is not a bare workflow app before running this command.
## Clear caches for bare workflow
These steps only apply when `ios/` and/or `android/` directories exist in the project:
- Clear the cocoapods cache for iOS: `cd ios && pod install --repo-update`
- Clear derived data for Xcode: `npx expo run:ios --no-build-cache`
- Clear the Gradle cache for Android: `cd android && ./gradlew clean`
## Housekeeping
- Review release notes for the target SDK version at https://expo.dev/changelog
- If using Expo SDK 54 or later, ensure react-native-worklets is installed — this is required for react-native-reanimated to work.
- Enable React Compiler in SDK 54+ by adding `"experiments": { "reactCompiler": true }` to app.json — it's stable and recommended
- Delete sdkVersion from `app.json` to let Expo manage it automatically
- Remove implicit packages from `package.json`: `@babel/core`, `babel-preset-expo`, `expo-constants`.
- If the babel.config.js only contains 'babel-preset-expo', delete the file
- If the metro.config.js only contains expo defaults, delete the file
## Deprecated Packages
| Old Package | Replacement |
| -------------------- | ---------------------------------------------------- |
| `expo-av` | `expo-audio` and `expo-video` |
| `expo-permissions` | Individual package permission APIs |
| `@expo/vector-icons` | `expo-symbols` (for SF Symbols) |
| `AsyncStorage` | `expo-sqlite/localStorage/install` |
| `expo-app-loading` | `expo-splash-screen` |
| expo-linear-gradient | experimental_backgroundImage + CSS gradients in View |
When migrating deprecated packages, update all code usage before removing the old package. For expo-av, consult the migration references to convert Audio.Sound to useAudioPlayer, Audio.Recording to useAudioRecorder, and Video components to VideoView with useVideoPlayer.
## expo.install.exclude
Check if package.json has excluded packages:
```json
{
"expo": { "install": { "exclude": ["react-native-reanimated"] } }
}
```
Exclusions are often workarounds that may no longer be needed after upgrading. Review each one.
## Removing patches
Check if there are any outdated patches in the `patches/` directory. Remove them if they are no longer needed.
## Postcss
- `autoprefixer` isn't needed in SDK +53. Remove it from dependencies and check `postcss.config.js` or `postcss.config.mjs` to remove it from the plugins list.
- Use `postcss.config.mjs` in SDK +53.
## Metro
Remove redundant metro config options:
- resolver.unstable_enablePackageExports is enabled by default in SDK +53.
- `experimentalImportSupport` is enabled by default in SDK +54.
- `EXPO_USE_FAST_RESOLVER=1` is removed in SDK +54.
- cjs and mjs extensions are supported by default in SDK +50.
- Expo webpack is deprecated, migrate to [Expo Router and Metro web](https://docs.expo.dev/router/migrate/from-expo-webpack/).
## Hermes engine v1
Since SDK 55, users can opt-in to use Hermes engine v1 for improved runtime performance. This requires setting `useHermesV1: true` in the `expo-build-properties` config plugin, and may require a specific version of the `hermes-compiler` npm package. Hermes v1 will become a default in some future SDK release.
## New Architecture
The new architecture is enabled by default, the app.json field `"newArchEnabled": true` is no longer needed as it's the default. Expo Go only supports the new architecture as of SDK +53.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Upgrading Expo"
short_description: "Upgrade Expo SDKs, fix dependencies, adopt React 19 / React Compiler, and migrate deprecated Expo packages"
default_prompt: "Use $upgrading-expo to upgrade an Expo SDK, run diagnostics, fix dependency conflicts, decide whether prebuild/cache clearing applies, and migrate away from deprecated Expo packages."

View File

@@ -0,0 +1,132 @@
# Migrating from expo-av to expo-audio
## Imports
```tsx
// Before
import { Audio } from 'expo-av';
// After
import { useAudioPlayer, useAudioRecorder, RecordingPresets, AudioModule, setAudioModeAsync } from 'expo-audio';
```
## Audio Playback
### Before (expo-av)
```tsx
const [sound, setSound] = useState<Audio.Sound>();
async function playSound() {
const { sound } = await Audio.Sound.createAsync(require('./audio.mp3'));
setSound(sound);
await sound.playAsync();
}
useEffect(() => {
return sound ? () => { sound.unloadAsync(); } : undefined;
}, [sound]);
```
### After (expo-audio)
```tsx
const player = useAudioPlayer(require('./audio.mp3'));
// Play
player.play();
```
## Audio Recording
### Before (expo-av)
```tsx
const [recording, setRecording] = useState<Audio.Recording>();
async function startRecording() {
await Audio.requestPermissionsAsync();
await Audio.setAudioModeAsync({ allowsRecordingIOS: true, playsInSilentModeIOS: true });
const { recording } = await Audio.Recording.createAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
setRecording(recording);
}
async function stopRecording() {
await recording?.stopAndUnloadAsync();
const uri = recording?.getURI();
}
```
### After (expo-audio)
```tsx
const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
async function startRecording() {
await AudioModule.requestRecordingPermissionsAsync();
await recorder.prepareToRecordAsync();
recorder.record();
}
async function stopRecording() {
await recorder.stop();
const uri = recorder.uri;
}
```
## Audio Mode
### Before (expo-av)
```tsx
await Audio.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true,
staysActiveInBackground: true,
interruptionModeIOS: InterruptionModeIOS.DoNotMix,
});
```
### After (expo-audio)
```tsx
await setAudioModeAsync({
playsInSilentMode: true,
shouldPlayInBackground: true,
interruptionMode: 'doNotMix',
});
```
## API Mapping
| expo-av | expo-audio |
|---------|------------|
| `Audio.Sound.createAsync()` | `useAudioPlayer(source)` |
| `sound.playAsync()` | `player.play()` |
| `sound.pauseAsync()` | `player.pause()` |
| `sound.setPositionAsync(ms)` | `player.seekTo(seconds)` |
| `sound.setVolumeAsync(vol)` | `player.volume = vol` |
| `sound.setRateAsync(rate)` | `player.playbackRate = rate` |
| `sound.setIsLoopingAsync(loop)` | `player.loop = loop` |
| `sound.unloadAsync()` | Automatic via hook |
| `playbackStatus.positionMillis` | `player.currentTime` (seconds) |
| `playbackStatus.durationMillis` | `player.duration` (seconds) |
| `playbackStatus.isPlaying` | `player.playing` |
| `Audio.Recording.createAsync()` | `useAudioRecorder(preset)` |
| `Audio.RecordingOptionsPresets.*` | `RecordingPresets.*` |
| `recording.stopAndUnloadAsync()` | `recorder.stop()` |
| `recording.getURI()` | `recorder.uri` |
| `Audio.requestPermissionsAsync()` | `AudioModule.requestRecordingPermissionsAsync()` |
## Key Differences
- **No auto-reset on finish**: After `play()` completes, the player stays paused at the end. To replay, call `player.seekTo(0)` then `play()`
- **Time in seconds**: expo-audio uses seconds, not milliseconds (matching web standards)
- **Immediate loading**: Audio loads immediately when the hook mounts—no explicit preloading needed
- **Automatic cleanup**: No need to call `unloadAsync()`, hooks handle resource cleanup on unmount
- **Multiple players**: Create multiple `useAudioPlayer` instances and store them—all load immediately
- **Direct property access**: Set volume, rate, loop directly on the player object (`player.volume = 0.5`)
## API Reference
https://docs.expo.dev/versions/latest/sdk/audio/

View File

@@ -0,0 +1,160 @@
# Migrating from expo-av to expo-video
## Imports
```tsx
// Before
import { Video, ResizeMode } from 'expo-av';
// After
import { useVideoPlayer, VideoView, VideoSource } from 'expo-video';
import { useEvent, useEventListener } from 'expo';
```
## Video Playback
### Before (expo-av)
```tsx
const videoRef = useRef<Video>(null);
const [status, setStatus] = useState({});
<Video
ref={videoRef}
source={{ uri: 'https://example.com/video.mp4' }}
style={{ width: 350, height: 200 }}
resizeMode={ResizeMode.CONTAIN}
isLooping
onPlaybackStatusUpdate={setStatus}
/>
// Control
videoRef.current?.playAsync();
videoRef.current?.pauseAsync();
```
### After (expo-video)
```tsx
const player = useVideoPlayer('https://example.com/video.mp4', player => {
player.loop = true;
});
const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
<VideoView
player={player}
style={{ width: 350, height: 200 }}
contentFit="contain"
/>
// Control
player.play();
player.pause();
```
## Status Updates
### Before (expo-av)
```tsx
<Video
onPlaybackStatusUpdate={status => {
if (status.isLoaded) {
console.log(status.positionMillis, status.durationMillis, status.isPlaying);
if (status.didJustFinish) console.log('finished');
}
}}
/>
```
### After (expo-video)
```tsx
// Reactive state
const { isPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
// Side effects
useEventListener(player, 'playToEnd', () => console.log('finished'));
// Direct access
console.log(player.currentTime, player.duration, player.playing);
```
## Local Files
### Before (expo-av)
```tsx
<Video source={require('./video.mp4')} />
```
### After (expo-video)
```tsx
const player = useVideoPlayer({ assetId: require('./video.mp4') });
```
## Fullscreen and PiP
```tsx
<VideoView
player={player}
allowsFullscreen
allowsPictureInPicture
onFullscreenEnter={() => {}}
onFullscreenExit={() => {}}
/>
```
For PiP and background playback, add to app.json:
```json
{
"expo": {
"plugins": [
["expo-video", { "supportsBackgroundPlayback": true, "supportsPictureInPicture": true }]
]
}
}
```
## API Mapping
| expo-av | expo-video |
|---------|------------|
| `<Video>` | `<VideoView>` |
| `ref={videoRef}` | `player={useVideoPlayer()}` |
| `source={{ uri }}` | Pass to `useVideoPlayer(uri)` |
| `resizeMode={ResizeMode.CONTAIN}` | `contentFit="contain"` |
| `isLooping` | `player.loop = true` |
| `shouldPlay` | `player.play()` in setup |
| `isMuted` | `player.muted = true` |
| `useNativeControls` | `nativeControls={true}` |
| `onPlaybackStatusUpdate` | `useEvent` / `useEventListener` |
| `videoRef.current.playAsync()` | `player.play()` |
| `videoRef.current.pauseAsync()` | `player.pause()` |
| `videoRef.current.replayAsync()` | `player.replay()` |
| `videoRef.current.setPositionAsync(ms)` | `player.currentTime = seconds` |
| `status.positionMillis` | `player.currentTime` (seconds) |
| `status.durationMillis` | `player.duration` (seconds) |
| `status.didJustFinish` | `useEventListener(player, 'playToEnd')` |
## Key Differences
- **Separate player and view**: Player logic decoupled from the view—one player can be used across multiple views
- **Time in seconds**: Uses seconds, not milliseconds
- **Event system**: Uses `useEvent`/`useEventListener` from `expo` instead of callback props
- **Video preloading**: Create a player without mounting a VideoView to preload for faster transitions
- **Built-in caching**: Set `useCaching: true` in VideoSource for persistent offline caching
## Known Issues
- **Uninstall expo-av first**: On Android, having both expo-av and expo-video installed can cause VideoView to show a black screen. Uninstall expo-av before installing expo-video
- **Android: Reusing players**: Mounting the same player in multiple VideoViews simultaneously can cause black screens on Android (works on iOS)
- **Android: currentTime in setup**: Setting `player.currentTime` in the `useVideoPlayer` setup callback may not work on Android—set it after mount instead
- **Changing source**: Use `player.replace(newSource)` to change videos without recreating the player
## API Reference
https://docs.expo.dev/versions/latest/sdk/video/

View File

@@ -0,0 +1,124 @@
# Native Tabs Migration (SDK 55)
In SDK 55, `Label`, `Icon`, `Badge`, and `VectorIcon` are now accessed as static properties on `NativeTabs.Trigger` rather than separate imports.
## Import Changes
```tsx
// SDK 53/54
import {
NativeTabs,
Icon,
Label,
Badge,
VectorIcon,
} from "expo-router/unstable-native-tabs";
// SDK 55+
import { NativeTabs } from "expo-router/unstable-native-tabs";
```
## Component Changes
| SDK 53/54 | SDK 55+ |
| ---------------- | ----------------------------------- |
| `<Icon />` | `<NativeTabs.Trigger.Icon />` |
| `<Label />` | `<NativeTabs.Trigger.Label />` |
| `<Badge />` | `<NativeTabs.Trigger.Badge />` |
| `<VectorIcon />` | `<NativeTabs.Trigger.VectorIcon />` |
| (n/a) | `<NativeTabs.BottomAccessory />` |
## New in SDK 55
### BottomAccessory
New component for Apple Music-style mini players on iOS +26 that float above the tab bar:
```tsx
<NativeTabs>
<NativeTabs.BottomAccessory>
{/* Content above tabs */}
</NativeTabs.BottomAccessory>
<NativeTabs.Trigger name="(index)">
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
</NativeTabs>
```
On Android and web, this component will render as a no-op. Position a view absolutely above the tab bar instead.
### Icon `md` Prop
New `md` prop for Material icon glyphs on Android (alongside existing `drawable`):
```tsx
<NativeTabs.Trigger.Icon sf="house" md="home" />
```
## Full Migration Example
### Before (SDK 53/54)
```tsx
import {
NativeTabs,
Icon,
Label,
Badge,
} from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
<NativeTabs minimizeBehavior="onScrollDown">
<NativeTabs.Trigger name="(index)">
<Label>Home</Label>
<Icon sf="house.fill" />
<Badge>3</Badge>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(settings)">
<Label>Settings</Label>
<Icon sf="gear" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search">
<Label>Search</Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
```
### After (SDK 55+)
```tsx
import { NativeTabs } from "expo-router/unstable-native-tabs";
export default function TabLayout() {
return (
<NativeTabs minimizeBehavior="onScrollDown">
<NativeTabs.Trigger name="(index)">
<NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="house.fill" md="home" />
<NativeTabs.Trigger.Badge>3</NativeTabs.Trigger.Badge>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(settings)">
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Icon sf="gear" md="settings" />
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search">
<NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label>
</NativeTabs.Trigger>
</NativeTabs>
);
}
```
## Migration Checklist
1. Remove `Icon`, `Label`, `Badge`, `VectorIcon` from imports
2. Keep only `NativeTabs` import from `expo-router/unstable-native-tabs`
3. Replace `<Icon />` with `<NativeTabs.Trigger.Icon />`
4. Replace `<Label />` with `<NativeTabs.Trigger.Label />`
5. Replace `<Badge />` with `<NativeTabs.Trigger.Badge />`
6. Replace `<VectorIcon />` with `<NativeTabs.Trigger.VectorIcon />`
- Read docs for more info https://docs.expo.dev/versions/v55.0.0/sdk/router-native-tabs/

View File

@@ -0,0 +1,79 @@
# New Architecture
The New Architecture is enabled by default in Expo SDK 53+. It replaces the legacy bridge with a faster, synchronous communication layer between JavaScript and native code.
## Documentation
Full guide: https://docs.expo.dev/guides/new-architecture/
## What Changed
- **JSI (JavaScript Interface)** — Direct synchronous calls between JS and native
- **Fabric** — New rendering system with concurrent features
- **TurboModules** — Lazy-loaded native modules with type safety
## SDK Compatibility
| SDK Version | New Architecture Status |
| ----------- | ----------------------- |
| SDK 53+ | Enabled by default |
| SDK 52 | Opt-in via app.json |
| SDK 51- | Experimental |
## Configuration
New Architecture is enabled by default. To explicitly disable (not recommended):
```json
{
"expo": {
"newArchEnabled": false
}
}
```
## Expo Go
Expo Go only supports the New Architecture as of SDK 53. Apps using the old architecture must use development builds.
## Common Migration Issues
### Native Module Compatibility
Some older native modules may not support the New Architecture. Check:
1. Module documentation for New Architecture support
2. GitHub issues for compatibility discussions
3. Consider alternatives if module is unmaintained
### Reanimated
React Native Reanimated requires `react-native-worklets` in SDK 54+:
```bash
npx expo install react-native-worklets
```
### Layout Animations
Some layout animations behave differently. Test thoroughly after upgrading.
## Verifying New Architecture
Check if New Architecture is active:
```tsx
import { Platform } from "react-native";
// Returns true if Fabric is enabled
const isNewArch = global._IS_FABRIC !== undefined;
```
Verify from the command line if the currently running app uses the New Architecture: `bunx xcobra expo eval "_IS_FABRIC"` -> `true`
## Troubleshooting
1. **Clear caches**`npx expo start --clear`
2. **Clean prebuild**`npx expo prebuild --clean`
3. **Check native modules** — Ensure all dependencies support New Architecture
4. **Review console warnings** — Legacy modules log compatibility warnings

View File

@@ -0,0 +1,79 @@
# React 19
React 19 is included in Expo SDK 54. This release simplifies several common patterns.
## Context Changes
### useContext → use
The `use` hook replaces `useContext`:
```tsx
// Before (React 18)
import { useContext } from "react";
const value = useContext(MyContext);
// After (React 19)
import { use } from "react";
const value = use(MyContext);
```
- The `use` hook can also read promises, enabling Suspense-based data fetching.
- `use` can be called conditionally, this simplifies components that consume multiple contexts.
### Context.Provider → Context
Context providers no longer need the `.Provider` suffix:
```tsx
// Before (React 18)
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
// After (React 19)
<ThemeContext value={theme}>
{children}
</ThemeContext>
```
## ref as a Prop
### Removing forwardRef
Components can now receive `ref` as a regular prop. `forwardRef` is no longer needed:
```tsx
// Before (React 18)
import { forwardRef } from "react";
const Input = forwardRef<TextInput, Props>((props, ref) => {
return <TextInput ref={ref} {...props} />;
});
// After (React 19)
function Input({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {
return <TextInput ref={ref} {...props} />;
}
```
### Migration Steps
1. Remove `forwardRef` wrapper
2. Add `ref` to the props destructuring
3. Update the type to include `ref?: React.Ref<T>`
## Other React 19 Features
- **Actions** — Functions that handle async transitions
- **useOptimistic** — Optimistic UI updates
- **useFormStatus** — Form submission state (web)
- **Document Metadata** — Native `<title>` and `<meta>` support (web)
## Cleanup Checklist
When upgrading to SDK 54:
- [ ] Replace `useContext` with `use`
- [ ] Remove `.Provider` from Context components
- [ ] Remove `forwardRef` wrappers, use `ref` prop instead

View File

@@ -0,0 +1,59 @@
# React Compiler
React Compiler is stable in Expo SDK 54 and later. It automatically memoizes components and hooks, eliminating the need for manual `useMemo`, `useCallback`, and `React.memo`.
## Enabling React Compiler
Add to `app.json`:
```json
{
"expo": {
"experiments": {
"reactCompiler": true
}
}
}
```
## What React Compiler Does
- Automatically memoizes components and values
- Eliminates unnecessary re-renders
- Removes the need for manual `useMemo` and `useCallback`
- Works with existing code without modifications
## Cleanup After Enabling
Once React Compiler is enabled, you can remove manual memoization:
```tsx
// Before (manual memoization)
const memoizedValue = useMemo(() => computeExpensive(a, b), [a, b]);
const memoizedCallback = useCallback(() => doSomething(a), [a]);
const MemoizedComponent = React.memo(MyComponent);
// After (React Compiler handles it)
const value = computeExpensive(a, b);
const callback = () => doSomething(a);
// Just use MyComponent directly
```
## Requirements
- Expo SDK 54 or later
- New Architecture enabled (default in SDK 54+)
## Verifying It's Working
React Compiler runs at build time. Check the Metro bundler output for compilation messages. You can also use React DevTools to verify components are being optimized.
## Troubleshooting
If you encounter issues:
1. Ensure New Architecture is enabled
2. Clear Metro cache: `npx expo start --clear`
3. Check for incompatible patterns in your code (rare)
React Compiler is designed to work with idiomatic React code. If it can't safely optimize a component, it skips that component without breaking your app.

View File

@@ -0,0 +1,61 @@
# Migrating from react-navigation to expo-router
In SDK 56+, application code must not import from `@react-navigation/*` directly. Repoint those imports to the matching `expo-router` entry points. Runtime API is unchanged — only the module specifiers move.
## Steps
1. Prefer the automated codemod (see below). If it is not viable, fall back to the manual mapping.
2. Replace imports using the table. Use the entry point that matches the `@react-navigation/*` source.
3. After rewriting, check whether any of the rewritten imports are deprecated in `expo-router` (see [Check for deprecated imports](#check-for-deprecated-imports)). If so, surface the deprecation reason and the suggested replacement to the user before continuing.
4. Validate: search for remaining `@react-navigation/` references in source files, then run typecheck/build/start.
5. Remove `@react-navigation/*` packages that are no longer imported from `package.json` and reinstall (delete `node_modules` if needed).
## Automated migration (preferred)
Run from the project root over your application code (replace `src` with the actual directory or glob):
```sh
npx expo-codemod sdk-56-expo-router-react-navigation-replace src
```
```sh
npx expo-codemod sdk-56-expo-router-react-navigation-replace '**/*.{ts,tsx,js,jsx}'
```
## Manual API mapping
| React Navigation source | Expo Router target |
| ------------------------------------- | ------------------------------------------------------------------------ |
| `@react-navigation/native` | `expo-router/react-navigation` |
| `@react-navigation/core` | `expo-router/react-navigation` |
| `@react-navigation/elements` | `expo-router/react-navigation` |
| `@react-navigation/routers` | `expo-router/react-navigation` |
| `@react-navigation/stack` | `expo-router/js-stack` |
| `@react-navigation/bottom-tabs` | `expo-router/js-tabs` |
| `@react-navigation/material-top-tabs` | `expo-router/js-top-tabs` |
| `@react-navigation/native-stack` | No direct equivalent. Use the `Stack` layout from `expo-router` instead. |
**Stack caveat:** Do NOT rewrite `import { Stack } from "expo-router"` to `expo-router/js-stack`. The root `Stack` is the Expo Router layout component used in route files; only use `expo-router/js-stack` when replacing a `@react-navigation/stack` JS stack navigator.
If you encounter a symbol that has no replacement, ask the user to file an issue in the `expo/expo` repository describing what is needed and why.
## Check for deprecated imports
A successful rewrite to `expo-router/*` does not guarantee the new import is the recommended one. Some symbols are re-exported as deprecated shims and the project may need to migrate further (for example, to a different `expo-router` API or to a first-party Expo package).
For each symbol rewritten in step 2:
1. Resolve the rewritten module to its source in `node_modules` (e.g., `node_modules/expo-router/build/react-navigation.d.ts`, `js-stack`, `js-tabs`, `js-top-tabs`).
2. Look for a `@deprecated` JSDoc tag on the named export, or a runtime deprecation warning in the implementation file.
3. If deprecated, capture both the reason and the recommended replacement from the JSDoc/comment.
4. Report each deprecated symbol to the user with: the import path, the symbol, the deprecation reason, and the suggested replacement. Wait for the user to confirm before mass-applying further changes.
## Done when
1. No `@react-navigation/*` imports remain in source files.
2. No unused `@react-navigation/*` entries remain in `package.json`.
3. Typecheck and bundler start without `@react-navigation/*` errors.
## Reference
- Official Expo Router SDK 55 → 56 migration guide: https://docs.expo.dev/router/migrate/sdk-55-to-56

20
.zed/settings.json Normal file
View File

@@ -0,0 +1,20 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"languages": {
"TypeScript": {
"formatter": { "language_server": { "name": "oxfmt" } }
},
"TSX": {
"formatter": { "language_server": { "name": "oxfmt" } }
},
"JavaScript": {
"formatter": { "language_server": { "name": "oxfmt" } }
},
"JSX": {
"formatter": { "language_server": { "name": "oxfmt" } }
}
}
}

View File

@@ -15,6 +15,8 @@
"create-admin": "bun run src/scripts/create-admin.ts" "create-admin": "bun run src/scripts/create-admin.ts"
}, },
"dependencies": { "dependencies": {
"@better-auth/core": "^1.6.20",
"@better-auth/expo": "^1.6.20",
"@earendil-works/pi-coding-agent": "^0.79.1", "@earendil-works/pi-coding-agent": "^0.79.1",
"@freya/agent-protocol": "workspace:*", "@freya/agent-protocol": "workspace:*",
"@freya/core": "workspace:*", "@freya/core": "workspace:*",
@@ -29,7 +31,7 @@
"@nym.sh/jrpc": "^0.1.0", "@nym.sh/jrpc": "^0.1.0",
"@openrouter/sdk": "^0.9.11", "@openrouter/sdk": "^0.9.11",
"arktype": "^2.1.29", "arktype": "^2.1.29",
"better-auth": "^1", "better-auth": "^1.6.20",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"hono": "^4", "hono": "^4",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",

View File

@@ -1,3 +1,4 @@
import { ConversationEntryKind } from "@freya/core"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import type { AppendConversationEntryInput } from "../conversations/storage.ts" import type { AppendConversationEntryInput } from "../conversations/storage.ts"
@@ -6,7 +7,6 @@ import type {
ConversationStorageEntry, ConversationStorageEntry,
} from "./conversation-recording-query-agent.ts" } from "./conversation-recording-query-agent.ts"
import { ConversationEntryKind } from "../conversations/types.ts"
import { ConversationRecordingQueryAgent } from "./conversation-recording-query-agent.ts" import { ConversationRecordingQueryAgent } from "./conversation-recording-query-agent.ts"
import { import {
createQueryAgentEventListeners, createQueryAgentEventListeners,

View File

@@ -1,12 +1,13 @@
import type { ConversationEntryMetadata } from "@freya/core"
import { ConversationEntryKind } from "@freya/core"
import { randomUUID } from "node:crypto" import { randomUUID } from "node:crypto"
import type { import type {
AppendConversationEntryInput, AppendConversationEntryInput,
ConversationEntryRow, ConversationEntryRow,
} from "../conversations/storage.ts" } from "../conversations/storage.ts"
import type { ConversationEntryMetadata } from "../conversations/types.ts"
import { ConversationEntryKind } from "../conversations/types.ts"
import { import {
createQueryAgentEventListeners, createQueryAgentEventListeners,
QueryAgentEvent, QueryAgentEvent,
@@ -19,6 +20,7 @@ import {
type QueryAgentStreamEvent, type QueryAgentStreamEvent,
} from "./query-agent.ts" } from "./query-agent.ts"
/** Storage operations used to persist and replay query-agent conversation entries. */
export interface ConversationStorage { export interface ConversationStorage {
getOrCreateConversation(): Promise<{ id: string }> getOrCreateConversation(): Promise<{ id: string }>
appendEntry( appendEntry(
@@ -28,11 +30,13 @@ export interface ConversationStorage {
listEntries(conversationId: string): Promise<ConversationStorageEntry[]> listEntries(conversationId: string): Promise<ConversationStorageEntry[]>
} }
/** Minimal persisted entry shape needed by recording and replay agents. */
export type ConversationStorageEntry = Pick< export type ConversationStorageEntry = Pick<
ConversationEntryRow, ConversationEntryRow,
"id" | "sequence" | "kind" | "payload" | "metadata" | "createdAt" "id" | "sequence" | "kind" | "payload" | "metadata" | "createdAt"
> >
/** Configuration for wrapping a QueryAgent with conversation recording. */
export interface ConversationRecordingQueryAgentConfig { export interface ConversationRecordingQueryAgentConfig {
agent: QueryAgent agent: QueryAgent
storage: ConversationStorage storage: ConversationStorage

View File

@@ -1,9 +1,9 @@
import { ConversationEntryKind } from "@freya/core"
import { beforeEach, describe, expect, mock, test } from "bun:test" import { beforeEach, describe, expect, mock, test } from "bun:test"
import type { QueryAgentToolbox } from "./query-agent-toolbox.ts" import type { QueryAgentToolbox } from "./query-agent-toolbox.ts"
import type { QueryAgentStreamEvent } from "./query-agent.ts" import type { QueryAgentStreamEvent } from "./query-agent.ts"
import { ConversationEntryKind } from "../conversations/types.ts"
import { QueryAgentEvent } from "./query-agent.ts" import { QueryAgentEvent } from "./query-agent.ts"
interface FakePiSession { interface FakePiSession {

View File

@@ -33,13 +33,25 @@ import {
import { createSessionManager } from "./session-manager.ts" import { createSessionManager } from "./session-manager.ts"
import { createFreyaAgentTools, FREYA_AGENT_TOOL_NAMES } from "./tools.ts" import { createFreyaAgentTools, FREYA_AGENT_TOOL_NAMES } from "./tools.ts"
/** Active Pi SDK session instance returned by createAgentSession. */
type PiSession = Awaited<ReturnType<typeof createAgentSession>>["session"] type PiSession = Awaited<ReturnType<typeof createAgentSession>>["session"]
/** Pi event emitted when a message finishes. */
type PiMessageEndEvent = Extract<AgentSessionEvent, { type: "message_end" }> type PiMessageEndEvent = Extract<AgentSessionEvent, { type: "message_end" }>
/** Message payload carried by Pi's message-end event. */
type PiAgentMessage = PiMessageEndEvent["message"] type PiAgentMessage = PiMessageEndEvent["message"]
/** Pi event emitted when an agent run finishes. */
type PiAgentEndEvent = Extract<AgentSessionEvent, { type: "agent_end" }> type PiAgentEndEvent = Extract<AgentSessionEvent, { type: "agent_end" }>
/** Session manager created for Pi conversation replay. */
type PiSessionManager = ReturnType<typeof createSessionManager> type PiSessionManager = ReturnType<typeof createSessionManager>
/** Message shape accepted by the replay session manager. */
type PiSessionMessage = Parameters<PiSessionManager["appendMessage"]>[0] type PiSessionMessage = Parameters<PiSessionManager["appendMessage"]>[0]
/** Configuration for the Pi-backed query agent. */
export interface PiQueryAgentConfig { export interface PiQueryAgentConfig {
toolbox: QueryAgentToolbox toolbox: QueryAgentToolbox
apiKey?: string apiKey?: string

View File

@@ -1,8 +1,8 @@
import { ConversationEntryKind } from "@freya/core"
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import type { ConversationStorageEntry } from "./conversation-recording-query-agent.ts" import type { ConversationStorageEntry } from "./conversation-recording-query-agent.ts"
import { ConversationEntryKind } from "../conversations/types.ts"
import { createSessionManager } from "./session-manager.ts" import { createSessionManager } from "./session-manager.ts"
describe("createSessionManager", () => { describe("createSessionManager", () => {

View File

@@ -1,18 +1,21 @@
import { SessionManager } from "@earendil-works/pi-coding-agent" import { SessionManager } from "@earendil-works/pi-coding-agent"
import { tmpdir } from "node:os"
import type { ConversationStorageEntry } from "./conversation-recording-query-agent.ts"
import { import {
AssistantMessagePayload, AssistantMessagePayload,
ContextSummaryPayload, ContextSummaryPayload,
ConversationEntryKind, ConversationEntryKind,
UserMessagePayload, UserMessagePayload,
} from "../conversations/types.ts" } from "@freya/core"
import { tmpdir } from "node:os"
import type { ConversationStorageEntry } from "./conversation-recording-query-agent.ts"
/** Message shape accepted by Pi's SessionManager.appendMessage API. */
type PiMessage = Parameters<SessionManager["appendMessage"]>[0] type PiMessage = Parameters<SessionManager["appendMessage"]>[0]
/** Assistant message variant required when replaying stored assistant entries. */
type PiAssistantMessage = Extract<PiMessage, { role: "assistant" }> type PiAssistantMessage = Extract<PiMessage, { role: "assistant" }>
/** Inputs required to rebuild a Pi session manager from stored conversation entries. */
export interface CreateSessionManagerInput { export interface CreateSessionManagerInput {
cwd?: string cwd?: string
entries: ConversationStorageEntry[] entries: ConversationStorageEntry[]

View File

@@ -1,3 +1,4 @@
import { expo } from "@better-auth/expo"
import { betterAuth } from "better-auth" import { betterAuth } from "better-auth"
import { drizzleAdapter } from "better-auth/adapters/drizzle" import { drizzleAdapter } from "better-auth/adapters/drizzle"
import { admin } from "better-auth/plugins" import { admin } from "better-auth/plugins"
@@ -32,7 +33,7 @@ export function createAuth(db: Database) {
}, },
}, },
}, },
plugins: [admin()], plugins: [admin(), expo()],
}) })
} }

View File

@@ -0,0 +1,11 @@
export class ConversationNotFoundError extends Error {
readonly conversationId: string
readonly userId: string
constructor(conversationId: string, userId: string) {
super(`Conversation "${conversationId}" not found for user "${userId}"`)
this.name = "ConversationNotFoundError"
this.conversationId = conversationId
this.userId = userId
}
}

View File

@@ -1,21 +1,55 @@
import { ConversationEntryKind, ConversationEntryVisibility } from "@freya/core"
import { beforeEach, describe, expect, mock, test } from "bun:test" import { beforeEach, describe, expect, mock, test } from "bun:test"
import { Hono } from "hono" import { Hono } from "hono"
import type { Database } from "../db/index.ts" import type { Database } from "../db/index.ts"
import type { ConversationRow } from "./storage.ts" import type {
ConversationEntryRow,
ConversationRow,
ListConversationEntriesParams,
} from "./storage.ts"
import { mockAuthSessionMiddleware } from "../auth/session-middleware.ts" import { mockAuthSessionMiddleware } from "../auth/session-middleware.ts"
import { ConversationNotFoundError } from "./errors.ts"
import { registerConversationsHttpHandlers } from "./http.ts" import { registerConversationsHttpHandlers } from "./http.ts"
const MockUserId = "k7Gx2mPqRvNwYs9TdLfA4bHcJeUo1iZn" const MockUserId = "k7Gx2mPqRvNwYs9TdLfA4bHcJeUo1iZn"
const ConversationId = "11111111-1111-4111-8111-111111111111"
const MissingConversationId = "22222222-2222-4222-8222-222222222222"
const conversationRowsByUser = new Map<string, ConversationRow[]>() const conversationRowsByUser = new Map<string, ConversationRow[]>()
const conversationEntryRowsByUserAndConversation = new Map<string, ConversationEntryRow[]>()
const listEntriesCalls: Array<{
userId: string
conversationId: string
params: ListConversationEntriesParams
}> = []
mock.module("./storage.ts", () => ({ mock.module("./storage.ts", () => ({
conversations: (_db: Database, userId: string) => ({ conversations: (_db: Database, userId: string) => ({
async listConversations(): Promise<ConversationRow[]> { async listConversations(): Promise<ConversationRow[]> {
return conversationRowsByUser.get(userId) ?? [] return conversationRowsByUser.get(userId) ?? []
}, },
async listEntries(
conversationId: string,
params: ListConversationEntriesParams = {},
): Promise<ConversationEntryRow[]> {
listEntriesCalls.push({ userId, conversationId, params })
const rows = conversationEntryRowsByUserAndConversation.get(
conversationEntriesKey(userId, conversationId),
)
if (!rows) {
throw new ConversationNotFoundError(conversationId, userId)
}
if (params.visibility) {
return rows.filter((row) => row.visibility === params.visibility)
}
return rows
},
}), }),
})) }))
@@ -44,9 +78,39 @@ function createConversationRow(
} }
} }
function createConversationEntryRow(
id: string,
conversationId: string,
sequence: number,
kind: ConversationEntryRow["kind"],
visibility: ConversationEntryRow["visibility"],
payload: ConversationEntryRow["payload"],
createdAt: string,
metadata: ConversationEntryRow["metadata"] = {},
fileId: string | null = null,
): ConversationEntryRow {
return {
id,
conversationId,
sequence,
kind,
visibility,
fileId,
payload,
metadata,
createdAt: new Date(createdAt),
}
}
function conversationEntriesKey(userId: string, conversationId: string): string {
return `${userId}:${conversationId}`
}
describe("GET /api/conversations", () => { describe("GET /api/conversations", () => {
beforeEach(() => { beforeEach(() => {
conversationRowsByUser.clear() conversationRowsByUser.clear()
conversationEntryRowsByUserAndConversation.clear()
listEntriesCalls.length = 0
}) })
test("returns 401 without auth", async () => { test("returns 401 without auth", async () => {
@@ -108,3 +172,162 @@ describe("GET /api/conversations", () => {
}) })
}) })
}) })
describe("GET /api/conversations/:id/entries", () => {
beforeEach(() => {
conversationRowsByUser.clear()
conversationEntryRowsByUserAndConversation.clear()
listEntriesCalls.length = 0
})
test("returns 401 without auth", async () => {
const app = buildTestApp()
const res = await app.request("/api/conversations/conversation-1/entries")
expect(res.status).toBe(401)
})
test("returns user-visible entries for the authenticated user", async () => {
conversationEntryRowsByUserAndConversation.set(
conversationEntriesKey(MockUserId, ConversationId),
[
createConversationEntryRow(
"entry-user",
ConversationId,
1,
ConversationEntryKind.UserMessage,
ConversationEntryVisibility.UserVisible,
{
role: "user",
parts: [{ type: "text", text: "What is on today?" }],
},
"2026-06-17T09:30:00.000Z",
),
createConversationEntryRow(
"entry-tool",
ConversationId,
2,
ConversationEntryKind.ToolCall,
ConversationEntryVisibility.Internal,
{
toolName: "freya_list_context",
input: {},
},
"2026-06-17T09:30:01.000Z",
),
createConversationEntryRow(
"entry-assistant",
ConversationId,
3,
ConversationEntryKind.AssistantMessage,
ConversationEntryVisibility.UserVisible,
{
role: "assistant",
parts: [{ type: "text", text: "You have two calendar events." }],
},
"2026-06-17T09:30:02.000Z",
{ runId: "run-1" },
),
],
)
const app = buildTestApp("user-1")
const res = await app.request(`/api/conversations/${ConversationId}/entries`)
expect(res.status).toBe(200)
expect(listEntriesCalls).toEqual([
{
userId: MockUserId,
conversationId: ConversationId,
params: { visibility: ConversationEntryVisibility.UserVisible },
},
])
const body = (await res.json()) as { entries: unknown[] }
expect(body).toEqual({
entries: [
{
id: "entry-user",
conversationId: ConversationId,
sequence: 1,
kind: ConversationEntryKind.UserMessage,
visibility: ConversationEntryVisibility.UserVisible,
fileId: null,
payload: {
role: "user",
parts: [{ type: "text", text: "What is on today?" }],
},
metadata: {},
createdAt: "2026-06-17T09:30:00.000Z",
},
{
id: "entry-assistant",
conversationId: ConversationId,
sequence: 3,
kind: ConversationEntryKind.AssistantMessage,
visibility: ConversationEntryVisibility.UserVisible,
fileId: null,
payload: {
role: "assistant",
parts: [{ type: "text", text: "You have two calendar events." }],
},
metadata: { runId: "run-1" },
createdAt: "2026-06-17T09:30:02.000Z",
},
],
})
})
test("returns an empty list when the conversation has no user-visible entries", async () => {
conversationEntryRowsByUserAndConversation.set(
conversationEntriesKey(MockUserId, ConversationId),
[
createConversationEntryRow(
"entry-tool",
ConversationId,
1,
ConversationEntryKind.ToolResult,
ConversationEntryVisibility.Internal,
{ toolCallId: "call-1", output: { ok: true } },
"2026-06-17T09:30:00.000Z",
),
],
)
const app = buildTestApp("user-1")
const res = await app.request(`/api/conversations/${ConversationId}/entries`)
expect(res.status).toBe(200)
const body = (await res.json()) as { entries: unknown[] }
expect(body).toEqual({ entries: [] })
})
test("returns 404 for malformed conversation ids without querying storage", async () => {
const app = buildTestApp("user-1")
const res = await app.request("/api/conversations/missing-conversation/entries")
expect(res.status).toBe(404)
expect(listEntriesCalls).toEqual([])
const body = (await res.json()) as { error: string }
expect(body).toEqual({ error: "Conversation not found" })
})
test("returns 404 when the conversation does not exist for the user", async () => {
const app = buildTestApp("user-1")
const res = await app.request(`/api/conversations/${MissingConversationId}/entries`)
expect(res.status).toBe(404)
expect(listEntriesCalls).toEqual([
{
userId: MockUserId,
conversationId: MissingConversationId,
params: { visibility: ConversationEntryVisibility.UserVisible },
},
])
const body = (await res.json()) as { error: string }
expect(body).toEqual({ error: "Conversation not found" })
})
})

View File

@@ -1,23 +1,38 @@
import type { Context, Hono } from "hono" import type { Context, Hono } from "hono"
import { ConversationEntryVisibility } from "@freya/core"
import { type } from "arktype"
import { createMiddleware } from "hono/factory" import { createMiddleware } from "hono/factory"
import type { AuthSessionMiddleware } from "../auth/session-middleware.ts" import type { AuthSessionMiddleware } from "../auth/session-middleware.ts"
import type { Database } from "../db/index.ts" import type { Database } from "../db/index.ts"
import type { ConversationRow } from "./storage.ts"
import { ConversationNotFoundError } from "./errors.ts"
import { conversations } from "./storage.ts" import { conversations } from "./storage.ts"
/** Hono environment populated by the conversations route middleware. */
type Env = { type Env = {
Variables: { Variables: {
db: Database db: Database
} }
} }
/** Serialized conversation summary returned by the list endpoint. */
interface ConversationSummaryResponse {
id: string
createdAt: string
updatedAt: string
}
/** Dependencies required to register conversation HTTP handlers. */
interface ConversationsHttpHandlersDeps { interface ConversationsHttpHandlersDeps {
db: Database db: Database
authSessionMiddleware: AuthSessionMiddleware authSessionMiddleware: AuthSessionMiddleware
} }
const ConversationIdParam = type("string.uuid")
export function registerConversationsHttpHandlers( export function registerConversationsHttpHandlers(
app: Hono, app: Hono,
{ db, authSessionMiddleware }: ConversationsHttpHandlersDeps, { db, authSessionMiddleware }: ConversationsHttpHandlersDeps,
@@ -28,6 +43,7 @@ export function registerConversationsHttpHandlers(
}) })
app.get("/api/conversations", inject, authSessionMiddleware, handleListConversations) app.get("/api/conversations", inject, authSessionMiddleware, handleListConversations)
app.get("/api/conversations/:id/entries", inject, authSessionMiddleware, handleListEntries)
} }
async function handleListConversations(c: Context<Env>) { async function handleListConversations(c: Context<Env>) {
@@ -35,10 +51,54 @@ async function handleListConversations(c: Context<Env>) {
const db = c.get("db") const db = c.get("db")
return c.json({ return c.json({
conversations: (await conversations(db, user.id).listConversations()).map((row) => ({ conversations: (await conversations(db, user.id).listConversations()).map(
serializeConversation,
),
})
}
async function handleListEntries(c: Context<Env>) {
const user = c.get("user")!
const db = c.get("db")
const conversationId = c.req.param("id")
if (!conversationId) {
return c.json({ error: "Conversation not found" }, 404)
}
const parsedConversationId = ConversationIdParam(conversationId)
if (parsedConversationId instanceof type.errors) {
return c.json({ error: "Conversation not found" }, 404)
}
try {
const entries = await conversations(db, user.id).listEntries(parsedConversationId, {
visibility: ConversationEntryVisibility.UserVisible,
})
return c.json({
entries: entries.map((row) => ({
id: row.id,
conversationId: row.conversationId,
sequence: row.sequence,
kind: row.kind,
visibility: row.visibility,
fileId: row.fileId,
payload: row.payload,
metadata: row.metadata,
createdAt: row.createdAt.toISOString(),
})),
})
} catch (err) {
if (err instanceof ConversationNotFoundError) {
return c.json({ error: "Conversation not found" }, 404)
}
throw err
}
}
function serializeConversation(row: ConversationRow): ConversationSummaryResponse {
return {
id: row.id, id: row.id,
createdAt: row.createdAt.toISOString(), createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(), updatedAt: row.updatedAt.toISOString(),
})), }
})
} }

View File

@@ -1,17 +1,18 @@
import {
AssistantMessagePayload,
AttachmentPayload,
ConversationEntryKind,
ConversationEntryVisibility,
ContextSummaryPayload,
ConversationEntryMetadata,
GenericObjectPayload,
UserMessagePayload,
type ConversationEntryPayload,
} from "@freya/core"
import { type } from "arktype"
import { and, asc, desc, eq } from "drizzle-orm" import { and, asc, desc, eq } from "drizzle-orm"
import type { Database } from "../db/index.ts" import type { Database } from "../db/index.ts"
import type {
AssistantMessagePayload,
AttachmentPayload,
ContextSummaryPayload,
ConversationEntryKind as ConversationEntryKindType,
ConversationEntryMetadata,
ConversationEntryPayload,
ConversationEntryVisibility as ConversationEntryVisibilityType,
GenericObjectPayload,
UserMessagePayload,
} from "./types.ts"
import { import {
conversationEntries, conversationEntries,
@@ -19,23 +20,21 @@ import {
files, files,
user, user,
} from "../db/schema.ts" } from "../db/schema.ts"
import { import { ConversationNotFoundError } from "./errors.ts"
ConversationEntryMetadata as ConversationEntryMetadataSchema,
AssistantMessagePayload as AssistantMessagePayloadSchema,
AttachmentPayload as AttachmentPayloadSchema,
ConversationEntryKind,
ConversationEntryKindInput,
ConversationEntryVisibility,
ConversationEntryVisibilityInput,
ContextSummaryPayload as ContextSummaryPayloadSchema,
GenericObjectPayload as GenericObjectPayloadSchema,
UserMessagePayload as UserMessagePayloadSchema,
} from "./types.ts"
const conversationEntryKind = type.enumerated(...Object.values(ConversationEntryKind))
const conversationEntryVisibility = type.enumerated(...Object.values(ConversationEntryVisibility))
/** Database row shape for a conversation owned by a user. */
export type ConversationRow = typeof conversationsTable.$inferSelect export type ConversationRow = typeof conversationsTable.$inferSelect
/** Database row shape for an entry in a conversation timeline. */
export type ConversationEntryRow = typeof conversationEntries.$inferSelect export type ConversationEntryRow = typeof conversationEntries.$inferSelect
/** Database row shape for an uploaded file referenced by conversations. */
export type FileRow = typeof files.$inferSelect export type FileRow = typeof files.$inferSelect
/** Input required to create a stored file record. */
export interface CreateFileInput { export interface CreateFileInput {
storageKey: string storageKey: string
originalName?: string originalName?: string
@@ -44,23 +43,27 @@ export interface CreateFileInput {
metadata?: Record<string, unknown> metadata?: Record<string, unknown>
} }
/** Input for creating a file and appending its attachment entry together. */
export interface AppendAttachmentEntryInput { export interface AppendAttachmentEntryInput {
file: CreateFileInput file: CreateFileInput
payload: AttachmentPayload payload: AttachmentPayload
visibility?: ConversationEntryVisibilityType visibility?: ConversationEntryVisibility
metadata?: ConversationEntryMetadata metadata?: ConversationEntryMetadata
} }
/** Result returned after a file-backed attachment entry is appended. */
export interface AppendAttachmentEntryResult { export interface AppendAttachmentEntryResult {
file: FileRow file: FileRow
entry: ConversationEntryRow entry: ConversationEntryRow
} }
/** Common fields accepted when appending any conversation entry. */
interface AppendConversationEntryBase { interface AppendConversationEntryBase {
visibility?: ConversationEntryVisibilityType visibility?: ConversationEntryVisibility
metadata?: ConversationEntryMetadata metadata?: ConversationEntryMetadata
} }
/** Discriminated input for appending any supported entry kind to a conversation. */
export type AppendConversationEntryInput = export type AppendConversationEntryInput =
| (AppendConversationEntryBase & { | (AppendConversationEntryBase & {
kind: typeof ConversationEntryKind.UserMessage kind: typeof ConversationEntryKind.UserMessage
@@ -91,12 +94,13 @@ export type AppendConversationEntryInput =
fileId?: never fileId?: never
}) })
/** Filters accepted when listing conversation entries. */
export interface ListConversationEntriesParams { export interface ListConversationEntriesParams {
visibility?: ConversationEntryVisibilityType visibility?: ConversationEntryVisibility
} }
export function conversations(db: Database, userId: string) { export function conversations(db: Database, userId: string) {
return { const storage = {
async createConversation(): Promise<ConversationRow> { async createConversation(): Promise<ConversationRow> {
return insertConversation(db, userId) return insertConversation(db, userId)
}, },
@@ -109,6 +113,18 @@ export function conversations(db: Database, userId: string) {
.orderBy(desc(conversationsTable.updatedAt), desc(conversationsTable.createdAt)) .orderBy(desc(conversationsTable.updatedAt), desc(conversationsTable.createdAt))
}, },
async getConversation(conversationId: string): Promise<ConversationRow | null> {
const rows = await db
.select()
.from(conversationsTable)
.where(
and(eq(conversationsTable.id, conversationId), eq(conversationsTable.userId, userId)),
)
.limit(1)
return rows[0] ?? null
},
async getOrCreateConversation(): Promise<ConversationRow> { async getOrCreateConversation(): Promise<ConversationRow> {
return db.transaction(async (tx) => { return db.transaction(async (tx) => {
await requireUserForUpdate(tx, userId) await requireUserForUpdate(tx, userId)
@@ -127,12 +143,12 @@ export function conversations(db: Database, userId: string) {
conversationId: string, conversationId: string,
input: AppendConversationEntryInput, input: AppendConversationEntryInput,
): Promise<ConversationEntryRow> { ): Promise<ConversationEntryRow> {
const kind = ConversationEntryKindInput.assert(input.kind) const kind = conversationEntryKind.assert(input.kind)
const visibility = ConversationEntryVisibilityInput.assert( const visibility = conversationEntryVisibility.assert(
input.visibility ?? defaultVisibilityForKind(kind), input.visibility ?? defaultVisibilityForKind(kind),
) )
const payload = payloadForKind(kind, input.payload) const payload = payloadForKind(kind, input.payload)
const metadata = ConversationEntryMetadataSchema.assert(input.metadata ?? {}) const metadata = ConversationEntryMetadata.assert(input.metadata ?? {})
let fileId: string | null = null let fileId: string | null = null
if (input.kind === ConversationEntryKind.Attachment) { if (input.kind === ConversationEntryKind.Attachment) {
@@ -141,7 +157,9 @@ export function conversations(db: Database, userId: string) {
} }
const rows = await db.transaction(async (tx) => { const rows = await db.transaction(async (tx) => {
await requireConversationForUpdate(tx, userId, conversationId) if (!(await findConversationForUpdate(tx, userId, conversationId))) {
throw new ConversationNotFoundError(conversationId, userId)
}
const sequence = await nextSequence(tx, conversationId) const sequence = await nextSequence(tx, conversationId)
const rows = await tx const rows = await tx
@@ -168,14 +186,16 @@ export function conversations(db: Database, userId: string) {
conversationId: string, conversationId: string,
input: AppendAttachmentEntryInput, input: AppendAttachmentEntryInput,
): Promise<AppendAttachmentEntryResult> { ): Promise<AppendAttachmentEntryResult> {
const payload = AttachmentPayloadSchema.assert(input.payload) const payload = AttachmentPayload.assert(input.payload)
const visibility = ConversationEntryVisibilityInput.assert( const visibility = conversationEntryVisibility.assert(
input.visibility ?? defaultVisibilityForKind(ConversationEntryKind.Attachment), input.visibility ?? defaultVisibilityForKind(ConversationEntryKind.Attachment),
) )
const metadata = ConversationEntryMetadataSchema.assert(input.metadata ?? {}) const metadata = ConversationEntryMetadata.assert(input.metadata ?? {})
return db.transaction(async (tx) => { return db.transaction(async (tx) => {
await requireConversationForUpdate(tx, userId, conversationId) if (!(await findConversationForUpdate(tx, userId, conversationId))) {
throw new ConversationNotFoundError(conversationId, userId)
}
const file = await insertFile(tx, userId, input.file) const file = await insertFile(tx, userId, input.file)
const sequence = await nextSequence(tx, conversationId) const sequence = await nextSequence(tx, conversationId)
@@ -204,7 +224,9 @@ export function conversations(db: Database, userId: string) {
conversationId: string, conversationId: string,
params: ListConversationEntriesParams = {}, params: ListConversationEntriesParams = {},
): Promise<ConversationEntryRow[]> { ): Promise<ConversationEntryRow[]> {
await requireConversation(db, userId, conversationId) if (!(await storage.getConversation(conversationId))) {
throw new ConversationNotFoundError(conversationId, userId)
}
if (params.visibility) { if (params.visibility) {
return db return db
@@ -226,25 +248,27 @@ export function conversations(db: Database, userId: string) {
.orderBy(asc(conversationEntries.sequence)) .orderBy(asc(conversationEntries.sequence))
}, },
} }
return storage
} }
function payloadForKind( function payloadForKind(
kind: ConversationEntryKindType, kind: ConversationEntryKind,
payload: AppendConversationEntryInput["payload"], payload: AppendConversationEntryInput["payload"],
): ConversationEntryPayload { ): ConversationEntryPayload {
switch (kind) { switch (kind) {
case ConversationEntryKind.UserMessage: case ConversationEntryKind.UserMessage:
return UserMessagePayloadSchema.assert(payload) return UserMessagePayload.assert(payload)
case ConversationEntryKind.AssistantMessage: case ConversationEntryKind.AssistantMessage:
return AssistantMessagePayloadSchema.assert(payload) return AssistantMessagePayload.assert(payload)
case ConversationEntryKind.Attachment: case ConversationEntryKind.Attachment:
return AttachmentPayloadSchema.assert(payload) return AttachmentPayload.assert(payload)
case ConversationEntryKind.ContextSummary: case ConversationEntryKind.ContextSummary:
return ContextSummaryPayloadSchema.assert(payload) return ContextSummaryPayload.assert(payload)
case ConversationEntryKind.ToolCall: case ConversationEntryKind.ToolCall:
case ConversationEntryKind.ToolResult: case ConversationEntryKind.ToolResult:
case ConversationEntryKind.SystemNote: case ConversationEntryKind.SystemNote:
return GenericObjectPayloadSchema.assert(payload) return GenericObjectPayload.assert(payload)
} }
} }
@@ -259,25 +283,11 @@ async function requireUserForUpdate(db: Database, userId: string): Promise<void>
requireRow(rows, `User not found: ${userId}`) requireRow(rows, `User not found: ${userId}`)
} }
async function requireConversation( async function findConversationForUpdate(
db: Database, db: Database,
userId: string, userId: string,
conversationId: string, conversationId: string,
): Promise<ConversationRow> { ): Promise<ConversationRow | null> {
const rows = await db
.select()
.from(conversationsTable)
.where(and(eq(conversationsTable.id, conversationId), eq(conversationsTable.userId, userId)))
.limit(1)
return requireRow(rows, `Conversation not found: ${conversationId}`)
}
async function requireConversationForUpdate(
db: Database,
userId: string,
conversationId: string,
): Promise<ConversationRow> {
const rows = await db const rows = await db
.select() .select()
.from(conversationsTable) .from(conversationsTable)
@@ -285,7 +295,7 @@ async function requireConversationForUpdate(
.limit(1) .limit(1)
.for("update") .for("update")
return requireRow(rows, `Conversation not found: ${conversationId}`) return rows[0] ?? null
} }
async function latestConversation(db: Database, userId: string): Promise<ConversationRow | null> { async function latestConversation(db: Database, userId: string): Promise<ConversationRow | null> {
@@ -364,9 +374,7 @@ function requireRow<T>(rows: T[], message = "Expected database row"): T {
return row return row
} }
function defaultVisibilityForKind( function defaultVisibilityForKind(kind: ConversationEntryKind): ConversationEntryVisibility {
kind: ConversationEntryKindType,
): ConversationEntryVisibilityType {
switch (kind) { switch (kind) {
case ConversationEntryKind.UserMessage: case ConversationEntryKind.UserMessage:
case ConversationEntryKind.AssistantMessage: case ConversationEntryKind.AssistantMessage:

View File

@@ -1,3 +1,10 @@
import {
ConversationEntryVisibility,
type ConversationEntryKind,
type ConversationEntryMetadata,
type ConversationEntryPayload,
type ConversationEntryVisibility as ConversationEntryVisibilityType,
} from "@freya/core"
import { sql } from "drizzle-orm" import { sql } from "drizzle-orm"
import { import {
boolean, boolean,
@@ -13,14 +20,6 @@ import {
uuid, uuid,
} from "drizzle-orm/pg-core" } from "drizzle-orm/pg-core"
import {
ConversationEntryVisibility,
type ConversationEntryKind,
type ConversationEntryMetadata,
type ConversationEntryPayload,
type ConversationEntryVisibility as ConversationEntryVisibilityType,
} from "../conversations/types.ts"
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Better Auth core tables // Better Auth core tables
// Re-exported from CLI-generated schema. // Re-exported from CLI-generated schema.

View File

@@ -11,6 +11,7 @@ import { registerAuthHandlers } from "./auth/http.ts"
import { createAuth } from "./auth/index.ts" import { createAuth } from "./auth/index.ts"
import { createRequireSession } from "./auth/session-middleware.ts" import { createRequireSession } from "./auth/session-middleware.ts"
import { CalDavSourceProvider } from "./caldav/provider.ts" import { CalDavSourceProvider } from "./caldav/provider.ts"
import { registerConversationsHttpHandlers } from "./conversations/http.ts"
import { createDatabase } from "./db/index.ts" import { createDatabase } from "./db/index.ts"
import { registerFeedHttpHandlers } from "./engine/http.ts" import { registerFeedHttpHandlers } from "./engine/http.ts"
import { createFeedEnhancer } from "./enhancement/enhance-feed.ts" import { createFeedEnhancer } from "./enhancement/enhance-feed.ts"
@@ -129,6 +130,7 @@ function main() {
sessionManager, sessionManager,
authSessionMiddleware, authSessionMiddleware,
}) })
registerConversationsHttpHandlers(app, { db, authSessionMiddleware })
if (isDebugMode) { if (isDebugMode) {
registerDebugAgentHttpHandlers(app, { registerDebugAgentHttpHandlers(app, {
authSessionMiddleware, authSessionMiddleware,

View File

@@ -1,5 +1,6 @@
import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@freya/core" import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@freya/core"
import { ConversationEntryKind } from "@freya/core"
import { LocationSource } from "@freya/source-location" import { LocationSource } from "@freya/source-location"
import { WeatherSource } from "@freya/source-weatherkit" import { WeatherSource } from "@freya/source-weatherkit"
import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test" import { beforeEach, describe, expect, mock, spyOn, test } from "bun:test"
@@ -9,7 +10,6 @@ import type { AppendConversationEntryInput } from "../conversations/storage.ts"
import type { Database } from "../db/index.ts" import type { Database } from "../db/index.ts"
import type { FeedSourceProvider } from "./feed-source-provider.ts" import type { FeedSourceProvider } from "./feed-source-provider.ts"
import { ConversationEntryKind } from "../conversations/types.ts"
import { CredentialEncryptor } from "../lib/crypto.ts" import { CredentialEncryptor } from "../lib/crypto.ts"
import { import {
CredentialStorageUnavailableError, CredentialStorageUnavailableError,

View File

@@ -1,5 +1,6 @@
import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@freya/core" import type { ActionDefinition, ContextEntry, FeedItem, FeedSource } from "@freya/core"
import { ConversationEntryKind } from "@freya/core"
import { LocationSource } from "@freya/source-location" import { LocationSource } from "@freya/source-location"
import { describe, expect, spyOn, test } from "bun:test" import { describe, expect, spyOn, test } from "bun:test"
@@ -9,7 +10,6 @@ import type {
} from "../agent/conversation-recording-query-agent.ts" } from "../agent/conversation-recording-query-agent.ts"
import type { AppendConversationEntryInput } from "../conversations/storage.ts" import type { AppendConversationEntryInput } from "../conversations/storage.ts"
import { ConversationEntryKind } from "../conversations/types.ts"
import { UserSession } from "./user-session.ts" import { UserSession } from "./user-session.ts"
function createStubSource(id: string, items: FeedItem[] = []): FeedSource { function createStubSource(id: string, items: FeedItem[] = []): FeedSource {

View File

@@ -255,7 +255,8 @@
} }
], ],
"expo-web-browser", "expo-web-browser",
"expo-image" "expo-image",
"expo-secure-store"
], ],
"experiments": { "experiments": {
"typedRoutes": true, "typedRoutes": true,

View File

@@ -6,12 +6,16 @@
"build": { "build": {
"development": { "development": {
"developmentClient": true, "developmentClient": true,
"distribution": "internal" "distribution": "internal",
"ios": {
"image": "sdk-56"
}
}, },
"development-simulator": { "development-simulator": {
"extends": "development", "extends": "development",
"ios": { "ios": {
"simulator": "true" "image": "sdk-56",
"simulator": true
} }
}, },
"preview": { "preview": {

View File

@@ -15,29 +15,45 @@
"debugger": "bun run scripts/open-debugger.ts" "debugger": "bun run scripts/open-debugger.ts"
}, },
"dependencies": { "dependencies": {
"@better-auth/core": "^1.6.20",
"@better-auth/expo": "^1.6.20",
"@expo-google-fonts/inter": "^0.4.2", "@expo-google-fonts/inter": "^0.4.2",
"@expo-google-fonts/source-serif-4": "^0.4.1", "@expo-google-fonts/source-serif-4": "^0.4.1",
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@freya/core": "workspace:*",
"@json-render/react-native": "^0.13.0", "@json-render/react-native": "^0.13.0",
"@react-native-masked-view/masked-view": "0.3.2",
"@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"arktype": "^2.2.1",
"better-auth": "^1.6.20",
"class-variance-authority": "^0.7.1",
"expo": "^56.0.0", "expo": "^56.0.0",
"expo-blur": "~56.0.3",
"expo-constants": "~56.0.18", "expo-constants": "~56.0.18",
"expo-dev-client": "~56.0.20", "expo-dev-client": "~56.0.20",
"expo-font": "~56.0.7", "expo-font": "~56.0.7",
"expo-glass-effect": "~0.1.10",
"expo-haptics": "~56.0.3", "expo-haptics": "~56.0.3",
"expo-image": "~56.0.11", "expo-image": "~56.0.11",
"expo-linear-gradient": "~56.0.4",
"expo-linking": "~56.0.14", "expo-linking": "~56.0.14",
"expo-location": "~56.0.18", "expo-location": "~56.0.18",
"expo-network": "~56.0.5",
"expo-router": "~56.2.11", "expo-router": "~56.2.11",
"expo-secure-store": "~56.0.4",
"expo-splash-screen": "~56.0.10", "expo-splash-screen": "~56.0.10",
"expo-status-bar": "~56.0.4", "expo-status-bar": "~56.0.4",
"expo-symbols": "~56.0.6", "expo-symbols": "~56.0.6",
"expo-system-ui": "~56.0.5", "expo-system-ui": "~56.0.5",
"expo-web-browser": "~56.0.5", "expo-web-browser": "~56.0.5",
"jotai": "^2.20.1",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-native": "0.85.3", "react-native": "0.85.3",
"react-native-easing-gradient": "^1.1.1",
"react-native-gesture-handler": "~2.31.1", "react-native-gesture-handler": "~2.31.1",
"react-native-keyboard-controller": "1.21.6",
"react-native-reanimated": "4.3.1", "react-native-reanimated": "4.3.1",
"react-native-safe-area-context": "~5.7.0", "react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2", "react-native-screens": "4.25.2",

View File

@@ -23,6 +23,8 @@ function forwardHeaders(headers: Headers): Headers {
interface WsData { interface WsData {
upstream: WebSocket upstream: WebSocket
upstreamUrl: string
path: string
isDevice: boolean isDevice: boolean
} }
@@ -42,8 +44,10 @@ Bun.serve<WsData>({
// WebSocket upgrade — bridge to Metro's ws endpoint // WebSocket upgrade — bridge to Metro's ws endpoint
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") { if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
const wsUrl = `${METRO_WS_BASE}${url.pathname}${url.search}` const path = `${url.pathname}${url.search}`
const upstream = new WebSocket(wsUrl) const wsUrl = `${METRO_WS_BASE}${path}`
console.log(`[proxy] ws connecting ${path}`)
const upstream = connectUpstreamWebSocket(wsUrl, getWebSocketHeaders(req, url))
// Wait for upstream to connect before upgrading the client // Wait for upstream to connect before upgrading the client
try { try {
@@ -56,7 +60,7 @@ Bun.serve<WsData>({
} }
const isDevice = url.pathname.startsWith("/inspector/device") const isDevice = url.pathname.startsWith("/inspector/device")
const ok = server.upgrade(req, { data: { upstream, isDevice } }) const ok = server.upgrade(req, { data: { upstream, upstreamUrl: wsUrl, path, isDevice } })
if (!ok) { if (!ok) {
upstream.close() upstream.close()
return new Response("WebSocket upgrade failed", { status: 500 }) return new Response("WebSocket upgrade failed", { status: 500 })
@@ -83,19 +87,28 @@ Bun.serve<WsData>({
websocket: { websocket: {
message(ws: ServerWebSocket<WsData>, msg) { message(ws: ServerWebSocket<WsData>, msg) {
ws.data.upstream.send(msg) sendUpstream(ws.data.upstream, msg)
}, },
open(ws: ServerWebSocket<WsData>) { open(ws: ServerWebSocket<WsData>) {
const { upstream } = ws.data const { upstream } = ws.data
console.log(`[proxy] ws open ${ws.data.path}`)
upstream.addEventListener("message", (ev) => { upstream.addEventListener("message", (ev) => {
if (typeof ev.data === "string") { if (typeof ev.data === "string") {
ws.send(ev.data) ws.send(ev.data)
} else if (ev.data instanceof ArrayBuffer) { } else if (ev.data instanceof ArrayBuffer) {
ws.sendBinary(new Uint8Array(ev.data)) ws.sendBinary(new Uint8Array(ev.data))
} else if (ev.data instanceof Blob) {
ev.data.arrayBuffer().then((buffer) => ws.sendBinary(new Uint8Array(buffer)))
} }
}) })
upstream.addEventListener("close", () => ws.close()) upstream.addEventListener("close", (ev) => {
upstream.addEventListener("error", () => ws.close()) console.log(`[proxy] upstream close ${ws.data.path} code=${ev.code} reason=${ev.reason}`)
ws.close(ev.code, ev.reason)
})
upstream.addEventListener("error", () => {
console.error(`[proxy] upstream error ${ws.data.upstreamUrl}`)
ws.close()
})
// Print debugger URL shortly after a device connects, // Print debugger URL shortly after a device connects,
// giving Metro time to register the target. // giving Metro time to register the target.
@@ -104,6 +117,7 @@ Bun.serve<WsData>({
} }
}, },
close(ws: ServerWebSocket<WsData>) { close(ws: ServerWebSocket<WsData>) {
console.log(`[proxy] client close ${ws.data.path}`)
ws.data.upstream.close() ws.data.upstream.close()
}, },
}, },
@@ -120,7 +134,7 @@ async function printDebuggerUrl() {
if (!Array.isArray(parsedTargets)) return if (!Array.isArray(parsedTargets)) return
const targets = parsedTargets.filter(isDebugTarget) const targets = parsedTargets.filter(isDebugTarget)
const target = targets.find((t) => t.reactNative?.capabilities?.prefersFuseboxFrontend) const target = targets.find(prefersFuseboxFrontend) ?? targets[0]
if (!target) return if (!target) return
const wsPath = getProxyWebSocketPath(target.webSocketDebuggerUrl) const wsPath = getProxyWebSocketPath(target.webSocketDebuggerUrl)
@@ -153,6 +167,29 @@ async function fetchUpstream(
} }
} }
function sendUpstream(upstream: WebSocket, msg: string | Buffer) {
if (typeof msg === "string") {
upstream.send(msg)
return
}
upstream.send(new Uint8Array(msg))
}
function getWebSocketHeaders(req: Request, url: URL) {
return {
Origin: req.headers.get("origin") ?? url.origin,
}
}
function connectUpstreamWebSocket(url: string, headers: Record<string, string>) {
const BunWebSocket = WebSocket as unknown as {
new (url: string, options: { headers: Record<string, string> }): WebSocket
}
return new BunWebSocket(url, { headers })
}
function isDebugTarget(value: unknown): value is DebugTarget { function isDebugTarget(value: unknown): value is DebugTarget {
if (!isRecord(value) || typeof value.webSocketDebuggerUrl !== "string") return false if (!isRecord(value) || typeof value.webSocketDebuggerUrl !== "string") return false
@@ -168,6 +205,10 @@ function isDebugTarget(value: unknown): value is DebugTarget {
return prefersFuseboxFrontend === undefined || typeof prefersFuseboxFrontend === "boolean" return prefersFuseboxFrontend === undefined || typeof prefersFuseboxFrontend === "boolean"
} }
function prefersFuseboxFrontend(target: DebugTarget) {
return target.reactNative?.capabilities?.prefersFuseboxFrontend === true
}
function getProxyWebSocketPath(webSocketDebuggerUrl: string) { function getProxyWebSocketPath(webSocketDebuggerUrl: string) {
const url = new URL(webSocketDebuggerUrl) const url = new URL(webSocketDebuggerUrl)
return `${tsIp}:${PROXY_PORT}${url.pathname}${url.search}` return `${tsIp}:${PROXY_PORT}${url.pathname}${url.search}`

View File

@@ -29,7 +29,7 @@ if (!Array.isArray(parsedTargets)) {
} }
const targets = parsedTargets.filter(isDebugTarget) const targets = parsedTargets.filter(isDebugTarget)
const target = targets.find((t) => t.reactNative?.capabilities?.prefersFuseboxFrontend) const target = targets.find(prefersFuseboxFrontend) ?? targets[0]
if (!target) { if (!target) {
console.error("No debug target found. Is the app connected?") console.error("No debug target found. Is the app connected?")
@@ -68,6 +68,10 @@ function isDebugTarget(value: unknown): value is DebugTarget {
return prefersFuseboxFrontend === undefined || typeof prefersFuseboxFrontend === "boolean" return prefersFuseboxFrontend === undefined || typeof prefersFuseboxFrontend === "boolean"
} }
function prefersFuseboxFrontend(target: DebugTarget) {
return target.reactNative?.capabilities?.prefersFuseboxFrontend === true
}
function getProxyWebSocketPath(webSocketDebuggerUrl: string) { function getProxyWebSocketPath(webSocketDebuggerUrl: string) {
const url = new URL(webSocketDebuggerUrl) const url = new URL(webSocketDebuggerUrl)
return `${tsIp}:${PROXY_PORT}${url.pathname}${url.search}` return `${tsIp}:${PROXY_PORT}${url.pathname}${url.search}`

View File

@@ -1,6 +0,0 @@
import { ApiRequestMiddleware } from "./client"
export const authMiddleware: ApiRequestMiddleware = (_url, init) => {
// TODO: placeholder auth middleware
return init
}

View File

@@ -27,9 +27,12 @@ export class ApiClient {
(prevInit, middleware) => middleware(url, prevInit), (prevInit, middleware) => middleware(url, prevInit),
init, init,
) )
return fetch(this.baseUrl ? new URL(url.toString(), this.baseUrl) : url, finalInit).then( return fetch(this.baseUrl ? this.baseUrl + url : url, finalInit)
(res) => Promise.all([Promise.resolve(res), res.json()]), .then((res) => Promise.all([Promise.resolve(res), res.json()]))
) .catch((err) => {
console.log(`request error: ${url}`, err)
throw err
})
} }
} }

View File

@@ -0,0 +1,134 @@
import { Link } from "expo-router"
import { Pressable, ScrollView, View } from "react-native"
import tw from "twrnc"
import { FeedCard } from "@/components/ui/feed-card"
import { MonospaceText } from "@/components/ui/monospace-text"
import { SansSerifText } from "@/components/ui/sans-serif-text"
import { SerifText } from "@/components/ui/serif-text"
import { ChatOverlay } from "@/conversations/chat-overlay"
export default function HomeScreen() {
return (
<View style={tw`bg-stone-100 dark:bg-stone-950 flex-1 relative`}>
<ScrollView
contentContainerStyle={tw`px-5 pt-6 pb-28 gap-3 justify-end flex-grow`}
showsVerticalScrollIndicator={false}
>
<FeedCard style={tw`bg-white dark:bg-stone-900 p-4 gap-3`}>
<View style={tw`flex-row items-center justify-between gap-3`}>
<SansSerifText style={tw`text-xs text-stone-500 dark:text-stone-400 uppercase`}>
Morning brief
</SansSerifText>
<MonospaceText style={tw`text-xs text-teal-700 dark:text-teal-300`}>
08:42
</MonospaceText>
</View>
<SerifText style={tw`text-3xl leading-9`}>
A calm start with two useful windows.
</SerifText>
<SansSerifText style={tw`text-base leading-6 text-stone-600 dark:text-stone-300`}>
Your morning is light until the project sync. Rain holds off until late afternoon, and
your last note suggests starting with the proposal outline.
</SansSerifText>
<View style={tw`flex-row flex-wrap gap-2`}>
<View style={tw`rounded-full bg-teal-50 dark:bg-teal-950 px-3 py-1`}>
<SansSerifText style={tw`text-xs text-teal-700 dark:text-teal-200`}>
90 min focus
</SansSerifText>
</View>
<View style={tw`rounded-full bg-stone-100 dark:bg-stone-800 px-3 py-1`}>
<SansSerifText style={tw`text-xs text-stone-600 dark:text-stone-300`}>
2 reminders
</SansSerifText>
</View>
<View style={tw`rounded-full bg-stone-100 dark:bg-stone-800 px-3 py-1`}>
<SansSerifText style={tw`text-xs text-stone-600 dark:text-stone-300`}>
Low email volume
</SansSerifText>
</View>
</View>
</FeedCard>
<FeedCard style={tw`bg-white dark:bg-stone-900 p-4 gap-4`}>
<View style={tw`flex-row items-baseline justify-between gap-3`}>
<SerifText style={tw`text-2xl`}>Next up</SerifText>
<SansSerifText style={tw`text-xs text-stone-500 dark:text-stone-400`}>
Today
</SansSerifText>
</View>
<View style={tw`gap-3`}>
<View style={tw`flex-row gap-3`}>
<MonospaceText style={tw`w-14 text-xs text-stone-500 dark:text-stone-400`}>
09:30
</MonospaceText>
<View style={tw`flex-1 gap-1`}>
<SansSerifText style={tw`font-semibold`}>Project sync prep</SansSerifText>
<SansSerifText style={tw`text-sm leading-5 text-stone-600 dark:text-stone-300`}>
Three notes from yesterday are ready to skim.
</SansSerifText>
</View>
</View>
<View style={tw`h-px bg-stone-200 dark:bg-stone-800`} />
<View style={tw`flex-row gap-3`}>
<MonospaceText style={tw`w-14 text-xs text-stone-500 dark:text-stone-400`}>
11:00
</MonospaceText>
<View style={tw`flex-1 gap-1`}>
<SansSerifText style={tw`font-semibold`}>Quiet work block</SansSerifText>
<SansSerifText style={tw`text-sm leading-5 text-stone-600 dark:text-stone-300`}>
Best slot for the proposal before the day gets busy.
</SansSerifText>
</View>
</View>
</View>
</FeedCard>
<FeedCard style={tw`bg-white dark:bg-stone-900 p-4 gap-3`}>
<View style={tw`flex-row items-center justify-between gap-3`}>
<SerifText style={tw`text-2xl`}>Personal radar</SerifText>
<MonospaceText style={tw`text-xs text-stone-500 dark:text-stone-400`}>
4 signals
</MonospaceText>
</View>
<View style={tw`gap-2.5`}>
<View style={tw`flex-row gap-2.5`}>
<View style={tw`mt-1.5 size-2 rounded-full bg-teal-500`} />
<SansSerifText
style={tw`flex-1 text-sm leading-5 text-stone-600 dark:text-stone-300`}
>
Package is likely to arrive before 2 PM.
</SansSerifText>
</View>
<View style={tw`flex-row gap-2.5`}>
<View style={tw`mt-1.5 size-2 rounded-full bg-amber-500`} />
<SansSerifText
style={tw`flex-1 text-sm leading-5 text-stone-600 dark:text-stone-300`}
>
Energy prices dip again after midnight.
</SansSerifText>
</View>
<View style={tw`flex-row gap-2.5`}>
<View style={tw`mt-1.5 size-2 rounded-full bg-sky-500`} />
<SansSerifText
style={tw`flex-1 text-sm leading-5 text-stone-600 dark:text-stone-300`}
>
A recipe you saved matches what is in the fridge.
</SansSerifText>
</View>
</View>
</FeedCard>
<Link href="/components" asChild>
<Pressable style={tw`self-start px-1 py-1`}>
<SansSerifText style={tw`text-sm text-teal-700 dark:text-teal-300`}>
View component library
</SansSerifText>
</Pressable>
</Link>
</ScrollView>
<ChatOverlay />
</View>
)
}

View File

@@ -3,11 +3,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { Stack } from "expo-router" import { Stack } from "expo-router"
import { StatusBar } from "expo-status-bar" import { StatusBar } from "expo-status-bar"
import React from "react" import React from "react"
import { useColorScheme } from "react-native" import { useColorScheme, View } from "react-native"
import { KeyboardProvider } from "react-native-keyboard-controller"
import tw, { useDeviceContext } from "twrnc" import tw, { useDeviceContext } from "twrnc"
import { authMiddleware } from "@/api/auth-middleware"
import { ApiClient, ApiClientContext } from "@/api/client" import { ApiClient, ApiClientContext } from "@/api/client"
import { auth, authMiddleware } from "@/auth/auth"
const queryClient = new QueryClient() const queryClient = new QueryClient()
const apiClient = new ApiClient({ const apiClient = new ApiClient({
@@ -22,6 +23,12 @@ export default function RootLayout() {
const headerBg = colorScheme === "dark" ? "#1c1917" : "#f5f5f4" const headerBg = colorScheme === "dark" ? "#1c1917" : "#f5f5f4"
const headerTint = colorScheme === "dark" ? "#e7e5e4" : "#1c1917" const headerTint = colorScheme === "dark" ? "#e7e5e4" : "#1c1917"
const { data: session, isPending: isLoadingSession } = auth.useSession()
if (isLoadingSession) {
return null
}
return ( return (
<ContextProvider> <ContextProvider>
<Stack <Stack
@@ -30,6 +37,13 @@ export default function RootLayout() {
contentStyle: { backgroundColor: headerBg }, contentStyle: { backgroundColor: headerBg },
}} }}
> >
<Stack.Protected guard={!session}>
<Stack.Screen name="sign-in" />
</Stack.Protected>
<Stack.Protected guard={Boolean(session)}>
<Stack.Screen name="(app)" />
<Stack.Screen <Stack.Screen
name="components/index" name="components/index"
options={{ options={{
@@ -50,6 +64,7 @@ export default function RootLayout() {
headerShadowVisible: false, headerShadowVisible: false,
}} }}
/> />
</Stack.Protected>
</Stack> </Stack>
<StatusBar style="auto" /> <StatusBar style="auto" />
</ContextProvider> </ContextProvider>
@@ -58,8 +73,10 @@ export default function RootLayout() {
function ContextProvider({ children }: React.PropsWithChildren) { function ContextProvider({ children }: React.PropsWithChildren) {
return ( return (
<KeyboardProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ApiClientContext value={apiClient}>{children}</ApiClientContext> <ApiClientContext value={apiClient}>{children}</ApiClientContext>
</QueryClientProvider> </QueryClientProvider>
</KeyboardProvider>
) )
} }

View File

@@ -1,28 +0,0 @@
import { Link } from "expo-router"
import { Pressable } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"
import tw from "twrnc"
import { Button } from "@/components/ui/button"
import { FeedCard } from "@/components/ui/feed-card"
import { MonospaceText } from "@/components/ui/monospace-text"
import { SansSerifText } from "@/components/ui/sans-serif-text"
import { SerifText } from "@/components/ui/serif-text"
export default function HomeScreen() {
return (
<SafeAreaView style={tw`bg-stone-100 dark:bg-stone-900 flex-1 px-5 pt-6 gap-4`}>
<FeedCard>
<SerifText style={tw`text-4xl`}>Hello world asdsadsa</SerifText>
<SansSerifText style={tw`text-4xl font-bold`}>Hello world</SansSerifText>
<MonospaceText style={tw`text-4xl`}>asdjsakljdl</MonospaceText>
<Button style={tw`self-start`} label="Test" />
</FeedCard>
<Link href="/components" asChild>
<Pressable>
<SansSerifText style={tw`text-teal-600`}>View component library</SansSerifText>
</Pressable>
</Link>
</SafeAreaView>
)
}

View File

@@ -0,0 +1,221 @@
import { mutationOptions, useMutation } from "@tanstack/react-query"
import { useRouter } from "expo-router"
/* eslint-disable react-hooks/immutability */
import { useCallback, useImperativeHandle, useRef } from "react"
import { ActivityIndicator, Alert, View } from "react-native"
import { KeyboardAvoidingView, useKeyboardHandler } from "react-native-keyboard-controller"
import Animated, {
useAnimatedStyle,
useSharedValue,
withDelay,
withSpring,
} from "react-native-reanimated"
import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"
import tw from "twrnc"
import { auth, signInMutation } from "@/auth/auth"
import { InvalidCredentialsError, BetterAuthError } from "@/auth/error"
import { Button } from "@/components/ui/button"
import { SansSerifText } from "@/components/ui/sans-serif-text"
import { SerifText } from "@/components/ui/serif-text"
import { TextInput } from "@/components/ui/text-input"
export default function SignInPage() {
console.log("sing in page ")
const loginFormRef = useRef<LoginFormContainerRef>(null)
const emailButtonHeight = useRef(0)
return (
<View style={tw`size-full relative`}>
<SafeAreaView
style={tw`flex-1 bg-stone-50 dark:bg-stone-900 justify-center items-start px-6`}
>
<View style={tw`flex-1 justify-center items-start`}>
<SerifText style={tw`text-lg mb-1.5`}>I&apos;m Freya!</SerifText>
<SerifText style={tw`text-lg opacity-70 leading-tight`}>
Before I can help you with your daily routines, please sign in below.
</SerifText>
</View>
<Button
onLayout={({ nativeEvent: { layout } }) => {
emailButtonHeight.current = layout.height
}}
intent="secondary"
style={tw`w-full`}
onPress={() => {
loginFormRef.current?.show({
fromHeight: emailButtonHeight.current,
})
}}
>
<Button.Label>Continue with email</Button.Label>
</Button>
</SafeAreaView>
<LoginFormContainer ref={loginFormRef}>
<LoginForm />
</LoginFormContainer>
</View>
)
}
interface LoginFormContainerRef {
show: ({ fromHeight }: { fromHeight: number }) => void
}
function LoginFormContainer({
ref,
children,
}: React.PropsWithChildren<{ ref?: React.Ref<LoginFormContainerRef> }>) {
console.log("LoginFormContainer")
const safeAreaInsets = useSafeAreaInsets()
const opacity = useSharedValue(0)
const contentOpacity = useSharedValue(0)
const insetX = useSharedValue(0)
const bottom = useSharedValue(0)
const height = useSharedValue(0)
const finalHeight = useRef(0)
const show = useCallback(
({ fromHeight }: { fromHeight: number }) => {
insetX.value = 24
bottom.value = safeAreaInsets.bottom
height.value = fromHeight
opacity.value = 1
insetX.value = withSpring(0)
bottom.value = withSpring(0)
height.value = withSpring(finalHeight.current)
contentOpacity.value = withDelay(100, withSpring(1))
},
[opacity, insetX, bottom, safeAreaInsets.bottom, height, contentOpacity],
)
useImperativeHandle(ref, () => ({ show }))
useKeyboardHandler({
onMove: ({ progress, height, duration }) => {
"worklet"
bottom.value = height
},
})
const animatedStyle = useAnimatedStyle(() => ({
height: opacity.value !== 0 ? height.value : undefined,
opacity: opacity.value,
left: insetX.value,
right: insetX.value,
bottom: bottom.value,
}))
return (
<Animated.View
onLayout={({ nativeEvent: { layout } }) => {
finalHeight.current = layout.height
}}
style={[
tw`absolute overflow-hidden border border-stone-200 dark:border-stone-700 rounded-2xl`,
animatedStyle,
]}
>
<KeyboardAvoidingView behavior="padding">
<SafeAreaView
edges={["bottom"]}
style={tw`px-4 bg-stone-100 dark:bg-stone-800 overflow-hidden`}
>
<Animated.View style={[tw`w-full`, { opacity: contentOpacity }]}>
{children}
</Animated.View>
</SafeAreaView>
</KeyboardAvoidingView>
</Animated.View>
)
}
function LoginForm() {
console.log("LoginForm")
const emailRef = useRef("")
const passwordRef = useRef("")
const router = useRouter()
const { mutate: signIn, isPending: isSigningIn } = useMutation(
mutationOptions({
...signInMutation,
onSuccess: (data) => {
if (data) {
router.replace("/")
} else {
// if no data is returned, nothing was done, so do nothing
}
},
onError: (error) => {
console.log(error)
if (error instanceof InvalidCredentialsError) {
Alert.alert("Failed to sign in", "Incorrect email or password")
} else if (error instanceof BetterAuthError) {
Alert.alert(
"Failed to sign in",
"This is a fault on Freya's end. Please try again later.",
)
} else {
Alert.alert(
"Unable to connect to Freya",
"Please check your internet connection and try again.",
)
}
},
}),
)
const handleSignInButtonPress = () => {
signIn({
email: emailRef.current,
password: passwordRef.current,
})
}
return (
<View style={[tw`w-full py-4`]}>
<View style={tw`flex flex-row w-full`}>
<View>
<View style={tw`h-8 justify-center mr-4`}>
<SansSerifText>Email</SansSerifText>
</View>
<View style={tw`my-1 h-px w-full bg-stone-200 dark:bg-stone-700 rounded-l-full`} />
<View style={tw`h-8 justify-center mr-4`}>
<SansSerifText>Password</SansSerifText>
</View>
</View>
<View style={tw`flex-1`}>
<TextInput
defaultValue=""
autoCapitalize="none"
keyboardType="email-address"
style={tw`w-full h-8 font-medium`}
onChangeText={(text) => {
emailRef.current = text
}}
/>
<View style={tw`my-1 h-px w-full bg-stone-200 dark:bg-stone-700 rounded-r-full`} />
<TextInput
defaultValue=""
secureTextEntry
style={tw`w-full h-8 font-medium`}
onChangeText={(text) => {
passwordRef.current = text
}}
/>
</View>
</View>
<Button
intent="primary"
style={tw`w-full mt-6`}
onPress={handleSignInButtonPress}
enabled={!isSigningIn}
>
{isSigningIn ? <Button.Loading /> : <Button.Label>Sign in</Button.Label>}
</Button>
</View>
)
}

View File

@@ -0,0 +1,57 @@
import { expoClient } from "@better-auth/expo/client"
import { mutationOptions } from "@tanstack/react-query"
import { createAuthClient } from "better-auth/react"
import * as SecureStore from "expo-secure-store"
import type { ApiRequestMiddleware } from "../api/client"
import { BetterAuthError, InvalidCredentialsError } from "./error"
export const auth = createAuthClient({
baseURL: process.env.EXPO_PUBLIC_SERVER_URL,
plugins: [
expoClient({
scheme: "freya",
storagePrefix: "chat.freya",
storage: SecureStore,
}),
],
})
export const authMiddleware: ApiRequestMiddleware = (_url, init) => {
const cookie = auth.getCookie()
const headers = new Headers(init.headers)
if (cookie) {
headers.set("Cookie", cookie)
}
return {
...init,
credentials: "omit",
headers,
}
}
export const signInMutation = mutationOptions({
mutationFn: async ({ email, password }: { email: string; password: string }) => {
if (email && password) {
const result = await auth.signIn.email({
email,
password,
})
if (result.error?.code) {
switch (result.error.code) {
case "INVALID_EMAIL":
throw new InvalidCredentialsError("Invalid email")
case "INVALID_PASSWORD":
throw new InvalidCredentialsError("Invalid password")
case "INVALID_EMAIL_OR_PASSWORD":
throw new InvalidCredentialsError("Invalid email or password")
default:
throw new BetterAuthError(result.error)
}
}
return result
}
return null
},
})

View File

@@ -0,0 +1,27 @@
import type { auth } from "./auth"
export class InvalidCredentialsError extends Error {
constructor(cause: unknown) {
super(`Invalid credentials: ${cause}`)
}
}
export class BetterAuthError extends Error {
// the type is copied from the shape of result.error from authClient.signIn.email
constructor(error: {
code?: string | undefined
message?: string | undefined
status: number
statusText: string
}) {
super(`${error.message ?? "BetterAuthError"}: ${error.status} ${error.statusText}`)
}
}
type BetterAuthErrorTypes = Partial<Record<keyof typeof auth.$ERROR_CODES, string>>
export const AuthErrorCode = {
INVALID_EMAIL: "INVALID_EMAIL",
INVALID_PASSWORD: "INVALID_PASSWORD",
INVALID_EMAIL_OR_PASSWORD: "INVALID_EMAIL_OR_PASSWORD",
} satisfies BetterAuthErrorTypes

View File

@@ -8,25 +8,28 @@ function ButtonShowcase() {
return ( return (
<View style={tw`gap-6`}> <View style={tw`gap-6`}>
<Section title="Default"> <Section title="Default">
<Button style={tw`self-start`} label="Press me" /> <Button style={tw`self-start`}>
<Button.Label>Press me</Button.Label>
</Button>
</Section> </Section>
<Section title="Leading icon"> <Section title="Leading icon">
<Button style={tw`self-start`} label="Add item" leadingIcon={<Button.Icon name="plus" />} /> <Button style={tw`self-start`}>
<Button.Icon name="plus" />
<Button.Label>Add item</Button.Label>
</Button>
</Section> </Section>
<Section title="Trailing icon"> <Section title="Trailing icon">
<Button <Button style={tw`self-start`}>
style={tw`self-start`} <Button.Label>Next</Button.Label>
label="Next" <Button.Icon name="arrow-right" />
trailingIcon={<Button.Icon name="arrow-right" />} </Button>
/>
</Section> </Section>
<Section title="Both icons"> <Section title="Both icons">
<Button <Button style={tw`self-start`}>
style={tw`self-start`} <Button.Icon name="download" />
label="Download" <Button.Label>Download</Button.Label>
leadingIcon={<Button.Icon name="download" />} <Button.Icon name="chevron-down" />
trailingIcon={<Button.Icon name="chevron-down" />} </Button>
/>
</Section> </Section>
</View> </View>
) )

View File

@@ -1,48 +1,214 @@
import Feather from "@expo/vector-icons/Feather" import Feather from "@expo/vector-icons/Feather"
import { type PressableProps, Pressable, type StyleProp, View, type ViewStyle } from "react-native" import { createContext, useContext } from "react"
import {
type PressableProps,
Pressable,
type TextStyle,
useColorScheme,
ActivityIndicator,
} from "react-native"
import tw from "twrnc" import tw from "twrnc"
import { rva, type RvaProps } from "@/lib/rva"
import { SansSerifText } from "./sans-serif-text" import { SansSerifText } from "./sans-serif-text"
type FeatherIconName = React.ComponentProps<typeof Feather>["name"] type FeatherIconName = React.ComponentProps<typeof Feather>["name"]
const button = rva(
tw.style("rounded-2xl px-4 py-3 w-fit flex-row items-center justify-center gap-1.5 h-10", {
borderCurve: "continuous",
}),
{
variants: {
intent: {
primary: tw`bg-teal-600`,
secondary: tw`bg-stone-100 dark:bg-stone-800`,
},
pressed: {
true: tw`translate-y-px`,
false: null,
},
enabled: {
true: null,
false: tw`opacity-50`,
},
dark: {
true: null,
false: null,
},
},
defaultVariants: {
intent: "primary",
pressed: false,
enabled: true,
dark: false,
},
compoundVariants: [
// primary variants
{
intent: "primary",
enabled: true,
pressed: false,
style: {
boxShadow:
"inset 0 1px 0 0 #2dd4bf66, inset 0 -1px 0 0 #0f766eb3, 0 2px 4px 0 #0000001a, 0 0 0 1px #0f766e",
},
},
{
intent: "primary",
enabled: true,
pressed: true,
style: tw.style("bg-teal-700", {
boxShadow:
"inset 0 1px 2px 0 #042f2e80, inset 0 0 0 1px #0f766e, inset 0 -1px 0 0 #2dd4bf26",
}),
},
// secondary variants
{
intent: "secondary",
dark: false,
enabled: true,
pressed: false,
style: {
boxShadow: "inset 0 1px 0 0 #fdfdfd, 0 2px 4px 0 #0000001a, 0 0 0 1px #00000029",
},
},
{
intent: "secondary",
dark: false,
enabled: true,
pressed: true,
style: tw.style("bg-stone-200", {
boxShadow:
"inset 0 1px 2px 0 #0000001f, inset 0 0 0 1px #00000012, inset 0 -1px 0 0 #ffffff80",
}),
},
{
intent: "secondary",
dark: true,
enabled: true,
pressed: false,
style: tw.style("bg-stone-800", {
boxShadow:
"inset 0 1px 0 0 #4b4951, inset 0 -1px 0 0 #313036, 0 2px 4px 0 #0000001a, 0 0 0 1px #0d0d0d",
}),
},
{
intent: "secondary",
dark: true,
enabled: true,
pressed: true,
style: tw.style("bg-stone-900", {
boxShadow:
"inset 0 1px 2px 0 #00000080, inset 0 0 0 1px #00000066, inset 0 -1px 0 0 #ffffff0a",
}),
},
],
},
)
const label = rva<TextStyle>(
{},
{
variants: {
intent: {
primary: tw`text-stone-100 dark:text-stone-200 font-medium`,
secondary: tw`text-stone-800 dark:text-stone-200 font-medium`,
},
},
},
)
type ButtonVariants = Omit<RvaProps<typeof button>, "dark" | "pressed">
type ButtonProps = PressableProps & ButtonVariants
interface ButtonContext extends ButtonVariants {}
const Context = createContext<ButtonContext>({})
export function Button({ style, intent = "primary", enabled = true, ...props }: ButtonProps) {
const theme = useColorScheme()
return (
<Context value={{ enabled, intent }}>
<Pressable
style={(state) => [
button({
intent,
enabled,
pressed: state.pressed,
dark: theme === "dark",
}),
typeof style === "function" ? style(state) : style,
]}
{...props}
/>
</Context>
)
}
type ButtonIconProps = { type ButtonIconProps = {
name: FeatherIconName name: FeatherIconName
} }
function ButtonIcon({ name }: ButtonIconProps) { function ButtonIcon({ name }: ButtonIconProps) {
return <Feather name={name} size={18} color={tw.color("text-stone-100 dark:text-stone-200")} /> const context = useContext(Context)
let color: string
switch (context.intent) {
case "primary":
color = tw.color("text-stone-100 dark:text-stone-200") ?? ""
break
case "secondary":
color = tw.color("text-stone-800 dark:text-stone-200") ?? ""
break
default:
color = ""
break
}
return <Feather name={name} size={18} color={color} />
} }
type ButtonProps = Omit<PressableProps, "children" | "style"> & { type ButtonLabelProps = React.ComponentProps<typeof SansSerifText>
label: string
leadingIcon?: React.ReactNode
style?: StyleProp<ViewStyle>
trailingIcon?: React.ReactNode
}
export function Button({ style, label, leadingIcon, trailingIcon, ...props }: ButtonProps) {
const hasIcons = leadingIcon != null || trailingIcon != null
const textElement = (
<SansSerifText style={tw`text-stone-100 dark:text-stone-200 font-medium`}>
{label}
</SansSerifText>
)
function ButtonLabel({ style, ...props }: ButtonLabelProps) {
const context = useContext(Context)
return ( return (
<Pressable style={[tw`rounded-full bg-teal-600 px-4 py-3 w-fit`, style]} {...props}> <SansSerifText
{hasIcons ? ( style={[
<View style={tw`flex-row items-center gap-1.5`}> label({
{leadingIcon} intent: context.intent,
{textElement} }),
{trailingIcon} style,
</View> ]}
) : ( {...props}
textElement />
)}
</Pressable>
) )
} }
function ButtonLoadingIndicator() {
const context = useContext(Context)
let color: string
switch (context.intent) {
case "primary":
color = tw.color("text-stone-100 dark:text-stone-200") ?? ""
break
case "secondary":
color = tw.color("text-stone-800 dark:text-stone-200") ?? ""
break
default:
color = ""
break
}
return <ActivityIndicator color={color} />
}
Button.Icon = ButtonIcon Button.Icon = ButtonIcon
Button.Label = ButtonLabel
Button.Loading = ButtonLoadingIndicator

View File

@@ -19,7 +19,9 @@ function FeedCardShowcase() {
<FeedCard style={tw`p-4 gap-2`}> <FeedCard style={tw`p-4 gap-2`}>
<SerifText style={tw`text-xl`}>Title</SerifText> <SerifText style={tw`text-xl`}>Title</SerifText>
<SansSerifText>Body text inside a feed card.</SansSerifText> <SansSerifText>Body text inside a feed card.</SansSerifText>
<Button style={tw`self-start mt-2`} label="Action" /> <Button style={tw`self-start mt-2`}>
<Button.Label>Action</Button.Label>
</Button>
</FeedCard> </FeedCard>
</Section> </Section>
</View> </View>

View File

@@ -0,0 +1,6 @@
import { TextInput as NativeTextInput, type TextInputProps } from "react-native"
import tw from "twrnc"
export function TextInput({ style, ...props }: TextInputProps) {
return <NativeTextInput style={[tw`text-stone-800 dark:text-stone-200`, style]} {...props} />
}

View File

@@ -0,0 +1,284 @@
/* eslint-disable react-hooks/immutability */
import MaskedView from "@react-native-masked-view/masked-view"
import { BlurView } from "expo-blur"
import { LinearGradient } from "expo-linear-gradient"
import { atom, useAtomValue, useSetAtom, useStore } from "jotai"
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef } from "react"
import { useColorScheme, View, StyleSheet, Platform, Dimensions } from "react-native"
import { easeGradient } from "react-native-easing-gradient"
import { useKeyboardHandler } from "react-native-keyboard-controller"
import Animated, { FadeIn, FadeOut, useSharedValue, withSpring } from "react-native-reanimated"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import tw from "twrnc"
import { Button } from "@/components/ui/button"
import { TextInput } from "@/components/ui/text-input"
import { ConversationList } from "./conversation-list"
interface BottomProgressiveBlurRef {
setBlurHeight: (height: number) => void
}
interface ConversationListContainerRef {
showFullChat: () => void
}
const ChatViewMode = {
Hidden: "hidden",
Peek: "peek",
FullChat: "full-chat",
} as const
type ChatViewMode = (typeof ChatViewMode)[keyof typeof ChatViewMode]
const chatInputHeightAtom = atom(0)
const isChatInputFocusedAtom = atom(false)
const chatViewModeAtom = atom<ChatViewMode>(ChatViewMode.Hidden)
export function ChatOverlay() {
const theme = useColorScheme()
const setChatInputHeight = useSetAtom(chatInputHeightAtom)
const setIsChatInputFocused = useSetAtom(isChatInputFocusedAtom)
const setChatViewMode = useSetAtom(chatViewModeAtom)
const store = useStore()
const conversationListContainerRef = useRef<ConversationListContainerRef>(null)
const onTextInputFocus = () => {
setChatViewMode(ChatViewMode.Peek)
}
const onConversationListScroll = () => {
if (store.get(chatViewModeAtom) !== ChatViewMode.FullChat) {
setChatViewMode(ChatViewMode.FullChat)
conversationListContainerRef?.current?.showFullChat()
}
}
return (
<ChatOverlayContainer>
<OverlayBackdrop />
<ConversationListContainer ref={conversationListContainerRef}>
<ConversationList
ListHeaderComponent={ConversationListHeader}
ListFooterComponent={ConversationListFooter}
onScrollBeginDrag={onConversationListScroll}
/>
</ConversationListContainer>
<ChatInputContainer>
<BlurView
onLayout={({ nativeEvent: { layout } }) => {
setChatInputHeight(layout.height)
}}
intensity={35}
tint={theme === "dark" ? "systemThickMaterialDark" : "systemThickMaterialLight"}
style={tw`flex flex-row w-full py-1 pl-4 pr-1 border border-stone-300 dark:border-stone-700 rounded-full overflow-hidden`}
>
<TextInput
onFocus={onTextInputFocus}
onBlur={() => {
setIsChatInputFocused(false)
}}
style={tw`flex-1`}
placeholder="Message Freya..."
/>
<Button style={tw`size-8 p-0`}>
<Button.Icon name="arrow-up" />
</Button>
</BlurView>
</ChatInputContainer>
</ChatOverlayContainer>
)
}
function ChatOverlayContainer({ children }: React.PropsWithChildren) {
const bottom = useSharedValue(0)
useKeyboardHandler({
onMove: (event) => {
"worklet"
bottom.value = event.height
},
})
return (
<Animated.View pointerEvents="box-none" style={[tw`absolute top-0 left-0 right-0`, { bottom }]}>
{children}
</Animated.View>
)
}
function ConversationListContainer({
ref,
children,
}: React.PropsWithChildren<{ ref?: React.Ref<ConversationListContainerRef> }>) {
const chatViewMode = useAtomValue(chatViewModeAtom)
const height = useSharedValue(Dimensions.get("window").height * 0.4)
const { colors, locations } = useMemo(
() =>
easeGradient({
colorStops: {
0: { color: "transparent" },
0.1: { color: "transparent" },
0.3: { color: tw.color("bg-stone-100 dark:bg-stone-950")! },
0.9: { color: tw.color("bg-stone-100 dark:bg-stone-950")! },
1: { color: tw.color("bg-stone-100 dark:bg-stone-950")! },
},
}),
[],
)
const showFullChat = useCallback(() => {
height.value = withSpring(Dimensions.get("window").height + 80)
}, [height])
useImperativeHandle(
ref,
() => ({
showFullChat,
}),
[showFullChat],
)
return (
<View pointerEvents="box-none" style={tw.style("absolute top-0 left-0 right-0 bottom-0")}>
<MaskedView
pointerEvents="box-none"
maskElement={
<Animated.View style={[tw`absolute bottom-0 right-0 left-0`, { height }]}>
<LinearGradient
locations={locations as any}
colors={colors as any}
style={tw`size-full`}
/>
</Animated.View>
}
style={tw`size-full`}
>
<View
style={tw.style("size-full", chatViewMode === ChatViewMode.Hidden ? "opacity-0" : "")}
>
{children}
</View>
</MaskedView>
</View>
)
}
function OverlayBackdrop() {
const chatViewMode = useAtomValue(chatViewModeAtom)
const bottomProgressiveBlurRef = useRef<BottomProgressiveBlurRef>(null)
useEffect(() => {
if (chatViewMode === ChatViewMode.Peek) {
bottomProgressiveBlurRef?.current?.setBlurHeight(Dimensions.get("window").height * 0.75)
}
}, [chatViewMode])
if (chatViewMode === ChatViewMode.FullChat) {
return <BlurBackground />
}
return <BottomProgressiveBlur ref={bottomProgressiveBlurRef} />
}
function BottomProgressiveBlur({ ref }: { ref?: React.Ref<BottomProgressiveBlurRef> }) {
const progressiveBlurHeight = useSharedValue(192)
const colorScheme = useColorScheme()
const { colors, locations } = useMemo(
() =>
easeGradient({
colorStops: {
0: { color: "transparent" },
0.7: { color: tw.color("bg-stone-100 dark:bg-stone-950")! },
1: { color: tw.color("bg-stone-100 dark:bg-stone-950")! },
},
}),
[],
)
const setBlurHeight = useCallback(
(height: number) => {
progressiveBlurHeight.value = withSpring(height)
},
[progressiveBlurHeight],
)
useImperativeHandle(ref, () => ({ setBlurHeight }), [setBlurHeight])
return (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
style={[tw`absolute bottom-0 left-0 right-0`, { height: progressiveBlurHeight }]}
>
<MaskedView
maskElement={
<LinearGradient
locations={locations as any}
colors={colors as any}
style={tw`absolute top-0 bottom-0 left-0 right-0`}
/>
}
style={[StyleSheet.absoluteFill, tw`z-[1]`]}
>
<BlurView
intensity={65}
tint={Platform.select({
ios:
colorScheme === "dark"
? "systemUltraThinMaterialDark"
: "systemUltraThinMaterialLight",
android: "systemMaterialDark",
})}
style={StyleSheet.absoluteFill}
/>
</MaskedView>
</Animated.View>
)
}
function BlurBackground() {
const colorScheme = useColorScheme()
return (
<Animated.View entering={FadeIn} exiting={FadeOut} style={StyleSheet.absoluteFill}>
<BlurView
intensity={65}
tint={Platform.select({
ios:
colorScheme === "dark" ? "systemUltraThinMaterialDark" : "systemUltraThinMaterialLight",
android: "systemMaterialDark",
})}
style={StyleSheet.absoluteFill}
/>
</Animated.View>
)
}
function ChatInputContainer({ children }: React.PropsWithChildren) {
const keyboardHeight = useSharedValue(0)
const insets = useSafeAreaInsets()
useKeyboardHandler({
onMove: (event) => {
"worklet"
keyboardHeight.value = Math.max(event.height - insets.bottom + 8, 0)
},
})
return <View style={tw`absolute bottom-0 left-0 right-0 px-4 pb-10`}>{children}</View>
}
function ConversationListHeader() {
const safeAreaInsets = useSafeAreaInsets()
return <View style={{ height: safeAreaInsets.top }} />
}
function ConversationListFooter() {
const chatInputHeight = useAtomValue(chatInputHeightAtom)
const safeAreaInsets = useSafeAreaInsets()
return <View style={{ height: chatInputHeight + 24 + safeAreaInsets.bottom }} />
}

View File

@@ -0,0 +1,183 @@
import { AssistantMessagePayload, UserMessagePayload } from "@freya/core"
import { FlashList } from "@shopify/flash-list"
import { useQuery } from "@tanstack/react-query"
import { View, ViewStyle } from "react-native"
import tw from "twrnc"
import { SansSerifText } from "@/components/ui/sans-serif-text"
import { ConversationEntry } from "./conversations"
import { useListConversationEntriesQuery, useDefaultConversationQuery } from "./queries"
type ConversationListProps = Omit<
React.ComponentProps<typeof FlashList>,
"data" | "keyExtractor" | "renderItem" | "maintainVisibleContentPosition"
>
type PositionInGroup = "single" | "first" | "in-between" | "last"
const messageBubbleRadius = 18
const groupedMessageBubbleRadius = 6
export function ConversationList({
ListFooterComponent,
ListHeaderComponent,
onScrollBeginDrag,
}: ConversationListProps) {
const { data: conversation } = useQuery(useDefaultConversationQuery())
const { data: entries } = useQuery(useListConversationEntriesQuery(conversation?.id))
const conversationEntries = entries ?? []
return (
<FlashList
style={tw`size-full`}
maintainVisibleContentPosition={{ startRenderingFromBottom: true }}
data={conversationEntries}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => {
const previousEntryIsSameKind = conversationEntries[index - 1]?.kind === item.kind
const nextEntryIsSameKind = conversationEntries[index + 1]?.kind === item.kind
let position: PositionInGroup
if (!previousEntryIsSameKind && !nextEntryIsSameKind) {
position = "single"
} else if (!previousEntryIsSameKind) {
position = "first"
} else if (!nextEntryIsSameKind) {
position = "last"
} else {
position = "in-between"
}
return <MessageBubble entry={item} position={position} />
}}
onScrollBeginDrag={onScrollBeginDrag}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={ListFooterComponent}
/>
)
}
function MessageBubble({
entry,
position,
}: {
entry: typeof ConversationEntry.infer
position: PositionInGroup
}) {
if (entry.kind === "user_message") {
const payload = UserMessagePayload.assert(entry.payload)
return <UserMessageBubble payload={payload} position={position} />
}
if (entry.kind === "assistant_message") {
const payload = AssistantMessagePayload.assert(entry.payload)
return <AssistantMessageBubble payload={payload} position={position} />
}
return null
}
function UserMessageBubble({
payload,
position,
}: {
payload: UserMessagePayload
position: PositionInGroup
}) {
const content = payload.parts.reduce((final, part) => {
if (part.type === "text") {
return final + part.text
}
return final
}, "")
let corners: ViewStyle
switch (position) {
case "single":
case "first":
corners = {
borderRadius: messageBubbleRadius,
borderBottomRightRadius: groupedMessageBubbleRadius,
}
break
case "in-between":
corners = {
borderRadius: messageBubbleRadius,
borderTopRightRadius: groupedMessageBubbleRadius,
borderBottomRightRadius: groupedMessageBubbleRadius,
}
break
case "last":
corners = {
borderRadius: messageBubbleRadius,
borderTopRightRadius: groupedMessageBubbleRadius,
}
break
}
return (
<View style={tw`w-full flex-row justify-end mb-4 pr-4`}>
<View
style={tw.style("bg-teal-600 px-3 py-2 overflow-hidden max-w-56", corners, {
borderCurve: "circular",
})}
>
<SansSerifText style={tw`text-stone-100`}>{content}</SansSerifText>
</View>
</View>
)
}
function AssistantMessageBubble({
payload,
position,
}: {
payload: AssistantMessagePayload
position: PositionInGroup
}) {
const content = payload.parts.reduce((final, part) => {
if (part.type === "text") {
return final + part.text
}
return final
}, "")
let corners: ViewStyle
switch (position) {
case "single":
case "first":
corners = {
borderRadius: messageBubbleRadius,
borderBottomLeftRadius: groupedMessageBubbleRadius,
}
break
case "in-between":
corners = {
borderRadius: messageBubbleRadius,
borderTopLeftRadius: groupedMessageBubbleRadius,
borderBottomLeftRadius: groupedMessageBubbleRadius,
}
break
case "last":
corners = {
borderRadius: messageBubbleRadius,
borderTopLeftRadius: groupedMessageBubbleRadius,
}
break
}
return (
<View style={tw`w-full flex-row justify-start mb-4 pl-4`}>
<View
style={tw.style(
"bg-stone-200 dark:bg-stone-800 border border-stone-300 dark:border-stone-700 px-3 py-2 overflow-hidden max-w-56",
corners,
{
borderCurve: "circular",
},
)}
>
<SansSerifText style={tw`text-stone-950 dark:text-stone-100`}>{content}</SansSerifText>
</View>
</View>
)
}

View File

@@ -0,0 +1,21 @@
import {
ConversationEntryKind,
ConversationEntryPayload,
ConversationEntryVisibility,
} from "@freya/core"
import { type } from "arktype"
export const Conversation = type({
id: "string.uuid",
createdAt: "string.date.iso",
updatedAt: "string.date.iso",
})
export const ConversationEntry = type({
id: "string.uuid",
sequence: "number",
kind: type.enumerated(...Object.values(ConversationEntryKind)),
visibility: type.enumerated(...Object.values(ConversationEntryVisibility)),
fileId: "string | null",
payload: ConversationEntryPayload,
})

View File

@@ -0,0 +1,47 @@
import { queryOptions, skipToken } from "@tanstack/react-query"
import { type } from "arktype"
import { useApiClient } from "@/api/client"
import { Conversation, ConversationEntry } from "./conversations"
const ListConversationsResponse = type({
conversations: Conversation.array(),
})
const ConversationEntriesResponse = type({
entries: ConversationEntry.array(),
})
export function useListConversationsQuery() {
const api = useApiClient()
return queryOptions({
queryKey: ["conversations"],
queryFn: async () =>
api
.request("/conversations", { method: "GET" })
.then(([, json]) => ListConversationsResponse.assert(json)),
})
}
export function useDefaultConversationQuery() {
return queryOptions({
...useListConversationsQuery(),
select: (data) => {
return data.conversations.length === 0 ? null : data.conversations[0]
},
})
}
export function useListConversationEntriesQuery(id?: string) {
const api = useApiClient()
return queryOptions({
queryKey: ["conversations", id],
queryFn: id
? async () =>
api
.request(`/conversations/${id}/entries`, { method: "GET" })
.then(([, json]) => ConversationEntriesResponse.assert(json).entries)
: skipToken,
})
}

View File

@@ -15,15 +15,29 @@ export const catalog = defineCatalog(schema, {
}, },
Button: { Button: {
props: z.object({ props: z.object({
label: z.string(), intent: z.enum(["primary", "secondary"]).nullable(),
leadingIcon: z.string().nullable(),
trailingIcon: z.string().nullable(),
}), }),
events: ["press"], events: ["press"],
slots: [], slots: ["default"],
description: description:
"Pressable button with a label and optional Feather icons. Icon values are Feather icon names (e.g. 'plus', 'arrow-right'). Bind on.press to trigger an action.", "Pressable button. Add ButtonLabel and optional ButtonIcon children in the default slot. Bind on.press to trigger an action.",
example: { label: "Add item", leadingIcon: "plus", trailingIcon: null }, example: { intent: "primary" },
},
ButtonIcon: {
props: z.object({
name: z.string(),
}),
slots: [],
description: "Feather icon for use inside a Button.",
example: { name: "plus" },
},
ButtonLabel: {
props: z.object({
text: z.string(),
}),
slots: [],
description: "Text label for use inside a Button.",
example: { text: "Add item" },
}, },
FeedCard: { FeedCard: {
props: z.object({ props: z.object({

View File

@@ -17,20 +17,13 @@ export const { registry } = defineRegistry(catalog, {
View: ({ props, children }) => ( View: ({ props, children }) => (
<View style={props.style ? tw`${props.style}` : undefined}>{children}</View> <View style={props.style ? tw`${props.style}` : undefined}>{children}</View>
), ),
Button: ({ props, emit }) => ( Button: ({ props, children, emit }) => (
<Button <Button intent={props.intent ?? undefined} onPress={() => emit("press")}>
label={props.label} {children}
leadingIcon={ </Button>
props.leadingIcon ? <Button.Icon name={props.leadingIcon as ButtonIconName} /> : undefined
}
trailingIcon={
props.trailingIcon ? (
<Button.Icon name={props.trailingIcon as ButtonIconName} />
) : undefined
}
onPress={() => emit("press")}
/>
), ),
ButtonIcon: ({ props }) => <Button.Icon name={props.name as ButtonIconName} />,
ButtonLabel: ({ props }) => <Button.Label>{props.text}</Button.Label>,
FeedCard: ({ props, children }) => ( FeedCard: ({ props, children }) => (
<FeedCard style={props.style ? tw`${props.style}` : undefined}>{children}</FeedCard> <FeedCard style={props.style ? tw`${props.style}` : undefined}>{children}</FeedCard>
), ),

View File

@@ -0,0 +1,215 @@
import type { StyleProp, ViewStyle } from "react-native"
type RvaNoInfer<TValue> = [TValue][TValue extends unknown ? 0 : never]
type VariantValue = string | number | boolean | null | undefined
type VariantOptionKey<TOptions> = Extract<keyof TOptions, string>
type BooleanVariantInput<TOptions> =
Extract<VariantOptionKey<TOptions>, "true" | "false"> extends never ? never : boolean
type StringVariantInput<TOptions> = Exclude<VariantOptionKey<TOptions>, "true" | "false">
type VariantInput<TOptions> =
| StringVariantInput<TOptions>
| BooleanVariantInput<TOptions>
| null
| undefined
type CompoundCondition<TVariants extends RvaVariants<unknown>> = {
name: keyof TVariants
value: string
}
type NormalizedCompoundVariant<TStyle, TVariants extends RvaVariants<TStyle>> = {
conditions: CompoundCondition<TVariants>[]
style: StyleProp<TStyle>
}
export type RvaVariants<TStyle = unknown> = {
readonly [name: string]: {
readonly [value: string]: StyleProp<TStyle>
}
}
export type RvaVariantProps<TVariants extends RvaVariants<unknown>> = {
readonly [TName in keyof TVariants]?: VariantInput<TVariants[TName]>
}
type MutableRvaVariantProps<TVariants extends RvaVariants<unknown>> = {
-readonly [TName in keyof TVariants]?: VariantInput<TVariants[TName]>
}
export type RvaCompoundVariant<
TStyle,
TVariants extends RvaVariants<TStyle>,
> = RvaVariantProps<TVariants> & {
readonly style: StyleProp<TStyle>
}
export type RvaConfig<TStyle, TVariants extends RvaVariants<TStyle>> = {
readonly variants?: TVariants
readonly defaultVariants?: RvaVariantProps<TVariants>
readonly compoundVariants?: readonly RvaCompoundVariant<TStyle, TVariants>[]
}
export type RvaResolver<TStyle, TVariants extends RvaVariants<TStyle>> = (
props?: RvaVariantProps<TVariants>,
) => StyleProp<TStyle>
export type RvaProps<TResolver> = TResolver extends (props?: infer TProps) => unknown
? NonNullable<TProps>
: never
export function rva<TStyle = ViewStyle>(): <
const TVariants extends RvaVariants<TStyle> = RvaVariants<TStyle>,
>(
base: StyleProp<RvaNoInfer<TStyle>>,
config?: RvaConfig<RvaNoInfer<TStyle>, TVariants>,
) => RvaResolver<TStyle, TVariants>
export function rva<
TStyle = ViewStyle,
const TVariants extends RvaVariants<TStyle> = RvaVariants<TStyle>,
>(
base: StyleProp<RvaNoInfer<TStyle>>,
config?: RvaConfig<RvaNoInfer<TStyle>, TVariants>,
): RvaResolver<TStyle, TVariants>
export function rva<
TStyle = ViewStyle,
const TVariants extends RvaVariants<TStyle> = RvaVariants<TStyle>,
>(
base?: StyleProp<TStyle>,
config?: RvaConfig<TStyle, TVariants>,
):
| RvaResolver<TStyle, TVariants>
| (<const TTypedVariants extends RvaVariants<TStyle> = RvaVariants<TStyle>>(
base: StyleProp<TStyle>,
config?: RvaConfig<TStyle, TTypedVariants>,
) => RvaResolver<TStyle, TTypedVariants>) {
if (base === undefined && config === undefined) {
return function createTypedRva<
const TTypedVariants extends RvaVariants<TStyle> = RvaVariants<TStyle>,
>(typedBase: StyleProp<TStyle>, typedConfig: RvaConfig<TStyle, TTypedVariants> = {}) {
return createRva(typedBase, typedConfig)
}
}
return createRva(base, config ?? {})
}
function createRva<TStyle, const TVariants extends RvaVariants<TStyle> = RvaVariants<TStyle>>(
base: StyleProp<TStyle>,
config: RvaConfig<TStyle, TVariants>,
): RvaResolver<TStyle, TVariants> {
const compoundVariants = normalizeCompoundVariants(config.compoundVariants)
return function resolveRva(props: RvaVariantProps<TVariants> = {}) {
const merged = mergeVariantProps(config.defaultVariants, props)
const styles: StyleProp<TStyle>[] = [base]
const variants = config.variants
if (variants !== undefined) {
for (const name in variants) {
const value = normalizeVariantValue(merged[name])
if (value === undefined) {
continue
}
const style = variants[name]?.[value]
if (style !== undefined) {
styles.push(style)
}
}
}
for (const compound of compoundVariants) {
if (compoundMatches(compound, merged)) {
styles.push(compound.style)
}
}
return styles
}
}
function mergeVariantProps<TVariants extends RvaVariants<unknown>>(
defaultVariants: RvaVariantProps<TVariants> | undefined,
props: RvaVariantProps<TVariants>,
): RvaVariantProps<TVariants> {
const merged: MutableRvaVariantProps<TVariants> = {}
if (defaultVariants !== undefined) {
for (const name of Object.keys(defaultVariants) as (keyof TVariants)[]) {
const value = defaultVariants[name]
if (value !== undefined) {
merged[name] = value
}
}
}
for (const name of Object.keys(props) as (keyof TVariants)[]) {
const value = props[name]
if (value !== undefined) {
merged[name] = value
}
}
return merged
}
function normalizeCompoundVariants<TStyle, TVariants extends RvaVariants<TStyle>>(
compoundVariants: readonly RvaCompoundVariant<TStyle, TVariants>[] | undefined,
) {
const normalized: NormalizedCompoundVariant<TStyle, TVariants>[] = []
if (compoundVariants === undefined) {
return normalized
}
for (const compound of compoundVariants) {
const conditions: CompoundCondition<TVariants>[] = []
for (const name in compound) {
if (name === "style") {
continue
}
const variantName = name as keyof TVariants
const value = normalizeVariantValue(compound[variantName])
if (value !== undefined) {
conditions.push({ name: variantName, value })
}
}
normalized.push({ conditions, style: compound.style })
}
return normalized
}
function compoundMatches<TStyle, TVariants extends RvaVariants<TStyle>>(
compound: NormalizedCompoundVariant<TStyle, TVariants>,
props: RvaVariantProps<TVariants>,
) {
for (const condition of compound.conditions) {
if (normalizeVariantValue(props[condition.name]) !== condition.value) {
return false
}
}
return true
}
function normalizeVariantValue(value: VariantValue) {
if (value === null || value === undefined) {
return undefined
}
if (value === true) {
return "true"
}
if (value === false) {
return "false"
}
return String(value)
}

182
bun.lock
View File

@@ -58,6 +58,8 @@
"name": "@freya/backend", "name": "@freya/backend",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@better-auth/core": "^1.6.20",
"@better-auth/expo": "^1.6.20",
"@earendil-works/pi-coding-agent": "^0.79.1", "@earendil-works/pi-coding-agent": "^0.79.1",
"@freya/agent-protocol": "workspace:*", "@freya/agent-protocol": "workspace:*",
"@freya/core": "workspace:*", "@freya/core": "workspace:*",
@@ -72,7 +74,7 @@
"@nym.sh/jrpc": "^0.1.0", "@nym.sh/jrpc": "^0.1.0",
"@openrouter/sdk": "^0.9.11", "@openrouter/sdk": "^0.9.11",
"arktype": "^2.1.29", "arktype": "^2.1.29",
"better-auth": "^1", "better-auth": "^1.6.20",
"drizzle-orm": "^0.45.1", "drizzle-orm": "^0.45.1",
"hono": "^4", "hono": "^4",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
@@ -87,29 +89,45 @@
"name": "freya-client", "name": "freya-client",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@better-auth/core": "^1.6.20",
"@better-auth/expo": "^1.6.20",
"@expo-google-fonts/inter": "^0.4.2", "@expo-google-fonts/inter": "^0.4.2",
"@expo-google-fonts/source-serif-4": "^0.4.1", "@expo-google-fonts/source-serif-4": "^0.4.1",
"@expo/vector-icons": "^15.0.3", "@expo/vector-icons": "^15.0.3",
"@freya/core": "workspace:*",
"@json-render/react-native": "^0.13.0", "@json-render/react-native": "^0.13.0",
"@react-native-masked-view/masked-view": "0.3.2",
"@shopify/flash-list": "2.0.2",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.90.21",
"arktype": "^2.2.1",
"better-auth": "^1.6.20",
"class-variance-authority": "^0.7.1",
"expo": "^56.0.0", "expo": "^56.0.0",
"expo-blur": "~56.0.3",
"expo-constants": "~56.0.18", "expo-constants": "~56.0.18",
"expo-dev-client": "~56.0.20", "expo-dev-client": "~56.0.20",
"expo-font": "~56.0.7", "expo-font": "~56.0.7",
"expo-glass-effect": "~0.1.10",
"expo-haptics": "~56.0.3", "expo-haptics": "~56.0.3",
"expo-image": "~56.0.11", "expo-image": "~56.0.11",
"expo-linear-gradient": "~56.0.4",
"expo-linking": "~56.0.14", "expo-linking": "~56.0.14",
"expo-location": "~56.0.18", "expo-location": "~56.0.18",
"expo-network": "~56.0.5",
"expo-router": "~56.2.11", "expo-router": "~56.2.11",
"expo-secure-store": "~56.0.4",
"expo-splash-screen": "~56.0.10", "expo-splash-screen": "~56.0.10",
"expo-status-bar": "~56.0.4", "expo-status-bar": "~56.0.4",
"expo-symbols": "~56.0.6", "expo-symbols": "~56.0.6",
"expo-system-ui": "~56.0.5", "expo-system-ui": "~56.0.5",
"expo-web-browser": "~56.0.5", "expo-web-browser": "~56.0.5",
"jotai": "^2.20.1",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-native": "0.85.3", "react-native": "0.85.3",
"react-native-easing-gradient": "^1.1.1",
"react-native-gesture-handler": "~2.31.1", "react-native-gesture-handler": "~2.31.1",
"react-native-keyboard-controller": "1.21.6",
"react-native-reanimated": "4.3.1", "react-native-reanimated": "4.3.1",
"react-native-safe-area-context": "~5.7.0", "react-native-safe-area-context": "~5.7.0",
"react-native-screens": "4.25.2", "react-native-screens": "4.25.2",
@@ -172,6 +190,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.1.0", "@standard-schema/spec": "^1.1.0",
"arktype": "^2.1.29",
}, },
"peerDependencies": { "peerDependencies": {
"@json-render/core": "*", "@json-render/core": "*",
@@ -276,6 +295,9 @@
}, },
}, },
}, },
"patchedDependencies": {
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch",
},
"packages": { "packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
@@ -481,23 +503,25 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@better-auth/core": ["@better-auth/core@1.5.4", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-k5AdwPRQETZn0vdB60EB9CDxxfllpJXKqVxTjyXIUSRz7delNGlU0cR/iRP3VfVJwvYR1NbekphBDNo+KGoEzQ=="], "@better-auth/core": ["@better-auth/core@1.6.20", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.6", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-y73I1xNXuNYiHBFduWGRcJ2ro2rNuVDEYkgVMJtIaRXtbosdXHs9gfyQrHecgeHMHKx1SYSBT/CExak0vVMTng=="],
"@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" } }, "sha512-4M4nMAWrDd3TmpV6dONkJjybBVKRZghe5Oj0NNyDEoXubxastQdO7Sb5B54I1rTx5yoMgsqaB+kbJnu/9UgjQg=="], "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["drizzle-orm"] }, "sha512-hJHfCdAiZrC7EmZAt3NAiGgcNo9Y5Qz3PLL+a9rODXaAJGCMvzUJniqef9wHuJBwU0SWW+2f4wXe8xQmaC/IKQ=="],
"@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "kysely": "^0.27.0 || ^0.28.0" } }, "sha512-DPww7rIfz6Ed7dZlJSW9xMQ42VKaJLB5Cs+pPqd+UHKRyighKjf3VgvMIcAdFPc4olQ0qRHo3+ZJhFlBCxRhxA=="], "@better-auth/expo": ["@better-auth/expo@1.6.20", "", { "dependencies": { "@better-fetch/fetch": "1.3.1", "better-call": "1.3.6", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "^1.6.20", "better-auth": "^1.6.20", "expo-constants": ">=17.0.0", "expo-linking": ">=7.0.0", "expo-network": ">=8.0.7", "expo-web-browser": ">=14.0.0" }, "optionalPeers": ["expo-constants", "expo-linking", "expo-network", "expo-web-browser"] }, "sha512-ngpD3ov51mcADMGqxVYu74mZLrjef72hFEcgpRNtJWc7uznrDn43LgiFRyrZWG8NUp3HaIK21D64/CVFvlVaKQ=="],
"@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0" } }, "sha512-iiWYut9rbQqiAsgRBtj6+nxanwjapxRgpIJbiS2o81h7b9iclE0AiDA0Foes590gdFQvskNauZcCpuF8ytxthg=="], "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "kysely": "^0.28.17 || ^0.29.0" }, "optionalPeers": ["kysely"] }, "sha512-Uvpmgbx5y8JqXroVanNzDdKzOl3HojoTz+/X6MR6zOUr25IzlYz660mjnu0rxKiIF55kD3CroqFsDzjNUw7ERw=="],
"@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "mongodb": "^6.0.0 || ^7.0.0" } }, "sha512-ArzJN5Obk6i6+vLK1HpPzLIcsjxZYXPPUvxVU8eyU5HyoUT2MlswWfPQ8UJAKPn0iq/T4PVp/wZcQMhWk1tuNA=="], "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2" } }, "sha512-J5Ni0LlFijbzXlwu2rFHaD8zEFocmajyzWkRnHsq8LhV/Dk4iWQwwnqzLrPoDQEj8roECAUF03hrIeMzqWRqJQ=="],
"@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.5.4", "", { "peerDependencies": { "@better-auth/core": "1.5.4", "@better-auth/utils": "^0.3.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-ZQTbcBopw/ezjjbNFsfR3CRp0QciC4tJCarAnB5G9fZtUYbDjfY0vZOxIRmU4kI3x755CXQpGqTrkwmXaMRa3w=="], "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-ClDBJf6h4g85WJswxwQwxLaiyRU67Gmz/uaIf19tY1gqlLJDykSGjmqRNSBMG5rWABNzcNqbO4KG31rYUldbIw=="],
"@better-auth/telemetry": ["@better-auth/telemetry@1.5.4", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.4" } }, "sha512-mGXTY7Ecxo7uvlMr6TFCBUvlH0NUMOeE9LKgPhG4HyhBN6VfCEg/DD9PG0Z2IatmMWQbckkt7ox5A0eBpG9m5w=="], "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-WhYdhSGuVSfu1peCSf2snmmVzfWjRaEvbSrsNCusiwGE9l94HlES4mjSPM48fed24hL7yg4j1dYK/yjEt87FpQ=="],
"@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], "@better-auth/telemetry": ["@better-auth/telemetry@1.6.20", "", { "peerDependencies": { "@better-auth/core": "^1.6.20", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1" } }, "sha512-3BhbY3naQDERvdJvJ7fGszVY6rpsVfc6c9uyBVZlC1coVEF/rkM0rIcjtMVI1GUH7vWy1wjR6qF5vQnMun3XNQ=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], "@better-auth/utils": ["@better-auth/utils@0.4.2", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A=="],
"@better-fetch/fetch": ["@better-fetch/fetch@1.3.1", "", {}, "sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g=="],
"@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="],
@@ -849,6 +873,8 @@
"@openrouter/sdk": ["@openrouter/sdk@0.9.11", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-BgFu6NcIJO4a9aVjr04y3kZ8pyM71j15I+bzfVAGEvxnj+KQNIkBYQGgwrG3D+aT1QpDKLki8btcQmpaxUas6A=="], "@openrouter/sdk": ["@openrouter/sdk@0.9.11", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-BgFu6NcIJO4a9aVjr04y3kZ8pyM71j15I+bzfVAGEvxnj+KQNIkBYQGgwrG3D+aT1QpDKLki8btcQmpaxUas6A=="],
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
"@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="], "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="],
"@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="], "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="],
@@ -1375,6 +1401,8 @@
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@shopify/flash-list": ["@shopify/flash-list@2.0.2", "", { "dependencies": { "tslib": "2.8.1" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-zhlrhA9eiuEzja4wxVvotgXHtqd3qsYbXkQ3rsBfOgbFA9BVeErpDE/yEwtlIviRGEqpuFj/oU5owD6ByaNX+w=="],
"@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="],
"@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="], "@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="],
@@ -1643,9 +1671,9 @@
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"arkregex": ["arkregex@0.0.5", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw=="], "arkregex": ["arkregex@0.0.6", "", { "dependencies": { "@ark/util": "0.56.0" } }, "sha512-9mvuMKQuibfWhBrsNYhsKhNb6k9oEHoAJ/FvDiqe8h+E9Siwe0/cro1WVOGgpajXQ9ZHd24yCOf2k35Q/QqUQw=="],
"arktype": ["arktype@2.2.0", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.5" } }, "sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ=="], "arktype": ["arktype@2.2.1", "", { "dependencies": { "@ark/schema": "0.56.0", "@ark/util": "0.56.0", "arkregex": "0.0.6" } }, "sha512-CWPJxNoSxrS+NYGB3ufwc/blFonESEW5vBQyYPVS0rf4STu8VWoAWfKJSl5vVVm56h4yxpwbODeYwy6XFKvojA=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
@@ -1707,9 +1735,9 @@
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="], "basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
"better-auth": ["better-auth@1.5.4", "", { "dependencies": { "@better-auth/core": "1.5.4", "@better-auth/drizzle-adapter": "1.5.4", "@better-auth/kysely-adapter": "1.5.4", "@better-auth/memory-adapter": "1.5.4", "@better-auth/mongo-adapter": "1.5.4", "@better-auth/prisma-adapter": "1.5.4", "@better-auth/telemetry": "1.5.4", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-ReykcEKx6Kp9560jG1wtlDBnftA7L7xb3ZZdDWm5yGXKKe2pUf+oBjH0fqekrkRII0m4XBVQbQ0mOrFv+3FdYg=="], "better-auth": ["better-auth@1.6.20", "", { "dependencies": { "@better-auth/core": "1.6.20", "@better-auth/drizzle-adapter": "1.6.20", "@better-auth/kysely-adapter": "1.6.20", "@better-auth/memory-adapter": "1.6.20", "@better-auth/mongo-adapter": "1.6.20", "@better-auth/prisma-adapter": "1.6.20", "@better-auth/telemetry": "1.6.20", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.6", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": "^0.45.2", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-fSpGHGRKiGRiYVd3QTQtuVZ8oxpiSe/7ip0Rpvt/Sy8zQbEbVKUPMOhE0gLXg+FjqTUsIo7582hxUYxtEcqUpA=="],
"better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], "better-call": ["better-call@1.3.6", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-no1jI+h6Bkxs1NVBo4rONbVIzsPjZ8IUu7IHaJBiFwVX1XEQGN8KpHots5fSWmXe9nNyLuLIcgx6WEUcE6EDaA=="],
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
@@ -2055,6 +2083,8 @@
"expo-asset": ["expo-asset@56.0.17", "", { "dependencies": { "@expo/image-utils": "^0.10.1", "expo-constants": "~56.0.18" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-GFN5j+8SPkyv0nfsiFHewmdB/D0tL237TsBE/gSfFOFy/J3a52py7IulcSqkA3sQE/u/UlD5BmvP5ssS4//nUg=="], "expo-asset": ["expo-asset@56.0.17", "", { "dependencies": { "@expo/image-utils": "^0.10.1", "expo-constants": "~56.0.18" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-GFN5j+8SPkyv0nfsiFHewmdB/D0tL237TsBE/gSfFOFy/J3a52py7IulcSqkA3sQE/u/UlD5BmvP5ssS4//nUg=="],
"expo-blur": ["expo-blur@56.0.3", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-KDDtrpWc2tYlm1WCPaOgBtv+YEGqe5ELheFPIgSNgHt28NQUDcfBcFsA9Us2StDh6osmSD6NbKxOt5bU6PcDbQ=="],
"expo-constants": ["expo-constants@56.0.18", "", { "dependencies": { "@expo/env": "~2.3.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw=="], "expo-constants": ["expo-constants@56.0.18", "", { "dependencies": { "@expo/env": "~2.3.0" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw=="],
"expo-dev-client": ["expo-dev-client@56.0.20", "", { "dependencies": { "expo-dev-launcher": "~56.0.20", "expo-dev-menu": "~56.0.17", "expo-dev-menu-interface": "~56.0.0", "expo-manifests": "~56.0.4", "expo-updates-interface": "~56.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-KebW4r8HhIiRrPzs6ZqVhp/so8buyglAO1h4No0Ibr5C2XRnlIoGWCN4zC6rW7IsI3iKUXcofLAQV9OjoxjiwQ=="], "expo-dev-client": ["expo-dev-client@56.0.20", "", { "dependencies": { "expo-dev-launcher": "~56.0.20", "expo-dev-menu": "~56.0.17", "expo-dev-menu-interface": "~56.0.0", "expo-manifests": "~56.0.4", "expo-updates-interface": "~56.0.1" }, "peerDependencies": { "expo": "*" } }, "sha512-KebW4r8HhIiRrPzs6ZqVhp/so8buyglAO1h4No0Ibr5C2XRnlIoGWCN4zC6rW7IsI3iKUXcofLAQV9OjoxjiwQ=="],
@@ -2069,7 +2099,7 @@
"expo-font": ["expo-font@56.0.7", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-hpU/vRwPzsby9lPGkA4blDqLIIXYzoWnCZHr6PxvcWbY/uPObAiyhh6q+e0WYsB65SthK+PLH95jEnVag7fwEg=="], "expo-font": ["expo-font@56.0.7", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-hpU/vRwPzsby9lPGkA4blDqLIIXYzoWnCZHr6PxvcWbY/uPObAiyhh6q+e0WYsB65SthK+PLH95jEnVag7fwEg=="],
"expo-glass-effect": ["expo-glass-effect@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-xI9rXtDwi7RW82uAlfyaXO6+k21ApWJ2tHAWYqPr/FjfmZbKsgNJ4Q0iZzGPCwboqjTGxaRZ61SZxBl8hDt5iA=="], "expo-glass-effect": ["expo-glass-effect@0.1.10", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-R0OrsMbs2TxFQZp26rRDl1Wxu5PaBXM7qAxiT0Bfyb1bojr3eI90bMKry9lBM3aKbq7DOBXYT7ePrE3vUaf41g=="],
"expo-haptics": ["expo-haptics@56.0.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-ycoahZJnR9tWAVh/0mJYxbETtHRYaWjiWS8cHlP6aDGU6Q6Y8rZ5NKsuBwWw6HR2Pe30mfVFgbF2HrBR6gtYmw=="], "expo-haptics": ["expo-haptics@56.0.3", "", { "peerDependencies": { "expo": "*" } }, "sha512-ycoahZJnR9tWAVh/0mJYxbETtHRYaWjiWS8cHlP6aDGU6Q6Y8rZ5NKsuBwWw6HR2Pe30mfVFgbF2HrBR6gtYmw=="],
@@ -2079,6 +2109,8 @@
"expo-keep-awake": ["expo-keep-awake@56.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CLMJXtEiMKknD3Rpm8CRwE6ZJUzu2yCEmRk1sgfHAJ1zIbuEWY3dpPDubtsnuzWm+2k6Sru+yaFbYsvPWmTiBA=="], "expo-keep-awake": ["expo-keep-awake@56.0.3", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CLMJXtEiMKknD3Rpm8CRwE6ZJUzu2yCEmRk1sgfHAJ1zIbuEWY3dpPDubtsnuzWm+2k6Sru+yaFbYsvPWmTiBA=="],
"expo-linear-gradient": ["expo-linear-gradient@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-KUp1dNSRtuMyiExhf6FJf5YUtmw2cRaPytl10HQi7isj5Yac38udmD55T2tglNYTZlvgT5+oflpyFoH15hmOcw=="],
"expo-linking": ["expo-linking@56.0.14", "", { "dependencies": { "expo-constants": "~56.0.18", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-IvVQHWC+Cj4fK5qD3iEVYqpU2a4rLW0IpAAlGJ4MH+H1fyZiHh3eN6qg2WmoclOEPfYATSuEa+dQT6wfgVpXlQ=="], "expo-linking": ["expo-linking@56.0.14", "", { "dependencies": { "expo-constants": "~56.0.18", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-IvVQHWC+Cj4fK5qD3iEVYqpU2a4rLW0IpAAlGJ4MH+H1fyZiHh3eN6qg2WmoclOEPfYATSuEa+dQT6wfgVpXlQ=="],
"expo-location": ["expo-location@56.0.18", "", { "dependencies": { "@expo/image-utils": "^0.10.1" }, "peerDependencies": { "expo": "*" } }, "sha512-6xP0UwGy8a7EEHAMeigYAp3HNo3yWHAg05tVPUfwrOWepWPpFXmjsfUBUxQdkpfpjddJ9r+f4PplxZqKI0LtjA=="], "expo-location": ["expo-location@56.0.18", "", { "dependencies": { "@expo/image-utils": "^0.10.1" }, "peerDependencies": { "expo": "*" } }, "sha512-6xP0UwGy8a7EEHAMeigYAp3HNo3yWHAg05tVPUfwrOWepWPpFXmjsfUBUxQdkpfpjddJ9r+f4PplxZqKI0LtjA=="],
@@ -2091,8 +2123,12 @@
"expo-modules-jsi": ["expo-modules-jsi@56.0.10", "", { "peerDependencies": { "react-native": "*" } }, "sha512-fHZcFpYO/o62GYa6fJyAQJZcAShzhoN0iMMDzbr7vD3ewET6e1vAlTonbEakN9F0VHEgBFJ4NREy87uwVcpCuA=="], "expo-modules-jsi": ["expo-modules-jsi@56.0.10", "", { "peerDependencies": { "react-native": "*" } }, "sha512-fHZcFpYO/o62GYa6fJyAQJZcAShzhoN0iMMDzbr7vD3ewET6e1vAlTonbEakN9F0VHEgBFJ4NREy87uwVcpCuA=="],
"expo-network": ["expo-network@56.0.5", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-zmuyO95jayDY9jyUfOAlNp9XXJrJaAOkBXXLy0TS/nh2kppj7CHirRPkQ/tf0rsxhIL3AEd9nsRTiPtNsGT9Lw=="],
"expo-router": ["expo-router@56.2.11", "", { "dependencies": { "@expo/log-box": "^56.0.13", "@expo/metro-runtime": "^56.0.15", "@expo/schema-utils": "^56.0.0", "@expo/ui": "^56.0.18", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "client-only": "^0.0.1", "color": "^4.2.3", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^56.0.4", "expo-server": "^56.0.5", "expo-symbols": "^56.0.6", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-is": "^19.1.0", "react-native-drawer-layout": "^4.2.2", "react-native-screens": "^4.25.2", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "standard-navigation": "^0.0.5", "vaul": "^1.1.2" }, "peerDependencies": { "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^56.0.18", "expo-linking": "^56.0.14", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-08DBTrKv3QanOc9u1JNxSEChW9c/qNFbQ0dO28OLvufWWfdSRkSdHmh365D2FgoZg1qaOzZPCDuL3tM6nGSfkQ=="], "expo-router": ["expo-router@56.2.11", "", { "dependencies": { "@expo/log-box": "^56.0.13", "@expo/metro-runtime": "^56.0.15", "@expo/schema-utils": "^56.0.0", "@expo/ui": "^56.0.18", "@radix-ui/react-slot": "^1.2.0", "@radix-ui/react-tabs": "^1.1.12", "@react-native-masked-view/masked-view": "^0.3.2", "@testing-library/jest-dom": "^6.9.1", "@testing-library/user-event": "^14.6.1", "client-only": "^0.0.1", "color": "^4.2.3", "debug": "^4.3.4", "escape-string-regexp": "^4.0.0", "expo-glass-effect": "^56.0.4", "expo-server": "^56.0.5", "expo-symbols": "^56.0.6", "fast-deep-equal": "^3.1.3", "invariant": "^2.2.4", "nanoid": "^3.3.8", "query-string": "^7.1.3", "react-fast-compare": "^3.2.2", "react-is": "^19.1.0", "react-native-drawer-layout": "^4.2.2", "react-native-screens": "^4.25.2", "server-only": "^0.0.1", "sf-symbols-typescript": "^2.1.0", "shallowequal": "^1.1.0", "standard-navigation": "^0.0.5", "vaul": "^1.1.2" }, "peerDependencies": { "@testing-library/react-native": ">= 13.2.0", "expo": "*", "expo-constants": "^56.0.18", "expo-linking": "^56.0.14", "react": "*", "react-dom": "*", "react-native": "*", "react-native-gesture-handler": "*", "react-native-reanimated": "*", "react-native-safe-area-context": ">= 5.4.0", "react-native-web": "*", "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4" }, "optionalPeers": ["@testing-library/react-native", "react-dom", "react-native-gesture-handler", "react-native-reanimated", "react-native-web", "react-server-dom-webpack"] }, "sha512-08DBTrKv3QanOc9u1JNxSEChW9c/qNFbQ0dO28OLvufWWfdSRkSdHmh365D2FgoZg1qaOzZPCDuL3tM6nGSfkQ=="],
"expo-secure-store": ["expo-secure-store@56.0.4", "", { "peerDependencies": { "expo": "*" } }, "sha512-hjEi/gmpdFFJ9lYbdp3k3p/WchV7Gi0Qt8jt/m/0WJadqQrskafHAlDxbZkII1cN3Yd7zp9Lvkeq3UfGhSwirQ=="],
"expo-server": ["expo-server@56.0.5", "", {}, "sha512-SmM2p2g3Jrktpiazcst+OxhjSzOHXKAY4BPURHYHXvApzzoybMmrNF4IEZ8DKZ145BhSe4ydAmlEFCRTsdtgUQ=="], "expo-server": ["expo-server@56.0.5", "", {}, "sha512-SmM2p2g3Jrktpiazcst+OxhjSzOHXKAY4BPURHYHXvApzzoybMmrNF4IEZ8DKZ145BhSe4ydAmlEFCRTsdtgUQ=="],
"expo-splash-screen": ["expo-splash-screen@56.0.10", "", { "dependencies": { "@expo/config-plugins": "~56.0.8", "@expo/image-utils": "^0.10.1", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-vDIlo8hzt9HlCZQ0kSY66v83D1WEXOJbVMeyPDfXDu9tbDdPMNUyDpi4WGJXikAjxnAKfbt5Mv5NnEbxINy+VA=="], "expo-splash-screen": ["expo-splash-screen@56.0.10", "", { "dependencies": { "@expo/config-plugins": "~56.0.8", "@expo/image-utils": "^0.10.1", "xml2js": "0.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-vDIlo8hzt9HlCZQ0kSY66v83D1WEXOJbVMeyPDfXDu9tbDdPMNUyDpi4WGJXikAjxnAKfbt5Mv5NnEbxINy+VA=="],
@@ -2463,6 +2499,8 @@
"jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="], "jose": ["jose@6.2.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
"jotai": ["jotai@2.20.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-dnuKfU/GLi8B28RRMjQ3AfoN7kfzP8o41+AX2FmITZqEMY8PHnjABq+VkEooomLwYaGjda+pgy0yFSjaHX/ZPg=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
@@ -2499,7 +2537,7 @@
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"kysely": ["kysely@0.28.11", "", {}, "sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg=="], "kysely": ["kysely@0.29.2", "", {}, "sha512-s6WVJyEZrbm6jhBpiKHsGHyePMrVQKJ85wZCFCr9W4QHv6WTjWIrdvTmO9hDEA3bNK0xkrE2DqrHsXMLWuZpQg=="],
"lan-network": ["lan-network@0.2.1", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A=="], "lan-network": ["lan-network@0.2.1", "", { "bin": { "lan-network": "dist/lan-network-cli.js" } }, "sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A=="],
@@ -3015,10 +3053,14 @@
"react-native-drawer-layout": ["react-native-drawer-layout@4.2.5", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-Yl82uLkXjXuq7222hWGIDsq5A6R/bsCeCEgdIxQUxAEHf00oRdDnRByLx3Fsij3qwtmYNPGrHV1NH8G8hbCbLQ=="], "react-native-drawer-layout": ["react-native-drawer-layout@4.2.5", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-Yl82uLkXjXuq7222hWGIDsq5A6R/bsCeCEgdIxQUxAEHf00oRdDnRByLx3Fsij3qwtmYNPGrHV1NH8G8hbCbLQ=="],
"react-native-easing-gradient": ["react-native-easing-gradient@1.1.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-KrGU97kPTMrLNzVXxFirFMgl65uo70gVNoKw2OngDzcDfoswb648I3ggeCnoY+JgMWkPyx4LlG7n3ZNgSPFnLw=="],
"react-native-gesture-handler": ["react-native-gesture-handler@2.31.2", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A=="], "react-native-gesture-handler": ["react-native-gesture-handler@2.31.2", "", { "dependencies": { "@egjs/hammerjs": "^2.0.17", "@types/react-test-renderer": "^19.1.0", "hoist-non-react-statics": "^3.3.0", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-rw5q74i2AfS7YGYdbxQDhOU7xqgY6WRM1132/CCm3erqjblhECZDZFHIm0tteHoC9ih24wogVBVVzcTBQtZ+5A=="],
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="], "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.3.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-NIXU/iT5+ORyCc7p0z2nnlkouYKX425vuU1OEm6bMMtWWR9yvb+Xg5AZmImTKoF9abxCPqrKC3rOZsKzUYgYZA=="],
"react-native-keyboard-controller": ["react-native-keyboard-controller@1.21.6", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-reanimated": ">=3.0.0" } }, "sha512-nAXCmar/W8Gn4iQV7O5fAVuTh57JszCsqTS+cfR95WFOLR/AfbwfPz/+sWyz/q2SOIe2VpyQzq6hzYiwErhqqw=="],
"react-native-reanimated": ["react-native-reanimated@4.3.1", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.81 - 0.85", "react-native-worklets": "0.8.x" } }, "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q=="], "react-native-reanimated": ["react-native-reanimated@4.3.1", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.3.1", "semver": "^7.7.3" }, "peerDependencies": { "react": "*", "react-native": "0.81 - 0.85", "react-native-worklets": "0.8.x" } }, "sha512-KhGsS0YkCA+gusgyzlf9hnqzVPIR398KTpqXyqq/+yYJJPAvyEEPKcxlB0xtOOXSMrR2A9uRKVARVQhZwrOh+Q=="],
"react-native-safe-area-context": ["react-native-safe-area-context@5.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ=="], "react-native-safe-area-context": ["react-native-safe-area-context@5.7.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ=="],
@@ -3341,7 +3383,7 @@
"tsdav": ["tsdav@2.1.8", "", { "dependencies": { "base-64": "1.0.0", "cross-fetch": "4.1.0", "debug": "4.4.3", "xml-js": "1.6.11" } }, "sha512-zvQvhZLzTaEmNNgJbBlUYT/JOq9Xpw/xkxCqs7IT2d2/7o7pss0iZOlZXuHJ5VcvSvTny42Vc6+6GyzZcrCJ1g=="], "tsdav": ["tsdav@2.1.8", "", { "dependencies": { "base-64": "1.0.0", "cross-fetch": "4.1.0", "debug": "4.4.3", "xml-js": "1.6.11" } }, "sha512-zvQvhZLzTaEmNNgJbBlUYT/JOq9Xpw/xkxCqs7IT2d2/7o7pss0iZOlZXuHJ5VcvSvTny42Vc6+6GyzZcrCJ1g=="],
"tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
@@ -3521,60 +3563,12 @@
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@aws-crypto/crc32/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/sha256-browser/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/sha256-js/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/supports-web-crypto/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-crypto/util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/client-bedrock-runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-env/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.8", "", { "dependencies": { "@smithy/core": "^3.24.7", "@smithy/types": "^4.14.4", "tslib": "^2.6.2" } }, "sha512-f+DbsWUwSbtMu1a/j8Y93KiU1SRg9nyzfjereqn1BJ33QOTUXxdlYvVXMhAYl1vuR1Kmna5aIJe09KSIfyFNYw=="], "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.8", "", { "dependencies": { "@smithy/core": "^3.24.7", "@smithy/types": "^4.14.4", "tslib": "^2.6.2" } }, "sha512-f+DbsWUwSbtMu1a/j8Y93KiU1SRg9nyzfjereqn1BJ33QOTUXxdlYvVXMhAYl1vuR1Kmna5aIJe09KSIfyFNYw=="],
"@aws-sdk/credential-provider-http/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-ini/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-login/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-node/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-process/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1066.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA=="], "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1066.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.20", "@aws-sdk/nested-clients": "^3.997.20", "@aws-sdk/types": "^3.973.12", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA=="],
"@aws-sdk/credential-provider-sso/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/credential-provider-web-identity/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/eventstream-handler-node/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-eventstream/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/middleware-websocket/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.8", "", { "dependencies": { "@smithy/core": "^3.24.7", "@smithy/types": "^4.14.4", "tslib": "^2.6.2" } }, "sha512-f+DbsWUwSbtMu1a/j8Y93KiU1SRg9nyzfjereqn1BJ33QOTUXxdlYvVXMhAYl1vuR1Kmna5aIJe09KSIfyFNYw=="], "@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.8", "", { "dependencies": { "@smithy/core": "^3.24.7", "@smithy/types": "^4.14.4", "tslib": "^2.6.2" } }, "sha512-f+DbsWUwSbtMu1a/j8Y93KiU1SRg9nyzfjereqn1BJ33QOTUXxdlYvVXMhAYl1vuR1Kmna5aIJe09KSIfyFNYw=="],
"@aws-sdk/nested-clients/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/signature-v4-multi-region/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/token-providers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/util-locate-window/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@aws-sdk/xml-builder/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/generator/jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "@babel/generator/jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -3615,6 +3609,12 @@
"@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], "@ecies/ciphers/@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="],
"@emnapi/core/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"@emnapi/runtime/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"@emnapi/wasi-threads/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
@@ -3679,16 +3679,6 @@
"@expo/xcpretty/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "@expo/xcpretty/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"@formatjs/ecma402-abstract/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@formatjs/fast-memoize/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@formatjs/icu-messageformat-parser/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@formatjs/icu-skeleton-parser/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@formatjs/intl-localematcher/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@google/genai/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "@google/genai/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -3737,26 +3727,6 @@
"@react-router/serve/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], "@react-router/serve/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="],
"@smithy/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/credential-provider-imds/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/fetch-http-handler/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/is-array-buffer/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/node-http-handler/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/signature-v4/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-buffer-from/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@smithy/util-utf8/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "@tailwindcss/node/lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
@@ -3781,6 +3751,8 @@
"@ts-morph/common/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "@ts-morph/common/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
"@tybys/wasm-util/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"@types/pg/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], "@types/pg/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
@@ -3793,7 +3765,7 @@
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "aria-hidden/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -3877,6 +3849,8 @@
"expo-router/@expo/metro-runtime": ["@expo/metro-runtime@56.0.15", "", { "dependencies": { "@expo/log-box": "^56.0.13", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-WIWeVsL6kCSB57oYZdUA4MTkH7c67UFMIjdNoQzKXwxZYwBFE/xL2cGPDC3z8RWt0femzJTVxAVZUOW/hiqRzA=="], "expo-router/@expo/metro-runtime": ["@expo/metro-runtime@56.0.15", "", { "dependencies": { "@expo/log-box": "^56.0.13", "anser": "^1.4.9", "pretty-format": "^29.7.0", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-dom": "*", "react-native": "*" }, "optionalPeers": ["react-dom"] }, "sha512-WIWeVsL6kCSB57oYZdUA4MTkH7c67UFMIjdNoQzKXwxZYwBFE/xL2cGPDC3z8RWt0femzJTVxAVZUOW/hiqRzA=="],
"expo-router/expo-glass-effect": ["expo-glass-effect@56.0.4", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-xI9rXtDwi7RW82uAlfyaXO6+k21ApWJ2tHAWYqPr/FjfmZbKsgNJ4Q0iZzGPCwboqjTGxaRZ61SZxBl8hDt5iA=="],
"express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
@@ -3887,8 +3861,6 @@
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"framer-motion/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"freya-client/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "freya-client/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "giget/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
@@ -3899,8 +3871,6 @@
"import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
"intl-messageformat/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"jest-util/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], "jest-util/@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="],
"jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -3937,6 +3907,8 @@
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], "morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
"motion/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"npm-package-arg/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], "npm-package-arg/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
@@ -3983,7 +3955,11 @@
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], "react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
"recast/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "react-remove-scroll/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"react-remove-scroll-bar/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"react-style-singleton/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"regjsparser/jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "regjsparser/jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -4013,6 +3989,10 @@
"twrnc/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], "twrnc/tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="],
"use-callback-ref/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"use-sidecar/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
"vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
"vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "vite-node/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],

View File

@@ -29,5 +29,8 @@
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "^6" "typescript": "^6"
},
"patchedDependencies": {
"@ark/schema@0.56.0": "patches/@ark%2Fschema@0.56.0.patch"
} }
} }

View File

@@ -8,7 +8,8 @@
"test": "bun test ." "test": "bun test ."
}, },
"dependencies": { "dependencies": {
"@standard-schema/spec": "^1.1.0" "@standard-schema/spec": "^1.1.0",
"arktype": "^2.1.29"
}, },
"peerDependencies": { "peerDependencies": {
"@json-render/core": "*", "@json-render/core": "*",

View File

@@ -7,7 +7,7 @@ import {
ConversationEntryMetadata, ConversationEntryMetadata,
GenericObjectPayload, GenericObjectPayload,
UserMessagePayload, UserMessagePayload,
} from "./types.ts" } from "./conversation"
describe("conversation entry schemas", () => { describe("conversation entry schemas", () => {
test("parses valid user message payloads", () => { test("parses valid user message payloads", () => {

View File

@@ -1,5 +1,6 @@
import { type } from "arktype" import { type } from "arktype"
/** Entry kinds supported by the persisted conversation timeline. */
export const ConversationEntryKind = { export const ConversationEntryKind = {
UserMessage: "user_message", UserMessage: "user_message",
AssistantMessage: "assistant_message", AssistantMessage: "assistant_message",
@@ -10,17 +11,21 @@ export const ConversationEntryKind = {
SystemNote: "system_note", SystemNote: "system_note",
} as const } as const
/** Discriminator for the payload shape and handling of a conversation entry. */
export type ConversationEntryKind = export type ConversationEntryKind =
(typeof ConversationEntryKind)[keyof typeof ConversationEntryKind] (typeof ConversationEntryKind)[keyof typeof ConversationEntryKind]
/** Visibility scopes supported by stored conversation entries. */
export const ConversationEntryVisibility = { export const ConversationEntryVisibility = {
UserVisible: "user_visible", UserVisible: "user_visible",
Internal: "internal", Internal: "internal",
} as const } as const
/** Indicates whether a conversation entry should be exposed to the user. */
export type ConversationEntryVisibility = export type ConversationEntryVisibility =
(typeof ConversationEntryVisibility)[keyof typeof ConversationEntryVisibility] (typeof ConversationEntryVisibility)[keyof typeof ConversationEntryVisibility]
/** Attachment media categories accepted by conversation entries. */
export const AttachmentType = { export const AttachmentType = {
Image: "image", Image: "image",
Audio: "audio", Audio: "audio",
@@ -29,57 +34,64 @@ export const AttachmentType = {
Other: "other", Other: "other",
} as const } as const
/** File or media category associated with an attachment payload. */
export type AttachmentType = (typeof AttachmentType)[keyof typeof AttachmentType] export type AttachmentType = (typeof AttachmentType)[keyof typeof AttachmentType]
export const ConversationEntryKindInput = type.enumerated(...Object.values(ConversationEntryKind)) /** Plain text content part for a message. */
export const ConversationEntryVisibilityInput = type.enumerated( export const TextMessagePart = type({
...Object.values(ConversationEntryVisibility),
)
export const AttachmentTypeInput = type.enumerated(...Object.values(AttachmentType))
const TextMessagePart = type({
"+": "reject", "+": "reject",
type: "'text'", type: "'text'",
text: "string", text: "string",
}) })
const JsonMessagePart = type({ /** Structured JSON content part for a message. */
export const JsonMessagePart = type({
"+": "reject", "+": "reject",
type: "'json'", type: "'json'",
value: "unknown", value: "unknown",
}) })
/** Content part variants supported by user and assistant messages. */
export const MessagePart = type.or(TextMessagePart, JsonMessagePart) export const MessagePart = type.or(TextMessagePart, JsonMessagePart)
/** A structured content part inside a user or assistant message payload. */
export type MessagePart = typeof MessagePart.infer export type MessagePart = typeof MessagePart.infer
/** User-authored message entry payload. */
export const UserMessagePayload = type({ export const UserMessagePayload = type({
"+": "reject", "+": "reject",
role: "'user'", role: "'user'",
parts: MessagePart.array().atLeastLength(1), parts: MessagePart.array().atLeastLength(1),
}) })
/** Payload stored for a conversation entry containing a user message. */
export type UserMessagePayload = typeof UserMessagePayload.infer export type UserMessagePayload = typeof UserMessagePayload.infer
/** Assistant-authored message entry payload. */
export const AssistantMessagePayload = type({ export const AssistantMessagePayload = type({
"+": "reject", "+": "reject",
role: "'assistant'", role: "'assistant'",
parts: MessagePart.array().atLeastLength(1), parts: MessagePart.array().atLeastLength(1),
}) })
/** Payload stored for a conversation entry containing an assistant message. */
export type AssistantMessagePayload = typeof AssistantMessagePayload.infer export type AssistantMessagePayload = typeof AssistantMessagePayload.infer
/** Attachment entry payload. */
export const AttachmentPayload = type({ export const AttachmentPayload = type({
"+": "reject", "+": "reject",
role: type.enumerated("user", "assistant"), role: type.enumerated("user", "assistant"),
name: "string", name: "string",
mimeType: "string", mimeType: "string",
attachmentType: AttachmentTypeInput, attachmentType: type.enumerated(...Object.values(AttachmentType)),
"caption?": "string", "caption?": "string",
}) })
/** Payload stored for a conversation entry that references an uploaded file. */
export type AttachmentPayload = typeof AttachmentPayload.infer export type AttachmentPayload = typeof AttachmentPayload.infer
const ContextSummary = type({ /** Durable facts extracted from compacted conversation history. */
export const ContextSummary = type({
"+": "reject", "+": "reject",
"userIntent?": "string", "userIntent?": "string",
durableFacts: type.string.array(), durableFacts: type.string.array(),
@@ -89,6 +101,10 @@ const ContextSummary = type({
importantDetails: type.string.array(), importantDetails: type.string.array(),
}) })
/** Durable facts and follow-ups retained from compacted conversation history. */
export type ContextSummary = typeof ContextSummary.infer
/** Context-summary conversation entry payload. */
export const ContextSummaryPayload = type({ export const ContextSummaryPayload = type({
"+": "reject", "+": "reject",
covers: type({ covers: type({
@@ -101,8 +117,10 @@ export const ContextSummaryPayload = type({
"sourceEntryIds?": type.string.array(), "sourceEntryIds?": type.string.array(),
}) })
/** Payload describing a compaction summary and the sequence range it covers. */
export type ContextSummaryPayload = typeof ContextSummaryPayload.infer export type ContextSummaryPayload = typeof ContextSummaryPayload.infer
/** Model invocation metadata recorded on generated entries. */
export const ModelRunMetadata = type({ export const ModelRunMetadata = type({
"+": "reject", "+": "reject",
route: "string", route: "string",
@@ -116,18 +134,33 @@ export const ModelRunMetadata = type({
"providerRequestId?": "string", "providerRequestId?": "string",
}) })
/** Metadata describing the model run that produced a conversation entry. */
export type ModelRunMetadata = typeof ModelRunMetadata.infer export type ModelRunMetadata = typeof ModelRunMetadata.infer
/** Arbitrary metadata stored alongside conversation entries. */
export const ConversationEntryMetadata = type({ export const ConversationEntryMetadata = type({
"modelRun?": ModelRunMetadata, "modelRun?": ModelRunMetadata,
"[string]": "unknown", "[string]": "unknown",
}) })
/** Metadata bag attached to a conversation entry. */
export type ConversationEntryMetadata = typeof ConversationEntryMetadata.infer export type ConversationEntryMetadata = typeof ConversationEntryMetadata.infer
/** Generic object payload used by operational entries. */
export const GenericObjectPayload = type("Record<string, unknown>") export const GenericObjectPayload = type("Record<string, unknown>")
/** Fallback payload shape for tool calls, tool results, and system notes. */
export type GenericObjectPayload = typeof GenericObjectPayload.infer export type GenericObjectPayload = typeof GenericObjectPayload.infer
export const ConversationEntryPayload = type.or(
UserMessagePayload,
AssistantMessagePayload,
AttachmentPayload,
ContextSummaryPayload,
GenericObjectPayload,
)
/** Union of payload shapes that can be stored on a conversation entry. */
export type ConversationEntryPayload = export type ConversationEntryPayload =
| UserMessagePayload | UserMessagePayload
| AssistantMessagePayload | AssistantMessagePayload

View File

@@ -1,50 +1,84 @@
// Context // Context
export type { ContextEntry, ContextKey, ContextKeyPart } from "./context" export type { ContextEntry, ContextKey, ContextKeyPart } from "./context";
export { Context, contextKey, serializeKey } from "./context" export { Context, contextKey, serializeKey } from "./context";
// Actions // Actions
export type { ActionDefinition } from "./action" export type { ActionDefinition } from "./action";
export { UnknownActionError } from "./action" export { UnknownActionError } from "./action";
// Conversation
export {
AssistantMessagePayload,
AttachmentPayload,
AttachmentType,
ContextSummary,
ContextSummaryPayload,
ConversationEntryKind,
ConversationEntryMetadata,
ConversationEntryVisibility,
GenericObjectPayload,
JsonMessagePart,
MessagePart,
ModelRunMetadata,
TextMessagePart,
UserMessagePayload,
ConversationEntryPayload,
} from "./conversation";
// Feed // Feed
export type { FeedItem, FeedItemRenderer, FeedItemSignals, RenderedFeedItem, Slot } from "./feed" export type {
export { TimeRelevance } from "./feed" FeedItem,
FeedItemRenderer,
FeedItemSignals,
RenderedFeedItem,
Slot,
} from "./feed";
export { TimeRelevance } from "./feed";
// Feed Source // Feed Source
export type { FeedSource } from "./feed-source" export type { FeedSource } from "./feed-source";
// Feed Post-Processor // Feed Post-Processor
export type { FeedEnhancement, FeedPostProcessor, ItemGroup } from "./feed-post-processor" export type {
FeedEnhancement,
FeedPostProcessor,
ItemGroup,
} from "./feed-post-processor";
// Feed Engine // Feed Engine
export type { FeedEngineConfig, FeedResult, FeedSubscriber, SourceError } from "./feed-engine" export type {
export { FeedEngine } from "./feed-engine" FeedEngineConfig,
FeedResult,
FeedSubscriber,
SourceError,
} from "./feed-engine";
export { FeedEngine } from "./feed-engine";
// ============================================================================= // =============================================================================
// DEPRECATED - Use FeedSource + FeedEngine instead // DEPRECATED - Use FeedSource + FeedEngine instead
// ============================================================================= // =============================================================================
// Data Source (deprecated - use FeedSource) // Data Source (deprecated - use FeedSource)
export type { DataSource } from "./data-source" export type { DataSource } from "./data-source";
// Context Provider (deprecated - use FeedSource) // Context Provider (deprecated - use FeedSource)
export type { ContextProvider } from "./context-provider" export type { ContextProvider } from "./context-provider";
// Context Bridge (deprecated - use FeedEngine) // Context Bridge (deprecated - use FeedEngine)
export type { ProviderError, RefreshResult } from "./context-bridge" export type { ProviderError, RefreshResult } from "./context-bridge";
export { ContextBridge } from "./context-bridge" export { ContextBridge } from "./context-bridge";
// Reconciler (deprecated - use FeedEngine) // Reconciler (deprecated - use FeedEngine)
export type { export type {
ReconcileResult, ReconcileResult,
ReconcilerConfig, ReconcilerConfig,
SourceError as ReconcilerSourceError, SourceError as ReconcilerSourceError,
} from "./reconciler" } from "./reconciler";
export { Reconciler } from "./reconciler" export { Reconciler } from "./reconciler";
// Feed Controller (deprecated - use FeedEngine) // Feed Controller (deprecated - use FeedEngine)
export type { export type {
FeedControllerConfig, FeedControllerConfig,
FeedSubscriber as FeedControllerSubscriber, FeedSubscriber as FeedControllerSubscriber,
} from "./feed-controller" } from "./feed-controller";
export { FeedController } from "./feed-controller" export { FeedController } from "./feed-controller";

View File

@@ -0,0 +1,23 @@
diff --git a/out/shared/disjoint.js b/out/shared/disjoint.js
index 9fb94ad4f257b95225536eb5fdf20eaf6193b7a6..377af13b28807b1943f14240c1e889c3b16efe94 100644
--- a/out/shared/disjoint.js
+++ b/out/shared/disjoint.js
@@ -48,11 +48,17 @@ export class Disjoint extends Array {
return result;
}
withPrefixKey(key, kind) {
- return this.map(entry => ({
+ const result = this.map(entry => ({
...entry,
path: [key, ...entry.path],
optional: entry.optional || kind === "optional"
}));
+ // Workaround for Static Hermes, which doesn't preserve the Disjoint Array subclass here.
+ // Mirrors the existing invert() guard added for https://github.com/arktypeio/arktype/issues/1027
+ // and covers the same failure mode reported in https://github.com/arktypeio/arktype/issues/1415.
+ if (!(result instanceof Disjoint))
+ return new Disjoint(...result);
+ return result;
}
toNeverIfDisjoint() {
return $ark.intrinsic.never;

18
patches/README.md Normal file
View File

@@ -0,0 +1,18 @@
# Dependency Patches
## `@ark/schema@0.56.0`
`@ark/schema` is patched for React Native/Hermes compatibility.
ArkType's internal `Disjoint` type extends `Array`. In Hermes, `Array.prototype.map()`
does not always preserve the subclass instance. If `Disjoint.withPrefixKey()` returns
a plain array, later ArkType reduction code can call schema methods such as `isRoot()`
on that array and crash during app startup.
The patch mirrors ArkType's existing guard in `Disjoint.invert()` by wrapping the
mapped result back into `new Disjoint(...)` when Hermes returns a plain array.
Upstream context:
- https://github.com/arktypeio/arktype/issues/1415
- https://github.com/arktypeio/arktype/issues/1027

11
skills-lock.json Normal file
View File

@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"upgrading-expo": {
"source": "expo/skills",
"sourceType": "github",
"skillPath": "plugins/expo/skills/upgrading-expo/SKILL.md",
"computedHash": "98d228925a442126789d90783fdd4ae5de33ab690024b575698057d2ffc44b40"
}
}
}