diff --git a/.gitignore b/.gitignore
index 83ee0ef..1b4d9ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,7 @@ apps/mobile/ios/build/
apps/mobile/office-game/dist/
apps/mobile/office-game/node_modules/
+# Claude Code / oh-my-claudecode session state
+.omc/
+
apps/*/wrangler.local.toml
diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx
index 10ac2b7..63feb30 100644
--- a/apps/mobile/App.tsx
+++ b/apps/mobile/App.tsx
@@ -238,7 +238,13 @@ const OFFICE_DEV_PORT = 5174;
const DEV_URL_OVERRIDE = process.env.EXPO_PUBLIC_OFFICE_DEV_URL;
// eslint-disable-next-line @typescript-eslint/no-var-requires
-const officeHtmlString: string = require('./office-game/dist/office-inline.js').html;
+let officeHtmlString: string = '';
+try {
+ officeHtmlString = require('./office-game/dist/office-inline.js').html;
+} catch (e) {
+ console.warn('Office game bundle not found, using fallback:', (e as Error).message);
+ officeHtmlString = '
';
+}
const OFFICE_HTML_BG_REGEX = /(html,\s*body\s*\{[^}]*background-color:\s*)([^;]+)(;)/;
@@ -247,10 +253,13 @@ const GATEWAY_KEEPALIVE_INTERVAL_MS = 5_000;
function resolveDevHost(): string {
const scriptURL = (NativeModules as { SourceCode?: { scriptURL?: string } }).SourceCode?.scriptURL;
- if (!scriptURL) return 'localhost';
+ if (!scriptURL) {
+ return 'localhost';
+ }
try {
- const { hostname } = new URL(scriptURL);
- return hostname || 'localhost';
+ const url = new URL(scriptURL);
+ const hostname = url.hostname || 'localhost';
+ return hostname;
} catch {
return 'localhost';
}
@@ -258,7 +267,9 @@ function resolveDevHost(): string {
function resolveOfficeDevUrl(): string {
const override = DEV_URL_OVERRIDE?.trim();
- if (override) return override;
+ if (override) {
+ return override;
+ }
const host = resolveDevHost();
return `http://${host}:${OFFICE_DEV_PORT}`;
}
diff --git a/apps/mobile/app.json b/apps/mobile/app.json
index 3a423cc..d69d4ba 100644
--- a/apps/mobile/app.json
+++ b/apps/mobile/app.json
@@ -2,7 +2,7 @@
"expo": {
"name": "Clawket",
"slug": "clawket",
- "version": "2.1.0",
+ "version": "3.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
@@ -14,7 +14,7 @@
},
"ios": {
"supportsTablet": false,
- "bundleIdentifier": "com.p697.clawket",
+ "bundleIdentifier": "com.binarybros.clawket",
"infoPlist": {
"NSCameraUsageDescription": "Allow Clawket to use your camera to scan QR codes and take photos to send to AI.",
"NSPhotoLibraryAddUsageDescription": "Allow Clawket to save stats posters to your photo library.",
@@ -32,7 +32,8 @@
"CFBundleDevelopmentRegion": "en",
"UIPrefersShowingLanguageSettings": true
},
- "appleTeamId": "C8TM82D73W"
+ "appleTeamId": "ZWT3N8XKE7",
+ "buildNumber": "260626001"
},
"android": {
"adaptiveIcon": {
@@ -126,6 +127,6 @@
"projectId": "972e845f-da81-44db-a908-24be4ca80288"
}
},
- "owner": "p697"
+ "owner": "binarybros"
}
}
diff --git a/apps/mobile/ios/SceneDelegate.swift b/apps/mobile/ios/SceneDelegate.swift
new file mode 100644
index 0000000..0075dc7
--- /dev/null
+++ b/apps/mobile/ios/SceneDelegate.swift
@@ -0,0 +1,35 @@
+ import UIKit
+
+ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
+
+ var window: UIWindow?
+
+ func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
+ // Scene setup (e.g., initial view controller)
+ guard let windowScene = scene as? UIWindowScene else { return }
+ window = UIWindow(windowScene: windowScene)
+ // Assign your root view controller (e.g., from AppDelegate or React Native entry point)
+ window?.rootViewController = UIViewController() // Replace with your app's root (e.g., from Expo)
+ window?.makeKeyAndVisible()
+ }
+
+ func sceneDidDisconnect(_ scene: UIScene) {
+ // Cleanup when scene disconnects
+ }
+
+ func sceneDidBecomeActive(_ scene: UIScene) {
+ // App became active
+ }
+
+ func sceneWillResignActive(_ scene: UIScene) {
+ // App will resign active
+ }
+
+ func sceneWillEnterForeground(_ scene: UIScene) {
+ // App entering foreground
+ }
+
+ func sceneDidEnterBackground(_ scene: UIScene) {
+ // App entered background
+ }
+ }
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 0a79b3e..36eed77 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -1,6 +1,6 @@
{
"name": "clawket",
- "version": "2.1.0",
+ "version": "3.0.0",
"license": "AGPL-3.0-only",
"main": "index.ts",
"scripts": {
diff --git a/apps/mobile/src/bootstrap/useAppBootstrap.ts b/apps/mobile/src/bootstrap/useAppBootstrap.ts
index 6963762..0c793ed 100644
--- a/apps/mobile/src/bootstrap/useAppBootstrap.ts
+++ b/apps/mobile/src/bootstrap/useAppBootstrap.ts
@@ -25,7 +25,7 @@ type Props = {
function buildAgentPreview(
agentId: string,
- backendKind: 'openclaw' | 'hermes' | 'youmind',
+ backendKind: 'openclaw' | 'hermes' | 'youmind' | 'agentzero',
identity?: {
agentName?: string;
agentEmoji?: string;
diff --git a/apps/mobile/src/components/chat/PairingPendingCard.tsx b/apps/mobile/src/components/chat/PairingPendingCard.tsx
index e44cb96..85d868f 100644
--- a/apps/mobile/src/components/chat/PairingPendingCard.tsx
+++ b/apps/mobile/src/components/chat/PairingPendingCard.tsx
@@ -8,7 +8,7 @@ type Props = {
approveCommand: string;
copied: boolean;
onCopy: () => void;
- connectionMode?: 'relay' | 'local' | 'tailscale' | 'cloudflare' | 'custom' | 'hermes';
+ connectionMode?: 'relay' | 'local' | 'tailscale' | 'cloudflare' | 'custom' | 'hermes' | 'agentzero';
onRetry?: () => void;
};
diff --git a/apps/mobile/src/features/app-updates/releases.ts b/apps/mobile/src/features/app-updates/releases.ts
index 0fe5622..cf7e45d 100644
--- a/apps/mobile/src/features/app-updates/releases.ts
+++ b/apps/mobile/src/features/app-updates/releases.ts
@@ -52,6 +52,23 @@ export const DEFAULT_APP_UPDATE_DEBUG_HINT =
// Keep this array newest-first. The first entry is treated as the latest release.
export const APP_UPDATE_RELEASES: AppUpdateRelease[] = [
+ {
+ version: '3.0.0',
+ releasedAt: '2026-05-30',
+ entries: [
+ {
+ id: 'hermes-tailscale-endpoint',
+ emoji: '🪽',
+ title: 'Hermes over Tailscale',
+ subtitle: 'Reach your Hermes bridge by MagicDNS — no more ephemeral tunnels expiring.',
+ action: {
+ type: 'navigate_config_add_connection',
+ tab: 'quick',
+ flow: 'local',
+ },
+ },
+ ],
+ },
{
version: '2.1.0',
releasedAt: '2026-04-12',
diff --git a/apps/mobile/src/hooks/gatewayScanFlow.ts b/apps/mobile/src/hooks/gatewayScanFlow.ts
index 3d59760..850bda3 100644
--- a/apps/mobile/src/hooks/gatewayScanFlow.ts
+++ b/apps/mobile/src/hooks/gatewayScanFlow.ts
@@ -59,6 +59,7 @@ export function toRuntimeConfig(item: SavedGatewayConfig, debugMode: boolean): G
transportKind,
mode: toLegacyGatewayMode({ backendKind, transportKind }),
hermes: item.hermes,
+ agentzero: item.agentzero,
relay: item.relay,
debugMode,
};
diff --git a/apps/mobile/src/hooks/useGatewayConfigForm.test.ts b/apps/mobile/src/hooks/useGatewayConfigForm.test.ts
index 6beb79b..e506f43 100644
--- a/apps/mobile/src/hooks/useGatewayConfigForm.test.ts
+++ b/apps/mobile/src/hooks/useGatewayConfigForm.test.ts
@@ -620,4 +620,97 @@ describe('useGatewayConfigForm', () => {
expect(gateway.disconnect).toHaveBeenCalledTimes(1);
expect(onReset).toHaveBeenCalledTimes(1);
});
+
+ it('saves an Agent Zero connection with backendKind=agentzero, normalized bridgeUrl, and token', async () => {
+ const onSaved = jest.fn();
+ const gateway = {
+ disconnect: jest.fn(),
+ configure: jest.fn(),
+ connect: jest.fn(),
+ getDeviceIdentity: jest.fn().mockResolvedValue({ deviceId: 'device-az' }),
+ } as any;
+
+ const { result } = renderHook(() =>
+ useGatewayConfigForm({
+ gateway,
+ initialConfig: null,
+ debugMode: false,
+ onSaved,
+ onReset: jest.fn(),
+ }),
+ );
+
+ await act(async () => {
+ result.current.openCreateEditor('manual');
+ });
+
+ await act(async () => {
+ result.current.setEditorBackendKind('agentzero');
+ result.current.setEditorUrl('https://agent-habitat.capybara-loggerhead.ts.net:8443/');
+ result.current.setEditorAuthMethod('token');
+ result.current.setEditorToken('6Gv7AhbIbZ8CEjUb');
+ result.current.setEditorName('Habitat AZ');
+ });
+
+ await act(async () => {
+ await result.current.saveEditor();
+ });
+
+ const lastCall = (StorageService.setGatewayConfigsState as jest.Mock).mock.calls.at(-1)?.[0];
+ expect(lastCall.configs[0]).toMatchObject({
+ name: 'Habitat AZ',
+ backendKind: 'agentzero',
+ transportKind: 'custom',
+ mode: 'agentzero',
+ url: 'https://agent-habitat.capybara-loggerhead.ts.net:8443/',
+ token: '6Gv7AhbIbZ8CEjUb',
+ password: undefined,
+ hermes: undefined,
+ agentzero: {
+ // Trailing slash stripped, no path appended.
+ bridgeUrl: 'https://agent-habitat.capybara-loggerhead.ts.net:8443',
+ },
+ });
+ expect(onSaved).toHaveBeenCalled();
+ });
+
+ it('rejects an Agent Zero connection without a token (X-API-KEY is required)', async () => {
+ const alertSpy = jest.spyOn(Alert, 'alert').mockImplementation(() => undefined);
+ const onSaved = jest.fn();
+ const gateway = {
+ disconnect: jest.fn(),
+ configure: jest.fn(),
+ connect: jest.fn(),
+ getDeviceIdentity: jest.fn().mockResolvedValue({ deviceId: 'device-az' }),
+ } as any;
+
+ const { result } = renderHook(() =>
+ useGatewayConfigForm({
+ gateway,
+ initialConfig: null,
+ debugMode: false,
+ onSaved,
+ onReset: jest.fn(),
+ }),
+ );
+
+ await act(async () => {
+ result.current.openCreateEditor('manual');
+ });
+
+ await act(async () => {
+ result.current.setEditorBackendKind('agentzero');
+ result.current.setEditorUrl('https://agent-habitat.capybara-loggerhead.ts.net:8443');
+ // no token
+ });
+
+ await act(async () => {
+ await result.current.saveEditor();
+ });
+
+ expect(alertSpy).toHaveBeenCalledWith('Missing Auth', expect.any(String));
+ expect(StorageService.setGatewayConfigsState).not.toHaveBeenCalled();
+ expect(onSaved).not.toHaveBeenCalled();
+ alertSpy.mockRestore();
+ });
});
diff --git a/apps/mobile/src/hooks/useGatewayConfigForm.ts b/apps/mobile/src/hooks/useGatewayConfigForm.ts
index 6db0b80..93b418f 100644
--- a/apps/mobile/src/hooks/useGatewayConfigForm.ts
+++ b/apps/mobile/src/hooks/useGatewayConfigForm.ts
@@ -63,6 +63,10 @@ function normalizeEditorTransportKind(backendKind: GatewayBackendKind, transport
if (backendKind === 'youmind') {
return 'custom';
}
+ if (backendKind === 'agentzero') {
+ // AZ is plain HTTP to its web server; no relay/local/tailscale distinction.
+ return 'custom';
+ }
return transportKind;
}
@@ -88,6 +92,35 @@ function deriveHermesBridgeConfig(
}
}
+/**
+ * Agent Zero wants a base HTTP URL (e.g. https://agent-habitat.capybara-loggerhead.ts.net:8443).
+ * We accept ws://… too as a courtesy — users frequently paste their Hermes-style URL
+ * by accident — and rewrite to http(s).
+ */
+function deriveAgentZeroConfig(
+ url: string,
+ existing?: SavedGatewayConfig['agentzero'],
+): SavedGatewayConfig['agentzero'] | undefined {
+ const trimmed = url.trim();
+ if (!trimmed) {
+ return existing;
+ }
+ try {
+ const parsed = new URL(trimmed.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
+ parsed.hash = '';
+ parsed.search = '';
+ // AZ has no canonical chat path; strip anything the user accidentally pasted.
+ parsed.pathname = '/';
+ return {
+ bridgeUrl: parsed.toString().replace(/\/+$/, ''),
+ ...(existing?.displayName ? { displayName: existing.displayName } : {}),
+ ...(existing?.projectName ? { projectName: existing.projectName } : {}),
+ };
+ } catch {
+ return existing;
+ }
+}
+
function detectAuthMethod(token?: string, password?: string): GatewayAuthMethod {
if ((token ?? '').trim()) return 'token';
if ((password ?? '').trim()) return 'password';
@@ -187,7 +220,12 @@ export function useGatewayConfigForm({ gateway, initialConfig, debugMode, onSave
() => toLegacyGatewayMode({ backendKind: editorBackendKind, transportKind: manualEditorTransportKind }),
[editorBackendKind, manualEditorTransportKind],
);
- const editorRequiresDirectAuth = editorBackendKind === 'openclaw' && manualEditorTransportKind !== 'relay';
+ // OpenClaw wants a chat-server token/password; Agent Zero wants its X-API-KEY
+ // surfaced via the same token field. Hermes embeds its token directly in the
+ // ws:// URL query string, so it doesn't need a separate auth input.
+ const editorRequiresDirectAuth =
+ (editorBackendKind === 'openclaw' && manualEditorTransportKind !== 'relay')
+ || editorBackendKind === 'agentzero';
const setEditorBackendKind = useCallback((nextBackendKind: GatewayBackendKind) => {
setEditorBackendKindState(nextBackendKind);
@@ -474,11 +512,16 @@ export function useGatewayConfigForm({ gateway, initialConfig, debugMode, onSave
url: trimmedUrl,
index: configs.length + 1,
});
- const token = backendKind === 'openclaw' && editorAuthMethodState === 'token' ? (trimmedToken || undefined) : undefined;
+ // Agent Zero piggybacks on the OpenClaw `token` field for its X-API-KEY.
+ const tokenBackendUsesAuthField = backendKind === 'openclaw' || backendKind === 'agentzero';
+ const token = tokenBackendUsesAuthField && editorAuthMethodState === 'token' ? (trimmedToken || undefined) : undefined;
const password = backendKind === 'openclaw' && editorAuthMethodState === 'password' ? (trimmedPassword || undefined) : undefined;
const hermes = backendKind === 'hermes'
? deriveHermesBridgeConfig(trimmedUrl, configs.find((item) => item.id === editingConfigId)?.hermes)
: undefined;
+ const agentzero = backendKind === 'agentzero'
+ ? deriveAgentZeroConfig(trimmedUrl, configs.find((item) => item.id === editingConfigId)?.agentzero)
+ : undefined;
const relay = transportKind === 'relay'
? {
serverUrl: trimmedServerUrl,
@@ -506,6 +549,7 @@ export function useGatewayConfigForm({ gateway, initialConfig, debugMode, onSave
token,
password,
hermes,
+ agentzero,
relay,
updatedAt: now,
};
@@ -540,6 +584,7 @@ export function useGatewayConfigForm({ gateway, initialConfig, debugMode, onSave
token,
password,
hermes,
+ agentzero,
relay,
createdAt: now,
updatedAt: now,
diff --git a/apps/mobile/src/i18n/locales/de/chat.json b/apps/mobile/src/i18n/locales/de/chat.json
index 2fd39f8..460fc66 100644
--- a/apps/mobile/src/i18n/locales/de/chat.json
+++ b/apps/mobile/src/i18n/locales/de/chat.json
@@ -322,9 +322,8 @@
"New message from {{agentName}}": "Neue Nachricht von {{agentName}}",
"YouMind": "YouMind",
"New chat": "New chat",
- "Sign in to YouMind": "Sign in to YouMind",
- "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Sign in to YouMind": "Bei YouMind anmelden",
+ "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Continue with Apple, Google, or email to open your YouMind account.": "Continue with Apple, Google, or email to open your YouMind account.",
"Continue with Apple": "Continue with Apple",
"Continue with Google": "Continue with Google",
@@ -381,5 +380,14 @@
"You now can connect your YouMind account.": "Du kannst jetzt dein YouMind-Konto verbinden.",
"Open Add Connection and choose Use YouMind to pick an existing account or sign in with another one.": "Öffne Add Connection und wähle Use YouMind, um ein vorhandenes Konto auszuwählen oder dich mit einem anderen Konto anzumelden.",
"Manage YouMind accounts": "YouMind-Konten verwalten",
- "Sign out": "Sign out"
+ "Sign out": "Sign out",
+ "Connection not configured": "Verbindung nicht konfiguriert",
+ "Set the Agent Zero base URL and Auth Token in Settings → Connections.": "Lege die Agent-Zero-Basis-URL und das Auth-Token in Einstellungen → Verbindungen fest.",
+ "Reset Chat": "Chat zurücksetzen",
+ "Project: {{name}}": "Projekt: {{name}}",
+ "Say hi to Agent Zero. Streaming begins after your first message.": "Begrüße Agent Zero. Das Streaming startet nach deiner ersten Nachricht.",
+ "Agent Zero is working…": "Agent Zero arbeitet…",
+ "Message Agent Zero": "Nachricht an Agent Zero",
+ "Send": "Senden",
+ "Agent Zero": "Agent Zero"
}
diff --git a/apps/mobile/src/i18n/locales/de/config.json b/apps/mobile/src/i18n/locales/de/config.json
index 4d61062..4146cc1 100644
--- a/apps/mobile/src/i18n/locales/de/config.json
+++ b/apps/mobile/src/i18n/locales/de/config.json
@@ -79,6 +79,7 @@
"Backend": "Backend",
"Transport": "Transport",
"OpenClaw": "OpenClaw",
+ "Agent Zero": "Agent Zero",
"Connection Mode": "Verbindungsmodus",
"Relay": "Relay",
"Local": "Lokal",
diff --git a/apps/mobile/src/i18n/locales/en/chat.json b/apps/mobile/src/i18n/locales/en/chat.json
index 8464eef..6f5d50c 100644
--- a/apps/mobile/src/i18n/locales/en/chat.json
+++ b/apps/mobile/src/i18n/locales/en/chat.json
@@ -324,7 +324,6 @@
"New chat": "New chat",
"Sign in to YouMind": "Sign in to YouMind",
"Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
- "Sign in to YouMind": "Sign in to YouMind",
"Continue with Apple, Google, or email to open your YouMind account.": "Continue with Apple, Google, or email to open your YouMind account.",
"Continue with Apple": "Continue with Apple",
"Continue with Google": "Continue with Google",
@@ -381,5 +380,14 @@
"You now can connect your YouMind account.": "You now can connect your YouMind account.",
"Open Add Connection and choose Use YouMind to pick an existing account or sign in with another one.": "Open Add Connection and choose Use YouMind to pick an existing account or sign in with another one.",
"Manage YouMind accounts": "Manage YouMind accounts",
- "Sign out": "Sign out"
+ "Sign out": "Sign out",
+ "Connection not configured": "Connection not configured",
+ "Set the Agent Zero base URL and Auth Token in Settings → Connections.": "Set the Agent Zero base URL and Auth Token in Settings → Connections.",
+ "Reset Chat": "Reset Chat",
+ "Project: {{name}}": "Project: {{name}}",
+ "Say hi to Agent Zero. Streaming begins after your first message.": "Say hi to Agent Zero. Streaming begins after your first message.",
+ "Agent Zero is working…": "Agent Zero is working…",
+ "Message Agent Zero": "Message Agent Zero",
+ "Send": "Send",
+ "Agent Zero": "Agent Zero"
}
diff --git a/apps/mobile/src/i18n/locales/en/config.json b/apps/mobile/src/i18n/locales/en/config.json
index e34edb9..df9f339 100644
--- a/apps/mobile/src/i18n/locales/en/config.json
+++ b/apps/mobile/src/i18n/locales/en/config.json
@@ -79,6 +79,7 @@
"Backend": "Backend",
"Transport": "Transport",
"OpenClaw": "OpenClaw",
+ "Agent Zero": "Agent Zero",
"Connection Mode": "Connection Mode",
"Relay": "Relay",
"Local": "Local",
diff --git a/apps/mobile/src/i18n/locales/es/chat.json b/apps/mobile/src/i18n/locales/es/chat.json
index 524abc9..368e1bd 100644
--- a/apps/mobile/src/i18n/locales/es/chat.json
+++ b/apps/mobile/src/i18n/locales/es/chat.json
@@ -322,9 +322,8 @@
"New message from {{agentName}}": "Nuevo mensaje de {{agentName}}",
"YouMind": "YouMind",
"New chat": "New chat",
- "Sign in to YouMind": "Sign in to YouMind",
- "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Sign in to YouMind": "Inicia sesión en YouMind",
+ "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Continue with Apple, Google, or email to open your YouMind account.": "Continue with Apple, Google, or email to open your YouMind account.",
"Continue with Apple": "Continue with Apple",
"Continue with Google": "Continue with Google",
@@ -381,5 +380,14 @@
"You now can connect your YouMind account.": "Ahora puedes conectar tu cuenta de YouMind.",
"Open Add Connection and choose Use YouMind to pick an existing account or sign in with another one.": "Abre Add Connection y elige Use YouMind para seleccionar una cuenta existente o iniciar sesión con otra.",
"Manage YouMind accounts": "Gestionar cuentas de YouMind",
- "Sign out": "Sign out"
+ "Sign out": "Sign out",
+ "Connection not configured": "Conexión no configurada",
+ "Set the Agent Zero base URL and Auth Token in Settings → Connections.": "Configura la URL base de Agent Zero y el Auth Token en Ajustes → Conexiones.",
+ "Reset Chat": "Reiniciar chat",
+ "Project: {{name}}": "Proyecto: {{name}}",
+ "Say hi to Agent Zero. Streaming begins after your first message.": "Saluda a Agent Zero. La transmisión comienza tras tu primer mensaje.",
+ "Agent Zero is working…": "Agent Zero está trabajando…",
+ "Message Agent Zero": "Mensaje a Agent Zero",
+ "Send": "Enviar",
+ "Agent Zero": "Agent Zero"
}
diff --git a/apps/mobile/src/i18n/locales/es/config.json b/apps/mobile/src/i18n/locales/es/config.json
index dca58e9..405c707 100644
--- a/apps/mobile/src/i18n/locales/es/config.json
+++ b/apps/mobile/src/i18n/locales/es/config.json
@@ -79,6 +79,7 @@
"Backend": "Backend",
"Transport": "Transporte",
"OpenClaw": "OpenClaw",
+ "Agent Zero": "Agent Zero",
"Connection Mode": "Modo de conexión",
"Relay": "Relay",
"Local": "Local",
diff --git a/apps/mobile/src/i18n/locales/ja/chat.json b/apps/mobile/src/i18n/locales/ja/chat.json
index 268f938..06b752b 100644
--- a/apps/mobile/src/i18n/locales/ja/chat.json
+++ b/apps/mobile/src/i18n/locales/ja/chat.json
@@ -322,9 +322,8 @@
"New message from {{agentName}}": "{{agentName}} から新しいメッセージ",
"YouMind": "YouMind",
"New chat": "New chat",
- "Sign in to YouMind": "Sign in to YouMind",
- "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Sign in to YouMind": "YouMind にサインイン",
+ "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Continue with Apple, Google, or email to open your YouMind account.": "Continue with Apple, Google, or email to open your YouMind account.",
"Continue with Apple": "Continue with Apple",
"Continue with Google": "Continue with Google",
@@ -381,5 +380,14 @@
"You now can connect your YouMind account.": "YouMind アカウントを接続できるようになりました。",
"Open Add Connection and choose Use YouMind to pick an existing account or sign in with another one.": "Add Connection を開いて Use YouMind を選択すると、既存アカウントを選ぶか別のアカウントでサインインできます。",
"Manage YouMind accounts": "YouMind アカウントを管理",
- "Sign out": "Sign out"
+ "Sign out": "Sign out",
+ "Connection not configured": "接続が未設定です",
+ "Set the Agent Zero base URL and Auth Token in Settings → Connections.": "設定 → 接続 で Agent Zero のベース URL と Auth Token を設定してください。",
+ "Reset Chat": "チャットをリセット",
+ "Project: {{name}}": "プロジェクト: {{name}}",
+ "Say hi to Agent Zero. Streaming begins after your first message.": "Agent Zero に挨拶しましょう。最初のメッセージを送るとストリーミングが始まります。",
+ "Agent Zero is working…": "Agent Zero が処理中…",
+ "Message Agent Zero": "Agent Zero にメッセージ",
+ "Send": "送信",
+ "Agent Zero": "Agent Zero"
}
diff --git a/apps/mobile/src/i18n/locales/ja/config.json b/apps/mobile/src/i18n/locales/ja/config.json
index 2b2fa47..dce6287 100644
--- a/apps/mobile/src/i18n/locales/ja/config.json
+++ b/apps/mobile/src/i18n/locales/ja/config.json
@@ -79,6 +79,7 @@
"Backend": "バックエンド",
"Transport": "接続方式",
"OpenClaw": "OpenClaw",
+ "Agent Zero": "Agent Zero",
"Connection Mode": "接続モード",
"Relay": "リレー",
"Local": "ローカル",
diff --git a/apps/mobile/src/i18n/locales/ko/chat.json b/apps/mobile/src/i18n/locales/ko/chat.json
index d97edcd..fca677e 100644
--- a/apps/mobile/src/i18n/locales/ko/chat.json
+++ b/apps/mobile/src/i18n/locales/ko/chat.json
@@ -322,9 +322,8 @@
"New message from {{agentName}}": "{{agentName}} 님의 새 메시지",
"YouMind": "YouMind",
"New chat": "New chat",
- "Sign in to YouMind": "Sign in to YouMind",
- "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Sign in to YouMind": "YouMind에 로그인",
+ "Open your YouMind boards, materials, and chats from here.": "Open your YouMind boards, materials, and chats from here.",
"Continue with Apple, Google, or email to open your YouMind account.": "Continue with Apple, Google, or email to open your YouMind account.",
"Continue with Apple": "Continue with Apple",
"Continue with Google": "Continue with Google",
@@ -381,5 +380,14 @@
"You now can connect your YouMind account.": "이제 YouMind 계정을 연결할 수 있어요.",
"Open Add Connection and choose Use YouMind to pick an existing account or sign in with another one.": "Add Connection을 연 뒤 Use YouMind를 선택하면 기존 계정을 고르거나 다른 계정으로 로그인할 수 있습니다.",
"Manage YouMind accounts": "YouMind 계정 관리",
- "Sign out": "Sign out"
+ "Sign out": "Sign out",
+ "Connection not configured": "연결이 구성되지 않았습니다",
+ "Set the Agent Zero base URL and Auth Token in Settings → Connections.": "설정 → 연결에서 Agent Zero 기본 URL과 Auth Token을 설정하세요.",
+ "Reset Chat": "채팅 초기화",
+ "Project: {{name}}": "프로젝트: {{name}}",
+ "Say hi to Agent Zero. Streaming begins after your first message.": "Agent Zero에 인사를 건네 보세요. 첫 메시지를 보내면 스트리밍이 시작됩니다.",
+ "Agent Zero is working…": "Agent Zero가 작업 중…",
+ "Message Agent Zero": "Agent Zero에 메시지",
+ "Send": "전송",
+ "Agent Zero": "Agent Zero"
}
diff --git a/apps/mobile/src/i18n/locales/ko/config.json b/apps/mobile/src/i18n/locales/ko/config.json
index 311750b..e090b1f 100644
--- a/apps/mobile/src/i18n/locales/ko/config.json
+++ b/apps/mobile/src/i18n/locales/ko/config.json
@@ -79,6 +79,7 @@
"Backend": "백엔드",
"Transport": "전송 방식",
"OpenClaw": "OpenClaw",
+ "Agent Zero": "Agent Zero",
"Connection Mode": "연결 모드",
"Relay": "릴레이",
"Local": "로컬",
diff --git a/apps/mobile/src/i18n/locales/zh-Hans/chat.json b/apps/mobile/src/i18n/locales/zh-Hans/chat.json
index fd960a1..c27c536 100644
--- a/apps/mobile/src/i18n/locales/zh-Hans/chat.json
+++ b/apps/mobile/src/i18n/locales/zh-Hans/chat.json
@@ -324,7 +324,6 @@
"New chat": "新聊天",
"Sign in to YouMind": "登录 YouMind",
"Open your YouMind boards, materials, and chats from here.": "在这里打开你的 YouMind 画板、资料和聊天。",
- "Sign in to YouMind": "登录 YouMind",
"Continue with Apple, Google, or email to open your YouMind account.": "使用 Apple、Google 或邮箱继续,打开你的 YouMind 账户。",
"Continue with Apple": "使用 Apple 继续",
"Continue with Google": "使用 Google 继续",
@@ -381,5 +380,14 @@
"You now can connect your YouMind account.": "你现在可以连接你的 YouMind 账号。",
"Open Add Connection and choose Use YouMind to pick an existing account or sign in with another one.": "打开 Add Connection,然后选择 Use YouMind,以选择已有账号或登录另一个账号。",
"Manage YouMind accounts": "管理 YouMind 账号",
- "Sign out": "退出登录"
+ "Sign out": "退出登录",
+ "Connection not configured": "连接未配置",
+ "Set the Agent Zero base URL and Auth Token in Settings → Connections.": "在 设置 → 连接 中填写 Agent Zero 的地址和 Auth Token。",
+ "Reset Chat": "重置对话",
+ "Project: {{name}}": "项目:{{name}}",
+ "Say hi to Agent Zero. Streaming begins after your first message.": "向 Agent Zero 打个招呼。发送第一条消息后即开始流式响应。",
+ "Agent Zero is working…": "Agent Zero 正在处理…",
+ "Message Agent Zero": "发送给 Agent Zero",
+ "Send": "发送",
+ "Agent Zero": "Agent Zero"
}
diff --git a/apps/mobile/src/i18n/locales/zh-Hans/config.json b/apps/mobile/src/i18n/locales/zh-Hans/config.json
index 9217c95..339eca7 100644
--- a/apps/mobile/src/i18n/locales/zh-Hans/config.json
+++ b/apps/mobile/src/i18n/locales/zh-Hans/config.json
@@ -79,6 +79,7 @@
"Backend": "后端",
"Transport": "连接方式",
"OpenClaw": "OpenClaw",
+ "Agent Zero": "Agent Zero",
"Connection Mode": "连接模式",
"Relay": "中继",
"Local": "本地",
diff --git a/apps/mobile/src/screens/ChatScreen/AgentZeroChatTab.tsx b/apps/mobile/src/screens/ChatScreen/AgentZeroChatTab.tsx
new file mode 100644
index 0000000..ec991a1
--- /dev/null
+++ b/apps/mobile/src/screens/ChatScreen/AgentZeroChatTab.tsx
@@ -0,0 +1,446 @@
+// Agent Zero chat tab.
+//
+// Reached when the active gateway config has backendKind === 'agentzero'.
+// Self-contained: doesn't share the OpenClaw/Hermes WS gateway, no drawer,
+// no sidebar (yet). Streams progressive log items via AgentZeroClient and
+// renders them as bubbles. The full a0 CLI flavor (project switching, slash
+// commands, agent profile picker) is intentionally deferred — those layer on
+// top of this baseline.
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import {
+ ActivityIndicator,
+ FlatList,
+ KeyboardAvoidingView,
+ Platform,
+ Pressable,
+ StyleSheet,
+ Text,
+ TextInput,
+ View,
+ type ListRenderItem,
+} from 'react-native';
+import { useTranslation } from 'react-i18next';
+import { useSafeAreaInsets } from 'react-native-safe-area-context';
+import { RotateCcw, SendHorizontal } from 'lucide-react-native';
+import { useTabBarHeight } from '../../hooks/useTabBarHeight';
+import { useAppContext } from '../../contexts/AppContext';
+import { useAppTheme, type AppTheme } from '../../theme';
+import { FontSize, FontWeight, Radius, Space } from '../../theme/tokens';
+import {
+ createAgentZeroClient,
+ AgentZeroError,
+ type AgentZeroLogItem,
+} from '../../services/agentzero-client';
+
+type Colors = AppTheme['colors'];
+
+type BubbleAuthor = 'you' | 'agent' | 'tool' | 'system' | 'error';
+
+interface ChatBubble {
+ /** Stable id — for user messages we mint a uuid; for AZ items we reuse
+ * the framework's log item id (or its `no` if id is null). */
+ key: string;
+ author: BubbleAuthor;
+ /** Display text. */
+ content: string;
+ /** Optional short label rendered above the bubble (tool name, etc.). */
+ heading?: string;
+}
+
+const AZ_TYPE_TO_AUTHOR: Record = {
+ user: 'you',
+ agent: 'agent',
+ response: 'agent',
+ tool: 'tool',
+ util: 'tool',
+ error: 'error',
+};
+
+function classifyLogItem(item: AgentZeroLogItem): BubbleAuthor {
+ return AZ_TYPE_TO_AUTHOR[item.type] ?? 'system';
+}
+
+function mintKey(prefix: string): string {
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
+}
+
+export function AgentZeroChatTab(): React.JSX.Element {
+ const { config } = useAppContext();
+ const { theme } = useAppTheme();
+ const styles = useMemo(() => createStyles(theme.colors), [theme]);
+ const insets = useSafeAreaInsets();
+ // Bottom tab bar overlays the bottom of the screen on iOS; subtract its
+ // height so the composer sits above it instead of behind it. Falls back to
+ // 0 in non-tab contexts (modal flows).
+ const tabBarHeight = useTabBarHeight();
+ const { t } = useTranslation(['chat', 'common']);
+
+ const bridgeUrl = config?.agentzero?.bridgeUrl ?? '';
+ const token = config?.token ?? '';
+ const projectName = config?.agentzero?.projectName;
+
+ const client = useMemo(
+ () => (bridgeUrl && token
+ ? createAgentZeroClient({ bridgeUrl, projectName }, token)
+ : null),
+ [bridgeUrl, token, projectName],
+ );
+
+ const [bubbles, setBubbles] = useState([]);
+ const [contextId, setContextId] = useState(null);
+ const [draft, setDraft] = useState('');
+ const [sending, setSending] = useState(false);
+ const abortRef = useRef(null);
+ const listRef = useRef>(null);
+
+ // Clean up any in-flight stream if the screen unmounts.
+ useEffect(() => () => abortRef.current?.abort(), []);
+
+ const appendBubble = useCallback((b: ChatBubble) => {
+ setBubbles((prev) => [...prev, b]);
+ // Defer scroll-to-end so FlatList has the new item.
+ requestAnimationFrame(() => {
+ listRef.current?.scrollToEnd({ animated: true });
+ });
+ }, []);
+
+ const resetChat = useCallback(() => {
+ abortRef.current?.abort();
+ abortRef.current = null;
+ setBubbles([]);
+ setContextId(null);
+ setSending(false);
+ }, []);
+
+ const handleSend = useCallback(async () => {
+ if (!client) return;
+ const text = draft.trim();
+ if (!text || sending) return;
+
+ appendBubble({ key: mintKey('user'), author: 'you', content: text });
+ setDraft('');
+ setSending(true);
+
+ const controller = new AbortController();
+ abortRef.current = controller;
+ // Dedupe per-stream: AZ's poll-window returns historical items too; the
+ // client already filters by `no` cursor, but we additionally skip user
+ // echoes (we just rendered the user bubble ourselves).
+ const seen = new Set();
+ try {
+ const final = await client.streamMessage(
+ { message: text, contextId: contextId ?? undefined },
+ (item) => {
+ if (item.type === 'user') return;
+ const key = item.id ?? `no-${item.no}`;
+ if (seen.has(key)) return;
+ seen.add(key);
+ appendBubble({
+ key,
+ author: classifyLogItem(item),
+ content: item.content,
+ heading: item.heading || undefined,
+ });
+ },
+ { pollIntervalMs: 400 },
+ controller.signal,
+ );
+ setContextId(final.contextId);
+ } catch (err) {
+ const message = err instanceof AgentZeroError
+ ? `${err.status}: ${err.message}`
+ : err instanceof Error
+ ? err.message
+ : String(err);
+ appendBubble({ key: mintKey('err'), author: 'error', content: message });
+ } finally {
+ setSending(false);
+ abortRef.current = null;
+ }
+ }, [appendBubble, client, contextId, draft, sending]);
+
+ const renderItem: ListRenderItem = useCallback(({ item }) => (
+
+
+ {item.heading ? {item.heading} : null}
+
+ {item.content || ' '}
+
+
+
+ ), [styles]);
+
+ // Inline header (the shared ChatHeader is shaped for OpenClaw connection
+ // state which doesn't apply to AZ's HTTP polling model).
+ const headerTitle = t('Agent Zero');
+ const headerSubtitle = projectName
+ ? t('Project: {{name}}', { name: projectName })
+ : undefined;
+
+ if (!client) {
+ return (
+
+
+
+ {headerTitle}
+ {t('Connection not configured')}
+
+
+
+
+ {t('Set the Agent Zero base URL and Auth Token in Settings → Connections.')}
+
+
+
+ );
+ }
+
+ // Composer bottom padding has to clear:
+ // - the tab bar (iOS native bottom tabs draw OVER the screen content)
+ // - the home indicator (insets.bottom on devices without a tab bar)
+ // - never less than Space.md so the input doesn't kiss the edge
+ const composerBottomInset = Math.max(Space.md, tabBarHeight, insets.bottom);
+
+ return (
+
+
+
+ {headerTitle}
+ {headerSubtitle ? {headerSubtitle} : null}
+
+ [styles.headerAction, pressed && styles.headerActionPressed]}
+ >
+
+
+
+
+ {bubbles.length === 0 && !sending ? (
+
+
+ {t('Say hi to Agent Zero. Streaming begins after your first message.')}
+
+
+ ) : (
+ b.key}
+ renderItem={renderItem}
+ contentContainerStyle={styles.bubbleList}
+ onContentSizeChange={() => listRef.current?.scrollToEnd({ animated: false })}
+ />
+ )}
+
+ {sending ? (
+
+
+ {t('Agent Zero is working…')}
+
+ ) : null}
+
+
+
+ [
+ styles.sendButton,
+ (sending || !draft.trim()) && styles.sendButtonDisabled,
+ pressed && styles.sendButtonPressed,
+ ]}
+ >
+
+
+
+
+ );
+}
+
+function bubbleStyleByAuthor(s: ReturnType, author: BubbleAuthor) {
+ switch (author) {
+ case 'you': return s.bubbleYou;
+ case 'agent': return s.bubbleAgent;
+ case 'tool': return s.bubbleTool;
+ case 'error': return s.bubbleError;
+ default: return s.bubbleSystem;
+ }
+}
+
+function bubbleTextStyleByAuthor(s: ReturnType, author: BubbleAuthor) {
+ switch (author) {
+ case 'you': return s.bubbleTextYou;
+ case 'error': return s.bubbleTextError;
+ default: return s.bubbleTextAgent;
+ }
+}
+
+function createStyles(colors: Colors) {
+ return StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ emptyHint: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: Space.xl,
+ },
+ emptyHintText: {
+ color: colors.textMuted,
+ fontSize: FontSize.base,
+ textAlign: 'center',
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: Space.lg,
+ paddingBottom: Space.sm,
+ borderBottomWidth: 1,
+ borderBottomColor: colors.border,
+ backgroundColor: colors.surface,
+ },
+ headerTextWrap: {
+ flex: 1,
+ },
+ headerTitle: {
+ color: colors.text,
+ fontSize: FontSize.lg,
+ fontWeight: FontWeight.semibold,
+ },
+ headerSubtitle: {
+ color: colors.textMuted,
+ fontSize: FontSize.sm,
+ marginTop: 2,
+ },
+ headerAction: {
+ padding: Space.sm,
+ },
+ headerActionPressed: {
+ opacity: 0.6,
+ },
+ bubbleList: {
+ paddingHorizontal: Space.lg,
+ paddingTop: Space.md,
+ paddingBottom: Space.md,
+ },
+ bubbleRow: {
+ flexDirection: 'row',
+ marginBottom: Space.sm,
+ },
+ bubbleRowRight: {
+ justifyContent: 'flex-end',
+ },
+ bubble: {
+ maxWidth: '85%',
+ paddingHorizontal: Space.md,
+ paddingVertical: Space.sm,
+ borderRadius: Radius.md,
+ },
+ bubbleYou: {
+ backgroundColor: colors.primary,
+ },
+ bubbleAgent: {
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.border,
+ },
+ bubbleTool: {
+ backgroundColor: colors.surfaceMuted,
+ },
+ bubbleSystem: {
+ backgroundColor: colors.surfaceMuted,
+ },
+ bubbleError: {
+ backgroundColor: colors.surface,
+ borderWidth: 1,
+ borderColor: colors.error,
+ },
+ bubbleHeading: {
+ color: colors.textMuted,
+ fontSize: FontSize.xs,
+ fontWeight: FontWeight.semibold,
+ marginBottom: Space.xs,
+ textTransform: 'uppercase',
+ },
+ bubbleText: {
+ fontSize: FontSize.base,
+ lineHeight: 20,
+ },
+ bubbleTextYou: {
+ color: colors.primaryText,
+ },
+ bubbleTextAgent: {
+ color: colors.text,
+ },
+ bubbleTextError: {
+ color: colors.error,
+ },
+ sendingRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: Space.sm,
+ paddingHorizontal: Space.lg,
+ paddingVertical: Space.xs,
+ },
+ sendingText: {
+ color: colors.textMuted,
+ fontSize: FontSize.sm,
+ },
+ composerWrap: {
+ flexDirection: 'row',
+ alignItems: 'flex-end',
+ gap: Space.sm,
+ paddingHorizontal: Space.md,
+ paddingTop: Space.sm,
+ backgroundColor: colors.surface,
+ borderTopWidth: 1,
+ borderTopColor: colors.border,
+ },
+ composerInput: {
+ flex: 1,
+ maxHeight: 120,
+ paddingHorizontal: Space.md,
+ paddingVertical: Space.sm,
+ borderRadius: Radius.lg,
+ backgroundColor: colors.surfaceMuted,
+ color: colors.text,
+ fontSize: FontSize.base,
+ },
+ sendButton: {
+ width: 44,
+ height: 44,
+ borderRadius: Radius.full,
+ backgroundColor: colors.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ sendButtonDisabled: {
+ opacity: 0.4,
+ },
+ sendButtonPressed: {
+ opacity: 0.7,
+ },
+ });
+}
diff --git a/apps/mobile/src/screens/ChatScreen/ChatTab.tsx b/apps/mobile/src/screens/ChatScreen/ChatTab.tsx
index de387ec..f07768d 100644
--- a/apps/mobile/src/screens/ChatScreen/ChatTab.tsx
+++ b/apps/mobile/src/screens/ChatScreen/ChatTab.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { InteractionManager, Platform, StyleSheet, View } from 'react-native';
+import { Platform, StyleSheet, View } from 'react-native';
import { CommonActions } from '@react-navigation/native';
import { DrawerContentComponentProps, createDrawerNavigator, useDrawerProgress } from '@react-navigation/drawer';
import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated';
@@ -15,6 +15,7 @@ import { ChatControllerProvider, useChatControllerContext } from './ChatControll
import { useChatController } from './hooks/useChatController';
import { ChatScreen } from './index';
import { YouMindChatTab } from './YouMindChatTab';
+import { AgentZeroChatTab } from './AgentZeroChatTab';
export type ChatDrawerParamList = {
ChatMain: undefined;
@@ -104,7 +105,7 @@ const ChatDrawerContent = React.memo(function ChatDrawerContent({
prevProgressRef.current = cur;
// Drawer just finished opening (progress crossed 1.0)
if (cur === 1 && prev < 1) {
- InteractionManager.runAfterInteractions(() => {
+ requestIdleCallback(() => {
void refreshSessions();
});
}
@@ -241,6 +242,9 @@ export function ChatTab(): React.JSX.Element {
if (config?.backendKind === 'youmind') {
return ;
}
+ if (config?.backendKind === 'agentzero') {
+ return ;
+ }
const [sidebarPreset, setSidebarPreset] = React.useState<{
requestedAt: number;
tab: 'sessions' | 'subagents' | 'cron';
diff --git a/apps/mobile/src/screens/ChatScreen/YouMindChatTab.tsx b/apps/mobile/src/screens/ChatScreen/YouMindChatTab.tsx
index 9ea7d45..8942c98 100644
--- a/apps/mobile/src/screens/ChatScreen/YouMindChatTab.tsx
+++ b/apps/mobile/src/screens/ChatScreen/YouMindChatTab.tsx
@@ -4,7 +4,6 @@ import {
Alert,
Animated as RNAnimated,
Image,
- InteractionManager,
Platform,
Pressable,
StyleProp,
@@ -404,7 +403,7 @@ const YouMindDrawerContent = React.memo(function YouMindDrawerContent({
const previous = prevProgressRef.current;
prevProgressRef.current = current;
if (current === 1 && previous < 1) {
- InteractionManager.runAfterInteractions(() => {
+ requestIdleCallback(() => {
void onRefreshSessions();
});
}
@@ -1062,7 +1061,7 @@ export function YouMindChatTab(): React.JSX.Element {
if (!selectedSkill) return;
const trackedSkill = selectedSkill;
setSelectedSkill(null);
- InteractionManager.runAfterInteractions(() => {
+ requestIdleCallback(() => {
analyticsEvents.chatSkillSelected({
source: 'youmind_chat',
action: 'clear',
@@ -1075,7 +1074,7 @@ export function YouMindChatTab(): React.JSX.Element {
const handleSelectRecommendedSkill = React.useCallback((skill: YouMindSkillSummary) => {
setSelectedSkill(skill);
- InteractionManager.runAfterInteractions(() => {
+ requestIdleCallback(() => {
analyticsEvents.chatSkillSelected({
source: 'youmind_chat_recommended',
action: 'select',
@@ -1101,7 +1100,7 @@ export function YouMindChatTab(): React.JSX.Element {
const handleSelectSkill = React.useCallback((skill: YouMindSkillSummary) => {
setSelectedSkill(skill);
skillPickerRef.current?.dismiss();
- InteractionManager.runAfterInteractions(() => {
+ requestIdleCallback(() => {
analyticsEvents.chatSkillSelected({
source: 'youmind_chat_picker',
action: 'select',
diff --git a/apps/mobile/src/screens/ChatScreen/hooks/useChatController.ts b/apps/mobile/src/screens/ChatScreen/hooks/useChatController.ts
index c38299b..4faf831 100644
--- a/apps/mobile/src/screens/ChatScreen/hooks/useChatController.ts
+++ b/apps/mobile/src/screens/ChatScreen/hooks/useChatController.ts
@@ -1075,10 +1075,14 @@ export function useChatController({
const hasRunningChat = !!currentRunIdRef.current;
// Refresh visible history after transport freshness has been re-established.
scheduleForegroundRefresh(awayMs, hasRunningChat);
+ // Always re-verify the socket on foreground: iOS freezes the WS in the
+ // background and it often returns half-open (state still "ready" but
+ // dead), so probeConnection forces a reconnect. Previously this was
+ // gated on hasRunningChat, leaving an idle open chat with a dead socket.
+ if (awayMs >= 12_000 || gateway.getConnectionState() !== "ready") {
+ void gateway.probeConnection();
+ }
if (hasRunningChat) {
- if (awayMs >= 12_000 || gateway.getConnectionState() !== "ready") {
- void gateway.probeConnection();
- }
if (history.sessionKey) {
void requestRunRecovery(history.sessionKey, "app-active");
}
diff --git a/apps/mobile/src/screens/ConfigScreen/ConfigScreenLayout.tsx b/apps/mobile/src/screens/ConfigScreen/ConfigScreenLayout.tsx
index 8e2ef27..3de14d2 100644
--- a/apps/mobile/src/screens/ConfigScreen/ConfigScreenLayout.tsx
+++ b/apps/mobile/src/screens/ConfigScreen/ConfigScreenLayout.tsx
@@ -101,6 +101,7 @@ function getBackendLabels(t: (key: string) => string): Record ((isEditing && controller.editorBackendKind === 'youmind')
? (['youmind'] as const)
- : (['openclaw', 'hermes'] as const)),
+ : (['openclaw', 'hermes', 'agentzero'] as const)),
[controller.editorBackendKind, isEditing],
);
const authInputLabel = controller.editorAuthMethod === 'token' ? t('Auth Token') : t('Password');
diff --git a/apps/mobile/src/screens/ConfigScreen/index.tsx b/apps/mobile/src/screens/ConfigScreen/index.tsx
index 683a107..bf21408 100644
--- a/apps/mobile/src/screens/ConfigScreen/index.tsx
+++ b/apps/mobile/src/screens/ConfigScreen/index.tsx
@@ -1,5 +1,5 @@
import React, { useCallback, useRef } from 'react';
-import { InteractionManager, Platform } from 'react-native';
+import { Platform } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { RouteProp, useIsFocused, useNavigation, useRoute } from '@react-navigation/native';
import { useTabBarHeight } from '../../hooks/useTabBarHeight';
@@ -103,7 +103,7 @@ export function ConfigScreen(): React.JSX.Element {
const pendingRequest = consumePendingConfigAddConnectionRequest();
if (!pendingRequest) return;
let cancelled = false;
- const task = InteractionManager.runAfterInteractions(() => {
+ const task = requestIdleCallback(() => {
requestAnimationFrame(() => {
if (cancelled) return;
openRequestedAddConnection(pendingRequest);
@@ -111,7 +111,7 @@ export function ConfigScreen(): React.JSX.Element {
});
return () => {
cancelled = true;
- task.cancel();
+ cancelIdleCallback(task);
};
}, [isFocused, openRequestedAddConnection]);
diff --git a/apps/mobile/src/screens/ConfigScreen/qrPayload.ts b/apps/mobile/src/screens/ConfigScreen/qrPayload.ts
index 3ec5de7..e769003 100644
--- a/apps/mobile/src/screens/ConfigScreen/qrPayload.ts
+++ b/apps/mobile/src/screens/ConfigScreen/qrPayload.ts
@@ -195,7 +195,7 @@ export function parseQRPayload(raw: string): QRScanResult | null {
return {
url: String(obj.url),
...(hermes ? { backendKind: 'hermes' as const } : {}),
- ...(mode && mode !== 'hermes' ? { transportKind: mode } : {}),
+ ...(mode && mode !== 'hermes' && mode !== 'agentzero' ? { transportKind: mode } : {}),
...(typeof obj.token === 'string' ? { token: obj.token } : {}),
...(typeof obj.password === 'string' ? { password: obj.password } : {}),
mode,
@@ -223,7 +223,7 @@ export function parseQRPayload(raw: string): QRScanResult | null {
return {
url: `${scheme}://${obj.host}:${port}`,
...(hermes ? { backendKind: 'hermes' as const } : {}),
- ...(mode && mode !== 'hermes' ? { transportKind: mode } : {}),
+ ...(mode && mode !== 'hermes' && mode !== 'agentzero' ? { transportKind: mode } : {}),
...(typeof obj.token === 'string' ? { token: obj.token } : {}),
...(typeof obj.password === 'string' ? { password: obj.password } : {}),
mode,
diff --git a/apps/mobile/src/services/agentzero-client.test.ts b/apps/mobile/src/services/agentzero-client.test.ts
new file mode 100644
index 0000000..3ac9ee8
--- /dev/null
+++ b/apps/mobile/src/services/agentzero-client.test.ts
@@ -0,0 +1,374 @@
+import { AgentZeroClient, AgentZeroError, createAgentZeroClient } from './agentzero-client';
+
+type MockFetchCall = { url: string; init: RequestInit };
+
+function buildMockFetch(
+ responder: (req: MockFetchCall) => { status: number; bodyJson?: unknown; bodyText?: string },
+): {
+ fetchImpl: typeof fetch;
+ calls: MockFetchCall[];
+} {
+ const calls: MockFetchCall[] = [];
+ const fetchImpl = (async (url: RequestInfo | URL, init: RequestInit = {}) => {
+ const call: MockFetchCall = { url: String(url), init };
+ calls.push(call);
+ const { status, bodyJson, bodyText } = responder(call);
+ const body = bodyText ?? (bodyJson !== undefined ? JSON.stringify(bodyJson) : '');
+ return {
+ ok: status >= 200 && status < 300,
+ status,
+ text: async () => body,
+ // The client uses text() not json(), but provide json() for completeness.
+ json: async () => (bodyJson !== undefined ? bodyJson : JSON.parse(body)),
+ } as unknown as Response;
+ }) as unknown as typeof fetch;
+ return { fetchImpl, calls };
+}
+
+describe('AgentZeroClient', () => {
+ describe('sendMessage', () => {
+ it('POSTs JSON to /api/api_message with X-API-KEY header and parses the response', async () => {
+ const { fetchImpl, calls } = buildMockFetch(() => ({
+ status: 200,
+ bodyJson: { context_id: 'ctx-1', response: 'pong' },
+ }));
+ const client = new AgentZeroClient({
+ baseUrl: 'http://az.example.com:5000',
+ token: 'tok-123',
+ fetchImpl,
+ });
+
+ const out = await client.sendMessage({ message: 'ping' });
+
+ expect(out).toEqual({ contextId: 'ctx-1', response: 'pong' });
+ expect(calls).toHaveLength(1);
+ expect(calls[0].url).toBe('http://az.example.com:5000/api/api_message');
+ expect(calls[0].init.method).toBe('POST');
+ const headers = calls[0].init.headers as Record;
+ expect(headers['Content-Type']).toBe('application/json');
+ expect(headers['X-API-KEY']).toBe('tok-123');
+ expect(JSON.parse(String(calls[0].init.body))).toEqual({ message: 'ping' });
+ });
+
+ it('strips trailing slashes from the baseUrl so /api/api_message is well-formed', async () => {
+ const { fetchImpl, calls } = buildMockFetch(() => ({
+ status: 200,
+ bodyJson: { context_id: 'c', response: 'r' },
+ }));
+ const client = new AgentZeroClient({
+ baseUrl: 'http://az.example.com:5000///',
+ token: 't',
+ fetchImpl,
+ });
+
+ await client.sendMessage({ message: 'hi' });
+ expect(calls[0].url).toBe('http://az.example.com:5000/api/api_message');
+ });
+
+ it('threads contextId, projectName, agentProfile, lifetimeHours through the body', async () => {
+ const { fetchImpl, calls } = buildMockFetch(() => ({
+ status: 200,
+ bodyJson: { context_id: 'ctx-2', response: 'ok' },
+ }));
+ const client = new AgentZeroClient({
+ baseUrl: 'http://az',
+ token: 't',
+ fetchImpl,
+ });
+
+ await client.sendMessage({
+ message: 'hi',
+ contextId: 'ctx-2',
+ projectName: 'demo',
+ agentProfile: 'catgirl',
+ lifetimeHours: 1,
+ attachments: [{ filename: 'a.txt', base64: 'aGk=' }],
+ });
+
+ expect(JSON.parse(String(calls[0].init.body))).toEqual({
+ message: 'hi',
+ context_id: 'ctx-2',
+ project_name: 'demo',
+ agent_profile: 'catgirl',
+ lifetime_hours: 1,
+ attachments: [{ filename: 'a.txt', base64: 'aGk=' }],
+ });
+ });
+
+ it('omits optional fields entirely when not provided (no nulls)', async () => {
+ const { fetchImpl, calls } = buildMockFetch(() => ({
+ status: 200,
+ bodyJson: { context_id: 'c', response: 'r' },
+ }));
+ const client = new AgentZeroClient({
+ baseUrl: 'http://az',
+ token: 't',
+ fetchImpl,
+ });
+
+ await client.sendMessage({ message: 'hi' });
+ const body = JSON.parse(String(calls[0].init.body));
+ expect(Object.keys(body).sort()).toEqual(['message']);
+ });
+
+ it('throws AgentZeroError with the server-provided error message and HTTP status on 4xx', async () => {
+ const { fetchImpl } = buildMockFetch(() => ({
+ status: 404,
+ bodyJson: { error: 'Context not found' },
+ }));
+ const client = new AgentZeroClient({
+ baseUrl: 'http://az',
+ token: 't',
+ fetchImpl,
+ });
+
+ await expect(client.sendMessage({ message: 'hi', contextId: 'ctx-x' })).rejects.toMatchObject({
+ name: 'AgentZeroError',
+ message: 'Context not found',
+ status: 404,
+ });
+ });
+
+ it('throws AgentZeroError with a synthetic message when the server returns no body', async () => {
+ const { fetchImpl } = buildMockFetch(() => ({ status: 500, bodyText: '' }));
+ const client = new AgentZeroClient({
+ baseUrl: 'http://az',
+ token: 't',
+ fetchImpl,
+ });
+
+ await expect(client.sendMessage({ message: 'hi' })).rejects.toMatchObject({
+ name: 'AgentZeroError',
+ message: 'HTTP 500',
+ status: 500,
+ });
+ });
+
+ it('rejects when the success body is missing context_id or response', async () => {
+ const { fetchImpl } = buildMockFetch(() => ({ status: 200, bodyJson: { context_id: 'x' } }));
+ const client = new AgentZeroClient({
+ baseUrl: 'http://az',
+ token: 't',
+ fetchImpl,
+ });
+
+ await expect(client.sendMessage({ message: 'hi' })).rejects.toThrow(
+ /Invalid Agent Zero response shape/,
+ );
+ });
+
+ it('rejects empty messages without making a request', async () => {
+ const { fetchImpl, calls } = buildMockFetch(() => ({ status: 200, bodyJson: {} }));
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+ await expect(client.sendMessage({ message: ' ' })).rejects.toThrow('message is required');
+ expect(calls).toHaveLength(0);
+ });
+ });
+
+ describe('health', () => {
+ it.each([200, 302])('returns true on HTTP %i', async (status) => {
+ const { fetchImpl } = buildMockFetch(() => ({ status }));
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+ await expect(client.health()).resolves.toBe(true);
+ });
+
+ it('returns false on a non-2xx/302 status', async () => {
+ const { fetchImpl } = buildMockFetch(() => ({ status: 500 }));
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+ await expect(client.health()).resolves.toBe(false);
+ });
+
+ it('returns false when fetch throws (network down, abort)', async () => {
+ const fetchImpl = (async () => {
+ throw new Error('network down');
+ }) as unknown as typeof fetch;
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+ await expect(client.health()).resolves.toBe(false);
+ });
+ });
+
+ describe('createAgentZeroClient factory', () => {
+ it('creates a client bound to the gateway config bridgeUrl', async () => {
+ const { fetchImpl, calls } = buildMockFetch(() => ({
+ status: 200,
+ bodyJson: { context_id: 'c', response: 'r' },
+ }));
+ const client = createAgentZeroClient(
+ { bridgeUrl: 'http://az.example.com:5000', displayName: 'Habitat AZ' },
+ 'tok-via-factory',
+ fetchImpl,
+ );
+ await client.sendMessage({ message: 'hi' });
+ expect(calls[0].url).toBe('http://az.example.com:5000/api/api_message');
+ expect((calls[0].init.headers as Record)['X-API-KEY']).toBe('tok-via-factory');
+ });
+ });
+
+ it('exports a named AgentZeroError class for instanceof checks at call sites', () => {
+ expect(new AgentZeroError('x', 503)).toBeInstanceOf(Error);
+ expect(new AgentZeroError('x', 503).status).toBe(503);
+ expect(new AgentZeroError('x', 503).name).toBe('AgentZeroError');
+ });
+
+ describe('streamMessage', () => {
+ function logItem(no: number, type: string, content: string): Record {
+ return { no, id: `id-${no}`, type, heading: '', content, kvps: null, timestamp: 0, agentno: 0 };
+ }
+
+ it('kicks off via /api/api_message_async, polls /api/api_log_get, emits items, resolves with final response', async () => {
+ // Sequential responder: kickoff first, then 2 polls (still active, then done with response item).
+ let callIdx = 0;
+ const { fetchImpl, calls } = buildMockFetch(() => {
+ callIdx += 1;
+ if (callIdx === 1) {
+ return { status: 200, bodyJson: { message: 'received', context_id: 'ctx-async' } };
+ }
+ if (callIdx === 2) {
+ return {
+ status: 200,
+ bodyJson: {
+ context_id: 'ctx-async',
+ log: {
+ items: [logItem(0, 'user', 'hi'), logItem(1, 'agent', 'thinking...')],
+ progress_active: true,
+ },
+ },
+ };
+ }
+ return {
+ status: 200,
+ bodyJson: {
+ context_id: 'ctx-async',
+ log: {
+ items: [
+ logItem(0, 'user', 'hi'),
+ logItem(1, 'agent', 'thinking...'),
+ logItem(2, 'response', 'done!'),
+ ],
+ progress_active: false,
+ },
+ },
+ };
+ });
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+ const seen: string[] = [];
+ const out = await client.streamMessage(
+ { message: 'go' },
+ (item) => { seen.push(`${item.type}:${item.content}`); },
+ { pollIntervalMs: 1 }, // jest doesn't need real sleeps
+ );
+
+ expect(out).toEqual({ contextId: 'ctx-async', response: 'done!' });
+ // Each item should be emitted exactly once, in order.
+ expect(seen).toEqual(['user:hi', 'agent:thinking...', 'response:done!']);
+ // First call was the async kickoff.
+ expect(calls[0].url).toBe('http://az/api/api_message_async');
+ // Subsequent calls are log polls.
+ expect(calls.slice(1).every((c) => c.url === 'http://az/api/api_log_get')).toBe(true);
+ });
+
+ it('threads contextId through the kickoff body when continuing an existing chat', async () => {
+ let callIdx = 0;
+ const { fetchImpl, calls } = buildMockFetch(() => {
+ callIdx += 1;
+ if (callIdx === 1) {
+ return { status: 200, bodyJson: { message: 'received', context_id: 'ctx-existing' } };
+ }
+ return {
+ status: 200,
+ bodyJson: {
+ context_id: 'ctx-existing',
+ log: { items: [logItem(0, 'response', 'ok')], progress_active: false },
+ },
+ };
+ });
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+
+ await client.streamMessage(
+ { message: 'follow-up', contextId: 'ctx-existing' },
+ () => {},
+ { pollIntervalMs: 1 },
+ );
+
+ const kickoffBody = JSON.parse(String(calls[0].init.body));
+ expect(kickoffBody).toEqual({ message: 'follow-up', context_id: 'ctx-existing' });
+ });
+
+ it('throws AgentZeroError synchronously when the async kickoff fails', async () => {
+ const { fetchImpl } = buildMockFetch(() => ({
+ status: 401,
+ bodyJson: { error: 'Invalid API key' },
+ }));
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 'bad', fetchImpl });
+
+ await expect(
+ client.streamMessage({ message: 'hi' }, () => {}, { pollIntervalMs: 1 }),
+ ).rejects.toMatchObject({ name: 'AgentZeroError', status: 401, message: 'Invalid API key' });
+ });
+
+ it('does not emit the same log item twice across poll ticks', async () => {
+ let callIdx = 0;
+ const item0 = logItem(0, 'agent', 'still working');
+ const item1 = logItem(1, 'response', 'final answer');
+ const { fetchImpl } = buildMockFetch(() => {
+ callIdx += 1;
+ if (callIdx === 1) {
+ return { status: 200, bodyJson: { message: 'received', context_id: 'c' } };
+ }
+ if (callIdx === 2) {
+ return {
+ status: 200,
+ bodyJson: { context_id: 'c', log: { items: [item0], progress_active: true } },
+ };
+ }
+ // Tick 3 also returns item0 (no progress) then tick 4 adds item1 + closes.
+ if (callIdx === 3) {
+ return {
+ status: 200,
+ bodyJson: { context_id: 'c', log: { items: [item0], progress_active: true } },
+ };
+ }
+ return {
+ status: 200,
+ bodyJson: { context_id: 'c', log: { items: [item0, item1], progress_active: false } },
+ };
+ });
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+
+ const seen: number[] = [];
+ const out = await client.streamMessage(
+ { message: 'go' },
+ (it) => { seen.push(it.no); },
+ { pollIntervalMs: 1 },
+ );
+ expect(seen).toEqual([0, 1]);
+ expect(out.response).toBe('final answer');
+ });
+
+ it('rejects with AgentZeroError(408) when no response arrives within maxStreamMs', async () => {
+ let callIdx = 0;
+ const { fetchImpl } = buildMockFetch(() => {
+ callIdx += 1;
+ if (callIdx === 1) {
+ return { status: 200, bodyJson: { message: 'received', context_id: 'c' } };
+ }
+ // Endless work, never a response, never goes idle.
+ return {
+ status: 200,
+ bodyJson: { context_id: 'c', log: { items: [logItem(0, 'agent', 'hmm')], progress_active: true } },
+ };
+ });
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+ await expect(
+ client.streamMessage({ message: 'go' }, () => {}, { pollIntervalMs: 1, maxStreamMs: 5 }),
+ ).rejects.toMatchObject({ name: 'AgentZeroError', status: 408 });
+ });
+
+ it('rejects empty messages without making a request', async () => {
+ const { fetchImpl, calls } = buildMockFetch(() => ({ status: 200, bodyJson: {} }));
+ const client = new AgentZeroClient({ baseUrl: 'http://az', token: 't', fetchImpl });
+ await expect(client.streamMessage({ message: '' }, () => {})).rejects.toThrow('message is required');
+ expect(calls).toHaveLength(0);
+ });
+ });
+});
diff --git a/apps/mobile/src/services/agentzero-client.ts b/apps/mobile/src/services/agentzero-client.ts
new file mode 100644
index 0000000..b0ed5e4
--- /dev/null
+++ b/apps/mobile/src/services/agentzero-client.ts
@@ -0,0 +1,382 @@
+// Agent Zero HTTP client.
+//
+// Three surfaces against AZ's REST API:
+//
+// 1. sendMessage(req) — POST /api/api_message, blocks until the agent finishes.
+// Returns { contextId, response }. Same endpoint a0 CLI + the habitat
+// health-check cron use.
+//
+// 2. streamMessage(req, onLogItem) — kicks off the agent and streams its log
+// items as they happen. Two-phase:
+// a. POST /api/api_message_async — returns the context_id immediately
+// (the agent task runs in the background).
+// b. Poll /api/api_log_get on that context_id every ~400ms, emit each
+// new log item via onLogItem, stop when progress_active=false.
+// Returns the final { contextId, response } where `response` is the
+// last log item of type 'response'.
+//
+// NOTE: api_message_async is a small AZ patch (deployed on habitat as
+// api/api_message_async.py) — a key-authed clone of the framework's own
+// web-authed message_async. Without it the framework only exposes the
+// blocking api_message under X-API-KEY.
+//
+// 3. health() — light liveness check against GET /.
+//
+// Endpoint contracts (from agent-zero/api/api_message{,_async}.py + api_log_get.py):
+// POST {baseUrl}/api/api_message
+// headers: X-API-KEY:
+// Content-Type: application/json
+// body: { message, context_id?, attachments?, project_name?,
+// agent_profile?, lifetime_hours? }
+// ok: { context_id, response }
+//
+// POST {baseUrl}/api/api_message_async
+// same body as api_message
+// ok: { message: "Message received.", context_id } (fire-and-forget)
+//
+// POST {baseUrl}/api/api_log_get
+// body: { context_id, length? }
+// ok: { context_id, log: { items: LogItem[], progress_active, ... } }
+//
+// Error shape (all three): { error: string } with non-2xx status.
+
+import type { AgentZeroGatewayConfig } from '../types';
+
+export interface AgentZeroMessageRequest {
+ /** User-facing message body. Required. */
+ message: string;
+ /** AZ chat context to continue. Omit for a new chat. */
+ contextId?: string;
+ /** Inline attachments (base64-encoded). */
+ attachments?: Array<{ filename: string; base64: string }>;
+ /** Switch active AZ project for this turn (analogous to `/project foo` in a0 CLI). */
+ projectName?: string;
+ /** Override the agent profile (e.g. `'helpful'`, `'catgirl'`). */
+ agentProfile?: string;
+ /** Context lifetime in hours (default 24 in AZ). */
+ lifetimeHours?: number;
+}
+
+export interface AgentZeroMessageResponse {
+ /** AZ chat context id — pass back on the next request to continue the conversation. */
+ contextId: string;
+ /** Final agent reply text. */
+ response: string;
+}
+
+export class AgentZeroError extends Error {
+ readonly status: number;
+ constructor(message: string, status: number) {
+ super(message);
+ this.name = 'AgentZeroError';
+ this.status = status;
+ }
+}
+
+/** Mirrors AZ's LogItem dict shape (helpers/log.py:LogItem.output()). */
+export interface AgentZeroLogItem {
+ /** Monotonic position in the context's log — use as your poll cursor. */
+ no: number;
+ /** AZ log entry id (UUID-ish). May be null for tombstone entries. */
+ id: string | null;
+ /** AZ log type: 'user', 'agent', 'tool', 'error', 'response', etc. */
+ type: string;
+ heading: string;
+ content: string;
+ /** AZ key/value pairs attached to the log (attachments, tool args, etc.). */
+ kvps: Record | null;
+ /** Unix seconds (float). */
+ timestamp: number;
+ /** Multi-agent index (0 = root). */
+ agentno: number;
+}
+
+export interface AgentZeroStreamOptions {
+ /**
+ * How often to poll /api/api_log_get for new items, ms. Default 400.
+ * The AZ web UI polls every 500ms; 400 keeps mobile feeling snappy without
+ * hammering loopback.
+ */
+ pollIntervalMs?: number;
+ /**
+ * Max number of items to ask AZ for per poll (window into the tail of the
+ * log). Default 200 — enough for typical turns, small enough to stay cheap.
+ */
+ pollWindow?: number;
+ /**
+ * Hard cap on how long streamMessage will wait for a final response item,
+ * ms. Default 5 minutes. Hitting this rejects with AgentZeroError(408).
+ */
+ maxStreamMs?: number;
+}
+
+export interface AgentZeroClientOptions {
+ /** Base HTTP URL of the AZ instance, no trailing slash. */
+ baseUrl: string;
+ /** AZ auth token (the `X-API-KEY` header value — same string `create_auth_token()` produces). */
+ token: string;
+ /** Optional fetch impl override for tests. */
+ fetchImpl?: typeof fetch;
+}
+
+export class AgentZeroClient {
+ private readonly baseUrl: string;
+ private readonly token: string;
+ private readonly doFetch: typeof fetch;
+
+ constructor(options: AgentZeroClientOptions) {
+ this.baseUrl = options.baseUrl.replace(/\/+$/, '');
+ this.token = options.token;
+ this.doFetch = options.fetchImpl ?? fetch;
+ }
+
+ /**
+ * Liveness check: GET / returns 200 (or 302 to login).
+ * Anything else (or a network failure) → false.
+ */
+ async health(signal?: AbortSignal): Promise {
+ try {
+ const res = await this.doFetch(`${this.baseUrl}/`, { method: 'GET', signal });
+ return res.status === 200 || res.status === 302;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * Send one user turn. On success the caller MUST store `contextId` and pass
+ * it back on the next call to continue the same AZ chat.
+ *
+ * Throws `AgentZeroError` for non-2xx responses (status is preserved) and
+ * stdlib `Error` for shape problems.
+ */
+ async sendMessage(req: AgentZeroMessageRequest, signal?: AbortSignal): Promise {
+ if (!req.message || !req.message.trim()) {
+ throw new Error('message is required');
+ }
+
+ const body: Record = { message: req.message };
+ if (req.contextId !== undefined) body.context_id = req.contextId;
+ if (req.attachments !== undefined) body.attachments = req.attachments;
+ if (req.projectName !== undefined) body.project_name = req.projectName;
+ if (req.agentProfile !== undefined) body.agent_profile = req.agentProfile;
+ if (req.lifetimeHours !== undefined) body.lifetime_hours = req.lifetimeHours;
+
+ const res = await this.doFetch(`${this.baseUrl}/api/api_message`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-API-KEY': this.token,
+ },
+ body: JSON.stringify(body),
+ signal,
+ });
+
+ const text = await res.text();
+ let data: unknown = null;
+ try {
+ data = text ? JSON.parse(text) : null;
+ } catch {
+ // body wasn't JSON — fall through to status-based error
+ }
+
+ if (!res.ok) {
+ const errMsg = (data as { error?: unknown } | null)?.error;
+ throw new AgentZeroError(
+ typeof errMsg === 'string' && errMsg ? errMsg : `HTTP ${res.status}`,
+ res.status,
+ );
+ }
+
+ if (!data || typeof data !== 'object' || !('context_id' in data) || !('response' in data)) {
+ throw new Error('Invalid Agent Zero response shape — missing context_id or response');
+ }
+
+ return {
+ contextId: String((data as { context_id: unknown }).context_id),
+ response: String((data as { response: unknown }).response),
+ };
+ }
+
+ /**
+ * Start an AZ turn and stream the agent's log items as they happen.
+ *
+ * Flow:
+ * 1. POST /api/api_message_async to kick the agent off and capture its
+ * `context_id` immediately (no waiting for the final answer).
+ * 2. Poll /api/api_log_get every `pollIntervalMs` ms, emit each new log
+ * item via `onLogItem`. Stop when `progress_active=false` AND we
+ * received a `type === 'response'` item.
+ *
+ * Resolves with `{ contextId, response }`. The `response` is the content of
+ * the last `type === 'response'` log item (matches what sendMessage would
+ * have returned synchronously).
+ *
+ * Errors:
+ * - kickoff failure (4xx/5xx on api_message_async) → rejects synchronously
+ * - transient polling errors → swallowed, next tick retries
+ * - timeout watchdog (no response item within `maxStreamMs`, default 5min)
+ * → rejects with AgentZeroError(status 408)
+ * - signal abort → rejects with the abort reason
+ */
+ async streamMessage(
+ req: AgentZeroMessageRequest,
+ onLogItem: (item: AgentZeroLogItem) => void,
+ options: AgentZeroStreamOptions = {},
+ signal?: AbortSignal,
+ ): Promise {
+ if (!req.message || !req.message.trim()) {
+ throw new Error('message is required');
+ }
+ const pollIntervalMs = options.pollIntervalMs ?? 400;
+ const pollWindow = options.pollWindow ?? 200;
+ const maxStreamMs = options.maxStreamMs ?? 5 * 60_000;
+
+ // Phase 1: kickoff
+ const kickoffBody: Record = { message: req.message };
+ if (req.contextId !== undefined) kickoffBody.context_id = req.contextId;
+ if (req.attachments !== undefined) kickoffBody.attachments = req.attachments;
+ if (req.projectName !== undefined) kickoffBody.project_name = req.projectName;
+ if (req.agentProfile !== undefined) kickoffBody.agent_profile = req.agentProfile;
+ if (req.lifetimeHours !== undefined) kickoffBody.lifetime_hours = req.lifetimeHours;
+
+ const kickoffRes = await this.doFetch(`${this.baseUrl}/api/api_message_async`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-API-KEY': this.token,
+ },
+ body: JSON.stringify(kickoffBody),
+ signal,
+ });
+ const kickoffText = await kickoffRes.text();
+ let kickoffData: unknown = null;
+ try {
+ kickoffData = kickoffText ? JSON.parse(kickoffText) : null;
+ } catch {
+ // fall through
+ }
+ if (!kickoffRes.ok) {
+ const errMsg = (kickoffData as { error?: unknown } | null)?.error;
+ throw new AgentZeroError(
+ typeof errMsg === 'string' && errMsg ? errMsg : `HTTP ${kickoffRes.status}`,
+ kickoffRes.status,
+ );
+ }
+ const ctxId = (kickoffData as { context_id?: unknown } | null)?.context_id;
+ if (typeof ctxId !== 'string' || !ctxId) {
+ throw new Error('Invalid Agent Zero kickoff response — missing context_id');
+ }
+
+ // Phase 2: poll until the agent emits a 'response' item AND goes idle.
+ const startedAt = Date.now();
+ let highWater = -1;
+ let finalResponse: string | null = null;
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ if (signal?.aborted) {
+ throw signal.reason ?? new Error('aborted');
+ }
+ if (Date.now() - startedAt > maxStreamMs) {
+ throw new AgentZeroError(
+ `Streaming timeout: no response within ${maxStreamMs}ms`,
+ 408,
+ );
+ }
+
+ let snapshot: { items: AgentZeroLogItem[]; progressActive: boolean };
+ try {
+ snapshot = await this.fetchLog(ctxId, pollWindow, signal);
+ } catch {
+ // transient — back off and retry
+ await sleep(pollIntervalMs, signal);
+ continue;
+ }
+
+ for (const item of snapshot.items) {
+ if (item.no > highWater) {
+ highWater = item.no;
+ onLogItem(item);
+ if (item.type === 'response') {
+ finalResponse = item.content;
+ }
+ }
+ }
+
+ if (!snapshot.progressActive && finalResponse !== null) {
+ return { contextId: ctxId, response: finalResponse };
+ }
+
+ await sleep(pollIntervalMs, signal);
+ }
+ }
+
+ private async fetchLog(
+ contextId: string,
+ length: number,
+ signal?: AbortSignal,
+ ): Promise<{ items: AgentZeroLogItem[]; progressActive: boolean }> {
+ const res = await this.doFetch(`${this.baseUrl}/api/api_log_get`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-API-KEY': this.token,
+ },
+ body: JSON.stringify({ context_id: contextId, length }),
+ signal,
+ });
+ if (!res.ok) {
+ throw new AgentZeroError(`HTTP ${res.status}`, res.status);
+ }
+ const data = await res.json().catch(() => null) as unknown;
+ const log = (data as { log?: unknown } | null)?.log as
+ | { items?: unknown; progress_active?: unknown }
+ | undefined;
+ if (!log || !Array.isArray(log.items)) {
+ throw new Error('Invalid Agent Zero log response shape');
+ }
+ return {
+ items: (log.items as unknown[]).filter(isLogItem),
+ progressActive: Boolean(log.progress_active),
+ };
+ }
+}
+
+function isLogItem(value: unknown): value is AgentZeroLogItem {
+ if (!value || typeof value !== 'object') return false;
+ const v = value as Record;
+ return typeof v.no === 'number'
+ && typeof v.type === 'string'
+ && typeof v.heading === 'string'
+ && typeof v.content === 'string';
+}
+
+/** Promise-based sleep that respects an AbortSignal. */
+function sleep(ms: number, signal?: AbortSignal): Promise {
+ return new Promise((resolve) => {
+ if (signal?.aborted) {
+ resolve();
+ return;
+ }
+ const timer = setTimeout(() => {
+ signal?.removeEventListener('abort', onAbort);
+ resolve();
+ }, ms);
+ const onAbort = (): void => {
+ clearTimeout(timer);
+ signal?.removeEventListener('abort', onAbort);
+ resolve();
+ };
+ signal?.addEventListener('abort', onAbort);
+ });
+}
+
+/** Convenience factory that pulls baseUrl from an AgentZeroGatewayConfig. */
+export function createAgentZeroClient(
+ config: AgentZeroGatewayConfig,
+ token: string,
+ fetchImpl?: typeof fetch,
+): AgentZeroClient {
+ return new AgentZeroClient({ baseUrl: config.bridgeUrl, token, fetchImpl });
+}
diff --git a/apps/mobile/src/services/app-update-announcement.test.ts b/apps/mobile/src/services/app-update-announcement.test.ts
index f365b93..7b8958d 100644
--- a/apps/mobile/src/services/app-update-announcement.test.ts
+++ b/apps/mobile/src/services/app-update-announcement.test.ts
@@ -57,7 +57,7 @@ describe('app update announcement service', () => {
});
it('returns null when the app version is not in the unified release history', () => {
- expect(getCurrentAppUpdateAnnouncement('3.0.0')).toBeNull();
+ expect(getCurrentAppUpdateAnnouncement('99.99.99')).toBeNull();
});
it('returns null when the app version is empty', () => {
@@ -109,7 +109,7 @@ describe('app update announcement service', () => {
await markCurrentAppUpdateAnnouncementShown();
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
- 'clawket.appUpdateAnnouncementSeen.v1:2.1.0',
+ `clawket.appUpdateAnnouncementSeen.v1:${APP_PACKAGE_VERSION}`,
'1',
);
});
diff --git a/apps/mobile/src/services/auto-app-review.test.ts b/apps/mobile/src/services/auto-app-review.test.ts
index b9025ca..6debac1 100644
--- a/apps/mobile/src/services/auto-app-review.test.ts
+++ b/apps/mobile/src/services/auto-app-review.test.ts
@@ -1,12 +1,14 @@
jest.mock('react-native', () => ({
- InteractionManager: {
- runAfterInteractions: (callback: () => void) => callback(),
- },
Platform: {
OS: 'ios',
},
}));
+// requestIdleCallback is not available in Node.js test env
+if (typeof requestIdleCallback === 'undefined') {
+ (globalThis as any).requestIdleCallback = (cb: () => void) => cb();
+}
+
jest.mock('expo-store-review', () => ({
isAvailableAsync: jest.fn(),
requestReview: jest.fn(),
diff --git a/apps/mobile/src/services/auto-app-review.ts b/apps/mobile/src/services/auto-app-review.ts
index 87f304f..b93c50c 100644
--- a/apps/mobile/src/services/auto-app-review.ts
+++ b/apps/mobile/src/services/auto-app-review.ts
@@ -1,4 +1,4 @@
-import { InteractionManager, Platform } from 'react-native';
+import { Platform } from 'react-native';
import * as StoreReview from 'expo-store-review';
import { APP_PACKAGE_VERSION } from '../constants/app-version';
import { AutoAppReviewState, StorageService } from './storage';
@@ -32,7 +32,7 @@ export function shouldAttemptAutomaticReview(params: {
function runAfterInteractions(): Promise {
return new Promise((resolve) => {
- InteractionManager.runAfterInteractions(() => resolve());
+ requestIdleCallback(() => resolve());
});
}
diff --git a/apps/mobile/src/services/gateway-backends.test.ts b/apps/mobile/src/services/gateway-backends.test.ts
index 07dd9cd..443d856 100644
--- a/apps/mobile/src/services/gateway-backends.test.ts
+++ b/apps/mobile/src/services/gateway-backends.test.ts
@@ -37,6 +37,20 @@ describe('gateway-backends', () => {
expect(resolveGatewayBackendKind({ mode: 'local' } as any)).toBe('openclaw');
expect(resolveGatewayBackendKind({ mode: 'custom' } as any)).toBe('openclaw');
});
+
+ it('honors explicit backendKind === agentzero', () => {
+ expect(resolveGatewayBackendKind({ backendKind: 'agentzero' } as any)).toBe('agentzero');
+ });
+
+ it('falls back to legacy mode === agentzero', () => {
+ expect(resolveGatewayBackendKind({ mode: 'agentzero' } as any)).toBe('agentzero');
+ });
+
+ it('falls back to presence of agentzero config block', () => {
+ expect(
+ resolveGatewayBackendKind({ agentzero: { bridgeUrl: 'http://host:5000' } } as any),
+ ).toBe('agentzero');
+ });
});
describe('resolveGatewayTransportKind', () => {
@@ -96,6 +110,11 @@ describe('gateway-backends', () => {
expect(resolveGlobalMainSessionKey('hermes')).toBe('main');
expect(resolveGlobalMainSessionKey({ backendKind: 'hermes' } as any)).toBe('main');
});
+
+ it('returns "main" for agentzero (session-shaped backend)', () => {
+ expect(resolveGlobalMainSessionKey('agentzero')).toBe('main');
+ expect(resolveGlobalMainSessionKey({ backendKind: 'agentzero' } as any)).toBe('main');
+ });
});
describe('getGatewayModeLabel', () => {
@@ -111,6 +130,11 @@ describe('gateway-backends', () => {
expect(getGatewayModeLabel({ backendKind: 'hermes', transportKind: 'relay' } as any)).toBe('Hermes');
expect(getGatewayModeLabel({ backendKind: 'hermes', transportKind: 'local' } as any)).toBe('Hermes');
});
+
+ it('labels Agent Zero independently of transport', () => {
+ expect(getGatewayModeLabel({ backendKind: 'agentzero', transportKind: 'tailscale' } as any)).toBe('Agent Zero');
+ expect(getGatewayModeLabel({ backendKind: 'agentzero', transportKind: 'custom' } as any)).toBe('Agent Zero');
+ });
});
describe('getGatewayBackendCapabilities', () => {
@@ -167,9 +191,11 @@ describe('gateway-backends', () => {
});
describe('type guards', () => {
- it('isGatewayBackendKind accepts only the two known backends', () => {
+ it('isGatewayBackendKind accepts every registered backend', () => {
expect(isGatewayBackendKind('openclaw')).toBe(true);
expect(isGatewayBackendKind('hermes')).toBe(true);
+ expect(isGatewayBackendKind('youmind')).toBe(true);
+ expect(isGatewayBackendKind('agentzero')).toBe(true);
expect(isGatewayBackendKind('other')).toBe(false);
expect(isGatewayBackendKind(undefined)).toBe(false);
});
@@ -199,5 +225,25 @@ describe('gateway-backends', () => {
it('defaults to "custom" when no transport is provided for OpenClaw', () => {
expect(toLegacyGatewayMode({ backendKind: 'openclaw' })).toBe('custom');
});
+
+ it('maps Agent Zero to the legacy "agentzero" mode irrespective of transport', () => {
+ expect(toLegacyGatewayMode({ backendKind: 'agentzero', transportKind: 'tailscale' })).toBe('agentzero');
+ expect(toLegacyGatewayMode({ backendKind: 'agentzero', transportKind: 'custom' })).toBe('agentzero');
+ });
+ });
+
+ describe('Agent Zero capabilities (phase-1 sketch)', () => {
+ it('has the chat capability enabled but advanced console screens off until adapters land', () => {
+ const caps = getGatewayBackendCapabilities('agentzero');
+ expect(caps.consoleRoot).toBe(true);
+ expect(caps.gatewayConnection).toBe(true);
+ expect(caps.chatAbort).toBe(true);
+ expect(caps.chatAttachments).toBe(true);
+ // Phase-1 intentionally off:
+ expect(caps.consoleCron).toBe(false);
+ expect(caps.consoleClawHub).toBe(false);
+ expect(caps.modelSelection).toBe(false);
+ expect(caps.openClawConfigScreens).toBe(false);
+ });
});
});
diff --git a/apps/mobile/src/services/gateway-backends.ts b/apps/mobile/src/services/gateway-backends.ts
index 5abeadc..6835655 100644
--- a/apps/mobile/src/services/gateway-backends.ts
+++ b/apps/mobile/src/services/gateway-backends.ts
@@ -8,7 +8,7 @@ import type {
import { THINKING_LEVELS } from '../utils/gateway-settings';
import type { ThinkingLevel } from '../utils/gateway-settings';
-type GatewayLike = Pick;
+type GatewayLike = Pick;
export type GatewayBackendCapabilities = {
consoleRoot: boolean;
@@ -102,6 +102,42 @@ const HERMES_CAPABILITIES: GatewayBackendCapabilities = {
openClawConfigScreens: false,
};
+// Agent Zero capabilities — phase 1 sketch.
+//
+// AZ is a session-shaped backend (one project at a time, like Hermes' "main"
+// session model). Phase 1 ships only the chat path; console screens that need
+// AZ-specific HTTP wiring stay off until their adapters land. When toggling
+// any flag on, search for `selectByBackend` call sites in the corresponding
+// screen and decide whether the existing `openclaw` fallthrough is acceptable
+// or a dedicated `agentzero` branch is needed.
+const AGENTZERO_CAPABILITIES: GatewayBackendCapabilities = {
+ consoleRoot: true,
+ gatewayConnection: true,
+ chatAbort: true,
+ chatAttachments: true,
+ consoleDiscover: false,
+ consoleClawHub: false,
+ modelCatalog: false,
+ modelSelection: false,
+ configRead: false,
+ configWrite: false,
+ consoleChannels: false,
+ consoleCron: false,
+ consoleCronCreate: false,
+ consoleSkills: false,
+ consoleUsage: false,
+ consoleCost: false,
+ consoleTools: false,
+ consoleNodes: false,
+ consoleFiles: false,
+ consoleLogs: false,
+ consoleAgentList: false,
+ consoleAgentDetail: false,
+ consoleAgentSessionsBoard: false,
+ consoleHeartbeat: false,
+ openClawConfigScreens: false,
+};
+
const YOUMIND_CAPABILITIES: GatewayBackendCapabilities = {
consoleRoot: true,
gatewayConnection: false,
@@ -141,6 +177,11 @@ const BACKENDS: Record = {
label: 'Hermes',
capabilities: HERMES_CAPABILITIES,
},
+ agentzero: {
+ kind: 'agentzero',
+ label: 'Agent Zero',
+ capabilities: AGENTZERO_CAPABILITIES,
+ },
youmind: {
kind: 'youmind',
label: 'YouMind',
@@ -150,6 +191,12 @@ const BACKENDS: Record = {
const HERMES_THINKING_LEVELS: ThinkingLevel[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
+// Agent Zero does not yet expose a thinking-level selector (the framework
+// owns reasoning depth via the model config). Surface a single "off" entry
+// so the UI selector renders consistently without claiming a feature we
+// don't support over the wire.
+const AGENTZERO_THINKING_LEVELS: ThinkingLevel[] = ['off'];
+
export function isGatewayTransportKind(value: unknown): value is GatewayTransportKind {
return value === 'local'
|| value === 'tailscale'
@@ -159,12 +206,13 @@ export function isGatewayTransportKind(value: unknown): value is GatewayTranspor
}
export function isGatewayBackendKind(value: unknown): value is GatewayBackendKind {
- return value === 'openclaw' || value === 'hermes' || value === 'youmind';
+ return value === 'openclaw' || value === 'hermes' || value === 'youmind' || value === 'agentzero';
}
export function resolveGatewayBackendKind(value: GatewayLike | null | undefined): GatewayBackendKind {
if (isGatewayBackendKind(value?.backendKind)) return value.backendKind;
if (value?.mode === 'hermes' || value?.hermes) return 'hermes';
+ if (value?.mode === 'agentzero' || value?.agentzero) return 'agentzero';
return 'openclaw';
}
@@ -180,6 +228,7 @@ export function toLegacyGatewayMode(value: {
transportKind?: GatewayTransportKind;
}): GatewayMode {
if (value.backendKind === 'hermes') return 'hermes';
+ if (value.backendKind === 'agentzero') return 'agentzero';
return value.transportKind ?? 'custom';
}
@@ -198,6 +247,7 @@ export function getGatewayThinkingLevels(
return selectByBackend(input, {
openclaw: [...THINKING_LEVELS],
hermes: [...HERMES_THINKING_LEVELS],
+ agentzero: [...AGENTZERO_THINKING_LEVELS],
});
}
@@ -216,13 +266,17 @@ export function getGatewayThinkingLevels(
*/
export function selectByBackend(
input: GatewayLike | GatewayBackendKind | null | undefined,
- options: { openclaw: T; hermes: T; youmind?: T },
+ options: { openclaw: T; hermes: T; youmind?: T; agentzero?: T },
): T {
const kind = typeof input === 'string' && isGatewayBackendKind(input)
? input
: resolveGatewayBackendKind(input as GatewayLike | null | undefined);
if (kind === 'hermes') return options.hermes;
if (kind === 'youmind') return options.youmind ?? options.openclaw;
+ // Agent Zero falls back to openclaw branches at call sites that haven't
+ // opted in yet — same pattern as youmind. Wire a dedicated `agentzero`
+ // option as each screen learns its AZ behavior.
+ if (kind === 'agentzero') return options.agentzero ?? options.openclaw;
return options.openclaw;
}
@@ -244,6 +298,7 @@ export function resolveGlobalMainSessionKey(
openclaw: null,
hermes: 'main',
youmind: 'main',
+ agentzero: 'main',
});
}
@@ -251,6 +306,7 @@ export function getGatewayModeLabel(input: GatewayLike): string {
const backendKind = resolveGatewayBackendKind(input);
const transportKind = resolveGatewayTransportKind(input);
if (backendKind === 'hermes') return 'Hermes';
+ if (backendKind === 'agentzero') return 'Agent Zero';
if (backendKind === 'youmind') return 'YouMind';
switch (transportKind) {
case 'relay':
@@ -277,11 +333,13 @@ export function buildGatewayDefaultName(input: {
const host = parseHost(input.url);
const baseLabel = backendKind === 'hermes'
? 'Hermes'
- : backendKind === 'youmind'
- ? 'YouMind'
- : transportKind === 'relay'
- ? 'Relay'
- : 'Custom';
+ : backendKind === 'agentzero'
+ ? 'Agent Zero'
+ : backendKind === 'youmind'
+ ? 'YouMind'
+ : transportKind === 'relay'
+ ? 'Relay'
+ : 'Custom';
if (host) return `${baseLabel} (${host})`;
return `${baseLabel} Gateway ${input.index}`;
}
diff --git a/apps/mobile/src/services/storage.ts b/apps/mobile/src/services/storage.ts
index 6099e6b..5710877 100644
--- a/apps/mobile/src/services/storage.ts
+++ b/apps/mobile/src/services/storage.ts
@@ -697,7 +697,22 @@ export const StorageService = {
await this.setGatewayConfigsState(migrated);
return migrated;
}
- return { activeId: null, configs: [] };
+ const _now = Date.now();
+ const _defaultHermes: GatewayConfigsState = {
+ activeId: 'hermes-binarybros-default',
+ configs: [{
+ id: 'hermes-binarybros-default',
+ name: 'Hermes (Binary Bros)',
+ backendKind: 'hermes',
+ transportKind: 'local',
+ mode: 'hermes',
+ url: 'ws://studio.capybara-loggerhead.ts.net:4319/v1/hermes/ws?token=binarybros-hermes-clawket-2026',
+ hermes: { bridgeUrl: 'http://studio.capybara-loggerhead.ts.net:4319', displayName: 'Binary Bros Hermes' },
+ createdAt: _now,
+ updatedAt: _now,
+ }],
+ };
+ return _defaultHermes;
},
async setIdentity(identity: DeviceIdentity): Promise {
diff --git a/apps/mobile/src/types/index.ts b/apps/mobile/src/types/index.ts
index c33be64..5190657 100644
--- a/apps/mobile/src/types/index.ts
+++ b/apps/mobile/src/types/index.ts
@@ -30,9 +30,29 @@ export interface HermesGatewayConfig {
displayName?: string;
}
-export type GatewayBackendKind = 'openclaw' | 'hermes' | 'youmind';
+/**
+ * Agent Zero connection block.
+ *
+ * Agent Zero exposes a Socket.IO transport on the same HTTP host that serves
+ * its web UI (port 5000 by default). Unlike OpenClaw the chat path is not a
+ * bare WebSocket — clients connect via Socket.IO and authenticate with the
+ * `X-API-KEY` header (the value comes from helpers/settings.create_auth_token
+ * on the AZ side, currently surfaced as `token` on this config).
+ *
+ * `projectName` switches the active AZ project (analogous to `/project foo`
+ * in the `a0` CLI). When unset, AZ uses the default project.
+ */
+export interface AgentZeroGatewayConfig {
+ /** Base HTTP URL of the AZ instance (no trailing slash). e.g. https://agent-habitat.capybara-loggerhead.ts.net:8443 */
+ bridgeUrl: string;
+ displayName?: string;
+ /** Optional AZ project to scope the conversation to. */
+ projectName?: string;
+}
+
+export type GatewayBackendKind = 'openclaw' | 'hermes' | 'youmind' | 'agentzero';
export type GatewayTransportKind = 'local' | 'tailscale' | 'cloudflare' | 'custom' | 'relay';
-export type GatewayMode = GatewayTransportKind | 'hermes';
+export type GatewayMode = GatewayTransportKind | 'hermes' | 'agentzero';
export interface GatewayConfig {
url: string;
@@ -44,6 +64,7 @@ export interface GatewayConfig {
mode?: GatewayMode;
relay?: RelayGatewayConfig;
hermes?: HermesGatewayConfig;
+ agentzero?: AgentZeroGatewayConfig;
debugMode?: boolean;
}
@@ -74,6 +95,7 @@ export interface SavedGatewayConfig {
password?: string;
relay?: RelayGatewayConfig;
hermes?: HermesGatewayConfig;
+ agentzero?: AgentZeroGatewayConfig;
createdAt: number;
updatedAt: number;
}
diff --git a/apps/mobile/src/utils/usage-format.ts b/apps/mobile/src/utils/usage-format.ts
index 4476a34..8d9e7f9 100644
--- a/apps/mobile/src/utils/usage-format.ts
+++ b/apps/mobile/src/utils/usage-format.ts
@@ -52,7 +52,9 @@ export function formatDayLabel(dateStr: string): string {
const [, y, m, d] = match;
const date = new Date(Date.UTC(Number(y), Number(m) - 1, Number(d)));
if (Number.isNaN(date.valueOf())) return dateStr;
- return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
+ // Format in UTC to match how the date was constructed. Without timeZone: 'UTC'
+ // a viewer west of UTC sees "Jan 14" for a "2024-01-15" input.
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' });
}
export function pct(part: number, total: number): number {
diff --git a/package-lock.json b/package-lock.json
index 0ae8c5b..87375ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
},
"apps/bridge-cli": {
"name": "@p697/clawket",
- "version": "0.6.3",
+ "version": "0.6.4",
"license": "AGPL-3.0-only",
"dependencies": {
"qrcode": "^1.5.4",
@@ -1544,7 +1544,8 @@
},
"apps/mobile": {
"name": "clawket",
- "version": "2.0.0",
+ "version": "2.1.0",
+ "hasInstallScript": true,
"license": "AGPL-3.0-only",
"dependencies": {
"@bottom-tabs/react-navigation": "^1.1.0",
@@ -1697,20 +1698,6 @@
"node": ">=12.20"
}
},
- "apps/mobile/node_modules/@expo/ui": {
- "version": "55.0.1",
- "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-55.0.1.tgz",
- "integrity": "sha512-j9UFdW2OaM4+dmk5UYzgwtwioreWJlGQUhBxPUH2/5U4ROS2JPyJo0jtwgM6bfawyYQfobKvp0utVoRQJ3Ul1A==",
- "license": "MIT",
- "dependencies": {
- "sf-symbols-typescript": "^2.1.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
"apps/mobile/node_modules/@gorhom/bottom-sheet": {
"version": "5.2.8",
"resolved": "https://registry.npmjs.org/@gorhom/bottom-sheet/-/bottom-sheet-5.2.8.tgz",
@@ -2302,130 +2289,6 @@
"devOptional": true,
"license": "MIT"
},
- "apps/mobile/node_modules/brace-expansion": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "apps/mobile/node_modules/brace-expansion/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "apps/mobile/node_modules/expo-application": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-application/-/expo-application-55.0.8.tgz",
- "integrity": "sha512-PeZk4Zj8LlzRcRtK3J4ouSPBoi9lroYsRbbz/0HEvx+uB6HIaM1qfzgpcctvjkdJJfnidBQNyieW5BVO/qUQ6w==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "apps/mobile/node_modules/expo-battery": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-battery/-/expo-battery-55.0.8.tgz",
- "integrity": "sha512-kPNpYWAvsjQxEcmdHOU1nJGnLua/Ov8EBd2zRxpxs/saNIoOeZXpupVz74/pTrG/9JgTrPm+v1RoLqVRM7fgkg==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*"
- }
- },
- "apps/mobile/node_modules/expo-blur": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-55.0.8.tgz",
- "integrity": "sha512-hmYhQiYFMTpHNzFaCF63jTABgnsozB5+wSEV8rCNBfNEHTdRVNFPkF+TZpbIqJpkhHfGLsKptRmR/wyZmAqREA==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "apps/mobile/node_modules/expo-camera": {
- "version": "55.0.9",
- "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-55.0.9.tgz",
- "integrity": "sha512-ywjmZChGPqyIginN9K1lEJCSTB6M4the37m56tAcvYs3cu0DAUAlTl4n2MY6S7kGdm3iEi0cGmlDpHFyh7rmwg==",
- "license": "MIT",
- "dependencies": {
- "barcode-detector": "^3.0.0"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*",
- "react-native-web": "*"
- },
- "peerDependenciesMeta": {
- "react-native-web": {
- "optional": true
- }
- }
- },
- "apps/mobile/node_modules/expo-clipboard": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-55.0.8.tgz",
- "integrity": "sha512-s0Hkop+dc6m09LwzUAWweNI0gzLAaX5CgEGR8TMdOdSPKTPc2rCl8h8Ji/cUNM1wYoJQ4Wysa15E8If/Vlu7WA==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "apps/mobile/node_modules/expo-device": {
- "version": "55.0.9",
- "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-55.0.9.tgz",
- "integrity": "sha512-BzeuL7lwg2jh/tU+HTJ5dxygB1tpfgThaguPPH86K0ujcj/4RBkC27i/i7nhSoWvL1pQIgUqL0L7WTtjcS9t/w==",
- "license": "MIT",
- "dependencies": {
- "ua-parser-js": "^0.7.33"
- },
- "peerDependencies": {
- "expo": "*"
- }
- },
- "apps/mobile/node_modules/expo-document-picker": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-55.0.8.tgz",
- "integrity": "sha512-p6rYEQ1/h3UqGl3+hzTjv51fsNxoOVfMGSYjHX2/e3cvcy02MWWE+bpj4QEGo9MBwU4RyyIbuv/SCxGtAtG+eA==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "apps/mobile/node_modules/expo-file-system": {
- "version": "55.0.10",
- "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.10.tgz",
- "integrity": "sha512-ysFdVdUgtfj2ApY0Cn+pBg+yK4xp+SNwcaH8j2B91JJQ4OXJmnyCSmrNZYz7J4mdYVuv2GzxIP+N/IGlHQG3Yw==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "apps/mobile/node_modules/expo-haptics": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-55.0.8.tgz",
- "integrity": "sha512-yVR6EsQwl1WuhFITc0PpfI/7dsBdjK/F2YA8xB80UUW9iTa+Tqz21FpH4n/vtbargpzFxkhl5WNYMa419+QWFQ==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
"apps/mobile/node_modules/expo-image-manipulator": {
"version": "55.0.11",
"resolved": "https://registry.npmjs.org/expo-image-manipulator/-/expo-image-manipulator-55.0.11.tgz",
@@ -2438,120 +2301,6 @@
"expo": "*"
}
},
- "apps/mobile/node_modules/expo-image-picker": {
- "version": "55.0.9",
- "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-55.0.9.tgz",
- "integrity": "sha512-e82tLpbg5U2qFdtDQLIcISMNhIr+a4Nt/ZfM+nC6X7qaGjzeYAuzvcb3xUe80Ix1H3D64b/tiA6a7KjQ48cfFA==",
- "license": "MIT",
- "dependencies": {
- "expo-image-loader": "~55.0.0"
- },
- "peerDependencies": {
- "expo": "*"
- }
- },
- "apps/mobile/node_modules/expo-linking": {
- "version": "55.0.7",
- "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-55.0.7.tgz",
- "integrity": "sha512-MiGCedere1vzQTEi2aGrkzd7eh/rPSz4w6F3GMBuAJzYl+/0VhIuyhozpEGrueyDIXWfzaUVOcn3SfxVi+kwQQ==",
- "license": "MIT",
- "dependencies": {
- "expo-constants": "~55.0.7",
- "invariant": "^2.2.4"
- },
- "peerDependencies": {
- "react": "*",
- "react-native": "*"
- }
- },
- "apps/mobile/node_modules/expo-localization": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-55.0.8.tgz",
- "integrity": "sha512-uFmpTsoDT7JE5Nwgt0EQ5gBvFVo7/u458SlY6V9Ep9wY/WPucL0o00VpXoFULaMtKHquKBgVUdHwk6E+JFz4dg==",
- "license": "MIT",
- "dependencies": {
- "rtl-detect": "^1.0.2"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*"
- }
- },
- "apps/mobile/node_modules/expo-location": {
- "version": "55.1.2",
- "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-55.1.2.tgz",
- "integrity": "sha512-QT/9Hh0mjBnsJD1A6VYVKskFK9PODTpHVMA3DmGDTbvKS55yhY/fj6f28dn3zAYTaLAWIZa5j3/C+LCJSq7kwA==",
- "license": "MIT",
- "dependencies": {
- "@expo/image-utils": "^0.8.12"
- },
- "peerDependencies": {
- "expo": "*"
- }
- },
- "apps/mobile/node_modules/expo-media-library": {
- "version": "55.0.9",
- "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-55.0.9.tgz",
- "integrity": "sha512-E12e4gjQEZNdAa7MHDLOAiOMQmhmOGHFMMU5DpiK6I01hPXyRcaxgAQQLpibIXNP4O5LjGi2psa3NBacjyjxkw==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
- "apps/mobile/node_modules/expo-network": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-network/-/expo-network-55.0.8.tgz",
- "integrity": "sha512-IXxwFq5B2OImc6BvHHGN9QOCYfMk3wPnbBL+NiyBFqy+g2LsdnGzWLA2HXD8+1Ngdvi7PwckQO+q6WKHLRx4Vw==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react": "*"
- }
- },
- "apps/mobile/node_modules/expo-notifications": {
- "version": "55.0.10",
- "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-55.0.10.tgz",
- "integrity": "sha512-F+ozrVFthKCwfqz2cXmcqrqwzBMTAwoNBqTZERuFtgc+6I++mweVzLLTtbAy8kBDZ33MA13GKyeq7mw13/rDgw==",
- "license": "MIT",
- "dependencies": {
- "@expo/image-utils": "^0.8.12",
- "abort-controller": "^3.0.0",
- "badgin": "^1.1.5",
- "expo-application": "~55.0.8",
- "expo-constants": "~55.0.7"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
- "apps/mobile/node_modules/expo-secure-store": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-55.0.8.tgz",
- "integrity": "sha512-8w9tQe8U6oRo5YIzqCqVhRrOnfoODNDoitBtLXEx+zS6WLUnkRq5kH7ViJuOgiM7PzLr9pvAliRiDOKyvFbTuQ==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*"
- }
- },
- "apps/mobile/node_modules/expo-sharing": {
- "version": "55.0.11",
- "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-55.0.11.tgz",
- "integrity": "sha512-YlVez832W0sYR2KJY4Dr8ON9aC+Wp8a/r40eQyhoHT9Tetkr2KBM7tWLT0CGKRuTTnrqJL1C51UacLkHJ9zmNA==",
- "license": "MIT",
- "dependencies": {
- "@expo/config-plugins": "^55.0.6",
- "@expo/config-types": "^55.0.5",
- "@expo/plist": "^0.5.2"
- },
- "peerDependencies": {
- "expo": "*",
- "react": "*",
- "react-native": "*"
- }
- },
"apps/mobile/node_modules/expo-status-bar": {
"version": "55.0.4",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.4.tgz",
@@ -2575,16 +2324,6 @@
"react-native": "*"
}
},
- "apps/mobile/node_modules/expo-store-review": {
- "version": "55.0.8",
- "resolved": "https://registry.npmjs.org/expo-store-review/-/expo-store-review-55.0.8.tgz",
- "integrity": "sha512-DxDD5F8jEAFV2daotIQtLRO5KTtKE/cEgaTC5fHvMDLn0BWfXRrPKsYoMma4vvcLdx4zqf905OTK2tT4w7L/zw==",
- "license": "MIT",
- "peerDependencies": {
- "expo": "*",
- "react-native": "*"
- }
- },
"apps/mobile/node_modules/i18next": {
"version": "25.10.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.3.tgz",
@@ -2643,309 +2382,49 @@
}
}
},
- "apps/mobile/node_modules/jest-expo": {
- "version": "55.0.9",
- "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-55.0.9.tgz",
- "integrity": "sha512-6wz7JJUeW2e0+APRQP7eOcXKPdI7bdmAIoBiPJbtzSBRlghho8LzPcv4jkoVFoYi8SKb9k3BTKx4GcUlyVMedw==",
+ "apps/mobile/node_modules/jest/node_modules/@jest/core": {
+ "version": "30.3.0",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz",
+ "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@expo/config": "~55.0.8",
- "@expo/json-file": "^10.0.12",
- "@jest/create-cache-key-function": "^29.2.1",
- "@jest/globals": "^29.2.1",
- "babel-jest": "^29.2.1",
- "jest-environment-jsdom": "^29.2.1",
- "jest-snapshot": "^29.2.1",
- "jest-watch-select-projects": "^2.0.0",
- "jest-watch-typeahead": "2.2.1",
- "json5": "^2.2.3",
- "lodash": "^4.17.19",
- "react-test-renderer": "19.2.0",
- "server-only": "^0.0.1",
- "stacktrace-js": "^2.0.2"
+ "@jest/console": "30.3.0",
+ "@jest/pattern": "30.0.1",
+ "@jest/reporters": "30.3.0",
+ "@jest/test-result": "30.3.0",
+ "@jest/transform": "30.3.0",
+ "@jest/types": "30.3.0",
+ "@types/node": "*",
+ "ansi-escapes": "^4.3.2",
+ "chalk": "^4.1.2",
+ "ci-info": "^4.2.0",
+ "exit-x": "^0.2.2",
+ "graceful-fs": "^4.2.11",
+ "jest-changed-files": "30.3.0",
+ "jest-config": "30.3.0",
+ "jest-haste-map": "30.3.0",
+ "jest-message-util": "30.3.0",
+ "jest-regex-util": "30.0.1",
+ "jest-resolve": "30.3.0",
+ "jest-resolve-dependencies": "30.3.0",
+ "jest-runner": "30.3.0",
+ "jest-runtime": "30.3.0",
+ "jest-snapshot": "30.3.0",
+ "jest-util": "30.3.0",
+ "jest-validate": "30.3.0",
+ "jest-watcher": "30.3.0",
+ "pretty-format": "30.3.0",
+ "slash": "^3.0.0"
},
- "bin": {
- "jest": "bin/jest.js"
+ "engines": {
+ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
},
"peerDependencies": {
- "expo": "*",
- "react-native": "*",
- "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4"
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
},
"peerDependenciesMeta": {
- "react-server-dom-webpack": {
- "optional": true
- }
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@expo/config": {
- "version": "55.0.10",
- "resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.10.tgz",
- "integrity": "sha512-qCHxo9H1ZoeW+y0QeMtVZ3JfGmumpGrgUFX60wLWMarraoQZSe47ZUm9kJSn3iyoPjUtUNanO3eXQg+K8k4rag==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@expo/config-plugins": "~55.0.7",
- "@expo/config-types": "^55.0.5",
- "@expo/json-file": "^10.0.12",
- "@expo/require-utils": "^55.0.3",
- "deepmerge": "^4.3.1",
- "getenv": "^2.0.0",
- "glob": "^13.0.0",
- "resolve-from": "^5.0.0",
- "resolve-workspace-root": "^2.0.0",
- "semver": "^7.6.0",
- "slugify": "^1.3.4"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@expo/config/node_modules/glob": {
- "version": "13.0.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
- "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "minimatch": "^10.2.2",
- "minipass": "^7.1.3",
- "path-scurry": "^2.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@jest/globals": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
- "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/environment": "^29.7.0",
- "@jest/expect": "^29.7.0",
- "@jest/types": "^29.6.3",
- "jest-mock": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@jest/globals/node_modules/@jest/expect": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
- "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "expect": "^29.7.0",
- "jest-snapshot": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@jest/globals/node_modules/@jest/expect/node_modules/expect": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
- "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/expect-utils": "^29.7.0",
- "jest-get-type": "^29.6.3",
- "jest-matcher-utils": "^29.7.0",
- "jest-message-util": "^29.7.0",
- "jest-util": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@jest/globals/node_modules/@jest/expect/node_modules/expect/node_modules/@jest/expect-utils": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
- "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "jest-get-type": "^29.6.3"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@jest/globals/node_modules/@jest/expect/node_modules/expect/node_modules/jest-matcher-utils": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
- "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.0.0",
- "jest-diff": "^29.7.0",
- "jest-get-type": "^29.6.3",
- "pretty-format": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/@jest/globals/node_modules/@jest/expect/node_modules/expect/node_modules/jest-matcher-utils/node_modules/jest-diff": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
- "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.0.0",
- "diff-sequences": "^29.6.3",
- "jest-get-type": "^29.6.3",
- "pretty-format": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/jest-snapshot": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
- "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.11.6",
- "@babel/generator": "^7.7.2",
- "@babel/plugin-syntax-jsx": "^7.7.2",
- "@babel/plugin-syntax-typescript": "^7.7.2",
- "@babel/types": "^7.3.3",
- "@jest/expect-utils": "^29.7.0",
- "@jest/transform": "^29.7.0",
- "@jest/types": "^29.6.3",
- "babel-preset-current-node-syntax": "^1.0.0",
- "chalk": "^4.0.0",
- "expect": "^29.7.0",
- "graceful-fs": "^4.2.9",
- "jest-diff": "^29.7.0",
- "jest-get-type": "^29.6.3",
- "jest-matcher-utils": "^29.7.0",
- "jest-message-util": "^29.7.0",
- "jest-util": "^29.7.0",
- "natural-compare": "^1.4.0",
- "pretty-format": "^29.7.0",
- "semver": "^7.5.3"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/jest-snapshot/node_modules/@jest/expect-utils": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
- "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "jest-get-type": "^29.6.3"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/jest-snapshot/node_modules/expect": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
- "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/expect-utils": "^29.7.0",
- "jest-get-type": "^29.6.3",
- "jest-matcher-utils": "^29.7.0",
- "jest-message-util": "^29.7.0",
- "jest-util": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/jest-snapshot/node_modules/jest-diff": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
- "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.0.0",
- "diff-sequences": "^29.6.3",
- "jest-get-type": "^29.6.3",
- "pretty-format": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest-expo/node_modules/jest-snapshot/node_modules/jest-matcher-utils": {
- "version": "29.7.0",
- "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
- "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "chalk": "^4.0.0",
- "jest-diff": "^29.7.0",
- "jest-get-type": "^29.6.3",
- "pretty-format": "^29.7.0"
- },
- "engines": {
- "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
- }
- },
- "apps/mobile/node_modules/jest/node_modules/@jest/core": {
- "version": "30.3.0",
- "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz",
- "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jest/console": "30.3.0",
- "@jest/pattern": "30.0.1",
- "@jest/reporters": "30.3.0",
- "@jest/test-result": "30.3.0",
- "@jest/transform": "30.3.0",
- "@jest/types": "30.3.0",
- "@types/node": "*",
- "ansi-escapes": "^4.3.2",
- "chalk": "^4.1.2",
- "ci-info": "^4.2.0",
- "exit-x": "^0.2.2",
- "graceful-fs": "^4.2.11",
- "jest-changed-files": "30.3.0",
- "jest-config": "30.3.0",
- "jest-haste-map": "30.3.0",
- "jest-message-util": "30.3.0",
- "jest-regex-util": "30.0.1",
- "jest-resolve": "30.3.0",
- "jest-resolve-dependencies": "30.3.0",
- "jest-runner": "30.3.0",
- "jest-runtime": "30.3.0",
- "jest-snapshot": "30.3.0",
- "jest-util": "30.3.0",
- "jest-validate": "30.3.0",
- "jest-watcher": "30.3.0",
- "pretty-format": "30.3.0",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
- },
- "peerDependencies": {
- "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
- },
- "peerDependenciesMeta": {
- "node-notifier": {
+ "node-notifier": {
"optional": true
}
}
@@ -8266,22 +7745,6 @@
"react-native-svg": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0"
}
},
- "apps/mobile/node_modules/minimatch": {
- "version": "10.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
- "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"apps/mobile/node_modules/posthog-react-native": {
"version": "4.37.5",
"resolved": "https://registry.npmjs.org/posthog-react-native/-/posthog-react-native-4.37.5.tgz",
@@ -9478,13 +8941,6 @@
"@cloudflare/workers-types": "^4.20260220.0"
}
},
- "apps/relay-registry/node_modules/@cloudflare/workers-types": {
- "version": "4.20260317.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz",
- "integrity": "sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==",
- "dev": true,
- "license": "MIT OR Apache-2.0"
- },
"apps/relay-worker": {
"name": "@clawket/relay-worker",
"version": "0.1.0",
@@ -9496,13 +8952,6 @@
"@cloudflare/workers-types": "^4.20260220.0"
}
},
- "apps/relay-worker/node_modules/@cloudflare/workers-types": {
- "version": "4.20260317.1",
- "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz",
- "integrity": "sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==",
- "dev": true,
- "license": "MIT OR Apache-2.0"
- },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -11883,6 +11332,20 @@
"integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==",
"license": "MIT"
},
+ "node_modules/@expo/ui": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/@expo/ui/-/ui-55.0.13.tgz",
+ "integrity": "sha512-B4qNSksgHEOx/q+zwz6NNomos/w7ZptjewjfMpGVc09CUBYWBGHLYP8i5Tc4HgqLN3p4BJalReRv+X39OefJaw==",
+ "license": "MIT",
+ "dependencies": {
+ "sf-symbols-typescript": "^2.1.0"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@expo/vector-icons": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz",
@@ -15027,27 +14490,64 @@
"react-native": "*"
}
},
- "node_modules/expo-auth-session/node_modules/expo-constants": {
+ "node_modules/expo-battery": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/expo-battery/-/expo-battery-55.0.13.tgz",
+ "integrity": "sha512-yt2BZCs76aeM9UPbIf8pAGvZJ6AQ7UQ8qjiQcciU3o3q512ZTgsIWObfOqZlTy/abzU2njHW6OikWbtQbeyeZg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*"
+ }
+ },
+ "node_modules/expo-blur": {
"version": "55.0.14",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.14.tgz",
- "integrity": "sha512-l23QVQCYBPKT5zbxxZdJeuhiunadvWdjcQ9+GC8h+02jCoLmWRk20064nCINnQTP3Hf+uLPteUiwYrJd0e446w==",
+ "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-55.0.14.tgz",
+ "integrity": "sha512-NKyCKFWTNpX4CZSsiE1sgkqk/yvR1K0UTukuIbxVKoobB+yALLg1CFav0NqfdQqjhtoj5oEzP0Brlq92Z08Zfg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-camera": {
+ "version": "55.0.16",
+ "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-55.0.16.tgz",
+ "integrity": "sha512-9c6FGrLVwMLyQ08wqwkH7DXAC8Oj1VD0LXM4hFu62Nuq2f2zIAZwsXxG7J5ex+HHHAGyligGGi6VJWuiib9qNg==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~55.0.15",
- "@expo/env": "~2.1.1"
+ "barcode-detector": "^3.0.0"
},
"peerDependencies": {
"expo": "*",
+ "react": "*",
+ "react-native": "*",
+ "react-native-web": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-web": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/expo-clipboard": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-55.0.13.tgz",
+ "integrity": "sha512-PrOmmuVsGW4bAkNQmGKtxMXj3invsfN+jfIKmQxHwE/dn7ODqwFWviUTa+PMUjP3XZmYCDLyu/i0GLeu7HF9Ew==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
"react-native": "*"
}
},
"node_modules/expo-constants": {
- "version": "55.0.9",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.9.tgz",
- "integrity": "sha512-iBiXjZeuU5S/8docQeNzsVvtDy4w0zlmXBpFEi1ypwugceEpdQQab65TVRbusXAcwpNVxCPMpNlDssYp0Pli2g==",
+ "version": "55.0.15",
+ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.15.tgz",
+ "integrity": "sha512-w394fcZLJjeKN+9ZnJzL/HiarE1nwZFDa+3S9frevh6Ur+MAAs9QDrcXhDrV8T3xqRzzYaqsP6Z8TFZ4efWN1A==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~55.0.10",
"@expo/env": "~2.1.1"
},
"peerDependencies": {
@@ -15064,6 +14564,27 @@
"expo": "*"
}
},
+ "node_modules/expo-device": {
+ "version": "55.0.15",
+ "resolved": "https://registry.npmjs.org/expo-device/-/expo-device-55.0.15.tgz",
+ "integrity": "sha512-vXy4U/IeYI+zHGG45Ap6J7EuyQmkstyo8I+/5YGr5q2zmqLBo6SWE62wii8i9hLHheHn6AtF9UPrSWAREJrE8A==",
+ "license": "MIT",
+ "dependencies": {
+ "ua-parser-js": "^0.7.33"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-document-picker": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-55.0.13.tgz",
+ "integrity": "sha512-IhswJElhdzs3fKDEKW8KXYRoFkWGEsXRMYAZT46Yo56zqqy8yQXrczo33RSwD2hFzNQBdLT97SJL9N311UyS3g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-file-system": {
"version": "55.0.12",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-55.0.12.tgz",
@@ -15088,6 +14609,15 @@
"react-native": "*"
}
},
+ "node_modules/expo-haptics": {
+ "version": "55.0.14",
+ "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-55.0.14.tgz",
+ "integrity": "sha512-KjDItBsA9mi1f5nRwf8g1wOdfEcLHwvEdt5Jl1sMCDETR/homcGOl+F3QIiPOl/PRlbGVieQsjTtF4DGtHOj6g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-image-loader": {
"version": "55.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-55.0.0.tgz",
@@ -15097,6 +14627,18 @@
"expo": "*"
}
},
+ "node_modules/expo-image-picker": {
+ "version": "55.0.19",
+ "resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-55.0.19.tgz",
+ "integrity": "sha512-PqOOfRz7+hbB9IFN0LfNxpJJwuPlUG0Abr0qM3Wc61OJ7FFyuKJ50QJ/fFItzSuoXifET1YIFBiXx5nA8Gkinw==",
+ "license": "MIT",
+ "dependencies": {
+ "expo-image-loader": "~55.0.0"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-keep-awake": {
"version": "55.0.4",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-55.0.4.tgz",
@@ -15121,15 +14663,36 @@
"react-native": "*"
}
},
- "node_modules/expo-linking/node_modules/expo-constants": {
- "version": "55.0.14",
- "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.14.tgz",
- "integrity": "sha512-l23QVQCYBPKT5zbxxZdJeuhiunadvWdjcQ9+GC8h+02jCoLmWRk20064nCINnQTP3Hf+uLPteUiwYrJd0e446w==",
+ "node_modules/expo-localization": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-55.0.13.tgz",
+ "integrity": "sha512-fXiEUUihIrXmAEzoneaTOFcQ7TKmr25RR/ymrB/MvYTVnmevFA1zY2KI0VSiXY+NKKjZ8mG65YSn1wh4gEYKxA==",
"license": "MIT",
"dependencies": {
- "@expo/config": "~55.0.15",
- "@expo/env": "~2.1.1"
+ "rtl-detect": "^1.0.2"
},
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*"
+ }
+ },
+ "node_modules/expo-location": {
+ "version": "55.1.8",
+ "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-55.1.8.tgz",
+ "integrity": "sha512-mEExFf84nmWLwi14GFfUsFLrCm10gbcqFn9EPXpuruQ28YMtJWgCD+jJtESYPQkYF44N21fVok3T28fLuCqydA==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/image-utils": "^0.8.13"
+ },
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
+ "node_modules/expo-media-library": {
+ "version": "55.0.15",
+ "resolved": "https://registry.npmjs.org/expo-media-library/-/expo-media-library-55.0.15.tgz",
+ "integrity": "sha512-VD2Lppq0JR6TeI4ivVGsbz4UJ+hKD0DSB/P+I0eoOpoOcLc3/5WboDKgy2uW8YYZqee55V+JU7TMdRDi+lTnCg==",
+ "license": "MIT",
"peerDependencies": {
"expo": "*",
"react-native": "*"
@@ -15172,6 +14735,43 @@
"react-native": "*"
}
},
+ "node_modules/expo-network": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/expo-network/-/expo-network-55.0.13.tgz",
+ "integrity": "sha512-7u+npCmCPRpVrjkUlQtUetPnTN1gRyj7z13bBM5w9w1AHMb4PfoxtIys5EB9ukzNYBg/gaZ/y5dtxomGpc6BKw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*"
+ }
+ },
+ "node_modules/expo-notifications": {
+ "version": "55.0.22",
+ "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-55.0.22.tgz",
+ "integrity": "sha512-Rwvsp/lAEXfDYBxkQZpaLF9ZB25cJ/yfHhD/ESclbPesN0nbQBZ/5rGb1xS/saANtkStbEGfDlA80uHh2zEpsA==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/image-utils": "^0.8.13",
+ "abort-controller": "^3.0.0",
+ "badgin": "^1.1.5",
+ "expo-application": "~55.0.14",
+ "expo-constants": "~55.0.15"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-secure-store": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-55.0.13.tgz",
+ "integrity": "sha512-I6r0JNO1Fd4o0Gu7Ixiic7s89lqgdUHq17uBH9y1f/AntoyKn71TdtYJH82RgfsBbu5qNVzrwImmvlANyOlITQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-server": {
"version": "55.0.6",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-55.0.6.tgz",
@@ -15181,6 +14781,32 @@
"node": ">=20.16.0"
}
},
+ "node_modules/expo-sharing": {
+ "version": "55.0.18",
+ "resolved": "https://registry.npmjs.org/expo-sharing/-/expo-sharing-55.0.18.tgz",
+ "integrity": "sha512-Tqy4LXRLw/UEg5mT7BKhx8y4ReNz8fVldvhHJV5cesH3kRgEerHkYxVwid2vd7v34KnNp0RH1OqUyDlzZTQ9AQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@expo/config-plugins": "^55.0.8",
+ "@expo/config-types": "^55.0.5",
+ "@expo/plist": "^0.5.2"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
+ "node_modules/expo-store-review": {
+ "version": "55.0.13",
+ "resolved": "https://registry.npmjs.org/expo-store-review/-/expo-store-review-55.0.13.tgz",
+ "integrity": "sha512-3cIfDUOBArAeuDQEiYToTZdB1UGSHSe4NhLunEf7hYG86ICgSYIXcosisYnsMLTE6GxL/0XJ34sQOfrP7HfASA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-web-browser": {
"version": "55.0.14",
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-55.0.14.tgz",
@@ -16748,6 +16374,166 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/jest-expo": {
+ "version": "55.0.16",
+ "resolved": "https://registry.npmjs.org/jest-expo/-/jest-expo-55.0.16.tgz",
+ "integrity": "sha512-bOvrTNyDaiaoTz9GhvnXib9v9rjX9PTJFvvoqRMRKEg4MoHghG82E7YF+pH71EWSXTaibQ07F46GS+fcUxTWEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@expo/config": "~55.0.15",
+ "@expo/json-file": "^10.0.13",
+ "@jest/create-cache-key-function": "^29.2.1",
+ "@jest/globals": "^29.2.1",
+ "babel-jest": "^29.2.1",
+ "jest-environment-jsdom": "^29.2.1",
+ "jest-snapshot": "^29.2.1",
+ "jest-watch-select-projects": "^2.0.0",
+ "jest-watch-typeahead": "2.2.1",
+ "json5": "^2.2.3",
+ "lodash": "^4.17.19",
+ "react-test-renderer": "19.2.0",
+ "server-only": "^0.0.1",
+ "stacktrace-js": "^2.0.2"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "peerDependencies": {
+ "expo": "*",
+ "react-native": "*",
+ "react-server-dom-webpack": "~19.0.4 || ~19.1.5 || ~19.2.4"
+ },
+ "peerDependenciesMeta": {
+ "react-server-dom-webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-expo/node_modules/@jest/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "expect": "^29.7.0",
+ "jest-snapshot": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/@jest/expect-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
+ "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "jest-get-type": "^29.6.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/@jest/globals": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz",
+ "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/environment": "^29.7.0",
+ "@jest/expect": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "jest-mock": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/expect": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
+ "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jest/expect-utils": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/jest-diff": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
+ "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^29.6.3",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/jest-matcher-utils": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
+ "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "pretty-format": "^29.7.0"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
+ "node_modules/jest-expo/node_modules/jest-snapshot": {
+ "version": "29.7.0",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz",
+ "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-jsx": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^29.7.0",
+ "@jest/transform": "^29.7.0",
+ "@jest/types": "^29.6.3",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^29.7.0",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^29.7.0",
+ "jest-get-type": "^29.6.3",
+ "jest-matcher-utils": "^29.7.0",
+ "jest-message-util": "^29.7.0",
+ "jest-util": "^29.7.0",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^29.7.0",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
+ }
+ },
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",