@komaa/standin-sdk 0.1.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/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/ambient.d.ts +80 -0
- package/dist/ambient.d.ts.map +1 -0
- package/dist/ambient.js +281 -0
- package/dist/ambient.js.map +1 -0
- package/dist/attachments.d.ts +149 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +451 -0
- package/dist/attachments.js.map +1 -0
- package/dist/audio.d.ts +80 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +170 -0
- package/dist/audio.js.map +1 -0
- package/dist/avatar.d.ts +111 -0
- package/dist/avatar.d.ts.map +1 -0
- package/dist/avatar.js +201 -0
- package/dist/avatar.js.map +1 -0
- package/dist/callServer.d.ts +151 -0
- package/dist/callServer.d.ts.map +1 -0
- package/dist/callServer.js +1097 -0
- package/dist/callServer.js.map +1 -0
- package/dist/callTools.d.ts +154 -0
- package/dist/callTools.d.ts.map +1 -0
- package/dist/callTools.js +255 -0
- package/dist/callTools.js.map +1 -0
- package/dist/chat.d.ts +226 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +574 -0
- package/dist/chat.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +86 -0
- package/dist/config.js.map +1 -0
- package/dist/consult.d.ts +147 -0
- package/dist/consult.d.ts.map +1 -0
- package/dist/consult.js +388 -0
- package/dist/consult.js.map +1 -0
- package/dist/delivery.d.ts +81 -0
- package/dist/delivery.d.ts.map +1 -0
- package/dist/delivery.js +187 -0
- package/dist/delivery.js.map +1 -0
- package/dist/echoGuard.d.ts +56 -0
- package/dist/echoGuard.d.ts.map +1 -0
- package/dist/echoGuard.js +57 -0
- package/dist/echoGuard.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -0
- package/dist/fetch.d.ts +40 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +240 -0
- package/dist/fetch.js.map +1 -0
- package/dist/gate.d.ts +80 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +249 -0
- package/dist/gate.js.map +1 -0
- package/dist/handler.d.ts +283 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +4 -0
- package/dist/handler.js.map +1 -0
- package/dist/hmac.d.ts +32 -0
- package/dist/hmac.d.ts.map +1 -0
- package/dist/hmac.js +108 -0
- package/dist/hmac.js.map +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/lane.d.ts +92 -0
- package/dist/lane.d.ts.map +1 -0
- package/dist/lane.js +269 -0
- package/dist/lane.js.map +1 -0
- package/dist/lipsync.d.ts +136 -0
- package/dist/lipsync.d.ts.map +1 -0
- package/dist/lipsync.js +310 -0
- package/dist/lipsync.js.map +1 -0
- package/dist/log.d.ts +18 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +20 -0
- package/dist/log.js.map +1 -0
- package/dist/media.d.ts +43 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +190 -0
- package/dist/media.js.map +1 -0
- package/dist/minutes.d.ts +398 -0
- package/dist/minutes.d.ts.map +1 -0
- package/dist/minutes.js +805 -0
- package/dist/minutes.js.map +1 -0
- package/dist/outbound.d.ts +388 -0
- package/dist/outbound.d.ts.map +1 -0
- package/dist/outbound.js +933 -0
- package/dist/outbound.js.map +1 -0
- package/dist/plugins/cartesia/agent.d.ts +58 -0
- package/dist/plugins/cartesia/agent.d.ts.map +1 -0
- package/dist/plugins/cartesia/agent.js +254 -0
- package/dist/plugins/cartesia/agent.js.map +1 -0
- package/dist/plugins/cartesia/cli.d.ts +3 -0
- package/dist/plugins/cartesia/cli.d.ts.map +1 -0
- package/dist/plugins/cartesia/cli.js +10 -0
- package/dist/plugins/cartesia/cli.js.map +1 -0
- package/dist/plugins/cartesia/config.d.ts +30 -0
- package/dist/plugins/cartesia/config.d.ts.map +1 -0
- package/dist/plugins/cartesia/config.js +29 -0
- package/dist/plugins/cartesia/config.js.map +1 -0
- package/dist/plugins/cartesia/handler.d.ts +40 -0
- package/dist/plugins/cartesia/handler.d.ts.map +1 -0
- package/dist/plugins/cartesia/handler.js +142 -0
- package/dist/plugins/cartesia/handler.js.map +1 -0
- package/dist/plugins/cartesia/index.d.ts +6 -0
- package/dist/plugins/cartesia/index.d.ts.map +1 -0
- package/dist/plugins/cartesia/index.js +54 -0
- package/dist/plugins/cartesia/index.js.map +1 -0
- package/dist/plugins/deepgram/agent.d.ts +60 -0
- package/dist/plugins/deepgram/agent.d.ts.map +1 -0
- package/dist/plugins/deepgram/agent.js +302 -0
- package/dist/plugins/deepgram/agent.js.map +1 -0
- package/dist/plugins/deepgram/cli.d.ts +3 -0
- package/dist/plugins/deepgram/cli.d.ts.map +1 -0
- package/dist/plugins/deepgram/cli.js +10 -0
- package/dist/plugins/deepgram/cli.js.map +1 -0
- package/dist/plugins/deepgram/config.d.ts +39 -0
- package/dist/plugins/deepgram/config.d.ts.map +1 -0
- package/dist/plugins/deepgram/config.js +66 -0
- package/dist/plugins/deepgram/config.js.map +1 -0
- package/dist/plugins/deepgram/handler.d.ts +87 -0
- package/dist/plugins/deepgram/handler.d.ts.map +1 -0
- package/dist/plugins/deepgram/handler.js +268 -0
- package/dist/plugins/deepgram/handler.js.map +1 -0
- package/dist/plugins/deepgram/index.d.ts +11 -0
- package/dist/plugins/deepgram/index.d.ts.map +1 -0
- package/dist/plugins/deepgram/index.js +76 -0
- package/dist/plugins/deepgram/index.js.map +1 -0
- package/dist/plugins/echo/cli.d.ts +3 -0
- package/dist/plugins/echo/cli.d.ts.map +1 -0
- package/dist/plugins/echo/cli.js +10 -0
- package/dist/plugins/echo/cli.js.map +1 -0
- package/dist/plugins/echo/index.d.ts +37 -0
- package/dist/plugins/echo/index.d.ts.map +1 -0
- package/dist/plugins/echo/index.js +61 -0
- package/dist/plugins/echo/index.js.map +1 -0
- package/dist/plugins/elevenlabs/agent.d.ts +72 -0
- package/dist/plugins/elevenlabs/agent.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/agent.js +305 -0
- package/dist/plugins/elevenlabs/agent.js.map +1 -0
- package/dist/plugins/elevenlabs/cli.d.ts +3 -0
- package/dist/plugins/elevenlabs/cli.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/cli.js +10 -0
- package/dist/plugins/elevenlabs/cli.js.map +1 -0
- package/dist/plugins/elevenlabs/config.d.ts +27 -0
- package/dist/plugins/elevenlabs/config.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/config.js +42 -0
- package/dist/plugins/elevenlabs/config.js.map +1 -0
- package/dist/plugins/elevenlabs/handler.d.ts +67 -0
- package/dist/plugins/elevenlabs/handler.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/handler.js +419 -0
- package/dist/plugins/elevenlabs/handler.js.map +1 -0
- package/dist/plugins/elevenlabs/index.d.ts +11 -0
- package/dist/plugins/elevenlabs/index.d.ts.map +1 -0
- package/dist/plugins/elevenlabs/index.js +72 -0
- package/dist/plugins/elevenlabs/index.js.map +1 -0
- package/dist/plugins/livekit/cli.d.ts +3 -0
- package/dist/plugins/livekit/cli.d.ts.map +1 -0
- package/dist/plugins/livekit/cli.js +10 -0
- package/dist/plugins/livekit/cli.js.map +1 -0
- package/dist/plugins/livekit/config.d.ts +47 -0
- package/dist/plugins/livekit/config.d.ts.map +1 -0
- package/dist/plugins/livekit/config.js +46 -0
- package/dist/plugins/livekit/config.js.map +1 -0
- package/dist/plugins/livekit/handler.d.ts +59 -0
- package/dist/plugins/livekit/handler.d.ts.map +1 -0
- package/dist/plugins/livekit/handler.js +211 -0
- package/dist/plugins/livekit/handler.js.map +1 -0
- package/dist/plugins/livekit/index.d.ts +6 -0
- package/dist/plugins/livekit/index.d.ts.map +1 -0
- package/dist/plugins/livekit/index.js +68 -0
- package/dist/plugins/livekit/index.js.map +1 -0
- package/dist/plugins/livekit/room.d.ts +117 -0
- package/dist/plugins/livekit/room.d.ts.map +1 -0
- package/dist/plugins/livekit/room.js +449 -0
- package/dist/plugins/livekit/room.js.map +1 -0
- package/dist/plugins/openai/agent.d.ts +74 -0
- package/dist/plugins/openai/agent.d.ts.map +1 -0
- package/dist/plugins/openai/agent.js +264 -0
- package/dist/plugins/openai/agent.js.map +1 -0
- package/dist/plugins/openai/cli.d.ts +3 -0
- package/dist/plugins/openai/cli.d.ts.map +1 -0
- package/dist/plugins/openai/cli.js +10 -0
- package/dist/plugins/openai/cli.js.map +1 -0
- package/dist/plugins/openai/config.d.ts +29 -0
- package/dist/plugins/openai/config.d.ts.map +1 -0
- package/dist/plugins/openai/config.js +37 -0
- package/dist/plugins/openai/config.js.map +1 -0
- package/dist/plugins/openai/handler.d.ts +65 -0
- package/dist/plugins/openai/handler.d.ts.map +1 -0
- package/dist/plugins/openai/handler.js +297 -0
- package/dist/plugins/openai/handler.js.map +1 -0
- package/dist/plugins/openai/index.d.ts +6 -0
- package/dist/plugins/openai/index.d.ts.map +1 -0
- package/dist/plugins/openai/index.js +72 -0
- package/dist/plugins/openai/index.js.map +1 -0
- package/dist/plugins/openclaw/allowlist.d.ts +9 -0
- package/dist/plugins/openclaw/allowlist.d.ts.map +1 -0
- package/dist/plugins/openclaw/allowlist.js +11 -0
- package/dist/plugins/openclaw/allowlist.js.map +1 -0
- package/dist/plugins/openclaw/config.d.ts +50 -0
- package/dist/plugins/openclaw/config.d.ts.map +1 -0
- package/dist/plugins/openclaw/config.js +53 -0
- package/dist/plugins/openclaw/config.js.map +1 -0
- package/dist/plugins/openclaw/handler.d.ts +90 -0
- package/dist/plugins/openclaw/handler.d.ts.map +1 -0
- package/dist/plugins/openclaw/handler.js +146 -0
- package/dist/plugins/openclaw/handler.js.map +1 -0
- package/dist/plugins/openclaw/index.d.ts +55 -0
- package/dist/plugins/openclaw/index.d.ts.map +1 -0
- package/dist/plugins/openclaw/index.js +89 -0
- package/dist/plugins/openclaw/index.js.map +1 -0
- package/dist/plugins/openclaw/openclaw.plugin.json +74 -0
- package/dist/plugins/openclaw/package.json +14 -0
- package/dist/plugins/openclaw/realtime.d.ts +82 -0
- package/dist/plugins/openclaw/realtime.d.ts.map +1 -0
- package/dist/plugins/openclaw/realtime.js +238 -0
- package/dist/plugins/openclaw/realtime.js.map +1 -0
- package/dist/plugins/openclaw/runtime.d.ts +19 -0
- package/dist/plugins/openclaw/runtime.d.ts.map +1 -0
- package/dist/plugins/openclaw/runtime.js +160 -0
- package/dist/plugins/openclaw/runtime.js.map +1 -0
- package/dist/policy.d.ts +38 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +95 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol.d.ts +44 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +77 -0
- package/dist/protocol.js.map +1 -0
- package/dist/protocolRuntime.d.ts +19 -0
- package/dist/protocolRuntime.d.ts.map +1 -0
- package/dist/protocolRuntime.js +78 -0
- package/dist/protocolRuntime.js.map +1 -0
- package/dist/smoke.d.ts +65 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +293 -0
- package/dist/smoke.js.map +1 -0
- package/dist/startup.d.ts +74 -0
- package/dist/startup.d.ts.map +1 -0
- package/dist/startup.js +112 -0
- package/dist/startup.js.map +1 -0
- package/dist/tile.d.ts +101 -0
- package/dist/tile.d.ts.map +1 -0
- package/dist/tile.js +195 -0
- package/dist/tile.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/vision.d.ts +160 -0
- package/dist/vision.d.ts.map +1 -0
- package/dist/vision.js +301 -0
- package/dist/vision.js.map +1 -0
- package/dist/visionTools.d.ts +237 -0
- package/dist/visionTools.d.ts.map +1 -0
- package/dist/visionTools.js +578 -0
- package/dist/visionTools.js.map +1 -0
- package/dist/voice.d.ts +143 -0
- package/dist/voice.d.ts.map +1 -0
- package/dist/voice.js +403 -0
- package/dist/voice.js.map +1 -0
- package/package.json +129 -0
package/dist/fetch.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Copyright (c) 2026 Komaa DigiTech
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Fetching a URL an agent chose, without fetching your own infrastructure.
|
|
5
|
+
*
|
|
6
|
+
* An agent that can show the caller a picture will sooner or later be handed a
|
|
7
|
+
* URL by its own model, and that model is steered by whoever is on the call. So
|
|
8
|
+
* the URL is untrusted input wearing a trusted costume, and a naive GET of it is
|
|
9
|
+
* a server-side request forgery: `169.254.169.254` is cloud credentials,
|
|
10
|
+
* `127.0.0.1` is whatever else you run, and `10.0.0.0/8` is the rest of your
|
|
11
|
+
* network.
|
|
12
|
+
*
|
|
13
|
+
* {@link fetchPublicImage} is the guarded way to do it: http and https only, no
|
|
14
|
+
* embedded credentials, and no host that resolves into private, loopback,
|
|
15
|
+
* link-local or reserved space.
|
|
16
|
+
*
|
|
17
|
+
* The subtle half is the rebind. Validating a hostname resolves it once, and the
|
|
18
|
+
* resolution the HTTP stack does a moment later can answer differently, so the
|
|
19
|
+
* request is made through a guarded `lookup` that re-checks the address the
|
|
20
|
+
* socket will actually connect to. One redirect hop is followed, because image
|
|
21
|
+
* CDNs habitually redirect to the real asset, and the target goes through the
|
|
22
|
+
* whole guard again rather than being trusted for having come from a host that
|
|
23
|
+
* passed.
|
|
24
|
+
*
|
|
25
|
+
* Identical in shape to the Python SDK's `standin.fetch`.
|
|
26
|
+
*/
|
|
27
|
+
import { lookup } from "node:dns/promises";
|
|
28
|
+
import { request as httpRequest } from "node:http";
|
|
29
|
+
import { request as httpsRequest } from "node:https";
|
|
30
|
+
import { isIP } from "node:net";
|
|
31
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
32
|
+
function ipv4ToInt(ip) {
|
|
33
|
+
return (ip.split(".").reduce((acc, octet) => (acc << 8) + Number(octet), 0) >>> 0);
|
|
34
|
+
}
|
|
35
|
+
function inCidr4(ip, base, maskBits) {
|
|
36
|
+
const mask = maskBits === 0 ? 0 : (~0 << (32 - maskBits)) >>> 0;
|
|
37
|
+
return (ip & mask) === (ipv4ToInt(base) & mask);
|
|
38
|
+
}
|
|
39
|
+
/** True for any IPv4 address that must never be fetched server-side. */
|
|
40
|
+
export function isForbiddenIpv4(ip) {
|
|
41
|
+
const n = ipv4ToInt(ip);
|
|
42
|
+
return (inCidr4(n, "0.0.0.0", 8) || // "this" network
|
|
43
|
+
inCidr4(n, "10.0.0.0", 8) || // RFC1918
|
|
44
|
+
inCidr4(n, "100.64.0.0", 10) || // carrier-grade NAT
|
|
45
|
+
inCidr4(n, "127.0.0.0", 8) || // loopback
|
|
46
|
+
inCidr4(n, "169.254.0.0", 16) || // link-local, including cloud metadata
|
|
47
|
+
inCidr4(n, "172.16.0.0", 12) || // RFC1918
|
|
48
|
+
inCidr4(n, "192.0.0.0", 24) || // IETF protocol assignments
|
|
49
|
+
inCidr4(n, "192.168.0.0", 16) || // RFC1918
|
|
50
|
+
inCidr4(n, "198.18.0.0", 15) || // benchmarking
|
|
51
|
+
inCidr4(n, "224.0.0.0", 3) // multicast, reserved and broadcast
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
/** True for any IPv6 address that must never be fetched server-side. */
|
|
55
|
+
export function isForbiddenIpv6(ip) {
|
|
56
|
+
const lower = ip.toLowerCase();
|
|
57
|
+
// A v4 address wearing a v6 spelling is still that v4 address, so judge what
|
|
58
|
+
// it embeds. Otherwise the whole v4 table above is one notation away from
|
|
59
|
+
// being bypassed.
|
|
60
|
+
if (lower.startsWith("::ffff:") || lower.startsWith("64:ff9b:")) {
|
|
61
|
+
const dotted = lower.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
62
|
+
if (dotted?.[1])
|
|
63
|
+
return isForbiddenIpv4(dotted[1]);
|
|
64
|
+
const groups = (lower.split("::").pop() ?? "").split(":").filter(Boolean);
|
|
65
|
+
if (groups.length <= 2) {
|
|
66
|
+
const hi = groups.length === 2 ? parseInt(groups[0] ?? "0", 16) : 0;
|
|
67
|
+
const lo = parseInt(groups[groups.length - 1] ?? "0", 16) || 0;
|
|
68
|
+
return isForbiddenIpv4(`${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`);
|
|
69
|
+
}
|
|
70
|
+
return true; // a malformed mapped form: refuse rather than guess
|
|
71
|
+
}
|
|
72
|
+
if (lower === "::" || lower === "::1")
|
|
73
|
+
return true;
|
|
74
|
+
const firstGroup = lower.split(":")[0] || "0";
|
|
75
|
+
const first16 = parseInt(firstGroup === "" ? "0" : firstGroup, 16);
|
|
76
|
+
if ((first16 & 0xfe00) === 0xfc00)
|
|
77
|
+
return true; // fc00::/7 unique-local
|
|
78
|
+
if ((first16 & 0xffc0) === 0xfe80)
|
|
79
|
+
return true; // fe80::/10 link-local
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* True for any address that must never be fetched server-side.
|
|
84
|
+
*
|
|
85
|
+
* Unparseable input is forbidden too: something that is not an address cannot be
|
|
86
|
+
* proven safe, and failing closed is the only correct direction here.
|
|
87
|
+
*/
|
|
88
|
+
export function isForbiddenIp(ip) {
|
|
89
|
+
const family = isIP(ip);
|
|
90
|
+
if (family === 4)
|
|
91
|
+
return isForbiddenIpv4(ip);
|
|
92
|
+
if (family === 6)
|
|
93
|
+
return isForbiddenIpv6(ip);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check a URL is safe to fetch, or throw saying why.
|
|
98
|
+
*
|
|
99
|
+
* http and https only, no embedded credentials, and every address the host
|
|
100
|
+
* resolves to must be public.
|
|
101
|
+
*/
|
|
102
|
+
export async function assertPublicHttpUrl(raw, lookupFn = lookup, isForbiddenFn = isForbiddenIp) {
|
|
103
|
+
let url;
|
|
104
|
+
try {
|
|
105
|
+
url = new URL(raw);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error("not a valid URL");
|
|
109
|
+
}
|
|
110
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
111
|
+
throw new Error(`forbidden protocol ${url.protocol}`);
|
|
112
|
+
}
|
|
113
|
+
if (url.username || url.password) {
|
|
114
|
+
throw new Error("URLs with embedded credentials are not allowed");
|
|
115
|
+
}
|
|
116
|
+
// The WHATWG URL keeps the brackets on an IPv6 literal, which isIP rejects.
|
|
117
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
118
|
+
if (isIP(host)) {
|
|
119
|
+
if (isForbiddenFn(host))
|
|
120
|
+
throw new Error(`address ${host} is private or reserved`);
|
|
121
|
+
return url;
|
|
122
|
+
}
|
|
123
|
+
let addrs;
|
|
124
|
+
try {
|
|
125
|
+
addrs = await lookupFn(host, { all: true, verbatim: true });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
throw new Error(`cannot resolve host ${host}`);
|
|
129
|
+
}
|
|
130
|
+
if (addrs.length === 0)
|
|
131
|
+
throw new Error(`host ${host} resolves to no addresses`);
|
|
132
|
+
for (const addr of addrs) {
|
|
133
|
+
if (isForbiddenFn(addr.address)) {
|
|
134
|
+
throw new Error(`host ${host} resolves to private or reserved address ${addr.address}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return url;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Fetch an image from an untrusted URL.
|
|
141
|
+
*
|
|
142
|
+
* Bounded in every direction that matters: total time, declared length, streamed
|
|
143
|
+
* length, and one redirect hop. Throws for anything that fails, so a caller can
|
|
144
|
+
* hand the reason straight back to the agent that supplied the URL.
|
|
145
|
+
*/
|
|
146
|
+
export async function fetchPublicImage(rawUrl, maxBytes, timeoutMs = 10_000, lookupFn = lookup, isForbiddenFn = isForbiddenIp, redirectsLeft = 1) {
|
|
147
|
+
const url = await assertPublicHttpUrl(rawUrl, lookupFn, isForbiddenFn);
|
|
148
|
+
// The HTTP stack calls this to resolve the host, so an answer that was public
|
|
149
|
+
// during validation and private a moment later is refused here rather than
|
|
150
|
+
// quietly connecting to something internal.
|
|
151
|
+
const guardedLookup = (hostname, options, cb) => {
|
|
152
|
+
lookupFn(hostname, { all: true, verbatim: true }).then((addrs) => {
|
|
153
|
+
const bad = addrs.find((a) => isForbiddenFn(a.address));
|
|
154
|
+
if (bad || addrs.length === 0) {
|
|
155
|
+
cb(new Error(`DNS rebind blocked: ${hostname} resolved to ${bad?.address ?? "nothing"}`), null);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (options.all) {
|
|
159
|
+
cb(null, addrs);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const first = addrs[0];
|
|
163
|
+
if (first === undefined) {
|
|
164
|
+
cb(new Error(`${hostname} resolved to nothing`), null);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
cb(null, first.address, first.family);
|
|
168
|
+
}, (err) => cb(err, null));
|
|
169
|
+
};
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const req = (url.protocol === "https:" ? httpsRequest : httpRequest)(url,
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
173
|
+
{ lookup: guardedLookup, headers: { accept: "image/*" } }, (res) => {
|
|
174
|
+
if (REDIRECT_STATUSES.has(res.statusCode ?? 0)) {
|
|
175
|
+
const location = res.headers.location;
|
|
176
|
+
res.resume();
|
|
177
|
+
if (!location) {
|
|
178
|
+
reject(new Error(`fetch ${rawUrl} returned HTTP ${res.statusCode} with no Location`));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (redirectsLeft <= 0) {
|
|
182
|
+
reject(new Error(`fetch ${rawUrl} followed too many redirects`));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
let next;
|
|
186
|
+
try {
|
|
187
|
+
next = new URL(location, url).toString();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
reject(new Error(`fetch ${rawUrl} returned an invalid redirect location`));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// Resolved against the CURRENT url, then put through the whole guard
|
|
194
|
+
// again. A target is not trustworthy for having been named by a host
|
|
195
|
+
// that passed.
|
|
196
|
+
fetchPublicImage(next, maxBytes, timeoutMs, lookupFn, isForbiddenFn, redirectsLeft - 1).then(resolve, reject);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (res.statusCode !== 200) {
|
|
200
|
+
res.resume();
|
|
201
|
+
reject(new Error(`fetch ${rawUrl} returned HTTP ${res.statusCode}`));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const declared = Number(res.headers["content-length"] ?? NaN);
|
|
205
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
206
|
+
res.destroy();
|
|
207
|
+
reject(new Error(`response too large (${declared} bytes, max ${maxBytes})`));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const mime = (res.headers["content-type"] ?? "image/jpeg").split(";")[0]?.trim() ??
|
|
211
|
+
"image/jpeg";
|
|
212
|
+
const chunks = [];
|
|
213
|
+
let total = 0;
|
|
214
|
+
res.on("data", (chunk) => {
|
|
215
|
+
// The declared length is a claim, not a promise. Counting what
|
|
216
|
+
// actually arrives is what bounds a lying or chunked response.
|
|
217
|
+
total += chunk.length;
|
|
218
|
+
if (total > maxBytes) {
|
|
219
|
+
res.destroy();
|
|
220
|
+
reject(new Error(`response exceeded ${maxBytes} bytes; aborting read`));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
chunks.push(chunk);
|
|
224
|
+
});
|
|
225
|
+
res.on("end", () => resolve({ bytes: Buffer.concat(chunks), mime }));
|
|
226
|
+
res.on("error", reject);
|
|
227
|
+
});
|
|
228
|
+
// A hard total deadline: the socket's own timeout option is only an idle
|
|
229
|
+
// timer, so a slow drip would never trip it.
|
|
230
|
+
const deadline = setTimeout(() => req.destroy(new Error(`fetch ${rawUrl} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
231
|
+
deadline.unref?.();
|
|
232
|
+
req.on("error", (err) => {
|
|
233
|
+
clearTimeout(deadline);
|
|
234
|
+
reject(err);
|
|
235
|
+
});
|
|
236
|
+
req.on("close", () => clearTimeout(deadline));
|
|
237
|
+
req.end();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../src/fetch.ts"],"names":[],"mappings":"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"}
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when the utterance, AS A WHOLE, is a verbal interrupt.
|
|
3
|
+
*
|
|
4
|
+
* Deterministic and conservative: lowercase, punctuation stripped, surrounding
|
|
5
|
+
* filler AND the configured wake phrases removed - "hey <name>, stop" must cut as
|
|
6
|
+
* instantly as a bare "stop", because addressing the bot by name is how people
|
|
7
|
+
* interrupt it in a meeting - then an exact phrase-set match capped at four
|
|
8
|
+
* words. A longer sentence that merely contains "stop" does not match.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_FOLLOW_UP_WINDOW_MS = 12000;
|
|
11
|
+
/**
|
|
12
|
+
* Is this call attached to a Microsoft Teams meeting or channel thread?
|
|
13
|
+
*
|
|
14
|
+
* The group signal that actually arrives. The participant count that used to
|
|
15
|
+
* carry this does not reach a bot that joined through the meeting, so a gate
|
|
16
|
+
* keyed on it alone never fires.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isMeetingThread(threadId: string | undefined): boolean;
|
|
19
|
+
/** The outcome for one finished caller turn. */
|
|
20
|
+
export interface GateDecision {
|
|
21
|
+
/** Speak an answer to this turn. */
|
|
22
|
+
readonly respond: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The turn named the assistant. Opens the follow-up window, and is worth
|
|
25
|
+
* knowing separately from `respond`: a turn inside the window is answered
|
|
26
|
+
* without having been addressed.
|
|
27
|
+
*/
|
|
28
|
+
readonly addressed: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Case-insensitive, word-boundary match of any wake phrase.
|
|
32
|
+
*
|
|
33
|
+
* Boundaries rather than substrings, so "assistant" matches and "assistants"
|
|
34
|
+
* does not. The boundary is expressed as "not a word character" so a non-Latin
|
|
35
|
+
* wake phrase behaves the same as a Latin one. An empty phrase list never
|
|
36
|
+
* matches.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isAddressed(transcript: string, wakePhrases: readonly string[]): boolean;
|
|
39
|
+
/** Options for {@link GroupGate}. */
|
|
40
|
+
export interface GroupGateOptions {
|
|
41
|
+
wakePhrases: readonly string[];
|
|
42
|
+
requireAddress?: boolean;
|
|
43
|
+
followUpWindowMs?: number;
|
|
44
|
+
threadId?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The group-call gate for one call. Holds the follow-up window's state.
|
|
48
|
+
*
|
|
49
|
+
* An agent in a meeting that answers every sentence is worse than one that says
|
|
50
|
+
* nothing. The gate keeps it quiet until somebody names it, then leaves the
|
|
51
|
+
* floor open for a short while so a follow-up does not need the name again.
|
|
52
|
+
*/
|
|
53
|
+
export declare class GroupGate {
|
|
54
|
+
#private;
|
|
55
|
+
readonly wakePhrases: readonly string[];
|
|
56
|
+
readonly requireAddress: boolean;
|
|
57
|
+
readonly followUpWindowMs: number;
|
|
58
|
+
constructor(options: GroupGateOptions);
|
|
59
|
+
/** More than one human on this call, by either signal. */
|
|
60
|
+
get isGroup(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Is the gate actually muting anything right now?
|
|
63
|
+
*
|
|
64
|
+
* A gate with no wake phrase configured can never be opened, so it would mute
|
|
65
|
+
* the assistant for the whole call. Treat "no trigger configured" as gate off.
|
|
66
|
+
*/
|
|
67
|
+
get active(): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Record a participant count from call context.
|
|
70
|
+
*
|
|
71
|
+
* Corroborating, never authoritative: a count that says 1 does not clear a
|
|
72
|
+
* meeting thread. The count is the signal that goes missing, so it may add
|
|
73
|
+
* certainty and must not remove it.
|
|
74
|
+
*/
|
|
75
|
+
noteParticipants(count: number): void;
|
|
76
|
+
/** Answer this turn, or stay out of the meeting? */
|
|
77
|
+
decide(transcript: string, nowMs: number): GateDecision;
|
|
78
|
+
}
|
|
79
|
+
export declare function isVerbalInterrupt(text: string | undefined, wakePhrases?: string[]): boolean;
|
|
80
|
+
//# sourceMappingURL=gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAoHA;;;;;;;;GAQG;AACH,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAQlD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAErE;AAED,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAgBT;AAED,qCAAqC;AACrC,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,qBAAa,SAAS;;IACpB,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;gBAKtB,OAAO,EAAE,gBAAgB;IAQrC,0DAA0D;IAC1D,IAAI,OAAO,IAAI,OAAO,CAErB;IAED;;;;;OAKG;IACH,IAAI,MAAM,IAAI,OAAO,CAMpB;IAED;;;;;;OAMG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC,oDAAoD;IACpD,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY;CAexD;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,WAAW,CAAC,EAAE,MAAM,EAAE,GACrB,OAAO,CAiCT"}
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Copyright (c) 2026 Komaa DigiTech
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic verbal interrupts: "stop", "hold on", "never mind".
|
|
5
|
+
*
|
|
6
|
+
* These are handled in code rather than left to the model because the model is
|
|
7
|
+
* mid-generation when they arrive. Waiting for its own interruption handling is
|
|
8
|
+
* what makes a cut feel late; matching the phrase ourselves and flushing the
|
|
9
|
+
* caller's playback queue is what makes it feel instant.
|
|
10
|
+
*
|
|
11
|
+
* Matching is WHOLE-UTTERANCE only, after stripping filler and the configured
|
|
12
|
+
* wake phrases, so "stop by the store on your way" never triggers.
|
|
13
|
+
*/
|
|
14
|
+
/** Normalized utterances that mean "stop talking / hold on". */
|
|
15
|
+
const INTERRUPT_PHRASES = new Set([
|
|
16
|
+
"stop",
|
|
17
|
+
"stop it",
|
|
18
|
+
"stop talking",
|
|
19
|
+
"wait",
|
|
20
|
+
"wait wait",
|
|
21
|
+
"wait a second",
|
|
22
|
+
"wait a minute",
|
|
23
|
+
"hold on",
|
|
24
|
+
"hang on",
|
|
25
|
+
"never mind",
|
|
26
|
+
"nevermind",
|
|
27
|
+
"be quiet",
|
|
28
|
+
"quiet",
|
|
29
|
+
"shut up",
|
|
30
|
+
"enough",
|
|
31
|
+
"thats enough",
|
|
32
|
+
"pause",
|
|
33
|
+
"one second",
|
|
34
|
+
"one sec",
|
|
35
|
+
"give me a second",
|
|
36
|
+
// Arabic: the deterministic cut has to work in both call languages, because the
|
|
37
|
+
// model mirrors the caller's. Whole-utterance matching plus the normalizer's
|
|
38
|
+
// tashkeel stripping keep these as conservative as the English set.
|
|
39
|
+
"توقف", // stop
|
|
40
|
+
"قف", // halt
|
|
41
|
+
"اسكت", // be quiet
|
|
42
|
+
"اصمت", // silence
|
|
43
|
+
"انتظر", // wait
|
|
44
|
+
"انتظر لحظة", // wait a moment
|
|
45
|
+
"استنى", // wait (colloquial)
|
|
46
|
+
"استنا",
|
|
47
|
+
"لحظة", // one moment
|
|
48
|
+
"لحظه",
|
|
49
|
+
"لحظة واحدة", // just a moment
|
|
50
|
+
"ثانية", // one second
|
|
51
|
+
"ثانيه",
|
|
52
|
+
"ثانية واحدة",
|
|
53
|
+
"دقيقة", // one minute
|
|
54
|
+
"دقيقه",
|
|
55
|
+
"مهلا", // hold on
|
|
56
|
+
"خلاص", // enough / that is it
|
|
57
|
+
"كفى", // enough
|
|
58
|
+
"كفاية",
|
|
59
|
+
"كفايه",
|
|
60
|
+
"بس", // enough/stop (colloquial; safe only because matching is whole-utterance)
|
|
61
|
+
]);
|
|
62
|
+
/**
|
|
63
|
+
* Leading and trailing filler stripped before matching ("ok stop", "no wait",
|
|
64
|
+
* "stop please"; Arabic "طيب توقف" = "ok stop", "يا" = the vocative that precedes
|
|
65
|
+
* a name).
|
|
66
|
+
*/
|
|
67
|
+
const FILLER_TOKENS = new Set([
|
|
68
|
+
"ok",
|
|
69
|
+
"okay",
|
|
70
|
+
"oh",
|
|
71
|
+
"no",
|
|
72
|
+
"hey",
|
|
73
|
+
"please",
|
|
74
|
+
"now",
|
|
75
|
+
"طيب",
|
|
76
|
+
"لا",
|
|
77
|
+
"يا",
|
|
78
|
+
]);
|
|
79
|
+
function normalizeWords(text) {
|
|
80
|
+
return ((text ?? "")
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/['’]/g, "") // "that's" -> "thats", not "that s"
|
|
83
|
+
// Combining marks (Arabic tashkeel, accents) and the Arabic tatweel attach
|
|
84
|
+
// INSIDE a word - delete them outright so "تَوَقَّف" normalizes to "توقف"
|
|
85
|
+
// instead of splitting apart.
|
|
86
|
+
.replace(/[\p{M}ـ]/gu, "")
|
|
87
|
+
// Letters in ANY script survive: an Arabic "توقف" must cut as instantly as
|
|
88
|
+
// "stop". Everything else separates words.
|
|
89
|
+
.replace(/[^\p{L}\s]/gu, " ")
|
|
90
|
+
.split(/\s+/)
|
|
91
|
+
.filter(Boolean));
|
|
92
|
+
}
|
|
93
|
+
function startsWithSeq(words, seq) {
|
|
94
|
+
return (seq.length > 0 &&
|
|
95
|
+
seq.length <= words.length &&
|
|
96
|
+
seq.every((w, i) => words[i] === w));
|
|
97
|
+
}
|
|
98
|
+
function endsWithSeq(words, seq) {
|
|
99
|
+
const offset = words.length - seq.length;
|
|
100
|
+
return (seq.length > 0 &&
|
|
101
|
+
offset >= 0 &&
|
|
102
|
+
seq.every((w, i) => words[offset + i] === w));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* True when the utterance, AS A WHOLE, is a verbal interrupt.
|
|
106
|
+
*
|
|
107
|
+
* Deterministic and conservative: lowercase, punctuation stripped, surrounding
|
|
108
|
+
* filler AND the configured wake phrases removed - "hey <name>, stop" must cut as
|
|
109
|
+
* instantly as a bare "stop", because addressing the bot by name is how people
|
|
110
|
+
* interrupt it in a meeting - then an exact phrase-set match capped at four
|
|
111
|
+
* words. A longer sentence that merely contains "stop" does not match.
|
|
112
|
+
*/
|
|
113
|
+
export const DEFAULT_FOLLOW_UP_WINDOW_MS = 12_000;
|
|
114
|
+
/**
|
|
115
|
+
* Microsoft's thread-id prefix for a meeting or channel conversation. A 1:1 call
|
|
116
|
+
* has no such thread at all, so the presence of one is the signal.
|
|
117
|
+
*/
|
|
118
|
+
const MEETING_THREAD_PREFIX = "19:";
|
|
119
|
+
/**
|
|
120
|
+
* Is this call attached to a Microsoft Teams meeting or channel thread?
|
|
121
|
+
*
|
|
122
|
+
* The group signal that actually arrives. The participant count that used to
|
|
123
|
+
* carry this does not reach a bot that joined through the meeting, so a gate
|
|
124
|
+
* keyed on it alone never fires.
|
|
125
|
+
*/
|
|
126
|
+
export function isMeetingThread(threadId) {
|
|
127
|
+
return (threadId ?? "").trim().startsWith(MEETING_THREAD_PREFIX);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Case-insensitive, word-boundary match of any wake phrase.
|
|
131
|
+
*
|
|
132
|
+
* Boundaries rather than substrings, so "assistant" matches and "assistants"
|
|
133
|
+
* does not. The boundary is expressed as "not a word character" so a non-Latin
|
|
134
|
+
* wake phrase behaves the same as a Latin one. An empty phrase list never
|
|
135
|
+
* matches.
|
|
136
|
+
*/
|
|
137
|
+
export function isAddressed(transcript, wakePhrases) {
|
|
138
|
+
if (!transcript || wakePhrases.length === 0)
|
|
139
|
+
return false;
|
|
140
|
+
const lowered = transcript.toLowerCase();
|
|
141
|
+
for (const raw of wakePhrases) {
|
|
142
|
+
const phrase = raw.trim().toLowerCase();
|
|
143
|
+
if (phrase === "")
|
|
144
|
+
continue;
|
|
145
|
+
const escaped = phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
146
|
+
if (new RegExp(`(?<![\\p{L}\\p{N}_])${escaped}(?![\\p{L}\\p{N}_])`, "u").test(lowered)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The group-call gate for one call. Holds the follow-up window's state.
|
|
154
|
+
*
|
|
155
|
+
* An agent in a meeting that answers every sentence is worse than one that says
|
|
156
|
+
* nothing. The gate keeps it quiet until somebody names it, then leaves the
|
|
157
|
+
* floor open for a short while so a follow-up does not need the name again.
|
|
158
|
+
*/
|
|
159
|
+
export class GroupGate {
|
|
160
|
+
wakePhrases;
|
|
161
|
+
requireAddress;
|
|
162
|
+
followUpWindowMs;
|
|
163
|
+
#meetingThread;
|
|
164
|
+
#humanCount = 0;
|
|
165
|
+
#lastAddressedMs;
|
|
166
|
+
constructor(options) {
|
|
167
|
+
this.wakePhrases = options.wakePhrases;
|
|
168
|
+
this.requireAddress = options.requireAddress ?? true;
|
|
169
|
+
this.followUpWindowMs =
|
|
170
|
+
options.followUpWindowMs ?? DEFAULT_FOLLOW_UP_WINDOW_MS;
|
|
171
|
+
this.#meetingThread = isMeetingThread(options.threadId);
|
|
172
|
+
}
|
|
173
|
+
/** More than one human on this call, by either signal. */
|
|
174
|
+
get isGroup() {
|
|
175
|
+
return this.#meetingThread || this.#humanCount >= 2;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Is the gate actually muting anything right now?
|
|
179
|
+
*
|
|
180
|
+
* A gate with no wake phrase configured can never be opened, so it would mute
|
|
181
|
+
* the assistant for the whole call. Treat "no trigger configured" as gate off.
|
|
182
|
+
*/
|
|
183
|
+
get active() {
|
|
184
|
+
return (this.isGroup &&
|
|
185
|
+
this.requireAddress &&
|
|
186
|
+
this.wakePhrases.some((p) => p.trim() !== ""));
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Record a participant count from call context.
|
|
190
|
+
*
|
|
191
|
+
* Corroborating, never authoritative: a count that says 1 does not clear a
|
|
192
|
+
* meeting thread. The count is the signal that goes missing, so it may add
|
|
193
|
+
* certainty and must not remove it.
|
|
194
|
+
*/
|
|
195
|
+
noteParticipants(count) {
|
|
196
|
+
this.#humanCount = Math.max(this.#humanCount, count);
|
|
197
|
+
}
|
|
198
|
+
/** Answer this turn, or stay out of the meeting? */
|
|
199
|
+
decide(transcript, nowMs) {
|
|
200
|
+
const addressed = isAddressed(transcript, this.wakePhrases);
|
|
201
|
+
if (!this.active)
|
|
202
|
+
return { respond: true, addressed };
|
|
203
|
+
if (addressed) {
|
|
204
|
+
this.#lastAddressedMs = nowMs;
|
|
205
|
+
return { respond: true, addressed: true };
|
|
206
|
+
}
|
|
207
|
+
if (this.#lastAddressedMs !== undefined &&
|
|
208
|
+
nowMs - this.#lastAddressedMs <= this.followUpWindowMs) {
|
|
209
|
+
return { respond: true, addressed: false };
|
|
210
|
+
}
|
|
211
|
+
return { respond: false, addressed: false };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export function isVerbalInterrupt(text, wakePhrases) {
|
|
215
|
+
let core = normalizeWords(text);
|
|
216
|
+
const wake = (wakePhrases ?? [])
|
|
217
|
+
.map(normalizeWords)
|
|
218
|
+
.filter((seq) => seq.length > 0);
|
|
219
|
+
// Strip surrounding filler and wake tokens until stable - they interleave
|
|
220
|
+
// ("ok <name> please stop").
|
|
221
|
+
let changed = true;
|
|
222
|
+
while (changed && core.length > 0) {
|
|
223
|
+
changed = false;
|
|
224
|
+
while (core.length > 0 && FILLER_TOKENS.has(core[0] ?? "")) {
|
|
225
|
+
core.shift();
|
|
226
|
+
changed = true;
|
|
227
|
+
}
|
|
228
|
+
while (core.length > 0 && FILLER_TOKENS.has(core[core.length - 1] ?? "")) {
|
|
229
|
+
core.pop();
|
|
230
|
+
changed = true;
|
|
231
|
+
}
|
|
232
|
+
for (const seq of wake) {
|
|
233
|
+
if (startsWithSeq(core, seq)) {
|
|
234
|
+
core = core.slice(seq.length);
|
|
235
|
+
changed = true;
|
|
236
|
+
}
|
|
237
|
+
else if (endsWithSeq(core, seq)) {
|
|
238
|
+
core = core.slice(0, core.length - seq.length);
|
|
239
|
+
changed = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// The wake word alone ("<name>?") is an address, not an interrupt.
|
|
244
|
+
if (core.length === 0 || core.length > 4) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return INTERRUPT_PHRASES.has(core.join(" "));
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=gate.js.map
|
package/dist/gate.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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"}
|