@kisev/memomatic 1.0.0-dev.59.g0506803c61e5 → 1.0.0-dev.61.gdf462654550b
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 +7 -3
- package/README.ru.md +7 -3
- package/dist/cli.js +5 -4
- package/dist/dream.js +33 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp.js +2 -12
- package/dist/service.d.ts +12 -1
- package/dist/service.js +60 -37
- package/dist/store.d.ts +3 -0
- package/dist/store.js +39 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,13 +10,13 @@ rebuildable SQLite index with FTS5 and optional local embeddings, and a nightly
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install @kisev/memomatic
|
|
13
|
+
npm install --global @kisev/memomatic
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
The corpus lives under `$XDG_STATE_HOME/memomatic/` (`MEMORY.md`, `USER.md`,
|
|
17
17
|
daily notes, `DREAMS.md`); rules live in `$XDG_CONFIG_HOME/memomatic/MEMORY_RULES.md`.
|
|
18
|
-
|
|
19
|
-
standalone use
|
|
18
|
+
Requires Node.js 22+. Explicitly select `memomatic` in the `@kisev/agentomatic`
|
|
19
|
+
installer and restart OpenCode. For standalone use, run the CLI:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
memomatic process # validate the inbox and index accepted entries (no model)
|
|
@@ -26,6 +26,10 @@ memomatic dream # full sweep: inbox + sessions + consolidation
|
|
|
26
26
|
The nightly sweep is scheduled by the systemd user units in `assets/systemd/`
|
|
27
27
|
(`memomatic-dream.service` and `memomatic-dream.timer`).
|
|
28
28
|
|
|
29
|
+
Configure `dream.model` and an OpenCode provider before expecting session
|
|
30
|
+
extraction; without a model, the session watermark is preserved. Follow the
|
|
31
|
+
[setup, first-entry, and scheduling guide](../../docs/how-to/memomatic.md).
|
|
32
|
+
|
|
29
33
|
## Inbox
|
|
30
34
|
|
|
31
35
|
All writes are asynchronous. Skills, agents, and the `memory_write` tool
|
package/README.ru.md
CHANGED
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
## Установка
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install @kisev/memomatic
|
|
13
|
+
npm install --global @kisev/memomatic
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
Корпус живёт в `$XDG_STATE_HOME/memomatic/` (`MEMORY.md`, `USER.md`, ежедневные
|
|
17
17
|
записи, `DREAMS.md`); правила — в `$XDG_CONFIG_HOME/memomatic/MEMORY_RULES.md`.
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
Нужен Node.js 22+. Явно выберите `memomatic` в установщике `@kisev/agentomatic`
|
|
19
|
+
и перезапустите OpenCode. Автономное использование доступно через CLI:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
memomatic process # валидация inbox и индексация принятого (без модели)
|
|
@@ -26,6 +26,10 @@ memomatic dream # полный проход: inbox + сессии + кон
|
|
|
26
26
|
Ночной проход планируют user-юниты systemd из `assets/systemd/`
|
|
27
27
|
(`memomatic-dream.service` и `memomatic-dream.timer`).
|
|
28
28
|
|
|
29
|
+
Перед извлечением сессий настройте `dream.model` и провайдера OpenCode;
|
|
30
|
+
без модели отметка обработанных сессий сохраняется. См.
|
|
31
|
+
[настройку, первую запись и расписание](../../docs/ru/how-to/memomatic.md).
|
|
32
|
+
|
|
29
33
|
## Inbox
|
|
30
34
|
|
|
31
35
|
Все записи асинхронны. Скиллы, агенты и инструмент `memory_write` кладут
|
package/dist/cli.js
CHANGED
|
@@ -41,11 +41,11 @@ async function main() {
|
|
|
41
41
|
process.stdout.write(`${version}\n`);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
-
const
|
|
44
|
+
const dryRun = (command === "dream" || command === "process") && rest.includes("--dry-run");
|
|
45
|
+
const context = await openMemomatic({ readOnly: dryRun });
|
|
45
46
|
try {
|
|
46
47
|
if (command === "dream" || command === "process") {
|
|
47
|
-
const
|
|
48
|
-
const result = await withRunLock(context.paths, async () => {
|
|
48
|
+
const execute = async () => {
|
|
49
49
|
if (command === "process")
|
|
50
50
|
return processInbox(context, { dryRun });
|
|
51
51
|
const executor = context.settings.dream.model
|
|
@@ -55,7 +55,8 @@ async function main() {
|
|
|
55
55
|
})
|
|
56
56
|
: null;
|
|
57
57
|
return runDream(context, executor, { dryRun });
|
|
58
|
-
}
|
|
58
|
+
};
|
|
59
|
+
const result = dryRun ? await execute() : await withRunLock(context.paths, execute);
|
|
59
60
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
60
61
|
return;
|
|
61
62
|
}
|
package/dist/dream.js
CHANGED
|
@@ -71,6 +71,8 @@ export function parseConsolidation(response) {
|
|
|
71
71
|
return result.slice(0, 32);
|
|
72
72
|
}
|
|
73
73
|
async function ingestSessions(context, executor) {
|
|
74
|
+
if (!executor)
|
|
75
|
+
return { sessions: 0, extracted: [], watermark: null, rejected: [] };
|
|
74
76
|
const watermark = Number.parseInt(context.store.getMeta(WATERMARK_KEY) ?? "0", 10) || 0;
|
|
75
77
|
const sessions = loadRecentSessions(opencodeDatabasePath(), watermark, {
|
|
76
78
|
before: Date.now() - 10 * 60_000,
|
|
@@ -82,8 +84,6 @@ async function ingestSessions(context, executor) {
|
|
|
82
84
|
let latest = watermark;
|
|
83
85
|
for (const session of sessions) {
|
|
84
86
|
latest = Math.max(latest, session.timeCreated);
|
|
85
|
-
if (!executor)
|
|
86
|
-
continue;
|
|
87
87
|
const transcript = session.messages
|
|
88
88
|
.map((message) => `${message.role}: ${message.text}`)
|
|
89
89
|
.join("\n\n")
|
|
@@ -108,6 +108,17 @@ async function ingestSessions(context, executor) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
export async function runDream(context, executor, options = {}) {
|
|
111
|
+
if (!options.dryRun)
|
|
112
|
+
return executeDream(context, executor, options);
|
|
113
|
+
const store = context.store.fork();
|
|
114
|
+
try {
|
|
115
|
+
return await executeDream({ ...context, store }, executor, options);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
store.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function executeDream(context, executor, options) {
|
|
111
122
|
const dryRun = options.dryRun === true;
|
|
112
123
|
const report = {
|
|
113
124
|
inbox: {
|
|
@@ -168,12 +179,27 @@ export async function runDream(context, executor, options = {}) {
|
|
|
168
179
|
.map((candidate) => `- ${candidate.entry.text} <!-- key: ${candidate.entry.key ?? candidate.entry.stableId} -->`)
|
|
169
180
|
.join("\n")}`,
|
|
170
181
|
});
|
|
171
|
-
|
|
182
|
+
let operations;
|
|
183
|
+
let invalidResponse = false;
|
|
184
|
+
try {
|
|
185
|
+
operations = parseConsolidation(response);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
invalidResponse = true;
|
|
189
|
+
operations = finalCandidates.map(({ entry }) => ({
|
|
190
|
+
op: "add",
|
|
191
|
+
line: entryLine(entry.text.replace(/^- /, ""), {
|
|
192
|
+
key: entry.key ?? undefined,
|
|
193
|
+
origin: entry.origin ?? "agent",
|
|
194
|
+
source: entry.source ?? undefined,
|
|
195
|
+
}),
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
172
198
|
const outcome = await applyConsolidation(context, operations, current, dryRun);
|
|
173
199
|
report.promoted = outcome.promoted;
|
|
174
200
|
report.superseded = outcome.superseded;
|
|
175
201
|
report.dropped = outcome.dropped;
|
|
176
|
-
report.appendOnlyFallback = outcome.appendOnlyFallback;
|
|
202
|
+
report.appendOnlyFallback = invalidResponse || outcome.appendOnlyFallback;
|
|
177
203
|
}
|
|
178
204
|
if (!dryRun)
|
|
179
205
|
report.archived = await archiveOldEpisodic(context);
|
|
@@ -215,6 +241,9 @@ async function applyConsolidation(context, operations, currentMemory, dryRun) {
|
|
|
215
241
|
const adds = operations.filter((operation) => operation.op === "add");
|
|
216
242
|
const supersessions = operations.filter((operation) => operation.op === "supersede" && existingKeys.has(operation.key));
|
|
217
243
|
const drops = operations.filter((operation) => operation.op === "drop" && existingKeys.has(operation.key));
|
|
244
|
+
// Consolidation never grants permission to delete curated memory.
|
|
245
|
+
if (drops.length)
|
|
246
|
+
result.appendOnlyFallback = true;
|
|
218
247
|
if (removals.length !== supersessions.length + drops.length)
|
|
219
248
|
result.appendOnlyFallback = true;
|
|
220
249
|
if (!result.appendOnlyFallback && lossRatio > context.settings.dream.maxPriorEntryLoss)
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { writeCorpusFile } from "./corpus.js";
|
|
|
2
2
|
export { entryLine, parseEntryLine } from "./entries.js";
|
|
3
3
|
export { visibilityForSource } from "./visibility.js";
|
|
4
4
|
export { dropToInbox, processInbox, withRunLock } from "./inbox.js";
|
|
5
|
-
export { forgetEntry, getEntry, openMemomatic, searchMemory, writeEntry } from "./service.js";
|
|
5
|
+
export { forgetEntry, getEntry, openMemomatic, searchMemory, searchMemoryResults, writeEntry, } from "./service.js";
|
|
6
6
|
export type { MemomaticContext } from "./service.js";
|
|
7
7
|
export { bootstrapContext, resolveProject } from "./bootstrap.js";
|
|
8
8
|
export { opencodeDatabasePath, sessionFacts } from "./ingest.js";
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,6 @@ export { writeCorpusFile } from "./corpus.js";
|
|
|
2
2
|
export { entryLine, parseEntryLine } from "./entries.js";
|
|
3
3
|
export { visibilityForSource } from "./visibility.js";
|
|
4
4
|
export { dropToInbox, processInbox, withRunLock } from "./inbox.js";
|
|
5
|
-
export { forgetEntry, getEntry, openMemomatic, searchMemory, writeEntry } from "./service.js";
|
|
5
|
+
export { forgetEntry, getEntry, openMemomatic, searchMemory, searchMemoryResults, writeEntry, } from "./service.js";
|
|
6
6
|
export { bootstrapContext, resolveProject } from "./bootstrap.js";
|
|
7
7
|
export { opencodeDatabasePath, sessionFacts } from "./ingest.js";
|
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { forgetEntry, getEntry, openMemomatic,
|
|
2
|
-
import { visibilityForSource } from "./visibility.js";
|
|
1
|
+
import { forgetEntry, getEntry, openMemomatic, searchMemoryResults, writeEntry, } from "./service.js";
|
|
3
2
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
4
3
|
const TOOLS = [
|
|
5
4
|
{
|
|
@@ -69,16 +68,7 @@ async function callTool(name, args) {
|
|
|
69
68
|
case "memory_search": {
|
|
70
69
|
if (typeof args.query !== "string" || !args.query.trim())
|
|
71
70
|
throw new Error("query is required");
|
|
72
|
-
|
|
73
|
-
return hits.map((hit) => ({
|
|
74
|
-
file: hit.entry.file.replace(`${context.paths.stateRoot}/`, ""),
|
|
75
|
-
kind: hit.entry.kind,
|
|
76
|
-
line: hit.entry.line,
|
|
77
|
-
score: Number(hit.score.toFixed(4)),
|
|
78
|
-
snippet: hit.snippet,
|
|
79
|
-
source: hit.entry.source,
|
|
80
|
-
visibility: visibilityForSource(hit.entry.source),
|
|
81
|
-
}));
|
|
71
|
+
return await searchMemoryResults(context, args.query);
|
|
82
72
|
}
|
|
83
73
|
case "memory_get": {
|
|
84
74
|
if (typeof args.file !== "string" || !args.file)
|
package/dist/service.d.ts
CHANGED
|
@@ -20,9 +20,20 @@ export type MemomaticContext = {
|
|
|
20
20
|
rules: MemoryRules;
|
|
21
21
|
store: MemoryStore;
|
|
22
22
|
};
|
|
23
|
-
export declare function openMemomatic(
|
|
23
|
+
export declare function openMemomatic(options?: {
|
|
24
|
+
readOnly?: boolean;
|
|
25
|
+
}): Promise<MemomaticContext>;
|
|
24
26
|
export declare function writeEntry(context: MemomaticContext, request: WriteRequest): Promise<string>;
|
|
25
27
|
export declare function searchMemory(context: MemomaticContext, query: string): Promise<SearchHit[]>;
|
|
28
|
+
export declare function searchMemoryResults(context: MemomaticContext, query: string): Promise<{
|
|
29
|
+
file: string;
|
|
30
|
+
kind: import("./store.js").EntryKind;
|
|
31
|
+
line: number;
|
|
32
|
+
score: number;
|
|
33
|
+
snippet: string;
|
|
34
|
+
source: string | null;
|
|
35
|
+
visibility: import("./visibility.js").EntryVisibility;
|
|
36
|
+
}[]>;
|
|
26
37
|
export declare function getEntry(context: MemomaticContext, file: string, line?: number): Promise<{
|
|
27
38
|
file: string;
|
|
28
39
|
line: number;
|
package/dist/service.js
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
|
-
import { entryLine } from "./entries.js";
|
|
2
|
-
import {
|
|
1
|
+
import { entryLine, parseEntryLine } from "./entries.js";
|
|
2
|
+
import { readTextIfExists, replaceEntryLine, writeCorpusFile } from "./corpus.js";
|
|
3
|
+
import { assertSafePath } from "@kisev/safe-fs";
|
|
4
|
+
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { rm } from "node:fs/promises";
|
|
3
6
|
import { dropToInbox } from "./inbox.js";
|
|
4
7
|
import { isForbidden, loadRules } from "./rules.js";
|
|
5
8
|
import { memomaticPaths } from "./paths.js";
|
|
6
9
|
import { loadSettings } from "./settings.js";
|
|
7
10
|
import { reindex, search } from "./search.js";
|
|
8
11
|
import { MemoryStore } from "./store.js";
|
|
9
|
-
import {
|
|
10
|
-
async function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
12
|
+
import { visibilityForSource } from "./visibility.js";
|
|
13
|
+
async function memoryPath(context, file) {
|
|
14
|
+
const target = resolve(context.paths.stateRoot, file);
|
|
15
|
+
const part = relative(context.paths.stateRoot, target);
|
|
16
|
+
if (!part || part === ".." || part.startsWith(`..${sep}`) || isAbsolute(part))
|
|
17
|
+
throw new Error("path escapes memomatic state root");
|
|
18
|
+
await assertSafePath(target, { target: "file" });
|
|
19
|
+
return target;
|
|
18
20
|
}
|
|
19
|
-
export async function openMemomatic() {
|
|
21
|
+
export async function openMemomatic(options = {}) {
|
|
20
22
|
const paths = memomaticPaths();
|
|
21
23
|
const [settings, rules, store] = await Promise.all([
|
|
22
24
|
loadSettings(paths.settingsFile),
|
|
23
25
|
loadRules(paths.rulesFile),
|
|
24
|
-
MemoryStore.open(paths.indexFile),
|
|
26
|
+
options.readOnly ? MemoryStore.preview(paths.indexFile) : MemoryStore.open(paths.indexFile),
|
|
25
27
|
]);
|
|
26
28
|
return { paths, settings, rules, store };
|
|
27
29
|
}
|
|
@@ -49,19 +51,26 @@ export async function writeEntry(context, request) {
|
|
|
49
51
|
export async function searchMemory(context, query) {
|
|
50
52
|
return search(context.store, query, context.settings);
|
|
51
53
|
}
|
|
54
|
+
export async function searchMemoryResults(context, query) {
|
|
55
|
+
return (await searchMemory(context, query)).map((hit) => ({
|
|
56
|
+
file: hit.entry.file.replace(`${context.paths.stateRoot}/`, ""),
|
|
57
|
+
kind: hit.entry.kind,
|
|
58
|
+
line: hit.entry.line,
|
|
59
|
+
score: Number(hit.score.toFixed(4)),
|
|
60
|
+
snippet: hit.snippet,
|
|
61
|
+
source: hit.entry.source,
|
|
62
|
+
visibility: visibilityForSource(hit.entry.source),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
52
65
|
export async function getEntry(context, file, line) {
|
|
53
|
-
const target = file
|
|
54
|
-
? file
|
|
55
|
-
: `${context.paths.stateRoot}/${file.replace(/^\/+/, "")}`;
|
|
56
|
-
if (!target.startsWith(context.paths.stateRoot))
|
|
57
|
-
throw new Error("path escapes memomatic state root");
|
|
66
|
+
const target = await memoryPath(context, file);
|
|
58
67
|
const content = await readTextIfExists(target);
|
|
59
68
|
if (content === undefined)
|
|
60
69
|
throw new Error(`memory file not found: ${file}`);
|
|
61
70
|
if (line === undefined)
|
|
62
71
|
return { file: target, line: 0, content };
|
|
63
72
|
const lines = content.split("\n");
|
|
64
|
-
if (line < 1 || line > lines.length)
|
|
73
|
+
if (!Number.isSafeInteger(line) || line < 1 || line > lines.length)
|
|
65
74
|
throw new Error("line is out of range");
|
|
66
75
|
context.store.markUseful(context.store
|
|
67
76
|
.allEntries()
|
|
@@ -70,16 +79,12 @@ export async function getEntry(context, file, line) {
|
|
|
70
79
|
return { file: target, line, content: lines[line - 1] };
|
|
71
80
|
}
|
|
72
81
|
export async function forgetEntry(context, target) {
|
|
73
|
-
const file = target.file
|
|
74
|
-
? target.file
|
|
75
|
-
: `${context.paths.stateRoot}/${target.file.replace(/^\/+/, "")}`;
|
|
76
|
-
if (!file.startsWith(context.paths.stateRoot))
|
|
77
|
-
throw new Error("path escapes memomatic state root");
|
|
82
|
+
const file = await memoryPath(context, target.file);
|
|
78
83
|
const content = await readTextIfExists(file);
|
|
79
84
|
if (content === undefined)
|
|
80
85
|
throw new Error(`memory file not found: ${target.file}`);
|
|
81
86
|
const lines = content.split("\n");
|
|
82
|
-
if (target.line < 1 || target.line > lines.length)
|
|
87
|
+
if (!Number.isSafeInteger(target.line) || target.line < 1 || target.line > lines.length)
|
|
83
88
|
throw new Error("line is out of range");
|
|
84
89
|
await replaceEntryLine(context.paths, file, target.line, null);
|
|
85
90
|
return file;
|
|
@@ -90,20 +95,38 @@ export async function archiveOldEpisodic(context) {
|
|
|
90
95
|
return archived;
|
|
91
96
|
const rule = context.rules.autoClean;
|
|
92
97
|
const cutoff = Date.now() - rule.olderThanDays * 86_400_000;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
const files = new Set(context.store
|
|
99
|
+
.allEntries()
|
|
100
|
+
.filter((entry) => entry.kind === "episodic")
|
|
101
|
+
.map((entry) => entry.file));
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
await memoryPath(context, file);
|
|
104
|
+
const content = await readTextIfExists(file);
|
|
105
|
+
if (content === undefined)
|
|
99
106
|
continue;
|
|
100
|
-
|
|
107
|
+
const selected = [];
|
|
108
|
+
const remaining = content.split("\n").filter((line) => {
|
|
109
|
+
const entry = parseEntryLine(line)?.annotations;
|
|
110
|
+
const observed = entry?.observed ? Date.parse(entry.observed) : NaN;
|
|
111
|
+
if (!entry ||
|
|
112
|
+
!Number.isFinite(observed) ||
|
|
113
|
+
observed >= cutoff ||
|
|
114
|
+
entry.pinned ||
|
|
115
|
+
(rule.source && entry.source !== rule.source))
|
|
116
|
+
return true;
|
|
117
|
+
selected.push(line);
|
|
118
|
+
return false;
|
|
119
|
+
});
|
|
120
|
+
if (!selected.length)
|
|
101
121
|
continue;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
122
|
+
const target = join(context.paths.archiveDir, basename(file));
|
|
123
|
+
const prior = (await readTextIfExists(target)) ?? "";
|
|
124
|
+
const seen = new Set(prior.split("\n"));
|
|
125
|
+
await writeCorpusFile(context.paths, target, `${[prior.trimEnd(), ...selected.filter((line) => !seen.has(line))].filter(Boolean).join("\n")}\n`);
|
|
126
|
+
await writeCorpusFile(context.paths, file, remaining.join("\n"));
|
|
127
|
+
if (!remaining.some((line) => line.trim()))
|
|
128
|
+
await rm(file);
|
|
129
|
+
archived.push(file);
|
|
107
130
|
}
|
|
108
131
|
return archived;
|
|
109
132
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export declare class MemoryStore {
|
|
|
28
28
|
private constructor();
|
|
29
29
|
private migrate;
|
|
30
30
|
static open(indexFile: string): Promise<MemoryStore>;
|
|
31
|
+
private static copy;
|
|
32
|
+
fork(): MemoryStore;
|
|
33
|
+
static preview(indexFile: string): Promise<MemoryStore>;
|
|
31
34
|
private ensureFts;
|
|
32
35
|
hasFts(): boolean;
|
|
33
36
|
replaceEntries(entries: IndexedEntry[]): void;
|
package/dist/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { mkdir, stat } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
export function stableIdFor(file, text, key) {
|
|
@@ -60,6 +60,44 @@ export class MemoryStore {
|
|
|
60
60
|
await mkdir(dirname(indexFile), { mode: 0o700, recursive: true });
|
|
61
61
|
return new MemoryStore(indexFile);
|
|
62
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
|
+
return copy;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
copy.close();
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
fork() {
|
|
82
|
+
return MemoryStore.copy(this.db);
|
|
83
|
+
}
|
|
84
|
+
static async preview(indexFile) {
|
|
85
|
+
try {
|
|
86
|
+
await stat(indexFile);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.code === "ENOENT")
|
|
90
|
+
return new MemoryStore(":memory:");
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const database = new DatabaseSync(indexFile, { readOnly: true });
|
|
94
|
+
try {
|
|
95
|
+
return MemoryStore.copy(database);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
database.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
63
101
|
ensureFts() {
|
|
64
102
|
try {
|
|
65
103
|
this.db.exec("CREATE VIRTUAL TABLE IF NOT EXISTS entries_fts USING fts5(stable_id UNINDEXED, text);");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kisev/memomatic",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.61.gdf462654550b",
|
|
4
4
|
"description": "Personal learning memory for agents: tiered Markdown corpus, rebuildable index, dream sweep.",
|
|
5
5
|
"homepage": "https://github.com/kisev/skills#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"pack:check": "node test/pack-allowlist.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@kisev/safe-fs": "1.0.0-dev.
|
|
37
|
+
"@kisev/safe-fs": "1.0.0-dev.61.gdf462654550b"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "22.15.30",
|