@kisev/memomatic 1.0.0-dev.69.gd644a007a7ee → 1.0.0-dev.71.gee4e37a5bb61

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 CHANGED
@@ -15,7 +15,7 @@ npm install --global @kisev/memomatic
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
- Requires Node.js 22+. Connect `memomatic mcp-serve` as a local stdio MCP server
18
+ Requires Node.js 22.13+. Connect `memomatic mcp-serve` as a local stdio MCP server
19
19
  in OpenCode, Kilo, MiMo, or another MCP host and restart that host. No plugin or
20
20
  automatic context injection is used. For background processing, run the CLI:
21
21
 
@@ -31,6 +31,15 @@ Configure `dream.model` and an OpenCode provider before expecting session
31
31
  extraction; without a model, the session watermark is preserved. Follow the
32
32
  [setup, first-entry, and scheduling guide](../../docs/how-to/memomatic.md).
33
33
 
34
+ ## CLI and Dream
35
+
36
+ See [CLI conventions](../../docs/reference/cli.md) and
37
+ [Dream controls](../../docs/how-to/memomatic.md#observe-limit-and-resume-dream).
38
+ `dream --plan` inspects pending work without a model; `dream --dry-run` may call
39
+ models. Normal Dream drains the eligible snapshot with per-fragment checkpoints,
40
+ reusing one OpenCode server and unchanged embeddings. The first legacy-cursor
41
+ migration checks prior history once without discarding existing memory.
42
+
34
43
  ## Inbox
35
44
 
36
45
  All writes are asynchronous. Skills, agents, and the `memory_write` tool
package/README.ru.md CHANGED
@@ -15,7 +15,7 @@ npm install --global @kisev/memomatic
15
15
 
16
16
  Корпус живёт в `$XDG_STATE_HOME/memomatic/` (`MEMORY.md`, `USER.md`, ежедневные
17
17
  записи, `DREAMS.md`); правила — в `$XDG_CONFIG_HOME/memomatic/MEMORY_RULES.md`.
18
- Нужен Node.js 22+. Подключите `memomatic mcp-serve` как локальный MCP-сервер stdio
18
+ Нужен Node.js 22.13+. Подключите `memomatic mcp-serve` как локальный MCP-сервер stdio
19
19
  в OpenCode, Kilo, MiMo или другом MCP-хосте и перезапустите хост. Плагин и
20
20
  автоматическая подстановка контекста не используются. Фоновая обработка доступна через CLI:
21
21
 
@@ -31,6 +31,15 @@ memomatic dream # полный проход: inbox + сессии + кон
31
31
  без модели отметка обработанных сессий сохраняется. См.
32
32
  [настройку, первую запись и расписание](../../docs/ru/how-to/memomatic.md).
33
33
 
34
+ ## CLI и Dream
35
+
36
+ См. [соглашения CLI](../../docs/ru/reference/cli.md) и
37
+ [управление Dream](../../docs/ru/how-to/memomatic.md#наблюдение-ограничения-и-продолжение-dream).
38
+ `dream --plan` читает очередь без модели; `dream --dry-run` может вызывать модели.
39
+ Обычный Dream обрабатывает весь подходящий снимок с контрольными точками фрагментов,
40
+ переиспользуя один сервер OpenCode и неизменённые эмбеддинги. Первая миграция старого
41
+ курсора перепроверяет историю один раз, сохраняя накопленную память.
42
+
34
43
  ## Inbox
35
44
 
36
45
  Все записи асинхронны. Скиллы, агенты и инструмент `memory_write` кладут
package/dist/cli.js CHANGED
@@ -1,108 +1,217 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from "node:fs";
3
+ import { isAbsolute } from "node:path";
3
4
  import { openMemomatic, rebuildIndex, searchMemory } from "./service.js";
4
5
  import { runDream } from "./dream.js";
5
6
  import { processInbox, withRunLock } from "./inbox.js";
6
7
  import { OpenCodeExecutor } from "./executor.js";
7
- import { handleMcpRequest } from "./mcp.js";
8
- const USAGE = `usage: memomatic <command> [args]
9
-
10
- commands:
11
- process [--dry-run] validate the inbox and move accepted entries into the corpus
12
- dream [--dry-run] run the consolidation sweep (scheduled by memomatic-dream.timer)
13
- search <query> search memory from the command line
14
- status report corpus and index status
15
- index rebuild the search index
16
- mcp-serve run the MCP stdio server`;
17
- function parseArguments(argv) {
18
- const [command, ...rest] = argv;
19
- if (!command || command === "help" || command === "--help") {
20
- process.stderr.write(`${USAGE}\nSearch options: --explain, --project NAME\n`);
21
- process.exitCode = command ? 0 : 2;
22
- return { command: "", rest };
8
+ import { normalizeSettings } from "./settings.js";
9
+ import { planIngestion } from "./ingestion.js";
10
+ import { opencodeDatabasePath } from "./ingest.js";
11
+ import { memomaticPaths } from "./paths.js";
12
+ import { program, common, Option, configFile, applyConfig, reporter, cancellation, duration, integer, fail, } from "./generated/cli.js";
13
+ const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
14
+ 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"));
15
+ const add = (name, description) => cli.command(name).description(description);
16
+ const resolveOptions = async (command) => {
17
+ const initial = command.optsWithGlobals();
18
+ if (initial.stateDir) {
19
+ if (!isAbsolute(initial.stateDir))
20
+ throw new Error("state-dir must be absolute");
21
+ process.env.MEMOMATIC_HOME = initial.stateDir;
23
22
  }
24
- return { command, rest };
25
- }
26
- async function main() {
27
- const { command, rest } = parseArguments(process.argv.slice(2));
28
- if (!command)
29
- return;
30
- if (command === "mcp-serve") {
31
- const { runMcpServer } = await import("./mcp.js");
32
- await runMcpServer();
33
- return;
23
+ const config = await configFile(initial.config ?? memomaticPaths().settingsFile).catch((error) => {
24
+ if (!initial.config && error.code === "ENOENT")
25
+ return {};
26
+ throw error;
27
+ });
28
+ applyConfig(cli, config);
29
+ applyConfig(command, config);
30
+ const opts = command.optsWithGlobals();
31
+ if (opts.stateDir) {
32
+ if (!isAbsolute(opts.stateDir))
33
+ throw new Error("state-dir must be absolute");
34
+ process.env.MEMOMATIC_HOME = opts.stateDir;
34
35
  }
35
- if (command === "mcp-selftest") {
36
- process.stdout.write(await handleMcpRequest({ id: 1, method: "tools/list" }));
37
- return;
36
+ if (opts.config)
37
+ process.env.MEMOMATIC_CONFIG = opts.config;
38
+ return opts;
39
+ };
40
+ const run = async (command, action) => {
41
+ const opts = await resolveOptions(command);
42
+ const log = reporter(opts);
43
+ const abort = cancellation();
44
+ const limited = opts.maxDuration
45
+ ? AbortSignal.any([abort.signal, AbortSignal.timeout(opts.maxDuration)])
46
+ : abort.signal;
47
+ let context;
48
+ try {
49
+ context = await openMemomatic({
50
+ readOnly: opts.dryRun || opts.plan || command.name() === "status",
51
+ configure: (settings) => {
52
+ const dream = { ...settings.dream };
53
+ for (const [flag, key] of [
54
+ ["model", "model"],
55
+ ["variant", "variant"],
56
+ ["timeout", "timeoutMs"],
57
+ ["maxDuration", "maxDurationMs"],
58
+ ["maxSessions", "maxSessions"],
59
+ ["chunkChars", "maxChars"],
60
+ ["retries", "retries"],
61
+ ["idle", "idleMs"],
62
+ ["opencodeUrl", "opencodeUrl"],
63
+ ])
64
+ if (opts[flag] !== undefined)
65
+ Object.assign(dream, { [key]: opts[flag] });
66
+ return normalizeSettings({ ...settings, dream });
67
+ },
68
+ });
69
+ const signal = context.settings.dream.maxDurationMs && !opts.maxDuration
70
+ ? AbortSignal.any([limited, AbortSignal.timeout(context.settings.dream.maxDurationMs)])
71
+ : limited;
72
+ await action(context, opts, { signal, observe: log.emit });
38
73
  }
39
- if (command === "--version") {
40
- const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
41
- process.stdout.write(`${version}\n`);
74
+ finally {
75
+ context?.store.close();
76
+ abort.dispose();
77
+ log.close();
78
+ }
79
+ };
80
+ const output = (value, opts, summary) => console.log(opts.json || !process.stdout.isTTY ? JSON.stringify(value, null, 2) : summary);
81
+ const dream = add("dream", "Drain the session snapshot, checkpointing completed fragments")
82
+ .option("--plan", "count pending work without model calls or writes")
83
+ .option("--dry-run", "preview without persistent writes (model calls still occur)")
84
+ .addOption(new Option("--model <provider/model>", "OpenCode model; defaults to settings.dream.model").env("MEMOMATIC_MODEL"))
85
+ .addOption(new Option("--variant <name>", "provider reasoning variant").env("MEMOMATIC_VARIANT"))
86
+ .addOption(new Option("--timeout <duration>", "per-model-call timeout (default 3m)")
87
+ .argParser(duration)
88
+ .env("MEMOMATIC_TIMEOUT"))
89
+ .addOption(new Option("--max-duration <duration>", "whole-run deadline; 0 means drain the snapshot")
90
+ .argParser(duration)
91
+ .env("MEMOMATIC_MAX_DURATION"))
92
+ .addOption(new Option("--max-sessions <count>", "optional session limit; 0 means all")
93
+ .argParser(integer)
94
+ .env("MEMOMATIC_MAX_SESSIONS"))
95
+ .addOption(new Option("--chunk-chars <count>", "maximum new text per fragment (default 24000)")
96
+ .argParser(integer)
97
+ .env("MEMOMATIC_CHUNK_CHARS"))
98
+ .addOption(new Option("--retries <count>", "additional model attempts (default 1)")
99
+ .argParser(integer)
100
+ .env("MEMOMATIC_RETRIES"))
101
+ .addOption(new Option("--idle <duration>", "minimum session idle age (default 10m)")
102
+ .argParser(duration)
103
+ .env("MEMOMATIC_IDLE"))
104
+ .addOption(new Option("--opencode-url <url>", "reuse an existing OpenCode server; otherwise start one per run").env("MEMOMATIC_OPENCODE_URL"))
105
+ .addOption(new Option("--database <path>", "OpenCode SQLite session database").env("MEMOMATIC_DATABASE"))
106
+ .addHelpText("after", "\nExamples:\n memomatic dream\n memomatic dream --max-duration 15m --log-format json\n memomatic dream --dry-run --max-sessions 2\n\nCompleted fragments survive cancellation. Logs use stderr; --json keeps stdout machine-readable.");
107
+ dream.action(async () => run(dream, async (context, opts, runtime) => {
108
+ if (opts.plan) {
109
+ const start = Date.now();
110
+ const plan = planIngestion(opts.database ?? opencodeDatabasePath(), context.store, {
111
+ before: Date.now() - context.settings.dream.idleMs,
112
+ maxChars: context.settings.dream.maxChars,
113
+ maxSessions: context.settings.dream.maxSessions,
114
+ });
115
+ const summary = {
116
+ sessions: plan.sessions,
117
+ messages: plan.messages,
118
+ fragments: plan.fragments.length,
119
+ characters: plan.characters,
120
+ cachedFragments: plan.cached,
121
+ cachedSessions: plan.cachedSessions,
122
+ unscannedSessions: plan.unscannedSessions,
123
+ scanMs: Date.now() - start,
124
+ };
125
+ output(summary, opts, `${summary.sessions} sessions, ${summary.fragments} fragments, ${summary.characters} characters (${summary.scanMs}ms scan). No model calls.`);
42
126
  return;
43
127
  }
44
- const dryRun = (command === "dream" || command === "process") && rest.includes("--dry-run");
45
- const context = await openMemomatic({ readOnly: dryRun });
128
+ const executor = context.settings.dream.model
129
+ ? new OpenCodeExecutor({
130
+ model: context.settings.dream.model,
131
+ variant: context.settings.dream.variant,
132
+ url: context.settings.dream.opencodeUrl,
133
+ timeoutMs: context.settings.dream.timeoutMs,
134
+ retries: context.settings.dream.retries,
135
+ ...runtime,
136
+ })
137
+ : null;
46
138
  try {
47
- if (command === "dream" || command === "process") {
48
- const execute = async () => {
49
- if (command === "process")
50
- return processInbox(context, { dryRun });
51
- const executor = context.settings.dream.model
52
- ? new OpenCodeExecutor({
53
- model: context.settings.dream.model,
54
- variant: context.settings.dream.variant,
55
- })
56
- : null;
57
- return runDream(context, executor, { dryRun });
58
- };
59
- const result = dryRun ? await execute() : await withRunLock(context.paths, execute);
60
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
61
- return;
62
- }
63
- if (command === "search") {
64
- const parts = [];
65
- let explain = false;
66
- let project;
67
- for (let index = 0; index < rest.length; index += 1) {
68
- if (rest[index] === "--explain")
69
- explain = true;
70
- else if (rest[index] === "--project") {
71
- project = rest[++index];
72
- if (!project || project.startsWith("--"))
73
- throw new Error("--project requires a value");
74
- }
75
- else
76
- parts.push(rest[index]);
77
- }
78
- const query = parts.join(" ").trim();
79
- if (!query)
80
- throw new Error("search requires a query");
81
- const hits = await searchMemory(context, query, { project });
82
- if (!hits.length)
83
- process.stdout.write("No relevant memory found.\n");
84
- for (const hit of hits) {
85
- process.stdout.write(`${hit.score.toFixed(3)} ${hit.entry.file.replace(`${context.paths.stateRoot}/`, "")}:${hit.entry.line} ${hit.snippet}\n`);
86
- if (explain)
87
- process.stdout.write(` ${JSON.stringify(hit.explanation)}\n`);
88
- }
89
- return;
90
- }
91
- if (command === "status") {
92
- const count = await rebuildIndex(context);
93
- const fts = context.store.hasFts();
94
- process.stdout.write(`${JSON.stringify({ entries: count, fts5: fts, stateRoot: context.paths.stateRoot }, null, 2)}\n`);
95
- return;
96
- }
97
- if (command === "index") {
98
- process.stdout.write(`${await rebuildIndex(context)} entries indexed\n`);
99
- return;
100
- }
101
- process.stderr.write(`unknown command: ${command}\n${USAGE}`);
102
- process.exitCode = 2;
139
+ const execute = () => runDream(context, executor, {
140
+ ...runtime,
141
+ dryRun: opts.dryRun,
142
+ databaseFile: opts.database,
143
+ });
144
+ const result = opts.dryRun ? await execute() : await withRunLock(context.paths, execute);
145
+ output({ ...result, modelUsage: executor?.usage ?? null }, opts, `Dream complete: ${result.sessionsIngested} sessions, ${result.candidatesExtracted} candidates, ${result.elapsedMs}ms`);
103
146
  }
104
147
  finally {
105
- context.store.close();
148
+ await executor?.close();
149
+ }
150
+ }));
151
+ const processCommand = add("process", "Process explicit inbox entries without LLM extraction").option("--dry-run", "preview without writes");
152
+ processCommand.action(() => run(processCommand, async (context, opts, runtime) => {
153
+ const execute = () => processInbox(context, { ...runtime, dryRun: opts.dryRun });
154
+ const result = opts.dryRun ? await execute() : await withRunLock(context.paths, execute);
155
+ output(result, opts, `Processed ${result.filesProcessed} inbox files; ${result.entriesAppended} entries added`);
156
+ }));
157
+ const index = add("index", "Update derived search data, reusing unchanged embeddings").option("--force", "recompute all document embeddings");
158
+ index.action(() => run(index, async (context, opts, runtime) => {
159
+ const entries = await rebuildIndex(context, { ...runtime, force: opts.force });
160
+ if (opts.json)
161
+ output({ entries }, opts, "");
162
+ else
163
+ console.log(`${entries} entries indexed`);
164
+ }));
165
+ const status = add("status", "Read corpus/index status without indexing or model calls");
166
+ status.action(() => run(status, async (context, opts) => output({
167
+ entries: context.store.allEntries().length,
168
+ fts5: context.store.hasFts(),
169
+ stateRoot: context.paths.stateRoot,
170
+ }, opts, `${context.store.allEntries().length} indexed entries in ${context.paths.stateRoot}`)));
171
+ const search = add("search <query...>", "Find relevant memory or return no matches")
172
+ .option("--explain", "include relevance diagnostics")
173
+ .addOption(new Option("--project <name>", "restrict to a project and user-level memory").env("MEMOMATIC_PROJECT"));
174
+ search.action((query) => run(search, async (context, opts) => {
175
+ const hits = await searchMemory(context, query.join(" "), { project: opts.project });
176
+ if (opts.json) {
177
+ console.log(JSON.stringify(hits.map((hit) => ({
178
+ file: hit.entry.file,
179
+ line: hit.entry.line,
180
+ score: hit.score,
181
+ text: hit.snippet,
182
+ ...(opts.explain ? { explanation: hit.explanation } : {}),
183
+ })), null, 2));
184
+ return;
106
185
  }
186
+ if (!hits.length)
187
+ console.log("No relevant memory found.");
188
+ for (const hit of hits) {
189
+ console.log(`${hit.score.toFixed(3)} ${hit.entry.file.replace(`${context.paths.stateRoot}/`, "")}:${hit.entry.line} ${hit.snippet}`);
190
+ if (opts.explain)
191
+ console.log(` ${JSON.stringify(hit.explanation)}`);
192
+ }
193
+ }));
194
+ cli
195
+ .command("mcp-serve")
196
+ .description("Serve newline-delimited MCP; stdout is protocol-only")
197
+ .action(async () => {
198
+ await resolveOptions(cli);
199
+ const { runMcpServer } = await import("./mcp.js");
200
+ await runMcpServer();
201
+ });
202
+ cli
203
+ .command("mcp-selftest")
204
+ .description("Print the MCP tool registry without touching state")
205
+ .action(async () => {
206
+ const { handleMcpRequest } = await import("./mcp.js");
207
+ process.stdout.write(await handleMcpRequest({ id: 1, method: "tools/list" }));
208
+ });
209
+ try {
210
+ if (process.argv.length === 2)
211
+ cli.outputHelp();
212
+ else
213
+ await cli.parseAsync(process.argv);
214
+ }
215
+ catch (error) {
216
+ fail("memomatic", error, process.argv.includes("--json") || cli.opts().json === true || cli.opts().logFormat === "json");
107
217
  }
108
- await main();
package/dist/dream.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type InboxReport } from "./inbox.js";
2
2
  import { type ModelExecutor } from "./executor.js";
3
+ import { type OperationOptions } from "./operations.js";
3
4
  import type { MemomaticContext } from "./service.js";
4
5
  export type DreamReport = {
5
6
  inbox: InboxReport;
@@ -15,6 +16,21 @@ export type DreamReport = {
15
16
  archived: string[];
16
17
  appendOnlyFallback: boolean;
17
18
  dryRun: boolean;
19
+ ingestion?: {
20
+ messages: number;
21
+ fragments: number;
22
+ completed: number;
23
+ cached: number;
24
+ characters: number;
25
+ remaining: number;
26
+ cachedSessions: number;
27
+ unscannedSessions: number;
28
+ };
29
+ elapsedMs?: number;
30
+ };
31
+ export type DreamOptions = OperationOptions & {
32
+ dryRun?: boolean;
33
+ databaseFile?: string;
18
34
  };
19
35
  export type ExtractedCandidate = {
20
36
  text: string;
@@ -34,6 +50,4 @@ export type ConsolidationOperation = {
34
50
  key: string;
35
51
  };
36
52
  export declare function parseConsolidation(response: string): ConsolidationOperation[];
37
- export declare function runDream(context: MemomaticContext, executor: ModelExecutor | null, options?: {
38
- dryRun?: boolean;
39
- }): Promise<DreamReport>;
53
+ export declare function runDream(context: MemomaticContext, executor: ModelExecutor | null, options?: DreamOptions): Promise<DreamReport>;