clay-server 4.3.1-beta.5 → 4.4.0-beta.1

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.
@@ -0,0 +1,25 @@
1
+ // Provider IDs and capabilities are server-owned; clients cannot choose upstream URLs.
2
+ var providers = [
3
+ { id: 'openai', name: 'OpenAI', icon: '/codex-avatar.png', url: 'https://platform.openai.com/api-keys' },
4
+ { id: 'elevenlabs', name: 'ElevenLabs', icon: '/icons/speech/elevenlabs.svg', url: 'https://elevenlabs.io/app/settings/api-keys' },
5
+ { id: 'soniox', name: 'Soniox', icon: '/icons/speech/soniox.png', url: 'https://console.soniox.com/' },
6
+ { id: 'groq', name: 'Groq', icon: '/icons/speech/groq.svg', url: 'https://console.groq.com/keys' },
7
+ { id: 'deepgram', name: 'Deepgram', icon: '/icons/speech/deepgram.svg', url: 'https://console.deepgram.com/' }
8
+ ];
9
+ var models = [
10
+ ['openai-live', 'openai', 'GPT Live Transcribe', 'live', 'gpt-live-transcribe', 'Low-latency transcription with text appearing as you speak.'],
11
+ ['openai-transcribe', 'openai', 'GPT Transcribe', 'recorded', 'gpt-transcribe', 'Transcribes your recording after you stop.'],
12
+ ['openai-whisper', 'openai', 'Whisper', 'recorded', 'whisper-1', 'General-purpose multilingual transcription.'],
13
+ ['browser', 'browser', 'Browser speech', 'live', '', 'Uses your browser’s speech service. No API key required.'],
14
+ ['elevenlabs-live', 'elevenlabs', 'Scribe v2 Realtime', 'live', 'scribe_v2_realtime', 'Live multilingual dictation with partial text as you speak.'],
15
+ ['elevenlabs-file', 'elevenlabs', 'Scribe v2', 'recorded', 'scribe_v2', 'Multilingual transcription of completed recordings.'],
16
+ ['soniox-live', 'soniox', 'Soniox Realtime', 'live', 'stt-rt-v5', 'Live transcription with automatic language recognition.'],
17
+ ['soniox-file', 'soniox', 'Soniox Async', 'recorded', 'stt-async-v5', 'Asynchronous transcription of completed recordings.'],
18
+ ['groq-whisper', 'groq', 'Whisper Large v3', 'recorded', 'whisper-large-v3', 'Multilingual Whisper transcription hosted by Groq.'],
19
+ ['groq-turbo', 'groq', 'Whisper Turbo', 'recorded', 'whisper-large-v3-turbo', 'A faster Whisper variant hosted by Groq.'],
20
+ ['deepgram-live', 'deepgram', 'Nova-3', 'live', 'nova-3', 'Live transcription in your selected language.'],
21
+ ['deepgram-file', 'deepgram', 'Nova-3', 'recorded', 'nova-3', 'Recorded-audio transcription in your selected language.']
22
+ ].map(function (row) { return { id: row[0], provider: row[1], name: row[2], mode: row[3], model: row[4], description: row[5] }; });
23
+ function findModel(id) { return models.find(function (model) { return model.id === id; }); }
24
+ function findProvider(id) { return providers.find(function (provider) { return provider.id === id; }); }
25
+ module.exports = { models: models, providers: providers, findModel: findModel, findProvider: findProvider };
@@ -0,0 +1,102 @@
1
+ var providerError = require('./speech-transcribe').providerError;
2
+
3
+ // Interpolate the shared 16 kHz PCM stream to OpenAI's required 24 kHz,
4
+ // carrying sample position across chunks so chunk boundaries do not lose audio.
5
+ function pcmResampler() {
6
+ var count = 0; var tick = 0; var previous = 0;
7
+ return function (bytes, flush) {
8
+ var samples = [];
9
+ for (var at = 0; at < bytes.length; at += 2) {
10
+ var current = bytes.readInt16LE(at);
11
+ while (tick <= count * 3) {
12
+ var fraction = (tick - (count - 1) * 3) / 3;
13
+ samples.push(count ? Math.round(previous + (current - previous) * fraction) : current);
14
+ tick += 2;
15
+ }
16
+ previous = current; count++;
17
+ }
18
+ if (flush) while (tick < count * 3) { samples.push(previous); tick += 2; }
19
+ var output = Buffer.alloc(samples.length * 2);
20
+ samples.forEach(function (sample, index) { output.writeInt16LE(sample, index * 2); });
21
+ return output;
22
+ };
23
+ }
24
+
25
+ function openOpenAIStream(model, key, language, events, Socket) {
26
+ var socket = new Socket('wss://api.openai.com/v1/realtime?intent=transcription', {
27
+ headers: { Authorization: 'Bearer ' + key }, handshakeTimeout: 15000, maxPayload: 1024 * 1024
28
+ });
29
+ var ended = false; var ready = false; var finishing = false; var timer;
30
+ var partial = ''; var itemId = null; var committedId = null; var completed = new Map();
31
+ var bytesSent = 0; var resample = pcmResampler();
32
+ function send(message) { socket.send(JSON.stringify(message)); }
33
+ function fail(error) {
34
+ if (ended) return;
35
+ ended = true; clearTimeout(timer); socket.terminate(); events.error(error);
36
+ }
37
+ function done(text) {
38
+ if (ended) return;
39
+ ended = true; clearTimeout(timer); events.done(text.trim()); socket.close();
40
+ }
41
+ function failure(message) {
42
+ var code = message.error && message.error.code;
43
+ if (code === 'invalid_api_key') return providerError(401);
44
+ if (code === 'insufficient_quota' || code === 'rate_limit_exceeded') return providerError(429);
45
+ if (code === 'model_not_found') return new Error('GPT Live Transcribe is unavailable for this OpenAI project. Check model access.');
46
+ return new Error('OpenAI could not complete live transcription. Check your API key, model access and connection.');
47
+ }
48
+ function append(bytes) {
49
+ if (bytes.length) send({ type: 'input_audio_buffer.append', audio: bytes.toString('base64') });
50
+ }
51
+ socket.on('open', function () {
52
+ if (ended) return;
53
+ send({ type: 'session.update', session: { type: 'transcription', audio: { input: {
54
+ format: { type: 'audio/pcm', rate: 24000 },
55
+ transcription: { model: model.model, languages: [language.toLowerCase().split('-')[0]] },
56
+ turn_detection: null
57
+ } } } });
58
+ });
59
+ socket.on('message', function (raw) {
60
+ if (ended) return;
61
+ try {
62
+ var message = JSON.parse(raw.toString());
63
+ if (message.type === 'error' || message.type === 'conversation.item.input_audio_transcription.failed') { fail(failure(message)); return; }
64
+ if (message.type === 'session.updated' && !ready) { ready = true; events.ready(); }
65
+ else if (message.type === 'conversation.item.input_audio_transcription.delta') {
66
+ if (!itemId) itemId = message.item_id;
67
+ if (message.item_id !== itemId) return;
68
+ partial += message.delta || ''; events.text(partial);
69
+ } else if (message.type === 'input_audio_buffer.committed') {
70
+ committedId = message.item_id;
71
+ if (finishing && completed.has(committedId)) done(completed.get(committedId));
72
+ } else if (message.type === 'conversation.item.input_audio_transcription.completed') {
73
+ if (!itemId) itemId = message.item_id;
74
+ if (message.item_id !== itemId) return;
75
+ completed.set(message.item_id, message.transcript || '');
76
+ if (finishing && message.item_id === committedId) done(message.transcript || '');
77
+ }
78
+ } catch (error) { fail(providerError(502)); }
79
+ });
80
+ socket.on('error', function () { fail(new Error('Could not connect to OpenAI live transcription. Please try again.')); });
81
+ socket.on('unexpected-response', function (request, response) { response.resume(); fail(providerError(response.statusCode)); });
82
+ socket.on('close', function () { if (!ended) fail(new Error('The OpenAI live connection ended unexpectedly. Your draft has been preserved.')); });
83
+ return {
84
+ audio: function (bytes) {
85
+ if (ended || finishing || !ready || socket.readyState !== 1) return;
86
+ if (socket.bufferedAmount > 1024 * 1024) { fail(new Error('The connection is too slow for live transcription.')); return; }
87
+ bytesSent += bytes.length; append(resample(bytes, false));
88
+ },
89
+ finish: function () {
90
+ if (ended || finishing || !ready) return;
91
+ finishing = true;
92
+ if (!bytesSent) { done(''); return; }
93
+ // OpenAI requires at least 100 ms per commit; pad only very short captures.
94
+ if (bytesSent < 3200) append(resample(Buffer.alloc(3200 - bytesSent), false));
95
+ append(resample(Buffer.alloc(0), true));
96
+ timer = setTimeout(function () { fail(new Error('OpenAI transcription timed out. Your draft has been preserved.')); }, 20000);
97
+ send({ type: 'input_audio_buffer.commit' });
98
+ },
99
+ abort: function () { ended = true; clearTimeout(timer); socket.terminate(); }
100
+ };
101
+ }
102
+ module.exports = { openOpenAIStream: openOpenAIStream, pcmResampler: pcmResampler };
@@ -0,0 +1,71 @@
1
+ var fs = require('fs');
2
+ var path = require('path');
3
+ var crypto = require('crypto');
4
+ var catalog = require('./speech-catalog');
5
+
6
+ function attachSpeechStore(root) {
7
+ var directory = path.join(root, 'speech');
8
+ function prepare() { fs.mkdirSync(directory, { recursive: true, mode: 0o700 }); }
9
+ function filename(owner) {
10
+ if (typeof owner !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(owner)) throw new Error('Invalid owner');
11
+ return path.join(directory, owner + '.json');
12
+ }
13
+ function read(owner) {
14
+ try { return JSON.parse(fs.readFileSync(filename(owner), 'utf8')); }
15
+ catch (error) { if (error.code === 'ENOENT') return { selected: 'browser', keys: {} }; throw error; }
16
+ }
17
+ function masterKey() {
18
+ prepare();
19
+ var file = path.join(directory, '.key');
20
+ try { fs.writeFileSync(file, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 }); }
21
+ catch (error) { if (error.code !== 'EEXIST') throw error; }
22
+ return fs.readFileSync(file);
23
+ }
24
+ function encrypt(owner, provider, text) {
25
+ var iv = crypto.randomBytes(12);
26
+ var cipher = crypto.createCipheriv('aes-256-gcm', masterKey(), iv);
27
+ cipher.setAAD(Buffer.from(owner + ':' + provider));
28
+ var data = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
29
+ return [iv, cipher.getAuthTag(), data].map(function (part) { return part.toString('base64'); }).join('.');
30
+ }
31
+ function key(owner, provider) {
32
+ var value = (read(owner).keys || {})[provider];
33
+ if (!value) return null;
34
+ var parts = value.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
35
+ var cipher = crypto.createDecipheriv('aes-256-gcm', masterKey(), parts[0]);
36
+ cipher.setAAD(Buffer.from(owner + ':' + provider)); cipher.setAuthTag(parts[1]);
37
+ return Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8');
38
+ }
39
+ function view(owner) {
40
+ var data = read(owner);
41
+ return { selected: data.selected || 'browser', language: data.language || 'en-US', configured: Object.keys(data.keys || {}), models: catalog.models, providers: catalog.providers };
42
+ }
43
+ function update(owner, change) {
44
+ var data = read(owner); data.keys = data.keys || {};
45
+ if (change.provider !== undefined) {
46
+ if (!catalog.findProvider(change.provider)) throw new Error('Unknown speech provider');
47
+ if (change.key === null) delete data.keys[change.provider];
48
+ else {
49
+ if (typeof change.key !== 'string' || !change.key.trim() || change.key.length > 4096 || /[\r\n]/.test(change.key)) throw new Error('Enter a valid API key');
50
+ data.keys[change.provider] = encrypt(owner, change.provider, change.key.trim());
51
+ }
52
+ }
53
+ if (change.language !== undefined) {
54
+ if (typeof change.language !== 'string' || !/^[a-z]{2,3}(?:-[a-zA-Z]{2,8})?$/.test(change.language)) throw new Error('Invalid speech language');
55
+ data.language = change.language;
56
+ }
57
+ if (change.selected !== undefined) {
58
+ var model = catalog.findModel(change.selected);
59
+ if (!model || (model.provider !== 'browser' && !data.keys[model.provider])) throw new Error('Connect the provider before selecting this model');
60
+ data.selected = model.id;
61
+ }
62
+ // Keep selection after key removal so there is no silent provider fallback.
63
+ prepare();
64
+ var file = filename(owner); var temporary = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
65
+ try { fs.writeFileSync(temporary, JSON.stringify(data), { mode: 0o600 }); fs.renameSync(temporary, file); }
66
+ finally { try { fs.unlinkSync(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
67
+ return view(owner);
68
+ }
69
+ return { view: view, update: update, key: key };
70
+ }
71
+ module.exports = { attachSpeechStore: attachSpeechStore };
@@ -0,0 +1,108 @@
1
+ var WebSocket = require('ws');
2
+ var providerError = require('./speech-transcribe').providerError;
3
+
4
+ function streamError(model, message) {
5
+ var status = Number(message.error_code) || 502;
6
+ if (model.provider !== 'soniox') return providerError(status);
7
+ var reasons = {
8
+ unauthenticated: 'Soniox rejected the API key. Check the key in Voice input settings.',
9
+ permission_denied: 'The Soniox key does not have permission for live transcription. Check its permissions in Soniox Console.',
10
+ organization_balance_exhausted: 'Your Soniox credit balance is empty. Add credits in Soniox Console to use voice input.',
11
+ organization_monthly_budget_exhausted: 'Your Soniox organization has reached its monthly budget. Check its billing limits.',
12
+ project_monthly_budget_exhausted: 'Your Soniox project has reached its monthly budget. Check its billing limits.',
13
+ model_not_available: 'The selected Soniox model is unavailable for your project or region. Check model access in Soniox Console.',
14
+ invalid_request: 'Soniox rejected the live transcription configuration. Try another voice model.',
15
+ limit_exceeded: 'Your Soniox request limit was reached. Wait for other transcriptions to finish and try again.',
16
+ request_timeout: 'Soniox timed out waiting for microphone audio. Check your microphone and connection, then try again.',
17
+ service_unavailable: 'Soniox is temporarily unavailable. Please try again later.'
18
+ };
19
+ var reason = Object.prototype.hasOwnProperty.call(reasons, message.error_type) ? reasons[message.error_type] : null;
20
+ if (!reason && status === 402) reason = 'Soniox requires available credits and budget. Check billing in Soniox Console.';
21
+ return reason ? new Error(reason) : providerError(status);
22
+ }
23
+
24
+ function openSpeechStream(model, key, language, events, Socket) {
25
+ Socket = Socket || WebSocket;
26
+ if (model.provider === 'openai') return require('./speech-openai-stream').openOpenAIStream(model, key, language, events, Socket);
27
+ var code = language.split('-')[0];
28
+ var url; var options = { handshakeTimeout: 15000, maxPayload: 1024 * 1024 };
29
+ if (model.provider === 'soniox') url = 'wss://stt-rt.soniox.com/transcribe-websocket';
30
+ else if (model.provider === 'elevenlabs') {
31
+ url = 'wss://api.elevenlabs.io/v1/speech-to-text/realtime?model_id=scribe_v2_realtime&audio_format=pcm_16000&commit_strategy=manual&language_code=' + encodeURIComponent(code);
32
+ options.headers = { 'xi-api-key': key };
33
+ } else if (model.provider === 'deepgram') {
34
+ url = 'wss://api.deepgram.com/v1/listen?model=nova-3&encoding=linear16&sample_rate=16000&channels=1&interim_results=true&smart_format=true&language=' + encodeURIComponent(code);
35
+ options.headers = { Authorization: 'Token ' + key };
36
+ } else throw new Error('This model does not support live transcription');
37
+ var socket = new Socket(url, options);
38
+ var finalText = ''; var ended = false; var finishing = false; var finishTimer;
39
+ function diagnostic(stage, details) { if (events.diagnostic) events.diagnostic(stage, details); }
40
+ function fail(error) { if (!ended) { ended = true; clearTimeout(finishTimer); socket.terminate(); events.error(error); } }
41
+ function done() { if (!ended) { ended = true; clearTimeout(finishTimer); events.done(finalText.trim()); socket.close(); } }
42
+ function emit(partial) { events.text((finalText + partial).trim()); }
43
+ socket.on('open', function () {
44
+ if (ended) return;
45
+ diagnostic('upstream-websocket-open');
46
+ if (model.provider === 'soniox') socket.send(JSON.stringify({ api_key: key, model: model.model,
47
+ audio_format: 'pcm_s16le', sample_rate: 16000, num_channels: 1, language_hints: [code], enable_endpoint_detection: true }));
48
+ if (model.provider === 'soniox') diagnostic('soniox-config-sent');
49
+ if (model.provider !== 'elevenlabs') events.ready();
50
+ });
51
+ socket.on('message', function (raw) {
52
+ if (ended) return;
53
+ try {
54
+ var message = JSON.parse(raw.toString());
55
+ if (message.error || message.error_code || message.error_type || message.type === 'Error' || /error|rate_limited|quota_exceeded/.test(message.message_type || '')) {
56
+ diagnostic('upstream-error-response', { status: Number(message.error_code) || 502 });
57
+ fail(streamError(model, message)); return;
58
+ }
59
+ if (model.provider === 'soniox') {
60
+ var partial = '';
61
+ (message.tokens || []).forEach(function (token) {
62
+ if (/^<.*>$/.test(token.text)) return;
63
+ if (token.is_final) finalText += token.text; else partial += token.text;
64
+ });
65
+ emit(partial);
66
+ if (message.finished) done();
67
+ } else if (model.provider === 'elevenlabs') {
68
+ if (message.message_type === 'session_started') events.ready();
69
+ if (message.message_type === 'partial_transcript') emit(message.text || '');
70
+ if (message.message_type === 'committed_transcript') {
71
+ finalText += (message.text || '') + ' '; emit('');
72
+ if (finishing) done();
73
+ }
74
+ } else if (message.type === 'Results') {
75
+ var text = message.channel.alternatives[0].transcript || '';
76
+ if (message.is_final) { finalText += text + (text ? ' ' : ''); emit(''); }
77
+ else emit(text);
78
+ } else if (message.type === 'Metadata' && finishing) done();
79
+ } catch (error) { fail(providerError(502)); }
80
+ });
81
+ socket.on('error', function () { diagnostic('upstream-transport-error'); fail(providerError(502)); });
82
+ socket.on('unexpected-response', function (request, response) { diagnostic('upstream-handshake-rejected', { status: response.statusCode }); response.resume(); fail(providerError(response.statusCode)); });
83
+ socket.on('close', function (code) {
84
+ diagnostic('upstream-websocket-close', { status: code });
85
+ if (!ended) {
86
+ if (finishing && model.provider === 'deepgram') done();
87
+ else fail(new Error('The live connection ended unexpectedly. Your draft has been preserved.'));
88
+ }
89
+ });
90
+ return {
91
+ audio: function (bytes) {
92
+ if (ended || finishing || socket.readyState !== 1) return;
93
+ if (socket.bufferedAmount > 1024 * 1024) { fail(new Error('The connection is too slow for live transcription.')); return; }
94
+ if (model.provider === 'elevenlabs') socket.send(JSON.stringify({ message_type: 'input_audio_chunk', audio_base_64: bytes.toString('base64'), sample_rate: 16000 }));
95
+ else socket.send(bytes);
96
+ },
97
+ finish: function () {
98
+ if (ended || finishing) return;
99
+ finishing = true;
100
+ finishTimer = setTimeout(function () { fail(new Error('Transcription timed out. Your draft has been preserved.')); }, 20000);
101
+ if (model.provider === 'soniox') socket.send('');
102
+ else if (model.provider === 'deepgram') socket.send(JSON.stringify({ type: 'CloseStream' }));
103
+ else socket.send(JSON.stringify({ message_type: 'input_audio_chunk', audio_base_64: '', commit: true, sample_rate: 16000 }));
104
+ },
105
+ abort: function () { ended = true; clearTimeout(finishTimer); socket.terminate(); }
106
+ };
107
+ }
108
+ module.exports = { openSpeechStream: openSpeechStream };
@@ -0,0 +1,63 @@
1
+ // Recorded audio adapters. No provider response body (which may contain private data) is logged.
2
+ function providerError(status) {
3
+ var error = new Error(status === 401 || status === 403 ? 'The provider rejected your API key. Check its permissions in Voice input settings.' :
4
+ status === 429 ? 'The provider limit was reached. Check your quota or try again later.' : 'The transcription provider could not complete the request. Please try again.');
5
+ error.status = 502;
6
+ return error;
7
+ }
8
+ async function transcribe(model, key, audio, language, signal, fetcher) {
9
+ fetcher = fetcher || fetch;
10
+ var languageCode = (language || 'en-US').split('-')[0];
11
+ async function request(url, options) {
12
+ var response = await fetcher(url, Object.assign({}, options, { signal: signal }));
13
+ if (!response.ok) throw providerError(response.status);
14
+ return response;
15
+ }
16
+ function form() { var data = new FormData(); data.append('file', new Blob([audio], { type: 'audio/wav' }), 'voice.wav'); return data; }
17
+ if (model.provider === 'openai' || model.provider === 'groq') {
18
+ var data = form(); data.append('model', model.model); data.append(model.model === 'gpt-transcribe' ? 'languages[]' : 'language', languageCode);
19
+ var endpoint = model.provider === 'openai' ? 'https://api.openai.com/v1/audio/transcriptions' : 'https://api.groq.com/openai/v1/audio/transcriptions';
20
+ var response = await request(endpoint, { method: 'POST', headers: { Authorization: 'Bearer ' + key }, body: data });
21
+ return (await response.json()).text;
22
+ }
23
+ if (model.provider === 'elevenlabs') {
24
+ var data = form(); data.append('model_id', model.model); data.append('language_code', languageCode);
25
+ data.append('tag_audio_events', 'false'); data.append('diarize', 'false');
26
+ var response = await request('https://api.elevenlabs.io/v1/speech-to-text', { method: 'POST', headers: { 'xi-api-key': key }, body: data });
27
+ return (await response.json()).text;
28
+ }
29
+ if (model.provider === 'deepgram') {
30
+ var url = 'https://api.deepgram.com/v1/listen?model=nova-3&smart_format=true&language=' + encodeURIComponent(languageCode);
31
+ var response = await request(url, { method: 'POST', headers: { Authorization: 'Token ' + key, 'Content-Type': 'audio/wav' }, body: audio });
32
+ return (await response.json()).results.channels[0].alternatives[0].transcript;
33
+ }
34
+ if (model.provider !== 'soniox') throw new Error('Unsupported speech provider');
35
+ var base = 'https://api.soniox.com/v1';
36
+ var headers = { Authorization: 'Bearer ' + key };
37
+ var fileId; var transcriptionId;
38
+ try {
39
+ var uploaded = await request(base + '/files', { method: 'POST', headers: headers, body: form() });
40
+ fileId = (await uploaded.json()).id;
41
+ var created = await request(base + '/transcriptions', { method: 'POST', headers: Object.assign({}, headers, { 'Content-Type': 'application/json' }),
42
+ body: JSON.stringify({ model: model.model, file_id: fileId, language_hints: [languageCode] }) });
43
+ transcriptionId = (await created.json()).id;
44
+ for (;;) {
45
+ var result = await request(base + '/transcriptions/' + encodeURIComponent(transcriptionId), { headers: headers });
46
+ var state = await result.json();
47
+ if (state.status === 'error') throw providerError(502);
48
+ if (state.status === 'completed') break;
49
+ await require('timers/promises').setTimeout(750, undefined, { signal: signal });
50
+ }
51
+ var transcript = await request(base + '/transcriptions/' + encodeURIComponent(transcriptionId) + '/transcript', { headers: headers });
52
+ return (await transcript.json()).text;
53
+ } finally {
54
+ // Cleanup uses its own bounded signal, including when the user cancels the transcription.
55
+ var paths = [];
56
+ if (transcriptionId) paths.push('/transcriptions/' + encodeURIComponent(transcriptionId));
57
+ if (fileId) paths.push('/files/' + encodeURIComponent(fileId));
58
+ await Promise.allSettled(paths.map(function (path) {
59
+ return fetcher(base + path, { method: 'DELETE', headers: headers, signal: AbortSignal.timeout(5000) });
60
+ }));
61
+ }
62
+ }
63
+ module.exports = { transcribe: transcribe, providerError: providerError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "4.3.1-beta.5",
3
+ "version": "4.4.0-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"