@qearlyao/familiar 0.2.5 → 0.4.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/HEARTBEAT.md +1 -1
- package/README.md +33 -0
- package/config.example.toml +4 -2
- package/dist/{agent.js → agent/factory.js} +97 -328
- 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/cli.js +45 -15
- package/dist/config/enums.js +35 -0
- package/dist/{config.js → config/index.js} +9 -272
- package/dist/config/interpolate.js +15 -0
- package/dist/config/model-refs.js +11 -0
- package/dist/{config-overrides.js → config/overrides.js} +1 -1
- package/dist/config/readers.js +116 -0
- package/dist/{config-registry.js → config/registry.js} +27 -8
- package/dist/config/sections.js +113 -0
- package/dist/{settings.js → config/settings.js} +5 -2
- package/dist/config/types.js +1 -0
- package/dist/{chat-log.js → conversation/chat-log.js} +16 -4
- package/dist/{contact-note.js → conversation/contact-note.js} +1 -1
- package/dist/conversation/ids.js +11 -0
- package/dist/conversation/owner-identity.js +29 -0
- 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/daemon.js +379 -0
- package/dist/discord/inbound.js +44 -0
- package/dist/discord/send.js +115 -0
- package/dist/discord/turn.js +55 -0
- package/dist/index.js +12 -11
- package/dist/lifecycle/control.js +1 -0
- package/dist/{data-retention.js → lifecycle/data-retention.js} +1 -1
- package/dist/{hot-reload.js → lifecycle/hot-reload.js} +2 -2
- package/dist/{service.js → lifecycle/service.js} +1 -0
- package/dist/media/attachment-limits.js +3 -0
- package/dist/{generated-media.js → media/generated-media.js} +1 -1
- package/dist/{image-gen.js → media/image-gen.js} +2 -2
- package/dist/{inbound-attachments.js → media/inbound-attachments.js} +47 -43
- package/dist/media/media-understanding.js +215 -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/memory/lcm/summarizer.js +1 -1
- package/dist/{added-models.js → models/added-models.js} +1 -1
- package/dist/{persona.js → prompting/persona.js} +1 -1
- package/dist/runtime/agent-core.js +82 -0
- package/dist/{agent-events.js → runtime/agent-events.js} +1 -1
- package/dist/runtime/agent-work-queue.js +55 -0
- package/dist/{runtime.js → runtime/conversation-runtime.js} +91 -43
- package/dist/runtime/runtime-manager.js +51 -0
- package/dist/runtime/scheduler-runner.js +243 -0
- package/dist/{scheduler.js → runtime/scheduler.js} +4 -4
- package/dist/{browser-tools.js → tools/browser-tools.js} +24 -34
- package/dist/util/fs.js +2 -1
- package/dist/web/agent-routes.js +104 -0
- package/dist/web/auth-routes.js +39 -0
- package/dist/web/auth.js +205 -0
- package/dist/web/config-routes.js +55 -0
- package/dist/web/conversation-routes.js +122 -0
- package/dist/web/daemon.js +108 -0
- package/dist/web/diary-routes.js +88 -0
- package/dist/web/errors.js +3 -0
- package/dist/web/event-hub.js +246 -0
- package/dist/{web-http.js → web/http.js} +19 -5
- package/dist/web/memes.js +25 -0
- package/dist/web/messages.js +348 -0
- package/dist/web/multipart.js +86 -0
- package/dist/web/payloads.js +34 -0
- package/dist/web/request-context.js +25 -0
- package/dist/web/route-helpers.js +9 -0
- package/dist/web/routes.js +37 -0
- package/dist/web/runtime-actions.js +231 -0
- package/dist/web/session-store.js +161 -0
- package/dist/{web-static.js → web/static.js} +19 -14
- package/dist/web/stream.js +78 -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/index.js +152 -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/npm-shrinkwrap.json +319 -201
- package/package.json +6 -4
- package/web/dist/assets/index-C-k4O5Dz.js +6 -0
- package/web/dist/assets/index-Dj-L9nX4.css +2 -0
- package/web/dist/assets/markdown-kaIeGxdv.js +14 -0
- package/web/dist/assets/react-Bi_azaFt.js +9 -0
- package/web/dist/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
- package/web/dist/assets/ui-C12-nN_X.js +51 -0
- package/web/dist/assets/vendor-D1QXMhXm.js +16 -0
- package/web/dist/index.html +11 -3
- package/dist/discord.js +0 -1299
- package/dist/media-understanding.js +0 -120
- package/dist/web-auth.js +0 -111
- package/dist/web-tools.js +0 -941
- package/dist/web.js +0 -1209
- package/web/dist/assets/index-B23WT77N.js +0 -63
- package/web/dist/assets/index-D3MotFzN.css +0 -2
- /package/dist/{control.js → agent/types.js} +0 -0
- /package/dist/{image-derivatives.js → media/image-derivatives.js} +0 -0
- /package/dist/{tts.js → media/tts.js} +0 -0
- /package/dist/{models.js → models/index.js} +0 -0
- /package/dist/{skills.js → prompting/skills.js} +0 -0
- /package/dist/{silent-marker.js → runtime/silent-marker.js} +0 -0
- /package/dist/{web-events.js → web/events.js} +0 -0
- /package/dist/{web-types.js → web/types.js} +0 -0
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
-
import { createPartFromBase64, createUserContent, GoogleGenAI } from "@google/genai";
|
|
3
|
-
import { parseModelRef, resolveModel } from "./models.js";
|
|
4
|
-
const GEMINI_API_VERSION_PATTERN = /\/(v1(?:beta|alpha)?|v\d+beta\d*)\/?$/;
|
|
5
|
-
function normalizeDerivedText(text) {
|
|
6
|
-
return text.trim().replace(/\n{3,}/g, "\n\n");
|
|
7
|
-
}
|
|
8
|
-
function labelForAttachment(kind) {
|
|
9
|
-
if (kind === "audio")
|
|
10
|
-
return "transcription";
|
|
11
|
-
if (kind === "video")
|
|
12
|
-
return "summary";
|
|
13
|
-
return "text";
|
|
14
|
-
}
|
|
15
|
-
function geminiHttpOptions(config) {
|
|
16
|
-
const ref = parseModelRef(`google/${config.mediaUnderstanding.video.model}`);
|
|
17
|
-
const model = ref ? resolveModel(ref, config) : undefined;
|
|
18
|
-
const baseUrl = model?.baseUrl;
|
|
19
|
-
if (!baseUrl)
|
|
20
|
-
return { timeout: 60_000 };
|
|
21
|
-
const match = baseUrl.match(GEMINI_API_VERSION_PATTERN);
|
|
22
|
-
if (!match)
|
|
23
|
-
return { baseUrl, timeout: 60_000 };
|
|
24
|
-
return {
|
|
25
|
-
baseUrl: baseUrl.slice(0, match.index),
|
|
26
|
-
apiVersion: match[1],
|
|
27
|
-
timeout: 60_000,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
async function transcribeAudioAttachment(config, attachment) {
|
|
31
|
-
if (!attachment.localPath || !attachment.mimeType?.startsWith("audio/"))
|
|
32
|
-
return undefined;
|
|
33
|
-
const apiKey = process.env[config.mediaUnderstanding.audio.apiKeyEnv];
|
|
34
|
-
if (!apiKey) {
|
|
35
|
-
console.warn(`media understanding skipped: ${config.mediaUnderstanding.audio.apiKeyEnv} is not set`);
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
const form = new FormData();
|
|
39
|
-
form.set("model", config.mediaUnderstanding.audio.model);
|
|
40
|
-
form.set("file", new Blob([await readFile(attachment.localPath)], { type: attachment.mimeType }), attachment.name);
|
|
41
|
-
const response = await fetch("https://api.groq.com/openai/v1/audio/transcriptions", {
|
|
42
|
-
method: "POST",
|
|
43
|
-
headers: {
|
|
44
|
-
Authorization: `Bearer ${apiKey}`,
|
|
45
|
-
},
|
|
46
|
-
body: form,
|
|
47
|
-
signal: AbortSignal.timeout(30_000),
|
|
48
|
-
});
|
|
49
|
-
if (!response.ok)
|
|
50
|
-
throw new Error(`Groq transcription failed: HTTP ${response.status}`);
|
|
51
|
-
const parsed = (await response.json());
|
|
52
|
-
const text = parsed.text?.trim();
|
|
53
|
-
if (!text)
|
|
54
|
-
return undefined;
|
|
55
|
-
return {
|
|
56
|
-
provider: "groq",
|
|
57
|
-
model: config.mediaUnderstanding.audio.model,
|
|
58
|
-
text: normalizeDerivedText(text),
|
|
59
|
-
label: labelForAttachment(attachment.kind),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
async function summarizeVideoAttachment(config, attachment) {
|
|
63
|
-
if (!attachment.localPath || !attachment.mimeType?.startsWith("video/"))
|
|
64
|
-
return undefined;
|
|
65
|
-
const apiKey = process.env[config.mediaUnderstanding.video.apiKeyEnv];
|
|
66
|
-
if (!apiKey) {
|
|
67
|
-
console.warn(`media understanding skipped: ${config.mediaUnderstanding.video.apiKeyEnv} is not set`);
|
|
68
|
-
return undefined;
|
|
69
|
-
}
|
|
70
|
-
const ai = new GoogleGenAI({ apiKey, httpOptions: geminiHttpOptions(config) });
|
|
71
|
-
const video = await readFile(attachment.localPath);
|
|
72
|
-
const response = await ai.models.generateContent({
|
|
73
|
-
model: config.mediaUnderstanding.video.model,
|
|
74
|
-
contents: createUserContent([
|
|
75
|
-
{
|
|
76
|
-
text: "Provide a concise description of this video, including any spoken content if present, and summarize the key visible events.",
|
|
77
|
-
},
|
|
78
|
-
createPartFromBase64(video.toString("base64"), attachment.mimeType),
|
|
79
|
-
]),
|
|
80
|
-
});
|
|
81
|
-
const text = response.text?.trim();
|
|
82
|
-
if (!text)
|
|
83
|
-
return undefined;
|
|
84
|
-
return {
|
|
85
|
-
provider: "google",
|
|
86
|
-
model: config.mediaUnderstanding.video.model,
|
|
87
|
-
text: normalizeDerivedText(text),
|
|
88
|
-
label: labelForAttachment(attachment.kind),
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
export async function deriveInboundAttachmentText(config, attachments) {
|
|
92
|
-
const next = [];
|
|
93
|
-
for (const attachment of attachments) {
|
|
94
|
-
if (attachment.derived?.text || !attachment.localPath) {
|
|
95
|
-
next.push(attachment);
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
if (attachment.mimeType?.startsWith("audio/")) {
|
|
100
|
-
const text = await transcribeAudioAttachment(config, attachment);
|
|
101
|
-
if (text) {
|
|
102
|
-
next.push({ ...attachment, derived: { ...(attachment.derived ?? {}), text } });
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (attachment.mimeType?.startsWith("video/")) {
|
|
107
|
-
const text = await summarizeVideoAttachment(config, attachment);
|
|
108
|
-
if (text) {
|
|
109
|
-
next.push({ ...attachment, derived: { ...(attachment.derived ?? {}), text } });
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
console.error("media understanding failed", error);
|
|
116
|
-
}
|
|
117
|
-
next.push(attachment);
|
|
118
|
-
}
|
|
119
|
-
return next;
|
|
120
|
-
}
|
package/dist/web-auth.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
const SESSION_TTL_MS = 12 * 60 * 60 * 1000;
|
|
3
|
-
function safeEqual(a, b) {
|
|
4
|
-
const left = Buffer.from(a);
|
|
5
|
-
const right = Buffer.from(b);
|
|
6
|
-
return left.length === right.length && timingSafeEqual(left, right);
|
|
7
|
-
}
|
|
8
|
-
function parseCookies(header) {
|
|
9
|
-
const cookies = {};
|
|
10
|
-
for (const part of (header ?? "").split(";")) {
|
|
11
|
-
const [name, ...valueParts] = part.trim().split("=");
|
|
12
|
-
if (!name)
|
|
13
|
-
continue;
|
|
14
|
-
try {
|
|
15
|
-
cookies[name] = decodeURIComponent(valueParts.join("="));
|
|
16
|
-
}
|
|
17
|
-
catch { }
|
|
18
|
-
}
|
|
19
|
-
return cookies;
|
|
20
|
-
}
|
|
21
|
-
function decodeTotpSecret(secret) {
|
|
22
|
-
const normalized = secret
|
|
23
|
-
.replace(/\s/g, "")
|
|
24
|
-
.replace(/={1,8}$/, "")
|
|
25
|
-
.toUpperCase();
|
|
26
|
-
if (!/^[A-Z2-7]+$/.test(normalized))
|
|
27
|
-
return Buffer.from(secret, "utf8");
|
|
28
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
29
|
-
let bits = "";
|
|
30
|
-
for (const char of normalized) {
|
|
31
|
-
const value = alphabet.indexOf(char);
|
|
32
|
-
if (value < 0)
|
|
33
|
-
return Buffer.from(secret, "utf8");
|
|
34
|
-
bits += value.toString(2).padStart(5, "0");
|
|
35
|
-
}
|
|
36
|
-
const bytes = [];
|
|
37
|
-
for (let offset = 0; offset + 8 <= bits.length; offset += 8) {
|
|
38
|
-
bytes.push(Number.parseInt(bits.slice(offset, offset + 8), 2));
|
|
39
|
-
}
|
|
40
|
-
return Buffer.from(bytes);
|
|
41
|
-
}
|
|
42
|
-
export function verifyTotp(secret, token, now = Date.now()) {
|
|
43
|
-
const normalized = token.replace(/\s+/g, "");
|
|
44
|
-
if (!/^\d{6}$/.test(normalized))
|
|
45
|
-
return false;
|
|
46
|
-
const secretBuffer = decodeTotpSecret(secret);
|
|
47
|
-
const counter = Math.floor(now / 30000);
|
|
48
|
-
for (let offset = -1; offset <= 1; offset++) {
|
|
49
|
-
const counterBuffer = Buffer.alloc(8);
|
|
50
|
-
counterBuffer.writeBigUInt64BE(BigInt(counter + offset));
|
|
51
|
-
const hmac = createHmac("sha1", secretBuffer).update(counterBuffer).digest();
|
|
52
|
-
const digestOffset = hmac[hmac.length - 1] & 0x0f;
|
|
53
|
-
const code = (((hmac[digestOffset] & 0x7f) << 24) |
|
|
54
|
-
((hmac[digestOffset + 1] & 0xff) << 16) |
|
|
55
|
-
((hmac[digestOffset + 2] & 0xff) << 8) |
|
|
56
|
-
(hmac[digestOffset + 3] & 0xff)) %
|
|
57
|
-
1000000;
|
|
58
|
-
if (safeEqual(code.toString().padStart(6, "0"), normalized))
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
63
|
-
function readBearerToken(request) {
|
|
64
|
-
const header = request.headers.authorization;
|
|
65
|
-
if (!header)
|
|
66
|
-
return undefined;
|
|
67
|
-
const match = header.match(/^Bearer (.+)$/i);
|
|
68
|
-
return match?.[1];
|
|
69
|
-
}
|
|
70
|
-
export function createAuth(config) {
|
|
71
|
-
const sessions = new Map();
|
|
72
|
-
const pruneSessions = () => {
|
|
73
|
-
const now = Date.now();
|
|
74
|
-
for (const [id, session] of sessions) {
|
|
75
|
-
if (session.expiresAt <= now)
|
|
76
|
-
sessions.delete(id);
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
const hasBearer = (request) => {
|
|
80
|
-
if (!config.web.bearerToken)
|
|
81
|
-
return false;
|
|
82
|
-
const token = readBearerToken(request);
|
|
83
|
-
return token !== undefined && safeEqual(token, config.web.bearerToken);
|
|
84
|
-
};
|
|
85
|
-
const hasSession = (request) => {
|
|
86
|
-
pruneSessions();
|
|
87
|
-
const sessionId = parseCookies(request.headers.cookie).familiar_session;
|
|
88
|
-
if (!sessionId)
|
|
89
|
-
return false;
|
|
90
|
-
return sessions.has(sessionId);
|
|
91
|
-
};
|
|
92
|
-
const authorize = (request, pathname) => {
|
|
93
|
-
if (pathname === "/api/web/auth/mode")
|
|
94
|
-
return true;
|
|
95
|
-
if (config.web.authMode === "tailscale-only")
|
|
96
|
-
return true;
|
|
97
|
-
if (config.web.authMode === "bearer")
|
|
98
|
-
return hasBearer(request);
|
|
99
|
-
return hasSession(request) || hasBearer(request);
|
|
100
|
-
};
|
|
101
|
-
const createSession = () => {
|
|
102
|
-
pruneSessions();
|
|
103
|
-
const id = randomBytes(32).toString("base64url");
|
|
104
|
-
sessions.set(id, { expiresAt: Date.now() + SESSION_TTL_MS });
|
|
105
|
-
return id;
|
|
106
|
-
};
|
|
107
|
-
return { authorize, createSession };
|
|
108
|
-
}
|
|
109
|
-
export function sessionCookie(sessionId) {
|
|
110
|
-
return `familiar_session=${encodeURIComponent(sessionId)}; HttpOnly; SameSite=Lax; Max-Age=${Math.floor(SESSION_TTL_MS / 1000)}; Path=/api/web`;
|
|
111
|
-
}
|