@kisev/memomatic 1.0.0-dev.61.gdf462654550b → 1.0.0-dev.63.g16bac7f2191d

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,8 +15,9 @@ 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+. Explicitly select `memomatic` in the `@kisev/agentomatic`
19
- installer and restart OpenCode. For standalone use, run the CLI:
18
+ Requires Node.js 22+. Connect `memomatic mcp-serve` as a local stdio MCP server
19
+ in OpenCode, Kilo, MiMo, or another MCP host and restart that host. No plugin or
20
+ automatic context injection is used. For background processing, run the CLI:
20
21
 
21
22
  ```bash
22
23
  memomatic process # validate the inbox and index accepted entries (no model)
@@ -49,16 +50,17 @@ Every entry can carry a `source` annotation. Visibility derives from it:
49
50
  `team-*`, `gitlab`, and `spec-manage` entries may be quoted in team-facing
50
51
  artifacts; every other source (`people-journal`, `stopit`,
51
52
  `mattermost-triage`, `task-*`, `docs-*`, `user`) is personal-only. The label
52
- is exposed in search responses and session bootstrap blocks.
53
+ is exposed in search responses.
53
54
 
54
55
  ## Surfaces
55
56
 
56
57
  - `memomatic` CLI: `process`, `dream`, `search`, `status`, `index`.
57
58
  - MCP stdio server with `memory_search`, `memory_get`, `memory_write`, and
58
59
  `memory_forget` tools.
59
- - The OpenCode plugin re-exported by `@kisev/agentomatic`; its bootstrap
60
- injects curated memory plus project- and trigger-matched recall blocks
61
- resolved from the session database (`projects` map in `settings.json`).
60
+
61
+ The model initiates memory searches through visible MCP tool calls. Dream
62
+ processes OpenCode history separately; connecting another host does not import
63
+ its session history. Agentomatic and memomatic are installed independently.
62
64
 
63
65
  Nothing is deleted without the explicit directives documented in
64
66
  `MEMORY_RULES.md`.
package/README.ru.md CHANGED
@@ -15,8 +15,9 @@ 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` в установщике `@kisev/agentomatic`
19
- и перезапустите OpenCode. Автономное использование доступно через CLI:
18
+ Нужен Node.js 22+. Подключите `memomatic mcp-serve` как локальный MCP-сервер stdio
19
+ в OpenCode, Kilo, MiMo или другом MCP-хосте и перезапустите хост. Плагин и
20
+ автоматическая подстановка контекста не используются. Фоновая обработка доступна через CLI:
20
21
 
21
22
  ```bash
22
23
  memomatic process # валидация inbox и индексация принятого (без модели)
@@ -49,15 +50,16 @@ memomatic dream # полный проход: inbox + сессии + кон
49
50
  записи `team-*`, `gitlab` и `spec-manage` можно цитировать в командных
50
51
  артефактах; остальные источники (`people-journal`, `stopit`,
51
52
  `mattermost-triage`, `task-*`, `docs-*`, `user`) — personal-only. Метка
52
- возвращается в поиске и в bootstrap-блоках сессий.
53
+ возвращается в поиске.
53
54
 
54
55
  ## Поверхности
55
56
 
56
57
  - CLI `memomatic`: `process`, `dream`, `search`, `status`, `index`.
57
58
  - MCP-сервер stdio с инструментами `memory_search`, `memory_get`,
58
59
  `memory_write` и `memory_forget`.
59
- - Плагин OpenCode, реэкспортируемый пакетом `@kisev/agentomatic`; его
60
- bootstrap добавляет к кураторской памяти блоки напоминаний по проекту и
61
- trigger-фразам (контекст сессии и маппинг `projects` из `settings.json`).
60
+
61
+ Модель сама инициирует поиск памяти через видимые MCP-вызовы. Dream отдельно
62
+ обрабатывает историю OpenCode; подключение другого хоста не импортирует его
63
+ сессии. Agentomatic и memomatic устанавливаются независимо.
62
64
 
63
65
  Без явных директив, описанных в `MEMORY_RULES.md`, ничего не удаляется.
package/dist/executor.js CHANGED
@@ -23,16 +23,26 @@ export class OpenCodeExecutor {
23
23
  this.options = options;
24
24
  }
25
25
  async complete(request) {
26
- const args = ["run", "--agent", "build"];
26
+ const args = ["run", "--agent", "build", "--format", "json"];
27
27
  if (this.options.model)
28
28
  args.push("--model", this.options.model);
29
29
  if (this.options.variant)
30
30
  args.push("--variant", this.options.variant);
31
- args.push("--message", `[memomatic-internal] ${request.system}\n\n${request.prompt}`);
31
+ args.push("--", `[memomatic-internal] ${request.system}\n\n${request.prompt}`);
32
32
  for (let attempt = 0; attempt < 2; attempt += 1) {
33
33
  const output = await this.spawnOnce(args);
34
34
  try {
35
- const parsed = extractJson(output);
35
+ const texts = [];
36
+ for (const line of output.split("\n")) {
37
+ if (!line.trim())
38
+ continue;
39
+ const event = JSON.parse(line);
40
+ if (event.type === "error")
41
+ throw new Error("opencode returned an error event");
42
+ if (event.type === "text" && typeof event.part?.text === "string")
43
+ texts.push(event.part.text);
44
+ }
45
+ const parsed = extractJson(texts.join("\n"));
36
46
  return JSON.stringify(parsed);
37
47
  }
38
48
  catch {
package/dist/index.d.ts CHANGED
@@ -4,5 +4,4 @@ export { visibilityForSource } from "./visibility.js";
4
4
  export { dropToInbox, processInbox, withRunLock } from "./inbox.js";
5
5
  export { forgetEntry, getEntry, openMemomatic, searchMemory, searchMemoryResults, writeEntry, } from "./service.js";
6
6
  export type { MemomaticContext } from "./service.js";
7
- export { bootstrapContext, resolveProject } from "./bootstrap.js";
8
- export { opencodeDatabasePath, sessionFacts } from "./ingest.js";
7
+ export { opencodeDatabasePath } from "./ingest.js";
package/dist/index.js CHANGED
@@ -3,5 +3,4 @@ export { entryLine, parseEntryLine } from "./entries.js";
3
3
  export { visibilityForSource } from "./visibility.js";
4
4
  export { dropToInbox, processInbox, withRunLock } from "./inbox.js";
5
5
  export { forgetEntry, getEntry, openMemomatic, searchMemory, searchMemoryResults, writeEntry, } from "./service.js";
6
- export { bootstrapContext, resolveProject } from "./bootstrap.js";
7
- export { opencodeDatabasePath, sessionFacts } from "./ingest.js";
6
+ export { opencodeDatabasePath } from "./ingest.js";
package/dist/ingest.d.ts CHANGED
@@ -9,17 +9,6 @@ export type IngestSession = {
9
9
  }>;
10
10
  };
11
11
  export declare function opencodeDatabasePath(): string;
12
- export type SessionFacts = {
13
- directory: string | null;
14
- title: string | null;
15
- firstMessage: string | null;
16
- };
17
- /**
18
- * Resolve session context for the bootstrap block: working directory, title,
19
- * and the first user message, read-only from the OpenCode database. Returns
20
- * nulls when the session or database is unavailable.
21
- */
22
- export declare function sessionFacts(databaseFile: string, sessionId: string): SessionFacts;
23
12
  export declare function loadRecentSessions(databaseFile: string, afterTimeCreated: number, options?: {
24
13
  maxSessions: number;
25
14
  maxCharsPerSession: number;
package/dist/ingest.js CHANGED
@@ -9,60 +9,33 @@ export function opencodeDatabasePath() {
9
9
  throw new Error("XDG_DATA_HOME must be an absolute path");
10
10
  return join(base, "opencode", "opencode.db");
11
11
  }
12
- /**
13
- * Resolve session context for the bootstrap block: working directory, title,
14
- * and the first user message, read-only from the OpenCode database. Returns
15
- * nulls when the session or database is unavailable.
16
- */
17
- export function sessionFacts(databaseFile, sessionId) {
18
- let db;
19
- try {
20
- db = new DatabaseSync(databaseFile, { readOnly: true });
21
- }
22
- catch {
23
- return { directory: null, title: null, firstMessage: null };
24
- }
25
- try {
26
- const session = db
27
- .prepare("SELECT directory, title FROM session WHERE id = ?;")
28
- .get(sessionId);
29
- let firstMessage = null;
30
- const rows = db
31
- .prepare(`SELECT data FROM message WHERE session_id = ? ORDER BY time_created ASC LIMIT 20;`)
32
- .all(sessionId);
33
- for (const row of rows) {
34
- try {
35
- const parsed = JSON.parse(row.data);
36
- if (parsed.role !== "user")
37
- continue;
38
- const texts = [];
39
- for (const part of parsed.parts ?? []) {
40
- const value = part;
41
- if (value.type === "text" && typeof value.text === "string")
42
- texts.push(value.text);
12
+ function readMessages(db, sessionId) {
13
+ const hasParts = db
14
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'part'")
15
+ .get();
16
+ const rows = db
17
+ .prepare(`SELECT ${hasParts ? "id, " : ""}data FROM message WHERE session_id = ? ORDER BY time_created ASC;`)
18
+ .all(sessionId);
19
+ if (!hasParts)
20
+ return rows;
21
+ const parts = db.prepare("SELECT data FROM part WHERE message_id = ? ORDER BY time_created ASC, id ASC");
22
+ return rows.map((row) => {
23
+ try {
24
+ const message = JSON.parse(row.data);
25
+ message.parts = parts.all(row.id).flatMap((part) => {
26
+ try {
27
+ return [JSON.parse(part.data)];
43
28
  }
44
- const text = texts.join("\n").trim();
45
- if (text) {
46
- firstMessage = text;
47
- break;
29
+ catch {
30
+ return [];
48
31
  }
49
- }
50
- catch {
51
- continue;
52
- }
32
+ });
33
+ return { data: JSON.stringify(message) };
53
34
  }
54
- return {
55
- directory: session?.directory ?? null,
56
- title: session?.title ?? null,
57
- firstMessage,
58
- };
59
- }
60
- catch {
61
- return { directory: null, title: null, firstMessage: null };
62
- }
63
- finally {
64
- db.close();
65
- }
35
+ catch {
36
+ return row;
37
+ }
38
+ });
66
39
  }
67
40
  export function loadRecentSessions(databaseFile, afterTimeCreated, options = {
68
41
  maxSessions: 20,
@@ -86,9 +59,7 @@ export function loadRecentSessions(databaseFile, afterTimeCreated, options = {
86
59
  const messages = [];
87
60
  let budget = options.maxCharsPerSession;
88
61
  let internal = false;
89
- const rows = db
90
- .prepare(`SELECT data FROM message WHERE session_id = ? ORDER BY time_created ASC;`)
91
- .all(session.id);
62
+ const rows = readMessages(db, session.id);
92
63
  for (const row of rows) {
93
64
  if (budget <= 0)
94
65
  break;
package/dist/service.js CHANGED
@@ -3,7 +3,7 @@ import { readTextIfExists, replaceEntryLine, writeCorpusFile } from "./corpus.js
3
3
  import { assertSafePath } from "@kisev/safe-fs";
4
4
  import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { rm } from "node:fs/promises";
6
- import { dropToInbox } from "./inbox.js";
6
+ import { dropToInbox, withRunLock } from "./inbox.js";
7
7
  import { isForbidden, loadRules } from "./rules.js";
8
8
  import { memomaticPaths } from "./paths.js";
9
9
  import { loadSettings } from "./settings.js";
@@ -79,15 +79,23 @@ export async function getEntry(context, file, line) {
79
79
  return { file: target, line, content: lines[line - 1] };
80
80
  }
81
81
  export async function forgetEntry(context, target) {
82
- const file = await memoryPath(context, target.file);
83
- const content = await readTextIfExists(file);
84
- if (content === undefined)
85
- throw new Error(`memory file not found: ${target.file}`);
86
- const lines = content.split("\n");
87
- if (!Number.isSafeInteger(target.line) || target.line < 1 || target.line > lines.length)
88
- throw new Error("line is out of range");
89
- await replaceEntryLine(context.paths, file, target.line, null);
90
- return file;
82
+ return withRunLock(context.paths, async () => {
83
+ const file = await memoryPath(context, target.file);
84
+ const content = await readTextIfExists(file);
85
+ if (content === undefined)
86
+ throw new Error(`memory file not found: ${target.file}`);
87
+ const lines = content.split("\n");
88
+ if (!Number.isSafeInteger(target.line) || target.line < 1 || target.line > lines.length)
89
+ throw new Error("line is out of range");
90
+ await replaceEntryLine(context.paths, file, target.line, null);
91
+ context.store.replaceEntries(context.store
92
+ .allEntries()
93
+ .filter((entry) => entry.file !== file || entry.line !== target.line)
94
+ .map((entry) => entry.file === file && entry.line > target.line
95
+ ? { ...entry, line: entry.line - 1 }
96
+ : entry));
97
+ return file;
98
+ });
91
99
  }
92
100
  export async function archiveOldEpisodic(context) {
93
101
  const archived = [];
@@ -3,7 +3,6 @@ export type MemomaticSettings = {
3
3
  url: string;
4
4
  model: string;
5
5
  } | null;
6
- projects: Record<string, string>;
7
6
  dream: {
8
7
  model: string | null;
9
8
  variant: string | null;
package/dist/settings.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  export const defaultSettings = () => ({
3
3
  embedding: null,
4
- projects: {},
5
4
  dream: {
6
5
  model: null,
7
6
  variant: null,
@@ -29,19 +28,10 @@ export function normalizeSettings(raw) {
29
28
  const defaults = defaultSettings();
30
29
  const source = (raw ?? {});
31
30
  const embedding = source.embedding;
32
- const rawProjects = source.projects;
33
- const projects = {};
34
- if (rawProjects && typeof rawProjects === "object") {
35
- for (const [prefix, name] of Object.entries(rawProjects)) {
36
- if (typeof name === "string" && name.trim() && prefix.trim())
37
- projects[prefix] = name.trim();
38
- }
39
- }
40
31
  return {
41
32
  embedding: embedding && typeof embedding.url === "string" && typeof embedding.model === "string"
42
33
  ? { url: embedding.url, model: embedding.model }
43
34
  : null,
44
- projects,
45
35
  dream: mergeSection(defaults.dream, source.dream),
46
36
  search: mergeSection(defaults.search, source.search),
47
37
  archive: mergeSection(defaults.archive, source.archive),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kisev/memomatic",
3
- "version": "1.0.0-dev.61.gdf462654550b",
3
+ "version": "1.0.0-dev.63.g16bac7f2191d",
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.61.gdf462654550b"
37
+ "@kisev/safe-fs": "1.0.0-dev.63.g16bac7f2191d"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "22.15.30",
@@ -1,19 +0,0 @@
1
- import type { MemomaticContext } from "./service.js";
2
- export type SessionFacts = {
3
- directory: string | null;
4
- title: string | null;
5
- firstMessage: string | null;
6
- };
7
- export type BootstrapOptions = {
8
- curatedBudgetChars?: number;
9
- episodicBudgetChars?: number;
10
- };
11
- /** Longest-prefix match of a working directory against the projects map. */
12
- export declare function resolveProject(directory: string | null, projects: Record<string, string>): string | null;
13
- /**
14
- * Build the session bootstrap context: curated MEMORY.md and USER.md heads,
15
- * plus episodic entries matched by project annotation and trigger phrases.
16
- * Every episodic line carries its source visibility so team-facing artifacts
17
- * never quote personal-only memory.
18
- */
19
- export declare function bootstrapContext(context: MemomaticContext, facts: SessionFacts, options?: BootstrapOptions): Promise<string | null>;
package/dist/bootstrap.js DELETED
@@ -1,84 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { visibilityForSource } from "./visibility.js";
3
- const DEFAULT_CURATED_BUDGET = 4_000;
4
- const DEFAULT_EPISODIC_BUDGET = 1_500;
5
- const MAX_BLOCK_ENTRIES = 8;
6
- /** Longest-prefix match of a working directory against the projects map. */
7
- export function resolveProject(directory, projects) {
8
- if (!directory)
9
- return null;
10
- let best = null;
11
- for (const [prefix, name] of Object.entries(projects)) {
12
- if (directory === prefix || directory.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`))
13
- if (!best || prefix.length > best.prefix.length)
14
- best = { prefix, name };
15
- }
16
- return best?.name ?? null;
17
- }
18
- function renderEntry(entry) {
19
- const visibility = visibilityForSource(entry.source);
20
- const label = entry.source ? `source: ${entry.source}, ${visibility}-only` : `${visibility}-only`;
21
- const text = entry.text.startsWith("- ") ? entry.text.slice(2) : entry.text;
22
- return `- ${text} (${label})`;
23
- }
24
- function rankEntries(entries) {
25
- return [...entries].sort((left, right) => {
26
- if (right.importance !== left.importance)
27
- return right.importance - left.importance;
28
- return (right.observedAt ?? 0) - (left.observedAt ?? 0);
29
- });
30
- }
31
- function fitsBudget(lines, budget) {
32
- const result = [];
33
- let used = 0;
34
- for (const line of lines) {
35
- if (result.length >= MAX_BLOCK_ENTRIES)
36
- break;
37
- if (used + line.length > budget)
38
- break;
39
- result.push(line);
40
- used += line.length + 1;
41
- }
42
- return result;
43
- }
44
- /**
45
- * Build the session bootstrap context: curated MEMORY.md and USER.md heads,
46
- * plus episodic entries matched by project annotation and trigger phrases.
47
- * Every episodic line carries its source visibility so team-facing artifacts
48
- * never quote personal-only memory.
49
- */
50
- export async function bootstrapContext(context, facts, options = {}) {
51
- const curatedBudget = options.curatedBudgetChars ?? DEFAULT_CURATED_BUDGET;
52
- const episodicBudget = options.episodicBudgetChars ?? DEFAULT_EPISODIC_BUDGET;
53
- const blocks = [];
54
- for (const file of [context.paths.memoryFile, context.paths.userFile]) {
55
- const content = await readFile(file, "utf8").catch(() => undefined);
56
- if (content === undefined || !content.trim())
57
- continue;
58
- const name = file.slice(file.lastIndexOf("/") + 1);
59
- blocks.push(`# Memomatic ${name}\n\n${content.trim().slice(0, curatedBudget)}`);
60
- }
61
- const entries = context.store.allEntries().filter((entry) => entry.status === null);
62
- const project = resolveProject(facts.directory, context.settings.projects);
63
- const usedTexts = new Set();
64
- if (project) {
65
- const lines = rankEntries(entries.filter((entry) => entry.kind === "episodic" && entry.project === project)).map((entry) => {
66
- usedTexts.add(entry.text);
67
- return renderEntry(entry);
68
- });
69
- const fitted = fitsBudget(lines, episodicBudget);
70
- if (fitted.length)
71
- blocks.push(`# Memomatic project recall (${project})\n\n${fitted.join("\n")}\n\nEntries marked personal-only must never be quoted in team-facing artifacts.`);
72
- }
73
- const haystack = [facts.title ?? "", facts.firstMessage ?? ""].join("\n").toLowerCase();
74
- if (haystack.trim()) {
75
- const triggered = rankEntries(entries.filter((entry) => !usedTexts.has(entry.text) &&
76
- entry.trigger.some((phrase) => phrase.trim() && haystack.includes(phrase.toLowerCase())))).map((entry) => renderEntry(entry));
77
- const fitted = fitsBudget(triggered, episodicBudget);
78
- if (fitted.length)
79
- blocks.push(`# Memomatic triggered recall\n\n${fitted.join("\n")}\n\nEntries marked personal-only must never be quoted in team-facing artifacts.`);
80
- }
81
- if (!blocks.length)
82
- return null;
83
- return `${blocks.join("\n\n")}\n\nUse memory_search for details and memory_write to queue durable outcomes. Never re-save content that is already present in this memory.`;
84
- }