@sksoftofficial/mindroot 1.0.0

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/src/server.js ADDED
@@ -0,0 +1,192 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { timingSafeEqual } from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+ import { handleRpc } from "./mcp.js";
7
+ import { validSlug, ensureProject, getProjectRow } from "./indexer.js";
8
+ import * as core from "./core.js";
9
+
10
+ const DASHBOARD = path.join(path.dirname(fileURLToPath(import.meta.url)), "dashboard", "index.html");
11
+
12
+ export function createServer(db, cfg) {
13
+ return http.createServer(async (req, res) => {
14
+ try {
15
+ const url = new URL(req.url, `http://127.0.0.1:${cfg.port}`);
16
+ if (url.pathname.length > 1 && url.pathname.endsWith("/")) url.pathname = url.pathname.slice(0, -1);
17
+
18
+ if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/dashboard")) {
19
+ const html = await fs.readFile(DASHBOARD, "utf8");
20
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
21
+ res.end(html);
22
+ return;
23
+ }
24
+
25
+ if (url.pathname === "/health") {
26
+ sendJson(res, 200, { ok: true });
27
+ return;
28
+ }
29
+
30
+ if (!authorized(req, cfg)) {
31
+ sendJson(res, 401, {
32
+ error: "unauthorized",
33
+ hint: "the apiKey in ~/.mindroot/config.json changed — update the Authorization Bearer key in your MCP client config",
34
+ });
35
+ return;
36
+ }
37
+
38
+ if (url.pathname === "/mcp") {
39
+ if (req.method !== "POST") {
40
+ sendJson(res, 405, { error: "method not allowed" });
41
+ return;
42
+ }
43
+ const rpc = await readJson(req);
44
+ const out = await handleRpc(db, rpc);
45
+ if (out.body === null) {
46
+ res.writeHead(out.status);
47
+ res.end();
48
+ } else {
49
+ sendJson(res, out.status, out.body);
50
+ }
51
+ return;
52
+ }
53
+
54
+ if (url.pathname.startsWith("/api/")) {
55
+ await handleApi(req, res, url, db);
56
+ return;
57
+ }
58
+
59
+ sendJson(res, 404, { error: "not found" });
60
+ } catch (err) {
61
+ const status = err.status ?? 500;
62
+ sendJson(res, status, { error: String(err.message) });
63
+ }
64
+ });
65
+ }
66
+
67
+ async function handleApi(req, res, url, db) {
68
+ const parts = url.pathname.split("/").filter(Boolean);
69
+ if (parts[1] === "projects") {
70
+ const slug = parts[2];
71
+ if (!slug) {
72
+ if (req.method === "GET") return sendJson(res, 200, await core.listProjects(db));
73
+ if (req.method === "POST") {
74
+ const body = await readJson(req);
75
+ return sendJson(res, 201, ensureProject(db, body.slug));
76
+ }
77
+ throw methodNotAllowed();
78
+ }
79
+ if (!validSlug(slug)) throw badRequest("invalid project slug");
80
+
81
+ if (parts.length === 3) {
82
+ if (req.method === "GET") {
83
+ const project = getProjectRow(db, slug);
84
+ const [docs, memories] = await Promise.all([core.listDocs(db, slug), core.listMemories(db, slug)]);
85
+ return sendJson(res, 200, { slug, id: project.id, docs, memories });
86
+ }
87
+ if (req.method === "DELETE") return sendJson(res, 200, await core.deleteProject(db, slug));
88
+ throw methodNotAllowed();
89
+ }
90
+
91
+ if (parts[3] === "docs") {
92
+ if (req.method === "GET") return sendJson(res, 200, await core.listDocs(db, slug));
93
+ throw methodNotAllowed();
94
+ }
95
+ if (parts[3] === "doc") {
96
+ const relPath = url.searchParams.get("path");
97
+ if (!relPath) throw badRequest("path query param required");
98
+ if (req.method === "GET") return sendJson(res, 200, await core.readNote(db, slug, relPath));
99
+ if (req.method === "PUT") {
100
+ const body = await readJson(req);
101
+ return sendJson(res, 200, await core.saveNote(db, slug, relPath, body.content ?? ""));
102
+ }
103
+ if (req.method === "DELETE") return sendJson(res, 200, await core.deleteNote(db, slug, relPath));
104
+ throw methodNotAllowed();
105
+ }
106
+ if (parts[3] === "sections") {
107
+ if (req.method === "POST") {
108
+ const body = await readJson(req);
109
+ return sendJson(
110
+ res,
111
+ 200,
112
+ await core.updateSection(db, slug, body.path, body.heading_path, body.content),
113
+ );
114
+ }
115
+ throw methodNotAllowed();
116
+ }
117
+ if (parts[3] === "memories") {
118
+ if (req.method === "GET" && !parts[4]) return sendJson(res, 200, await core.listMemories(db, slug));
119
+ if (req.method === "POST") {
120
+ const body = await readJson(req);
121
+ return sendJson(res, 201, await core.addMemory(db, slug, body.text, body.target_path));
122
+ }
123
+ if (req.method === "DELETE" && parts[4]) {
124
+ return sendJson(res, 200, await core.deleteMemory(db, slug, Number(parts[4])));
125
+ }
126
+ throw methodNotAllowed();
127
+ }
128
+ throw notFound();
129
+ }
130
+
131
+ if (parts[1] === "search" && req.method === "POST") {
132
+ const body = await readJson(req);
133
+ const limit = body.limit ?? 8;
134
+ let hits;
135
+ if (body.kind === "memories") {
136
+ hits = await core.searchMemories(db, body.project, body.query, limit);
137
+ } else if (body.kind === "notes") {
138
+ hits = await core.searchNotes(db, body.project, body.query, limit);
139
+ } else {
140
+ const [memories, notes] = await Promise.all([
141
+ core.searchMemories(db, body.project, body.query, limit),
142
+ core.searchNotes(db, body.project, body.query, limit),
143
+ ]);
144
+ hits = [...memories, ...notes].sort((a, b) => b.score - a.score).slice(0, limit);
145
+ }
146
+ return sendJson(res, 200, hits);
147
+ }
148
+
149
+ throw notFound();
150
+ }
151
+
152
+ function authorized(req, cfg) {
153
+ const expected = `Bearer ${cfg.apiKey}`;
154
+ const got = req.headers.authorization ?? "";
155
+ if (got.length !== expected.length) return false;
156
+ return timingSafeEqual(Buffer.from(got), Buffer.from(expected));
157
+ }
158
+
159
+ function readJson(req) {
160
+ return new Promise((resolve, reject) => {
161
+ let size = 0;
162
+ const chunks = [];
163
+ req.on("data", (c) => {
164
+ size += c.length;
165
+ if (size > 10 * 1024 * 1024) {
166
+ reject(Object.assign(new Error("body too large"), { status: 413 }));
167
+ req.destroy();
168
+ return;
169
+ }
170
+ chunks.push(c);
171
+ });
172
+ req.on("end", () => {
173
+ if (!chunks.length) return resolve({});
174
+ try {
175
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
176
+ } catch {
177
+ reject(Object.assign(new Error("invalid json body"), { status: 400 }));
178
+ }
179
+ });
180
+ req.on("error", reject);
181
+ });
182
+ }
183
+
184
+ function sendJson(res, status, obj) {
185
+ const body = JSON.stringify(obj, null, 2);
186
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
187
+ res.end(body);
188
+ }
189
+
190
+ const badRequest = (m) => Object.assign(new Error(m), { status: 400 });
191
+ const notFound = () => Object.assign(new Error("not found"), { status: 404 });
192
+ const methodNotAllowed = () => Object.assign(new Error("method not allowed"), { status: 405 });
@@ -0,0 +1,107 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { DB_PATH } from "../paths.js";
3
+
4
+ const SCHEMA = `
5
+ CREATE TABLE IF NOT EXISTS projects (
6
+ id INTEGER PRIMARY KEY,
7
+ slug TEXT UNIQUE NOT NULL,
8
+ created_at INTEGER NOT NULL
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS docs (
12
+ id INTEGER PRIMARY KEY,
13
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
14
+ rel_path TEXT NOT NULL,
15
+ title TEXT NOT NULL,
16
+ content_hash TEXT,
17
+ mtime INTEGER NOT NULL,
18
+ indexed_at INTEGER NOT NULL,
19
+ UNIQUE(project_id, rel_path)
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS sections (
23
+ id INTEGER PRIMARY KEY,
24
+ doc_id INTEGER NOT NULL REFERENCES docs(id) ON DELETE CASCADE,
25
+ heading_path TEXT NOT NULL,
26
+ content TEXT NOT NULL,
27
+ updated_at INTEGER NOT NULL,
28
+ UNIQUE(doc_id, heading_path)
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS memories (
32
+ id INTEGER PRIMARY KEY,
33
+ project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
34
+ text TEXT NOT NULL,
35
+ target_type TEXT NOT NULL CHECK (target_type IN ('standalone','doc','section')),
36
+ target_path TEXT,
37
+ created_at INTEGER NOT NULL,
38
+ updated_at INTEGER NOT NULL
39
+ );
40
+
41
+ CREATE TABLE IF NOT EXISTS embeddings (
42
+ id INTEGER PRIMARY KEY,
43
+ owner_type TEXT NOT NULL CHECK (owner_type IN ('memory','section')),
44
+ owner_id INTEGER NOT NULL,
45
+ dim INTEGER NOT NULL,
46
+ model TEXT NOT NULL,
47
+ vec BLOB NOT NULL,
48
+ UNIQUE(owner_type, owner_id)
49
+ );
50
+
51
+ CREATE TABLE IF NOT EXISTS project_embeddings (
52
+ project_id INTEGER PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
53
+ dim INTEGER NOT NULL,
54
+ model TEXT NOT NULL,
55
+ vec BLOB NOT NULL
56
+ );
57
+
58
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
59
+ text,
60
+ content='memories',
61
+ content_rowid='id'
62
+ );
63
+
64
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
65
+ INSERT INTO memories_fts(rowid, text) VALUES (new.id, new.text);
66
+ END;
67
+
68
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
69
+ INSERT INTO memories_fts(memories_fts, rowid, text) VALUES ('delete', old.id, old.text);
70
+ END;
71
+
72
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
73
+ INSERT INTO memories_fts(memories_fts, rowid, text) VALUES ('delete', old.id, old.text);
74
+ INSERT INTO memories_fts(rowid, text) VALUES (new.id, new.text);
75
+ END;
76
+
77
+ CREATE TRIGGER IF NOT EXISTS memories_embeddings_ad AFTER DELETE ON memories BEGIN
78
+ DELETE FROM embeddings WHERE owner_type = 'memory' AND owner_id = old.id;
79
+ END;
80
+
81
+ CREATE TRIGGER IF NOT EXISTS sections_embeddings_ad AFTER DELETE ON sections BEGIN
82
+ DELETE FROM embeddings WHERE owner_type = 'section' AND owner_id = old.id;
83
+ END;
84
+ `;
85
+
86
+ function migrate(db) {
87
+ const hasDocs =
88
+ db.prepare("SELECT COUNT(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'docs'").get().n > 0;
89
+ if (!hasDocs) return;
90
+ const docCols = db.prepare("PRAGMA table_info(docs)").all().map((c) => c.name);
91
+ if (!docCols.includes("content_hash")) {
92
+ db.exec("ALTER TABLE docs ADD COLUMN content_hash TEXT");
93
+ }
94
+ }
95
+
96
+ export function open() {
97
+ const db = new DatabaseSync(DB_PATH);
98
+ db.exec("PRAGMA journal_mode = WAL");
99
+ db.exec("PRAGMA foreign_keys = ON");
100
+ ensureSchema(db);
101
+ return db;
102
+ }
103
+
104
+ export function ensureSchema(db) {
105
+ db.exec(SCHEMA);
106
+ migrate(db);
107
+ }
@@ -0,0 +1,32 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseMarkdown, docTitle, replaceSectionBody } from "../src/markdown.js";
4
+
5
+ test("parseMarkdown builds nested heading paths and excludes H1", () => {
6
+ const md = "# Title\n\nintro\n\n## Arch\n\ntop\n\n### Arch::Storage\n\nnested\n";
7
+ const parsed = parseMarkdown(md);
8
+ const paths = parsed.sections.map((s) => s.path);
9
+ assert.deepEqual(paths, ["", "Arch", "Arch::Arch::Storage"]);
10
+ });
11
+
12
+ test("parseMarkdown detects frontmatter", () => {
13
+ const parsed = parseMarkdown("---\ntitle: My Doc\n---\n\nbody");
14
+ assert.equal(parsed.frontmatter, "---\ntitle: My Doc\n---");
15
+ assert.equal(docTitle(parsed, "fallback.md"), "My Doc");
16
+ });
17
+
18
+ test("docTitle falls back to H1 then filename", () => {
19
+ assert.equal(docTitle(parseMarkdown("# H\n"), "x.md"), "H");
20
+ assert.equal(docTitle(parseMarkdown("no heads"), "x.md"), "x");
21
+ });
22
+
23
+ test("replaceSectionBody splices only the target section", () => {
24
+ const md = "# T\n\n## A\n\nold body\n\n## B\n\nkeep me\n";
25
+ const parsed = parseMarkdown(md);
26
+ const secA = parsed.sections.find((s) => s.path === "A");
27
+ const out = replaceSectionBody(md, secA, "new body");
28
+ assert.match(out, /new body/);
29
+ assert.doesNotMatch(out, /old body/);
30
+ assert.match(out, /keep me/);
31
+ assert.match(out, /^# T/);
32
+ });
@@ -0,0 +1,74 @@
1
+ import { test, before } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ let handleRpc;
8
+ let tools;
9
+ let db;
10
+ let open;
11
+
12
+ const EXPECTED_TOOLS = [
13
+ "mindroot_list_projects",
14
+ "mindroot_search_projects",
15
+ "mindroot_search_memories",
16
+ "mindroot_search_notes",
17
+ "mindroot_save_memory",
18
+ "mindroot_delete_memory",
19
+ "mindroot_list_notes",
20
+ "mindroot_read_note",
21
+ "mindroot_save_note",
22
+ "mindroot_update_section",
23
+ "mindroot_delete_note",
24
+ ];
25
+
26
+ before(async () => {
27
+ process.env.MINDROOT_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "mindroot-mcp-"));
28
+ ({ handleRpc, tools } = await import("../src/mcp.js"));
29
+ ({ open } = await import("../src/store/db.js"));
30
+ db = open();
31
+ });
32
+
33
+ function rpc(method, params) {
34
+ return handleRpc(db, { jsonrpc: "2.0", id: 1, method, params });
35
+ }
36
+
37
+ const call = (name, args) => rpc("tools/call", { name, arguments: args });
38
+
39
+ test("tools/list exposes exactly the expected surface", async () => {
40
+ const res = await rpc("tools/list", {});
41
+ assert.deepEqual(res.body.result.tools.map((t) => t.name).sort(), [...EXPECTED_TOOLS].sort());
42
+ for (const t of res.body.result.tools) {
43
+ assert.ok(t.description.length > 20, `${t.name} needs a description`);
44
+ }
45
+ });
46
+
47
+ test("notifications get 202 with no body", async () => {
48
+ const res = await handleRpc(db, { jsonrpc: "2.0", method: "notify/x" });
49
+ assert.equal(res.status, 202);
50
+ assert.equal(res.body, null);
51
+ });
52
+
53
+ test("unknown tool returns clean error", async () => {
54
+ const res = await call("mindroot_nope", {});
55
+ assert.equal(res.body.result.isError, true);
56
+ assert.match(res.body.result.content[0].text, /unknown tool/);
57
+ });
58
+
59
+ test("search without project is a clean isError", async () => {
60
+ const res = await call("mindroot_search_memories", { query: "x" });
61
+ assert.equal(res.body.result.isError, true);
62
+ assert.match(res.body.result.content[0].text, /project is required/);
63
+ });
64
+
65
+ test("invalid slug is rejected before any lookup", async () => {
66
+ const res = await call("mindroot_search_memories", { project: "../evil", query: "x" });
67
+ assert.equal(res.body.result.isError, true);
68
+ assert.match(res.body.result.content[0].text, /invalid project slug/);
69
+ });
70
+
71
+ test("unknown project is a clean error", async () => {
72
+ const res = await call("mindroot_search_memories", { project: "ghost", query: "x" });
73
+ assert.match(res.body.result.content[0].text, /unknown project: ghost/);
74
+ });
@@ -0,0 +1,87 @@
1
+ import { test, before } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ let open;
8
+
9
+ function vec(arr) {
10
+ const f = new Float32Array(arr);
11
+ return Buffer.from(f.buffer, f.byteOffset, f.byteLength);
12
+ }
13
+
14
+ async function freshDb(name) {
15
+ process.env.MINDROOT_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "mindroot-search-"));
16
+ ({ open } = await import("../src/store/db.js"));
17
+ return open();
18
+ }
19
+
20
+ before(freshDb);
21
+
22
+ test("rankMemories blends vector and keyword scores, respects limit", async () => {
23
+ const { rankMemories } = await import("../src/search.js");
24
+ const db = open();
25
+ db.prepare("INSERT INTO projects (slug, created_at) VALUES ('p1', 1)").run();
26
+ const pid = db.prepare("SELECT id FROM projects WHERE slug='p1'").get().id;
27
+
28
+ const ins = db.prepare(
29
+ "INSERT INTO memories (project_id, text, target_type, created_at, updated_at) VALUES (?, ?, 'standalone', 1, 1)",
30
+ );
31
+ const m1 = Number(ins.run(pid, "deploy by tag push to production").lastInsertRowid);
32
+ const m2 = Number(ins.run(pid, "unrelated note about cooking pasta").lastInsertRowid);
33
+ const emb = db.prepare(
34
+ "INSERT INTO embeddings (owner_type, owner_id, dim, model, vec) VALUES ('memory', ?, ?, 'm', ?)",
35
+ );
36
+ emb.run(m1, 2, vec([1, 0]));
37
+ emb.run(m2, 2, vec([0, 1]));
38
+
39
+ // query vector aligned with m1; keywords also match m1
40
+ const hits = rankMemories(db, pid, new Float32Array([1, 0]), "deploy production", 8);
41
+ assert.equal(hits[0].id, m1);
42
+ assert.ok(hits[0].score > 0.75);
43
+ assert.equal(hits.length, 2);
44
+
45
+ assert.equal(rankMemories(db, pid, new Float32Array([1, 0]), "x", 1).length, 1);
46
+
47
+ // embeddings with wrong dim are skipped, not crashed on
48
+ emb.run(Number(ins.run(pid, "dim mismatch").lastInsertRowid), 3, vec([1, 0, 0]));
49
+ assert.equal(rankMemories(db, pid, new Float32Array([1, 0]), "x", 10).some((h) => h.text === "dim mismatch"), false);
50
+ });
51
+
52
+ test("rankNotes ranks sections scoped to the project", async () => {
53
+ const { rankNotes } = await import("../src/search.js");
54
+ const db = open();
55
+ db.prepare("INSERT INTO projects (slug, created_at) VALUES ('p2', 2)").run();
56
+ const pid = db.prepare("SELECT id FROM projects WHERE slug='p2'").get().id;
57
+ db.prepare(
58
+ "INSERT INTO docs (project_id, rel_path, title, content_hash, mtime, indexed_at) VALUES (?, 'a.md', 'A', NULL, 1, 1)",
59
+ ).run(pid);
60
+ const docId = db.prepare("SELECT id FROM docs").get().id;
61
+ db.prepare(
62
+ "INSERT INTO sections (doc_id, heading_path, content, updated_at) VALUES (?, 'Deploy', 'tag push deploys', 1)",
63
+ ).run(docId);
64
+ const secId = db.prepare("SELECT id FROM sections").get().id;
65
+ db.prepare(
66
+ "INSERT INTO embeddings (owner_type, owner_id, dim, model, vec) VALUES ('section', ?, ?, 'm', ?)",
67
+ ).run(secId, 2, vec([0.9, 0.1]));
68
+
69
+ const hits = rankNotes(db, pid, new Float32Array([0.9, 0.1]), 8);
70
+ assert.equal(hits[0].kind, "note_section");
71
+ assert.equal(hits[0].target, "a.md::Deploy");
72
+ });
73
+
74
+ test("rankProjects applies token-overlap bonus and threshold", async () => {
75
+ const { rankProjects } = await import("../src/search.js");
76
+ const db = open();
77
+ db.prepare("INSERT INTO projects (slug, created_at) VALUES ('proper-agent-memory', 3)").run();
78
+ db.prepare("INSERT INTO projects (slug, created_at) VALUES ('zzz', 3)").run();
79
+
80
+ const hits = rankProjects(db, new Float32Array([1, 0]), "mem-agent-mcp");
81
+ const top = hits.find((h) => h.project === "proper-agent-memory");
82
+ assert.ok(top, "fuzzy name should match");
83
+ assert.ok(Math.abs(top.score - (0.55 + 0.4 * (2 / 3))) < 1e-9);
84
+
85
+ // no token overlap + no vector => excluded
86
+ assert.equal(hits.some((h) => h.project === "zzz"), false);
87
+ });
@@ -0,0 +1,34 @@
1
+ import { test, before } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ let dir;
8
+
9
+ before(async () => {
10
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "mindroot-store-"));
11
+ process.env.MINDROOT_DIR = dir;
12
+ });
13
+
14
+ test("open() creates the full schema and is idempotent", async () => {
15
+ const { open } = await import("../src/store/db.js");
16
+ const db = open();
17
+
18
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name").all().map((r) => r.name);
19
+ for (const t of ["projects", "docs", "sections", "memories", "embeddings", "project_embeddings"]) {
20
+ assert.ok(tables.includes(t), `missing table ${t}`);
21
+ }
22
+ assert.ok(tables.includes("memories_fts"));
23
+
24
+ db.prepare("INSERT INTO projects (slug, created_at) VALUES ('p1', 1)").run();
25
+ db.prepare(
26
+ "INSERT INTO memories (project_id, text, target_type, created_at, updated_at) VALUES (1, 'fts indexed', 'standalone', 1, 1)",
27
+ ).run();
28
+ const hits = db.prepare("SELECT rowid FROM memories_fts WHERE memories_fts MATCH 'indexed'").all();
29
+ assert.equal(hits.length, 1);
30
+
31
+ db.close();
32
+ const db2 = open();
33
+ assert.equal(db2.prepare("SELECT COUNT(*) n FROM memories").get().n, 1);
34
+ });