@qearlyao/familiar 0.2.5 → 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 +7 -8
- 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/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/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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const CACHE_RETENTIONS = ["none", "short", "long"];
|
|
2
|
+
export const THINKING_LEVELS = [
|
|
3
|
+
"off",
|
|
4
|
+
"minimal",
|
|
5
|
+
"low",
|
|
6
|
+
"medium",
|
|
7
|
+
"high",
|
|
8
|
+
"xhigh",
|
|
9
|
+
];
|
|
10
|
+
export const DISCORD_REPLY_MODES = ["plain", "reply"];
|
|
11
|
+
export const DISCORD_CHUNK_MODES = ["simple", "paragraph", "newline"];
|
|
12
|
+
export const DISCORD_DISPATCH_MODES = ["steer", "queue", "collect"];
|
|
13
|
+
export const DISCORD_CHANNEL_TRIGGERS = ["mention", "always"];
|
|
14
|
+
export const CRON_FREQUENCIES = [
|
|
15
|
+
"once",
|
|
16
|
+
"hourly",
|
|
17
|
+
"daily",
|
|
18
|
+
"weekly",
|
|
19
|
+
"monthly",
|
|
20
|
+
];
|
|
21
|
+
export const CRON_DELIVERY_MODES = ["queue", "follow_up"];
|
|
22
|
+
export const WEB_AUTH_MODES = ["tailscale-only", "bearer", "public-2fa"];
|
|
23
|
+
export const TTS_PROVIDERS = ["elevenlabs"];
|
|
24
|
+
export const IMAGE_GEN_APIS = ["openrouter-images"];
|
|
25
|
+
export const MEDIA_UNDERSTANDING_PROVIDERS = [
|
|
26
|
+
"groq",
|
|
27
|
+
"google",
|
|
28
|
+
];
|
|
29
|
+
export const MEMORY_EMBEDDING_FORMATS = [
|
|
30
|
+
"gemini",
|
|
31
|
+
"openai",
|
|
32
|
+
"voyage",
|
|
33
|
+
];
|
|
34
|
+
export const BROWSER_BACKENDS = ["opencli", "browser-harness"];
|
|
35
|
+
export const BROWSER_WINDOW_MODES = ["foreground", "background"];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function interpolateEnv(value) {
|
|
2
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_match, name, fallback) => {
|
|
3
|
+
return process.env[name] ?? fallback ?? "";
|
|
4
|
+
});
|
|
5
|
+
}
|
|
6
|
+
export function interpolateValue(value) {
|
|
7
|
+
if (typeof value === "string")
|
|
8
|
+
return interpolateEnv(value);
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return value.map(interpolateValue);
|
|
11
|
+
if (value && typeof value === "object") {
|
|
12
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, interpolateValue(child)]));
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parseModelRef } from "../models.js";
|
|
2
|
+
export function parseProviderModelRef(value, path) {
|
|
3
|
+
const parsed = maybeParseProviderModelRef(value);
|
|
4
|
+
if (parsed)
|
|
5
|
+
return parsed;
|
|
6
|
+
throw new Error(`Config value ${path} must be a provider/model id`);
|
|
7
|
+
}
|
|
8
|
+
export function maybeParseProviderModelRef(value) {
|
|
9
|
+
const parsed = parseModelRef(value);
|
|
10
|
+
return parsed ? { provider: parsed.provider, modelId: parsed.id, key: parsed.key } : undefined;
|
|
11
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
2
|
+
export function resolveWorkspacePath(workspacePath, filePath) {
|
|
3
|
+
return isAbsolute(filePath) ? filePath : resolve(workspacePath, filePath);
|
|
4
|
+
}
|
|
5
|
+
export function readString(value, path) {
|
|
6
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
7
|
+
throw new Error(`Missing required config value: ${path}`);
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
export function readOptionalString(value, fallback) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return fallback;
|
|
14
|
+
if (typeof value !== "string")
|
|
15
|
+
throw new Error(`Config value must be a string: ${JSON.stringify(value)}`);
|
|
16
|
+
return value.trim() !== "" ? value : fallback;
|
|
17
|
+
}
|
|
18
|
+
export function readOptionalConfigString(value, path) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (typeof value !== "string")
|
|
22
|
+
throw new Error(`Config value ${path} must be a string`);
|
|
23
|
+
const trimmed = value.trim();
|
|
24
|
+
return trimmed ? trimmed : undefined;
|
|
25
|
+
}
|
|
26
|
+
export function readConfigString(value, fallback, path) {
|
|
27
|
+
const read = readOptionalConfigString(value, path);
|
|
28
|
+
return read ?? fallback;
|
|
29
|
+
}
|
|
30
|
+
export function readStringArray(value, path) {
|
|
31
|
+
if (value === undefined)
|
|
32
|
+
return [];
|
|
33
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
34
|
+
throw new Error(`Config value must be a string array: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function readStringRecord(value, path) {
|
|
39
|
+
if (value === undefined)
|
|
40
|
+
return {};
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
42
|
+
throw new Error(`Config value must be a string map: ${path}`);
|
|
43
|
+
}
|
|
44
|
+
const entries = Object.entries(value);
|
|
45
|
+
for (const [key, child] of entries) {
|
|
46
|
+
if (typeof child !== "string")
|
|
47
|
+
throw new Error(`Config value must be a string map: ${path}.${key}`);
|
|
48
|
+
}
|
|
49
|
+
return Object.fromEntries(entries);
|
|
50
|
+
}
|
|
51
|
+
export function readBoolean(value, fallback, path) {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return fallback;
|
|
54
|
+
if (typeof value === "boolean")
|
|
55
|
+
return value;
|
|
56
|
+
throw new Error(`Config value ${path} must be a boolean`);
|
|
57
|
+
}
|
|
58
|
+
export function readInteger(value, fallback, path, min = 0) {
|
|
59
|
+
if (value === undefined)
|
|
60
|
+
return fallback;
|
|
61
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
|
|
62
|
+
throw new Error(`Config value ${path} must be an integer >= ${min}`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
export function readNumberInRange(value, fallback, path, min, max) {
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return fallback;
|
|
69
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
|
70
|
+
throw new Error(`Config value ${path} must be a number between ${min} and ${max}`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
export function readFraction(value, fallback, path) {
|
|
75
|
+
if (value === undefined)
|
|
76
|
+
return fallback;
|
|
77
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > 1) {
|
|
78
|
+
throw new Error(`Config value ${path} must be a number > 0 and <= 1`);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
export function readPositiveNumber(value, fallback, path) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return fallback;
|
|
85
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
86
|
+
throw new Error(`Config value ${path} must be a positive number`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
export function readOptionalInteger(value, path, min = 0) {
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min) {
|
|
94
|
+
throw new Error(`Config value ${path} must be an integer >= ${min}`);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
export function readIntegerInRange(value, fallback, path, min, max) {
|
|
99
|
+
const read = readInteger(value, fallback, path, min);
|
|
100
|
+
if (read > max)
|
|
101
|
+
throw new Error(`Config value ${path} must be an integer <= ${max}`);
|
|
102
|
+
return read;
|
|
103
|
+
}
|
|
104
|
+
export function readOptionalIntegerInRange(value, path, min, max) {
|
|
105
|
+
const read = readOptionalInteger(value, path, min);
|
|
106
|
+
if (read !== undefined && read > max)
|
|
107
|
+
throw new Error(`Config value ${path} must be an integer <= ${max}`);
|
|
108
|
+
return read;
|
|
109
|
+
}
|
|
110
|
+
export function assertKnownKeys(value, path, knownKeys) {
|
|
111
|
+
const known = new Set(knownKeys);
|
|
112
|
+
for (const key of Object.keys(value)) {
|
|
113
|
+
if (!known.has(key))
|
|
114
|
+
throw new Error(`Unknown config value: ${path}.${key}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readEnum } from "../util/guards.js";
|
|
2
|
+
import { CRON_DELIVERY_MODES, CRON_FREQUENCIES } from "./enums.js";
|
|
3
|
+
import { assertKnownKeys, readBoolean, readConfigString, readOptionalConfigString, readOptionalIntegerInRange, readString, resolveWorkspacePath, } from "./readers.js";
|
|
4
|
+
function assertCronTime(value, path) {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return;
|
|
7
|
+
if (!/^([01]?\d|2[0-3]):([0-5]\d)$/.test(value)) {
|
|
8
|
+
throw new Error(`Config value ${path} must be HH:MM local time`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function assertCronRunAt(value, path) {
|
|
12
|
+
if (value === undefined)
|
|
13
|
+
return;
|
|
14
|
+
if (Number.isFinite(Date.parse(value)))
|
|
15
|
+
return;
|
|
16
|
+
if (/^\d{4}-\d{2}-\d{2}[ T]([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d))?$/.test(value))
|
|
17
|
+
return;
|
|
18
|
+
throw new Error(`Config value ${path} must be an ISO timestamp or YYYY-MM-DD HH:MM local time`);
|
|
19
|
+
}
|
|
20
|
+
export function readPromptOverrides(value, workspacePath, prefix) {
|
|
21
|
+
const prompt = readOptionalConfigString(value.prompt, `${prefix}.prompt`);
|
|
22
|
+
const promptPath = readOptionalConfigString(value.prompt_path, `${prefix}.prompt_path`);
|
|
23
|
+
const systemPrompt = readOptionalConfigString(value.system_prompt, `${prefix}.system_prompt`);
|
|
24
|
+
const systemPromptPath = readOptionalConfigString(value.system_prompt_path, `${prefix}.system_prompt_path`);
|
|
25
|
+
if (prompt && promptPath)
|
|
26
|
+
throw new Error(`Set either ${prefix}.prompt or ${prefix}.prompt_path, not both`);
|
|
27
|
+
if (systemPrompt && systemPromptPath) {
|
|
28
|
+
throw new Error(`Set either ${prefix}.system_prompt or ${prefix}.system_prompt_path, not both`);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...(prompt ? { prompt } : {}),
|
|
32
|
+
...(promptPath ? { promptPath: resolveWorkspacePath(workspacePath, promptPath) } : {}),
|
|
33
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
34
|
+
...(systemPromptPath ? { systemPromptPath: resolveWorkspacePath(workspacePath, systemPromptPath) } : {}),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function readCronJobs(cron) {
|
|
38
|
+
const rawJobs = cron.jobs;
|
|
39
|
+
if (rawJobs === undefined)
|
|
40
|
+
return [];
|
|
41
|
+
if (!Array.isArray(rawJobs))
|
|
42
|
+
throw new Error("Config value cron.jobs must be an array");
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
return rawJobs.map((rawJob, index) => {
|
|
45
|
+
if (!rawJob || typeof rawJob !== "object" || Array.isArray(rawJob)) {
|
|
46
|
+
throw new Error(`Config value cron.jobs[${index}] must be a table`);
|
|
47
|
+
}
|
|
48
|
+
const job = rawJob;
|
|
49
|
+
const prefix = `cron.jobs[${index}]`;
|
|
50
|
+
assertKnownKeys(job, prefix, [
|
|
51
|
+
"id",
|
|
52
|
+
"enabled",
|
|
53
|
+
"frequency",
|
|
54
|
+
"delivery_mode",
|
|
55
|
+
"prompt",
|
|
56
|
+
"run_at",
|
|
57
|
+
"time",
|
|
58
|
+
"minute",
|
|
59
|
+
"weekday",
|
|
60
|
+
"day",
|
|
61
|
+
]);
|
|
62
|
+
const id = readString(job.id, `${prefix}.id`);
|
|
63
|
+
if (!/^[A-Za-z0-9._=-]+$/.test(id)) {
|
|
64
|
+
throw new Error(`Config value ${prefix}.id may only contain letters, numbers, dot, underscore, equals, or dash`);
|
|
65
|
+
}
|
|
66
|
+
if (seen.has(id))
|
|
67
|
+
throw new Error(`Duplicate cron job id: ${id}`);
|
|
68
|
+
seen.add(id);
|
|
69
|
+
const frequency = readEnum(readConfigString(job.frequency, "once", `${prefix}.frequency`), `${prefix}.frequency`, CRON_FREQUENCIES);
|
|
70
|
+
const runAt = readOptionalConfigString(job.run_at, `${prefix}.run_at`);
|
|
71
|
+
const time = readOptionalConfigString(job.time, `${prefix}.time`);
|
|
72
|
+
assertCronRunAt(runAt, `${prefix}.run_at`);
|
|
73
|
+
assertCronTime(time, `${prefix}.time`);
|
|
74
|
+
if (frequency === "once" && !runAt)
|
|
75
|
+
throw new Error(`Config value ${prefix}.run_at is required for once jobs`);
|
|
76
|
+
if (frequency === "once" && time)
|
|
77
|
+
throw new Error(`Config value ${prefix}.time is only valid for repeating jobs`);
|
|
78
|
+
if (frequency !== "once" && runAt)
|
|
79
|
+
throw new Error(`Config value ${prefix}.run_at is only valid for once jobs`);
|
|
80
|
+
if (frequency !== "once" && frequency !== "hourly" && !time) {
|
|
81
|
+
throw new Error(`Config value ${prefix}.time is required for ${frequency} jobs`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
id,
|
|
85
|
+
enabled: readBoolean(job.enabled, true, `${prefix}.enabled`),
|
|
86
|
+
frequency,
|
|
87
|
+
deliveryMode: readEnum(readConfigString(job.delivery_mode, "queue", `${prefix}.delivery_mode`), `${prefix}.delivery_mode`, CRON_DELIVERY_MODES),
|
|
88
|
+
prompt: readString(job.prompt, `${prefix}.prompt`),
|
|
89
|
+
...(runAt ? { runAt } : {}),
|
|
90
|
+
...(time ? { time } : {}),
|
|
91
|
+
...(job.minute !== undefined
|
|
92
|
+
? { minute: readOptionalIntegerInRange(job.minute, `${prefix}.minute`, 0, 59) }
|
|
93
|
+
: {}),
|
|
94
|
+
...(job.weekday !== undefined
|
|
95
|
+
? { weekday: readOptionalIntegerInRange(job.weekday, `${prefix}.weekday`, 0, 6) }
|
|
96
|
+
: {}),
|
|
97
|
+
...(job.day !== undefined ? { day: readOptionalIntegerInRange(job.day, `${prefix}.day`, 1, 31) } : {}),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
export function defaultBrowserAllowedSites() {
|
|
102
|
+
return {
|
|
103
|
+
twitter: true,
|
|
104
|
+
xiaohongshu: true,
|
|
105
|
+
rednote: true,
|
|
106
|
+
reddit: true,
|
|
107
|
+
bilibili: true,
|
|
108
|
+
youtube: true,
|
|
109
|
+
tiktok: true,
|
|
110
|
+
douyin: true,
|
|
111
|
+
spotify: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/config-registry.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfigOverrides } from "./config-overrides.js";
|
|
1
|
+
import { clearConfigOverride, loadConfigOverrides, setConfigOverride } from "./config-overrides.js";
|
|
2
2
|
import { isAllowedModel, parseModelRef, resolveProviderSetting } from "./models.js";
|
|
3
3
|
function requireBoolean(value, key) {
|
|
4
4
|
if (typeof value !== "boolean")
|
|
@@ -47,8 +47,8 @@ export const CONFIG_REGISTRY = {
|
|
|
47
47
|
write: (config, value) => {
|
|
48
48
|
config.heartbeat.enabled = value;
|
|
49
49
|
},
|
|
50
|
-
apply: ({
|
|
51
|
-
|
|
50
|
+
apply: ({ scheduler }) => {
|
|
51
|
+
scheduler.rearmHeartbeat();
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
"heartbeat.idleThresholdMs": {
|
|
@@ -57,8 +57,8 @@ export const CONFIG_REGISTRY = {
|
|
|
57
57
|
write: (config, value) => {
|
|
58
58
|
config.heartbeat.idleThresholdMs = value;
|
|
59
59
|
},
|
|
60
|
-
apply: ({
|
|
61
|
-
|
|
60
|
+
apply: ({ scheduler }) => {
|
|
61
|
+
scheduler.rearmHeartbeat();
|
|
62
62
|
},
|
|
63
63
|
},
|
|
64
64
|
"heartbeat.intervalMs": {
|
|
@@ -67,8 +67,8 @@ export const CONFIG_REGISTRY = {
|
|
|
67
67
|
write: (config, value) => {
|
|
68
68
|
config.heartbeat.intervalMs = value;
|
|
69
69
|
},
|
|
70
|
-
apply: ({
|
|
71
|
-
|
|
70
|
+
apply: ({ scheduler }) => {
|
|
71
|
+
scheduler.rearmHeartbeat();
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
"image_gen.enabled": {
|
|
@@ -284,3 +284,22 @@ export function applyConfigOverridesToConfig(config) {
|
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
|
+
async function mutateConfig(key, nextValue, persist, ctx) {
|
|
288
|
+
const entry = CONFIG_REGISTRY[key];
|
|
289
|
+
const previous = entry.read(ctx.config);
|
|
290
|
+
entry.write(ctx.config, nextValue);
|
|
291
|
+
try {
|
|
292
|
+
await persist();
|
|
293
|
+
await entry.apply?.(ctx);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
entry.write(ctx.config, previous);
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
export async function commitConfigChange(key, value, ctx) {
|
|
301
|
+
await mutateConfig(key, value, () => setConfigOverride(key, value), ctx);
|
|
302
|
+
}
|
|
303
|
+
export async function clearConfigChange(key, ctx) {
|
|
304
|
+
await mutateConfig(key, getConfigDefault(key), () => clearConfigOverride(key), ctx);
|
|
305
|
+
}
|
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),
|