@qearlyao/familiar 0.1.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/.env.example +31 -0
- package/HEARTBEAT.md +23 -0
- package/LICENSE +21 -0
- package/MEMORY.md +1 -0
- package/README.md +245 -0
- package/SOUL.md +13 -0
- package/USER.md +13 -0
- package/config.example.toml +221 -0
- package/dist/agent-events.js +167 -0
- package/dist/agent.js +590 -0
- package/dist/browser-tools.js +638 -0
- package/dist/chat-log.js +130 -0
- package/dist/cli.js +168 -0
- package/dist/config.js +804 -0
- package/dist/data-retention.js +54 -0
- package/dist/discord.js +1203 -0
- package/dist/generated-media.js +86 -0
- package/dist/image-derivatives.js +102 -0
- package/dist/image-gen.js +440 -0
- package/dist/inbound-attachments.js +266 -0
- package/dist/index.js +10 -0
- package/dist/media-understanding.js +120 -0
- package/dist/memory/diary/ambient-injector.js +180 -0
- package/dist/memory/diary/ambient.js +124 -0
- package/dist/memory/diary/chunks.js +231 -0
- package/dist/memory/diary/index.js +3 -0
- package/dist/memory/diary/indexer.js +93 -0
- package/dist/memory/doctor.js +250 -0
- package/dist/memory/index/chunk-indexer.js +151 -0
- package/dist/memory/index/embedding-provider.js +119 -0
- package/dist/memory/index/fts-query.js +18 -0
- package/dist/memory/index/retrieval.js +246 -0
- package/dist/memory/index/schema.js +157 -0
- package/dist/memory/index/store.js +513 -0
- package/dist/memory/index/vec.js +72 -0
- package/dist/memory/index/vector-codec.js +27 -0
- package/dist/memory/lcm/backfill.js +247 -0
- package/dist/memory/lcm/condense.js +146 -0
- package/dist/memory/lcm/context-transformer.js +662 -0
- package/dist/memory/lcm/context.js +421 -0
- package/dist/memory/lcm/eviction-score.js +38 -0
- package/dist/memory/lcm/index.js +6 -0
- package/dist/memory/lcm/indexer.js +200 -0
- package/dist/memory/lcm/normalize.js +235 -0
- package/dist/memory/lcm/schema.js +188 -0
- package/dist/memory/lcm/segment-manager.js +136 -0
- package/dist/memory/lcm/store.js +722 -0
- package/dist/memory/lcm/summarizer.js +258 -0
- package/dist/memory/lcm/types.js +1 -0
- package/dist/memory/operator.js +477 -0
- package/dist/memory/service.js +202 -0
- package/dist/memory/tools.js +205 -0
- package/dist/models.js +165 -0
- package/dist/persona.js +54 -0
- package/dist/runtime.js +493 -0
- package/dist/scheduler.js +200 -0
- package/dist/settings.js +116 -0
- package/dist/skills.js +38 -0
- package/dist/tts.js +143 -0
- package/dist/web-auth.js +105 -0
- package/dist/web-events.js +114 -0
- package/dist/web-http.js +29 -0
- package/dist/web-static.js +106 -0
- package/dist/web-tools.js +940 -0
- package/dist/web-types.js +2 -0
- package/dist/web.js +844 -0
- package/package.json +60 -0
- package/web/dist/assets/index-ClgkMgaq.css +2 -0
- package/web/dist/assets/index-Cu2QquuR.js +59 -0
- package/web/dist/favicon.svg +1 -0
- package/web/dist/icons.svg +24 -0
- package/web/dist/index.html +20 -0
package/dist/chat-log.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { appendFile, mkdir, open, readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
function sanitizeSegment(value) {
|
|
4
|
+
return value.replace(/[^A-Za-z0-9._=-]+/g, "_").slice(0, 120) || "unknown";
|
|
5
|
+
}
|
|
6
|
+
export function chatChannelKey(channel) {
|
|
7
|
+
const parts = [channel.service, channel.scope, channel.channelId];
|
|
8
|
+
if (channel.threadId)
|
|
9
|
+
parts.push(channel.threadId);
|
|
10
|
+
return parts.map(sanitizeSegment).join("-");
|
|
11
|
+
}
|
|
12
|
+
export function chatLogPath(config, channel, now = new Date()) {
|
|
13
|
+
const date = now.toISOString().slice(0, 10);
|
|
14
|
+
return resolve(config.workspace.dataDir, "chat", chatChannelKey(channel), `${date}.jsonl`);
|
|
15
|
+
}
|
|
16
|
+
function chatLockPath(config, channel) {
|
|
17
|
+
return resolve(config.workspace.dataDir, "chat", chatChannelKey(channel), ".lock");
|
|
18
|
+
}
|
|
19
|
+
function chatChannelDir(config, channel) {
|
|
20
|
+
return resolve(config.workspace.dataDir, "chat", chatChannelKey(channel));
|
|
21
|
+
}
|
|
22
|
+
function getErrorCode(error) {
|
|
23
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
24
|
+
}
|
|
25
|
+
function extractOwnerPid(owner) {
|
|
26
|
+
const match = owner.match(/^familiar-(\d+)-/);
|
|
27
|
+
if (!match)
|
|
28
|
+
return undefined;
|
|
29
|
+
const pid = Number(match[1]);
|
|
30
|
+
return Number.isFinite(pid) ? pid : undefined;
|
|
31
|
+
}
|
|
32
|
+
function isPidAlive(pid) {
|
|
33
|
+
try {
|
|
34
|
+
process.kill(pid, 0);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
return getErrorCode(error) === "EPERM";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isChatLogRecord(value) {
|
|
42
|
+
if (!value || typeof value !== "object")
|
|
43
|
+
return false;
|
|
44
|
+
const record = value;
|
|
45
|
+
return typeof record.recordId === "number" && typeof record.ts === "string" && typeof record.type === "string";
|
|
46
|
+
}
|
|
47
|
+
export function createChatLog(config, channel) {
|
|
48
|
+
const dir = chatChannelDir(config, channel);
|
|
49
|
+
const lockPath = chatLockPath(config, channel);
|
|
50
|
+
return {
|
|
51
|
+
channel,
|
|
52
|
+
dir,
|
|
53
|
+
lockPath,
|
|
54
|
+
async read() {
|
|
55
|
+
let files;
|
|
56
|
+
try {
|
|
57
|
+
files = await readdir(dir);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (getErrorCode(error) === "ENOENT")
|
|
61
|
+
return [];
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
const records = [];
|
|
65
|
+
for (const file of files.filter((entry) => entry.endsWith(".jsonl")).sort()) {
|
|
66
|
+
const filePath = resolve(dir, file);
|
|
67
|
+
const content = await readFile(filePath, "utf8");
|
|
68
|
+
for (const [index, line] of content.split(/\r?\n/).entries()) {
|
|
69
|
+
if (!line.trim())
|
|
70
|
+
continue;
|
|
71
|
+
const parsed = JSON.parse(line);
|
|
72
|
+
if (!isChatLogRecord(parsed))
|
|
73
|
+
throw new Error(`Malformed chat log record: ${filePath}:${index + 1}`);
|
|
74
|
+
records.push(parsed);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return records.sort((a, b) => a.recordId - b.recordId);
|
|
78
|
+
},
|
|
79
|
+
async append(record) {
|
|
80
|
+
const recordPath = chatLogPath(config, channel, new Date(record.ts));
|
|
81
|
+
await mkdir(dirname(recordPath), { recursive: true });
|
|
82
|
+
await appendFile(recordPath, `${JSON.stringify(record)}\n`, "utf8");
|
|
83
|
+
},
|
|
84
|
+
async acquire(owner) {
|
|
85
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
86
|
+
try {
|
|
87
|
+
const handle = await open(lockPath, "wx");
|
|
88
|
+
try {
|
|
89
|
+
await handle.writeFile(`${owner}\n`, "utf8");
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
await handle.close();
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (getErrorCode(error) !== "EEXIST")
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const existingOwner = (await readFile(lockPath, "utf8").catch(() => "")).trim();
|
|
101
|
+
const existingPid = extractOwnerPid(existingOwner);
|
|
102
|
+
if (existingPid !== undefined && !isPidAlive(existingPid)) {
|
|
103
|
+
await rm(lockPath, { force: true });
|
|
104
|
+
const handle = await open(lockPath, "wx");
|
|
105
|
+
try {
|
|
106
|
+
await handle.writeFile(`${owner}\n`, "utf8");
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await handle.close();
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Chat channel is already locked by ${existingOwner || "another familiar process"}`);
|
|
114
|
+
},
|
|
115
|
+
async release() {
|
|
116
|
+
await rm(lockPath, { force: true });
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
export function buildRecordBase(channel, recordId) {
|
|
121
|
+
return {
|
|
122
|
+
recordId,
|
|
123
|
+
ts: new Date().toISOString(),
|
|
124
|
+
service: channel.service,
|
|
125
|
+
scope: channel.scope,
|
|
126
|
+
channelId: channel.channelId,
|
|
127
|
+
channelName: channel.channelName,
|
|
128
|
+
threadId: channel.threadId,
|
|
129
|
+
};
|
|
130
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { config as loadDotenv } from "dotenv";
|
|
8
|
+
import { createFamiliarAgent } from "./agent.js";
|
|
9
|
+
import { loadConfig } from "./config.js";
|
|
10
|
+
import { runDataRetention } from "./data-retention.js";
|
|
11
|
+
import { startDiscordDaemon } from "./discord.js";
|
|
12
|
+
import { cleanupGeneratedAttachments } from "./generated-media.js";
|
|
13
|
+
import { memoryHelp, runMemoryOperator } from "./memory/operator.js";
|
|
14
|
+
import { createMemoryService } from "./memory/service.js";
|
|
15
|
+
import { loadSettingsStore } from "./settings.js";
|
|
16
|
+
import { startWebDaemon } from "./web.js";
|
|
17
|
+
const SOURCE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const PROJECT_ROOT = resolve(SOURCE_DIR, "..");
|
|
19
|
+
const DEFAULT_WORKSPACE_PATH = resolve(homedir(), ".familiar");
|
|
20
|
+
const MEMORY_SUBCOMMANDS = new Set(["status", "doctor", "reindex", "backfill", "prune", "backup", "help", "--help"]);
|
|
21
|
+
function defaultWorkspaceDirs(workspacePath) {
|
|
22
|
+
const memoryRoot = resolve(workspacePath, "memories");
|
|
23
|
+
return {
|
|
24
|
+
dataDir: resolve(workspacePath, "data"),
|
|
25
|
+
memoryIndexDir: resolve(memoryRoot, "index"),
|
|
26
|
+
memoryLcmDir: resolve(memoryRoot, "lcm"),
|
|
27
|
+
memoryDiariesDir: resolve(memoryRoot, "diaries"),
|
|
28
|
+
memoryArchiveDir: resolve(memoryRoot, "archive"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function configuredWorkspaceDirs(config) {
|
|
32
|
+
return {
|
|
33
|
+
dataDir: config.workspace.dataDir,
|
|
34
|
+
memoryIndexDir: config.memory.indexDir,
|
|
35
|
+
memoryLcmDir: config.memory.lcmDir,
|
|
36
|
+
memoryDiariesDir: config.memory.diariesDir,
|
|
37
|
+
memoryArchiveDir: config.memory.archiveDir,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function ensureWorkspaceDirs(dirs) {
|
|
41
|
+
await Promise.all([
|
|
42
|
+
mkdir(dirs.dataDir, { recursive: true }),
|
|
43
|
+
mkdir(dirs.memoryIndexDir, { recursive: true }),
|
|
44
|
+
mkdir(dirs.memoryLcmDir, { recursive: true }),
|
|
45
|
+
mkdir(dirs.memoryDiariesDir, { recursive: true }),
|
|
46
|
+
mkdir(dirs.memoryArchiveDir, { recursive: true }),
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
function resolveWorkspaceInput(workspaceInput) {
|
|
50
|
+
return workspaceInput ? resolve(workspaceInput) : DEFAULT_WORKSPACE_PATH;
|
|
51
|
+
}
|
|
52
|
+
function parseMemoryArgs(workspaceOrCommand, rest) {
|
|
53
|
+
if (workspaceOrCommand && !MEMORY_SUBCOMMANDS.has(workspaceOrCommand)) {
|
|
54
|
+
return { workspacePath: resolveWorkspaceInput(workspaceOrCommand), args: rest };
|
|
55
|
+
}
|
|
56
|
+
return { workspacePath: DEFAULT_WORKSPACE_PATH, args: workspaceOrCommand ? [workspaceOrCommand, ...rest] : rest };
|
|
57
|
+
}
|
|
58
|
+
function isMemoryHelp(args) {
|
|
59
|
+
const command = args[0];
|
|
60
|
+
return !command || command === "help" || command === "--help";
|
|
61
|
+
}
|
|
62
|
+
async function initWorkspace(workspaceInput) {
|
|
63
|
+
const workspacePath = resolveWorkspaceInput(workspaceInput);
|
|
64
|
+
await mkdir(workspacePath, { recursive: true });
|
|
65
|
+
const envPath = resolve(workspacePath, ".env");
|
|
66
|
+
if (!existsSync(envPath)) {
|
|
67
|
+
await copyFile(resolve(PROJECT_ROOT, ".env.example"), envPath);
|
|
68
|
+
}
|
|
69
|
+
await copyFile(resolve(PROJECT_ROOT, "config.example.toml"), resolve(workspacePath, "config.toml"));
|
|
70
|
+
await copyFile(resolve(PROJECT_ROOT, "SOUL.md"), resolve(workspacePath, "SOUL.md"));
|
|
71
|
+
await copyFile(resolve(PROJECT_ROOT, "USER.md"), resolve(workspacePath, "USER.md"));
|
|
72
|
+
await copyFile(resolve(PROJECT_ROOT, "MEMORY.md"), resolve(workspacePath, "MEMORY.md"));
|
|
73
|
+
await copyFile(resolve(PROJECT_ROOT, "HEARTBEAT.md"), resolve(workspacePath, "HEARTBEAT.md"));
|
|
74
|
+
await ensureWorkspaceDirs(defaultWorkspaceDirs(workspacePath));
|
|
75
|
+
console.log(`Initialized familiar workspace at ${workspacePath}`);
|
|
76
|
+
}
|
|
77
|
+
async function runDaemon(workspaceInput) {
|
|
78
|
+
const workspacePath = resolveWorkspaceInput(workspaceInput);
|
|
79
|
+
const envPath = resolve(workspacePath, ".env");
|
|
80
|
+
if (existsSync(envPath)) {
|
|
81
|
+
loadDotenv({ path: envPath, override: false });
|
|
82
|
+
}
|
|
83
|
+
const config = await loadConfig(workspacePath);
|
|
84
|
+
const reloadConfig = async () => {
|
|
85
|
+
if (existsSync(envPath)) {
|
|
86
|
+
loadDotenv({ path: envPath, override: true });
|
|
87
|
+
}
|
|
88
|
+
return loadConfig(workspacePath);
|
|
89
|
+
};
|
|
90
|
+
await ensureWorkspaceDirs(configuredWorkspaceDirs(config));
|
|
91
|
+
const removedAttachments = await cleanupGeneratedAttachments(config);
|
|
92
|
+
if (removedAttachments > 0) {
|
|
93
|
+
console.log(`Removed ${removedAttachments} expired generated attachment(s)`);
|
|
94
|
+
}
|
|
95
|
+
const retention = await runDataRetention(config);
|
|
96
|
+
const removedData = retention.chat + retention.transcripts + retention.payloads;
|
|
97
|
+
if (removedData > 0) {
|
|
98
|
+
console.log(`Removed ${removedData} expired data file(s)`);
|
|
99
|
+
}
|
|
100
|
+
const settings = await loadSettingsStore(config);
|
|
101
|
+
const memoryService = createMemoryService(config);
|
|
102
|
+
await memoryService.indexDiaries().catch((error) => console.error("initial diary indexing failed", error));
|
|
103
|
+
memoryService.watchDiaries();
|
|
104
|
+
const familiarAgent = await createFamiliarAgent(config, settings, memoryService, { reloadConfig });
|
|
105
|
+
const discordDaemon = await startDiscordDaemon(config, familiarAgent, settings, memoryService);
|
|
106
|
+
const webDaemon = await startWebDaemon(config, familiarAgent, discordDaemon);
|
|
107
|
+
console.log(`familiar running for workspace ${config.workspacePath}`);
|
|
108
|
+
console.log("agent sessions are created per channel");
|
|
109
|
+
console.log(`settings=${settings.path}`);
|
|
110
|
+
const stop = async () => {
|
|
111
|
+
console.log("Stopping familiar");
|
|
112
|
+
await Promise.all([webDaemon.stop(), discordDaemon.stop()]);
|
|
113
|
+
memoryService.close();
|
|
114
|
+
process.exit(0);
|
|
115
|
+
};
|
|
116
|
+
process.once("SIGINT", () => void stop());
|
|
117
|
+
process.once("SIGTERM", () => void stop());
|
|
118
|
+
await new Promise(() => { });
|
|
119
|
+
}
|
|
120
|
+
function usage() {
|
|
121
|
+
return [
|
|
122
|
+
"Usage:",
|
|
123
|
+
" familiar init [workspace]",
|
|
124
|
+
" familiar run [workspace]",
|
|
125
|
+
" familiar memory [workspace] <subcommand>",
|
|
126
|
+
" familiar install-service",
|
|
127
|
+
" familiar status",
|
|
128
|
+
" familiar upgrade",
|
|
129
|
+
"",
|
|
130
|
+
`Default workspace: ${DEFAULT_WORKSPACE_PATH}`,
|
|
131
|
+
].join("\n");
|
|
132
|
+
}
|
|
133
|
+
async function main() {
|
|
134
|
+
const [, , command, workspace, ...rest] = process.argv;
|
|
135
|
+
if (command === "init") {
|
|
136
|
+
await initWorkspace(workspace);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (command === "run") {
|
|
140
|
+
await runDaemon(workspace);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (command === "memory") {
|
|
144
|
+
const { workspacePath, args } = parseMemoryArgs(workspace, rest);
|
|
145
|
+
if (isMemoryHelp(args)) {
|
|
146
|
+
console.log(memoryHelp());
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const envPath = resolve(workspacePath, ".env");
|
|
150
|
+
if (existsSync(envPath)) {
|
|
151
|
+
loadDotenv({ path: envPath, override: false });
|
|
152
|
+
}
|
|
153
|
+
const config = await loadConfig(workspacePath);
|
|
154
|
+
await ensureWorkspaceDirs(configuredWorkspaceDirs(config));
|
|
155
|
+
await runMemoryOperator(config, args);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (command === "install-service" || command === "status" || command === "upgrade") {
|
|
159
|
+
console.log("not yet implemented");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.error(usage());
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
}
|
|
165
|
+
main().catch((error) => {
|
|
166
|
+
console.error(error instanceof Error ? error.message : error);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
});
|