@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.
Files changed (51) hide show
  1. package/README.md +97 -0
  2. package/README.ru.md +95 -0
  3. package/assets/systemd/memomatic-dream.service +9 -0
  4. package/assets/systemd/memomatic-dream.timer +10 -0
  5. package/assets/systemd/memomatic-sessions.service +9 -0
  6. package/assets/systemd/memomatic-sessions.timer +10 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +309 -0
  9. package/dist/corpus.d.ts +12 -0
  10. package/dist/corpus.js +138 -0
  11. package/dist/dream.d.ts +31 -0
  12. package/dist/dream.js +234 -0
  13. package/dist/entries.d.ts +31 -0
  14. package/dist/entries.js +119 -0
  15. package/dist/executor.d.ts +45 -0
  16. package/dist/executor.js +267 -0
  17. package/dist/gates.d.ts +27 -0
  18. package/dist/gates.js +69 -0
  19. package/dist/generated/cli.d.ts +28 -0
  20. package/dist/generated/cli.js +181 -0
  21. package/dist/inbox.d.ts +34 -0
  22. package/dist/inbox.js +224 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.js +6 -0
  25. package/dist/ingest.d.ts +1 -0
  26. package/dist/ingest.js +15 -0
  27. package/dist/ingestion.d.ts +30 -0
  28. package/dist/ingestion.js +179 -0
  29. package/dist/mcp.d.ts +15 -0
  30. package/dist/mcp.js +205 -0
  31. package/dist/opencode-config.d.ts +1 -0
  32. package/dist/opencode-config.js +84 -0
  33. package/dist/operations.d.ts +7 -0
  34. package/dist/operations.js +3 -0
  35. package/dist/paths.d.ts +18 -0
  36. package/dist/paths.js +55 -0
  37. package/dist/rules.d.ts +13 -0
  38. package/dist/rules.js +39 -0
  39. package/dist/search.d.ts +28 -0
  40. package/dist/search.js +191 -0
  41. package/dist/service.d.ts +63 -0
  42. package/dist/service.js +163 -0
  43. package/dist/sessions.d.ts +36 -0
  44. package/dist/sessions.js +238 -0
  45. package/dist/settings.d.ts +48 -0
  46. package/dist/settings.js +189 -0
  47. package/dist/store.d.ts +55 -0
  48. package/dist/store.js +286 -0
  49. package/dist/visibility.d.ts +7 -0
  50. package/dist/visibility.js +13 -0
  51. 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,10 @@
1
+ [Unit]
2
+ Description=Run the Memomatic dream sweep nightly after session extraction
3
+
4
+ [Timer]
5
+ OnCalendar=*-*-* 04:30:00
6
+ RandomizedDelaySec=30m
7
+ Persistent=true
8
+
9
+ [Install]
10
+ WantedBy=timers.target
@@ -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
@@ -0,0 +1,10 @@
1
+ [Unit]
2
+ Description=Run the Memomatic session extraction early nightly
3
+
4
+ [Timer]
5
+ OnCalendar=*-*-* 02:00:00
6
+ RandomizedDelaySec=30m
7
+ Persistent=true
8
+
9
+ [Install]
10
+ WantedBy=timers.target
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }
@@ -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;