@qearlyao/familiar 0.1.2 → 0.2.1
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/CONTACT.md +2 -0
- package/README.md +10 -0
- package/config.example.toml +7 -13
- package/dist/added-models.js +80 -0
- package/dist/agent.js +54 -8
- package/dist/browser-tools.js +128 -34
- package/dist/cli.js +1 -0
- package/dist/config-overrides.js +68 -0
- package/dist/config-registry.js +289 -0
- package/dist/config.js +23 -133
- package/dist/contact-note.js +41 -0
- package/dist/discord.js +17 -7
- package/dist/hot-reload.js +26 -2
- package/dist/memory/lcm/context-transformer.js +14 -3
- package/dist/memory/lcm/context.js +15 -4
- package/dist/models.js +3 -2
- package/dist/persona.js +1 -0
- package/dist/web-tools.js +1 -3
- package/dist/web.js +202 -26
- package/package.json +5 -4
- package/skills/memes/SKILL.md +238 -0
- package/web/dist/assets/index-BPZQbZh5.js +61 -0
- package/web/dist/assets/index-CcQ13VAY.css +2 -0
- package/web/dist/familiar.svg +1 -0
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-ClgkMgaq.css +0 -2
- package/web/dist/assets/index-Cu2QquuR.js +0 -59
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { loadConfigOverrides } from "./config-overrides.js";
|
|
2
|
+
import { isAllowedModel, parseModelRef } from "./models.js";
|
|
3
|
+
function requireBoolean(value, key) {
|
|
4
|
+
if (typeof value !== "boolean")
|
|
5
|
+
throw new Error(`${key} must be a boolean`);
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function requirePositiveInt(value, key) {
|
|
9
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
10
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
11
|
+
throw new Error(`${key} must be a positive integer`);
|
|
12
|
+
}
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
function requireInt(value, key, min) {
|
|
16
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
17
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < min) {
|
|
18
|
+
throw new Error(`${key} must be an integer >= ${min}`);
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
}
|
|
22
|
+
function requireNumberInRange(value, key, min, max) {
|
|
23
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
24
|
+
if (!Number.isFinite(n) || n < min || n > max) {
|
|
25
|
+
throw new Error(`${key} must be a number between ${min} and ${max}`);
|
|
26
|
+
}
|
|
27
|
+
return n;
|
|
28
|
+
}
|
|
29
|
+
function requireNonNegativeInt(value, key) {
|
|
30
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
31
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
|
|
32
|
+
throw new Error(`${key} must be a non-negative integer`);
|
|
33
|
+
}
|
|
34
|
+
return n;
|
|
35
|
+
}
|
|
36
|
+
function requireNonNegativeNumber(value, key) {
|
|
37
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
38
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
39
|
+
throw new Error(`${key} must be a number >= 0`);
|
|
40
|
+
}
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
function resolveProviderSetting(records, provider, modelId) {
|
|
44
|
+
return records[`${provider}/${modelId}`] ?? records[provider];
|
|
45
|
+
}
|
|
46
|
+
export const CONFIG_REGISTRY = {
|
|
47
|
+
"heartbeat.enabled": {
|
|
48
|
+
read: (config) => config.heartbeat.enabled,
|
|
49
|
+
validate: (value) => requireBoolean(value, "heartbeat.enabled"),
|
|
50
|
+
write: (config, value) => {
|
|
51
|
+
config.heartbeat.enabled = value;
|
|
52
|
+
},
|
|
53
|
+
apply: ({ discordDaemon }) => {
|
|
54
|
+
discordDaemon.rearmHeartbeat();
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
"heartbeat.idleThresholdMs": {
|
|
58
|
+
read: (config) => config.heartbeat.idleThresholdMs,
|
|
59
|
+
validate: (value) => requirePositiveInt(value, "heartbeat.idleThresholdMs"),
|
|
60
|
+
write: (config, value) => {
|
|
61
|
+
config.heartbeat.idleThresholdMs = value;
|
|
62
|
+
},
|
|
63
|
+
apply: ({ discordDaemon }) => {
|
|
64
|
+
discordDaemon.rearmHeartbeat();
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
"heartbeat.intervalMs": {
|
|
68
|
+
read: (config) => config.heartbeat.intervalMs,
|
|
69
|
+
validate: (value) => requirePositiveInt(value, "heartbeat.intervalMs"),
|
|
70
|
+
write: (config, value) => {
|
|
71
|
+
config.heartbeat.intervalMs = value;
|
|
72
|
+
},
|
|
73
|
+
apply: ({ discordDaemon }) => {
|
|
74
|
+
discordDaemon.rearmHeartbeat();
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
"image_gen.enabled": {
|
|
78
|
+
read: (config) => config.imageGen.enabled,
|
|
79
|
+
validate: (value) => requireBoolean(value, "image_gen.enabled"),
|
|
80
|
+
write: (config, value) => {
|
|
81
|
+
config.imageGen.enabled = value;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
"image_gen.model": {
|
|
85
|
+
read: (config) => config.imageGen.model,
|
|
86
|
+
validate: (value) => {
|
|
87
|
+
if (typeof value !== "string")
|
|
88
|
+
throw new Error("image_gen.model must be a string");
|
|
89
|
+
const ref = parseModelRef(value);
|
|
90
|
+
if (!ref)
|
|
91
|
+
throw new Error("image_gen.model: format must be provider/model-id");
|
|
92
|
+
return ref.key;
|
|
93
|
+
},
|
|
94
|
+
write: (config, value) => {
|
|
95
|
+
const ref = parseModelRef(value);
|
|
96
|
+
if (!ref)
|
|
97
|
+
return;
|
|
98
|
+
config.imageGen.model = ref.key;
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
"image_gen.fallback_model": {
|
|
102
|
+
read: (config) => config.imageGen.fallbackModel ?? "",
|
|
103
|
+
validate: (value) => {
|
|
104
|
+
if (value == null || value === "")
|
|
105
|
+
return "";
|
|
106
|
+
if (typeof value !== "string")
|
|
107
|
+
throw new Error("image_gen.fallback_model must be a string");
|
|
108
|
+
const ref = parseModelRef(value);
|
|
109
|
+
if (!ref)
|
|
110
|
+
throw new Error("image_gen.fallback_model: format must be provider/model-id");
|
|
111
|
+
return ref.key;
|
|
112
|
+
},
|
|
113
|
+
write: (config, value) => {
|
|
114
|
+
if (value == null || value === "") {
|
|
115
|
+
config.imageGen.fallbackModel = undefined;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const ref = parseModelRef(value);
|
|
119
|
+
if (!ref)
|
|
120
|
+
return;
|
|
121
|
+
config.imageGen.fallbackModel = ref.key;
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
"memory.lcm.enabled": {
|
|
125
|
+
read: (config) => config.memory.lcm.enabled,
|
|
126
|
+
validate: (value) => requireBoolean(value, "memory.lcm.enabled"),
|
|
127
|
+
write: (config, value) => {
|
|
128
|
+
config.memory.lcm.enabled = value;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
"memory.lcm.model": {
|
|
132
|
+
read: (config) => config.memory.lcm.model,
|
|
133
|
+
validate: (value, config) => {
|
|
134
|
+
if (typeof value !== "string")
|
|
135
|
+
throw new Error("memory.lcm.model must be a string");
|
|
136
|
+
const ref = parseModelRef(value);
|
|
137
|
+
if (!ref)
|
|
138
|
+
throw new Error("memory.lcm.model: format must be provider/model-id");
|
|
139
|
+
if (!isAllowedModel(config, ref))
|
|
140
|
+
throw new Error(`memory.lcm.model is not allowlisted: ${ref.key}`);
|
|
141
|
+
return ref.key;
|
|
142
|
+
},
|
|
143
|
+
write: (config, value) => {
|
|
144
|
+
const ref = parseModelRef(value);
|
|
145
|
+
if (!ref)
|
|
146
|
+
return;
|
|
147
|
+
config.memory.lcm.model = ref.key;
|
|
148
|
+
config.memory.lcm.provider = ref.provider;
|
|
149
|
+
config.memory.lcm.modelId = ref.id;
|
|
150
|
+
config.memory.lcm.baseUrl = resolveProviderSetting(config.models.baseUrls, ref.provider, ref.id);
|
|
151
|
+
config.memory.lcm.apiKeyEnv = resolveProviderSetting(config.models.apiKeyEnvs, ref.provider, ref.id);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
"memory.lcm.contextThreshold": {
|
|
155
|
+
read: (config) => config.memory.lcm.contextThreshold,
|
|
156
|
+
validate: (value) => requireNumberInRange(value, "memory.lcm.contextThreshold", 0, 1),
|
|
157
|
+
write: (config, value) => {
|
|
158
|
+
config.memory.lcm.contextThreshold = value;
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
"memory.lcm.freshTailCount": {
|
|
162
|
+
read: (config) => config.memory.lcm.freshTailCount,
|
|
163
|
+
validate: (value) => requirePositiveInt(value, "memory.lcm.freshTailCount"),
|
|
164
|
+
write: (config, value) => {
|
|
165
|
+
config.memory.lcm.freshTailCount = value;
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
"memory.lcm.leafChunkTokens": {
|
|
169
|
+
read: (config) => config.memory.lcm.leafChunkTokens,
|
|
170
|
+
validate: (value) => requirePositiveInt(value, "memory.lcm.leafChunkTokens"),
|
|
171
|
+
write: (config, value) => {
|
|
172
|
+
config.memory.lcm.leafChunkTokens = value;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
"memory.lcm.leafTargetTokens": {
|
|
176
|
+
read: (config) => config.memory.lcm.leafTargetTokens,
|
|
177
|
+
validate: (value) => requirePositiveInt(value, "memory.lcm.leafTargetTokens"),
|
|
178
|
+
write: (config, value) => {
|
|
179
|
+
config.memory.lcm.leafTargetTokens = value;
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
"memory.lcm.condenseGroupSize": {
|
|
183
|
+
read: (config) => config.memory.lcm.condenseGroupSize,
|
|
184
|
+
validate: (value) => requirePositiveInt(value, "memory.lcm.condenseGroupSize"),
|
|
185
|
+
write: (config, value) => {
|
|
186
|
+
config.memory.lcm.condenseGroupSize = value;
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
"memory.lcm.maxSummaryDepth": {
|
|
190
|
+
read: (config) => config.memory.lcm.maxSummaryDepth,
|
|
191
|
+
validate: (value) => requirePositiveInt(value, "memory.lcm.maxSummaryDepth"),
|
|
192
|
+
write: (config, value) => {
|
|
193
|
+
config.memory.lcm.maxSummaryDepth = value;
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
"memory.lcm.newSessionRetainDepth": {
|
|
197
|
+
read: (config) => config.memory.lcm.newSessionRetainDepth,
|
|
198
|
+
validate: (value) => requireInt(value, "memory.lcm.newSessionRetainDepth", -1),
|
|
199
|
+
write: (config, value) => {
|
|
200
|
+
config.memory.lcm.newSessionRetainDepth = value;
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
"memory.ambient.enabled": {
|
|
204
|
+
read: (config) => config.memory.ambient.enabled,
|
|
205
|
+
validate: (value) => requireBoolean(value, "memory.ambient.enabled"),
|
|
206
|
+
write: (config, value) => {
|
|
207
|
+
config.memory.ambient.enabled = value;
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
"memory.ambient.topK": {
|
|
211
|
+
read: (config) => config.memory.ambient.topK,
|
|
212
|
+
validate: (value) => requirePositiveInt(value, "memory.ambient.topK"),
|
|
213
|
+
write: (config, value) => {
|
|
214
|
+
config.memory.ambient.topK = value;
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
"memory.ambient.minQueryLength": {
|
|
218
|
+
read: (config) => config.memory.ambient.minQueryLength,
|
|
219
|
+
validate: (value) => requireNonNegativeInt(value, "memory.ambient.minQueryLength"),
|
|
220
|
+
write: (config, value) => {
|
|
221
|
+
config.memory.ambient.minQueryLength = value;
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
"memory.ambient.throttleSeconds": {
|
|
225
|
+
read: (config) => config.memory.ambient.throttleSeconds,
|
|
226
|
+
validate: (value) => requireNonNegativeInt(value, "memory.ambient.throttleSeconds"),
|
|
227
|
+
write: (config, value) => {
|
|
228
|
+
config.memory.ambient.throttleSeconds = value;
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
"memory.ambient.weightSimilarity": {
|
|
232
|
+
read: (config) => config.memory.ambient.weightSimilarity,
|
|
233
|
+
validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightSimilarity"),
|
|
234
|
+
write: (config, value) => {
|
|
235
|
+
config.memory.ambient.weightSimilarity = value;
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
"memory.ambient.weightValence": {
|
|
239
|
+
read: (config) => config.memory.ambient.weightValence,
|
|
240
|
+
validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightValence"),
|
|
241
|
+
write: (config, value) => {
|
|
242
|
+
config.memory.ambient.weightValence = value;
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
"memory.ambient.weightRecency": {
|
|
246
|
+
read: (config) => config.memory.ambient.weightRecency,
|
|
247
|
+
validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightRecency"),
|
|
248
|
+
write: (config, value) => {
|
|
249
|
+
config.memory.ambient.weightRecency = value;
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
"memory.ambient.weightIntensity": {
|
|
253
|
+
read: (config) => config.memory.ambient.weightIntensity,
|
|
254
|
+
validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightIntensity"),
|
|
255
|
+
write: (config, value) => {
|
|
256
|
+
config.memory.ambient.weightIntensity = value;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
export const CONFIG_KEYS = Object.keys(CONFIG_REGISTRY);
|
|
261
|
+
export function isConfigKey(value) {
|
|
262
|
+
return typeof value === "string" && value in CONFIG_REGISTRY;
|
|
263
|
+
}
|
|
264
|
+
const defaultSnapshot = new Map();
|
|
265
|
+
export function snapshotConfigDefaults(config) {
|
|
266
|
+
defaultSnapshot.clear();
|
|
267
|
+
for (const key of CONFIG_KEYS) {
|
|
268
|
+
defaultSnapshot.set(key, CONFIG_REGISTRY[key].read(config));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
export function getConfigDefault(key) {
|
|
272
|
+
return defaultSnapshot.get(key);
|
|
273
|
+
}
|
|
274
|
+
export function applyConfigOverridesToConfig(config) {
|
|
275
|
+
snapshotConfigDefaults(config);
|
|
276
|
+
const overrides = loadConfigOverrides();
|
|
277
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
278
|
+
if (!isConfigKey(key))
|
|
279
|
+
continue;
|
|
280
|
+
const entry = CONFIG_REGISTRY[key];
|
|
281
|
+
try {
|
|
282
|
+
const validated = entry.validate(value, config);
|
|
283
|
+
entry.write(config, validated);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
console.warn(`Skipping invalid config override ${key}:`, error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -340,140 +340,17 @@ function readCronJobs(cron) {
|
|
|
340
340
|
};
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
|
-
function readBrowserAllowedSites(browser) {
|
|
344
|
-
const rawSites = browser.sites;
|
|
345
|
-
if (rawSites === undefined)
|
|
346
|
-
return defaultBrowserAllowedSites();
|
|
347
|
-
if (!rawSites || typeof rawSites !== "object" || Array.isArray(rawSites)) {
|
|
348
|
-
throw new Error("Config value browser.sites must be a table");
|
|
349
|
-
}
|
|
350
|
-
const sites = {};
|
|
351
|
-
for (const [siteName, rawSite] of Object.entries(rawSites)) {
|
|
352
|
-
if (!rawSite || typeof rawSite !== "object" || Array.isArray(rawSite)) {
|
|
353
|
-
throw new Error(`Config value browser.sites.${siteName} must be a table`);
|
|
354
|
-
}
|
|
355
|
-
const site = rawSite;
|
|
356
|
-
assertKnownKeys(site, `browser.sites.${siteName}`, ["read", "write"]);
|
|
357
|
-
const read = readStringArray(site.read, `browser.sites.${siteName}.read`);
|
|
358
|
-
const write = readStringArray(site.write, `browser.sites.${siteName}.write`);
|
|
359
|
-
sites[siteName] = { read, write };
|
|
360
|
-
}
|
|
361
|
-
return sites;
|
|
362
|
-
}
|
|
363
343
|
function defaultBrowserAllowedSites() {
|
|
364
344
|
return {
|
|
365
|
-
twitter:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"notifications",
|
|
375
|
-
"profile",
|
|
376
|
-
"search",
|
|
377
|
-
"thread",
|
|
378
|
-
"timeline",
|
|
379
|
-
"trending",
|
|
380
|
-
"tweets",
|
|
381
|
-
],
|
|
382
|
-
write: [],
|
|
383
|
-
},
|
|
384
|
-
xiaohongshu: {
|
|
385
|
-
read: [
|
|
386
|
-
"comments",
|
|
387
|
-
"creator-note-detail",
|
|
388
|
-
"creator-notes",
|
|
389
|
-
"creator-notes-summary",
|
|
390
|
-
"creator-profile",
|
|
391
|
-
"creator-stats",
|
|
392
|
-
"feed",
|
|
393
|
-
"note",
|
|
394
|
-
"notifications",
|
|
395
|
-
"search",
|
|
396
|
-
"user",
|
|
397
|
-
],
|
|
398
|
-
write: [],
|
|
399
|
-
},
|
|
400
|
-
rednote: {
|
|
401
|
-
read: ["comments", "feed", "note", "notifications", "search", "user"],
|
|
402
|
-
write: [],
|
|
403
|
-
},
|
|
404
|
-
reddit: {
|
|
405
|
-
read: [
|
|
406
|
-
"frontpage",
|
|
407
|
-
"home",
|
|
408
|
-
"hot",
|
|
409
|
-
"popular",
|
|
410
|
-
"read",
|
|
411
|
-
"saved",
|
|
412
|
-
"search",
|
|
413
|
-
"subreddit",
|
|
414
|
-
"subreddit-info",
|
|
415
|
-
"upvoted",
|
|
416
|
-
"user",
|
|
417
|
-
"user-comments",
|
|
418
|
-
"user-posts",
|
|
419
|
-
"whoami",
|
|
420
|
-
],
|
|
421
|
-
write: [],
|
|
422
|
-
},
|
|
423
|
-
bilibili: {
|
|
424
|
-
read: [
|
|
425
|
-
"comments",
|
|
426
|
-
"dynamic",
|
|
427
|
-
"feed",
|
|
428
|
-
"following",
|
|
429
|
-
"history",
|
|
430
|
-
"hot",
|
|
431
|
-
"me",
|
|
432
|
-
"ranking",
|
|
433
|
-
"search",
|
|
434
|
-
"subtitle",
|
|
435
|
-
"user-videos",
|
|
436
|
-
"video",
|
|
437
|
-
],
|
|
438
|
-
write: [],
|
|
439
|
-
},
|
|
440
|
-
youtube: {
|
|
441
|
-
read: [
|
|
442
|
-
"channel",
|
|
443
|
-
"comments",
|
|
444
|
-
"feed",
|
|
445
|
-
"history",
|
|
446
|
-
"playlist",
|
|
447
|
-
"search",
|
|
448
|
-
"subscriptions",
|
|
449
|
-
"transcript",
|
|
450
|
-
"video",
|
|
451
|
-
"watch-later",
|
|
452
|
-
],
|
|
453
|
-
write: [],
|
|
454
|
-
},
|
|
455
|
-
tiktok: {
|
|
456
|
-
read: ["explore", "friends", "live", "notifications", "profile", "search", "user"],
|
|
457
|
-
write: [],
|
|
458
|
-
},
|
|
459
|
-
douyin: {
|
|
460
|
-
read: [
|
|
461
|
-
"activities",
|
|
462
|
-
"collections",
|
|
463
|
-
"drafts",
|
|
464
|
-
"hashtag",
|
|
465
|
-
"location",
|
|
466
|
-
"profile",
|
|
467
|
-
"stats",
|
|
468
|
-
"user-videos",
|
|
469
|
-
"videos",
|
|
470
|
-
],
|
|
471
|
-
write: [],
|
|
472
|
-
},
|
|
473
|
-
spotify: {
|
|
474
|
-
read: ["search", "status"],
|
|
475
|
-
write: [],
|
|
476
|
-
},
|
|
345
|
+
twitter: true,
|
|
346
|
+
xiaohongshu: true,
|
|
347
|
+
rednote: true,
|
|
348
|
+
reddit: true,
|
|
349
|
+
bilibili: true,
|
|
350
|
+
youtube: true,
|
|
351
|
+
tiktok: true,
|
|
352
|
+
douyin: true,
|
|
353
|
+
spotify: true,
|
|
477
354
|
};
|
|
478
355
|
}
|
|
479
356
|
export async function loadConfig(workspacePathInput) {
|
|
@@ -506,6 +383,18 @@ export async function loadConfig(workspacePathInput) {
|
|
|
506
383
|
const persona = (parsed.persona ?? {});
|
|
507
384
|
const workspace = (parsed.workspace ?? {});
|
|
508
385
|
const memory = (parsed.memory ?? {});
|
|
386
|
+
assertKnownKeys(browser, "browser", [
|
|
387
|
+
"enabled",
|
|
388
|
+
"backend",
|
|
389
|
+
"opencli_command",
|
|
390
|
+
"harness_command",
|
|
391
|
+
"session",
|
|
392
|
+
"profile",
|
|
393
|
+
"window",
|
|
394
|
+
"timeout_ms",
|
|
395
|
+
"max_output_chars",
|
|
396
|
+
"read_write",
|
|
397
|
+
]);
|
|
509
398
|
const memoryEmbedding = (memory.embedding ?? {});
|
|
510
399
|
const memoryAmbient = (memory.ambient ?? {});
|
|
511
400
|
const memoryLcm = (memory.lcm ?? {});
|
|
@@ -644,7 +533,7 @@ export async function loadConfig(workspacePathInput) {
|
|
|
644
533
|
timeoutMs: readInteger(browser.timeout_ms, 60_000, "browser.timeout_ms", 1),
|
|
645
534
|
maxOutputChars: readInteger(browser.max_output_chars, 12_000, "browser.max_output_chars", 1000),
|
|
646
535
|
readWrite: readBoolean(browser.read_write, false, "browser.read_write"),
|
|
647
|
-
allowedSites:
|
|
536
|
+
allowedSites: defaultBrowserAllowedSites(),
|
|
648
537
|
},
|
|
649
538
|
agent: {
|
|
650
539
|
model: agentModel,
|
|
@@ -710,6 +599,7 @@ export async function loadConfig(workspacePathInput) {
|
|
|
710
599
|
persona: {
|
|
711
600
|
soul: resolveWorkspacePath(workspacePath, readOptionalString(persona.soul, "SOUL.md")),
|
|
712
601
|
user: resolveWorkspacePath(workspacePath, readOptionalString(persona.user, "USER.md")),
|
|
602
|
+
contact: resolveWorkspacePath(workspacePath, readOptionalString(persona.contact, "CONTACT.md")),
|
|
713
603
|
memory: resolveWorkspacePath(workspacePath, readOptionalString(persona.memory, "MEMORY.md")),
|
|
714
604
|
inner: resolveWorkspacePath(workspacePath, readOptionalString(persona.inner, "INNER.md")),
|
|
715
605
|
},
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
let contactNotePath = resolve(process.cwd(), "CONTACT.md");
|
|
4
|
+
let cachedNickname = null;
|
|
5
|
+
function isMissingFile(error) {
|
|
6
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
7
|
+
}
|
|
8
|
+
export function setContactNotePath(path) {
|
|
9
|
+
contactNotePath = path;
|
|
10
|
+
cachedNickname = null;
|
|
11
|
+
}
|
|
12
|
+
export async function loadContactNote() {
|
|
13
|
+
try {
|
|
14
|
+
return await readFile(contactNotePath, "utf8");
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (isMissingFile(error))
|
|
18
|
+
return null;
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function parseContactNickname(raw, fallback) {
|
|
23
|
+
let remaining = raw?.trim() ?? "";
|
|
24
|
+
while (remaining.startsWith("<!--")) {
|
|
25
|
+
const end = remaining.indexOf("-->");
|
|
26
|
+
if (end === -1)
|
|
27
|
+
return fallback;
|
|
28
|
+
remaining = remaining.slice(end + 3).trim();
|
|
29
|
+
}
|
|
30
|
+
const firstLine = remaining
|
|
31
|
+
.split(/\r?\n/)
|
|
32
|
+
.map((line) => line.trim())
|
|
33
|
+
.find((line) => line && !line.startsWith("<!--"));
|
|
34
|
+
return firstLine ?? fallback;
|
|
35
|
+
}
|
|
36
|
+
export async function refreshContactNote() {
|
|
37
|
+
cachedNickname = parseContactNickname(await loadContactNote(), "");
|
|
38
|
+
}
|
|
39
|
+
export function getContactNickname(fallback) {
|
|
40
|
+
return cachedNickname || fallback;
|
|
41
|
+
}
|
package/dist/discord.js
CHANGED
|
@@ -625,7 +625,7 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
|
|
|
625
625
|
let heartbeatQueued = false;
|
|
626
626
|
let cronRunning = false;
|
|
627
627
|
let schedulerState = { cron: {} };
|
|
628
|
-
const promptForRuntime = async (runtime, jobId, prompt, attachments = [], onEvent) => {
|
|
628
|
+
const promptForRuntime = async (runtime, jobId, prompt, attachments = [], onEvent, onTurnEnd) => {
|
|
629
629
|
const run = agentWorkQueue.then(async () => {
|
|
630
630
|
if (!runtime.hasActiveJob(jobId))
|
|
631
631
|
throw canceledJobError();
|
|
@@ -635,6 +635,7 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
|
|
|
635
635
|
const input = [prompt, promptImages.promptSuffix].filter(Boolean).join("\n");
|
|
636
636
|
const reply = await familiarAgent.prompt(runtime.channelKey, input, promptImages.images, onEvent, {
|
|
637
637
|
referenceAttachments: attachments,
|
|
638
|
+
onTurnEnd,
|
|
638
639
|
});
|
|
639
640
|
if (!runtime.hasActiveJob(jobId))
|
|
640
641
|
throw canceledJobError();
|
|
@@ -1170,12 +1171,21 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
|
|
|
1170
1171
|
console.warn("Discord websocket closed; discord.js will reconnect when possible", event);
|
|
1171
1172
|
});
|
|
1172
1173
|
schedulerState = await loadSchedulerState(config.workspace.dataDir);
|
|
1174
|
+
const tickHeartbeat = () => {
|
|
1175
|
+
void runHeartbeat().catch((error) => console.error("Heartbeat tick failed", error));
|
|
1176
|
+
};
|
|
1177
|
+
const rearmHeartbeat = () => {
|
|
1178
|
+
if (heartbeatTimer) {
|
|
1179
|
+
clearInterval(heartbeatTimer);
|
|
1180
|
+
heartbeatTimer = undefined;
|
|
1181
|
+
}
|
|
1182
|
+
if (config.heartbeat.enabled) {
|
|
1183
|
+
heartbeatTimer = setInterval(tickHeartbeat, Math.min(config.heartbeat.intervalMs, 60_000));
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1173
1186
|
if (config.heartbeat.enabled) {
|
|
1174
1187
|
await initializeHeartbeatState((await getOwnerDmSession()).runtime);
|
|
1175
|
-
|
|
1176
|
-
void runHeartbeat().catch((error) => console.error("Heartbeat tick failed", error));
|
|
1177
|
-
};
|
|
1178
|
-
heartbeatTimer = setInterval(tickHeartbeat, Math.min(config.heartbeat.intervalMs, 60_000));
|
|
1188
|
+
rearmHeartbeat();
|
|
1179
1189
|
tickHeartbeat();
|
|
1180
1190
|
}
|
|
1181
1191
|
if (config.cron.enabled && config.cron.jobs.some((job) => job.enabled)) {
|
|
@@ -1191,12 +1201,12 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
|
|
|
1191
1201
|
getRuntimeForWebChannel,
|
|
1192
1202
|
runPromptForWeb: promptForRuntime,
|
|
1193
1203
|
abortWebRuntime(runtime) {
|
|
1194
|
-
|
|
1195
|
-
familiarAgent.abort(runtime.channelKey);
|
|
1204
|
+
familiarAgent.requestSoftStop(runtime.channelKey);
|
|
1196
1205
|
},
|
|
1197
1206
|
getActiveRuntimeKey() {
|
|
1198
1207
|
return activeAgentOwner;
|
|
1199
1208
|
},
|
|
1209
|
+
rearmHeartbeat,
|
|
1200
1210
|
async stop() {
|
|
1201
1211
|
client.off(Events.MessageCreate, onMessageCreate);
|
|
1202
1212
|
client.off(Events.InteractionCreate, onInteractionCreate);
|
package/dist/hot-reload.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import { basename, relative, resolve, sep } from "node:path";
|
|
4
|
-
|
|
4
|
+
import { refreshContactNote } from "./contact-note.js";
|
|
5
|
+
const ROOT_FILES = new Set([
|
|
6
|
+
"config.toml",
|
|
7
|
+
".env",
|
|
8
|
+
"SOUL.md",
|
|
9
|
+
"USER.md",
|
|
10
|
+
"CONTACT.md",
|
|
11
|
+
"MEMORY.md",
|
|
12
|
+
"INNER.md",
|
|
13
|
+
"HEARTBEAT.md",
|
|
14
|
+
]);
|
|
5
15
|
const SKILLS_DIR = "skills";
|
|
6
16
|
function isEnoent(error) {
|
|
7
17
|
return !!error && typeof error === "object" && error.code === "ENOENT";
|
|
@@ -94,7 +104,21 @@ export function startWorkspaceHotReload(options) {
|
|
|
94
104
|
if (!filename ||
|
|
95
105
|
shouldReloadForPath(workspacePath, changedPath) ||
|
|
96
106
|
shouldReloadForPath(workspacePath, dirPath)) {
|
|
97
|
-
|
|
107
|
+
const reason = relative(workspacePath, changedPath) || relative(workspacePath, dirPath) || ".";
|
|
108
|
+
if (reason === "CONTACT.md") {
|
|
109
|
+
reloadQueue = reloadQueue.then(async () => {
|
|
110
|
+
try {
|
|
111
|
+
await refreshContactNote();
|
|
112
|
+
logger.info(`contact note refresh complete after ${reason}`);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
logger.error("contact note refresh failed", error);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
scheduleReload(reason);
|
|
121
|
+
}
|
|
98
122
|
}
|
|
99
123
|
});
|
|
100
124
|
watcher.on("error", (error) => {
|
|
@@ -599,8 +599,13 @@ function lcmRecordPartsFromAgentMessage(message) {
|
|
|
599
599
|
return [];
|
|
600
600
|
const parts = [];
|
|
601
601
|
for (const item of content) {
|
|
602
|
-
if (item.type === "text")
|
|
603
|
-
parts.push({
|
|
602
|
+
if (item.type === "text") {
|
|
603
|
+
parts.push({
|
|
604
|
+
kind: "text",
|
|
605
|
+
text: item.text,
|
|
606
|
+
...(item.textSignature ? { signature: item.textSignature } : {}),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
604
609
|
else if (item.type === "thinking") {
|
|
605
610
|
parts.push({
|
|
606
611
|
kind: "thinking",
|
|
@@ -609,7 +614,13 @@ function lcmRecordPartsFromAgentMessage(message) {
|
|
|
609
614
|
});
|
|
610
615
|
}
|
|
611
616
|
else if (item.type === "toolCall") {
|
|
612
|
-
parts.push({
|
|
617
|
+
parts.push({
|
|
618
|
+
kind: "tool_call",
|
|
619
|
+
toolCallId: item.id,
|
|
620
|
+
toolName: item.name,
|
|
621
|
+
arguments: item.arguments,
|
|
622
|
+
...(item.thoughtSignature ? { signature: item.thoughtSignature } : {}),
|
|
623
|
+
});
|
|
613
624
|
}
|
|
614
625
|
else if (item.type === "image") {
|
|
615
626
|
parts.push({ kind: "text", text: `[image: ${item.mimeType}]` });
|