@hitch42/applet 0.1.1-beta.7 → 0.1.1-beta.8
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/dist/_internal/server.d.ts +14 -1
- package/dist/_internal/server.js +23 -74
- package/dist/applet.js +22 -11
- package/dist/build-CFCKKMn7.js +2 -0
- package/dist/{build-DauirTcY.js → build-DANx9Hgg.js} +1 -1
- package/dist/{ci-gPrwo6C0.js → ci-5qWl7lT3.js} +1 -1
- package/dist/client.d.ts +2 -2
- package/dist/{contract-DB9QQhD9.d.ts → contract-rP_eV9PK.d.ts} +1 -1
- package/dist/{deploy-CqrtNJ1x.js → deploy-MQWLG2EA.js} +3 -3
- package/dist/email-DBTVhNB_.js +62 -0
- package/dist/{generate-BIifT6PK.js → generate-BxYHdoAQ.js} +9 -8
- package/dist/generate-CQ07Hdpo.js +2 -0
- package/dist/index.d.ts +114 -3
- package/dist/index.js +141 -26
- package/dist/{init-Wu6XmyVc.js → init-jDEgYIXg.js} +5 -5
- package/dist/{load-config-BknBkdRs.js → load-config-BYSxDVAl.js} +2 -2
- package/dist/{login-iT9MRuaw.js → login-C7RKmmTe.js} +1 -1
- package/dist/{logs-B-qZGXSK.js → logs-DgjgAxj2.js} +3 -3
- package/dist/{manifest-Bd04cWhu.js → manifest-Bp87Uz0a.js} +3 -0
- package/dist/{manifest-C489nYUy.d.ts → manifest-DD_SGkS6.d.ts} +3 -0
- package/dist/me-BW7Paary.js +32 -0
- package/dist/{project-C_rtpjp8.js → project-CbYPtpPq.js} +6 -5
- package/dist/prompt-ChS-grYO.js +29 -0
- package/dist/schedules-dhU1yyFr.js +42 -0
- package/dist/server.d.ts +2 -2
- package/dist/telephony-protocol-DRgiYjqQ.d.ts +74 -0
- package/dist/telephony.d.ts +77 -0
- package/dist/telephony.js +607 -0
- package/dist/{variables-DcwMeIau.js → variables-Bwnr7oxf.js} +3 -28
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +457 -22
- package/dist/workflow-engine-MogejNfF.js +117 -0
- package/dist/{workspace-B6lenAS5.js → workspace-XVtM12ZC.js} +5 -2
- package/package.json +6 -1
- package/scaffold/.agents/skills/hitch-applets/SKILL.md +24 -18
- package/scaffold/.agents/skills/hitch-applets/references/email-tracking.md +49 -0
- package/scaffold/.agents/skills/hitch-applets/references/files.md +3 -3
- package/scaffold/.agents/skills/hitch-applets/references/manifest.md +4 -2
- package/scaffold/.agents/skills/hitch-applets/references/performance.md +1 -1
- package/scaffold/.agents/skills/hitch-applets/references/records.md +4 -3
- package/scaffold/.agents/skills/hitch-applets/references/schedules.md +7 -2
- package/scaffold/.agents/skills/hitch-applets/references/server.md +0 -3
- package/scaffold/.agents/skills/hitch-applets/references/workflows.md +11 -3
- package/scaffold/.agents/skills/hitch-cli/SKILL.md +20 -20
- package/scaffold/.agents/skills/hitch-data/SKILL.md +1 -1
- package/scaffold/.agents/skills/hitch-data/references/limits.md +10 -10
- package/scaffold/.agents/skills/hitch-theory/SKILL.md +150 -0
- package/dist/build-DhNDp7ji.js +0 -2
- package/dist/generate-DSk8ihsC.js +0 -2
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
//#region src/telephony/api.ts
|
|
3
|
+
var TelephonyApiError = class extends Error {
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
constructor(message, code, status) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = "TelephonyApiError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const createTelephonyApi = (baseUrl = location.origin) => {
|
|
14
|
+
const request = async (path, init) => {
|
|
15
|
+
const headers = new Headers(init?.headers);
|
|
16
|
+
if (init?.body) headers.set("content-type", "application/json");
|
|
17
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/telephony/${path}`, {
|
|
18
|
+
credentials: "include",
|
|
19
|
+
...init,
|
|
20
|
+
headers
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const body = await response.json().catch(() => null);
|
|
24
|
+
const error = body && typeof body === "object" ? body : {};
|
|
25
|
+
const code = "code" in error && typeof error.code === "string" ? error.code : "request_failed";
|
|
26
|
+
throw new TelephonyApiError("message" in error && typeof error.message === "string" ? error.message : response.statusText, code, response.status);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
token: (deviceKey) => request("voice/token", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: JSON.stringify({ deviceKey })
|
|
34
|
+
}),
|
|
35
|
+
startCall: (numberId, to) => request("calls", {
|
|
36
|
+
method: "POST",
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
numberId,
|
|
39
|
+
to
|
|
40
|
+
})
|
|
41
|
+
}),
|
|
42
|
+
getCall: (callId) => request(`calls/${encodeURIComponent(callId)}`),
|
|
43
|
+
uploadUrl: (callId) => request(`calls/${encodeURIComponent(callId)}/recording`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: "{}"
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/telephony/recorder.ts
|
|
51
|
+
const DB_NAME = "hitch-telephony";
|
|
52
|
+
const STORE = "recordings";
|
|
53
|
+
const openDatabase = () => new Promise((resolve, reject) => {
|
|
54
|
+
const request = indexedDB.open(DB_NAME, 1);
|
|
55
|
+
request.onupgradeneeded = () => request.result.createObjectStore(STORE, { keyPath: "key" });
|
|
56
|
+
request.onsuccess = () => resolve(request.result);
|
|
57
|
+
request.onerror = () => reject(request.error);
|
|
58
|
+
});
|
|
59
|
+
const withStore = async (mode, action) => {
|
|
60
|
+
const db = await openDatabase();
|
|
61
|
+
try {
|
|
62
|
+
return await new Promise((resolve, reject) => {
|
|
63
|
+
const transaction = db.transaction(STORE, mode);
|
|
64
|
+
const request = action(transaction.objectStore(STORE));
|
|
65
|
+
request.onsuccess = () => resolve(request.result);
|
|
66
|
+
request.onerror = () => reject(request.error);
|
|
67
|
+
transaction.onerror = () => reject(transaction.error);
|
|
68
|
+
});
|
|
69
|
+
} finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const allItems = () => withStore("readonly", (store) => store.getAll());
|
|
74
|
+
const putItem = (item) => withStore("readwrite", (store) => store.put(item));
|
|
75
|
+
const deleteItem = (key) => withStore("readwrite", (store) => store.delete(key));
|
|
76
|
+
const uploadRecording = async (callId, api) => {
|
|
77
|
+
const items = (await allItems()).filter((item) => item.callId === callId);
|
|
78
|
+
const chunks = items.filter((item) => item.blob).sort((a, b) => a.index - b.index);
|
|
79
|
+
const forget = () => Promise.all(items.map((item) => deleteItem(item.key)));
|
|
80
|
+
if (chunks.length === 0) {
|
|
81
|
+
await forget();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const upload = await api.uploadUrl(callId).catch(async (error) => {
|
|
85
|
+
if (error instanceof TelephonyApiError && error.code === "recording_exists") return null;
|
|
86
|
+
throw error;
|
|
87
|
+
});
|
|
88
|
+
if (!upload) {
|
|
89
|
+
await forget();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const { uploadUrl } = upload;
|
|
93
|
+
const response = await fetch(uploadUrl, {
|
|
94
|
+
method: "PUT",
|
|
95
|
+
headers: { "content-type": "audio/webm" },
|
|
96
|
+
body: new Blob(chunks.flatMap((item) => item.blob ? [item.blob] : []), { type: "audio/webm" })
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) throw new Error(`Recording upload failed: ${response.status}`);
|
|
99
|
+
await forget();
|
|
100
|
+
};
|
|
101
|
+
const uploadPendingRecordings = async (api) => {
|
|
102
|
+
await (await allItems()).filter((item) => item.index === -1).sort((a, b) => a.createdAt - b.createdAt).reduce(async (previous, item) => {
|
|
103
|
+
await previous;
|
|
104
|
+
try {
|
|
105
|
+
await uploadRecording(item.callId, api);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Recording upload failed", error);
|
|
108
|
+
}
|
|
109
|
+
}, Promise.resolve());
|
|
110
|
+
};
|
|
111
|
+
const startRecording = async (callId, call, api, onError) => {
|
|
112
|
+
const context = new AudioContext();
|
|
113
|
+
try {
|
|
114
|
+
await context.resume();
|
|
115
|
+
const merger = context.createChannelMerger(2);
|
|
116
|
+
context.createMediaStreamSource(call.localStream).connect(merger, 0, 0);
|
|
117
|
+
context.createMediaStreamSource(call.remoteStream).connect(merger, 0, 1);
|
|
118
|
+
const destination = context.createMediaStreamDestination();
|
|
119
|
+
destination.channelCount = 2;
|
|
120
|
+
merger.connect(destination);
|
|
121
|
+
const recorder = new MediaRecorder(destination.stream, {
|
|
122
|
+
mimeType: "audio/webm;codecs=opus",
|
|
123
|
+
audioBitsPerSecond: 32e3
|
|
124
|
+
});
|
|
125
|
+
await putItem({
|
|
126
|
+
key: `${callId}:pending`,
|
|
127
|
+
callId,
|
|
128
|
+
index: -1,
|
|
129
|
+
createdAt: Date.now()
|
|
130
|
+
});
|
|
131
|
+
let index = 0;
|
|
132
|
+
let writes = Promise.resolve();
|
|
133
|
+
recorder.ondataavailable = (event) => {
|
|
134
|
+
if (event.data.size === 0) return;
|
|
135
|
+
const item = {
|
|
136
|
+
key: `${callId}:${index}`,
|
|
137
|
+
callId,
|
|
138
|
+
index,
|
|
139
|
+
createdAt: Date.now(),
|
|
140
|
+
blob: event.data
|
|
141
|
+
};
|
|
142
|
+
index += 1;
|
|
143
|
+
writes = writes.then(() => putItem(item)).then(() => void 0);
|
|
144
|
+
writes.catch(onError);
|
|
145
|
+
};
|
|
146
|
+
recorder.onerror = onError;
|
|
147
|
+
recorder.start(5e3);
|
|
148
|
+
return async () => {
|
|
149
|
+
await new Promise((resolve) => {
|
|
150
|
+
recorder.onstop = () => resolve();
|
|
151
|
+
recorder.stop();
|
|
152
|
+
});
|
|
153
|
+
await writes;
|
|
154
|
+
await context.close();
|
|
155
|
+
await uploadRecording(callId, api);
|
|
156
|
+
};
|
|
157
|
+
} catch (error) {
|
|
158
|
+
await context.close();
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/telephony/phone.ts
|
|
164
|
+
const DEVICE_KEY = "hitch:telephony:device";
|
|
165
|
+
let singleton;
|
|
166
|
+
var Phone = class {
|
|
167
|
+
state = {
|
|
168
|
+
status: "off",
|
|
169
|
+
error: null,
|
|
170
|
+
active: null
|
|
171
|
+
};
|
|
172
|
+
listeners = /* @__PURE__ */ new Set();
|
|
173
|
+
api;
|
|
174
|
+
client = null;
|
|
175
|
+
currentCall = null;
|
|
176
|
+
connectPromise = null;
|
|
177
|
+
releaseLock = null;
|
|
178
|
+
lockAbortController = null;
|
|
179
|
+
channel = null;
|
|
180
|
+
ownsPhone = false;
|
|
181
|
+
connectResolve = null;
|
|
182
|
+
pendingCommands = /* @__PURE__ */ new Map();
|
|
183
|
+
pollTimer = null;
|
|
184
|
+
stopRecording = null;
|
|
185
|
+
recordingPromise = null;
|
|
186
|
+
audio = null;
|
|
187
|
+
firstLogin = true;
|
|
188
|
+
callStarting = false;
|
|
189
|
+
constructor(options = {}) {
|
|
190
|
+
this.api = createTelephonyApi(options.baseUrl);
|
|
191
|
+
}
|
|
192
|
+
subscribe = (listener) => {
|
|
193
|
+
this.listeners.add(listener);
|
|
194
|
+
return () => {
|
|
195
|
+
this.listeners.delete(listener);
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
getSnapshot = () => this.state;
|
|
199
|
+
update(patch) {
|
|
200
|
+
const wasActive = this.state.active !== null;
|
|
201
|
+
this.state = {
|
|
202
|
+
...this.state,
|
|
203
|
+
...patch
|
|
204
|
+
};
|
|
205
|
+
this.listeners.forEach((listener) => listener());
|
|
206
|
+
if (this.ownsPhone) {
|
|
207
|
+
this.channel?.postMessage({
|
|
208
|
+
type: "state",
|
|
209
|
+
state: this.state
|
|
210
|
+
});
|
|
211
|
+
if (wasActive !== (this.state.active !== null) && window.parent !== window) window.parent.postMessage({
|
|
212
|
+
type: "hitch:call-active",
|
|
213
|
+
active: !wasActive
|
|
214
|
+
}, "*");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
get active() {
|
|
218
|
+
if (!this.state.active) return null;
|
|
219
|
+
if (!this.ownsPhone) return {
|
|
220
|
+
hangup: () => this.sendCommand("hangup"),
|
|
221
|
+
mute: () => void this.sendCommand("mute").catch(console.error),
|
|
222
|
+
unmute: () => void this.sendCommand("unmute").catch(console.error),
|
|
223
|
+
hold: () => this.sendCommand("hold"),
|
|
224
|
+
unhold: () => this.sendCommand("unhold"),
|
|
225
|
+
dtmf: (digit) => void this.sendCommand("dtmf", digit).catch(console.error)
|
|
226
|
+
};
|
|
227
|
+
if (!this.currentCall) return null;
|
|
228
|
+
const call = this.currentCall;
|
|
229
|
+
return {
|
|
230
|
+
hangup: () => call.hangup(),
|
|
231
|
+
mute: () => {
|
|
232
|
+
call.muteAudio();
|
|
233
|
+
this.update({ active: {
|
|
234
|
+
...this.state.active,
|
|
235
|
+
muted: true
|
|
236
|
+
} });
|
|
237
|
+
},
|
|
238
|
+
unmute: () => {
|
|
239
|
+
call.unmuteAudio();
|
|
240
|
+
this.update({ active: {
|
|
241
|
+
...this.state.active,
|
|
242
|
+
muted: false
|
|
243
|
+
} });
|
|
244
|
+
},
|
|
245
|
+
hold: () => call.hold(),
|
|
246
|
+
unhold: () => call.unhold(),
|
|
247
|
+
dtmf: (digit) => call.dtmf(digit)
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
connect = () => {
|
|
251
|
+
this.connectPromise ??= this.connectInternal().finally(() => {
|
|
252
|
+
this.connectPromise = null;
|
|
253
|
+
});
|
|
254
|
+
return this.connectPromise;
|
|
255
|
+
};
|
|
256
|
+
async connectInternal() {
|
|
257
|
+
if (this.state.status === "ready") return;
|
|
258
|
+
this.channel ??= new BroadcastChannel("hitch:telephony");
|
|
259
|
+
this.channel.onmessage = this.onChannelMessage;
|
|
260
|
+
this.update({
|
|
261
|
+
status: "connecting",
|
|
262
|
+
error: null
|
|
263
|
+
});
|
|
264
|
+
if ("locks" in navigator) {
|
|
265
|
+
this.lockAbortController = new AbortController();
|
|
266
|
+
await new Promise((resolve) => {
|
|
267
|
+
this.connectResolve = resolve;
|
|
268
|
+
this.channel?.postMessage({ type: "state-request" });
|
|
269
|
+
navigator.locks.request("hitch:telephony", {
|
|
270
|
+
mode: "exclusive",
|
|
271
|
+
signal: this.lockAbortController.signal
|
|
272
|
+
}, async () => {
|
|
273
|
+
const released = new Promise((release) => {
|
|
274
|
+
this.releaseLock = release;
|
|
275
|
+
});
|
|
276
|
+
this.ownsPhone = true;
|
|
277
|
+
this.update({
|
|
278
|
+
status: "connecting",
|
|
279
|
+
error: null,
|
|
280
|
+
active: null
|
|
281
|
+
});
|
|
282
|
+
await this.connectOwned();
|
|
283
|
+
this.connectResolve?.();
|
|
284
|
+
this.connectResolve = null;
|
|
285
|
+
await released;
|
|
286
|
+
}).catch((error) => {
|
|
287
|
+
if (error instanceof DOMException && error.name === "AbortError") return;
|
|
288
|
+
console.error("Phone lock failed", error);
|
|
289
|
+
this.update({
|
|
290
|
+
status: "error",
|
|
291
|
+
error: String(error)
|
|
292
|
+
});
|
|
293
|
+
this.connectResolve?.();
|
|
294
|
+
this.connectResolve = null;
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.ownsPhone = true;
|
|
300
|
+
await this.connectOwned();
|
|
301
|
+
}
|
|
302
|
+
async connectOwned() {
|
|
303
|
+
try {
|
|
304
|
+
const deviceKey = localStorage.getItem(DEVICE_KEY) ?? crypto.randomUUID();
|
|
305
|
+
localStorage.setItem(DEVICE_KEY, deviceKey);
|
|
306
|
+
uploadPendingRecordings(this.api).catch((error) => console.error("Recording recovery failed", error));
|
|
307
|
+
for (const delay of [
|
|
308
|
+
0,
|
|
309
|
+
2e3,
|
|
310
|
+
4e3,
|
|
311
|
+
8e3
|
|
312
|
+
]) {
|
|
313
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
314
|
+
const { token } = await this.api.token(deviceKey);
|
|
315
|
+
const { TelnyxRTC: Client } = await import("@telnyx/webrtc");
|
|
316
|
+
const client = new Client({
|
|
317
|
+
login_token: token,
|
|
318
|
+
keepConnectionAliveOnSocketClose: true
|
|
319
|
+
});
|
|
320
|
+
this.client = client;
|
|
321
|
+
client.on("telnyx.notification", ({ type, call }) => {
|
|
322
|
+
if (type !== "callUpdate" || !call) return;
|
|
323
|
+
if (call.direction === "inbound") {
|
|
324
|
+
call.hangup();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (call === this.currentCall) this.onCallUpdate(call);
|
|
328
|
+
});
|
|
329
|
+
client.on("telnyx.warning", (warning) => {
|
|
330
|
+
if (Number(warning.warning.code) !== 34001) return;
|
|
331
|
+
this.api.token(deviceKey).then(({ token: nextToken }) => client.login({ creds: { login_token: nextToken } })).catch((error) => {
|
|
332
|
+
console.error("Phone token renewal failed", error);
|
|
333
|
+
this.update({ error: String(error) });
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
try {
|
|
337
|
+
await new Promise((resolve, reject) => {
|
|
338
|
+
client.on("telnyx.ready", () => resolve());
|
|
339
|
+
client.on("telnyx.error", (error) => reject(error));
|
|
340
|
+
client.connect().catch(reject);
|
|
341
|
+
});
|
|
342
|
+
this.firstLogin = false;
|
|
343
|
+
this.update({ status: "ready" });
|
|
344
|
+
window.addEventListener("pagehide", this.disconnect);
|
|
345
|
+
return;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
await client.disconnect().catch((disconnectError) => console.error("Phone disconnect failed", disconnectError));
|
|
348
|
+
this.client = null;
|
|
349
|
+
if (!this.firstLogin || delay === 8e3) throw error;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
this.update({
|
|
354
|
+
status: "error",
|
|
355
|
+
error: error instanceof Error ? error.message : String(error)
|
|
356
|
+
});
|
|
357
|
+
this.ownsPhone = false;
|
|
358
|
+
this.releaseLock?.();
|
|
359
|
+
this.releaseLock = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
disconnect = () => {
|
|
363
|
+
this.lockAbortController?.abort();
|
|
364
|
+
this.lockAbortController = null;
|
|
365
|
+
this.connectResolve?.();
|
|
366
|
+
this.connectResolve = null;
|
|
367
|
+
if (this.pollTimer) clearTimeout(this.pollTimer);
|
|
368
|
+
this.pollTimer = null;
|
|
369
|
+
window.removeEventListener("pagehide", this.disconnect);
|
|
370
|
+
window.removeEventListener("beforeunload", this.beforeUnload);
|
|
371
|
+
this.client?.disconnect().catch((error) => console.error("Phone disconnect failed", error));
|
|
372
|
+
this.client = null;
|
|
373
|
+
this.pendingCommands.forEach(({ reject, timer }) => {
|
|
374
|
+
clearTimeout(timer);
|
|
375
|
+
reject(/* @__PURE__ */ new Error("The phone did not respond."));
|
|
376
|
+
});
|
|
377
|
+
this.pendingCommands.clear();
|
|
378
|
+
this.releaseLock?.();
|
|
379
|
+
this.releaseLock = null;
|
|
380
|
+
this.update({
|
|
381
|
+
status: "off",
|
|
382
|
+
active: null
|
|
383
|
+
});
|
|
384
|
+
this.ownsPhone = false;
|
|
385
|
+
this.channel?.close();
|
|
386
|
+
this.channel = null;
|
|
387
|
+
};
|
|
388
|
+
beforeUnload = (event) => {
|
|
389
|
+
event.preventDefault();
|
|
390
|
+
event.returnValue = "";
|
|
391
|
+
};
|
|
392
|
+
call = async ({ numberId, to }) => {
|
|
393
|
+
if (this.callStarting || this.state.active) throw new Error("A call is already in progress.");
|
|
394
|
+
if (this.state.status !== "ready") throw new Error("The phone is not ready.");
|
|
395
|
+
if (!this.ownsPhone) return this.sendCommand("call", {
|
|
396
|
+
numberId,
|
|
397
|
+
to
|
|
398
|
+
});
|
|
399
|
+
if (!this.client) throw new Error("The phone is not ready.");
|
|
400
|
+
this.callStarting = true;
|
|
401
|
+
try {
|
|
402
|
+
const { callId, pass, callerNumber, record } = await this.api.startCall(numberId, to);
|
|
403
|
+
this.audio ??= document.createElement("audio");
|
|
404
|
+
this.audio.autoplay = true;
|
|
405
|
+
this.audio.hidden = true;
|
|
406
|
+
if (!this.audio.isConnected) document.body.append(this.audio);
|
|
407
|
+
const call = this.client.newCall({
|
|
408
|
+
destinationNumber: to,
|
|
409
|
+
callerNumber,
|
|
410
|
+
customHeaders: [{
|
|
411
|
+
name: "X-Hitch-Pass",
|
|
412
|
+
value: pass
|
|
413
|
+
}],
|
|
414
|
+
remoteElement: this.audio
|
|
415
|
+
});
|
|
416
|
+
this.currentCall = call;
|
|
417
|
+
this.update({
|
|
418
|
+
error: null,
|
|
419
|
+
active: {
|
|
420
|
+
callId,
|
|
421
|
+
to,
|
|
422
|
+
from: callerNumber,
|
|
423
|
+
state: "connecting",
|
|
424
|
+
muted: false,
|
|
425
|
+
startedAt: Date.now(),
|
|
426
|
+
recording: record
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
window.addEventListener("beforeunload", this.beforeUnload);
|
|
430
|
+
this.pollTimer = setTimeout(() => void this.pollCall(callId), 15e3);
|
|
431
|
+
} finally {
|
|
432
|
+
this.callStarting = false;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
sendCommand(action, value) {
|
|
436
|
+
const id = crypto.randomUUID();
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
const timer = setTimeout(() => {
|
|
439
|
+
this.pendingCommands.delete(id);
|
|
440
|
+
reject(/* @__PURE__ */ new Error("The phone did not respond."));
|
|
441
|
+
}, 15e3);
|
|
442
|
+
this.pendingCommands.set(id, {
|
|
443
|
+
resolve,
|
|
444
|
+
reject,
|
|
445
|
+
timer
|
|
446
|
+
});
|
|
447
|
+
this.channel?.postMessage({
|
|
448
|
+
type: "command",
|
|
449
|
+
id,
|
|
450
|
+
action,
|
|
451
|
+
value
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
onChannelMessage = (event) => {
|
|
456
|
+
const data = event.data;
|
|
457
|
+
if (typeof data !== "object" || data === null || !("type" in data)) return;
|
|
458
|
+
if (data.type === "state-request" && this.ownsPhone) this.channel?.postMessage({
|
|
459
|
+
type: "state",
|
|
460
|
+
state: this.state
|
|
461
|
+
});
|
|
462
|
+
else if (data.type === "state" && !this.ownsPhone && "state" in data) {
|
|
463
|
+
const state = data.state;
|
|
464
|
+
if (typeof state !== "object" || state === null || !("status" in state)) return;
|
|
465
|
+
this.state = state;
|
|
466
|
+
this.listeners.forEach((listener) => listener());
|
|
467
|
+
this.connectResolve?.();
|
|
468
|
+
this.connectResolve = null;
|
|
469
|
+
} else if (data.type === "reply" && "id" in data && typeof data.id === "string") {
|
|
470
|
+
const pending = this.pendingCommands.get(data.id);
|
|
471
|
+
if (!pending) return;
|
|
472
|
+
clearTimeout(pending.timer);
|
|
473
|
+
this.pendingCommands.delete(data.id);
|
|
474
|
+
if ("error" in data && typeof data.error === "string") pending.reject(new Error(data.error));
|
|
475
|
+
else pending.resolve();
|
|
476
|
+
} else if (data.type === "command" && this.ownsPhone && "id" in data && typeof data.id === "string" && "action" in data) this.runCommand(data).then(() => this.channel?.postMessage({
|
|
477
|
+
type: "reply",
|
|
478
|
+
id: data.id
|
|
479
|
+
}), (error) => this.channel?.postMessage({
|
|
480
|
+
type: "reply",
|
|
481
|
+
id: data.id,
|
|
482
|
+
error: error instanceof Error ? error.message : String(error)
|
|
483
|
+
}));
|
|
484
|
+
};
|
|
485
|
+
async runCommand(data) {
|
|
486
|
+
if (data.action === "call") {
|
|
487
|
+
const value = data.value;
|
|
488
|
+
if (!value || typeof value !== "object" || !("numberId" in value) || !("to" in value) || typeof value.numberId !== "string" || typeof value.to !== "string") throw new Error("Invalid call command.");
|
|
489
|
+
await this.call({
|
|
490
|
+
numberId: value.numberId,
|
|
491
|
+
to: value.to
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const active = this.active;
|
|
496
|
+
if (!active) throw new Error("The call is not active.");
|
|
497
|
+
switch (data.action) {
|
|
498
|
+
case "hangup":
|
|
499
|
+
await active.hangup();
|
|
500
|
+
return;
|
|
501
|
+
case "mute":
|
|
502
|
+
active.mute();
|
|
503
|
+
return;
|
|
504
|
+
case "unmute":
|
|
505
|
+
active.unmute();
|
|
506
|
+
return;
|
|
507
|
+
case "hold":
|
|
508
|
+
await active.hold();
|
|
509
|
+
return;
|
|
510
|
+
case "unhold":
|
|
511
|
+
await active.unhold();
|
|
512
|
+
return;
|
|
513
|
+
case "dtmf":
|
|
514
|
+
if (typeof data.value !== "string") throw new Error("Invalid digit.");
|
|
515
|
+
active.dtmf(data.value);
|
|
516
|
+
return;
|
|
517
|
+
default: throw new Error("Invalid phone command.");
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async pollCall(callId) {
|
|
521
|
+
if (this.state.active?.callId !== callId || this.state.active.state === "active") return;
|
|
522
|
+
try {
|
|
523
|
+
const { status } = await this.api.getCall(callId);
|
|
524
|
+
if ([
|
|
525
|
+
"starting",
|
|
526
|
+
"failed",
|
|
527
|
+
"ended"
|
|
528
|
+
].includes(status)) {
|
|
529
|
+
await this.currentCall?.hangup();
|
|
530
|
+
this.update({ error: "The call could not be connected." });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error("Call status check failed", error);
|
|
535
|
+
}
|
|
536
|
+
if (this.getSnapshot().active?.callId === callId) this.pollTimer = setTimeout(() => void this.pollCall(callId), 1e4);
|
|
537
|
+
}
|
|
538
|
+
onCallUpdate(call) {
|
|
539
|
+
const active = this.state.active;
|
|
540
|
+
if (!active) return;
|
|
541
|
+
const state = call.state;
|
|
542
|
+
if ([
|
|
543
|
+
"hangup",
|
|
544
|
+
"destroy",
|
|
545
|
+
"purge"
|
|
546
|
+
].includes(state)) {
|
|
547
|
+
if (this.pollTimer) clearTimeout(this.pollTimer);
|
|
548
|
+
this.pollTimer = null;
|
|
549
|
+
window.removeEventListener("beforeunload", this.beforeUnload);
|
|
550
|
+
this.update({ active: {
|
|
551
|
+
...active,
|
|
552
|
+
state: "ended"
|
|
553
|
+
} });
|
|
554
|
+
const stop = this.stopRecording;
|
|
555
|
+
const pending = this.recordingPromise;
|
|
556
|
+
this.stopRecording = null;
|
|
557
|
+
this.recordingPromise = null;
|
|
558
|
+
if (stop) stop().catch((error) => console.error("Recording upload failed", error));
|
|
559
|
+
else if (pending) pending.then((finish) => finish()).catch((error) => console.error("Recording upload failed", error));
|
|
560
|
+
this.currentCall = null;
|
|
561
|
+
this.update({ active: null });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const mapped = state === "active" ? "active" : state === "held" ? "held" : state === "ringing" ? "ringing" : "connecting";
|
|
565
|
+
if (mapped === "active" && active.recording && !this.stopRecording && !this.recordingPromise) {
|
|
566
|
+
this.recordingPromise = startRecording(active.callId, call, this.api, (error) => {
|
|
567
|
+
console.error("Call recording failed", error);
|
|
568
|
+
if (this.state.active?.callId === active.callId) this.update({ active: {
|
|
569
|
+
...this.state.active,
|
|
570
|
+
recording: false
|
|
571
|
+
} });
|
|
572
|
+
}).then((stop) => {
|
|
573
|
+
if (this.state.active?.callId === active.callId) this.stopRecording = stop;
|
|
574
|
+
return stop;
|
|
575
|
+
});
|
|
576
|
+
this.recordingPromise.catch((error) => {
|
|
577
|
+
console.error("Call recording failed", error);
|
|
578
|
+
if (this.state.active) this.update({ active: {
|
|
579
|
+
...this.state.active,
|
|
580
|
+
recording: false
|
|
581
|
+
} });
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (mapped === "active" && this.pollTimer) clearTimeout(this.pollTimer);
|
|
585
|
+
this.update({ active: {
|
|
586
|
+
...active,
|
|
587
|
+
state: mapped
|
|
588
|
+
} });
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
const createPhone = (options = {}) => singleton ??= new Phone(options);
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/telephony/use-phone.ts
|
|
594
|
+
const usePhone = () => {
|
|
595
|
+
const phone = createPhone();
|
|
596
|
+
const state = useSyncExternalStore(phone.subscribe, phone.getSnapshot);
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
phone.connect();
|
|
599
|
+
}, [phone]);
|
|
600
|
+
return {
|
|
601
|
+
state,
|
|
602
|
+
call: phone.call,
|
|
603
|
+
active: phone.active
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
//#endregion
|
|
607
|
+
export { createPhone, usePhone };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { n as fetchWithOrgAuth } from "./workspace-
|
|
2
|
-
import { t as loadAppletConfig } from "./load-config-
|
|
1
|
+
import { n as fetchWithOrgAuth } from "./workspace-XVtM12ZC.js";
|
|
2
|
+
import { t as loadAppletConfig } from "./load-config-BYSxDVAl.js";
|
|
3
|
+
import { t as question } from "./prompt-ChS-grYO.js";
|
|
3
4
|
import { z } from "zod";
|
|
4
|
-
import { createInterface } from "node:readline";
|
|
5
|
-
import { Writable } from "node:stream";
|
|
6
5
|
//#region src/variables.ts
|
|
7
6
|
const variableRowSchema = z.object({
|
|
8
7
|
key: z.string(),
|
|
@@ -13,30 +12,6 @@ const variableRowSchema = z.object({
|
|
|
13
12
|
});
|
|
14
13
|
const variableResponseSchema = z.object({ data: variableRowSchema });
|
|
15
14
|
const variablesResponseSchema = z.object({ data: z.array(variableRowSchema) });
|
|
16
|
-
function question(prompt, hidden = false) {
|
|
17
|
-
return new Promise((resolveAnswer, rejectAnswer) => {
|
|
18
|
-
let muted = false;
|
|
19
|
-
const output = hidden ? new Writable({ write(chunk, encoding, callback) {
|
|
20
|
-
if (!muted) process.stdout.write(chunk, encoding);
|
|
21
|
-
callback();
|
|
22
|
-
} }) : process.stdout;
|
|
23
|
-
const readline = createInterface({
|
|
24
|
-
input: process.stdin,
|
|
25
|
-
output,
|
|
26
|
-
terminal: true
|
|
27
|
-
});
|
|
28
|
-
readline.once("error", rejectAnswer);
|
|
29
|
-
if (hidden) {
|
|
30
|
-
process.stdout.write(prompt);
|
|
31
|
-
muted = true;
|
|
32
|
-
}
|
|
33
|
-
readline.question(hidden ? "" : prompt, (answer) => {
|
|
34
|
-
readline.close();
|
|
35
|
-
if (hidden) process.stdout.write("\n");
|
|
36
|
-
resolveAnswer(answer);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
15
|
async function getValue(options) {
|
|
41
16
|
if (options.value !== void 0) return options.value;
|
|
42
17
|
if (!(options.stdinIsTTY ?? process.stdin.isTTY === true)) throw new Error("A value is required when stdin is not a TTY.");
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { o as RegisteredAppletSourceDefinition } from "./manifest-
|
|
1
|
+
import { o as RegisteredAppletSourceDefinition } from "./manifest-DD_SGkS6.js";
|
|
2
2
|
import { PluginOption } from "vite";
|
|
3
3
|
//#region ../shared/auth/src/store.d.mts
|
|
4
4
|
interface SessionCredentials {
|