@kisev/memomatic 1.0.0-dev.100.g21366c4ad740
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 +97 -0
- package/README.ru.md +95 -0
- package/assets/systemd/memomatic-dream.service +9 -0
- package/assets/systemd/memomatic-dream.timer +10 -0
- package/assets/systemd/memomatic-sessions.service +9 -0
- package/assets/systemd/memomatic-sessions.timer +10 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +309 -0
- package/dist/corpus.d.ts +12 -0
- package/dist/corpus.js +138 -0
- package/dist/dream.d.ts +31 -0
- package/dist/dream.js +234 -0
- package/dist/entries.d.ts +31 -0
- package/dist/entries.js +119 -0
- package/dist/executor.d.ts +45 -0
- package/dist/executor.js +267 -0
- package/dist/gates.d.ts +27 -0
- package/dist/gates.js +69 -0
- package/dist/generated/cli.d.ts +28 -0
- package/dist/generated/cli.js +181 -0
- package/dist/inbox.d.ts +34 -0
- package/dist/inbox.js +224 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/ingest.d.ts +1 -0
- package/dist/ingest.js +15 -0
- package/dist/ingestion.d.ts +30 -0
- package/dist/ingestion.js +179 -0
- package/dist/mcp.d.ts +15 -0
- package/dist/mcp.js +205 -0
- package/dist/opencode-config.d.ts +1 -0
- package/dist/opencode-config.js +84 -0
- package/dist/operations.d.ts +7 -0
- package/dist/operations.js +3 -0
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +55 -0
- package/dist/rules.d.ts +13 -0
- package/dist/rules.js +39 -0
- package/dist/search.d.ts +28 -0
- package/dist/search.js +191 -0
- package/dist/service.d.ts +63 -0
- package/dist/service.js +163 -0
- package/dist/sessions.d.ts +36 -0
- package/dist/sessions.js +238 -0
- package/dist/settings.d.ts +48 -0
- package/dist/settings.js +189 -0
- package/dist/store.d.ts +55 -0
- package/dist/store.js +286 -0
- package/dist/visibility.d.ts +7 -0
- package/dist/visibility.js +13 -0
- package/package.json +48 -0
package/dist/settings.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
export const defaultSettings = () => ({
|
|
4
|
+
embedding: null,
|
|
5
|
+
sessions: {
|
|
6
|
+
model: null,
|
|
7
|
+
variant: null,
|
|
8
|
+
timeoutMs: 180000,
|
|
9
|
+
maxDurationMs: 0,
|
|
10
|
+
maxSessions: 0,
|
|
11
|
+
maxChars: 24000,
|
|
12
|
+
retries: 1,
|
|
13
|
+
idleMs: 600000,
|
|
14
|
+
opencodeUrl: null,
|
|
15
|
+
},
|
|
16
|
+
dream: {
|
|
17
|
+
model: null,
|
|
18
|
+
variant: null,
|
|
19
|
+
minScore: 0.45,
|
|
20
|
+
minUseful: 2,
|
|
21
|
+
minUniqueQueries: 2,
|
|
22
|
+
maxCandidates: 12,
|
|
23
|
+
memoryBudgetLines: 80,
|
|
24
|
+
maxPriorEntryLoss: 0.25,
|
|
25
|
+
timeoutMs: 180000,
|
|
26
|
+
maxDurationMs: 0,
|
|
27
|
+
retries: 1,
|
|
28
|
+
},
|
|
29
|
+
search: { maxResults: 6, minScore: 0.35, minSemanticScore: 0.45, halfLifeDays: 30 },
|
|
30
|
+
archive: { enabled: false, days: 60 },
|
|
31
|
+
});
|
|
32
|
+
function mergeSection(base, override) {
|
|
33
|
+
if (!override || typeof override !== "object")
|
|
34
|
+
return base;
|
|
35
|
+
const merged = { ...base };
|
|
36
|
+
for (const [key, value] of Object.entries(override)) {
|
|
37
|
+
if (key in base && value !== null && value !== undefined)
|
|
38
|
+
merged[key] = value;
|
|
39
|
+
}
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
const SESSIONS_KEYS = [
|
|
43
|
+
"model",
|
|
44
|
+
"variant",
|
|
45
|
+
"timeoutMs",
|
|
46
|
+
"maxDurationMs",
|
|
47
|
+
"maxSessions",
|
|
48
|
+
"maxChars",
|
|
49
|
+
"retries",
|
|
50
|
+
"idleMs",
|
|
51
|
+
"opencodeUrl",
|
|
52
|
+
];
|
|
53
|
+
export function normalizeSettings(raw) {
|
|
54
|
+
const defaults = defaultSettings();
|
|
55
|
+
const source = (raw ?? {});
|
|
56
|
+
const embedding = source.embedding;
|
|
57
|
+
const search = mergeSection(defaults.search, source.search);
|
|
58
|
+
const dream = mergeSection(defaults.dream, source.dream);
|
|
59
|
+
// Legacy single-model configs put extraction knobs under `dream`; adopt them
|
|
60
|
+
// for `sessions` unless the new section overrides the key.
|
|
61
|
+
const migrated = {
|
|
62
|
+
...(source.sessions ?? {}),
|
|
63
|
+
};
|
|
64
|
+
const legacyDream = source.dream ?? {};
|
|
65
|
+
for (const key of SESSIONS_KEYS)
|
|
66
|
+
if (migrated[key] === undefined && legacyDream[key] !== undefined)
|
|
67
|
+
migrated[key] = legacyDream[key];
|
|
68
|
+
const sessions = mergeSection(defaults.sessions, migrated);
|
|
69
|
+
for (const key of [
|
|
70
|
+
"timeoutMs",
|
|
71
|
+
"maxDurationMs",
|
|
72
|
+
"maxSessions",
|
|
73
|
+
"maxChars",
|
|
74
|
+
"retries",
|
|
75
|
+
"idleMs",
|
|
76
|
+
])
|
|
77
|
+
if (!Number.isSafeInteger(sessions[key]) || sessions[key] < 0)
|
|
78
|
+
throw new Error(`invalid sessions.${key}`);
|
|
79
|
+
for (const key of ["timeoutMs", "maxDurationMs", "retries"])
|
|
80
|
+
if (!Number.isSafeInteger(dream[key]) || dream[key] < 0)
|
|
81
|
+
throw new Error(`invalid dream.${key}`);
|
|
82
|
+
if (sessions.timeoutMs < 1 ||
|
|
83
|
+
sessions.maxChars < 256 ||
|
|
84
|
+
sessions.maxChars > 200000 ||
|
|
85
|
+
sessions.retries > 5)
|
|
86
|
+
throw new Error("invalid sessions request limits");
|
|
87
|
+
if (dream.timeoutMs < 1 || dream.retries > 5)
|
|
88
|
+
throw new Error("invalid dream request limits");
|
|
89
|
+
for (const [section, value] of [
|
|
90
|
+
["sessions", sessions],
|
|
91
|
+
["dream", dream],
|
|
92
|
+
])
|
|
93
|
+
if (value.model !== null &&
|
|
94
|
+
(typeof value.model !== "string" || !/^[^/]+\/.+/.test(value.model)))
|
|
95
|
+
throw new Error(`${section}.model must use provider/model format`);
|
|
96
|
+
if (sessions.variant !== null && typeof sessions.variant !== "string")
|
|
97
|
+
throw new Error("sessions.variant must be a string or null");
|
|
98
|
+
if (dream.variant !== null && typeof dream.variant !== "string")
|
|
99
|
+
throw new Error("dream.variant must be a string or null");
|
|
100
|
+
if (sessions.opencodeUrl !== null && typeof sessions.opencodeUrl !== "string")
|
|
101
|
+
throw new Error("sessions.opencodeUrl must be a string or null");
|
|
102
|
+
if (!Number.isInteger(search.maxResults) ||
|
|
103
|
+
search.maxResults < 1 ||
|
|
104
|
+
search.maxResults > 200 ||
|
|
105
|
+
!Number.isFinite(search.halfLifeDays) ||
|
|
106
|
+
search.halfLifeDays <= 0 ||
|
|
107
|
+
![search.minScore, search.minSemanticScore].every((value) => Number.isFinite(value) && value >= 0 && value <= 1))
|
|
108
|
+
throw new Error("invalid search settings");
|
|
109
|
+
return {
|
|
110
|
+
embedding: embedding && typeof embedding.url === "string" && typeof embedding.model === "string"
|
|
111
|
+
? {
|
|
112
|
+
url: embedding.url,
|
|
113
|
+
model: embedding.model,
|
|
114
|
+
...(typeof embedding.queryInstruction === "string"
|
|
115
|
+
? { queryInstruction: embedding.queryInstruction }
|
|
116
|
+
: {}),
|
|
117
|
+
}
|
|
118
|
+
: null,
|
|
119
|
+
sessions,
|
|
120
|
+
dream,
|
|
121
|
+
search,
|
|
122
|
+
archive: mergeSection(defaults.archive, source.archive),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export async function loadSettings(settingsFile) {
|
|
126
|
+
try {
|
|
127
|
+
return normalizeSettings(JSON.parse(await readFile(settingsFile, "utf8")));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error.code === "ENOENT")
|
|
131
|
+
return defaultSettings();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export async function embedTexts(settings, texts, purpose = "document", signal) {
|
|
136
|
+
if (!settings.embedding || !texts.length)
|
|
137
|
+
return null;
|
|
138
|
+
const response = await fetch(settings.embedding.url, {
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
input: texts.map((text) => (purpose === "query" ? embeddingQuery(settings, text) : text)),
|
|
141
|
+
model: settings.embedding.model,
|
|
142
|
+
}),
|
|
143
|
+
headers: { "content-type": "application/json" },
|
|
144
|
+
method: "POST",
|
|
145
|
+
signal: signal
|
|
146
|
+
? AbortSignal.any([signal, AbortSignal.timeout(30_000)])
|
|
147
|
+
: AbortSignal.timeout(30_000),
|
|
148
|
+
});
|
|
149
|
+
if (!response.ok)
|
|
150
|
+
throw new Error(`embedding request failed: ${response.status} ${await response.text()}`);
|
|
151
|
+
const payload = (await response.json());
|
|
152
|
+
const list = payload.data ?? [];
|
|
153
|
+
if (list.length !== texts.length)
|
|
154
|
+
throw new Error("embedding response is incomplete");
|
|
155
|
+
const byIndex = new Map();
|
|
156
|
+
for (const item of list) {
|
|
157
|
+
if (!Array.isArray(item.embedding) ||
|
|
158
|
+
!item.embedding.length ||
|
|
159
|
+
!item.embedding.every((value) => typeof value === "number" && Number.isFinite(value)) ||
|
|
160
|
+
!item.embedding.some((value) => value !== 0))
|
|
161
|
+
throw new Error("embedding response is malformed");
|
|
162
|
+
const index = item.index ?? byIndex.size;
|
|
163
|
+
if (!Number.isInteger(index) || index < 0 || index >= texts.length || byIndex.has(index))
|
|
164
|
+
throw new Error("embedding response has invalid indices");
|
|
165
|
+
byIndex.set(index, item.embedding);
|
|
166
|
+
}
|
|
167
|
+
const vectors = texts.map((_, index) => Float32Array.from(byIndex.get(index)));
|
|
168
|
+
if (!vectors.every((vector) => vector.length === vectors[0].length && vector.every(Number.isFinite)))
|
|
169
|
+
throw new Error("embedding response has incompatible vectors");
|
|
170
|
+
return vectors;
|
|
171
|
+
}
|
|
172
|
+
export const MEMORY_QUERY_INSTRUCTION = "Given a search query, retrieve relevant personal memory entries that answer the query.";
|
|
173
|
+
export function embeddingQuery(settings, query) {
|
|
174
|
+
const embedding = settings.embedding;
|
|
175
|
+
const instruction = embedding?.queryInstruction ??
|
|
176
|
+
(/qwen3[-_]?embedding/i.test(embedding?.model ?? "") ? MEMORY_QUERY_INSTRUCTION : "");
|
|
177
|
+
return instruction ? `Instruct: ${instruction}\nQuery:${query}` : query;
|
|
178
|
+
}
|
|
179
|
+
export function embeddingFingerprint(settings) {
|
|
180
|
+
return createHash("sha256")
|
|
181
|
+
.update(JSON.stringify(settings.embedding
|
|
182
|
+
? {
|
|
183
|
+
url: settings.embedding.url,
|
|
184
|
+
model: settings.embedding.model,
|
|
185
|
+
documentFormat: "raw-v1",
|
|
186
|
+
}
|
|
187
|
+
: null))
|
|
188
|
+
.digest("hex");
|
|
189
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export type EntryKind = "curated" | "user" | "episodic";
|
|
3
|
+
export type IndexedEntry = {
|
|
4
|
+
stableId: string;
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
kind: EntryKind;
|
|
8
|
+
key: string | null;
|
|
9
|
+
text: string;
|
|
10
|
+
trigger: string[];
|
|
11
|
+
importance: number;
|
|
12
|
+
pinned: boolean;
|
|
13
|
+
project: string | null;
|
|
14
|
+
origin: "user" | "agent" | null;
|
|
15
|
+
observedAt: number | null;
|
|
16
|
+
status: "active" | null;
|
|
17
|
+
source: string | null;
|
|
18
|
+
};
|
|
19
|
+
export type VectorRow = {
|
|
20
|
+
stableId: string;
|
|
21
|
+
dim: number;
|
|
22
|
+
data: Float32Array;
|
|
23
|
+
};
|
|
24
|
+
export declare function stableIdFor(file: string, text: string, key: string | null): string;
|
|
25
|
+
export declare class MemoryStore {
|
|
26
|
+
readonly db: DatabaseSync;
|
|
27
|
+
private readonly fts;
|
|
28
|
+
private constructor();
|
|
29
|
+
private migrate;
|
|
30
|
+
static open(indexFile: string): Promise<MemoryStore>;
|
|
31
|
+
private static copy;
|
|
32
|
+
fork(): MemoryStore;
|
|
33
|
+
static preview(indexFile: string): Promise<MemoryStore>;
|
|
34
|
+
private ensureFts;
|
|
35
|
+
hasFts(): boolean;
|
|
36
|
+
replaceEntries(entries: IndexedEntry[], manageTransaction?: boolean): void;
|
|
37
|
+
replaceIndex(entries: IndexedEntry[], vectors: Float32Array[] | null, fingerprint: string): void;
|
|
38
|
+
allEntries(): IndexedEntry[];
|
|
39
|
+
ftsSearch(query: string, limit: number): Map<string, number>;
|
|
40
|
+
markSurfaced(stableIds: string[], query: string, now?: number): void;
|
|
41
|
+
markUseful(stableIds: string[], now?: number): void;
|
|
42
|
+
usageFor(stableId: string): {
|
|
43
|
+
surfaced: number;
|
|
44
|
+
useful: number;
|
|
45
|
+
queries: string[];
|
|
46
|
+
days: string[];
|
|
47
|
+
lastUsefulAt: number | null;
|
|
48
|
+
};
|
|
49
|
+
setVector(stableId: string, vector: Float32Array): void;
|
|
50
|
+
vectors(): VectorRow[];
|
|
51
|
+
getMeta(key: string): string | null;
|
|
52
|
+
setMeta(key: string, value: string): void;
|
|
53
|
+
close(): void;
|
|
54
|
+
}
|
|
55
|
+
export declare function cosineSimilarity(left: Float32Array, right: Float32Array): number;
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
export function stableIdFor(file, text, key) {
|
|
6
|
+
if (key)
|
|
7
|
+
return `key:${key}`;
|
|
8
|
+
return `sha:${createHash("sha256").update(`${file}\n${text}`).digest("hex").slice(0, 24)}`;
|
|
9
|
+
}
|
|
10
|
+
const SCHEMA = `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS entries (
|
|
12
|
+
stable_id TEXT PRIMARY KEY,
|
|
13
|
+
file TEXT NOT NULL,
|
|
14
|
+
line INTEGER NOT NULL,
|
|
15
|
+
kind TEXT NOT NULL,
|
|
16
|
+
key TEXT,
|
|
17
|
+
text TEXT NOT NULL,
|
|
18
|
+
trigger_phrases TEXT NOT NULL DEFAULT '[]',
|
|
19
|
+
importance INTEGER NOT NULL DEFAULT 5,
|
|
20
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
project TEXT,
|
|
22
|
+
origin TEXT,
|
|
23
|
+
observed_at INTEGER,
|
|
24
|
+
status TEXT
|
|
25
|
+
);
|
|
26
|
+
CREATE TABLE IF NOT EXISTS usage (
|
|
27
|
+
stable_id TEXT PRIMARY KEY,
|
|
28
|
+
surfaced INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
useful INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
queries TEXT NOT NULL DEFAULT '[]',
|
|
31
|
+
days TEXT NOT NULL DEFAULT '[]',
|
|
32
|
+
last_useful_at INTEGER
|
|
33
|
+
);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS vectors (
|
|
35
|
+
stable_id TEXT PRIMARY KEY,
|
|
36
|
+
dim INTEGER NOT NULL,
|
|
37
|
+
data BLOB NOT NULL
|
|
38
|
+
);
|
|
39
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
40
|
+
key TEXT PRIMARY KEY,
|
|
41
|
+
value TEXT NOT NULL
|
|
42
|
+
);
|
|
43
|
+
`;
|
|
44
|
+
export class MemoryStore {
|
|
45
|
+
db;
|
|
46
|
+
fts;
|
|
47
|
+
constructor(indexFile) {
|
|
48
|
+
this.db = new DatabaseSync(indexFile);
|
|
49
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
50
|
+
this.db.exec(SCHEMA);
|
|
51
|
+
this.migrate();
|
|
52
|
+
this.fts = this.ensureFts();
|
|
53
|
+
}
|
|
54
|
+
migrate() {
|
|
55
|
+
const columns = this.db.prepare("PRAGMA table_info(entries);").all();
|
|
56
|
+
if (!columns.some((column) => column.name === "source"))
|
|
57
|
+
this.db.exec("ALTER TABLE entries ADD COLUMN source TEXT;");
|
|
58
|
+
}
|
|
59
|
+
static async open(indexFile) {
|
|
60
|
+
await mkdir(dirname(indexFile), { mode: 0o700, recursive: true });
|
|
61
|
+
return new MemoryStore(indexFile);
|
|
62
|
+
}
|
|
63
|
+
static copy(database) {
|
|
64
|
+
const copy = new MemoryStore(":memory:");
|
|
65
|
+
try {
|
|
66
|
+
for (const table of ["entries", "usage", "vectors", "meta"]) {
|
|
67
|
+
for (const row of database.prepare(`SELECT * FROM ${table}`).all()) {
|
|
68
|
+
const columns = Object.keys(row);
|
|
69
|
+
copy.db
|
|
70
|
+
.prepare(`INSERT INTO ${table} (${columns.join(",")}) VALUES (${columns.map(() => "?").join(",")})`)
|
|
71
|
+
.run(...Object.values(row));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (copy.fts)
|
|
75
|
+
copy.db.exec("INSERT INTO entries_fts(stable_id,text) SELECT stable_id,text FROM entries");
|
|
76
|
+
return copy;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
copy.close();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
fork() {
|
|
84
|
+
return MemoryStore.copy(this.db);
|
|
85
|
+
}
|
|
86
|
+
static async preview(indexFile) {
|
|
87
|
+
try {
|
|
88
|
+
await stat(indexFile);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error.code === "ENOENT")
|
|
92
|
+
return new MemoryStore(":memory:");
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const database = new DatabaseSync(indexFile, { readOnly: true });
|
|
96
|
+
try {
|
|
97
|
+
return MemoryStore.copy(database);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
database.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
ensureFts() {
|
|
104
|
+
try {
|
|
105
|
+
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(stable_id UNINDEXED, text);");
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
hasFts() {
|
|
113
|
+
return this.fts;
|
|
114
|
+
}
|
|
115
|
+
replaceEntries(entries, manageTransaction = true) {
|
|
116
|
+
if (manageTransaction)
|
|
117
|
+
this.db.exec("BEGIN IMMEDIATE;");
|
|
118
|
+
try {
|
|
119
|
+
this.db.exec("DELETE FROM entries;");
|
|
120
|
+
if (this.fts)
|
|
121
|
+
this.db.exec("DELETE FROM entries_fts;");
|
|
122
|
+
const seen = new Set();
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (seen.has(entry.stableId))
|
|
125
|
+
continue;
|
|
126
|
+
seen.add(entry.stableId);
|
|
127
|
+
this.db
|
|
128
|
+
.prepare(`INSERT INTO entries (stable_id, file, line, kind, key, text, trigger_phrases, importance, pinned, project, origin, observed_at, status, source)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
130
|
+
.run(entry.stableId, entry.file, entry.line, entry.kind, entry.key, entry.text, JSON.stringify(entry.trigger), entry.importance, entry.pinned ? 1 : 0, entry.project, entry.origin, entry.observedAt, entry.status, entry.source ?? null);
|
|
131
|
+
if (this.fts)
|
|
132
|
+
this.db
|
|
133
|
+
.prepare("INSERT INTO entries_fts (stable_id, text) VALUES (?, ?);")
|
|
134
|
+
.run(entry.stableId, entry.text);
|
|
135
|
+
}
|
|
136
|
+
this.db.exec("DELETE FROM usage WHERE stable_id NOT IN (SELECT stable_id FROM entries);");
|
|
137
|
+
this.db.exec("DELETE FROM vectors WHERE stable_id NOT IN (SELECT stable_id FROM entries);");
|
|
138
|
+
if (manageTransaction)
|
|
139
|
+
this.db.exec("COMMIT;");
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (manageTransaction)
|
|
143
|
+
this.db.exec("ROLLBACK;");
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
replaceIndex(entries, vectors, fingerprint) {
|
|
148
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
149
|
+
try {
|
|
150
|
+
this.replaceEntries(entries, false);
|
|
151
|
+
this.db.exec("DELETE FROM vectors");
|
|
152
|
+
if (vectors)
|
|
153
|
+
entries.forEach((entry, index) => this.setVector(entry.stableId, vectors[index]));
|
|
154
|
+
this.setMeta("embeddingFingerprint", fingerprint);
|
|
155
|
+
this.db.exec("COMMIT");
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.db.exec("ROLLBACK");
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
allEntries() {
|
|
163
|
+
return this.db
|
|
164
|
+
.prepare("SELECT stable_id, file, line, kind, key, text, trigger_phrases, importance, pinned, project, origin, observed_at, status, source FROM entries ORDER BY observed_at ASC")
|
|
165
|
+
.all()
|
|
166
|
+
.map((row) => {
|
|
167
|
+
const value = row;
|
|
168
|
+
return {
|
|
169
|
+
stableId: value.stable_id,
|
|
170
|
+
file: value.file,
|
|
171
|
+
line: value.line,
|
|
172
|
+
kind: value.kind,
|
|
173
|
+
key: value.key ?? null,
|
|
174
|
+
text: value.text,
|
|
175
|
+
trigger: JSON.parse(value.trigger_phrases),
|
|
176
|
+
importance: value.importance,
|
|
177
|
+
pinned: value.pinned === 1,
|
|
178
|
+
project: value.project ?? null,
|
|
179
|
+
origin: value.origin ?? null,
|
|
180
|
+
observedAt: value.observed_at ?? null,
|
|
181
|
+
status: value.status ?? null,
|
|
182
|
+
source: value.source ?? null,
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
ftsSearch(query, limit) {
|
|
187
|
+
if (!this.fts)
|
|
188
|
+
return new Map();
|
|
189
|
+
const tokens = query
|
|
190
|
+
.toLowerCase()
|
|
191
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
192
|
+
.filter((token) => token.length >= 2)
|
|
193
|
+
.slice(0, 12);
|
|
194
|
+
if (!tokens.length)
|
|
195
|
+
return new Map();
|
|
196
|
+
const pattern = tokens.map((token) => `"${token.replace(/"/g, "")}"*`).join(" OR ");
|
|
197
|
+
const rows = this.db
|
|
198
|
+
.prepare(`SELECT stable_id, rank FROM entries_fts WHERE entries_fts MATCH ? ORDER BY rank LIMIT ?;`)
|
|
199
|
+
.all(pattern, limit);
|
|
200
|
+
const result = new Map();
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
const quality = -row.rank;
|
|
203
|
+
const score = quality <= 0 ? 0.1 : Math.min(1, 0.5 + quality * 0.25);
|
|
204
|
+
result.set(row.stable_id, score);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
markSurfaced(stableIds, query, now = Date.now()) {
|
|
209
|
+
const queryHash = createHash("sha256").update(query.toLowerCase()).digest("hex").slice(0, 16);
|
|
210
|
+
const day = new Date(now).toISOString().slice(0, 10);
|
|
211
|
+
const read = this.db.prepare("SELECT queries, days FROM usage WHERE stable_id = ?;");
|
|
212
|
+
const upsert = this.db.prepare(`INSERT INTO usage (stable_id, surfaced, useful, queries, days) VALUES (?, 1, 0, ?, ?)
|
|
213
|
+
ON CONFLICT(stable_id) DO UPDATE SET
|
|
214
|
+
surfaced = usage.surfaced + 1,
|
|
215
|
+
queries = excluded.queries,
|
|
216
|
+
days = excluded.days;`);
|
|
217
|
+
for (const stableId of stableIds) {
|
|
218
|
+
const row = read.get(stableId);
|
|
219
|
+
const queries = row ? JSON.parse(row.queries) : [];
|
|
220
|
+
const days = row ? JSON.parse(row.days) : [];
|
|
221
|
+
if (!queries.includes(queryHash) && queries.length < 32)
|
|
222
|
+
queries.push(queryHash);
|
|
223
|
+
if (!days.includes(day))
|
|
224
|
+
days.push(day);
|
|
225
|
+
upsert.run(stableId, JSON.stringify(queries), JSON.stringify(days));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
markUseful(stableIds, now = Date.now()) {
|
|
229
|
+
const statement = this.db.prepare(`INSERT INTO usage (stable_id, useful, last_useful_at) VALUES (?, 1, ?)
|
|
230
|
+
ON CONFLICT(stable_id) DO UPDATE SET useful = useful + 1, last_useful_at = ?;`);
|
|
231
|
+
for (const stableId of stableIds)
|
|
232
|
+
statement.run(stableId, now, now);
|
|
233
|
+
}
|
|
234
|
+
usageFor(stableId) {
|
|
235
|
+
const row = this.db.prepare("SELECT * FROM usage WHERE stable_id = ?").get(stableId);
|
|
236
|
+
if (!row)
|
|
237
|
+
return { surfaced: 0, useful: 0, queries: [], days: [], lastUsefulAt: null };
|
|
238
|
+
return {
|
|
239
|
+
surfaced: row.surfaced,
|
|
240
|
+
useful: row.useful,
|
|
241
|
+
queries: JSON.parse(row.queries),
|
|
242
|
+
days: JSON.parse(row.days),
|
|
243
|
+
lastUsefulAt: row.last_useful_at ?? null,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
setVector(stableId, vector) {
|
|
247
|
+
this.db
|
|
248
|
+
.prepare(`INSERT INTO vectors (stable_id, dim, data) VALUES (?, ?, ?)
|
|
249
|
+
ON CONFLICT(stable_id) DO UPDATE SET dim = excluded.dim, data = excluded.data;`)
|
|
250
|
+
.run(stableId, vector.length, Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength));
|
|
251
|
+
}
|
|
252
|
+
vectors() {
|
|
253
|
+
return this.db.prepare("SELECT stable_id, dim, data FROM vectors").all().map((row) => ({
|
|
254
|
+
stableId: row.stable_id,
|
|
255
|
+
dim: row.dim,
|
|
256
|
+
data: new Float32Array(row.data.buffer.slice(row.data.byteOffset, row.data.byteOffset + row.data.byteLength)),
|
|
257
|
+
}));
|
|
258
|
+
}
|
|
259
|
+
getMeta(key) {
|
|
260
|
+
const row = this.db.prepare("SELECT value FROM meta WHERE key = ?").get(key);
|
|
261
|
+
return row?.value ?? null;
|
|
262
|
+
}
|
|
263
|
+
setMeta(key, value) {
|
|
264
|
+
this.db
|
|
265
|
+
.prepare(`INSERT INTO meta (key, value) VALUES (?, ?)
|
|
266
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value;`)
|
|
267
|
+
.run(key, value);
|
|
268
|
+
}
|
|
269
|
+
close() {
|
|
270
|
+
this.db.close();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function cosineSimilarity(left, right) {
|
|
274
|
+
const length = Math.min(left.length, right.length);
|
|
275
|
+
let dot = 0;
|
|
276
|
+
let leftNorm = 0;
|
|
277
|
+
let rightNorm = 0;
|
|
278
|
+
for (let index = 0; index < length; index += 1) {
|
|
279
|
+
dot += left[index] * right[index];
|
|
280
|
+
leftNorm += left[index] * left[index];
|
|
281
|
+
rightNorm += right[index] * right[index];
|
|
282
|
+
}
|
|
283
|
+
if (!leftNorm || !rightNorm)
|
|
284
|
+
return 0;
|
|
285
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
286
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type EntryVisibility = "team" | "personal";
|
|
2
|
+
/**
|
|
3
|
+
* Usage-level visibility derived from the entry source. Team-prefixed skills,
|
|
4
|
+
* the GitLab collector, and spec-manage produce entries citable in
|
|
5
|
+
* team-facing artifacts; every other source stays personal-only.
|
|
6
|
+
*/
|
|
7
|
+
export declare function visibilityForSource(source: string | null | undefined): EntryVisibility;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const TEAM_CITABLE_SOURCES = ["gitlab", "spec-manage"];
|
|
2
|
+
/**
|
|
3
|
+
* Usage-level visibility derived from the entry source. Team-prefixed skills,
|
|
4
|
+
* the GitLab collector, and spec-manage produce entries citable in
|
|
5
|
+
* team-facing artifacts; every other source stays personal-only.
|
|
6
|
+
*/
|
|
7
|
+
export function visibilityForSource(source) {
|
|
8
|
+
if (!source)
|
|
9
|
+
return "personal";
|
|
10
|
+
if (TEAM_CITABLE_SOURCES.includes(source) || source.startsWith("team-"))
|
|
11
|
+
return "team";
|
|
12
|
+
return "personal";
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kisev/memomatic",
|
|
3
|
+
"version": "1.0.0-dev.100.g21366c4ad740",
|
|
4
|
+
"description": "Personal learning memory for agents: tiered Markdown corpus, rebuildable index, dream sweep.",
|
|
5
|
+
"homepage": "https://github.com/kisev/skills#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/kisev/skills/issues"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/kisev/skills.git",
|
|
13
|
+
"directory": "apps/memomatic"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"memomatic": "./dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"assets",
|
|
21
|
+
"README.md",
|
|
22
|
+
"README.ru.md"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node scripts/clean-dist.mjs && tsc --project tsconfig.json",
|
|
33
|
+
"test": "node --test test/*.test.mjs",
|
|
34
|
+
"pack:check": "node test/pack-allowlist.mjs"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@kisev/safe-fs": "1.0.0-dev.100.g21366c4ad740",
|
|
38
|
+
"commander": "15.0.0",
|
|
39
|
+
"jsonc-parser": "3.3.1"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "22.15.30",
|
|
43
|
+
"typescript": "7.0.2"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=22.13"
|
|
47
|
+
}
|
|
48
|
+
}
|