@qearlyao/familiar 0.2.3 → 0.2.5
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 +5 -2
- package/config.example.toml +1 -1
- package/dist/added-models.js +6 -15
- package/dist/agent-events.js +1 -3
- package/dist/agent.js +3 -4
- package/dist/browser-tools.js +84 -30
- package/dist/chat-log.js +3 -2
- package/dist/cli.js +2 -2
- package/dist/config-overrides.js +5 -14
- package/dist/config-registry.js +1 -4
- package/dist/config.js +45 -113
- package/dist/contact-note.js +2 -12
- package/dist/data-retention.js +1 -3
- package/dist/discord.js +2 -2
- package/dist/generated-media.js +3 -2
- package/dist/hot-reload.js +1 -3
- package/dist/image-gen.js +102 -61
- package/dist/inbound-attachments.js +53 -22
- package/dist/memory/diary/ambient-injector.js +1 -3
- package/dist/memory/diary/ambient.js +1 -3
- package/dist/memory/diary/chunks.js +1 -3
- package/dist/memory/diary/indexer.js +1 -3
- package/dist/memory/doctor.js +3 -8
- package/dist/memory/index/chunk-indexer.js +6 -2
- package/dist/memory/index/retrieval.js +1 -3
- package/dist/memory/index/store.js +47 -19
- package/dist/memory/lcm/backfill.js +19 -16
- package/dist/memory/lcm/context-transformer.js +12 -24
- package/dist/memory/lcm/context.js +10 -4
- package/dist/memory/lcm/eviction-score.js +25 -13
- package/dist/memory/lcm/indexer.js +1 -5
- package/dist/memory/lcm/normalize.js +22 -1
- package/dist/memory/lcm/store.js +27 -24
- package/dist/memory/operator.js +2 -4
- package/dist/memory/service.js +1 -3
- package/dist/memory/tools.js +0 -4
- package/dist/memory/util.js +6 -0
- package/dist/models.js +3 -0
- package/dist/persona.js +2 -14
- package/dist/runtime.js +2 -23
- package/dist/scheduler.js +15 -49
- package/dist/service.js +24 -14
- package/dist/settings.js +7 -32
- package/dist/tts.js +0 -6
- package/dist/util/fs.js +41 -0
- package/dist/util/guards.js +8 -0
- package/dist/util/image-mime.js +31 -0
- package/dist/util/time.js +29 -0
- package/dist/web-auth.js +4 -1
- package/dist/web-tools.js +8 -5
- package/dist/web.js +188 -62
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/web/dist/assets/index-B23WT77N.js +63 -0
- package/web/dist/assets/index-D3MotFzN.css +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-C-w9fjBf.js +0 -61
- package/web/dist/assets/index-CcQ13VAY.css +0 -2
|
@@ -8,11 +8,7 @@ export async function projectNormalizedLcmBatch(options) {
|
|
|
8
8
|
}
|
|
9
9
|
const storedRecords = [];
|
|
10
10
|
for (const record of options.batch.records) {
|
|
11
|
-
|
|
12
|
-
const stored = options.lcmStore.getRecord(id);
|
|
13
|
-
if (!stored)
|
|
14
|
-
throw new Error(`Failed to read projected LCM record: ${id}`);
|
|
15
|
-
storedRecords.push(stored);
|
|
11
|
+
storedRecords.push(options.lcmStore.insertRecordReturningStored(record).record);
|
|
16
12
|
}
|
|
17
13
|
return {
|
|
18
14
|
segmentIds,
|
|
@@ -59,7 +59,7 @@ export function normalizeChatRecords(chatRecords, options) {
|
|
|
59
59
|
kind: "tool_result",
|
|
60
60
|
toolCallId: record.event.toolCallId,
|
|
61
61
|
toolName: record.event.toolName,
|
|
62
|
-
output: record.event.result,
|
|
62
|
+
output: toolResultVisibleOutput(record.event.result),
|
|
63
63
|
...(record.event.isError ? { isError: true } : {}),
|
|
64
64
|
},
|
|
65
65
|
];
|
|
@@ -191,6 +191,27 @@ function briefJson(value, maxLength = 160) {
|
|
|
191
191
|
const compact = text.replace(/\s+/g, " ").trim();
|
|
192
192
|
return compact.length > maxLength ? `${compact.slice(0, maxLength - 1)}...` : compact;
|
|
193
193
|
}
|
|
194
|
+
function toolResultVisibleOutput(result) {
|
|
195
|
+
if (!result || typeof result !== "object" || !("content" in result))
|
|
196
|
+
return result;
|
|
197
|
+
const content = result.content;
|
|
198
|
+
if (!Array.isArray(content))
|
|
199
|
+
return result;
|
|
200
|
+
return textFromToolResultContent(content);
|
|
201
|
+
}
|
|
202
|
+
function textFromToolResultContent(content) {
|
|
203
|
+
if (!Array.isArray(content))
|
|
204
|
+
return content;
|
|
205
|
+
return content
|
|
206
|
+
.map((item) => {
|
|
207
|
+
if (item && typeof item === "object" && item.type === "text") {
|
|
208
|
+
return item.text;
|
|
209
|
+
}
|
|
210
|
+
return "";
|
|
211
|
+
})
|
|
212
|
+
.filter((item) => typeof item === "string" && item.length > 0)
|
|
213
|
+
.join("\n");
|
|
214
|
+
}
|
|
194
215
|
function chatRecordSource(record, sourcePath) {
|
|
195
216
|
const messageId = "messageId" in record && typeof record.messageId === "string"
|
|
196
217
|
? record.messageId
|
package/dist/memory/lcm/store.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mkdirSync } from "node:fs";
|
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import Database from "better-sqlite3";
|
|
5
5
|
import { normalizeFtsMatchQuery } from "../index/fts-query.js";
|
|
6
|
+
import { runInTransaction } from "../util.js";
|
|
6
7
|
import { readMeta, runLcmMigrations } from "./schema.js";
|
|
7
8
|
export class LcmStore {
|
|
8
9
|
db;
|
|
@@ -34,6 +35,9 @@ export class LcmStore {
|
|
|
34
35
|
const raw = readMeta(this.db, "schema_version");
|
|
35
36
|
return raw ? Number(raw) : null;
|
|
36
37
|
}
|
|
38
|
+
computeRecordKey(input) {
|
|
39
|
+
return computeLcmRecordKey(input);
|
|
40
|
+
}
|
|
37
41
|
ensureSegment(input) {
|
|
38
42
|
const startedAt = input.startedAt ?? new Date().toISOString();
|
|
39
43
|
this.db
|
|
@@ -68,6 +72,9 @@ export class LcmStore {
|
|
|
68
72
|
return rows.map(segmentFromRow);
|
|
69
73
|
}
|
|
70
74
|
insertRecord(input) {
|
|
75
|
+
return this.insertRecordReturningStored(input).record.id;
|
|
76
|
+
}
|
|
77
|
+
insertRecordReturningStored(input) {
|
|
71
78
|
const normalized = normalizeRecordInput(input);
|
|
72
79
|
const runInsert = () => {
|
|
73
80
|
this.ensureSegment({
|
|
@@ -76,14 +83,13 @@ export class LcmStore {
|
|
|
76
83
|
channelKey: normalized.channelKey,
|
|
77
84
|
startedAt: normalized.happenedAt,
|
|
78
85
|
});
|
|
79
|
-
const existing = this.db
|
|
80
|
-
.prepare("SELECT id FROM lcm_records WHERE record_key = ?")
|
|
81
|
-
.get(normalized.recordKey);
|
|
86
|
+
const existing = this.db.prepare("SELECT * FROM lcm_records WHERE record_key = ?").get(normalized.recordKey);
|
|
82
87
|
if (existing)
|
|
83
|
-
return existing
|
|
84
|
-
|
|
88
|
+
return { record: recordFromRow(existing), inserted: false };
|
|
89
|
+
const inserted = insertRecordPrepared(this.db, normalized);
|
|
90
|
+
return { record: recordFromRow(inserted), inserted: true };
|
|
85
91
|
};
|
|
86
|
-
return this.db
|
|
92
|
+
return runInTransaction(this.db, runInsert);
|
|
87
93
|
}
|
|
88
94
|
getRecord(id) {
|
|
89
95
|
const row = this.db.prepare("SELECT * FROM lcm_records WHERE id = ?").get(id);
|
|
@@ -132,7 +138,7 @@ export class LcmStore {
|
|
|
132
138
|
this.insertSummarySources(id, sources);
|
|
133
139
|
this.insertSummaryParents(id, parents);
|
|
134
140
|
});
|
|
135
|
-
const result = this.db
|
|
141
|
+
const result = runInTransaction(this.db, runInsert);
|
|
136
142
|
return result;
|
|
137
143
|
}
|
|
138
144
|
getSummary(id) {
|
|
@@ -246,8 +252,14 @@ export class LcmStore {
|
|
|
246
252
|
.all(segmentId, retainDepth);
|
|
247
253
|
for (const summary of summaries) {
|
|
248
254
|
report.indexDeletes.push({ corpus: "lcm_summary", sourceId: lcmSummaryIndexSourceId(summary.id) });
|
|
249
|
-
report.summaryFtsRowsDeleted += this.deleteSummaryFtsRow(summary.id);
|
|
250
255
|
}
|
|
256
|
+
report.summaryFtsRowsDeleted += this.db
|
|
257
|
+
.prepare(`DELETE FROM lcm_summaries_fts
|
|
258
|
+
WHERE rowid IN (
|
|
259
|
+
SELECT id FROM lcm_summaries
|
|
260
|
+
WHERE segment_id = ? AND pinned = 0 AND depth < ?
|
|
261
|
+
)`)
|
|
262
|
+
.run(segmentId, retainDepth).changes;
|
|
251
263
|
report.summariesDeleted += this.db
|
|
252
264
|
.prepare("DELETE FROM lcm_summaries WHERE segment_id = ? AND pinned = 0 AND depth < ?")
|
|
253
265
|
.run(segmentId, retainDepth).changes;
|
|
@@ -257,8 +269,10 @@ export class LcmStore {
|
|
|
257
269
|
const records = this.db.prepare("SELECT id FROM lcm_records WHERE segment_id = ?").all(segmentId);
|
|
258
270
|
for (const record of records) {
|
|
259
271
|
report.indexDeletes.push({ corpus: "lcm_record", sourceId: lcmRecordIndexSourceId(record.id) });
|
|
260
|
-
report.recordFtsRowsDeleted += this.deleteRecordFtsRow(record.id);
|
|
261
272
|
}
|
|
273
|
+
report.recordFtsRowsDeleted += this.db
|
|
274
|
+
.prepare("DELETE FROM lcm_records_fts WHERE rowid IN (SELECT id FROM lcm_records WHERE segment_id = ?)")
|
|
275
|
+
.run(segmentId).changes;
|
|
262
276
|
report.rawRecordsDeleted += this.db
|
|
263
277
|
.prepare("DELETE FROM lcm_records WHERE segment_id = ?")
|
|
264
278
|
.run(segmentId).changes;
|
|
@@ -318,20 +332,6 @@ export class LcmStore {
|
|
|
318
332
|
}
|
|
319
333
|
return map;
|
|
320
334
|
}
|
|
321
|
-
deleteRecordFtsRow(id) {
|
|
322
|
-
const row = this.db.prepare("SELECT text_full FROM lcm_records WHERE id = ?").get(id);
|
|
323
|
-
if (!row)
|
|
324
|
-
return 0;
|
|
325
|
-
this.db.prepare("DELETE FROM lcm_records_fts WHERE rowid = ?").run(id);
|
|
326
|
-
return 1;
|
|
327
|
-
}
|
|
328
|
-
deleteSummaryFtsRow(id) {
|
|
329
|
-
const row = this.db.prepare("SELECT text_full FROM lcm_summaries WHERE id = ?").get(id);
|
|
330
|
-
if (!row)
|
|
331
|
-
return 0;
|
|
332
|
-
this.db.prepare("DELETE FROM lcm_summaries_fts WHERE rowid = ?").run(id);
|
|
333
|
-
return 1;
|
|
334
|
-
}
|
|
335
335
|
snapshotSummariesForPrunedRecords(segmentId) {
|
|
336
336
|
const summaries = this.db
|
|
337
337
|
.prepare(`SELECT * FROM lcm_summaries
|
|
@@ -424,7 +424,10 @@ function insertRecordPrepared(db, normalized) {
|
|
|
424
424
|
if (normalized.kind !== "boundary") {
|
|
425
425
|
db.prepare("INSERT INTO lcm_records_fts(rowid, text_full) VALUES (?, ?)").run(id, normalized.text);
|
|
426
426
|
}
|
|
427
|
-
|
|
427
|
+
const row = db.prepare("SELECT * FROM lcm_records WHERE id = ?").get(id);
|
|
428
|
+
if (!row)
|
|
429
|
+
throw new Error(`Failed to read inserted LCM record: ${id}`);
|
|
430
|
+
return row;
|
|
428
431
|
}
|
|
429
432
|
function normalizeSummaryInput(input) {
|
|
430
433
|
const text = (input.text ?? "").trim();
|
package/dist/memory/operator.js
CHANGED
|
@@ -11,6 +11,7 @@ import { backfillFromChatLogs } from "./lcm/backfill.js";
|
|
|
11
11
|
import { indexLcmRecords, indexLcmSummaries } from "./lcm/indexer.js";
|
|
12
12
|
import { lcmRecordIndexSourceId, lcmSummaryIndexSourceId } from "./lcm/store.js";
|
|
13
13
|
import { MemoryService } from "./service.js";
|
|
14
|
+
import { runInTransaction } from "./util.js";
|
|
14
15
|
export async function runMemoryOperator(config, argv) {
|
|
15
16
|
const [command, ...args] = argv;
|
|
16
17
|
if (!command || command === "--help" || command === "help") {
|
|
@@ -151,10 +152,7 @@ async function reindex(config, service, options, embeddingProvider, signal) {
|
|
|
151
152
|
for (const corpus of corpora)
|
|
152
153
|
deleteCorpus(service.memoryStore, corpus);
|
|
153
154
|
};
|
|
154
|
-
|
|
155
|
-
runDelete();
|
|
156
|
-
else
|
|
157
|
-
service.memoryStore.db.transaction(runDelete).immediate();
|
|
155
|
+
runInTransaction(service.memoryStore.db, runDelete);
|
|
158
156
|
const indexer = options.force
|
|
159
157
|
? new ChunkIndexer({
|
|
160
158
|
store: service.memoryStore,
|
package/dist/memory/service.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
|
+
import { isEnoent } from "../util/fs.js";
|
|
3
4
|
import { __ambientDiaryInjectorTest, AmbientDiaryInjector } from "./diary/ambient-injector.js";
|
|
4
5
|
import { DIARY_INDEX_FILE_RE, indexAllDiaryFiles, indexDiaryFile, removeDiaryFileIndex } from "./diary/indexer.js";
|
|
5
6
|
import { ChunkIndexer } from "./index/chunk-indexer.js";
|
|
@@ -196,7 +197,4 @@ function parseIndexSourceId(value, prefix) {
|
|
|
196
197
|
const id = Number(value.slice(prefix.length + 1));
|
|
197
198
|
return Number.isInteger(id) && id > 0 ? id : null;
|
|
198
199
|
}
|
|
199
|
-
function isEnoent(error) {
|
|
200
|
-
return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
201
|
-
}
|
|
202
200
|
export const __memoryServiceTest = __ambientDiaryInjectorTest;
|
package/dist/memory/tools.js
CHANGED
package/dist/models.js
CHANGED
|
@@ -95,6 +95,9 @@ export function createConfiguredModel(config) {
|
|
|
95
95
|
}
|
|
96
96
|
return resolveModel(ref, config);
|
|
97
97
|
}
|
|
98
|
+
export function resolveProviderSetting(records, provider, modelId) {
|
|
99
|
+
return records[`${provider}/${modelId}`] ?? records[provider];
|
|
100
|
+
}
|
|
98
101
|
export function resolveModelApiKey(config, model) {
|
|
99
102
|
const configuredEnv = config.models.apiKeyEnvs[`${model.provider}/${model.id}`] ??
|
|
100
103
|
config.models.apiKeyEnvs[model.provider] ??
|
package/dist/persona.js
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
-
|
|
3
|
-
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
4
|
-
}
|
|
5
|
-
async function readOptionalPersonaFile(path) {
|
|
6
|
-
try {
|
|
7
|
-
return await readFile(path, "utf8");
|
|
8
|
-
}
|
|
9
|
-
catch (error) {
|
|
10
|
-
if (isMissingFile(error))
|
|
11
|
-
return null;
|
|
12
|
-
throw error;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
2
|
+
import { readFileOrNull } from "./util/fs.js";
|
|
15
3
|
export async function loadPersona(config) {
|
|
16
4
|
const [soul, user, memory, inner] = await Promise.all([
|
|
17
5
|
readFile(config.persona.soul, "utf8"),
|
|
18
6
|
readFile(config.persona.user, "utf8"),
|
|
19
7
|
readFile(config.persona.memory, "utf8"),
|
|
20
|
-
|
|
8
|
+
readFileOrNull(config.persona.inner, "utf8"),
|
|
21
9
|
]);
|
|
22
10
|
return { soul, user, memory, inner };
|
|
23
11
|
}
|
package/dist/runtime.js
CHANGED
|
@@ -1,31 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { buildRecordBase, } from "./chat-log.js";
|
|
3
3
|
import { promptAttachmentNotes } from "./inbound-attachments.js";
|
|
4
|
+
import { formatLocalTimestamp } from "./util/time.js";
|
|
4
5
|
function formatAuthor(authorName, authorId) {
|
|
5
6
|
return authorName ? `${authorName} (uid:${authorId})` : `uid:${authorId}`;
|
|
6
7
|
}
|
|
7
|
-
function formatLocalTimestamp(ts) {
|
|
8
|
-
const date = new Date(ts);
|
|
9
|
-
if (Number.isNaN(date.getTime()))
|
|
10
|
-
return ts;
|
|
11
|
-
const offsetMinutes = -date.getTimezoneOffset();
|
|
12
|
-
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
13
|
-
const absolute = Math.abs(offsetMinutes);
|
|
14
|
-
const hours = Math.floor(absolute / 60);
|
|
15
|
-
const minutes = absolute % 60;
|
|
16
|
-
const offset = minutes === 0 ? `GMT${sign}${hours}` : `GMT${sign}${hours}:${String(minutes).padStart(2, "0")}`;
|
|
17
|
-
const local = [
|
|
18
|
-
date.getFullYear(),
|
|
19
|
-
String(date.getMonth() + 1).padStart(2, "0"),
|
|
20
|
-
String(date.getDate()).padStart(2, "0"),
|
|
21
|
-
].join("-");
|
|
22
|
-
const time = [
|
|
23
|
-
String(date.getHours()).padStart(2, "0"),
|
|
24
|
-
String(date.getMinutes()).padStart(2, "0"),
|
|
25
|
-
String(date.getSeconds()).padStart(2, "0"),
|
|
26
|
-
].join(":");
|
|
27
|
-
return `${local} ${time} ${offset}`;
|
|
28
|
-
}
|
|
29
8
|
function formatPromptRecord(record) {
|
|
30
9
|
const text = record.text.trim() || "(no text)";
|
|
31
10
|
const author = record.authorName?.trim()
|
|
@@ -365,7 +344,7 @@ export class ConversationRuntime {
|
|
|
365
344
|
}
|
|
366
345
|
async noteOutbound(options) {
|
|
367
346
|
const text = options.text.trim();
|
|
368
|
-
if (!text && options.messageIds.length === 0)
|
|
347
|
+
if (!text && options.messageIds.length === 0 && !options.silent)
|
|
369
348
|
return undefined;
|
|
370
349
|
const record = {
|
|
371
350
|
type: "outbound",
|
package/dist/scheduler.js
CHANGED
|
@@ -1,35 +1,9 @@
|
|
|
1
|
-
import { appendFile, mkdir,
|
|
1
|
+
import { appendFile, mkdir, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { readFileOrNull } from "./util/fs.js";
|
|
4
|
+
import { formatLocalTimestamp, toDate } from "./util/time.js";
|
|
5
|
+
export { formatLocalTimestamp } from "./util/time.js";
|
|
3
6
|
const stateWriteQueues = new Map();
|
|
4
|
-
function toDate(value) {
|
|
5
|
-
if (value instanceof Date)
|
|
6
|
-
return value;
|
|
7
|
-
return new Date(value);
|
|
8
|
-
}
|
|
9
|
-
function formatOffset(date) {
|
|
10
|
-
const offsetMinutes = -date.getTimezoneOffset();
|
|
11
|
-
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
12
|
-
const absolute = Math.abs(offsetMinutes);
|
|
13
|
-
const hours = Math.floor(absolute / 60);
|
|
14
|
-
const minutes = absolute % 60;
|
|
15
|
-
return minutes === 0 ? `GMT${sign}${hours}` : `GMT${sign}${hours}:${String(minutes).padStart(2, "0")}`;
|
|
16
|
-
}
|
|
17
|
-
export function formatLocalTimestamp(value) {
|
|
18
|
-
const date = toDate(value);
|
|
19
|
-
if (Number.isNaN(date.getTime()))
|
|
20
|
-
return String(value);
|
|
21
|
-
const localDate = [
|
|
22
|
-
date.getFullYear(),
|
|
23
|
-
String(date.getMonth() + 1).padStart(2, "0"),
|
|
24
|
-
String(date.getDate()).padStart(2, "0"),
|
|
25
|
-
].join("-");
|
|
26
|
-
const localTime = [
|
|
27
|
-
String(date.getHours()).padStart(2, "0"),
|
|
28
|
-
String(date.getMinutes()).padStart(2, "0"),
|
|
29
|
-
String(date.getSeconds()).padStart(2, "0"),
|
|
30
|
-
].join(":");
|
|
31
|
-
return `${localDate} ${localTime} ${formatOffset(date)}`;
|
|
32
|
-
}
|
|
33
7
|
export function formatIdleDuration(ms) {
|
|
34
8
|
if (!Number.isFinite(ms) || ms <= 0)
|
|
35
9
|
return "0m";
|
|
@@ -53,9 +27,7 @@ export function buildHeartbeatInjectionText(options) {
|
|
|
53
27
|
const body = options.body ??
|
|
54
28
|
`hey~ been quiet for a bit. this is your time now.
|
|
55
29
|
|
|
56
|
-
what you do with it is up to you — HEARTBEAT.md has the menu if you don't remember it. once you know the shape of it you don't have to re-read every fire
|
|
57
|
-
|
|
58
|
-
it's okay to sit one out, but only when that's actually the real answer — not when it's the easy one.`;
|
|
30
|
+
what you do with it is up to you — HEARTBEAT.md has the menu if you don't remember it. once you know the shape of it you don't have to re-read every fire.`;
|
|
59
31
|
return `<heartbeat local_time="${formatLocalTimestamp(nowDate)}" idle_duration="${formatIdleDuration(idleDurationMs)}" idle_minutes="${idleMinutes}">\n${body}\n</heartbeat>`;
|
|
60
32
|
}
|
|
61
33
|
function pad2(value) {
|
|
@@ -146,22 +118,16 @@ export function schedulerLogPath(dataDir, now = new Date()) {
|
|
|
146
118
|
return resolve(dataDir, "scheduler", `${now.toISOString().slice(0, 10)}.jsonl`);
|
|
147
119
|
}
|
|
148
120
|
export async function loadSchedulerState(dataDir) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
catch (error) {
|
|
161
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
162
|
-
return { cron: {} };
|
|
163
|
-
throw error;
|
|
164
|
-
}
|
|
121
|
+
const raw = await readFileOrNull(schedulerStatePath(dataDir), "utf8");
|
|
122
|
+
if (raw === null)
|
|
123
|
+
return { cron: {} };
|
|
124
|
+
const parsed = JSON.parse(raw);
|
|
125
|
+
return {
|
|
126
|
+
heartbeat: parsed.heartbeat && typeof parsed.heartbeat === "object" && !Array.isArray(parsed.heartbeat)
|
|
127
|
+
? parsed.heartbeat
|
|
128
|
+
: undefined,
|
|
129
|
+
cron: parsed.cron && typeof parsed.cron === "object" && !Array.isArray(parsed.cron) ? parsed.cron : {},
|
|
130
|
+
};
|
|
165
131
|
}
|
|
166
132
|
export async function saveSchedulerState(dataDir, state) {
|
|
167
133
|
const path = schedulerStatePath(dataDir);
|
package/dist/service.js
CHANGED
|
@@ -141,6 +141,26 @@ async function run(command, args, options) {
|
|
|
141
141
|
}
|
|
142
142
|
await execFileAsync(command, args);
|
|
143
143
|
}
|
|
144
|
+
async function runInteractive(command, args, options, errorPrefix) {
|
|
145
|
+
const currentPlatform = options.platform ?? platform();
|
|
146
|
+
if (options.runCommand) {
|
|
147
|
+
await options.runCommand(command, args);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await new Promise((resolveRun, rejectRun) => {
|
|
151
|
+
const child = spawn(command, args, {
|
|
152
|
+
shell: currentPlatform === "win32",
|
|
153
|
+
stdio: "inherit",
|
|
154
|
+
});
|
|
155
|
+
child.on("exit", (code) => {
|
|
156
|
+
if (code === 0)
|
|
157
|
+
resolveRun();
|
|
158
|
+
else
|
|
159
|
+
rejectRun(new Error(`${errorPrefix} failed with exit code ${code ?? "unknown"}`));
|
|
160
|
+
});
|
|
161
|
+
child.on("error", rejectRun);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
144
164
|
async function capture(command, args, options) {
|
|
145
165
|
if (options.captureCommand)
|
|
146
166
|
return options.captureCommand(command, args);
|
|
@@ -254,22 +274,12 @@ async function supervisorState(spec, options) {
|
|
|
254
274
|
return "not-loaded";
|
|
255
275
|
}
|
|
256
276
|
}
|
|
257
|
-
export async function upgradeFamiliar(options = {}) {
|
|
277
|
+
export async function upgradeFamiliar(workspacePath, options = {}) {
|
|
258
278
|
const currentPlatform = options.platform ?? platform();
|
|
259
279
|
const npmCommand = currentPlatform === "win32" ? "npm.cmd" : "npm";
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
stdio: "inherit",
|
|
264
|
-
});
|
|
265
|
-
child.on("exit", (code) => {
|
|
266
|
-
if (code === 0)
|
|
267
|
-
resolveUpgrade();
|
|
268
|
-
else
|
|
269
|
-
rejectUpgrade(new Error(`npm upgrade failed with exit code ${code ?? "unknown"}`));
|
|
270
|
-
});
|
|
271
|
-
child.on("error", rejectUpgrade);
|
|
272
|
-
});
|
|
280
|
+
const familiarCommand = currentPlatform === "win32" ? "familiar.cmd" : "familiar";
|
|
281
|
+
await runInteractive(npmCommand, ["install", "-g", "@qearlyao/familiar@latest"], options, "npm upgrade");
|
|
282
|
+
await runInteractive(familiarCommand, ["init", workspacePath], options, "workspace default refresh");
|
|
273
283
|
}
|
|
274
284
|
export function formatServiceResult(result) {
|
|
275
285
|
return [result.title, ...result.details].join("\n");
|
package/dist/settings.js
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
return (value === "off" ||
|
|
5
|
-
value === "minimal" ||
|
|
6
|
-
value === "low" ||
|
|
7
|
-
value === "medium" ||
|
|
8
|
-
value === "high" ||
|
|
9
|
-
value === "xhigh");
|
|
10
|
-
}
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { isThinkingLevel } from "./models.js";
|
|
3
|
+
import { atomicWriteJson, createWriteQueue, readFileOrNull } from "./util/fs.js";
|
|
11
4
|
function isChannelTrigger(value) {
|
|
12
5
|
return value === "mention" || value === "always";
|
|
13
6
|
}
|
|
@@ -37,16 +30,8 @@ function normalizeSettingsFile(value) {
|
|
|
37
30
|
return { version: 1, channels };
|
|
38
31
|
}
|
|
39
32
|
async function readSettingsFile(path) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return normalizeSettingsFile(JSON.parse(raw));
|
|
43
|
-
}
|
|
44
|
-
catch (error) {
|
|
45
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
46
|
-
return { version: 1, channels: {} };
|
|
47
|
-
}
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
33
|
+
const raw = await readFileOrNull(path, "utf8");
|
|
34
|
+
return raw === null ? { version: 1, channels: {} } : normalizeSettingsFile(JSON.parse(raw));
|
|
50
35
|
}
|
|
51
36
|
function pruneChannel(settings) {
|
|
52
37
|
const pruned = {};
|
|
@@ -61,18 +46,8 @@ function pruneChannel(settings) {
|
|
|
61
46
|
export async function loadSettingsStore(config) {
|
|
62
47
|
const path = resolve(config.workspace.dataDir, "settings", "channel-overrides.json");
|
|
63
48
|
let file = await readSettingsFile(path);
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
await mkdir(dirname(path), { recursive: true });
|
|
67
|
-
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
68
|
-
await writeFile(tmpPath, `${JSON.stringify(file, null, 2)}\n`, "utf8");
|
|
69
|
-
await rename(tmpPath, path);
|
|
70
|
-
};
|
|
71
|
-
const enqueuePersist = () => {
|
|
72
|
-
const run = writeQueue.then(persist, () => persist());
|
|
73
|
-
writeQueue = run.then(() => undefined, () => undefined);
|
|
74
|
-
return run;
|
|
75
|
-
};
|
|
49
|
+
const enqueueWrite = createWriteQueue("channel settings");
|
|
50
|
+
const enqueuePersist = () => enqueueWrite(() => atomicWriteJson(path, file));
|
|
76
51
|
const updateChannel = async (channelKey, patch) => {
|
|
77
52
|
const next = pruneChannel({ ...file.channels[channelKey], ...patch });
|
|
78
53
|
const channels = { ...file.channels };
|
package/dist/tts.js
CHANGED
|
@@ -129,13 +129,7 @@ export function createTtsTool(config, mediaSink) {
|
|
|
129
129
|
return {
|
|
130
130
|
content: [{ type: "text", text: formatTtsNotice(name) }],
|
|
131
131
|
details: {
|
|
132
|
-
provider: "elevenlabs",
|
|
133
|
-
voiceId,
|
|
134
|
-
modelId: config.tts.modelId,
|
|
135
|
-
outputFormat,
|
|
136
132
|
localPath,
|
|
137
|
-
mimeType,
|
|
138
|
-
size: buffer.length,
|
|
139
133
|
},
|
|
140
134
|
};
|
|
141
135
|
},
|
package/dist/util/fs.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
export function isEnoent(error) {
|
|
4
|
+
return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
5
|
+
}
|
|
6
|
+
export async function readFileOrNull(path, encoding) {
|
|
7
|
+
try {
|
|
8
|
+
return await readFile(path, encoding);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (isEnoent(error))
|
|
12
|
+
return null;
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function fsyncFile(path) {
|
|
17
|
+
const handle = await open(path, "r+");
|
|
18
|
+
try {
|
|
19
|
+
await handle.sync();
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
await handle.close();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export async function atomicWriteJson(path, value) {
|
|
26
|
+
await mkdir(dirname(path), { recursive: true });
|
|
27
|
+
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
28
|
+
await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
29
|
+
await fsyncFile(tmpPath);
|
|
30
|
+
await rename(tmpPath, path);
|
|
31
|
+
}
|
|
32
|
+
export function createWriteQueue(logLabel) {
|
|
33
|
+
let queue = Promise.resolve();
|
|
34
|
+
return async (write) => {
|
|
35
|
+
const run = queue.then(write, write);
|
|
36
|
+
queue = run.then(() => undefined, (error) => {
|
|
37
|
+
console.error(`${logLabel} write failed`, error);
|
|
38
|
+
});
|
|
39
|
+
return run;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function isRecord(value) {
|
|
2
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
export function readEnum(value, path, allowed) {
|
|
5
|
+
if (typeof value === "string" && allowed.includes(value))
|
|
6
|
+
return value;
|
|
7
|
+
throw new Error(`Config value ${path} must be one of ${allowed.map((item) => JSON.stringify(item)).join(", ")}`);
|
|
8
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
export const IMAGE_MIME_BY_EXTENSION = {
|
|
3
|
+
".jpg": "image/jpeg",
|
|
4
|
+
".jpeg": "image/jpeg",
|
|
5
|
+
".png": "image/png",
|
|
6
|
+
".gif": "image/gif",
|
|
7
|
+
".webp": "image/webp",
|
|
8
|
+
};
|
|
9
|
+
export const IMAGE_EXTENSION_BY_MIME = {
|
|
10
|
+
"image/jpeg": ".jpg",
|
|
11
|
+
"image/png": ".png",
|
|
12
|
+
"image/gif": ".gif",
|
|
13
|
+
"image/webp": ".webp",
|
|
14
|
+
};
|
|
15
|
+
export function imageMimeTypeFromPath(path) {
|
|
16
|
+
return IMAGE_MIME_BY_EXTENSION[extname(path).toLowerCase()];
|
|
17
|
+
}
|
|
18
|
+
export function sniffImageMimeType(buffer) {
|
|
19
|
+
if (buffer.subarray(0, 3).equals(Buffer.from([0xff, 0xd8, 0xff])))
|
|
20
|
+
return "image/jpeg";
|
|
21
|
+
if (buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
|
|
22
|
+
return "image/png";
|
|
23
|
+
}
|
|
24
|
+
if (buffer.subarray(0, 6).toString("ascii") === "GIF87a" || buffer.subarray(0, 6).toString("ascii") === "GIF89a") {
|
|
25
|
+
return "image/gif";
|
|
26
|
+
}
|
|
27
|
+
if (buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") {
|
|
28
|
+
return "image/webp";
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function toDate(value) {
|
|
2
|
+
if (value instanceof Date)
|
|
3
|
+
return value;
|
|
4
|
+
return new Date(value);
|
|
5
|
+
}
|
|
6
|
+
function formatOffset(date) {
|
|
7
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
8
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
9
|
+
const absolute = Math.abs(offsetMinutes);
|
|
10
|
+
const hours = Math.floor(absolute / 60);
|
|
11
|
+
const minutes = absolute % 60;
|
|
12
|
+
return minutes === 0 ? `GMT${sign}${hours}` : `GMT${sign}${hours}:${String(minutes).padStart(2, "0")}`;
|
|
13
|
+
}
|
|
14
|
+
export function formatLocalTimestamp(value) {
|
|
15
|
+
const date = toDate(value);
|
|
16
|
+
if (Number.isNaN(date.getTime()))
|
|
17
|
+
return String(value);
|
|
18
|
+
const localDate = [
|
|
19
|
+
date.getFullYear(),
|
|
20
|
+
String(date.getMonth() + 1).padStart(2, "0"),
|
|
21
|
+
String(date.getDate()).padStart(2, "0"),
|
|
22
|
+
].join("-");
|
|
23
|
+
const localTime = [
|
|
24
|
+
String(date.getHours()).padStart(2, "0"),
|
|
25
|
+
String(date.getMinutes()).padStart(2, "0"),
|
|
26
|
+
String(date.getSeconds()).padStart(2, "0"),
|
|
27
|
+
].join(":");
|
|
28
|
+
return `${localDate} ${localTime} ${formatOffset(date)}`;
|
|
29
|
+
}
|
package/dist/web-auth.js
CHANGED
|
@@ -11,7 +11,10 @@ function parseCookies(header) {
|
|
|
11
11
|
const [name, ...valueParts] = part.trim().split("=");
|
|
12
12
|
if (!name)
|
|
13
13
|
continue;
|
|
14
|
-
|
|
14
|
+
try {
|
|
15
|
+
cookies[name] = decodeURIComponent(valueParts.join("="));
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
15
18
|
}
|
|
16
19
|
return cookies;
|
|
17
20
|
}
|