@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/chat-log.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { appendFile, mkdir, open, readdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { isEnoent, readFileOrNull } from "./util/fs.js";
|
|
4
|
+
export function hiddenWebMessageIds(records) {
|
|
5
|
+
return new Set(records.flatMap((record) => {
|
|
6
|
+
if (record.type === "assistant_retry")
|
|
7
|
+
return [record.oldMessageId];
|
|
8
|
+
if (record.type === "message_delete")
|
|
9
|
+
return [record.messageId];
|
|
10
|
+
return [];
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
4
13
|
function sanitizeSegment(value) {
|
|
5
14
|
return value.replace(/[^A-Za-z0-9._=-]+/g, "_").slice(0, 120) || "unknown";
|
|
6
15
|
}
|
|
@@ -62,10 +71,13 @@ export function createChatLog(config, channel) {
|
|
|
62
71
|
return [];
|
|
63
72
|
throw error;
|
|
64
73
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
74
|
+
const jsonlFiles = files.filter((entry) => entry.endsWith(".jsonl")).sort();
|
|
75
|
+
const contents = await Promise.all(jsonlFiles.map(async (file) => {
|
|
67
76
|
const filePath = resolve(dir, file);
|
|
68
|
-
|
|
77
|
+
return { filePath, content: await readFile(filePath, "utf8") };
|
|
78
|
+
}));
|
|
79
|
+
const records = [];
|
|
80
|
+
for (const { filePath, content } of contents) {
|
|
69
81
|
for (const [index, line] of content.split(/\r?\n/).entries()) {
|
|
70
82
|
if (!line.trim())
|
|
71
83
|
continue;
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { copyFile, cp, mkdir } from "node:fs/promises";
|
|
3
|
+
import { copyFile, cp, mkdir, readFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { config as loadDotenv } from "dotenv";
|
|
8
8
|
import { createFamiliarAgent } from "./agent.js";
|
|
9
|
+
import { createAgentCore } from "./agent-core.js";
|
|
9
10
|
import { loadConfig } from "./config.js";
|
|
10
11
|
import { runDataRetention } from "./data-retention.js";
|
|
11
12
|
import { startDiscordDaemon } from "./discord.js";
|
|
@@ -13,6 +14,7 @@ import { cleanupGeneratedAttachments } from "./generated-media.js";
|
|
|
13
14
|
import { startWorkspaceHotReload } from "./hot-reload.js";
|
|
14
15
|
import { memoryHelp, runMemoryOperator } from "./memory/operator.js";
|
|
15
16
|
import { createMemoryService } from "./memory/service.js";
|
|
17
|
+
import { loadOwnerIdentity } from "./owner-identity.js";
|
|
16
18
|
import { formatServiceResult, installService, serviceStatus, uninstallService, upgradeFamiliar } from "./service.js";
|
|
17
19
|
import { loadSettingsStore } from "./settings.js";
|
|
18
20
|
import { startWebDaemon } from "./web.js";
|
|
@@ -76,6 +78,11 @@ function isMemoryHelp(args) {
|
|
|
76
78
|
const command = args[0];
|
|
77
79
|
return !command || command === "help" || command === "--help";
|
|
78
80
|
}
|
|
81
|
+
async function packageVersion() {
|
|
82
|
+
const raw = await readFile(resolve(PROJECT_ROOT, "package.json"), "utf8");
|
|
83
|
+
const packageJson = JSON.parse(raw);
|
|
84
|
+
return typeof packageJson.version === "string" ? packageJson.version : "0.0.0";
|
|
85
|
+
}
|
|
79
86
|
async function initWorkspace(workspaceInput) {
|
|
80
87
|
const workspacePath = resolveWorkspaceInput(workspaceInput);
|
|
81
88
|
await mkdir(workspacePath, { recursive: true });
|
|
@@ -119,6 +126,7 @@ async function runDaemon(workspaceInput) {
|
|
|
119
126
|
memoryService.watchDiaries();
|
|
120
127
|
const familiarAgent = await createFamiliarAgent(config, settings, memoryService, { reloadConfig });
|
|
121
128
|
const hotReload = startWorkspaceHotReload({ workspacePath: config.workspacePath, familiarAgent });
|
|
129
|
+
const agentCore = createAgentCore({ config, familiarAgent, memoryService });
|
|
122
130
|
let stopping = false;
|
|
123
131
|
let discordDaemon;
|
|
124
132
|
let webDaemon;
|
|
@@ -129,6 +137,7 @@ async function runDaemon(workspaceInput) {
|
|
|
129
137
|
console.log("Stopping familiar");
|
|
130
138
|
hotReload.close();
|
|
131
139
|
await Promise.all([webDaemon?.stop(), discordDaemon?.stop()]);
|
|
140
|
+
await agentCore.stop();
|
|
132
141
|
memoryService.close();
|
|
133
142
|
process.exit(exitCode);
|
|
134
143
|
};
|
|
@@ -137,10 +146,21 @@ async function runDaemon(workspaceInput) {
|
|
|
137
146
|
setTimeout(() => void stop(75), RESTART_EXIT_DELAY_MS);
|
|
138
147
|
return "Restart requested. If Familiar is managed by launchd/systemd, it should come back automatically; otherwise run familiar run again.";
|
|
139
148
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
const identity = await loadOwnerIdentity(config.workspace.dataDir);
|
|
150
|
+
const token = config.discord.token;
|
|
151
|
+
if (!identity && !token) {
|
|
152
|
+
throw new Error("First-time setup needs a DISCORD_TOKEN to establish owner identity. Set DISCORD_TOKEN and run again.");
|
|
153
|
+
}
|
|
154
|
+
// The scheduler starts with the first session source to arrive: the cached identity
|
|
155
|
+
// here, or the live Discord connection below when there is no cache yet.
|
|
156
|
+
if (identity)
|
|
157
|
+
await agentCore.useCachedIdentity(identity);
|
|
158
|
+
webDaemon = await startWebDaemon(config, familiarAgent, agentCore, { restart: requestRestart });
|
|
159
|
+
if (token) {
|
|
160
|
+
discordDaemon = startDiscordDaemon(config, token, familiarAgent, settings, memoryService, agentCore, {
|
|
161
|
+
restart: requestRestart,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
144
164
|
console.log(`familiar running for workspace ${config.workspacePath}`);
|
|
145
165
|
console.log("agent sessions are created per channel");
|
|
146
166
|
console.log(`settings=${settings.path}`);
|
|
@@ -151,6 +171,8 @@ async function runDaemon(workspaceInput) {
|
|
|
151
171
|
function usage() {
|
|
152
172
|
return [
|
|
153
173
|
"Usage:",
|
|
174
|
+
" familiar --help",
|
|
175
|
+
" familiar --version",
|
|
154
176
|
" familiar init [workspace]",
|
|
155
177
|
" familiar run [workspace]",
|
|
156
178
|
" familiar memory [workspace] <subcommand>",
|
|
@@ -164,6 +186,14 @@ function usage() {
|
|
|
164
186
|
}
|
|
165
187
|
async function main() {
|
|
166
188
|
const [, , command, workspace, ...rest] = process.argv;
|
|
189
|
+
if (!command || command === "--help" || command === "-h") {
|
|
190
|
+
console.log(usage());
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (command === "--version" || command === "-v") {
|
|
194
|
+
console.log(await packageVersion());
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
167
197
|
if (command === "init") {
|
|
168
198
|
await initWorkspace(workspace);
|
|
169
199
|
return;
|
|
@@ -200,7 +230,7 @@ async function main() {
|
|
|
200
230
|
return;
|
|
201
231
|
}
|
|
202
232
|
if (command === "upgrade") {
|
|
203
|
-
console.log("Upgrading @qearlyao/familiar globally...");
|
|
233
|
+
console.log("Upgrading @qearlyao/familiar and OpenCLI globally...");
|
|
204
234
|
await upgradeFamiliar(resolveWorkspaceInput(workspace));
|
|
205
235
|
return;
|
|
206
236
|
}
|
|
@@ -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
|
+
}
|