@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.
Files changed (83) hide show
  1. package/README.md +4 -0
  2. package/config.example.toml +2 -2
  3. package/dist/agent/payload-normalizers.js +52 -0
  4. package/dist/agent/session-helpers.js +86 -0
  5. package/dist/agent/tool-descriptions.js +4 -0
  6. package/dist/agent/tools.js +30 -0
  7. package/dist/agent/transcript-log.js +93 -0
  8. package/dist/agent/types.js +1 -0
  9. package/dist/agent-core.js +82 -0
  10. package/dist/agent-work-queue.js +55 -0
  11. package/dist/agent.js +91 -322
  12. package/dist/browser-tools.js +80 -28
  13. package/dist/chat-log.js +15 -3
  14. package/dist/cli.js +36 -6
  15. package/dist/config/enums.js +35 -0
  16. package/dist/config/interpolate.js +15 -0
  17. package/dist/config/model-refs.js +11 -0
  18. package/dist/config/readers.js +116 -0
  19. package/dist/config/sections.js +113 -0
  20. package/dist/config/types.js +1 -0
  21. package/dist/config-registry.js +26 -7
  22. package/dist/config.js +8 -271
  23. package/dist/discord/channel.js +32 -0
  24. package/dist/discord/chunking.js +163 -0
  25. package/dist/discord/client.js +44 -0
  26. package/dist/discord/commands.js +181 -0
  27. package/dist/discord/inbound.js +44 -0
  28. package/dist/discord/send.js +106 -0
  29. package/dist/discord/turn.js +55 -0
  30. package/dist/discord.js +266 -1186
  31. package/dist/ids.js +11 -0
  32. package/dist/image-gen.js +90 -10
  33. package/dist/index.js +1 -0
  34. package/dist/memory/index/store.js +21 -17
  35. package/dist/memory/index/vector-codec.js +2 -2
  36. package/dist/memory/lcm/context-transformer.js +6 -2
  37. package/dist/memory/lcm/segment-manager.js +6 -2
  38. package/dist/memory/lcm/store/index-ids.js +6 -0
  39. package/dist/memory/lcm/store/inserts.js +31 -0
  40. package/dist/memory/lcm/store/normalizers.js +91 -0
  41. package/dist/memory/lcm/store/row-mappers.js +114 -0
  42. package/dist/memory/lcm/store/row-types.js +1 -0
  43. package/dist/memory/lcm/store/serialization.js +37 -0
  44. package/dist/memory/lcm/store/snapshots.js +73 -0
  45. package/dist/memory/lcm/store.js +20 -360
  46. package/dist/owner-identity.js +29 -0
  47. package/dist/runtime-manager.js +51 -0
  48. package/dist/runtime.js +89 -41
  49. package/dist/scheduler-runner.js +243 -0
  50. package/dist/scheduler.js +1 -1
  51. package/dist/service.js +1 -0
  52. package/dist/settings.js +3 -0
  53. package/dist/util/fs.js +1 -1
  54. package/dist/web/event-hub.js +246 -0
  55. package/dist/{web-http.js → web/http.js} +19 -5
  56. package/dist/web/memes.js +25 -0
  57. package/dist/web/messages.js +345 -0
  58. package/dist/web/multipart.js +80 -0
  59. package/dist/web/payloads.js +34 -0
  60. package/dist/{web-static.js → web/static.js} +19 -14
  61. package/dist/web/stream.js +69 -0
  62. package/dist/web-tools/cache.js +42 -0
  63. package/dist/web-tools/config.js +16 -0
  64. package/dist/web-tools/fetch-providers.js +119 -0
  65. package/dist/web-tools/format.js +88 -0
  66. package/dist/web-tools/http.js +81 -0
  67. package/dist/web-tools/routing.js +29 -0
  68. package/dist/web-tools/safety.js +73 -0
  69. package/dist/web-tools/search-providers.js +277 -0
  70. package/dist/web-tools/types.js +54 -0
  71. package/dist/web-tools/util.js +23 -0
  72. package/dist/web-tools.js +9 -798
  73. package/dist/web.js +416 -984
  74. package/npm-shrinkwrap.json +242 -201
  75. package/package.json +4 -4
  76. package/web/dist/assets/index-CSkxUQCr.js +63 -0
  77. package/web/dist/assets/index-DllM6RqL.css +2 -0
  78. package/web/dist/index.html +6 -3
  79. package/web/dist/assets/index-B23WT77N.js +0 -63
  80. package/web/dist/assets/index-D3MotFzN.css +0 -2
  81. /package/dist/{web-auth.js → web/auth.js} +0 -0
  82. /package/dist/{web-events.js → web/events.js} +0 -0
  83. /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 records = [];
66
- for (const file of files.filter((entry) => entry.endsWith(".jsonl")).sort()) {
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
- const content = await readFile(filePath, "utf8");
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
- discordDaemon = await startDiscordDaemon(config, familiarAgent, settings, memoryService, {
141
- restart: requestRestart,
142
- });
143
- webDaemon = await startWebDaemon(config, familiarAgent, discordDaemon, { restart: requestRestart });
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 {};
@@ -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: ({ discordDaemon }) => {
51
- discordDaemon.rearmHeartbeat();
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: ({ discordDaemon }) => {
61
- discordDaemon.rearmHeartbeat();
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: ({ discordDaemon }) => {
71
- discordDaemon.rearmHeartbeat();
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
+ }