@kisev/memomatic 1.0.0-dev.46.gfbe1e4c6992e

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.
@@ -0,0 +1,102 @@
1
+ const TRAILING_ANNOTATION = /\s*<!--\s*([a-z-]+):\s*([^<]*?)\s*-->\s*$/;
2
+ export function parseEntryLine(line) {
3
+ let value = line.trimEnd();
4
+ if (!value.startsWith("- "))
5
+ return null;
6
+ const annotations = {};
7
+ for (let index = 0; index < 8; index += 1) {
8
+ const match = TRAILING_ANNOTATION.exec(value);
9
+ if (!match)
10
+ break;
11
+ const name = match[1];
12
+ const raw = match[2];
13
+ let consumed = true;
14
+ if (name === "key" || name === "project" || name === "observed") {
15
+ if (name === "observed")
16
+ annotations.observed = raw;
17
+ else if (name === "project")
18
+ annotations.project = raw;
19
+ else
20
+ annotations.key = raw;
21
+ }
22
+ else if (name === "status") {
23
+ if (raw === "active" || raw === "superseded")
24
+ annotations.status = raw;
25
+ else
26
+ consumed = false;
27
+ }
28
+ else if (name === "origin") {
29
+ if (raw === "user" || raw === "agent")
30
+ annotations.origin = raw;
31
+ else
32
+ consumed = false;
33
+ }
34
+ else if (name === "importance") {
35
+ const parsed = Number.parseInt(raw, 10);
36
+ if (Number.isFinite(parsed) && parsed >= 1 && parsed <= 10)
37
+ annotations.importance = parsed;
38
+ else
39
+ consumed = false;
40
+ }
41
+ else if (name === "trigger") {
42
+ annotations.trigger = raw
43
+ .split(/[;,]\s*/)
44
+ .map((item) => item.trim())
45
+ .filter(Boolean);
46
+ if (!annotations.trigger.length)
47
+ consumed = false;
48
+ }
49
+ else if (name === "pinned") {
50
+ if (raw === "true")
51
+ annotations.pinned = true;
52
+ else
53
+ consumed = false;
54
+ }
55
+ else
56
+ consumed = false;
57
+ if (!consumed)
58
+ break;
59
+ value = value.slice(0, match.index).trimEnd();
60
+ }
61
+ return { text: value, annotations };
62
+ }
63
+ export function serializeAnnotations(annotations) {
64
+ const parts = [];
65
+ if (annotations.key)
66
+ parts.push(`key: ${annotations.key}`);
67
+ if (annotations.status)
68
+ parts.push(`status: ${annotations.status}`);
69
+ if (annotations.origin)
70
+ parts.push(`origin: ${annotations.origin}`);
71
+ if (annotations.observed)
72
+ parts.push(`observed: ${annotations.observed}`);
73
+ if (annotations.project)
74
+ parts.push(`project: ${annotations.project}`);
75
+ if (annotations.importance !== undefined)
76
+ parts.push(`importance: ${annotations.importance}`);
77
+ if (annotations.trigger?.length)
78
+ parts.push(`trigger: ${annotations.trigger.join("; ")}`);
79
+ if (annotations.pinned)
80
+ parts.push("pinned: true");
81
+ return parts.map((part) => `<!-- ${part} -->`).join(" ");
82
+ }
83
+ export function entryLine(text, annotations) {
84
+ const suffix = serializeAnnotations(annotations);
85
+ return `- ${text}${suffix ? ` ${suffix}` : ""}`;
86
+ }
87
+ export function entryKey(entry) {
88
+ if (entry.annotations.key)
89
+ return entry.annotations.key;
90
+ return null;
91
+ }
92
+ export function entryImportance(entry) {
93
+ return entry.annotations.importance ?? 5;
94
+ }
95
+ export function entryObservedAt(entry) {
96
+ if (entry.annotations.observed) {
97
+ const parsed = Date.parse(entry.annotations.observed);
98
+ if (Number.isFinite(parsed))
99
+ return parsed;
100
+ }
101
+ return Number.NaN;
102
+ }
@@ -0,0 +1,22 @@
1
+ export type ModelExecutor = {
2
+ name: string;
3
+ complete(request: {
4
+ system: string;
5
+ prompt: string;
6
+ }): Promise<string>;
7
+ };
8
+ export declare function extractJson(response: string): unknown;
9
+ export declare class OpenCodeExecutor implements ModelExecutor {
10
+ private readonly options;
11
+ readonly name = "opencode";
12
+ constructor(options: {
13
+ model: string | null;
14
+ variant: string | null;
15
+ binary?: string;
16
+ });
17
+ complete(request: {
18
+ system: string;
19
+ prompt: string;
20
+ }): Promise<string>;
21
+ private spawnOnce;
22
+ }
@@ -0,0 +1,68 @@
1
+ import { spawn } from "node:child_process";
2
+ export function extractJson(response) {
3
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(response);
4
+ const candidates = [fenced?.[1], response].filter(Boolean);
5
+ for (const candidate of candidates) {
6
+ const start = candidate.indexOf("{");
7
+ const end = candidate.lastIndexOf("}");
8
+ if (start === -1 || end <= start)
9
+ continue;
10
+ try {
11
+ return JSON.parse(candidate.slice(start, end + 1));
12
+ }
13
+ catch {
14
+ continue;
15
+ }
16
+ }
17
+ throw new Error("model response contains no JSON object");
18
+ }
19
+ export class OpenCodeExecutor {
20
+ options;
21
+ name = "opencode";
22
+ constructor(options) {
23
+ this.options = options;
24
+ }
25
+ async complete(request) {
26
+ const args = ["run", "--agent", "build"];
27
+ if (this.options.model)
28
+ args.push("--model", this.options.model);
29
+ if (this.options.variant)
30
+ args.push("--variant", this.options.variant);
31
+ args.push("--message", `[memomatic-internal] ${request.system}\n\n${request.prompt}`);
32
+ for (let attempt = 0; attempt < 2; attempt += 1) {
33
+ const output = await this.spawnOnce(args);
34
+ try {
35
+ const parsed = extractJson(output);
36
+ return JSON.stringify(parsed);
37
+ }
38
+ catch {
39
+ if (attempt === 1)
40
+ throw new Error("opencode run returned no parsable JSON twice");
41
+ }
42
+ }
43
+ throw new Error("unreachable");
44
+ }
45
+ spawnOnce(args) {
46
+ return new Promise((resolve, reject) => {
47
+ const child = spawn(this.options.binary ?? "opencode", args, {
48
+ env: process.env,
49
+ stdio: ["ignore", "pipe", "pipe"],
50
+ });
51
+ let stdout = "";
52
+ let stderr = "";
53
+ child.stdout.on("data", (chunk) => {
54
+ stdout += chunk.toString("utf8");
55
+ });
56
+ child.stderr.on("data", (chunk) => {
57
+ stderr += chunk.toString("utf8");
58
+ });
59
+ child.on("error", reject);
60
+ child.on("close", (code) => {
61
+ if (code === 0)
62
+ resolve(stdout);
63
+ else
64
+ reject(new Error(`opencode run exited ${code}: ${stderr.slice(0, 400)}`));
65
+ });
66
+ });
67
+ }
68
+ }
@@ -0,0 +1,22 @@
1
+ import type { MemomaticSettings } from "./settings.js";
2
+ import type { IndexedEntry, MemoryStore } from "./store.js";
3
+ export type PromotionCandidate = {
4
+ entry: IndexedEntry;
5
+ score: number;
6
+ signals: {
7
+ relevance: number;
8
+ frequency: number;
9
+ diversity: number;
10
+ recency: number;
11
+ consolidation: number;
12
+ richness: number;
13
+ };
14
+ eligible: boolean;
15
+ reason: string | null;
16
+ };
17
+ export declare function scoreCandidate(entry: IndexedEntry, usage: {
18
+ useful: number;
19
+ queries: string[];
20
+ days: string[];
21
+ }, settings: MemomaticSettings, relevance: number, now?: number): PromotionCandidate;
22
+ export declare function promotionCandidates(store: MemoryStore, settings: MemomaticSettings, relevanceByStableId: Map<string, number>, now?: number): PromotionCandidate[];
package/dist/gates.js ADDED
@@ -0,0 +1,52 @@
1
+ export function scoreCandidate(entry, usage, settings, relevance, now = Date.now()) {
2
+ const signals = {
3
+ relevance: Math.max(0, Math.min(1, relevance)),
4
+ frequency: Math.min(1, usage.useful / 10),
5
+ diversity: Math.min(1, usage.queries.length / 5),
6
+ recency: entry.observedAt
7
+ ? Math.pow(0.5, Math.max(0, now - entry.observedAt) / 86_400_000 / settings.search.halfLifeDays)
8
+ : 0.5,
9
+ consolidation: Math.min(1, usage.useful / Math.max(1, settings.dream.minUseful)),
10
+ richness: Math.min(1, entry.text.length / 400),
11
+ };
12
+ const score = 0.3 * signals.relevance +
13
+ 0.24 * signals.frequency +
14
+ 0.15 * signals.diversity +
15
+ 0.15 * signals.recency +
16
+ 0.1 * signals.consolidation +
17
+ 0.06 * signals.richness;
18
+ let eligible = true;
19
+ let reason = null;
20
+ if (entry.kind !== "episodic") {
21
+ eligible = false;
22
+ reason = "not episodic";
23
+ }
24
+ else if (entry.origin === null) {
25
+ eligible = false;
26
+ reason = "unknown origin";
27
+ }
28
+ else if (usage.useful < settings.dream.minUseful) {
29
+ eligible = false;
30
+ reason = `useful ${usage.useful} < ${settings.dream.minUseful}`;
31
+ }
32
+ else if (usage.queries.length < settings.dream.minUniqueQueries) {
33
+ eligible = false;
34
+ reason = `queries ${usage.queries.length} < ${settings.dream.minUniqueQueries}`;
35
+ }
36
+ else if (score < settings.dream.minScore) {
37
+ eligible = false;
38
+ reason = `score ${score.toFixed(3)} < ${settings.dream.minScore}`;
39
+ }
40
+ return { entry, score, signals, eligible, reason };
41
+ }
42
+ export function promotionCandidates(store, settings, relevanceByStableId, now = Date.now()) {
43
+ const candidates = [];
44
+ for (const entry of store.allEntries()) {
45
+ const usage = store.usageFor(entry.stableId);
46
+ candidates.push(scoreCandidate(entry, usage, settings, relevanceByStableId.get(entry.stableId) ?? 0, now));
47
+ }
48
+ return candidates
49
+ .filter((candidate) => candidate.eligible)
50
+ .sort((left, right) => right.score - left.score)
51
+ .slice(0, settings.dream.maxCandidates);
52
+ }
@@ -0,0 +1,4 @@
1
+ export { writeCorpusFile } from "./corpus.js";
2
+ export { entryLine, parseEntryLine } from "./entries.js";
3
+ export { forgetEntry, getEntry, openMemomatic, searchMemory, writeEntry } from "./service.js";
4
+ export type { MemomaticContext } from "./service.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { writeCorpusFile } from "./corpus.js";
2
+ export { entryLine, parseEntryLine } from "./entries.js";
3
+ export { forgetEntry, getEntry, openMemomatic, searchMemory, writeEntry } from "./service.js";
@@ -0,0 +1,16 @@
1
+ export type IngestSession = {
2
+ id: string;
3
+ title: string;
4
+ directory: string;
5
+ timeCreated: number;
6
+ messages: Array<{
7
+ role: string;
8
+ text: string;
9
+ }>;
10
+ };
11
+ export declare function opencodeDatabasePath(): string;
12
+ export declare function loadRecentSessions(databaseFile: string, afterTimeCreated: number, options?: {
13
+ maxSessions: number;
14
+ maxCharsPerSession: number;
15
+ before?: number;
16
+ }): IngestSession[];
package/dist/ingest.js ADDED
@@ -0,0 +1,81 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ export function opencodeDatabasePath() {
5
+ const base = process.env.XDG_DATA_HOME
6
+ ? process.env.XDG_DATA_HOME
7
+ : join(homedir(), ".local", "share");
8
+ if (!isAbsolute(base))
9
+ throw new Error("XDG_DATA_HOME must be an absolute path");
10
+ return join(base, "opencode", "opencode.db");
11
+ }
12
+ export function loadRecentSessions(databaseFile, afterTimeCreated, options = {
13
+ maxSessions: 20,
14
+ maxCharsPerSession: 24_000,
15
+ }) {
16
+ let db;
17
+ try {
18
+ db = new DatabaseSync(databaseFile, { readOnly: true });
19
+ }
20
+ catch {
21
+ return [];
22
+ }
23
+ try {
24
+ const before = options.before ?? Number.MAX_SAFE_INTEGER;
25
+ const sessions = db
26
+ .prepare(`SELECT id, title, directory, time_created FROM session
27
+ WHERE time_created > ? AND time_created < ? ORDER BY time_created ASC LIMIT ?;`)
28
+ .all(afterTimeCreated, before, options.maxSessions);
29
+ const result = [];
30
+ for (const session of sessions) {
31
+ const messages = [];
32
+ let budget = options.maxCharsPerSession;
33
+ let internal = false;
34
+ const rows = db
35
+ .prepare(`SELECT data FROM message WHERE session_id = ? ORDER BY time_created ASC;`)
36
+ .all(session.id);
37
+ for (const row of rows) {
38
+ if (budget <= 0)
39
+ break;
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(row.data);
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ const texts = [];
48
+ for (const part of parsed.parts ?? []) {
49
+ const value = part;
50
+ if (value.type === "text" && typeof value.text === "string")
51
+ texts.push(value.text);
52
+ }
53
+ const text = texts.join("\n").trim();
54
+ if (!text)
55
+ continue;
56
+ if (text.includes("[memomatic-internal]"))
57
+ internal = true;
58
+ if (budget - text.length < 0) {
59
+ messages.push({ role: parsed.role ?? "unknown", text: text.slice(0, budget) });
60
+ budget = 0;
61
+ break;
62
+ }
63
+ budget -= text.length;
64
+ messages.push({ role: parsed.role ?? "unknown", text });
65
+ }
66
+ if (internal || !messages.length)
67
+ continue;
68
+ result.push({
69
+ id: session.id,
70
+ title: session.title,
71
+ directory: session.directory,
72
+ timeCreated: session.time_created,
73
+ messages,
74
+ });
75
+ }
76
+ return result;
77
+ }
78
+ finally {
79
+ db.close();
80
+ }
81
+ }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ type JsonRpcRequest = {
2
+ jsonrpc?: string;
3
+ id?: unknown;
4
+ method?: string;
5
+ params?: Record<string, unknown>;
6
+ };
7
+ export declare function mcpResponse(id: unknown, result: unknown): string;
8
+ export declare function mcpError(id: unknown, code: number, message: string): string;
9
+ export declare function handleMcpRequest(request: JsonRpcRequest): Promise<string>;
10
+ export declare function runMcpServer(input?: NodeJS.ReadStream & {
11
+ fd: 0;
12
+ }, output?: NodeJS.WriteStream & {
13
+ fd: 1;
14
+ }): Promise<void>;
15
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,187 @@
1
+ import { forgetEntry, getEntry, openMemomatic, searchMemory, writeEntry } from "./service.js";
2
+ const PROTOCOL_VERSION = "2025-06-18";
3
+ const TOOLS = [
4
+ {
5
+ description: "Search personal memory: durable decisions, discoveries, and session outcomes.",
6
+ inputSchema: {
7
+ additionalProperties: false,
8
+ properties: { query: { type: "string" } },
9
+ required: ["query"],
10
+ type: "object",
11
+ },
12
+ name: "memory_search",
13
+ },
14
+ {
15
+ description: "Read a memory file fully or one line. Fetching a line marks that entry as useful.",
16
+ inputSchema: {
17
+ additionalProperties: false,
18
+ properties: {
19
+ file: { description: "Path relative to the memomatic state root", type: "string" },
20
+ line: { type: "number" },
21
+ },
22
+ required: ["file"],
23
+ type: "object",
24
+ },
25
+ name: "memory_get",
26
+ },
27
+ {
28
+ description: "Save a memory entry. Use for standing decisions with rationale, discoveries, failed attempts with rejection reasons, session outcomes, and action-sensitive boundaries. Respect MEMORY_RULES.md; never-save topics are rejected.",
29
+ inputSchema: {
30
+ additionalProperties: false,
31
+ properties: {
32
+ importance: { maximum: 10, minimum: 1, type: "number" },
33
+ key: { type: "string" },
34
+ origin: { enum: ["user", "agent"], type: "string" },
35
+ pinned: { type: "boolean" },
36
+ project: { type: "string" },
37
+ target: { enum: ["episodic", "curated", "user"], type: "string" },
38
+ text: { type: "string" },
39
+ trigger: { items: { type: "string" }, type: "array" },
40
+ },
41
+ required: ["text"],
42
+ type: "object",
43
+ },
44
+ name: "memory_write",
45
+ },
46
+ {
47
+ description: "Remove one entry line from a memory file (explicit deletion only).",
48
+ inputSchema: {
49
+ additionalProperties: false,
50
+ properties: {
51
+ file: { description: "Path relative to the memomatic state root", type: "string" },
52
+ line: { type: "number" },
53
+ },
54
+ required: ["file", "line"],
55
+ type: "object",
56
+ },
57
+ name: "memory_forget",
58
+ },
59
+ ];
60
+ async function callTool(name, args) {
61
+ const context = await openMemomatic();
62
+ try {
63
+ switch (name) {
64
+ case "memory_search": {
65
+ if (typeof args.query !== "string" || !args.query.trim())
66
+ throw new Error("query is required");
67
+ const hits = await searchMemory(context, args.query);
68
+ return hits.map((hit) => ({
69
+ file: hit.entry.file.replace(`${context.paths.stateRoot}/`, ""),
70
+ kind: hit.entry.kind,
71
+ line: hit.entry.line,
72
+ score: Number(hit.score.toFixed(4)),
73
+ snippet: hit.snippet,
74
+ }));
75
+ }
76
+ case "memory_get": {
77
+ if (typeof args.file !== "string" || !args.file)
78
+ throw new Error("file is required");
79
+ const line = typeof args.line === "number" ? args.line : undefined;
80
+ const result = await getEntry(context, args.file, line);
81
+ return {
82
+ content: result.content,
83
+ file: result.file.replace(`${context.paths.stateRoot}/`, ""),
84
+ line: result.line,
85
+ };
86
+ }
87
+ case "memory_write": {
88
+ if (typeof args.text !== "string" || !args.text.trim())
89
+ throw new Error("text is required");
90
+ const file = await writeEntry(context, {
91
+ importance: typeof args.importance === "number" ? args.importance : undefined,
92
+ key: typeof args.key === "string" ? args.key : undefined,
93
+ origin: args.origin === "user" ? "user" : "agent",
94
+ pinned: args.pinned === true,
95
+ project: typeof args.project === "string" ? args.project : undefined,
96
+ target: args.target === "curated" || args.target === "user" ? args.target : "episodic",
97
+ text: args.text,
98
+ trigger: Array.isArray(args.trigger)
99
+ ? args.trigger.filter((item) => typeof item === "string")
100
+ : undefined,
101
+ });
102
+ return { file: file.replace(`${context.paths.stateRoot}/`, ""), saved: true };
103
+ }
104
+ case "memory_forget": {
105
+ if (typeof args.file !== "string" || typeof args.line !== "number")
106
+ throw new Error("file and line are required");
107
+ const file = await forgetEntry(context, { file: args.file, line: args.line });
108
+ return { file: file.replace(`${context.paths.stateRoot}/`, ""), forgotten: true };
109
+ }
110
+ default:
111
+ throw new Error(`unknown tool: ${name}`);
112
+ }
113
+ }
114
+ finally {
115
+ context.store.close();
116
+ }
117
+ }
118
+ export function mcpResponse(id, result) {
119
+ return `${JSON.stringify({ id, jsonrpc: "2.0", result })}\n`;
120
+ }
121
+ export function mcpError(id, code, message) {
122
+ return `${JSON.stringify({ error: { code, message }, id, jsonrpc: "2.0" })}\n`;
123
+ }
124
+ export async function handleMcpRequest(request) {
125
+ const { id, method } = request;
126
+ if (method === "initialize")
127
+ return mcpResponse(id, {
128
+ capabilities: { tools: {} },
129
+ protocolVersion: PROTOCOL_VERSION,
130
+ serverInfo: { name: "memomatic", version: "0.1.0" },
131
+ });
132
+ if (method === "ping")
133
+ return mcpResponse(id, {});
134
+ if (method === "tools/list")
135
+ return mcpResponse(id, { tools: TOOLS });
136
+ if (method === "tools/call") {
137
+ const params = request.params ?? {};
138
+ const name = typeof params.name === "string" ? params.name : "";
139
+ const args = params.arguments && typeof params.arguments === "object"
140
+ ? params.arguments
141
+ : {};
142
+ try {
143
+ const content = await callTool(name, args);
144
+ return mcpResponse(id, {
145
+ content: [{ text: JSON.stringify(content), type: "text" }],
146
+ });
147
+ }
148
+ catch (error) {
149
+ return mcpResponse(id, {
150
+ content: [
151
+ {
152
+ text: JSON.stringify({ error: error.message }),
153
+ type: "text",
154
+ },
155
+ ],
156
+ isError: true,
157
+ });
158
+ }
159
+ }
160
+ if (method?.startsWith("notifications/"))
161
+ return "";
162
+ return mcpError(id, -32601, `method not found: ${method ?? "(none)"}`);
163
+ }
164
+ export async function runMcpServer(input = process.stdin, output = process.stdout) {
165
+ let buffer = "";
166
+ input.setEncoding("utf8");
167
+ for await (const chunk of input) {
168
+ buffer += chunk;
169
+ let newline = buffer.indexOf("\n");
170
+ while (newline !== -1) {
171
+ const frame = buffer.slice(0, newline).trim();
172
+ buffer = buffer.slice(newline + 1);
173
+ if (frame) {
174
+ try {
175
+ const request = JSON.parse(frame);
176
+ const response = await handleMcpRequest(request);
177
+ if (response)
178
+ output.write(response);
179
+ }
180
+ catch {
181
+ output.write(mcpError(null, -32700, "parse error"));
182
+ }
183
+ }
184
+ newline = buffer.indexOf("\n");
185
+ }
186
+ }
187
+ }
@@ -0,0 +1,15 @@
1
+ export type MemomaticPaths = {
2
+ stateRoot: string;
3
+ configRoot: string;
4
+ memoryFile: string;
5
+ userFile: string;
6
+ dreamsFile: string;
7
+ rulesFile: string;
8
+ settingsFile: string;
9
+ dailyDir: string;
10
+ archiveDir: string;
11
+ historyDir: string;
12
+ indexFile: string;
13
+ };
14
+ export declare function memomaticPaths(): MemomaticPaths;
15
+ export declare function verifyMemomaticRoots(paths: MemomaticPaths): Promise<void>;
package/dist/paths.js ADDED
@@ -0,0 +1,47 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join, resolve, sep } from "node:path";
4
+ function safeXdg(name, fallback) {
5
+ const configured = process.env[name];
6
+ if (configured) {
7
+ if (!isAbsolute(configured) || configured.split(sep).includes(".."))
8
+ throw new Error(`${name} must be an absolute safe path`);
9
+ return resolve(configured);
10
+ }
11
+ const home = process.env.HOME ?? homedir();
12
+ if (!isAbsolute(home) || home.split(sep).includes(".."))
13
+ throw new Error("HOME must be an absolute safe path");
14
+ return join(resolve(home), fallback);
15
+ }
16
+ async function assertDirectory(path) {
17
+ try {
18
+ const info = await lstat(path);
19
+ if (!info.isDirectory() || info.isSymbolicLink())
20
+ throw new Error(`memomatic directory is unsafe: ${path}`);
21
+ }
22
+ catch (error) {
23
+ if (error.code !== "ENOENT")
24
+ throw error;
25
+ }
26
+ }
27
+ export function memomaticPaths() {
28
+ const stateRoot = join(safeXdg("XDG_STATE_HOME", ".local/state"), "memomatic");
29
+ const configRoot = join(safeXdg("XDG_CONFIG_HOME", ".config"), "memomatic");
30
+ return {
31
+ stateRoot,
32
+ configRoot,
33
+ memoryFile: join(stateRoot, "MEMORY.md"),
34
+ userFile: join(stateRoot, "USER.md"),
35
+ dreamsFile: join(stateRoot, "DREAMS.md"),
36
+ rulesFile: join(configRoot, "MEMORY_RULES.md"),
37
+ settingsFile: join(configRoot, "settings.json"),
38
+ dailyDir: join(stateRoot, "memory"),
39
+ archiveDir: join(stateRoot, "archive"),
40
+ historyDir: join(stateRoot, "history"),
41
+ indexFile: join(stateRoot, "index.sqlite"),
42
+ };
43
+ }
44
+ export async function verifyMemomaticRoots(paths) {
45
+ await assertDirectory(paths.stateRoot);
46
+ await assertDirectory(paths.configRoot);
47
+ }
@@ -0,0 +1,11 @@
1
+ export type MemoryRules = {
2
+ neverSave: string[];
3
+ autoClean: {
4
+ olderThanDays: number;
5
+ scope: "episodic";
6
+ } | null;
7
+ };
8
+ export declare const emptyRules: () => MemoryRules;
9
+ export declare function parseRules(markdown: string): MemoryRules;
10
+ export declare function loadRules(rulesFile: string): Promise<MemoryRules>;
11
+ export declare function isForbidden(text: string, rules: MemoryRules): boolean;