@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/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# memomatic
|
|
2
|
+
|
|
3
|
+
[Русская версия](README.ru.md)
|
|
4
|
+
|
|
5
|
+
`@kisev/memomatic` is a personal learning memory for agents following the OpenClaw
|
|
6
|
+
memory architecture: a tiered Markdown corpus you can read as plain files, a
|
|
7
|
+
rebuildable SQLite index with FTS5 and optional local embeddings, a `sessions`
|
|
8
|
+
extraction pass, and a nightly `dream` consolidation sweep.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Registry version
|
|
14
|
+
npm view --prefer-online @kisev/memomatic@latest version
|
|
15
|
+
|
|
16
|
+
# Install
|
|
17
|
+
npm install --global @kisev/memomatic
|
|
18
|
+
|
|
19
|
+
# Installed package and active CLI
|
|
20
|
+
npm list --global @kisev/memomatic --depth=0
|
|
21
|
+
memomatic --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Tags can move between preview and installation; the last commands show the
|
|
25
|
+
installed package and the CLI resolved from PATH. Use `@dev` in both the view
|
|
26
|
+
and install commands for the development channel.
|
|
27
|
+
|
|
28
|
+
The corpus lives under `$XDG_STATE_HOME/memomatic/` (`MEMORY.md`, `USER.md`,
|
|
29
|
+
daily notes, `DREAMS.md`); rules live in `$XDG_CONFIG_HOME/memomatic/MEMORY_RULES.md`.
|
|
30
|
+
Requires Node.js 22.13+. Connect `memomatic mcp-serve` as a local stdio MCP server
|
|
31
|
+
in OpenCode, Kilo, MiMo, or another MCP host and restart that host. No plugin or
|
|
32
|
+
automatic context injection is used. For background processing, run the CLI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
memomatic process # validate the inbox and index accepted entries (no model)
|
|
36
|
+
memomatic sessions # extract episodic memory from OpenCode sessions (model)
|
|
37
|
+
memomatic dream # consolidation: inbox + promotion + bounded rewrite + archive
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Nightly runs are scheduled by the systemd user units in `assets/systemd/`
|
|
41
|
+
(`memomatic-sessions.service`/`.timer` for extraction and
|
|
42
|
+
`memomatic-dream.service`/`.timer` for consolidation).
|
|
43
|
+
|
|
44
|
+
Configure `sessions.model` (or keep a legacy `dream.model`) and an OpenCode
|
|
45
|
+
V2 provider before expecting session extraction; without a model, the session
|
|
46
|
+
watermark is preserved. Session extraction and its model server require
|
|
47
|
+
OpenCode `2.x`; a pre-V2 session database is not parsed. Follow the
|
|
48
|
+
[setup, first-entry, and scheduling guide](../../docs/how-to/memomatic.md).
|
|
49
|
+
|
|
50
|
+
## CLI, Sessions, and Dream
|
|
51
|
+
|
|
52
|
+
See [CLI conventions](../../docs/reference/cli.md) and
|
|
53
|
+
[Dream controls](../../docs/how-to/memomatic.md#observe-limit-and-resume-dream).
|
|
54
|
+
`sessions --plan` inspects pending session work without a model; `dream --plan`
|
|
55
|
+
counts pending inbox files and promotion candidates. `--dry-run` previews
|
|
56
|
+
either command and may still call models. Sessions drain the eligible snapshot
|
|
57
|
+
with per-fragment checkpoints, reusing one OpenCode server and unchanged
|
|
58
|
+
embeddings; Dream then promotes usage-gated episodic entries into `MEMORY.md`
|
|
59
|
+
through bounded consolidation and archives old entries. The first
|
|
60
|
+
legacy-cursor migration checks prior history once without discarding existing
|
|
61
|
+
memory. `status` shows the queue: pending inbox files, session backlog,
|
|
62
|
+
promotion candidates, and the last run of each sweep.
|
|
63
|
+
|
|
64
|
+
## Inbox
|
|
65
|
+
|
|
66
|
+
All writes are asynchronous. Skills, agents, and the `memory_write` tool
|
|
67
|
+
append Markdown entry lines to `$XDG_STATE_HOME/memomatic/inbox/`:
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
- Durable outcome in one sentence. <!-- source: team-retro --> <!-- key: stable-id -->
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The next `process` or `dream` pass validates drops, applies `never-save`
|
|
74
|
+
rules, deduplicates exact texts, supersedes entries sharing a `key`, rebuilds
|
|
75
|
+
the SQLite index with batch embeddings, and moves rejected drops to
|
|
76
|
+
`inbox/rejected/`. Producers detect the inbox by presence and skip silently
|
|
77
|
+
when memomatic is absent.
|
|
78
|
+
|
|
79
|
+
Every entry can carry a `source` annotation. Visibility derives from it:
|
|
80
|
+
`team-*`, `gitlab`, and `spec-manage` entries may be quoted in team-facing
|
|
81
|
+
artifacts; every other source (`people-journal`, `stopit`,
|
|
82
|
+
`mattermost-triage`, `task-*`, `docs-*`, `user`) is personal-only. The label
|
|
83
|
+
is exposed in search responses.
|
|
84
|
+
|
|
85
|
+
## Surfaces
|
|
86
|
+
|
|
87
|
+
- `memomatic` CLI: `process`, `sessions`, `dream`, `search`, `status`, `index`.
|
|
88
|
+
- MCP stdio server with `memory_search`, `memory_get`, `memory_write`, and
|
|
89
|
+
`memory_forget` tools.
|
|
90
|
+
|
|
91
|
+
The model initiates memory searches through visible MCP tool calls. The
|
|
92
|
+
`sessions` command processes OpenCode history separately; connecting another
|
|
93
|
+
host does not import its session history. Agentomatic and memomatic are
|
|
94
|
+
installed independently.
|
|
95
|
+
|
|
96
|
+
Nothing is deleted without the explicit directives documented in
|
|
97
|
+
`MEMORY_RULES.md`.
|
package/README.ru.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# memomatic
|
|
2
|
+
|
|
3
|
+
[English version](README.md)
|
|
4
|
+
|
|
5
|
+
`@kisev/memomatic` — персональная обучающая память для агентов по архитектуре
|
|
6
|
+
памяти OpenClaw: ярусный Markdown-корпус, который можно читать как обычные файлы,
|
|
7
|
+
пересобираемый SQLite-индекс с FTS5 и опциональными локальными эмбеддингами,
|
|
8
|
+
проход извлечения `sessions` и ночной проход консолидации `dream`.
|
|
9
|
+
|
|
10
|
+
## Установка
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Registry version
|
|
14
|
+
npm view --prefer-online @kisev/memomatic@latest version
|
|
15
|
+
|
|
16
|
+
# Install
|
|
17
|
+
npm install --global @kisev/memomatic
|
|
18
|
+
|
|
19
|
+
# Installed package and active CLI
|
|
20
|
+
npm list --global @kisev/memomatic --depth=0
|
|
21
|
+
memomatic --version
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Тег может сдвинуться между просмотром и установкой; последние команды показывают
|
|
25
|
+
установленный пакет и CLI из PATH. Для dev-канала укажите `@dev` и в просмотре,
|
|
26
|
+
и в установке.
|
|
27
|
+
|
|
28
|
+
Корпус живёт в `$XDG_STATE_HOME/memomatic/` (`MEMORY.md`, `USER.md`, ежедневные
|
|
29
|
+
записи, `DREAMS.md`); правила — в `$XDG_CONFIG_HOME/memomatic/MEMORY_RULES.md`.
|
|
30
|
+
Нужен Node.js 22.13+. Подключите `memomatic mcp-serve` как локальный MCP-сервер stdio
|
|
31
|
+
в OpenCode, Kilo, MiMo или другом MCP-хосте и перезапустите хост. Плагин и
|
|
32
|
+
автоматическая подстановка контекста не используются. Фоновая обработка доступна через CLI:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
memomatic process # валидация inbox и индексация принятого (без модели)
|
|
36
|
+
memomatic sessions # извлечение эпизодической памяти из сессий OpenCode (модель)
|
|
37
|
+
memomatic dream # консолидация: inbox + продвижение + перезапись + архив
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Ночные проходы планируют user-юниты systemd из `assets/systemd/`
|
|
41
|
+
(`memomatic-sessions.service`/`.timer` для извлечения и
|
|
42
|
+
`memomatic-dream.service`/`.timer` для консолидации).
|
|
43
|
+
|
|
44
|
+
Перед извлечением сессий настройте `sessions.model` (или оставьте старый
|
|
45
|
+
`dream.model`) и провайдера OpenCode V2; без модели отметка обработанных сессий
|
|
46
|
+
сохраняется. Извлечение сессий и его сервер модели требуют OpenCode `2.x`;
|
|
47
|
+
база сессий до V2 не разбирается. См.
|
|
48
|
+
[настройку, первую запись и расписание](../../docs/ru/how-to/memomatic.md).
|
|
49
|
+
|
|
50
|
+
## CLI, Sessions и Dream
|
|
51
|
+
|
|
52
|
+
См. [соглашения CLI](../../docs/ru/reference/cli.md) и
|
|
53
|
+
[управление Dream](../../docs/ru/how-to/memomatic.md#наблюдение-ограничения-и-продолжение-dream).
|
|
54
|
+
`sessions --plan` читает очередь сессий без модели; `dream --plan` считает
|
|
55
|
+
ожидающие файлы inbox и кандидатов на продвижение. `--dry-run` показывает
|
|
56
|
+
превью любой команды и может вызывать модели. Sessions обрабатывают весь
|
|
57
|
+
подходящий снимок с контрольными точками фрагментов, переиспользуя один сервер
|
|
58
|
+
OpenCode и неизменённые эмбеддинги; Dream затем продвигает эпизодические записи,
|
|
59
|
+
прошедшие пороги использования, в `MEMORY.md` через ограниченную консолидацию и
|
|
60
|
+
архивирует старое. Первая миграция старого курсора перепроверяет историю один
|
|
61
|
+
раз, сохраняя накопленную память. `status` показывает очередь: ожидающие файлы
|
|
62
|
+
inbox, бэклог сессий, кандидатов на продвижение и время последнего прохода.
|
|
63
|
+
|
|
64
|
+
## Inbox
|
|
65
|
+
|
|
66
|
+
Все записи асинхронны. Скиллы, агенты и инструмент `memory_write` кладут
|
|
67
|
+
строки записей Markdown в `$XDG_STATE_HOME/memomatic/inbox/`:
|
|
68
|
+
|
|
69
|
+
```markdown
|
|
70
|
+
- Durable-вывод одним предложением. <!-- source: team-retro --> <!-- key: stable-id -->
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Следующий проход `process` или `dream` валидирует дропы, применяет правила
|
|
74
|
+
`never-save`, дедуплицирует точные тексты, замещает записи с тем же `key`,
|
|
75
|
+
пересобирает SQLite-индекс с батч-эмбеддингами и переносит отклонённое в
|
|
76
|
+
`inbox/rejected/`. Продюсеры детектируют inbox по наличию и молча
|
|
77
|
+
пропускают дроп, если memomatic нет.
|
|
78
|
+
|
|
79
|
+
Каждая запись может нести аннотацию `source`. Из неё выводится видимость:
|
|
80
|
+
записи `team-*`, `gitlab` и `spec-manage` можно цитировать в командных
|
|
81
|
+
артефактах; остальные источники (`people-journal`, `stopit`,
|
|
82
|
+
`mattermost-triage`, `task-*`, `docs-*`, `user`) — personal-only. Метка
|
|
83
|
+
возвращается в поиске.
|
|
84
|
+
|
|
85
|
+
## Поверхности
|
|
86
|
+
|
|
87
|
+
- CLI `memomatic`: `process`, `sessions`, `dream`, `search`, `status`, `index`.
|
|
88
|
+
- MCP-сервер stdio с инструментами `memory_search`, `memory_get`,
|
|
89
|
+
`memory_write` и `memory_forget`.
|
|
90
|
+
|
|
91
|
+
Модель сама инициирует поиск памяти через видимые MCP-вызовы. Команда
|
|
92
|
+
`sessions` отдельно обрабатывает историю OpenCode; подключение другого хоста
|
|
93
|
+
не импортирует его сессии. Agentomatic и memomatic устанавливаются независимо.
|
|
94
|
+
|
|
95
|
+
Без явных директив, описанных в `MEMORY_RULES.md`, ничего не удаляется.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Memomatic dream: background memory consolidation sweep
|
|
3
|
+
Documentation=https://github.com/kisev/skills#memomatic
|
|
4
|
+
After=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=oneshot
|
|
8
|
+
ExecStart=/bin/sh -lc 'exec memomatic dream'
|
|
9
|
+
Environment=MEMOMATIC_DREAM=1
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=Memomatic sessions: extract episodic memory from OpenCode sessions
|
|
3
|
+
Documentation=https://github.com/kisev/skills#memomatic
|
|
4
|
+
After=network-online.target
|
|
5
|
+
|
|
6
|
+
[Service]
|
|
7
|
+
Type=oneshot
|
|
8
|
+
ExecStart=/bin/sh -lc 'exec memomatic sessions'
|
|
9
|
+
Environment=MEMOMATIC_SESSIONS=1
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readdir, stat } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute } from "node:path";
|
|
5
|
+
import { openMemomatic, rebuildIndex, searchMemory } from "./service.js";
|
|
6
|
+
import { runDream } from "./dream.js";
|
|
7
|
+
import { runSessions } from "./sessions.js";
|
|
8
|
+
import { processInbox, withRunLock } from "./inbox.js";
|
|
9
|
+
import { OpenCodeExecutor } from "./executor.js";
|
|
10
|
+
import { normalizeSettings } from "./settings.js";
|
|
11
|
+
import { planIngestion } from "./ingestion.js";
|
|
12
|
+
import { opencodeDatabasePath } from "./ingest.js";
|
|
13
|
+
import { promotionCandidates, relevanceByFts } from "./gates.js";
|
|
14
|
+
import { memomaticPaths } from "./paths.js";
|
|
15
|
+
import { program, common, Option, configFile, applyConfig, reporter, cancellation, duration, integer, fail, } from "./generated/cli.js";
|
|
16
|
+
const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
17
|
+
const cli = common(program("memomatic", "Explicit memory retrieval and observable background processing", version), "MEMOMATIC").addOption(new Option("--state-dir <path>", "absolute private memory directory").env("MEMOMATIC_HOME"));
|
|
18
|
+
const add = (name, description) => cli.command(name).description(description);
|
|
19
|
+
const resolveOptions = async (command) => {
|
|
20
|
+
const initial = command.optsWithGlobals();
|
|
21
|
+
if (initial.stateDir) {
|
|
22
|
+
if (!isAbsolute(initial.stateDir))
|
|
23
|
+
throw new Error("state-dir must be absolute");
|
|
24
|
+
process.env.MEMOMATIC_HOME = initial.stateDir;
|
|
25
|
+
}
|
|
26
|
+
const config = await configFile(initial.config ?? memomaticPaths().settingsFile).catch((error) => {
|
|
27
|
+
if (!initial.config && error.code === "ENOENT")
|
|
28
|
+
return {};
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
31
|
+
applyConfig(cli, config);
|
|
32
|
+
applyConfig(command, config);
|
|
33
|
+
const opts = command.optsWithGlobals();
|
|
34
|
+
if (opts.stateDir) {
|
|
35
|
+
if (!isAbsolute(opts.stateDir))
|
|
36
|
+
throw new Error("state-dir must be absolute");
|
|
37
|
+
process.env.MEMOMATIC_HOME = opts.stateDir;
|
|
38
|
+
}
|
|
39
|
+
if (opts.config)
|
|
40
|
+
process.env.MEMOMATIC_CONFIG = opts.config;
|
|
41
|
+
return opts;
|
|
42
|
+
};
|
|
43
|
+
const run = async (command, action) => {
|
|
44
|
+
const opts = await resolveOptions(command);
|
|
45
|
+
const log = reporter(opts);
|
|
46
|
+
const abort = cancellation();
|
|
47
|
+
const limited = opts.maxDuration
|
|
48
|
+
? AbortSignal.any([abort.signal, AbortSignal.timeout(opts.maxDuration)])
|
|
49
|
+
: abort.signal;
|
|
50
|
+
let context;
|
|
51
|
+
try {
|
|
52
|
+
context = await openMemomatic({
|
|
53
|
+
readOnly: opts.dryRun || opts.plan || command.name() === "status",
|
|
54
|
+
configure: (settings) => {
|
|
55
|
+
const sessions = { ...settings.sessions };
|
|
56
|
+
const dream = { ...settings.dream };
|
|
57
|
+
for (const [flag, key] of [
|
|
58
|
+
["model", "model"],
|
|
59
|
+
["variant", "variant"],
|
|
60
|
+
["timeout", "timeoutMs"],
|
|
61
|
+
["maxDuration", "maxDurationMs"],
|
|
62
|
+
["retries", "retries"],
|
|
63
|
+
]) {
|
|
64
|
+
if (opts[flag] !== undefined) {
|
|
65
|
+
Object.assign(sessions, { [key]: opts[flag] });
|
|
66
|
+
Object.assign(dream, { [key]: opts[flag] });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const [flag, key] of [
|
|
70
|
+
["maxSessions", "maxSessions"],
|
|
71
|
+
["chunkChars", "maxChars"],
|
|
72
|
+
["idle", "idleMs"],
|
|
73
|
+
["opencodeUrl", "opencodeUrl"],
|
|
74
|
+
])
|
|
75
|
+
if (opts[flag] !== undefined)
|
|
76
|
+
Object.assign(sessions, { [key]: opts[flag] });
|
|
77
|
+
return normalizeSettings({ ...settings, sessions, dream });
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const deadline = (section) => context.settings[section].maxDurationMs && !opts.maxDuration
|
|
81
|
+
? AbortSignal.any([limited, AbortSignal.timeout(context.settings[section].maxDurationMs)])
|
|
82
|
+
: limited;
|
|
83
|
+
const signal = command.name() === "sessions" ? deadline("sessions") : deadline("dream");
|
|
84
|
+
await action(context, opts, { signal, observe: log.emit });
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
context?.store.close();
|
|
88
|
+
abort.dispose();
|
|
89
|
+
log.close();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const output = (value, opts, summary) => console.log(opts.json || !process.stdout.isTTY ? JSON.stringify(value, null, 2) : summary);
|
|
93
|
+
const modelOptions = (command) => command
|
|
94
|
+
.addOption(new Option("--model <provider/model>", "OpenCode model; defaults to settings").env("MEMOMATIC_MODEL"))
|
|
95
|
+
.addOption(new Option("--variant <name>", "provider reasoning variant").env("MEMOMATIC_VARIANT"))
|
|
96
|
+
.addOption(new Option("--timeout <duration>", "per-model-call timeout (default 3m)")
|
|
97
|
+
.argParser(duration)
|
|
98
|
+
.env("MEMOMATIC_TIMEOUT"))
|
|
99
|
+
.addOption(new Option("--max-duration <duration>", "whole-run deadline; 0 means unlimited")
|
|
100
|
+
.argParser(duration)
|
|
101
|
+
.env("MEMOMATIC_MAX_DURATION"))
|
|
102
|
+
.addOption(new Option("--retries <count>", "additional model attempts (default 1)")
|
|
103
|
+
.argParser(integer)
|
|
104
|
+
.env("MEMOMATIC_RETRIES"));
|
|
105
|
+
const executor = (context, runtime) => {
|
|
106
|
+
const sections = {
|
|
107
|
+
sessions: context.settings.sessions,
|
|
108
|
+
dream: context.settings.dream,
|
|
109
|
+
};
|
|
110
|
+
const create = (section) => {
|
|
111
|
+
const config = sections[section];
|
|
112
|
+
return config.model
|
|
113
|
+
? new OpenCodeExecutor({
|
|
114
|
+
model: config.model,
|
|
115
|
+
variant: config.variant,
|
|
116
|
+
url: "opencodeUrl" in config ? config.opencodeUrl : null,
|
|
117
|
+
timeoutMs: config.timeoutMs,
|
|
118
|
+
retries: config.retries,
|
|
119
|
+
...runtime,
|
|
120
|
+
})
|
|
121
|
+
: null;
|
|
122
|
+
};
|
|
123
|
+
return create;
|
|
124
|
+
};
|
|
125
|
+
const sessions = add("sessions", "Drain the OpenCode session snapshot into episodic memory, checkpointing fragments")
|
|
126
|
+
.option("--plan", "count pending work without model calls or writes")
|
|
127
|
+
.option("--dry-run", "preview without persistent writes (model calls still occur)");
|
|
128
|
+
modelOptions(sessions)
|
|
129
|
+
.addOption(new Option("--max-sessions <count>", "optional session limit; 0 means all")
|
|
130
|
+
.argParser(integer)
|
|
131
|
+
.env("MEMOMATIC_MAX_SESSIONS"))
|
|
132
|
+
.addOption(new Option("--chunk-chars <count>", "maximum new text per fragment (default 24000)")
|
|
133
|
+
.argParser(integer)
|
|
134
|
+
.env("MEMOMATIC_CHUNK_CHARS"))
|
|
135
|
+
.addOption(new Option("--idle <duration>", "minimum session idle age (default 10m)")
|
|
136
|
+
.argParser(duration)
|
|
137
|
+
.env("MEMOMATIC_IDLE"))
|
|
138
|
+
.addOption(new Option("--opencode-url <url>", "reuse an existing OpenCode server; otherwise start one per run").env("MEMOMATIC_OPENCODE_URL"))
|
|
139
|
+
.addOption(new Option("--database <path>", "OpenCode SQLite session database").env("MEMOMATIC_DATABASE"))
|
|
140
|
+
.addHelpText("after", "\nExamples:\n memomatic sessions\n memomatic sessions --max-duration 15m --log-format json\n memomatic sessions --dry-run --max-sessions 2\n\nCompleted fragments survive cancellation. Logs use stderr; --json keeps stdout machine-readable.");
|
|
141
|
+
sessions.action(async () => run(sessions, async (context, opts, runtime) => {
|
|
142
|
+
if (opts.plan) {
|
|
143
|
+
const start = Date.now();
|
|
144
|
+
const plan = planIngestion(opts.database ?? opencodeDatabasePath(), context.store, {
|
|
145
|
+
before: Date.now() - context.settings.sessions.idleMs,
|
|
146
|
+
maxChars: context.settings.sessions.maxChars,
|
|
147
|
+
maxSessions: context.settings.sessions.maxSessions,
|
|
148
|
+
});
|
|
149
|
+
const summary = {
|
|
150
|
+
sessions: plan.sessions,
|
|
151
|
+
messages: plan.messages,
|
|
152
|
+
fragments: plan.fragments.length,
|
|
153
|
+
characters: plan.characters,
|
|
154
|
+
cachedFragments: plan.cached,
|
|
155
|
+
cachedSessions: plan.cachedSessions,
|
|
156
|
+
unscannedSessions: plan.unscannedSessions,
|
|
157
|
+
scanMs: Date.now() - start,
|
|
158
|
+
};
|
|
159
|
+
output(summary, opts, `${summary.sessions} sessions, ${summary.fragments} fragments, ${summary.characters} characters (${summary.scanMs}ms scan). No model calls.`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const create = executor(context, runtime);
|
|
163
|
+
const model = create("sessions");
|
|
164
|
+
try {
|
|
165
|
+
const execute = () => runSessions(context, model, {
|
|
166
|
+
...runtime,
|
|
167
|
+
dryRun: opts.dryRun,
|
|
168
|
+
databaseFile: opts.database,
|
|
169
|
+
});
|
|
170
|
+
const result = opts.dryRun ? await execute() : await withRunLock(context.paths, execute);
|
|
171
|
+
output({ ...result, modelUsage: model?.usage ?? null }, opts, `Sessions complete: ${result.sessionsIngested} sessions, ${result.candidatesExtracted} candidates, ${result.elapsedMs}ms`);
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
await model?.close();
|
|
175
|
+
}
|
|
176
|
+
}));
|
|
177
|
+
const dream = add("dream", "Consolidate memory: inbox, promotion, bounded rewrite, archive")
|
|
178
|
+
.option("--plan", "count pending work without model calls or writes")
|
|
179
|
+
.option("--dry-run", "preview without persistent writes (model calls still occur)");
|
|
180
|
+
modelOptions(dream).addHelpText("after", "\nExamples:\n memomatic dream\n memomatic dream --max-duration 15m --log-format json\n memomatic dream --dry-run\n\nSession extraction lives in `memomatic sessions`. Logs use stderr; --json keeps stdout machine-readable.");
|
|
181
|
+
dream.action(async () => run(dream, async (context, opts, runtime) => {
|
|
182
|
+
if (opts.plan) {
|
|
183
|
+
const relevance = relevanceByFts(context.store, context.settings);
|
|
184
|
+
const candidates = promotionCandidates(context.store, context.settings, relevance);
|
|
185
|
+
const inboxFiles = (await readdir(context.paths.inboxDir).catch(() => [])).filter((name) => name.endsWith(".md")).length;
|
|
186
|
+
const summary = { inboxFiles, promotionCandidates: candidates.length };
|
|
187
|
+
output(summary, opts, `${summary.inboxFiles} inbox files, ${summary.promotionCandidates} promotion candidates. No model calls.`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const create = executor(context, runtime);
|
|
191
|
+
const model = create("dream");
|
|
192
|
+
try {
|
|
193
|
+
const execute = () => runDream(context, model, { ...runtime, dryRun: opts.dryRun });
|
|
194
|
+
const result = opts.dryRun ? await execute() : await withRunLock(context.paths, execute);
|
|
195
|
+
output({ ...result, modelUsage: model?.usage ?? null }, opts, `Dream complete: ${result.promoted.length} promoted, ${result.superseded.length} superseded, ${result.elapsedMs}ms`);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await model?.close();
|
|
199
|
+
}
|
|
200
|
+
}));
|
|
201
|
+
const processCommand = add("process", "Process explicit inbox entries without LLM extraction").option("--dry-run", "preview without writes");
|
|
202
|
+
processCommand.action(() => run(processCommand, async (context, opts, runtime) => {
|
|
203
|
+
const execute = () => processInbox(context, { ...runtime, dryRun: opts.dryRun });
|
|
204
|
+
const result = opts.dryRun ? await execute() : await withRunLock(context.paths, execute);
|
|
205
|
+
output(result, opts, `Processed ${result.filesProcessed} inbox files; ${result.entriesAppended} entries added`);
|
|
206
|
+
}));
|
|
207
|
+
const index = add("index", "Update derived search data, reusing unchanged embeddings").option("--force", "recompute all document embeddings");
|
|
208
|
+
index.action(() => run(index, async (context, opts, runtime) => {
|
|
209
|
+
const entries = await rebuildIndex(context, { ...runtime, force: opts.force });
|
|
210
|
+
if (opts.json)
|
|
211
|
+
output({ entries }, opts, "");
|
|
212
|
+
else
|
|
213
|
+
console.log(`${entries} entries indexed`);
|
|
214
|
+
}));
|
|
215
|
+
const status = add("status", "Read corpus/index/queue status without indexing or model calls");
|
|
216
|
+
status.action(() => run(status, async (context, opts, runtime) => {
|
|
217
|
+
const inboxFiles = (await readdir(context.paths.inboxDir).catch(() => [])).filter((name) => name.endsWith(".md")).length;
|
|
218
|
+
let sessionBacklog = null;
|
|
219
|
+
const databaseFile = opencodeDatabasePath();
|
|
220
|
+
if (await stat(databaseFile).catch(() => null)) {
|
|
221
|
+
try {
|
|
222
|
+
const plan = planIngestion(databaseFile, context.store, {
|
|
223
|
+
before: Date.now() - context.settings.sessions.idleMs,
|
|
224
|
+
maxChars: context.settings.sessions.maxChars,
|
|
225
|
+
maxSessions: context.settings.sessions.maxSessions,
|
|
226
|
+
});
|
|
227
|
+
sessionBacklog = {
|
|
228
|
+
sessions: plan.sessions,
|
|
229
|
+
messages: plan.messages,
|
|
230
|
+
fragments: plan.fragments.length,
|
|
231
|
+
characters: plan.characters,
|
|
232
|
+
cachedFragments: plan.cached,
|
|
233
|
+
cachedSessions: plan.cachedSessions,
|
|
234
|
+
unscannedSessions: plan.unscannedSessions,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
runtime.observe({
|
|
239
|
+
phase: "status.sessions",
|
|
240
|
+
message: `Cannot read OpenCode sessions: ${error.message}`,
|
|
241
|
+
level: "warn",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const relevance = relevanceByFts(context.store, context.settings);
|
|
246
|
+
const candidates = promotionCandidates(context.store, context.settings, relevance);
|
|
247
|
+
const report = {
|
|
248
|
+
entries: context.store.allEntries().length,
|
|
249
|
+
fts5: context.store.hasFts(),
|
|
250
|
+
stateRoot: context.paths.stateRoot,
|
|
251
|
+
queue: {
|
|
252
|
+
inboxFiles,
|
|
253
|
+
sessions: sessionBacklog,
|
|
254
|
+
promotionCandidates: candidates.length,
|
|
255
|
+
},
|
|
256
|
+
lastRun: {
|
|
257
|
+
dream: context.store.getMeta("last-dream-at"),
|
|
258
|
+
sessions: context.store.getMeta("last-sessions-at"),
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
output(report, opts, `${report.entries} indexed entries, ${inboxFiles} inbox files, ${sessionBacklog ? `${sessionBacklog.fragments} pending fragments` : "no session database"}, ${candidates.length} promotion candidates in ${context.paths.stateRoot}`);
|
|
262
|
+
}));
|
|
263
|
+
const search = add("search <query...>", "Find relevant memory or return no matches")
|
|
264
|
+
.option("--explain", "include relevance diagnostics")
|
|
265
|
+
.addOption(new Option("--project <name>", "restrict to a project and user-level memory").env("MEMOMATIC_PROJECT"));
|
|
266
|
+
search.action((query) => run(search, async (context, opts) => {
|
|
267
|
+
const hits = await searchMemory(context, query.join(" "), { project: opts.project });
|
|
268
|
+
if (opts.json) {
|
|
269
|
+
console.log(JSON.stringify(hits.map((hit) => ({
|
|
270
|
+
file: hit.entry.file,
|
|
271
|
+
line: hit.entry.line,
|
|
272
|
+
score: hit.score,
|
|
273
|
+
text: hit.snippet,
|
|
274
|
+
...(opts.explain ? { explanation: hit.explanation } : {}),
|
|
275
|
+
})), null, 2));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!hits.length)
|
|
279
|
+
console.log("No relevant memory found.");
|
|
280
|
+
for (const hit of hits) {
|
|
281
|
+
console.log(`${hit.score.toFixed(3)} ${hit.entry.file.replace(`${context.paths.stateRoot}/`, "")}:${hit.entry.line} ${hit.snippet}`);
|
|
282
|
+
if (opts.explain)
|
|
283
|
+
console.log(` ${JSON.stringify(hit.explanation)}`);
|
|
284
|
+
}
|
|
285
|
+
}));
|
|
286
|
+
cli
|
|
287
|
+
.command("mcp-serve")
|
|
288
|
+
.description("Serve newline-delimited MCP; stdout is protocol-only")
|
|
289
|
+
.action(async () => {
|
|
290
|
+
await resolveOptions(cli);
|
|
291
|
+
const { runMcpServer } = await import("./mcp.js");
|
|
292
|
+
await runMcpServer();
|
|
293
|
+
});
|
|
294
|
+
cli
|
|
295
|
+
.command("mcp-selftest")
|
|
296
|
+
.description("Print the MCP tool registry without touching state")
|
|
297
|
+
.action(async () => {
|
|
298
|
+
const { handleMcpRequest } = await import("./mcp.js");
|
|
299
|
+
process.stdout.write(await handleMcpRequest({ id: 1, method: "tools/list" }));
|
|
300
|
+
});
|
|
301
|
+
try {
|
|
302
|
+
if (process.argv.length === 2)
|
|
303
|
+
cli.outputHelp();
|
|
304
|
+
else
|
|
305
|
+
await cli.parseAsync(process.argv);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
fail("memomatic", error, process.argv.includes("--json") || cli.opts().json === true || cli.opts().logFormat === "json");
|
|
309
|
+
}
|
package/dist/corpus.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CorpusEntry } from "./entries.js";
|
|
2
|
+
import type { MemomaticPaths } from "./paths.js";
|
|
3
|
+
export declare function readTextIfExists(path: string): Promise<string | undefined>;
|
|
4
|
+
export declare function writeCorpusFile(paths: MemomaticPaths, file: string, next: string): Promise<void>;
|
|
5
|
+
export declare function dailyNotePath(paths: MemomaticPaths, date?: Date): string;
|
|
6
|
+
export declare function entryKind(file: string): "curated" | "user" | "episodic";
|
|
7
|
+
export declare function parseCorpusEntries(markdown: string, file: string): CorpusEntry[];
|
|
8
|
+
export declare function appendDailyEntry(paths: MemomaticPaths, line: string, date?: Date): Promise<string>;
|
|
9
|
+
export declare function replaceEntryLine(paths: MemomaticPaths, file: string, line: number, replacement: string | null): Promise<void>;
|
|
10
|
+
export declare function appendDreams(paths: MemomaticPaths, report: string): Promise<void>;
|
|
11
|
+
export declare function archiveFile(paths: MemomaticPaths, file: string): Promise<string>;
|
|
12
|
+
export declare function isCuratedFile(file: string): boolean;
|