@luisarg/memory-mcp 0.1.1 → 0.1.3
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/cordis.patch.yml +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/server/launcher.mjs +73 -0
- package/server/rebuild_index.py +191 -0
- package/server/requirements.txt +4 -0
- package/server/store.py +5 -2
package/cordis.patch.yml
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
config:
|
|
11
11
|
serverName: memory
|
|
12
12
|
transport: stdio
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# launcher.mjs (bundled with the server): `uv run` when uv is on PATH,
|
|
14
|
+
# else a pip-managed .venv (python3 -m venv + pip install -r requirements.txt)
|
|
15
|
+
command: !!js process.execPath
|
|
16
|
+
args: [!!js "(process.env.DSH_MEMORY_SERVER_DIR ?? dshHomePath('memory-vault-server')) + '/launcher.mjs'"]
|
|
15
17
|
env:
|
|
16
18
|
MEMORY_PATH: !!js process.env.DSH_MEMORY_PATH ?? dshHomePath('memory-vault')
|
|
17
19
|
UV_CACHE_DIR: /tmp/uv-cache
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;cAOa,IAAA;UAEI,MAAA;EAFJ,UAAI,EAAA,MAAA;EAEA,SAAM,EAAA,MAAA;AAKvB;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;cAOa,IAAA;UAEI,MAAA;EAFJ,UAAI,EAAA,MAAA;EAEA,SAAM,EAAA,MAAA;AAKvB;AAwCgB,cAxCH,MAwCc,EAxCN,MAwCuB,CAxChB,MAwCsB,CAAA;iBAAlC,KAAA,MAAW,iBAAiB"}
|
package/dist/index.js
CHANGED
|
@@ -30,11 +30,23 @@ function ensure(target, bundled, key) {
|
|
|
30
30
|
cpSync(bundled, target, { recursive: true });
|
|
31
31
|
return true;
|
|
32
32
|
}
|
|
33
|
+
/** Copy one bundled file into `target` when missing (upgrades add files 0.1.1 → 0.1.2). */
|
|
34
|
+
function ensureFile(target, bundled, file) {
|
|
35
|
+
const dest = join(target, file);
|
|
36
|
+
if (existsSync(dest)) return false;
|
|
37
|
+
const src = join(bundled, file);
|
|
38
|
+
if (!existsSync(src)) return false;
|
|
39
|
+
mkdirSync(target, { recursive: true });
|
|
40
|
+
cpSync(src, dest);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
33
43
|
function apply(ctx, config) {
|
|
34
44
|
const serverDir = resolveUnderHome(config.serverDir, "memory-vault-server");
|
|
35
45
|
const memoryPath = resolveUnderHome(config.memoryPath, "memory-vault");
|
|
36
46
|
if (ensure(serverDir, join(packageRoot, "server"), "server.py")) console.log(`[memory-mcp] installed memory-vault-server -> ${serverDir}`);
|
|
37
47
|
if (ensure(memoryPath, join(packageRoot, "vault"), "type-registry.yaml")) console.log(`[memory-mcp] installed vault starter -> ${memoryPath}`);
|
|
48
|
+
const bundled = join(packageRoot, "server");
|
|
49
|
+
for (const file of ["launcher.mjs", "requirements.txt"]) if (ensureFile(serverDir, bundled, file)) console.log(`[memory-mcp] installed ${file} -> ${serverDir}`);
|
|
38
50
|
if (!existsSync(join(serverDir, "server.py"))) console.warn(`[memory-mcp] memory-vault-server not found at ${serverDir} and not bundled — set DSH_MEMORY_SERVER_DIR (or run \`node scripts/bundle-assets.mjs\` in a checkout)`);
|
|
39
51
|
}
|
|
40
52
|
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["Config: Schema<Config>"],"sources":["../src/index.ts"],"sourcesContent":["import { cpSync, existsSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\n\nexport const name = 'memory-mcp'\n\nexport interface Config {\n memoryPath: string\n serverDir: string\n}\n\nexport const Config: Schema<Config> = Schema.object({\n memoryPath: Schema.string().default(process.env.DSH_MEMORY_PATH ?? ''),\n serverDir: Schema.string().default(process.env.DSH_MEMORY_SERVER_DIR ?? ''),\n})\n\nconst packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))\n\n/** Harness home, resolved like the harness itself ($DSH_HOME, or ~/.dsh). */\nfunction dshHome(): string {\n const env = process.env.DSH_HOME?.trim()\n return env && env.length > 0 ? env : join(homedir(), '.dsh')\n}\n\n/** Absolute paths stay; empty/relative values resolve under the harness home. */\nfunction resolveUnderHome(value: string, segment: string): string {\n const v = value.trim()\n if (v.length === 0) return join(dshHome(), segment)\n return isAbsolute(v) ? v : join(dshHome(), v)\n}\n\n/** Copy the bundled dir into `target` when `key` is missing there. */\nfunction ensure(target: string, bundled: string, key: string): boolean {\n if (existsSync(join(target, key))) return false\n if (!existsSync(bundled)) return false\n mkdirSync(target, { recursive: true })\n cpSync(bundled, target, { recursive: true })\n return true\n}\n\nexport function apply(ctx: Context, config: Config) {\n const serverDir = resolveUnderHome(config.serverDir, 'memory-vault-server')\n const memoryPath = resolveUnderHome(config.memoryPath, 'memory-vault')\n\n // Self-contained install: first boot copies the bundled server and vault\n // starter under the harness home when they are missing. Env-overridden\n // paths are respected (never overwritten, never copied over).\n if (ensure(serverDir, join(packageRoot, 'server'), 'server.py')) {\n console.log(`[memory-mcp] installed memory-vault-server -> ${serverDir}`)\n }\n if (ensure(memoryPath, join(packageRoot, 'vault'), 'type-registry.yaml')) {\n console.log(`[memory-mcp] installed vault starter -> ${memoryPath}`)\n }\n if (!existsSync(join(serverDir, 'server.py'))) {\n console.warn(\n `[memory-mcp] memory-vault-server not found at ${serverDir} and not bundled — ` +\n 'set DSH_MEMORY_SERVER_DIR (or run `node scripts/bundle-assets.mjs` in a checkout)',\n )\n }\n}\n"],"mappings":";;;;;;;AAOA,MAAa,OAAO;AAOpB,MAAaA,SAAyB,OAAO,OAAO;CAClD,YAAY,OAAO,QAAQ,CAAC,QAAQ,QAAQ,IAAI,mBAAmB,GAAG;CACtE,WAAW,OAAO,QAAQ,CAAC,QAAQ,QAAQ,IAAI,yBAAyB,GAAG;CAC5E,CAAC;AAEF,MAAM,cAAc,QAAQ,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,CAAC;;AAGpE,SAAS,UAAkB;CACzB,MAAM,MAAM,QAAQ,IAAI,UAAU,MAAM;AACxC,QAAO,OAAO,IAAI,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,OAAO;;;AAI9D,SAAS,iBAAiB,OAAe,SAAyB;CAChE,MAAM,IAAI,MAAM,MAAM;AACtB,KAAI,EAAE,WAAW,EAAG,QAAO,KAAK,SAAS,EAAE,QAAQ;AACnD,QAAO,WAAW,EAAE,GAAG,IAAI,KAAK,SAAS,EAAE,EAAE;;;AAI/C,SAAS,OAAO,QAAgB,SAAiB,KAAsB;AACrE,KAAI,WAAW,KAAK,QAAQ,IAAI,CAAC,CAAE,QAAO;AAC1C,KAAI,CAAC,WAAW,QAAQ,CAAE,QAAO;AACjC,WAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;AACtC,QAAO,SAAS,QAAQ,EAAE,WAAW,MAAM,CAAC;AAC5C,QAAO;;AAGT,SAAgB,MAAM,KAAc,QAAgB;CAClD,MAAM,YAAY,iBAAiB,OAAO,WAAW,sBAAsB;CAC3E,MAAM,aAAa,iBAAiB,OAAO,YAAY,eAAe;AAKtE,KAAI,OAAO,WAAW,KAAK,aAAa,SAAS,EAAE,YAAY,CAC7D,SAAQ,IAAI,iDAAiD,YAAY;AAE3E,KAAI,OAAO,YAAY,KAAK,aAAa,QAAQ,EAAE,qBAAqB,CACtE,SAAQ,IAAI,2CAA2C,aAAa;
|
|
1
|
+
{"version":3,"file":"index.js","names":["Config: Schema<Config>"],"sources":["../src/index.ts"],"sourcesContent":["import { cpSync, existsSync, mkdirSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\n\nexport const name = 'memory-mcp'\n\nexport interface Config {\n memoryPath: string\n serverDir: string\n}\n\nexport const Config: Schema<Config> = Schema.object({\n memoryPath: Schema.string().default(process.env.DSH_MEMORY_PATH ?? ''),\n serverDir: Schema.string().default(process.env.DSH_MEMORY_SERVER_DIR ?? ''),\n})\n\nconst packageRoot = dirname(dirname(fileURLToPath(import.meta.url)))\n\n/** Harness home, resolved like the harness itself ($DSH_HOME, or ~/.dsh). */\nfunction dshHome(): string {\n const env = process.env.DSH_HOME?.trim()\n return env && env.length > 0 ? env : join(homedir(), '.dsh')\n}\n\n/** Absolute paths stay; empty/relative values resolve under the harness home. */\nfunction resolveUnderHome(value: string, segment: string): string {\n const v = value.trim()\n if (v.length === 0) return join(dshHome(), segment)\n return isAbsolute(v) ? v : join(dshHome(), v)\n}\n\n/** Copy the bundled dir into `target` when `key` is missing there. */\nfunction ensure(target: string, bundled: string, key: string): boolean {\n if (existsSync(join(target, key))) return false\n if (!existsSync(bundled)) return false\n mkdirSync(target, { recursive: true })\n cpSync(bundled, target, { recursive: true })\n return true\n}\n\n/** Copy one bundled file into `target` when missing (upgrades add files 0.1.1 → 0.1.2). */\nfunction ensureFile(target: string, bundled: string, file: string): boolean {\n const dest = join(target, file)\n if (existsSync(dest)) return false\n const src = join(bundled, file)\n if (!existsSync(src)) return false\n mkdirSync(target, { recursive: true })\n cpSync(src, dest)\n return true\n}\n\nexport function apply(ctx: Context, config: Config) {\n const serverDir = resolveUnderHome(config.serverDir, 'memory-vault-server')\n const memoryPath = resolveUnderHome(config.memoryPath, 'memory-vault')\n\n // Self-contained install: first boot copies the bundled server and vault\n // starter under the harness home when they are missing. Env-overridden\n // paths are respected (never overwritten, never copied over).\n if (ensure(serverDir, join(packageRoot, 'server'), 'server.py')) {\n console.log(`[memory-mcp] installed memory-vault-server -> ${serverDir}`)\n }\n if (ensure(memoryPath, join(packageRoot, 'vault'), 'type-registry.yaml')) {\n console.log(`[memory-mcp] installed vault starter -> ${memoryPath}`)\n }\n // launcher.mjs runs the server via uv or the pip-venv fallback; upgrades of\n // existing installs (server.py already present) still need the new files.\n const bundled = join(packageRoot, 'server')\n for (const file of ['launcher.mjs', 'requirements.txt']) {\n if (ensureFile(serverDir, bundled, file)) {\n console.log(`[memory-mcp] installed ${file} -> ${serverDir}`)\n }\n }\n if (!existsSync(join(serverDir, 'server.py'))) {\n console.warn(\n `[memory-mcp] memory-vault-server not found at ${serverDir} and not bundled — ` +\n 'set DSH_MEMORY_SERVER_DIR (or run `node scripts/bundle-assets.mjs` in a checkout)',\n )\n }\n}\n"],"mappings":";;;;;;;AAOA,MAAa,OAAO;AAOpB,MAAaA,SAAyB,OAAO,OAAO;CAClD,YAAY,OAAO,QAAQ,CAAC,QAAQ,QAAQ,IAAI,mBAAmB,GAAG;CACtE,WAAW,OAAO,QAAQ,CAAC,QAAQ,QAAQ,IAAI,yBAAyB,GAAG;CAC5E,CAAC;AAEF,MAAM,cAAc,QAAQ,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,CAAC;;AAGpE,SAAS,UAAkB;CACzB,MAAM,MAAM,QAAQ,IAAI,UAAU,MAAM;AACxC,QAAO,OAAO,IAAI,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,OAAO;;;AAI9D,SAAS,iBAAiB,OAAe,SAAyB;CAChE,MAAM,IAAI,MAAM,MAAM;AACtB,KAAI,EAAE,WAAW,EAAG,QAAO,KAAK,SAAS,EAAE,QAAQ;AACnD,QAAO,WAAW,EAAE,GAAG,IAAI,KAAK,SAAS,EAAE,EAAE;;;AAI/C,SAAS,OAAO,QAAgB,SAAiB,KAAsB;AACrE,KAAI,WAAW,KAAK,QAAQ,IAAI,CAAC,CAAE,QAAO;AAC1C,KAAI,CAAC,WAAW,QAAQ,CAAE,QAAO;AACjC,WAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;AACtC,QAAO,SAAS,QAAQ,EAAE,WAAW,MAAM,CAAC;AAC5C,QAAO;;;AAIT,SAAS,WAAW,QAAgB,SAAiB,MAAuB;CAC1E,MAAM,OAAO,KAAK,QAAQ,KAAK;AAC/B,KAAI,WAAW,KAAK,CAAE,QAAO;CAC7B,MAAM,MAAM,KAAK,SAAS,KAAK;AAC/B,KAAI,CAAC,WAAW,IAAI,CAAE,QAAO;AAC7B,WAAU,QAAQ,EAAE,WAAW,MAAM,CAAC;AACtC,QAAO,KAAK,KAAK;AACjB,QAAO;;AAGT,SAAgB,MAAM,KAAc,QAAgB;CAClD,MAAM,YAAY,iBAAiB,OAAO,WAAW,sBAAsB;CAC3E,MAAM,aAAa,iBAAiB,OAAO,YAAY,eAAe;AAKtE,KAAI,OAAO,WAAW,KAAK,aAAa,SAAS,EAAE,YAAY,CAC7D,SAAQ,IAAI,iDAAiD,YAAY;AAE3E,KAAI,OAAO,YAAY,KAAK,aAAa,QAAQ,EAAE,qBAAqB,CACtE,SAAQ,IAAI,2CAA2C,aAAa;CAItE,MAAM,UAAU,KAAK,aAAa,SAAS;AAC3C,MAAK,MAAM,QAAQ,CAAC,gBAAgB,mBAAmB,CACrD,KAAI,WAAW,WAAW,SAAS,KAAK,CACtC,SAAQ,IAAI,0BAA0B,KAAK,MAAM,YAAY;AAGjE,KAAI,CAAC,WAAW,KAAK,WAAW,YAAY,CAAC,CAC3C,SAAQ,KACN,iDAAiD,UAAU,wGAE5D"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// memory-vault-server launcher — run `server.py` with uv when uv is on PATH,
|
|
2
|
+
// otherwise bootstrap a pip venv (.venv + requirements.txt) and run with that
|
|
3
|
+
// python. Used by both DSH launch points (memory-mcp cordis patch, memory-auto
|
|
4
|
+
// digest) so the fallback decision lives in exactly one place.
|
|
5
|
+
//
|
|
6
|
+
// stdout is the MCP channel of the spawned server — never print to it here.
|
|
7
|
+
// All progress/errors go to stderr.
|
|
8
|
+
//
|
|
9
|
+
// ponytail: fallback triggers only when the `uv` binary is absent, not when
|
|
10
|
+
// `uv run` fails (e.g. offline with an empty cache). If that ever matters,
|
|
11
|
+
// upgrade path: on uv run exit != 0, retry through the pip branch below.
|
|
12
|
+
|
|
13
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
14
|
+
import { existsSync } from 'node:fs'
|
|
15
|
+
import { tmpdir } from 'node:os'
|
|
16
|
+
import { dirname, join } from 'node:path'
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const DIR = dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
|
|
21
|
+
/** Pure decision: which command runs the server, given uv presence. */
|
|
22
|
+
export function resolveRunner(hasUv, dir = DIR, platform = process.platform) {
|
|
23
|
+
if (hasUv) return { command: 'uv', args: ['run', '--directory', dir, 'python', 'server.py'] }
|
|
24
|
+
const pyBin = join(dir, platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python')
|
|
25
|
+
return { command: pyBin, args: ['server.py'] }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function hasUv() {
|
|
29
|
+
return spawnSync('uv', ['--version'], { stdio: 'ignore' }).status === 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Ensure a runnable venv with the server's deps installed (pip branch only). */
|
|
33
|
+
export function ensurePipEnv(dir = DIR, platform = process.platform) {
|
|
34
|
+
const pyBin = join(dir, platform === 'win32' ? '.venv/Scripts/python.exe' : '.venv/bin/python')
|
|
35
|
+
const venvCmd = platform === 'win32' ? 'py' : 'python3'
|
|
36
|
+
if (!existsSync(pyBin)) {
|
|
37
|
+
console.error('[memory-vault-server] uv not found — creating fallback venv with ' + venvCmd)
|
|
38
|
+
// stdout is the MCP channel: keep install output off it.
|
|
39
|
+
const created = spawnSync(venvCmd, ['-m', 'venv', join(dir, '.venv')], { stdio: ['ignore', 'ignore', 'inherit'] })
|
|
40
|
+
if (created.status !== 0) {
|
|
41
|
+
console.error(`[memory-vault-server] venv creation failed (exit ${created.status}) — install uv or fix ${venvCmd}`)
|
|
42
|
+
process.exit(created.status ?? 1)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const depsOk = spawnSync(pyBin, ['-c', 'import mcp, yaml'], { stdio: 'ignore' }).status === 0
|
|
46
|
+
if (!depsOk) {
|
|
47
|
+
console.error('[memory-vault-server] fallback venv missing deps — pip install -r requirements.txt')
|
|
48
|
+
const pip = spawnSync(pyBin, ['-m', 'pip', 'install', '--quiet', '-r', join(dir, 'requirements.txt')], { stdio: ['ignore', 'ignore', 'inherit'] })
|
|
49
|
+
if (pip.status !== 0) {
|
|
50
|
+
console.error(`[memory-vault-server] pip install failed (exit ${pip.status}) — install uv, fix the network, or run the pip install manually`)
|
|
51
|
+
process.exit(pip.status ?? 1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return pyBin
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function main() {
|
|
58
|
+
const withUv = hasUv()
|
|
59
|
+
const { command, args } = resolveRunner(withUv)
|
|
60
|
+
if (!withUv) ensurePipEnv()
|
|
61
|
+
// Default the uv cache under the OS temp dir (Windows has no /tmp); an
|
|
62
|
+
// explicit UV_CACHE_DIR from the patch layer or env still wins.
|
|
63
|
+
const env = { UV_CACHE_DIR: join(tmpdir(), 'uv-cache'), ...process.env }
|
|
64
|
+
const child = spawn(command, args, { cwd: DIR, env, stdio: 'inherit' })
|
|
65
|
+
for (const sig of ['SIGTERM', 'SIGINT']) process.on(sig, () => child.kill(sig))
|
|
66
|
+
child.on('error', (err) => {
|
|
67
|
+
console.error(`[memory-vault-server] spawn failed (${command}): ${err.message}`)
|
|
68
|
+
process.exit(1)
|
|
69
|
+
})
|
|
70
|
+
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Rebuild the SQLite FTS5 index from the OKF Markdown bundle (index-only).
|
|
2
|
+
|
|
3
|
+
Walks `projects/<project>/<type>/*.md` and `raw/*.md`, parses each file and
|
|
4
|
+
inserts rows directly into SQLite. Markdown stays the source of truth: this
|
|
5
|
+
script never rewrites .md files (upsert_entry's write path would duplicate
|
|
6
|
+
them with today's date on a fresh DB).
|
|
7
|
+
|
|
8
|
+
Idempotent: row ids derive from file paths, dedup keys from content, so
|
|
9
|
+
re-running converges. Run after importing a bundle into a new vault dir
|
|
10
|
+
(memory.db* may be deleted first; the script also works on a populated DB):
|
|
11
|
+
|
|
12
|
+
uv run --directory memory-vault-server python rebuild_index.py \
|
|
13
|
+
--memory-path /path/to/vault
|
|
14
|
+
|
|
15
|
+
DB-only data (profiles) is NOT covered — migrate profiles separately.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import hashlib
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
_store_mod = None # set in main() once MEMORY_PATH points at the target bundle
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _id_for(relpath: Path) -> str:
|
|
30
|
+
"""Stable row id derived from the file path (idempotent re-runs)."""
|
|
31
|
+
digest = hashlib.sha1(str(relpath).encode()).hexdigest()[:24]
|
|
32
|
+
return f"rebuild-{digest}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _collect(memory_path: Path):
|
|
36
|
+
"""Walk the bundle -> (entries, errors); entries are
|
|
37
|
+
(singular, project, relpath, parsed) tuples, DB-only types skipped."""
|
|
38
|
+
entries: list[tuple[str, str, Path, dict]] = []
|
|
39
|
+
errors: list[str] = []
|
|
40
|
+
dir_to_singular = _store_mod._DIR_TO_SINGULAR
|
|
41
|
+
parse = _store_mod._parse_okf_file
|
|
42
|
+
|
|
43
|
+
def handle(path: Path, parsed: dict | None, default_project: str, singular: str):
|
|
44
|
+
if parsed is None:
|
|
45
|
+
errors.append(f"unparseable: {path.relative_to(memory_path)}")
|
|
46
|
+
return
|
|
47
|
+
# Defensive: trust the directory, not the parsed label.
|
|
48
|
+
parsed["entry_type"] = singular
|
|
49
|
+
parsed["project"] = parsed.get("project") or default_project
|
|
50
|
+
entries.append((singular, parsed["project"], path.relative_to(memory_path), parsed))
|
|
51
|
+
|
|
52
|
+
projects_dir = memory_path / "projects"
|
|
53
|
+
if projects_dir.is_dir():
|
|
54
|
+
# Projects may nest (e.g. `@deepseek-ai/dsh-root`): any directory whose
|
|
55
|
+
# name is a type dir is indexed; its project is the dir path below
|
|
56
|
+
# `projects/`, joined with "/".
|
|
57
|
+
def scan(prefix: Path, project: str):
|
|
58
|
+
for d in sorted(child for child in prefix.iterdir() if child.is_dir()):
|
|
59
|
+
singular = dir_to_singular.get(d.name)
|
|
60
|
+
if singular is None:
|
|
61
|
+
if d.name != ".obsidian":
|
|
62
|
+
scan(d, f"{project}/{d.name}" if project else d.name)
|
|
63
|
+
continue
|
|
64
|
+
if singular == "profile":
|
|
65
|
+
continue
|
|
66
|
+
for f in sorted(d.glob("*.md")):
|
|
67
|
+
if f.name == "index.md":
|
|
68
|
+
continue
|
|
69
|
+
handle(f, parse(f), project, singular)
|
|
70
|
+
|
|
71
|
+
for proj_dir in sorted(p for p in projects_dir.iterdir() if p.is_dir()):
|
|
72
|
+
scan(proj_dir, proj_dir.name)
|
|
73
|
+
|
|
74
|
+
raw_dir = memory_path / "raw"
|
|
75
|
+
if raw_dir.is_dir():
|
|
76
|
+
for f in sorted(raw_dir.glob("*.md")):
|
|
77
|
+
handle(f, parse(f), "", "source")
|
|
78
|
+
|
|
79
|
+
return entries, errors
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def rebuild(memory_path: Path) -> tuple[int, int, list[str]]:
|
|
83
|
+
"""Insert all bundle entries into the index -> (inserted, updated, errors)."""
|
|
84
|
+
store = _store_mod.MemoryStore(storage_path=memory_path)
|
|
85
|
+
store.initialize()
|
|
86
|
+
|
|
87
|
+
entries, errors = _collect(memory_path)
|
|
88
|
+
if not entries:
|
|
89
|
+
print("No entries found in bundle; nothing to do.", file=sys.stderr)
|
|
90
|
+
return 0, 0, errors
|
|
91
|
+
|
|
92
|
+
n_inserted = n_updated = 0
|
|
93
|
+
for singular, project, relpath, e in entries:
|
|
94
|
+
try:
|
|
95
|
+
dedup_key = (
|
|
96
|
+
store._make_dedup_key(singular, project, e["content"])
|
|
97
|
+
if singular != "source"
|
|
98
|
+
else _id_for(relpath)
|
|
99
|
+
)
|
|
100
|
+
existing = store.db.execute(
|
|
101
|
+
"SELECT id FROM entries WHERE dedup_key = ?", (dedup_key,)
|
|
102
|
+
).fetchone()
|
|
103
|
+
if existing:
|
|
104
|
+
if existing["id"] != _id_for(relpath):
|
|
105
|
+
# Same content already indexed (bundle duplicates collapse):
|
|
106
|
+
# adopt this file's id so re-runs converge, keep row content.
|
|
107
|
+
store.db.execute(
|
|
108
|
+
"UPDATE entries SET id = ?, created_at = ?, updated_at = ? WHERE dedup_key = ?",
|
|
109
|
+
(_id_for(relpath), e["created_at"], e["updated_at"], dedup_key),
|
|
110
|
+
)
|
|
111
|
+
store.db.commit()
|
|
112
|
+
n_updated += 1
|
|
113
|
+
continue
|
|
114
|
+
store._insert_row(
|
|
115
|
+
_id_for(relpath),
|
|
116
|
+
singular,
|
|
117
|
+
project,
|
|
118
|
+
e["content"],
|
|
119
|
+
e["tags"],
|
|
120
|
+
float(e["confidence"] or 1.0),
|
|
121
|
+
e["openspec_change_id"],
|
|
122
|
+
dedup_key,
|
|
123
|
+
e["created_at"],
|
|
124
|
+
e["updated_at"],
|
|
125
|
+
)
|
|
126
|
+
n_inserted += 1
|
|
127
|
+
except Exception as exc: # noqa: BLE001 — one bad file must not stop the rebuild
|
|
128
|
+
errors.append(f"failed: {project}/{relpath}: {exc}")
|
|
129
|
+
return n_inserted, n_updated, errors
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def regen_missing_indexes(memory_path: Path, store) -> int:
|
|
133
|
+
"""Regenerate `<project>/index.md` only where it is missing (nested too)."""
|
|
134
|
+
projects_dir = memory_path / "projects"
|
|
135
|
+
if not projects_dir.is_dir():
|
|
136
|
+
return 0
|
|
137
|
+
n = 0
|
|
138
|
+
|
|
139
|
+
def scan(prefix: Path):
|
|
140
|
+
for d in sorted(child for child in prefix.iterdir() if child.is_dir()):
|
|
141
|
+
if d.name in _store_mod._DIR_TO_SINGULAR or d.name == ".obsidian":
|
|
142
|
+
continue # type dir or editor cache — not a project
|
|
143
|
+
if not (d / "index.md").exists():
|
|
144
|
+
project = str(d.relative_to(projects_dir)).replace(os.sep, "/")
|
|
145
|
+
store._regenerate_project_index(project)
|
|
146
|
+
n += 1
|
|
147
|
+
scan(d)
|
|
148
|
+
|
|
149
|
+
for proj_dir in sorted(p for p in projects_dir.iterdir() if p.is_dir()):
|
|
150
|
+
if not (proj_dir / "index.md").exists():
|
|
151
|
+
store._regenerate_project_index(proj_dir.name)
|
|
152
|
+
n += 1
|
|
153
|
+
scan(proj_dir)
|
|
154
|
+
return n
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: list[str] | None = None) -> int:
|
|
158
|
+
global _store_mod
|
|
159
|
+
parser = argparse.ArgumentParser(description="Rebuild SQLite index from OKF bundle")
|
|
160
|
+
parser.add_argument(
|
|
161
|
+
"--memory-path",
|
|
162
|
+
default=None,
|
|
163
|
+
help="OKF bundle directory (default: $MEMORY_PATH or repo memory-vault)",
|
|
164
|
+
)
|
|
165
|
+
args = parser.parse_args(argv)
|
|
166
|
+
|
|
167
|
+
# store.py builds its type maps from MEMORY_PATH at import time — point it
|
|
168
|
+
# at the target bundle before importing.
|
|
169
|
+
if args.memory_path:
|
|
170
|
+
os.environ["MEMORY_PATH"] = str(Path(args.memory_path).resolve())
|
|
171
|
+
import store as _store_mod
|
|
172
|
+
|
|
173
|
+
memory_path = Path(os.environ.get("MEMORY_PATH", "")).resolve()
|
|
174
|
+
if not memory_path.is_dir():
|
|
175
|
+
print(f"error: {memory_path} is not a directory", file=sys.stderr)
|
|
176
|
+
return 2
|
|
177
|
+
|
|
178
|
+
print(f"Rebuilding index from {memory_path} ...")
|
|
179
|
+
n_ins, n_upd, errors = rebuild(memory_path)
|
|
180
|
+
store = _store_mod.MemoryStore(storage_path=memory_path)
|
|
181
|
+
store.initialize()
|
|
182
|
+
n_index = regen_missing_indexes(memory_path, store)
|
|
183
|
+
print(f"Inserted: {n_ins}, already-indexed: {n_upd}, index.md regenerated: {n_index}")
|
|
184
|
+
for err in errors:
|
|
185
|
+
print(f" {err}", file=sys.stderr)
|
|
186
|
+
print(f"Errors: {len(errors)}")
|
|
187
|
+
return 0 if not errors else 1
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
raise SystemExit(main())
|
package/server/store.py
CHANGED
|
@@ -263,7 +263,10 @@ class MemoryStore:
|
|
|
263
263
|
db_path = self.storage_path / "memory.db"
|
|
264
264
|
if db_path.exists() and db_path.stat().st_size > 0:
|
|
265
265
|
try:
|
|
266
|
-
|
|
266
|
+
# timeout=30: with WAL a live vault may be written by another
|
|
267
|
+
# client (DSH + opencode). Without it the probe waits only 5 s
|
|
268
|
+
# and a busy lock surfaces as "corrupt database".
|
|
269
|
+
test_conn = sqlite3.connect(str(db_path), timeout=30)
|
|
267
270
|
row = test_conn.execute("PRAGMA integrity_check").fetchone()
|
|
268
271
|
test_conn.close()
|
|
269
272
|
if row[0].lower() != "ok":
|
|
@@ -271,7 +274,7 @@ class MemoryStore:
|
|
|
271
274
|
except sqlite3.DatabaseError as e:
|
|
272
275
|
raise RuntimeError(f"corrupt database: {e}") from e
|
|
273
276
|
|
|
274
|
-
self._db = sqlite3.connect(str(db_path))
|
|
277
|
+
self._db = sqlite3.connect(str(db_path), timeout=30)
|
|
275
278
|
self._db.row_factory = sqlite3.Row
|
|
276
279
|
self.db.execute("PRAGMA journal_mode=WAL")
|
|
277
280
|
self.db.execute("PRAGMA foreign_keys=ON")
|