@qearlyao/familiar 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/config.example.toml +2 -2
- package/dist/agent/payload-normalizers.js +52 -0
- package/dist/agent/session-helpers.js +86 -0
- package/dist/agent/tool-descriptions.js +4 -0
- package/dist/agent/tools.js +30 -0
- package/dist/agent/transcript-log.js +93 -0
- package/dist/agent/types.js +1 -0
- package/dist/agent-core.js +82 -0
- package/dist/agent-work-queue.js +55 -0
- package/dist/agent.js +91 -322
- package/dist/browser-tools.js +80 -28
- package/dist/chat-log.js +15 -3
- package/dist/cli.js +36 -6
- package/dist/config/enums.js +35 -0
- package/dist/config/interpolate.js +15 -0
- package/dist/config/model-refs.js +11 -0
- package/dist/config/readers.js +116 -0
- package/dist/config/sections.js +113 -0
- package/dist/config/types.js +1 -0
- package/dist/config-registry.js +26 -7
- package/dist/config.js +8 -271
- package/dist/discord/channel.js +32 -0
- package/dist/discord/chunking.js +163 -0
- package/dist/discord/client.js +44 -0
- package/dist/discord/commands.js +181 -0
- package/dist/discord/inbound.js +44 -0
- package/dist/discord/send.js +106 -0
- package/dist/discord/turn.js +55 -0
- package/dist/discord.js +266 -1186
- package/dist/ids.js +11 -0
- package/dist/image-gen.js +90 -10
- package/dist/index.js +1 -0
- package/dist/memory/index/store.js +21 -17
- package/dist/memory/index/vector-codec.js +2 -2
- package/dist/memory/lcm/context-transformer.js +6 -2
- package/dist/memory/lcm/segment-manager.js +6 -2
- package/dist/memory/lcm/store/index-ids.js +6 -0
- package/dist/memory/lcm/store/inserts.js +31 -0
- package/dist/memory/lcm/store/normalizers.js +91 -0
- package/dist/memory/lcm/store/row-mappers.js +114 -0
- package/dist/memory/lcm/store/row-types.js +1 -0
- package/dist/memory/lcm/store/serialization.js +37 -0
- package/dist/memory/lcm/store/snapshots.js +73 -0
- package/dist/memory/lcm/store.js +20 -360
- package/dist/owner-identity.js +29 -0
- package/dist/runtime-manager.js +51 -0
- package/dist/runtime.js +89 -41
- package/dist/scheduler-runner.js +243 -0
- package/dist/scheduler.js +1 -1
- package/dist/service.js +1 -0
- package/dist/settings.js +3 -0
- package/dist/util/fs.js +1 -1
- package/dist/web/event-hub.js +246 -0
- package/dist/{web-http.js → web/http.js} +19 -5
- package/dist/web/memes.js +25 -0
- package/dist/web/messages.js +345 -0
- package/dist/web/multipart.js +80 -0
- package/dist/web/payloads.js +34 -0
- package/dist/{web-static.js → web/static.js} +19 -14
- package/dist/web/stream.js +69 -0
- package/dist/web-tools/cache.js +42 -0
- package/dist/web-tools/config.js +16 -0
- package/dist/web-tools/fetch-providers.js +119 -0
- package/dist/web-tools/format.js +88 -0
- package/dist/web-tools/http.js +81 -0
- package/dist/web-tools/routing.js +29 -0
- package/dist/web-tools/safety.js +73 -0
- package/dist/web-tools/search-providers.js +277 -0
- package/dist/web-tools/types.js +54 -0
- package/dist/web-tools/util.js +23 -0
- package/dist/web-tools.js +9 -798
- package/dist/web.js +416 -984
- package/npm-shrinkwrap.json +242 -201
- package/package.json +4 -4
- package/web/dist/assets/index-CSkxUQCr.js +63 -0
- package/web/dist/assets/index-DllM6RqL.css +2 -0
- package/web/dist/index.html +6 -3
- package/web/dist/assets/index-B23WT77N.js +0 -63
- package/web/dist/assets/index-D3MotFzN.css +0 -2
- /package/dist/{web-auth.js → web/auth.js} +0 -0
- /package/dist/{web-events.js → web/events.js} +0 -0
- /package/dist/{web-types.js → web/types.js} +0 -0
package/dist/config.js
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
4
|
import { parse } from "smol-toml";
|
|
5
|
-
import {
|
|
5
|
+
import { BROWSER_BACKENDS, BROWSER_WINDOW_MODES, CACHE_RETENTIONS, DISCORD_CHANNEL_TRIGGERS, DISCORD_CHUNK_MODES, DISCORD_DISPATCH_MODES, DISCORD_REPLY_MODES, IMAGE_GEN_APIS, MEDIA_UNDERSTANDING_PROVIDERS, MEMORY_EMBEDDING_FORMATS, THINKING_LEVELS, TTS_PROVIDERS, WEB_AUTH_MODES, } from "./config/enums.js";
|
|
6
|
+
import { interpolateValue } from "./config/interpolate.js";
|
|
7
|
+
import { maybeParseProviderModelRef, parseProviderModelRef } from "./config/model-refs.js";
|
|
8
|
+
import { assertKnownKeys, readBoolean, readConfigString, readFraction, readInteger, readIntegerInRange, readNumberInRange, readOptionalConfigString, readOptionalInteger, readOptionalString, readPositiveNumber, readString, readStringArray, readStringRecord, resolveWorkspacePath, } from "./config/readers.js";
|
|
9
|
+
import { defaultBrowserAllowedSites, readCronJobs, readPromptOverrides } from "./config/sections.js";
|
|
10
|
+
import { resolveProviderSetting } from "./models.js";
|
|
6
11
|
import { readEnum } from "./util/guards.js";
|
|
7
12
|
const loggedConfigWarnings = new Set();
|
|
8
13
|
const DEFAULT_MEMORY_EMBEDDING_BASE_URLS = {
|
|
@@ -11,280 +16,12 @@ const DEFAULT_MEMORY_EMBEDDING_BASE_URLS = {
|
|
|
11
16
|
const DEFAULT_MEMORY_EMBEDDING_API_KEY_ENVS = {
|
|
12
17
|
google: "GEMINI_API_KEY",
|
|
13
18
|
};
|
|
14
|
-
function interpolateEnv(value) {
|
|
15
|
-
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_match, name, fallback) => {
|
|
16
|
-
return process.env[name] ?? fallback ?? "";
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
function interpolateValue(value) {
|
|
20
|
-
if (typeof value === "string")
|
|
21
|
-
return interpolateEnv(value);
|
|
22
|
-
if (Array.isArray(value))
|
|
23
|
-
return value.map(interpolateValue);
|
|
24
|
-
if (value && typeof value === "object") {
|
|
25
|
-
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, interpolateValue(child)]));
|
|
26
|
-
}
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
function readString(value, path) {
|
|
30
|
-
if (typeof value !== "string" || value.trim() === "") {
|
|
31
|
-
throw new Error(`Missing required config value: ${path}`);
|
|
32
|
-
}
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
function readOptionalString(value, fallback) {
|
|
36
|
-
return typeof value === "string" && value.trim() !== "" ? value : fallback;
|
|
37
|
-
}
|
|
38
|
-
function readOptionalConfigString(value, path) {
|
|
39
|
-
if (value === undefined)
|
|
40
|
-
return undefined;
|
|
41
|
-
if (typeof value !== "string")
|
|
42
|
-
throw new Error(`Config value ${path} must be a string`);
|
|
43
|
-
const trimmed = value.trim();
|
|
44
|
-
return trimmed ? trimmed : undefined;
|
|
45
|
-
}
|
|
46
|
-
function readConfigString(value, fallback, path) {
|
|
47
|
-
const read = readOptionalConfigString(value, path);
|
|
48
|
-
return read ?? fallback;
|
|
49
|
-
}
|
|
50
|
-
function readStringArray(value, path) {
|
|
51
|
-
if (value === undefined)
|
|
52
|
-
return [];
|
|
53
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
54
|
-
throw new Error(`Config value must be a string array: ${path}`);
|
|
55
|
-
}
|
|
56
|
-
return value;
|
|
57
|
-
}
|
|
58
|
-
function readStringRecord(value, path) {
|
|
59
|
-
if (value === undefined)
|
|
60
|
-
return {};
|
|
61
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
62
|
-
throw new Error(`Config value must be a string map: ${path}`);
|
|
63
|
-
}
|
|
64
|
-
const entries = Object.entries(value);
|
|
65
|
-
for (const [key, child] of entries) {
|
|
66
|
-
if (typeof child !== "string")
|
|
67
|
-
throw new Error(`Config value must be a string map: ${path}.${key}`);
|
|
68
|
-
}
|
|
69
|
-
return Object.fromEntries(entries);
|
|
70
|
-
}
|
|
71
19
|
function warnOnce(key, message) {
|
|
72
20
|
if (loggedConfigWarnings.has(key))
|
|
73
21
|
return;
|
|
74
22
|
loggedConfigWarnings.add(key);
|
|
75
23
|
console.warn(message);
|
|
76
24
|
}
|
|
77
|
-
const CACHE_RETENTIONS = ["none", "short", "long"];
|
|
78
|
-
const THINKING_LEVELS = [
|
|
79
|
-
"off",
|
|
80
|
-
"minimal",
|
|
81
|
-
"low",
|
|
82
|
-
"medium",
|
|
83
|
-
"high",
|
|
84
|
-
"xhigh",
|
|
85
|
-
];
|
|
86
|
-
const DISCORD_REPLY_MODES = ["plain", "reply"];
|
|
87
|
-
const DISCORD_CHUNK_MODES = ["simple", "paragraph", "newline"];
|
|
88
|
-
const DISCORD_DISPATCH_MODES = ["steer", "queue", "collect"];
|
|
89
|
-
const DISCORD_CHANNEL_TRIGGERS = ["mention", "always"];
|
|
90
|
-
const CRON_FREQUENCIES = ["once", "hourly", "daily", "weekly", "monthly"];
|
|
91
|
-
const CRON_DELIVERY_MODES = ["queue", "follow_up"];
|
|
92
|
-
const WEB_AUTH_MODES = ["tailscale-only", "bearer", "public-2fa"];
|
|
93
|
-
const TTS_PROVIDERS = ["elevenlabs"];
|
|
94
|
-
const IMAGE_GEN_APIS = ["openrouter-images"];
|
|
95
|
-
const MEDIA_UNDERSTANDING_PROVIDERS = ["groq", "google"];
|
|
96
|
-
const MEMORY_EMBEDDING_FORMATS = ["gemini", "openai", "voyage"];
|
|
97
|
-
const BROWSER_BACKENDS = ["opencli", "browser-harness"];
|
|
98
|
-
const BROWSER_WINDOW_MODES = ["foreground", "background"];
|
|
99
|
-
function readBoolean(value, fallback, path) {
|
|
100
|
-
if (value === undefined)
|
|
101
|
-
return fallback;
|
|
102
|
-
if (typeof value === "boolean")
|
|
103
|
-
return value;
|
|
104
|
-
throw new Error(`Config value ${path} must be a boolean`);
|
|
105
|
-
}
|
|
106
|
-
function readInteger(value, fallback, path, min = 0) {
|
|
107
|
-
if (value === undefined)
|
|
108
|
-
return fallback;
|
|
109
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
|
|
110
|
-
throw new Error(`Config value ${path} must be an integer >= ${min}`);
|
|
111
|
-
}
|
|
112
|
-
return value;
|
|
113
|
-
}
|
|
114
|
-
function readNumberInRange(value, fallback, path, min, max) {
|
|
115
|
-
if (value === undefined)
|
|
116
|
-
return fallback;
|
|
117
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
118
|
-
throw new Error(`Config value ${path} must be a number between ${min} and ${max}`);
|
|
119
|
-
}
|
|
120
|
-
return value;
|
|
121
|
-
}
|
|
122
|
-
function readFraction(value, fallback, path) {
|
|
123
|
-
if (value === undefined)
|
|
124
|
-
return fallback;
|
|
125
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) {
|
|
126
|
-
throw new Error(`Config value ${path} must be a number > 0 and <= 1`);
|
|
127
|
-
}
|
|
128
|
-
return value;
|
|
129
|
-
}
|
|
130
|
-
function readPositiveNumber(value, fallback, path) {
|
|
131
|
-
if (value === undefined)
|
|
132
|
-
return fallback;
|
|
133
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
134
|
-
throw new Error(`Config value ${path} must be a positive number`);
|
|
135
|
-
}
|
|
136
|
-
return value;
|
|
137
|
-
}
|
|
138
|
-
function readOptionalInteger(value, path, min = 0) {
|
|
139
|
-
if (value === undefined)
|
|
140
|
-
return undefined;
|
|
141
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
|
|
142
|
-
throw new Error(`Config value ${path} must be an integer >= ${min}`);
|
|
143
|
-
}
|
|
144
|
-
return value;
|
|
145
|
-
}
|
|
146
|
-
function readIntegerInRange(value, fallback, path, min, max) {
|
|
147
|
-
const read = readInteger(value, fallback, path, min);
|
|
148
|
-
if (read > max)
|
|
149
|
-
throw new Error(`Config value ${path} must be an integer <= ${max}`);
|
|
150
|
-
return read;
|
|
151
|
-
}
|
|
152
|
-
function readOptionalIntegerInRange(value, path, min, max) {
|
|
153
|
-
const read = readOptionalInteger(value, path, min);
|
|
154
|
-
if (read !== undefined && read > max)
|
|
155
|
-
throw new Error(`Config value ${path} must be an integer <= ${max}`);
|
|
156
|
-
return read;
|
|
157
|
-
}
|
|
158
|
-
function assertCronTime(value, path) {
|
|
159
|
-
if (value === undefined)
|
|
160
|
-
return;
|
|
161
|
-
if (!/^([01]?\d|2[0-3]):([0-5]\d)$/.test(value)) {
|
|
162
|
-
throw new Error(`Config value ${path} must be HH:MM local time`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function assertCronRunAt(value, path) {
|
|
166
|
-
if (value === undefined)
|
|
167
|
-
return;
|
|
168
|
-
if (Number.isFinite(Date.parse(value)))
|
|
169
|
-
return;
|
|
170
|
-
if (/^\d{4}-\d{2}-\d{2}[ T]([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/.test(value))
|
|
171
|
-
return;
|
|
172
|
-
throw new Error(`Config value ${path} must be an ISO timestamp or YYYY-MM-DD HH:MM local time`);
|
|
173
|
-
}
|
|
174
|
-
function resolveWorkspacePath(workspacePath, filePath) {
|
|
175
|
-
return isAbsolute(filePath) ? filePath : resolve(workspacePath, filePath);
|
|
176
|
-
}
|
|
177
|
-
function parseProviderModelRef(value, path) {
|
|
178
|
-
const parsed = maybeParseProviderModelRef(value);
|
|
179
|
-
if (parsed)
|
|
180
|
-
return parsed;
|
|
181
|
-
throw new Error(`Config value ${path} must be a provider/model id`);
|
|
182
|
-
}
|
|
183
|
-
function maybeParseProviderModelRef(value) {
|
|
184
|
-
const parsed = parseModelRef(value);
|
|
185
|
-
return parsed ? { provider: parsed.provider, modelId: parsed.id, key: parsed.key } : undefined;
|
|
186
|
-
}
|
|
187
|
-
function assertKnownKeys(value, path, knownKeys) {
|
|
188
|
-
const known = new Set(knownKeys);
|
|
189
|
-
for (const key of Object.keys(value)) {
|
|
190
|
-
if (!known.has(key))
|
|
191
|
-
throw new Error(`Unknown config value: ${path}.${key}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function readPromptOverrides(value, workspacePath, prefix) {
|
|
195
|
-
const prompt = readOptionalConfigString(value.prompt, `${prefix}.prompt`);
|
|
196
|
-
const promptPath = readOptionalConfigString(value.prompt_path, `${prefix}.prompt_path`);
|
|
197
|
-
const systemPrompt = readOptionalConfigString(value.system_prompt, `${prefix}.system_prompt`);
|
|
198
|
-
const systemPromptPath = readOptionalConfigString(value.system_prompt_path, `${prefix}.system_prompt_path`);
|
|
199
|
-
if (prompt && promptPath)
|
|
200
|
-
throw new Error(`Set either ${prefix}.prompt or ${prefix}.prompt_path, not both`);
|
|
201
|
-
if (systemPrompt && systemPromptPath) {
|
|
202
|
-
throw new Error(`Set either ${prefix}.system_prompt or ${prefix}.system_prompt_path, not both`);
|
|
203
|
-
}
|
|
204
|
-
return {
|
|
205
|
-
...(prompt ? { prompt } : {}),
|
|
206
|
-
...(promptPath ? { promptPath: resolveWorkspacePath(workspacePath, promptPath) } : {}),
|
|
207
|
-
...(systemPrompt ? { systemPrompt } : {}),
|
|
208
|
-
...(systemPromptPath ? { systemPromptPath: resolveWorkspacePath(workspacePath, systemPromptPath) } : {}),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function readCronJobs(cron) {
|
|
212
|
-
const rawJobs = cron.jobs;
|
|
213
|
-
if (rawJobs === undefined)
|
|
214
|
-
return [];
|
|
215
|
-
if (!Array.isArray(rawJobs))
|
|
216
|
-
throw new Error("Config value cron.jobs must be an array");
|
|
217
|
-
const seen = new Set();
|
|
218
|
-
return rawJobs.map((rawJob, index) => {
|
|
219
|
-
if (!rawJob || typeof rawJob !== "object" || Array.isArray(rawJob)) {
|
|
220
|
-
throw new Error(`Config value cron.jobs[${index}] must be a table`);
|
|
221
|
-
}
|
|
222
|
-
const job = rawJob;
|
|
223
|
-
const prefix = `cron.jobs[${index}]`;
|
|
224
|
-
assertKnownKeys(job, prefix, [
|
|
225
|
-
"id",
|
|
226
|
-
"enabled",
|
|
227
|
-
"frequency",
|
|
228
|
-
"delivery_mode",
|
|
229
|
-
"prompt",
|
|
230
|
-
"run_at",
|
|
231
|
-
"time",
|
|
232
|
-
"minute",
|
|
233
|
-
"weekday",
|
|
234
|
-
"day",
|
|
235
|
-
]);
|
|
236
|
-
const id = readString(job.id, `${prefix}.id`);
|
|
237
|
-
if (!/^[A-Za-z0-9._=-]+$/.test(id)) {
|
|
238
|
-
throw new Error(`Config value ${prefix}.id may only contain letters, numbers, dot, underscore, equals, or dash`);
|
|
239
|
-
}
|
|
240
|
-
if (seen.has(id))
|
|
241
|
-
throw new Error(`Duplicate cron job id: ${id}`);
|
|
242
|
-
seen.add(id);
|
|
243
|
-
const frequency = readEnum(readConfigString(job.frequency, "once", `${prefix}.frequency`), `${prefix}.frequency`, CRON_FREQUENCIES);
|
|
244
|
-
const runAt = readOptionalConfigString(job.run_at, `${prefix}.run_at`);
|
|
245
|
-
const time = readOptionalConfigString(job.time, `${prefix}.time`);
|
|
246
|
-
assertCronRunAt(runAt, `${prefix}.run_at`);
|
|
247
|
-
assertCronTime(time, `${prefix}.time`);
|
|
248
|
-
if (frequency === "once" && !runAt)
|
|
249
|
-
throw new Error(`Config value ${prefix}.run_at is required for once jobs`);
|
|
250
|
-
if (frequency === "once" && time)
|
|
251
|
-
throw new Error(`Config value ${prefix}.time is only valid for repeating jobs`);
|
|
252
|
-
if (frequency !== "once" && runAt)
|
|
253
|
-
throw new Error(`Config value ${prefix}.run_at is only valid for once jobs`);
|
|
254
|
-
if (frequency !== "once" && frequency !== "hourly" && !time) {
|
|
255
|
-
throw new Error(`Config value ${prefix}.time is required for ${frequency} jobs`);
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
id,
|
|
259
|
-
enabled: readBoolean(job.enabled, true, `${prefix}.enabled`),
|
|
260
|
-
frequency,
|
|
261
|
-
deliveryMode: readEnum(readConfigString(job.delivery_mode, "queue", `${prefix}.delivery_mode`), `${prefix}.delivery_mode`, CRON_DELIVERY_MODES),
|
|
262
|
-
prompt: readString(job.prompt, `${prefix}.prompt`),
|
|
263
|
-
...(runAt ? { runAt } : {}),
|
|
264
|
-
...(time ? { time } : {}),
|
|
265
|
-
...(job.minute !== undefined
|
|
266
|
-
? { minute: readOptionalIntegerInRange(job.minute, `${prefix}.minute`, 0, 59) }
|
|
267
|
-
: {}),
|
|
268
|
-
...(job.weekday !== undefined
|
|
269
|
-
? { weekday: readOptionalIntegerInRange(job.weekday, `${prefix}.weekday`, 0, 6) }
|
|
270
|
-
: {}),
|
|
271
|
-
...(job.day !== undefined ? { day: readOptionalIntegerInRange(job.day, `${prefix}.day`, 1, 31) } : {}),
|
|
272
|
-
};
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
function defaultBrowserAllowedSites() {
|
|
276
|
-
return {
|
|
277
|
-
twitter: true,
|
|
278
|
-
xiaohongshu: true,
|
|
279
|
-
rednote: true,
|
|
280
|
-
reddit: true,
|
|
281
|
-
bilibili: true,
|
|
282
|
-
youtube: true,
|
|
283
|
-
tiktok: true,
|
|
284
|
-
douyin: true,
|
|
285
|
-
spotify: true,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
25
|
export async function loadConfig(workspacePathInput) {
|
|
289
26
|
const workspacePath = resolve(workspacePathInput);
|
|
290
27
|
const configPath = resolve(workspacePath, "config.toml");
|
|
@@ -436,7 +173,7 @@ export async function loadConfig(workspacePathInput) {
|
|
|
436
173
|
return {
|
|
437
174
|
workspacePath,
|
|
438
175
|
discord: {
|
|
439
|
-
token:
|
|
176
|
+
token: readOptionalConfigString(process.env.DISCORD_TOKEN, "DISCORD_TOKEN"),
|
|
440
177
|
ownerId,
|
|
441
178
|
allowedChannels: readStringArray(discord.allowed_channels, "discord.allowed_channels"),
|
|
442
179
|
replyMode: readEnum(readOptionalString(discord.reply_mode, "plain"), "discord.reply_mode", DISCORD_REPLY_MODES),
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ChannelType } from "discord.js";
|
|
2
|
+
import { chatChannelKey } from "../chat-log.js";
|
|
3
|
+
export function buildChannelRef(channel, channelId) {
|
|
4
|
+
const scope = channel.type === ChannelType.DM ? "dm" : channel.isThread() ? "thread" : "channel";
|
|
5
|
+
const channelName = "name" in channel ? channel.name : undefined;
|
|
6
|
+
return {
|
|
7
|
+
service: "discord",
|
|
8
|
+
scope,
|
|
9
|
+
channelId,
|
|
10
|
+
channelName: typeof channelName === "string" ? channelName : undefined,
|
|
11
|
+
threadId: channel.isThread() ? channel.id : undefined,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function getChannelRef(message) {
|
|
15
|
+
return buildChannelRef(message.channel, message.channelId);
|
|
16
|
+
}
|
|
17
|
+
export function runtimeKeyFromMessage(message) {
|
|
18
|
+
return chatChannelKey(getChannelRef(message));
|
|
19
|
+
}
|
|
20
|
+
export function isDmChannel(channel) {
|
|
21
|
+
return channel?.type === ChannelType.DM;
|
|
22
|
+
}
|
|
23
|
+
export async function fetchMessageAnchor(message, messageId) {
|
|
24
|
+
if (!messageId || message.id === messageId)
|
|
25
|
+
return message;
|
|
26
|
+
return message.channel.messages.fetch(messageId).catch(() => message);
|
|
27
|
+
}
|
|
28
|
+
export function messageMentionsBot(message, botUserId) {
|
|
29
|
+
if (message.mentions.users.has(botUserId))
|
|
30
|
+
return true;
|
|
31
|
+
return message.content.includes(`<@${botUserId}>`) || message.content.includes(`<@!${botUserId}>`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// A hard split at an arbitrary index can land between the two UTF-16 code units
|
|
2
|
+
// of an astral character (emoji, rare CJK), rendering as a broken � at the seam.
|
|
3
|
+
// Back off by one so the whole surrogate pair moves to the next chunk. Splits at
|
|
4
|
+
// whitespace land on a space and are unaffected.
|
|
5
|
+
function avoidSurrogateSplit(text, index) {
|
|
6
|
+
if (index <= 0 || index >= text.length)
|
|
7
|
+
return index;
|
|
8
|
+
const high = text.charCodeAt(index - 1);
|
|
9
|
+
const low = text.charCodeAt(index);
|
|
10
|
+
if (high >= 0xd800 && high <= 0xdbff && low >= 0xdc00 && low <= 0xdfff)
|
|
11
|
+
return index - 1;
|
|
12
|
+
return index;
|
|
13
|
+
}
|
|
14
|
+
function chunkDiscordSimple(text, limit = 2000) {
|
|
15
|
+
if (text.length <= limit)
|
|
16
|
+
return [text || "(empty response)"];
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let remaining = text;
|
|
19
|
+
while (remaining.length > 0) {
|
|
20
|
+
if (remaining.length <= limit) {
|
|
21
|
+
chunks.push(remaining);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
const breakpoint = Math.max(remaining.lastIndexOf("\n", limit), remaining.lastIndexOf(" ", limit));
|
|
25
|
+
const splitAt = breakpoint > 0 ? breakpoint : avoidSurrogateSplit(remaining, limit);
|
|
26
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
27
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
28
|
+
}
|
|
29
|
+
return chunks;
|
|
30
|
+
}
|
|
31
|
+
function splitLongBlock(block, limit) {
|
|
32
|
+
if (block.length <= limit)
|
|
33
|
+
return [block];
|
|
34
|
+
const pieces = [];
|
|
35
|
+
let lineCurrent = "";
|
|
36
|
+
for (const line of block.split("\n")) {
|
|
37
|
+
const candidate = lineCurrent ? `${lineCurrent}\n${line}` : line;
|
|
38
|
+
if (candidate.length <= limit) {
|
|
39
|
+
lineCurrent = candidate;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (lineCurrent) {
|
|
43
|
+
pieces.push(lineCurrent);
|
|
44
|
+
lineCurrent = "";
|
|
45
|
+
}
|
|
46
|
+
if (line.length <= limit) {
|
|
47
|
+
lineCurrent = line;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let remaining = line;
|
|
51
|
+
while (remaining.length > limit) {
|
|
52
|
+
let splitAt = remaining.lastIndexOf(" ", limit);
|
|
53
|
+
if (splitAt < Math.floor(limit * 0.6))
|
|
54
|
+
splitAt = avoidSurrogateSplit(remaining, limit);
|
|
55
|
+
pieces.push(remaining.slice(0, splitAt));
|
|
56
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
57
|
+
}
|
|
58
|
+
lineCurrent = remaining;
|
|
59
|
+
}
|
|
60
|
+
if (lineCurrent)
|
|
61
|
+
pieces.push(lineCurrent);
|
|
62
|
+
return pieces;
|
|
63
|
+
}
|
|
64
|
+
function chunkDiscordParagraph(text, limit = 2000) {
|
|
65
|
+
if (text.length <= limit)
|
|
66
|
+
return [text || "(empty response)"];
|
|
67
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
68
|
+
const paragraphs = normalized.split(/\n\n+/);
|
|
69
|
+
const chunks = [];
|
|
70
|
+
let current = "";
|
|
71
|
+
const pushCurrent = () => {
|
|
72
|
+
if (current.trim())
|
|
73
|
+
chunks.push(current);
|
|
74
|
+
current = "";
|
|
75
|
+
};
|
|
76
|
+
for (const paragraph of paragraphs) {
|
|
77
|
+
if (!paragraph)
|
|
78
|
+
continue;
|
|
79
|
+
for (const part of splitLongBlock(paragraph, limit)) {
|
|
80
|
+
const candidate = current ? `${current}\n\n${part}` : part;
|
|
81
|
+
if (candidate.length <= limit) {
|
|
82
|
+
current = candidate;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
pushCurrent();
|
|
86
|
+
current = part;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
pushCurrent();
|
|
91
|
+
return chunks.length > 0 ? chunks : [normalized.slice(0, limit)];
|
|
92
|
+
}
|
|
93
|
+
function splitPreservingCodeFences(text) {
|
|
94
|
+
const normalized = text.replace(/\r\n/g, "\n");
|
|
95
|
+
const segments = [];
|
|
96
|
+
const fence = /```/g;
|
|
97
|
+
let cursor = 0;
|
|
98
|
+
let inCode = false;
|
|
99
|
+
let buffer = "";
|
|
100
|
+
const flushParagraphs = (slab) => {
|
|
101
|
+
const parts = slab.split(/\n\n+/);
|
|
102
|
+
for (let i = 0; i < parts.length; i++) {
|
|
103
|
+
const part = parts[i];
|
|
104
|
+
if (i === 0) {
|
|
105
|
+
buffer += part;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
if (buffer.trim())
|
|
109
|
+
segments.push(buffer);
|
|
110
|
+
buffer = part;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
let match;
|
|
115
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex iteration
|
|
116
|
+
while ((match = fence.exec(normalized)) !== null) {
|
|
117
|
+
const slab = normalized.slice(cursor, match.index);
|
|
118
|
+
if (inCode) {
|
|
119
|
+
buffer += slab + match[0];
|
|
120
|
+
inCode = false;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
flushParagraphs(slab);
|
|
124
|
+
buffer += match[0];
|
|
125
|
+
inCode = true;
|
|
126
|
+
}
|
|
127
|
+
cursor = match.index + match[0].length;
|
|
128
|
+
}
|
|
129
|
+
const tail = normalized.slice(cursor);
|
|
130
|
+
if (inCode) {
|
|
131
|
+
buffer += tail;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
flushParagraphs(tail);
|
|
135
|
+
}
|
|
136
|
+
if (buffer.trim())
|
|
137
|
+
segments.push(buffer);
|
|
138
|
+
return segments.map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
139
|
+
}
|
|
140
|
+
function chunkDiscordNewline(text, limit = 2000) {
|
|
141
|
+
const segments = splitPreservingCodeFences(text);
|
|
142
|
+
if (segments.length === 0)
|
|
143
|
+
return [];
|
|
144
|
+
const chunks = [];
|
|
145
|
+
for (const segment of segments) {
|
|
146
|
+
if (segment.length <= limit) {
|
|
147
|
+
chunks.push(segment);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const part of splitLongBlock(segment, limit)) {
|
|
151
|
+
if (part.trim())
|
|
152
|
+
chunks.push(part);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return chunks;
|
|
156
|
+
}
|
|
157
|
+
export function chunkDiscord(config, text) {
|
|
158
|
+
if (config.discord.chunkMode === "simple")
|
|
159
|
+
return chunkDiscordSimple(text);
|
|
160
|
+
if (config.discord.chunkMode === "newline")
|
|
161
|
+
return chunkDiscordNewline(text);
|
|
162
|
+
return chunkDiscordParagraph(text);
|
|
163
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { ChannelType, Client, Events, GatewayIntentBits, Partials } from "discord.js";
|
|
3
|
+
export async function withReadyClient(token) {
|
|
4
|
+
const client = new Client({
|
|
5
|
+
intents: [
|
|
6
|
+
GatewayIntentBits.Guilds,
|
|
7
|
+
GatewayIntentBits.GuildMessages,
|
|
8
|
+
GatewayIntentBits.DirectMessages,
|
|
9
|
+
GatewayIntentBits.MessageContent,
|
|
10
|
+
],
|
|
11
|
+
partials: [Partials.Channel],
|
|
12
|
+
});
|
|
13
|
+
const readyPromise = once(client, Events.ClientReady);
|
|
14
|
+
try {
|
|
15
|
+
await client.login(token);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19
|
+
if (message.includes("Used disallowed intents")) {
|
|
20
|
+
throw new Error('Discord rejected the configured gateway intents. Enable the "Message Content Intent" in the Discord Developer Portal.');
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
if (!client.isReady()) {
|
|
25
|
+
await Promise.race([
|
|
26
|
+
readyPromise,
|
|
27
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Discord client failed to become ready")), 10000)),
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
if (!client.isReady())
|
|
31
|
+
throw new Error("Discord client failed to become ready");
|
|
32
|
+
return client;
|
|
33
|
+
}
|
|
34
|
+
export function isAllowedMessage(config, message, botUserId) {
|
|
35
|
+
if (message.author.id === botUserId)
|
|
36
|
+
return false;
|
|
37
|
+
if (message.author.bot && !config.discord.allowBotMessages)
|
|
38
|
+
return false;
|
|
39
|
+
if (message.channel.type === ChannelType.DM && message.author.id !== config.discord.ownerId)
|
|
40
|
+
return false;
|
|
41
|
+
if (message.channel.type === ChannelType.DM)
|
|
42
|
+
return true;
|
|
43
|
+
return config.discord.allowedChannels.includes(message.channelId);
|
|
44
|
+
}
|