conductor-remote 1.99.0 → 1.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/bin/cli.js +1 -0
- package/dist/assets/index-AS43kfu8.css +1 -0
- package/dist/assets/index-DH64usTv.js +52 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +1 -1
- package/dist-node/scripts/service.js +202 -9
- package/dist-node/src/logbuf.js +4 -1
- package/dist-node/src/notify.js +4 -11
- package/dist-node/src/reads.js +29 -1
- package/dist-node/src/routes.js +8 -0
- package/dist-node/src/run-activity.js +103 -0
- package/dist-node/src/server.js +160 -1
- package/dist-node/src/shared.js +26 -0
- package/dist-node/src/speech.js +68 -0
- package/dist-node/src/voice/brief.js +260 -0
- package/dist-node/src/voice/broker.js +447 -0
- package/dist-node/src/voice/config.js +171 -0
- package/dist-node/src/voice/funnel.js +75 -0
- package/dist-node/src/voice/gateway.js +86 -0
- package/dist-node/src/voice/preview.js +67 -0
- package/dist-node/src/voice/prompt.js +12 -0
- package/dist-node/src/voice/server.js +137 -0
- package/dist-node/src/voice/ticket.js +23 -0
- package/dist-node/src/voice/tools.js +198 -0
- package/dist-node/src/voice/twiml.js +124 -0
- package/dist-node/src/voice/webhook.js +113 -0
- package/dist-node/src/voice/webrtc.js +107 -0
- package/docs/voice-setup.md +223 -0
- package/package.json +3 -2
- package/dist/assets/index-0UqQA-X1.css +0 -1
- package/dist/assets/index-CatXxUrf.js +0 -52
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenAI side of the caller gate: verify `realtime.call.incoming` before anything answers it.
|
|
3
|
+
*
|
|
4
|
+
* Three independent checks, because each one fails differently. The **Standard Webhooks
|
|
5
|
+
* signature** proves OpenAI sent it. The **replay guard** stops the same signed delivery being
|
|
6
|
+
* re-used, which matters because OpenAI retries for 72 hours and a retry is indistinguishable
|
|
7
|
+
* from a capture. The **trunk marker** proves the call came through our Twilio number rather than
|
|
8
|
+
* from someone who guessed the project id and dialled the SIP address directly — probe 0b
|
|
9
|
+
* (2026-09-02) confirmed a custom SIP header survives into `sip_headers`, so the marker is a real
|
|
10
|
+
* gate rather than the layered fallback the design was prepared to settle for.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is pure apart from the guard's clock, so the vectors in the tests are real
|
|
13
|
+
* bytes rather than mocks.
|
|
14
|
+
*/
|
|
15
|
+
import crypto from 'node:crypto';
|
|
16
|
+
/** OpenAI retries for 72 hours; five minutes is the Standard Webhooks tolerance. */
|
|
17
|
+
export const SIGNATURE_TOLERANCE_SECONDS = 300;
|
|
18
|
+
/** How long a delivered `webhook-id` is remembered. Comfortably past the tolerance above. */
|
|
19
|
+
export const REPLAY_MEMORY_MS = 15 * 60 * 1000;
|
|
20
|
+
const bad = (reason) => ({ ok: false, reason });
|
|
21
|
+
/** `whsec_<base64>` is the dashboard's spelling; the bytes after the prefix are the key. */
|
|
22
|
+
export function webhookKey(secret) {
|
|
23
|
+
const raw = secret.startsWith('whsec_') ? secret.slice('whsec_'.length) : secret;
|
|
24
|
+
return Buffer.from(raw, 'base64');
|
|
25
|
+
}
|
|
26
|
+
function sameSignature(a, b) {
|
|
27
|
+
const left = Buffer.from(a);
|
|
28
|
+
const right = Buffer.from(b);
|
|
29
|
+
return left.length === right.length && crypto.timingSafeEqual(left, right);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Standard Webhooks: the signed content is `id.timestamp.body`, the signature is base64 HMAC-SHA256,
|
|
33
|
+
* and the header carries a space-separated list of `v1,<sig>` so a secret can be rotated without a
|
|
34
|
+
* gap. Any one match is enough, and each is compared in constant time.
|
|
35
|
+
*/
|
|
36
|
+
export function verifyWebhookSignature(body, headers, secret, nowMs = Date.now()) {
|
|
37
|
+
const header = (name) => {
|
|
38
|
+
const raw = headers[name] ?? headers[name.toLowerCase()];
|
|
39
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
40
|
+
return typeof value === 'string' && value ? value : null;
|
|
41
|
+
};
|
|
42
|
+
const id = header('webhook-id');
|
|
43
|
+
const timestamp = header('webhook-timestamp');
|
|
44
|
+
const signature = header('webhook-signature');
|
|
45
|
+
if (!id || !timestamp || !signature)
|
|
46
|
+
return bad('missing webhook-id, webhook-timestamp or webhook-signature');
|
|
47
|
+
const sent = Number(timestamp);
|
|
48
|
+
if (!Number.isFinite(sent))
|
|
49
|
+
return bad('webhook-timestamp is not a number');
|
|
50
|
+
const skew = Math.abs(nowMs / 1000 - sent);
|
|
51
|
+
if (skew > SIGNATURE_TOLERANCE_SECONDS)
|
|
52
|
+
return bad(`webhook-timestamp is ${Math.round(skew)}s away from now`);
|
|
53
|
+
const expected = crypto.createHmac('sha256', webhookKey(secret)).update(`${id}.${timestamp}.${body}`).digest('base64');
|
|
54
|
+
// The header may list several versioned signatures; `v1,` is the only scheme defined today.
|
|
55
|
+
const offered = signature.split(' ').flatMap(part => (part.startsWith('v1,') ? [part.slice('v1,'.length)] : []));
|
|
56
|
+
if (!offered.length)
|
|
57
|
+
return bad('no v1 signature offered');
|
|
58
|
+
return offered.some(sig => sameSignature(sig, expected)) ? { ok: true } : bad('signature does not match');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Refuses a `webhook-id` seen before. Bounded by time rather than by count, since the thing it
|
|
62
|
+
* defends against is a *re-delivery*, and OpenAI's own retry window is what sets the horizon.
|
|
63
|
+
*/
|
|
64
|
+
export class ReplayGuard {
|
|
65
|
+
seen = new Map();
|
|
66
|
+
// Explicit field, never a parameter property: the dev path runs these sources through Node's
|
|
67
|
+
// type *stripping*, which cannot transform one (CLAUDE.md ▸ Traps).
|
|
68
|
+
memoryMs;
|
|
69
|
+
constructor(memoryMs = REPLAY_MEMORY_MS) {
|
|
70
|
+
this.memoryMs = memoryMs;
|
|
71
|
+
}
|
|
72
|
+
/** True the first time an id is offered, false every time after, until it ages out. */
|
|
73
|
+
accept(id, nowMs = Date.now()) {
|
|
74
|
+
for (const [key, at] of this.seen)
|
|
75
|
+
if (nowMs - at > this.memoryMs)
|
|
76
|
+
this.seen.delete(key);
|
|
77
|
+
if (this.seen.has(id))
|
|
78
|
+
return false;
|
|
79
|
+
this.seen.set(id, nowMs);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
/** Let OpenAI retry a delivery whose handler failed before it could accept or reject the call. */
|
|
83
|
+
forget(id) {
|
|
84
|
+
this.seen.delete(id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The event body, or null when it is not an incoming call we can act on. */
|
|
88
|
+
export function parseIncomingCall(body) {
|
|
89
|
+
let event;
|
|
90
|
+
try {
|
|
91
|
+
event = JSON.parse(body);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (event?.type !== 'realtime.call.incoming')
|
|
97
|
+
return null;
|
|
98
|
+
const callId = event.data?.call_id;
|
|
99
|
+
if (typeof callId !== 'string' || !callId)
|
|
100
|
+
return null;
|
|
101
|
+
const raw = Array.isArray(event.data?.sip_headers) ? event.data.sip_headers : [];
|
|
102
|
+
const sipHeaders = raw.flatMap((h) => {
|
|
103
|
+
const entry = h;
|
|
104
|
+
return typeof entry?.name === 'string' && typeof entry?.value === 'string'
|
|
105
|
+
? [{ name: entry.name, value: entry.value }]
|
|
106
|
+
: [];
|
|
107
|
+
});
|
|
108
|
+
return { callId, sipHeaders };
|
|
109
|
+
}
|
|
110
|
+
export function sipHeader(headers, name) {
|
|
111
|
+
const wanted = name.toLowerCase();
|
|
112
|
+
return headers.find(h => h.name.toLowerCase() === wanted)?.value ?? null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { oneLine } from "../speech.js";
|
|
2
|
+
import { VOICE_INSTRUCTIONS } from "./prompt.js";
|
|
3
|
+
import { voiceFunctionTools } from "./tools.js";
|
|
4
|
+
export const TRANSCRIPTION_MODEL = 'gpt-live-transcribe';
|
|
5
|
+
export const MAX_SDP_CHARS = 100_000;
|
|
6
|
+
const TRANSCRIPTION_CONTEXT = 'Software development fleet control. Likely terms include Conductor, Codex, TypeScript, React, WebRTC, Tailwind, Biome, workspace, pull request, branch names, and file paths.';
|
|
7
|
+
function languageInstruction(language) {
|
|
8
|
+
switch (language) {
|
|
9
|
+
case 'no':
|
|
10
|
+
return 'Speak Norwegian Bokmål unless the user explicitly asks for another language.';
|
|
11
|
+
case 'en':
|
|
12
|
+
return 'Speak English unless the user explicitly asks for another language.';
|
|
13
|
+
case 'auto':
|
|
14
|
+
return 'Reply in the language the user is speaking. Do not translate unless asked.';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The PWA and SIP calls are the same orchestrator. The only transport-specific
|
|
19
|
+
* difference is the tool plumbing: these function definitions are executed by
|
|
20
|
+
* the relay over its private sideband instead of asking OpenAI to reach a public
|
|
21
|
+
* MCP URL.
|
|
22
|
+
*/
|
|
23
|
+
export function buildWebRtcSession(input) {
|
|
24
|
+
return {
|
|
25
|
+
type: 'realtime',
|
|
26
|
+
model: input.model,
|
|
27
|
+
instructions: `${input.instructions ?? VOICE_INSTRUCTIONS}\n\n${languageInstruction(input.language)}`,
|
|
28
|
+
max_output_tokens: 800,
|
|
29
|
+
output_modalities: ['audio'],
|
|
30
|
+
parallel_tool_calls: false,
|
|
31
|
+
audio: {
|
|
32
|
+
input: {
|
|
33
|
+
transcription: {
|
|
34
|
+
model: TRANSCRIPTION_MODEL,
|
|
35
|
+
prompt: TRANSCRIPTION_CONTEXT,
|
|
36
|
+
delay: 'low',
|
|
37
|
+
...(input.language === 'auto' ? {} : { languages: [input.language] })
|
|
38
|
+
},
|
|
39
|
+
noise_reduction: { type: 'near_field' },
|
|
40
|
+
turn_detection: {
|
|
41
|
+
type: 'server_vad',
|
|
42
|
+
threshold: 0.5,
|
|
43
|
+
prefix_padding_ms: 300,
|
|
44
|
+
silence_duration_ms: 650,
|
|
45
|
+
create_response: true,
|
|
46
|
+
interrupt_response: true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
output: { voice: input.voice }
|
|
50
|
+
},
|
|
51
|
+
tools: voiceFunctionTools(),
|
|
52
|
+
tool_choice: 'auto'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function upstreamMessage(response, action) {
|
|
56
|
+
const raw = await response.text().catch(() => '');
|
|
57
|
+
let detail = '';
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
if (typeof parsed.error?.message === 'string')
|
|
61
|
+
detail = parsed.error.message;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
detail = raw;
|
|
65
|
+
}
|
|
66
|
+
const suffix = detail.trim() ? `: ${oneLine(detail, 240)}` : '';
|
|
67
|
+
return new Error(`OpenAI ${action} returned ${response.status}${suffix}`);
|
|
68
|
+
}
|
|
69
|
+
function callIdFromLocation(location) {
|
|
70
|
+
if (!location)
|
|
71
|
+
return null;
|
|
72
|
+
const pathname = new URL(location, 'https://api.openai.invalid').pathname.replace(/\/+$/, '');
|
|
73
|
+
const value = pathname.split('/').pop();
|
|
74
|
+
return value?.startsWith('rtc_') ? value : null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Use OpenAI's unified interface: the standard key never leaves the relay, and
|
|
78
|
+
* the Location receipt gives the broker the call id for its private sideband.
|
|
79
|
+
*/
|
|
80
|
+
export async function createWebRtcCall(apiKey, apiOrigin, sdp, session, safetyIdentifier, fetcher = fetch) {
|
|
81
|
+
if (!sdp.trim())
|
|
82
|
+
throw new Error('WebRTC offer is required');
|
|
83
|
+
if (sdp.length > MAX_SDP_CHARS)
|
|
84
|
+
throw new Error('WebRTC offer is too large');
|
|
85
|
+
const form = new FormData();
|
|
86
|
+
form.set('sdp', sdp);
|
|
87
|
+
form.set('session', JSON.stringify(buildWebRtcSession(session)));
|
|
88
|
+
const origin = apiOrigin.replace(/\/+$/, '');
|
|
89
|
+
const response = await fetcher(`${origin}/v1/realtime/calls`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
signal: AbortSignal.timeout(20_000),
|
|
92
|
+
headers: {
|
|
93
|
+
authorization: `Bearer ${apiKey}`,
|
|
94
|
+
'openai-safety-identifier': safetyIdentifier
|
|
95
|
+
},
|
|
96
|
+
body: form
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok)
|
|
99
|
+
throw await upstreamMessage(response, 'WebRTC call');
|
|
100
|
+
const callId = callIdFromLocation(response.headers.get('location'));
|
|
101
|
+
if (!callId)
|
|
102
|
+
throw new Error('OpenAI WebRTC call returned no call id');
|
|
103
|
+
const answer = await response.text();
|
|
104
|
+
if (!answer.trim())
|
|
105
|
+
throw new Error('OpenAI WebRTC call returned no SDP answer');
|
|
106
|
+
return { callId, sdp: answer };
|
|
107
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Voice setup
|
|
2
|
+
|
|
3
|
+
## PWA fleet call over WebRTC
|
|
4
|
+
|
|
5
|
+
The primary foreground voice mode is one fleet-wide control room. In the
|
|
6
|
+
workspace-list header, tap the phone immediately left of **+**, then start the
|
|
7
|
+
call. The orchestrator surveys every workspace, presents one bounded decision
|
|
8
|
+
at a time, and can queue an exact prompt only after it reads the target and text
|
|
9
|
+
back and you confirm. It is not owned by the chat currently on screen: hide the
|
|
10
|
+
sheet or move between workspaces and the same call continues.
|
|
11
|
+
|
|
12
|
+
Live captions keep both sides readable, and the text box in the call sheet is a
|
|
13
|
+
fallback when speaking is inconvenient. The five available actions are roll
|
|
14
|
+
call, fresh workspace overview, next decision, send preview, and confirmed send.
|
|
15
|
+
They use the same preview/confirmation gate as the dial-in orchestrator.
|
|
16
|
+
|
|
17
|
+
This path needs the managed relay, its usual private phone URL, and an OpenAI API
|
|
18
|
+
key. It does **not** need a phone number, SIP, a webhook, Funnel, or any other
|
|
19
|
+
public endpoint. The permanent key stays on the Mac: the relay creates the
|
|
20
|
+
WebRTC call, then executes those five tools over its private sideband connection.
|
|
21
|
+
|
|
22
|
+
Store the key without leaving it in shell history:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
read -rs "OPENAI_KEY?OpenAI API key: "; echo
|
|
26
|
+
conductor-remote config set voice.openai-key "$OPENAI_KEY"
|
|
27
|
+
unset OPENAI_KEY
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
On the phone:
|
|
31
|
+
|
|
32
|
+
1. Open the workspace drawer and tap the phone immediately left of **+**.
|
|
33
|
+
2. Choose a voice and **Auto detect**, **Norsk**, or **English**, then tap
|
|
34
|
+
**Start fleet call** and grant microphone access the first time.
|
|
35
|
+
3. Speak naturally. Server-side voice activity detection turns each pause into a
|
|
36
|
+
turn; mute the microphone when needed.
|
|
37
|
+
4. Follow the live captions, or type into the call sheet when audio input is not
|
|
38
|
+
convenient.
|
|
39
|
+
5. Close the sheet to keep browsing while the call continues. Reopen it from the
|
|
40
|
+
same phone button; use the red handset to actually end the call.
|
|
41
|
+
|
|
42
|
+
The audio is AI-generated. Input captions use `gpt-live-transcribe`; the
|
|
43
|
+
configured Realtime model produces the answer and its audio. The PWA path is
|
|
44
|
+
foreground-only: mobile browsers cannot promise native-call continuity with the
|
|
45
|
+
screen locked, so use the optional dial-in transport for a pocketed commute.
|
|
46
|
+
|
|
47
|
+
## Optional dial-in orchestrator
|
|
48
|
+
|
|
49
|
+
The optional voice listener turns a phone call into a small Conductor control room: hear a bounded fleet tally, walk one decision at a time, and dispatch an exact prompt after a spoken read-back and explicit confirmation. It does not expose the PWA or the relay API publicly.
|
|
50
|
+
|
|
51
|
+
This first milestone has five tools: roll call, a fresh paged workspace overview, next decision, send preview, and send. Forward-to-owner answers, redial cursor resume, artifact pushes, and voice grooming remain later milestones.
|
|
52
|
+
|
|
53
|
+
## What you need
|
|
54
|
+
|
|
55
|
+
- The managed `conductor-remote` service and Tailscale on the Mac.
|
|
56
|
+
- Tailscale Funnel enabled for the tailnet. OpenAI's SIP sender must reach HTTPS port 443; 8443 and 10000 do not work for this webhook.
|
|
57
|
+
- An OpenAI API project with billing, an API key, its `proj_…` project ID, and a webhook signing secret.
|
|
58
|
+
- A Twilio account, auth token, and voice-capable phone number.
|
|
59
|
+
- The caller numbers to allow, in E.164 form, and a private 4–12 digit PIN.
|
|
60
|
+
|
|
61
|
+
OpenAI's [SIP guide](https://developers.openai.com/api/docs/guides/realtime-sip) documents the project-addressed SIP URI, `realtime.call.incoming` webhook, accept API, and the EU SIP host. Twilio's [incoming-call guide](https://www.twilio.com/docs/voice/tutorials/how-to-respond-to-incoming-phone-calls) describes the phone-number webhook/TwiML flow.
|
|
62
|
+
|
|
63
|
+
## 1. Install the service
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
conductor-remote service install --expose tailnet
|
|
67
|
+
conductor-remote service status
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Voice always keeps the main relay tailnet-only. Its dedicated listener binds `127.0.0.1:8788`; the installer moves the relay off HTTPS 443 when necessary, publishes only `/voice` on 443, verifies the live Tailscale state, and records a 0600 ownership receipt. It refuses to replace a manual or foreign mount.
|
|
71
|
+
|
|
72
|
+
Use `--voice-port <port>` or `conductor-remote config set voice-port <port>` only if 8788 is occupied.
|
|
73
|
+
|
|
74
|
+
## 2. Store the local gate and provider settings
|
|
75
|
+
|
|
76
|
+
The commands below keep literal secrets out of shell history by reading them into temporary shell variables. They still exist briefly in this local process tree, like any CLI argument.
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
read -rs "OPENAI_KEY?OpenAI API key: "; echo
|
|
80
|
+
conductor-remote config set voice.openai-key "$OPENAI_KEY"
|
|
81
|
+
unset OPENAI_KEY
|
|
82
|
+
|
|
83
|
+
read -rs "TWILIO_TOKEN?Twilio auth token: "; echo
|
|
84
|
+
conductor-remote config set voice.twilio-auth-token "$TWILIO_TOKEN"
|
|
85
|
+
unset TWILIO_TOKEN
|
|
86
|
+
|
|
87
|
+
conductor-remote config set voice.project-id 'proj_your_project_id'
|
|
88
|
+
conductor-remote config set voice.allowed-callers '+4712345678,+15551234567'
|
|
89
|
+
|
|
90
|
+
read -rs "VOICE_PIN?Voice PIN: "; echo
|
|
91
|
+
conductor-remote config set voice.pin "$VOICE_PIN"
|
|
92
|
+
unset VOICE_PIN
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Global residency is the default:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
conductor-remote config set voice.sip-host sip.api.openai.com
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Use `sip-eu.api.openai.com` only when **Project Settings → General → Project
|
|
102
|
+
Residency** says Europe. The relay then uses `eu.api.openai.com` for call accept
|
|
103
|
+
and its sideband WebSocket as well; mixing EU SIP ingress with global call
|
|
104
|
+
control makes the webhook's `call_id` invisible to the accept request.
|
|
105
|
+
|
|
106
|
+
## 3. Give the listener its permanent public URL
|
|
107
|
+
|
|
108
|
+
Replace the hostname with this Mac's MagicDNS name. Keep `/voice` exactly once.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
conductor-remote config set voice.public-url 'https://your-mac.your-tailnet.ts.net/voice'
|
|
112
|
+
conductor-remote service status
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Expected status has two different routes:
|
|
116
|
+
|
|
117
|
+
```text
|
|
118
|
+
Phone URL: https://your-mac.your-tailnet.ts.net:<port>/… (same Tailnet only)
|
|
119
|
+
voice: https://your-mac.your-tailnet.ts.net/voice (public) → 127.0.0.1:8788
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The public root should stay closed while the signed webhook path answers:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
curl -i 'https://your-mac.your-tailnet.ts.net/'
|
|
126
|
+
curl -i -X POST 'https://your-mac.your-tailnet.ts.net/voice'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The first request should get a Tailscale-layer 404. The second should reach the listener and reject the unsigned body; a rejection proves routing, not provider authentication.
|
|
130
|
+
|
|
131
|
+
## 4. Configure the OpenAI incoming-call webhook
|
|
132
|
+
|
|
133
|
+
In the OpenAI dashboard, open **Settings → Project → Webhooks** for the same project ID used above:
|
|
134
|
+
|
|
135
|
+
1. Create an endpoint with the exact URL `https://your-mac.your-tailnet.ts.net/voice`.
|
|
136
|
+
2. Subscribe it to `realtime.call.incoming`.
|
|
137
|
+
3. Copy the `whsec_…` signing secret and store it:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
read -rs "OPENAI_WEBHOOK_SECRET?OpenAI webhook secret: "; echo
|
|
141
|
+
conductor-remote config set voice.webhook-secret "$OPENAI_WEBHOOK_SECRET"
|
|
142
|
+
unset OPENAI_WEBHOOK_SECRET
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The relay accepts a valid incoming call through `POST /v1/realtime/calls/{call_id}/accept`, attaches an authenticated sideband WebSocket, and gives the session only the five scoped remote MCP tools. Remote MCP follow-up responses are driven by the broker only after both the response and every tool call in it have finished, as required by OpenAI's [Realtime MCP guide](https://developers.openai.com/api/docs/guides/realtime-mcp).
|
|
146
|
+
|
|
147
|
+
## 5. Configure the Twilio number
|
|
148
|
+
|
|
149
|
+
In the Twilio Console, open the voice-capable number. For **A call comes in**, select **Webhook**, set:
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
URL: https://your-mac.your-tailnet.ts.net/voice/twiml
|
|
153
|
+
Method: HTTP POST
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Save it, then call from an allowlisted number. Twilio signs the exact configured URL and form fields; even a harmless URL spelling change breaks validation. See Twilio's [webhook security guide](https://www.twilio.com/docs/usage/webhooks/webhooks-security).
|
|
157
|
+
|
|
158
|
+
The expected path is:
|
|
159
|
+
|
|
160
|
+
1. An unlisted caller is rejected before the bridge.
|
|
161
|
+
2. An allowed caller hears the PIN prompt.
|
|
162
|
+
3. A wrong PIN is rejected.
|
|
163
|
+
4. A correct PIN bridges to the case-sensitive OpenAI project SIP URI with a short-lived HMAC marker.
|
|
164
|
+
5. The OpenAI webhook verifies its own signature, replay ID, and that marker before accepting the call.
|
|
165
|
+
|
|
166
|
+
## Verify and operate
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
conductor-remote config
|
|
170
|
+
conductor-remote service status
|
|
171
|
+
conductor-remote service logs
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`config` prints only `(set)` for secrets. The files below are mode 0600:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
~/Library/Application Support/conductor-remote/voice.json
|
|
178
|
+
~/Library/Application Support/conductor-remote/voice-funnel.json
|
|
179
|
+
~/Library/Application Support/conductor-remote/voice-calls.json
|
|
180
|
+
~/Library/Application Support/conductor-remote/voice-previews.json
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Each call starts with the Mac's lock state. A confirmed send returns to the voice session immediately and reuses the relay's existing transcript receipt, retry, idempotency, and parked-prompt path. A landed send stays silent; a locked or failed send is announced. Merely hearing a decision does not clear it—dispatching it or explicitly skipping it advances the read mark.
|
|
184
|
+
|
|
185
|
+
### Cost
|
|
186
|
+
|
|
187
|
+
The default is `gpt-realtime-2.1-mini`. OpenAI currently publishes these per-million-token prices:
|
|
188
|
+
|
|
189
|
+
| Modality | Input | Cached input | Output |
|
|
190
|
+
| --- | ---: | ---: | ---: |
|
|
191
|
+
| Text | $0.60 | $0.06 | $2.40 |
|
|
192
|
+
| Audio | $10.00 | $0.30 | $20.00 |
|
|
193
|
+
|
|
194
|
+
The broker logs actual token usage and an estimate for every completed response. Use those lines for this workflow's real per-call cost; Twilio phone-number and PSTN charges are separate and depend on the account/country. The rate source is the [GPT-Realtime-2.1 Mini model page](https://developers.openai.com/api/docs/models/gpt-realtime-2.1-mini).
|
|
195
|
+
|
|
196
|
+
### Disable or rotate
|
|
197
|
+
|
|
198
|
+
This removes only the receipt-owned public voice mount and leaves the relay tailnet-only:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
conductor-remote config set voice.public-url unset
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Clear a credential the same way, for example:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
conductor-remote config set voice.openai-key unset
|
|
208
|
+
conductor-remote config set voice.webhook-secret unset
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Rotating a value through `config set` restarts the managed service. The relay token, generated MCP token, and trunk marker secret are separate; neither relay token nor provider secrets are returned by `config` or the log API.
|
|
212
|
+
|
|
213
|
+
## Troubleshooting
|
|
214
|
+
|
|
215
|
+
- **The call rings and then fails with no relay log:** confirm the public endpoint is on 443 and is exactly `/voice`. OpenAI does not deliver this webhook through Funnel 8443 or 10000.
|
|
216
|
+
- **Twilio gets 403:** check the allowlist, the account's primary auth token, and that `voice.public-url + /twiml` is byte-for-byte the URL configured on the number.
|
|
217
|
+
- **OpenAI gets 400:** check the webhook signing secret and the Mac's clock.
|
|
218
|
+
- **Accept returns `call_id_not_found`:** confirm `voice.sip-host` matches the
|
|
219
|
+
project residency. Global projects use `sip.api.openai.com`; EU-resident
|
|
220
|
+
projects use `sip-eu.api.openai.com`.
|
|
221
|
+
- **OpenAI accepts but the voice does not start:** inspect `service logs` for MCP import or observer socket errors. The broker waits for `mcp_list_tools.completed` before the greeting.
|
|
222
|
+
- **A send parks:** unlock the Mac. The same parked queue used by the PWA delivers it after unlock.
|
|
223
|
+
- **The installer refuses Funnel changes:** `tailscale serve status --json` contains a mount that has no matching conductor-remote ownership receipt. Move that mount yourself; the installer will not overwrite it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conductor-remote",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.101.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"packageManager": "yarn@4.15.0",
|
|
6
6
|
"description": "Phone control panel for local Conductor agents. Reads ride SQLite + git; prompts ride Conductor's own dispatch path.",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"files": [
|
|
31
31
|
"bin",
|
|
32
32
|
"dist",
|
|
33
|
-
"dist-node"
|
|
33
|
+
"dist-node",
|
|
34
|
+
"docs/voice-setup.md"
|
|
34
35
|
],
|
|
35
36
|
"publishConfig": {
|
|
36
37
|
"access": "public",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-outline-style:solid;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, -apple-system, "SF Pro Text", "Segoe UI", Roboto, sans-serif;--font-mono:ui-monospace, "SF Mono", "JetBrains Mono", "Fira Code", Menlo, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-xl:36rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--leading-tight:1.25;--leading-snug:1.375;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:light-dark(#f5f6f8,#0a0b0e);--color-surface:light-dark(#fff,#14161b);--color-surface-2:light-dark(#eceef2,#1b1e26);--color-border:light-dark(#d4d7de,#262a33);--color-border-soft:light-dark(#e2e4e9,#1e222a);--color-text:light-dark(#1c1e24,#e7e9ee);--color-muted:light-dark(#626773,#8b909d);--color-faint:light-dark(#9499a4,#5c616d);--color-accent:light-dark(#6757d9,#8b7dff);--color-accent-soft:light-dark(#ebe9ff,#2a2650);--color-on-solid:light-dark(#fff,#101116);--color-provider-openai:light-dark(#17191f,#fff);--color-working:light-dark(#a65f00,#f5a623);--color-idle:light-dark(#167a52,#3ecf8e);--color-done:light-dark(#286bc0,#5b9dff);--color-cold-cache:light-dark(#90611f,#d6a85f);--color-pr-merged:light-dark(#7b38b5,#ac47ff);--color-pr-draft:light-dark(#6e7075,#a4a3a2);--color-pr-attention:light-dark(#916b00,#facc15);--color-pr-mergeable:light-dark(#167b42,#49de80);--color-add:light-dark(#14784f,#3ecf8e);--color-del:light-dark(#c73c3c,#ff6b6b)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}*{-webkit-tap-highlight-color:transparent}html,body,#root{height:var(--app-height,100dvh)}html,body{overflow:hidden}@media(display-mode:standalone){html,body,#root{height:var(--app-height,100vh)}}html[data-standalone],html[data-standalone] body,html[data-standalone] #root{height:var(--app-height,100vh)}.app-height{height:var(--app-height,100dvh)}@media(display-mode:standalone){.app-height{height:var(--app-height,100vh)}}html[data-standalone] .app-height{height:var(--app-height,100vh)}@media(min-width:48rem){.app-height,html[data-standalone] .app-height{height:auto}}html{color-scheme:light dark;background:var(--color-bg)}html[data-theme=light]{color-scheme:light}html[data-theme=dark]{color-scheme:dark}body{background:var(--color-bg);color:var(--color-text);font-family:var(--font-sans);-webkit-font-smoothing:antialiased;overscroll-behavior:none;margin:0}button{font:inherit;color:inherit;cursor:pointer}::-webkit-scrollbar{width:0;height:0}}@layer components{.card{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-border);background-color:var(--color-surface);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3.5);text-align:left;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}.card:active{background-color:var(--color-surface-2);scale:.985}.pill{padding-inline:calc(var(--spacing) * 3.5);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.pill-active{background-color:var(--color-surface-2);color:var(--color-text)}.ctl{padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);--tw-font-weight:var(--font-weight-medium);font-size:13px;font-weight:var(--font-weight-medium);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px}.ctl-on{background-color:var(--color-surface-2);color:var(--color-text)}.ctl-staged{background-color:var(--color-accent-soft);color:var(--color-accent)}.dot{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5);background-color:var(--color-faint);border-radius:3.40282e38px;flex-shrink:0}.dot-spinner{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);animation:var(--animate-spin);border-style:var(--tw-border-style);--spin-color:var(--color-working);border-width:2px;border-color:var(--spin-color);border-radius:3.40282e38px;flex-shrink:0}@supports (color:color-mix(in lab,red,red)){.dot-spinner{border-color:color-mix(in srgb,var(--spin-color) 30%,transparent)}}.dot-spinner{border-top-color:var(--spin-color);border-right-color:var(--spin-color)}.dot-error{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5);border-style:var(--tw-border-style);border-width:2px;border-color:var(--color-del);color:var(--color-del);border-radius:3.40282e38px;flex-shrink:0;justify-content:center;align-items:center;display:flex}.dot-idle{background:var(--color-idle)}.dot-done{background:var(--color-done)}.typing-dot{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5);background-color:var(--color-muted);border-radius:3.40282e38px;animation:1.2s ease-in-out infinite typing}.typing-dot:nth-child(2){animation-delay:.15s}.typing-dot:nth-child(3){animation-delay:.3s}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p{margin:.5em 0}.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{margin:.9em 0 .4em;font-weight:600;line-height:1.3}.md h1{font-size:1.6em}.md h2{font-size:1.4em}.md h3{font-size:1.2em}.md h4{font-size:1.1em}.md ul,.md ol{margin:.5em 0;padding-left:1.4em}.md ul{list-style:outside}.md ol{list-style:decimal}.md li{margin:.3em 0}.md li>ul,.md li>ol{margin:.2em 0}.md code{background:light-dark(#eceef2cc,#1b1e26cc);border-radius:.3rem}@supports (color:color-mix(in lab,red,red)){.md code{background:color-mix(in srgb,var(--color-surface-2) 80%,transparent)}}.md code{font-family:var(--font-mono);padding:.1em .35em;font-size:.85em}.md pre{border:1px solid var(--color-border-soft);background:var(--color-bg);border-radius:.6rem;margin:.5em 0;padding:.6em .75em;overflow-x:auto}.md pre code{background:0 0;padding:0;font-size:12px;line-height:1.5}.md a{color:var(--color-accent);text-underline-offset:2px;text-decoration:underline}.md blockquote{border-left:2px solid var(--color-border);color:var(--color-muted);margin:.5em 0;padding-left:.75em}.md hr{border:0;border-top:1px solid var(--color-border-soft);margin:1.25em 0}.md img{border-radius:.6rem;max-width:100%;height:auto;margin:.5em 0;display:block}.md table{border-collapse:collapse;max-width:100%;margin:.5em 0;font-size:.9em;display:block;overflow-x:auto}.md th,.md td{border:1px solid var(--color-border);text-align:left;padding:.3em .6em}.md th{background:var(--color-surface-2);font-weight:600}.md strong{font-weight:600}.md input[type=checkbox]{margin-right:.4em}.hljs-comment,.hljs-quote{color:var(--color-faint);font-style:italic}.hljs-keyword,.hljs-literal,.hljs-type,.hljs-built_in,.hljs-selector-tag,.hljs-doctag,.hljs-meta .hljs-keyword{color:var(--color-accent)}.hljs-string,.hljs-regexp,.hljs-char.escape_,.hljs-addition,.hljs-meta .hljs-string{color:var(--color-add)}.hljs-number,.hljs-symbol,.hljs-bullet,.hljs-link,.hljs-meta,.hljs-template-variable{color:var(--color-working)}.hljs-title,.hljs-title.class_,.hljs-title.function_,.hljs-name,.hljs-section,.hljs-selector-id,.hljs-selector-class{color:var(--color-done)}.hljs-attr,.hljs-attribute,.hljs-property,.hljs-variable,.hljs-params,.hljs-subst,.hljs-template-tag{color:var(--color-text)}.hljs-deletion{color:var(--color-del)}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:600}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-1{inset:var(--spacing)}.inset-x-0{inset-inline:0}.inset-y-0{inset-block:0}.top-0{top:0}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-1\/2{top:50%}.top-full{top:100%}.-right-0\.5{right:calc(var(--spacing) * -.5)}.right-0{right:0}.right-1{right:var(--spacing)}.right-1\.5{right:calc(var(--spacing) * 1.5)}.right-2{right:calc(var(--spacing) * 2)}.right-3{right:calc(var(--spacing) * 3)}.-bottom-0\.5{bottom:calc(var(--spacing) * -.5)}.bottom-0{bottom:0}.bottom-full{bottom:100%}.left-0{left:0}.left-0\.5{left:calc(var(--spacing) * .5)}.left-3{left:calc(var(--spacing) * 3)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.z-\[60\]{z-index:60}.col-span-3{grid-column:span 3/span 3}.m-0{margin:0}.mx-3{margin-inline:calc(var(--spacing) * 3)}.mx-auto{margin-inline:auto}.my-1\.5{margin-block:calc(var(--spacing) * 1.5)}.my-2{margin-block:calc(var(--spacing) * 2)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mr-2{margin-right:calc(var(--spacing) * -2)}.mr-1{margin-right:var(--spacing)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.-ml-1{margin-left:calc(var(--spacing) * -1)}.ml-1{margin-left:var(--spacing)}.ml-1\.5{margin-left:calc(var(--spacing) * 1.5)}.ml-\[7px\]{margin-left:7px}.ml-auto{margin-left:auto}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-4{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.size-20{width:calc(var(--spacing) * 20);height:calc(var(--spacing) * 20)}.size-60{width:calc(var(--spacing) * 60);height:calc(var(--spacing) * 60)}.size-\[15px\]{width:15px;height:15px}.size-\[18px\]{width:18px;height:18px}.size-full{width:100%;height:100%}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-12{height:calc(var(--spacing) * 12)}.h-\[15px\]{height:15px}.h-full{height:100%}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-\[70\%\]{max-height:70%}.max-h-\[85dvh\]{max-height:85dvh}.max-h-\[calc\(100dvh-4rem\)\]{max-height:calc(100dvh - 4rem)}.max-h-full{max-height:100%}.min-h-0{min-height:0}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-14{min-height:calc(var(--spacing) * 14)}.w-0\.5{width:calc(var(--spacing) * .5)}.w-1{width:var(--spacing)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-9{width:calc(var(--spacing) * 9)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-60{width:calc(var(--spacing) * 60)}.w-64{width:calc(var(--spacing) * 64)}.w-\[85\%\]{width:85%}.w-full{width:100%}.max-w-20{max-width:calc(var(--spacing) * 20)}.max-w-24{max-width:calc(var(--spacing) * 24)}.max-w-28{max-width:calc(var(--spacing) * 28)}.max-w-36{max-width:calc(var(--spacing) * 36)}.max-w-40{max-width:calc(var(--spacing) * 40)}.max-w-56{max-width:calc(var(--spacing) * 56)}.max-w-64{max-width:calc(var(--spacing) * 64)}.max-w-72{max-width:calc(var(--spacing) * 72)}.max-w-80{max-width:calc(var(--spacing) * 80)}.max-w-\[70vw\]{max-width:70vw}.max-w-\[85\%\]{max-width:85%}.max-w-\[85vw\]{max-width:85vw}.max-w-\[92\%\]{max-width:92%}.max-w-full{max-width:100%}.max-w-md{max-width:var(--container-md)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-9{min-width:calc(var(--spacing) * 9)}.min-w-28{min-width:calc(var(--spacing) * 28)}.min-w-44{min-width:calc(var(--spacing) * 44)}.min-w-52{min-width:calc(var(--spacing) * 52)}.min-w-max{min-width:max-content}.flex-1{flex:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-3{--tw-translate-y:calc(var(--spacing) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.-rotate-90{rotate:-90deg}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.scrollbar-thin{scrollbar-width:thin}.list-none{list-style-type:none}.appearance-none{appearance:none}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-\[auto_1fr\]{grid-template-columns:auto 1fr}.grid-cols-\[minmax\(0\,1fr\)_auto_auto\]{grid-template-columns:minmax(0,1fr) auto auto}.grid-cols-subgrid{grid-template-columns:subgrid}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-start{justify-content:flex-start}.gap-0{gap:0}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-\[1\.5px\]{gap:1.5px}:where(.space-y-1\.25>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.25) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.25) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-y-1{row-gap:var(--spacing)}.self-center{align-self:center}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-x-hidden{overflow-x:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-\[inherit\]{border-radius:inherit}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-accent{border-color:var(--color-accent)}.border-accent\/50{border-color:light-dark(#6757d980,#8b7dff80)}@supports (color:color-mix(in lab,red,red)){.border-accent\/50{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.border-border{border-color:var(--color-border)}.border-border-soft{border-color:var(--color-border-soft)}.border-cold-cache\/20{border-color:light-dark(#90611f33,#d6a85f33)}@supports (color:color-mix(in lab,red,red)){.border-cold-cache\/20{border-color:color-mix(in oklab,var(--color-cold-cache) 20%,transparent)}}.border-del{border-color:var(--color-del)}.border-del\/30{border-color:light-dark(#c73c3c4d,#ff6b6b4d)}@supports (color:color-mix(in lab,red,red)){.border-del\/30{border-color:color-mix(in oklab,var(--color-del) 30%,transparent)}}.border-del\/40{border-color:light-dark(#c73c3c66,#ff6b6b66)}@supports (color:color-mix(in lab,red,red)){.border-del\/40{border-color:color-mix(in oklab,var(--color-del) 40%,transparent)}}.border-del\/50{border-color:light-dark(#c73c3c80,#ff6b6b80)}@supports (color:color-mix(in lab,red,red)){.border-del\/50{border-color:color-mix(in oklab,var(--color-del) 50%,transparent)}}.border-faint{border-color:var(--color-faint)}.border-pr-merged\/30{border-color:light-dark(#7b38b54d,#ac47ff4d)}@supports (color:color-mix(in lab,red,red)){.border-pr-merged\/30{border-color:color-mix(in oklab,var(--color-pr-merged) 30%,transparent)}}.border-white\/80{border-color:#fffc}@supports (color:color-mix(in lab,red,red)){.border-white\/80{border-color:color-mix(in oklab,var(--color-white) 80%,transparent)}}.border-working\/30{border-color:light-dark(#a65f004d,#f5a6234d)}@supports (color:color-mix(in lab,red,red)){.border-working\/30{border-color:color-mix(in oklab,var(--color-working) 30%,transparent)}}.border-t-accent{border-top-color:var(--color-accent)}.border-t-text{border-top-color:var(--color-text)}.border-l-accent{border-left-color:var(--color-accent)}.bg-accent{background-color:var(--color-accent)}.bg-accent-soft{background-color:var(--color-accent-soft)}.bg-accent-soft\/90{background-color:light-dark(#ebe9ffe6,#2a2650e6)}@supports (color:color-mix(in lab,red,red)){.bg-accent-soft\/90{background-color:color-mix(in oklab,var(--color-accent-soft) 90%,transparent)}}.bg-accent\/10{background-color:light-dark(#6757d91a,#8b7dff1a)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/10{background-color:color-mix(in oklab,var(--color-accent) 10%,transparent)}}.bg-accent\/25{background-color:light-dark(#6757d940,#8b7dff40)}@supports (color:color-mix(in lab,red,red)){.bg-accent\/25{background-color:color-mix(in oklab,var(--color-accent) 25%,transparent)}}.bg-add{background-color:var(--color-add)}.bg-add\/10{background-color:light-dark(#14784f1a,#3ecf8e1a)}@supports (color:color-mix(in lab,red,red)){.bg-add\/10{background-color:color-mix(in oklab,var(--color-add) 10%,transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-bg\/80{background-color:light-dark(#f5f6f8cc,#0a0b0ecc)}@supports (color:color-mix(in lab,red,red)){.bg-bg\/80{background-color:color-mix(in oklab,var(--color-bg) 80%,transparent)}}.bg-bg\/90{background-color:light-dark(#f5f6f8e6,#0a0b0ee6)}@supports (color:color-mix(in lab,red,red)){.bg-bg\/90{background-color:color-mix(in oklab,var(--color-bg) 90%,transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/15{background-color:#00000026}@supports (color:color-mix(in lab,red,red)){.bg-black\/15{background-color:color-mix(in oklab,var(--color-black) 15%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-black\/75{background-color:#000000bf}@supports (color:color-mix(in lab,red,red)){.bg-black\/75{background-color:color-mix(in oklab,var(--color-black) 75%,transparent)}}.bg-cold-cache\/8{background-color:light-dark(#90611f14,#d6a85f14)}@supports (color:color-mix(in lab,red,red)){.bg-cold-cache\/8{background-color:color-mix(in oklab,var(--color-cold-cache) 8%,transparent)}}.bg-cold-cache\/12{background-color:light-dark(#90611f1f,#d6a85f1f)}@supports (color:color-mix(in lab,red,red)){.bg-cold-cache\/12{background-color:color-mix(in oklab,var(--color-cold-cache) 12%,transparent)}}.bg-current{background-color:currentColor}.bg-del{background-color:var(--color-del)}.bg-del\/5{background-color:light-dark(#c73c3c0d,#ff6b6b0d)}@supports (color:color-mix(in lab,red,red)){.bg-del\/5{background-color:color-mix(in oklab,var(--color-del) 5%,transparent)}}.bg-del\/10{background-color:light-dark(#c73c3c1a,#ff6b6b1a)}@supports (color:color-mix(in lab,red,red)){.bg-del\/10{background-color:color-mix(in oklab,var(--color-del) 10%,transparent)}}.bg-del\/15{background-color:light-dark(#c73c3c26,#ff6b6b26)}@supports (color:color-mix(in lab,red,red)){.bg-del\/15{background-color:color-mix(in oklab,var(--color-del) 15%,transparent)}}.bg-inherit{background-color:inherit}.bg-pr-merged\/10{background-color:light-dark(#7b38b51a,#ac47ff1a)}@supports (color:color-mix(in lab,red,red)){.bg-pr-merged\/10{background-color:color-mix(in oklab,var(--color-pr-merged) 10%,transparent)}}.bg-pr-merged\/15{background-color:light-dark(#7b38b526,#ac47ff26)}@supports (color:color-mix(in lab,red,red)){.bg-pr-merged\/15{background-color:color-mix(in oklab,var(--color-pr-merged) 15%,transparent)}}.bg-surface{background-color:var(--color-surface)}.bg-surface-2{background-color:var(--color-surface-2)}.bg-surface-2\/60{background-color:light-dark(#eceef299,#1b1e2699)}@supports (color:color-mix(in lab,red,red)){.bg-surface-2\/60{background-color:color-mix(in oklab,var(--color-surface-2) 60%,transparent)}}.bg-surface\/40{background-color:light-dark(#fff6,#14161b66)}@supports (color:color-mix(in lab,red,red)){.bg-surface\/40{background-color:color-mix(in oklab,var(--color-surface) 40%,transparent)}}.bg-surface\/60{background-color:light-dark(#fff9,#14161b99)}@supports (color:color-mix(in lab,red,red)){.bg-surface\/60{background-color:color-mix(in oklab,var(--color-surface) 60%,transparent)}}.bg-surface\/70{background-color:light-dark(#ffffffb3,#14161bb3)}@supports (color:color-mix(in lab,red,red)){.bg-surface\/70{background-color:color-mix(in oklab,var(--color-surface) 70%,transparent)}}.bg-surface\/95{background-color:light-dark(#fffffff2,#14161bf2)}@supports (color:color-mix(in lab,red,red)){.bg-surface\/95{background-color:color-mix(in oklab,var(--color-surface) 95%,transparent)}}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.bg-working{background-color:var(--color-working)}.bg-working\/5{background-color:light-dark(#a65f000d,#f5a6230d)}@supports (color:color-mix(in lab,red,red)){.bg-working\/5{background-color:color-mix(in oklab,var(--color-working) 5%,transparent)}}.bg-working\/10{background-color:light-dark(#a65f001a,#f5a6231a)}@supports (color:color-mix(in lab,red,red)){.bg-working\/10{background-color:color-mix(in oklab,var(--color-working) 10%,transparent)}}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-3\.5{padding:calc(var(--spacing) * 3.5)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-16{padding-block:calc(var(--spacing) * 16)}.py-px{padding-block:1px}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1{padding-top:var(--spacing)}.pt-1\.5{padding-top:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-2\.5{padding-top:calc(var(--spacing) * 2.5)}.pr-1{padding-right:var(--spacing)}.pr-3\.5{padding-right:calc(var(--spacing) * 3.5)}.pr-7{padding-right:calc(var(--spacing) * 7)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-0\.5{padding-bottom:calc(var(--spacing) * .5)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-2\.5{padding-bottom:calc(var(--spacing) * 2.5)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-2\.5{padding-left:calc(var(--spacing) * 2.5)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-8{padding-left:calc(var(--spacing) * 8)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-baseline{vertical-align:baseline}.font-mono{font-family:var(--font-mono)}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[0\.9em\]{font-size:.9em}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[14px\]{font-size:14px}.text-\[15px\]{font-size:15px}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-\[1\.5\]{--tw-leading:1.5;line-height:1.5}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.\[overflow-wrap\:anywhere\]{overflow-wrap:anywhere}.break-words{overflow-wrap:break-word}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--color-accent)}.text-add{color:var(--color-add)}.text-bg{color:var(--color-bg)}.text-black{color:var(--color-black)}.text-cold-cache{color:var(--color-cold-cache)}.text-del{color:var(--color-del)}.text-del\/80{color:light-dark(#c73c3ccc,#ff6b6bcc)}@supports (color:color-mix(in lab,red,red)){.text-del\/80{color:color-mix(in oklab,var(--color-del) 80%,transparent)}}.text-faint{color:var(--color-faint)}.text-muted{color:var(--color-muted)}.text-on-solid{color:var(--color-on-solid)}.text-pr-merged{color:var(--color-pr-merged)}.text-text{color:var(--color-text)}.text-white{color:var(--color-white)}.text-working{color:var(--color-working)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.underline-offset-2{text-underline-offset:2px}.opacity-0{opacity:0}.opacity-20{opacity:.2}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_0_100vmax_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 0 0 100vmax var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color:#0006}@supports (color:color-mix(in lab,red,red)){.shadow-black\/40{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 40%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-black\/60{--tw-shadow-color:#0009}@supports (color:color-mix(in lab,red,red)){.shadow-black\/60{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-black) 60%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-surface{--tw-ring-color:var(--color-surface)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\/steps\:invisible:is(:where(.group\/steps):is([open],:popover-open,:open) *){visibility:hidden}.group-open\/steps\:rotate-90:is(:where(.group\/steps):is([open],:popover-open,:open) *),.group-open\/think\:rotate-90:is(:where(.group\/think):is([open],:popover-open,:open) *){rotate:90deg}.group-open\/tool\:invisible:is(:where(.group\/tool):is([open],:popover-open,:open) *){visibility:hidden}.group-open\/tool\:rotate-90:is(:where(.group\/tool):is([open],:popover-open,:open) *){rotate:90deg}.peer-focus-visible\:outline-2:is(:where(.peer):focus-visible~*){outline-style:var(--tw-outline-style);outline-width:2px}.peer-focus-visible\:outline-offset-2:is(:where(.peer):focus-visible~*){outline-offset:2px}.peer-focus-visible\:outline-accent:is(:where(.peer):focus-visible~*){outline-color:var(--color-accent)}.placeholder\:font-sans::placeholder{font-family:var(--font-sans)}.placeholder\:text-faint::placeholder{color:var(--color-faint)}@media(hover:hover){.hover\:bg-surface-2:hover{background-color:var(--color-surface-2)}.hover\:text-text:hover{color:var(--color-text)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:border-accent\/50:focus{border-color:light-dark(#6757d980,#8b7dff80)}@supports (color:color-mix(in lab,red,red)){.focus\:border-accent\/50:focus{border-color:color-mix(in oklab,var(--color-accent) 50%,transparent)}}.focus\:border-accent\/60:focus{border-color:light-dark(#6757d999,#8b7dff99)}@supports (color:color-mix(in lab,red,red)){.focus\:border-accent\/60:focus{border-color:color-mix(in oklab,var(--color-accent) 60%,transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:-outline-offset-2:focus-visible{outline-offset:-2px}.focus-visible\:outline-accent:focus-visible{outline-color:var(--color-accent)}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-\[0\.97\]:active{scale:.97}.active\:scale-\[0\.985\]:active{scale:.985}.active\:bg-bg\/70:active{background-color:light-dark(#f5f6f8b3,#0a0b0eb3)}@supports (color:color-mix(in lab,red,red)){.active\:bg-bg\/70:active{background-color:color-mix(in oklab,var(--color-bg) 70%,transparent)}}.active\:bg-black\/70:active{background-color:#000000b3}@supports (color:color-mix(in lab,red,red)){.active\:bg-black\/70:active{background-color:color-mix(in oklab,var(--color-black) 70%,transparent)}}.active\:bg-cold-cache\/20:active{background-color:light-dark(#90611f33,#d6a85f33)}@supports (color:color-mix(in lab,red,red)){.active\:bg-cold-cache\/20:active{background-color:color-mix(in oklab,var(--color-cold-cache) 20%,transparent)}}.active\:bg-surface:active{background-color:var(--color-surface)}.active\:bg-surface-2:active{background-color:var(--color-surface-2)}.active\:text-text:active{color:var(--color-text)}.active\:opacity-80:active{opacity:.8}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:invisible:disabled{visibility:hidden}.disabled\:cursor-default:disabled{cursor:default}.disabled\:bg-surface-2:disabled{background-color:var(--color-surface-2)}.disabled\:text-faint:disabled{color:var(--color-faint)}.disabled\:text-faint\/40:disabled{color:light-dark(#9499a466,#5c616d66)}@supports (color:color-mix(in lab,red,red)){.disabled\:text-faint\/40:disabled{color:color-mix(in oklab,var(--color-faint) 40%,transparent)}}.disabled\:opacity-25:disabled{opacity:.25}.disabled\:opacity-35:disabled{opacity:.35}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-60:disabled{opacity:.6}.has-\[textarea\:focus\]\:border-accent\/60:has(:is(textarea:focus)){border-color:light-dark(#6757d999,#8b7dff99)}@supports (color:color-mix(in lab,red,red)){.has-\[textarea\:focus\]\:border-accent\/60:has(:is(textarea:focus)){border-color:color-mix(in oklab,var(--color-accent) 60%,transparent)}}@media(prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:340px){.max-\[340px\]\:hidden{display:none}}@media(min-width:400px){.min-\[400px\]\:block{display:block}}@media(min-width:48rem){.md\:static{position:static}.md\:inset-0{inset:0}.md\:inset-6{inset:calc(var(--spacing) * 6)}.md\:inset-x-auto{inset-inline:auto}.md\:top-16{top:calc(var(--spacing) * 16)}.md\:left-1\/2{left:50%}.md\:z-auto{z-index:auto}.md\:m-auto{margin:auto}.md\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.md\:block{display:block}.md\:hidden{display:none}.md\:h-fit{height:fit-content}.md\:max-h-\[70vh\]{max-height:70vh}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:w-\[36rem\]{width:36rem}.md\:max-w-\[92vw\]{max-width:92vw}.md\:max-w-none{max-width:none}.md\:shrink-0{flex-shrink:0}.md\:-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:translate-x-0{--tw-translate-x:0;translate:var(--tw-translate-x) var(--tw-translate-y)}.md\:rounded-2xl{border-radius:var(--radius-2xl)}.md\:rounded-3xl{border-radius:var(--radius-3xl)}.md\:border{border-style:var(--tw-border-style);border-width:1px}.md\:border-border{border-color:var(--color-border)}.md\:border-border-soft{border-color:var(--color-border-soft)}.md\:shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.md\:transition-none{transition-property:none}}@media(min-width:64rem){.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:w-80{width:calc(var(--spacing) * 80)}.lg\:w-\[380px\]{width:380px}.lg\:shrink-0{flex-shrink:0}.lg\:border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.lg\:border-border-soft{border-color:var(--color-border-soft)}}@media(min-width:80rem){.xl\:w-\[460px\]{width:460px}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}@keyframes typing{0%,60%,to{opacity:.35;transform:none}30%{opacity:1;transform:translateY(-3px)}}@keyframes fade-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.fade-in{animation:.18s ease-out fade-in}@media(prefers-reduced-motion:reduce){.fade-in{opacity:1;animation:none}.typing-dot{opacity:.6;animation:none}}.pt-safe{padding-top:max(env(safe-area-inset-top),.5rem)}.pb-safe{padding-bottom:max(env(safe-area-inset-bottom),.5rem)}html[data-keyboard] .pb-safe{padding-bottom:.5rem}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@keyframes spin{to{transform:rotate(360deg)}}
|