@luisarg/memory-mcp 0.1.4 → 0.1.6
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 +24 -4
- package/cordis.patch.yml +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -4
- package/server/cli.py +8 -1
- package/server/rebuild_index.py +3 -2
- package/skills/brain/SKILL.md +76 -0
- package/skills/checkpoint/SKILL.md +75 -0
package/README.md
CHANGED
|
@@ -14,18 +14,38 @@ vault's tools (`search_memory`, `store_*`, `export_memories`, `get_profile`,
|
|
|
14
14
|
dsh plugin --profile web add @luisarg/memory-mcp
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
`uv` on PATH is recommended but not required: the bundled `launcher.mjs` runs the server with
|
|
18
|
+
`uv run` when uv is present and falls back to a pip-managed venv (`python3 -m venv` +
|
|
19
|
+
`pip install -r requirements.txt`, network on first boot) when it is not.
|
|
20
|
+
|
|
21
|
+
## Skills
|
|
22
|
+
|
|
23
|
+
The package also ships the two skills that drive its tools, so installing it is enough to get the
|
|
24
|
+
commands — nothing has to be copied into a skill directory:
|
|
25
|
+
|
|
26
|
+
| Command | Source | What it does |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `/brain` | `skills/brain/SKILL.md` | Reads the vault: topic search, recall by project, the profile, full exports — including the tool traps (`entry_type=profile` in `search_memory` always returns 0; `tags` are OR-matched; searches cap at 50) |
|
|
29
|
+
| `/checkpoint` | `skills/checkpoint/SKILL.md` | Captures the session as `decision`/`fact`/`learning`/`convention` entries, then commits the vault |
|
|
30
|
+
|
|
31
|
+
They register at DSH's **bundled** rank (600), the weakest in the local discovery table, so a
|
|
32
|
+
skill of the same name in a user or project root still wins. Each `SKILL.md` is the single source
|
|
33
|
+
of its own name, description and usage guidance: the provider parses its frontmatter with `yaml`,
|
|
34
|
+
so the same file also works copied into `~/.agents/skills`. Registration goes through
|
|
35
|
+
`ctx.inject(['skills'], …)`, so a deployment with no skill catalog still gets the vault
|
|
36
|
+
bootstrap and the MCP client.
|
|
18
37
|
|
|
19
38
|
## Configuration
|
|
20
39
|
|
|
21
40
|
| Variable | Purpose | Default |
|
|
22
41
|
|---|---|---|
|
|
23
42
|
| `DSH_MEMORY_SERVER_DIR` | directory containing `server.py` | `$DSH_HOME/memory-vault-server` |
|
|
24
|
-
| `DSH_MEMORY_PATH` | vault directory (forwarded to the server as `MEMORY_PATH`) |
|
|
43
|
+
| `DSH_MEMORY_PATH` | vault directory (forwarded to the server as `MEMORY_PATH`) | `~/.memories` |
|
|
25
44
|
|
|
26
45
|
The bundle ships no cwd-dependent paths: the `cordis.patch.yml` layer resolves both
|
|
27
|
-
values from the environment
|
|
28
|
-
|
|
46
|
+
values from the environment. The vault falls back to the OS home (`~/.memories`, which
|
|
47
|
+
`DSH_HOME` does not move); the server to the harness home (`$DSH_HOME`, or `~/.dsh`) —
|
|
48
|
+
DSH does not chdir, so launching from any directory works.
|
|
29
49
|
|
|
30
50
|
## Development
|
|
31
51
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# bundle @luisarg/memory-mcp — MCP stdio wrapper for memory-vault
|
|
2
2
|
# overrides vía env: DSH_MEMORY_PATH, DSH_MEMORY_SERVER_DIR
|
|
3
|
-
#
|
|
4
|
-
#
|
|
3
|
+
# cwd-independent:
|
|
4
|
+
# vault -> ~/.memories (OS home, ignores DSH_HOME)
|
|
5
|
+
# server -> $DSH_HOME/memory-vault-server (~/.dsh por defecto)
|
|
5
6
|
- insert:
|
|
6
7
|
- id: memory-mcp-bootstrap
|
|
7
8
|
name: '@luisarg/memory-mcp'
|
|
@@ -15,6 +16,6 @@
|
|
|
15
16
|
command: !!js process.execPath
|
|
16
17
|
args: [!!js "(process.env.DSH_MEMORY_SERVER_DIR ?? dshHomePath('memory-vault-server')) + '/launcher.mjs'"]
|
|
17
18
|
env:
|
|
18
|
-
MEMORY_PATH: !!js process.env.DSH_MEMORY_PATH ??
|
|
19
|
+
MEMORY_PATH: !!js process.env.DSH_MEMORY_PATH ?? (process.env.HOME || process.env.USERPROFILE || '') + '/.memories'
|
|
19
20
|
UV_CACHE_DIR: /tmp/uv-cache
|
|
20
21
|
cwd: !!js process.env.DSH_MEMORY_SERVER_DIR ?? dshHomePath('memory-vault-server')
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;qBAQa;iBAEI;EACf;EACA;;qBAGW,QAAQ,OAAO;wBA+CZ,MAAM,KAAK,SAAS,QAAQ"}
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,109 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import Schema from "@deepseek-ai/schemastery";
|
|
6
|
+
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
import { BUNDLED_SKILL_RANK } from "@deepseek-ai/dsh-skill";
|
|
9
|
+
//#region src/skills.ts
|
|
10
|
+
/**
|
|
11
|
+
* Bundled skills shipped in this package: `brain` (read the vault) and
|
|
12
|
+
* `checkpoint` (capture a session into it).
|
|
13
|
+
*
|
|
14
|
+
* A provider rather than `ctx.skills.register()` on purpose. A registration
|
|
15
|
+
* lands at the runtime rank, which outranks a user's own skill directories,
|
|
16
|
+
* while BUNDLED_SKILL_RANK is the weakest rank in the local discovery table:
|
|
17
|
+
* shipping at the weakest rank means a user who drops their own `brain` into
|
|
18
|
+
* `~/.agents/skills` keeps winning the name. These are defaults, not a takeover.
|
|
19
|
+
*
|
|
20
|
+
* Each SKILL.md stays the single source of its own name, description and
|
|
21
|
+
* usage guidance — the frontmatter is parsed here with the same `yaml`
|
|
22
|
+
* dependency the harness's own filesystem provider uses, so the exact file that
|
|
23
|
+
* ships in this package also works copied into a user skill root.
|
|
24
|
+
*/
|
|
25
|
+
/** Provider name registered on `ctx.skills`. */
|
|
26
|
+
const SKILLS_PROVIDER = "memory-mcp-skills";
|
|
27
|
+
/** Shipped skill directories, relative to the package root. */
|
|
28
|
+
const SKILL_NAMES = ["brain", "checkpoint"];
|
|
29
|
+
const SKILLS_ROOT = new URL("../skills/", import.meta.url);
|
|
30
|
+
const INVOCATION = {
|
|
31
|
+
modelInvocable: true,
|
|
32
|
+
userInvocable: true
|
|
33
|
+
};
|
|
34
|
+
function skillUrl(name) {
|
|
35
|
+
return new URL(`${name}/SKILL.md`, SKILLS_ROOT);
|
|
36
|
+
}
|
|
37
|
+
function resourceBase(name) {
|
|
38
|
+
return {
|
|
39
|
+
kind: "directory",
|
|
40
|
+
path: fileURLToPath(new URL(`${name}/`, SKILLS_ROOT))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Split YAML frontmatter from the body, mirroring the harness filesystem provider. */
|
|
44
|
+
function splitFrontmatter(raw) {
|
|
45
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(raw);
|
|
46
|
+
if (match === null) throw new Error("SKILL.md has no YAML frontmatter");
|
|
47
|
+
const parsed = parse(match[1]);
|
|
48
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new TypeError("SKILL.md frontmatter must be a YAML mapping");
|
|
49
|
+
return {
|
|
50
|
+
data: parsed,
|
|
51
|
+
body: raw.slice(match[0].length).trim()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function textField(data, key) {
|
|
55
|
+
const value = data[key];
|
|
56
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
57
|
+
}
|
|
58
|
+
/** Read one shipped skill, rejecting a file whose frontmatter disagrees with its directory. */
|
|
59
|
+
async function loadSkill(name) {
|
|
60
|
+
const { data, body } = splitFrontmatter(await readFile(skillUrl(name), "utf8"));
|
|
61
|
+
const declared = textField(data, "name");
|
|
62
|
+
if (declared !== name) throw new Error(`skills/${name}/SKILL.md declares name "${declared ?? "(none)"}"`);
|
|
63
|
+
const description = textField(data, "description");
|
|
64
|
+
if (description === void 0) throw new Error(`skills/${name}/SKILL.md has no description`);
|
|
65
|
+
const whenToUse = textField(data, "whenToUse");
|
|
66
|
+
return {
|
|
67
|
+
frontmatter: {
|
|
68
|
+
name,
|
|
69
|
+
description,
|
|
70
|
+
...whenToUse === void 0 ? {} : { whenToUse }
|
|
71
|
+
},
|
|
72
|
+
body
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Skills shipped as packaged Markdown assets. */
|
|
76
|
+
const skillsProvider = {
|
|
77
|
+
name: SKILLS_PROVIDER,
|
|
78
|
+
async list() {
|
|
79
|
+
return await Promise.all(SKILL_NAMES.map(async (name) => {
|
|
80
|
+
const { frontmatter } = await loadSkill(name);
|
|
81
|
+
return {
|
|
82
|
+
...frontmatter,
|
|
83
|
+
path: fileURLToPath(skillUrl(name)),
|
|
84
|
+
invocation: INVOCATION,
|
|
85
|
+
source: "bundled",
|
|
86
|
+
provider: SKILLS_PROVIDER,
|
|
87
|
+
resourceBase: resourceBase(name),
|
|
88
|
+
rank: BUNDLED_SKILL_RANK,
|
|
89
|
+
locator: skillUrl(name)
|
|
90
|
+
};
|
|
91
|
+
}));
|
|
92
|
+
},
|
|
93
|
+
async get(candidate) {
|
|
94
|
+
const loaded = await loadSkill(candidate.name).catch(() => void 0);
|
|
95
|
+
if (loaded === void 0) return void 0;
|
|
96
|
+
const { frontmatter, body } = loaded;
|
|
97
|
+
return {
|
|
98
|
+
...frontmatter,
|
|
99
|
+
path: fileURLToPath(skillUrl(candidate.name)),
|
|
100
|
+
invocation: INVOCATION,
|
|
101
|
+
source: "bundled",
|
|
102
|
+
provider: SKILLS_PROVIDER,
|
|
103
|
+
resourceBase: resourceBase(candidate.name),
|
|
104
|
+
content: body
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
6
109
|
//#region src/index.ts
|
|
7
110
|
const name = "memory-mcp";
|
|
8
111
|
const Config = Schema.object({
|
|
@@ -21,6 +124,12 @@ function resolveUnderHome(value, segment) {
|
|
|
21
124
|
if (v.length === 0) return join(dshHome(), segment);
|
|
22
125
|
return isAbsolute(v) ? v : join(dshHome(), v);
|
|
23
126
|
}
|
|
127
|
+
/** Vault root: `~/.memories` by default, independent of `$DSH_HOME`. */
|
|
128
|
+
function resolveMemoryPath(value) {
|
|
129
|
+
const v = value.trim();
|
|
130
|
+
if (v.length === 0) return join(homedir(), ".memories");
|
|
131
|
+
return isAbsolute(v) ? v : join(homedir(), v);
|
|
132
|
+
}
|
|
24
133
|
/** Copy the bundled dir into `target` when `key` is missing there. */
|
|
25
134
|
function ensure(target, bundled, key) {
|
|
26
135
|
if (existsSync(join(target, key))) return false;
|
|
@@ -40,8 +149,11 @@ function ensureFile(target, bundled, file) {
|
|
|
40
149
|
return true;
|
|
41
150
|
}
|
|
42
151
|
function apply(ctx, config) {
|
|
152
|
+
ctx.inject(["skills"], (ctx) => {
|
|
153
|
+
ctx.skills.registerProvider(() => skillsProvider);
|
|
154
|
+
});
|
|
43
155
|
const serverDir = resolveUnderHome(config.serverDir, "memory-vault-server");
|
|
44
|
-
const memoryPath =
|
|
156
|
+
const memoryPath = resolveMemoryPath(config.memoryPath);
|
|
45
157
|
if (ensure(serverDir, join(packageRoot, "server"), "server.py")) console.log(`[memory-mcp] installed memory-vault-server -> ${serverDir}`);
|
|
46
158
|
if (ensure(memoryPath, join(packageRoot, "vault"), "type-registry.yaml")) console.log(`[memory-mcp] installed vault starter -> ${memoryPath}`);
|
|
47
159
|
const bundled = join(packageRoot, "server");
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"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,MAAa,SAAyB,OAAO,OAAO;CAClD,YAAY,OAAO,OAAO,CAAC,CAAC,QAAQ,QAAQ,IAAI,mBAAmB,EAAE;CACrE,WAAW,OAAO,OAAO,CAAC,CAAC,QAAQ,QAAQ,IAAI,yBAAyB,EAAE;AAC5E,CAAC;AAED,MAAM,cAAc,QAAQ,QAAQ,cAAc,YAAY,GAAG,CAAC,CAAC;;AAGnE,SAAS,UAAkB;CACzB,MAAM,MAAM,QAAQ,IAAI,UAAU,KAAK;CACvC,OAAO,OAAO,IAAI,SAAS,IAAI,MAAM,KAAK,QAAQ,GAAG,MAAM;AAC7D;;AAGA,SAAS,iBAAiB,OAAe,SAAyB;CAChE,MAAM,IAAI,MAAM,KAAK;CACrB,IAAI,EAAE,WAAW,GAAG,OAAO,KAAK,QAAQ,GAAG,OAAO;CAClD,OAAO,WAAW,CAAC,IAAI,IAAI,KAAK,QAAQ,GAAG,CAAC;AAC9C;;AAGA,SAAS,OAAO,QAAgB,SAAiB,KAAsB;CACrE,IAAI,WAAW,KAAK,QAAQ,GAAG,CAAC,GAAG,OAAO;CAC1C,IAAI,CAAC,WAAW,OAAO,GAAG,OAAO;CACjC,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,OAAO,SAAS,QAAQ,EAAE,WAAW,KAAK,CAAC;CAC3C,OAAO;AACT;;AAGA,SAAS,WAAW,QAAgB,SAAiB,MAAuB;CAC1E,MAAM,OAAO,KAAK,QAAQ,IAAI;CAC9B,IAAI,WAAW,IAAI,GAAG,OAAO;CAC7B,MAAM,MAAM,KAAK,SAAS,IAAI;CAC9B,IAAI,CAAC,WAAW,GAAG,GAAG,OAAO;CAC7B,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;CACrC,OAAO,KAAK,IAAI;CAChB,OAAO;AACT;AAEA,SAAgB,MAAM,KAAc,QAAgB;CAClD,MAAM,YAAY,iBAAiB,OAAO,WAAW,qBAAqB;CAC1E,MAAM,aAAa,iBAAiB,OAAO,YAAY,cAAc;CAKrE,IAAI,OAAO,WAAW,KAAK,aAAa,QAAQ,GAAG,WAAW,GAC5D,QAAQ,IAAI,iDAAiD,WAAW;CAE1E,IAAI,OAAO,YAAY,KAAK,aAAa,OAAO,GAAG,oBAAoB,GACrE,QAAQ,IAAI,2CAA2C,YAAY;CAIrE,MAAM,UAAU,KAAK,aAAa,QAAQ;CAC1C,KAAK,MAAM,QAAQ,CAAC,gBAAgB,kBAAkB,GACpD,IAAI,WAAW,WAAW,SAAS,IAAI,GACrC,QAAQ,IAAI,0BAA0B,KAAK,MAAM,WAAW;CAGhE,IAAI,CAAC,WAAW,KAAK,WAAW,WAAW,CAAC,GAC1C,QAAQ,KACN,iDAAiD,UAAU,uGAE7D;AAEJ"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["parseYaml"],"sources":["../src/skills.ts","../src/index.ts"],"sourcesContent":["/**\n * Bundled skills shipped in this package: `brain` (read the vault) and\n * `checkpoint` (capture a session into it).\n *\n * A provider rather than `ctx.skills.register()` on purpose. A registration\n * lands at the runtime rank, which outranks a user's own skill directories,\n * while BUNDLED_SKILL_RANK is the weakest rank in the local discovery table:\n * shipping at the weakest rank means a user who drops their own `brain` into\n * `~/.agents/skills` keeps winning the name. These are defaults, not a takeover.\n *\n * Each SKILL.md stays the single source of its own name, description and\n * usage guidance — the frontmatter is parsed here with the same `yaml`\n * dependency the harness's own filesystem provider uses, so the exact file that\n * ships in this package also works copied into a user skill root.\n */\nimport { readFile } from 'node:fs/promises'\nimport { fileURLToPath } from 'node:url'\nimport { parse as parseYaml } from 'yaml'\nimport {\n BUNDLED_SKILL_RANK,\n type SkillCandidate,\n type SkillDefinition,\n type SkillProvider,\n type SkillResourceBase,\n} from '@deepseek-ai/dsh-skill'\n\n/** Provider name registered on `ctx.skills`. */\nexport const SKILLS_PROVIDER = 'memory-mcp-skills'\n\n/** Shipped skill directories, relative to the package root. */\nconst SKILL_NAMES = ['brain', 'checkpoint'] as const\n\nconst SKILLS_ROOT = new URL('../skills/', import.meta.url)\nconst INVOCATION = { modelInvocable: true, userInvocable: true } as const\n\ninterface Frontmatter {\n readonly name: string\n readonly description: string\n readonly whenToUse?: string\n}\n\nfunction skillUrl(name: string): URL {\n return new URL(`${name}/SKILL.md`, SKILLS_ROOT)\n}\n\nfunction resourceBase(name: string): SkillResourceBase {\n return { kind: 'directory', path: fileURLToPath(new URL(`${name}/`, SKILLS_ROOT)) }\n}\n\n/** Split YAML frontmatter from the body, mirroring the harness filesystem provider. */\nfunction splitFrontmatter(raw: string): { data: Record<string, unknown>; body: string } {\n const match = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n?/.exec(raw)\n if (match === null) throw new Error('SKILL.md has no YAML frontmatter')\n const parsed: unknown = parseYaml(match[1])\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new TypeError('SKILL.md frontmatter must be a YAML mapping')\n }\n return { data: parsed as Record<string, unknown>, body: raw.slice(match[0].length).trim() }\n}\n\nfunction textField(data: Record<string, unknown>, key: string): string | undefined {\n const value = data[key]\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\n/** Read one shipped skill, rejecting a file whose frontmatter disagrees with its directory. */\nasync function loadSkill(name: string): Promise<{ frontmatter: Frontmatter; body: string }> {\n const { data, body } = splitFrontmatter(await readFile(skillUrl(name), 'utf8'))\n const declared = textField(data, 'name')\n if (declared !== name) {\n throw new Error(`skills/${name}/SKILL.md declares name \"${declared ?? '(none)'}\"`)\n }\n const description = textField(data, 'description')\n if (description === undefined) throw new Error(`skills/${name}/SKILL.md has no description`)\n const whenToUse = textField(data, 'whenToUse')\n return {\n frontmatter: { name, description, ...whenToUse === undefined ? {} : { whenToUse } },\n body,\n }\n}\n\n/** Skills shipped as packaged Markdown assets. */\nexport const skillsProvider: SkillProvider = {\n name: SKILLS_PROVIDER,\n\n async list(): Promise<readonly SkillCandidate[]> {\n return await Promise.all(SKILL_NAMES.map(async (name) => {\n const { frontmatter } = await loadSkill(name)\n return {\n ...frontmatter,\n path: fileURLToPath(skillUrl(name)),\n invocation: INVOCATION,\n source: 'bundled',\n provider: SKILLS_PROVIDER,\n resourceBase: resourceBase(name),\n rank: BUNDLED_SKILL_RANK,\n locator: skillUrl(name),\n }\n }))\n },\n\n async get(candidate): Promise<SkillDefinition | undefined> {\n // A body that is no longer loadable resolves to `undefined`: the registry\n // passes that straight back to its caller, while throwing here would\n // surface a raw ENOENT as a tool error instead of \"no longer available\".\n const loaded = await loadSkill(candidate.name).catch(() => undefined)\n if (loaded === undefined) return undefined\n const { frontmatter, body } = loaded\n return {\n ...frontmatter,\n path: fileURLToPath(skillUrl(candidate.name)),\n invocation: INVOCATION,\n source: 'bundled',\n provider: SKILLS_PROVIDER,\n resourceBase: resourceBase(candidate.name),\n content: body,\n }\n },\n}\n","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'\nimport { skillsProvider } from './skills.js'\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/** Vault root: `~/.memories` by default, independent of `$DSH_HOME`. */\nfunction resolveMemoryPath(value: string): string {\n const v = value.trim()\n if (v.length === 0) return join(homedir(), '.memories')\n return isAbsolute(v) ? v : join(homedir(), 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 // Ship the skills that drive the tools this plugin exposes, at the bundled\n // rank, so a user's own `brain`/`checkpoint` in a skill directory still wins.\n // Injected rather than declared in the plugin's `inject`: the vault bootstrap\n // and the MCP client must keep working in a deployment with no skill catalog.\n ctx.inject(['skills'], (ctx) => {\n ctx.skills.registerProvider(() => skillsProvider)\n })\n\n const serverDir = resolveUnderHome(config.serverDir, 'memory-vault-server')\n const memoryPath = resolveMemoryPath(config.memoryPath)\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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luisarg/memory-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH MCP client for the memory vault",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"dist/",
|
|
20
20
|
"cordis.patch.yml",
|
|
21
21
|
"server/",
|
|
22
|
+
"skills/",
|
|
22
23
|
"vault/"
|
|
23
24
|
],
|
|
24
25
|
"dsh": {
|
|
@@ -30,14 +31,21 @@
|
|
|
30
31
|
"build": "tsdown",
|
|
31
32
|
"dev": "tsdown --watch",
|
|
32
33
|
"test": "vitest run --passWithNoTests",
|
|
33
|
-
"prepare": "tsdown"
|
|
34
|
+
"prepare": "tsdown",
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
38
|
"@deepseek-ai/cordis": "4.0.2",
|
|
37
|
-
"@deepseek-ai/schemastery": "3.18.2"
|
|
39
|
+
"@deepseek-ai/schemastery": "3.18.2",
|
|
40
|
+
"yaml": "^2.4.2"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-skill": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0"
|
|
38
44
|
},
|
|
39
45
|
"devDependencies": {
|
|
46
|
+
"@deepseek-ai/dsh-skill": "0.1.5-rc.2",
|
|
40
47
|
"typescript": "^7.0.2",
|
|
41
|
-
"tsdown": "^0.23.0"
|
|
48
|
+
"tsdown": "^0.23.0",
|
|
49
|
+
"vitest": "^5.0.0"
|
|
42
50
|
}
|
|
43
51
|
}
|
package/server/cli.py
CHANGED
|
@@ -8,10 +8,17 @@ from pathlib import Path
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def get_memory_path() -> Path:
|
|
11
|
-
"""
|
|
11
|
+
"""Vault root: `$MEMORY_PATH`, else `~/.memories`, else this checkout's starter.
|
|
12
|
+
|
|
13
|
+
The last fallback keeps a bare `python server.py` (CI smoke test, dev) working
|
|
14
|
+
before any vault exists at the default location.
|
|
15
|
+
"""
|
|
12
16
|
env = os.environ.get("MEMORY_PATH")
|
|
13
17
|
if env:
|
|
14
18
|
return Path(env)
|
|
19
|
+
home_vault = Path.home() / ".memories"
|
|
20
|
+
if (home_vault / "type-registry.yaml").is_file():
|
|
21
|
+
return home_vault
|
|
15
22
|
return Path(__file__).resolve().parent.parent / "memory-vault"
|
|
16
23
|
|
|
17
24
|
|
package/server/rebuild_index.py
CHANGED
|
@@ -160,7 +160,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
160
160
|
parser.add_argument(
|
|
161
161
|
"--memory-path",
|
|
162
162
|
default=None,
|
|
163
|
-
help="OKF bundle directory (default: $MEMORY_PATH
|
|
163
|
+
help="OKF bundle directory (default: $MEMORY_PATH, else ~/.memories)",
|
|
164
164
|
)
|
|
165
165
|
args = parser.parse_args(argv)
|
|
166
166
|
|
|
@@ -169,8 +169,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
169
169
|
if args.memory_path:
|
|
170
170
|
os.environ["MEMORY_PATH"] = str(Path(args.memory_path).resolve())
|
|
171
171
|
import store as _store_mod
|
|
172
|
+
from cli import get_memory_path
|
|
172
173
|
|
|
173
|
-
memory_path =
|
|
174
|
+
memory_path = get_memory_path().resolve()
|
|
174
175
|
if not memory_path.is_dir():
|
|
175
176
|
print(f"error: {memory_path} is not a directory", file=sys.stderr)
|
|
176
177
|
return 2
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brain
|
|
3
|
+
description: >-
|
|
4
|
+
Read what the memory vault already knows: topic search, recall by project, the user
|
|
5
|
+
profile, and full project dumps, through the memory plugin's MCP tools. Use when the
|
|
6
|
+
user invokes /brain, asks "what do you know about", "search your memory", "recall what
|
|
7
|
+
we said about", "do you remember when", "bring up the profile", or needs prior-session
|
|
8
|
+
context before acting.
|
|
9
|
+
whenToUse: "/brain <query> · /brain profile · /brain export <project>"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Brain — read from the vault
|
|
13
|
+
|
|
14
|
+
The memory plugin exposes a Markdown knowledge base (OKF) over MCP; the vault's
|
|
15
|
+
`memory.db` is a derived SQLite FTS5 index over that Markdown. Read it through the
|
|
16
|
+
plugin's tools — `mcp__memory__*` when the server is named `memory` — never by hand.
|
|
17
|
+
|
|
18
|
+
**`/brain` is read-only.** It writes nothing: capture is `/checkpoint`, and the profile
|
|
19
|
+
layer is `/checkpoint-perfil`.
|
|
20
|
+
|
|
21
|
+
## 1. Locate the vault
|
|
22
|
+
|
|
23
|
+
Only needed when you touch the filesystem. The plugin resolves the vault in this order:
|
|
24
|
+
`DSH_MEMORY_PATH` → `$HOME/.memories` (the OS home — `DSH_HOME` does not move it) → the
|
|
25
|
+
profile's own patch. The server receives the result as `MEMORY_PATH`. Paths are absolute
|
|
26
|
+
and do not depend on the cwd.
|
|
27
|
+
|
|
28
|
+
## 2. Resolve the project
|
|
29
|
+
|
|
30
|
+
`project` is the folder key under `<vault>/projects/`. A misspelled name silently creates
|
|
31
|
+
a new project — there is no validation against a list — so list before filtering:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
ls "${DSH_MEMORY_PATH:-$HOME/.memories}"/projects/
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The key is **not necessarily the repo basename**: it can be the `package.json` name, a
|
|
38
|
+
curated human name, or a nested path (`<org>/<repo>`). When unsure, search **without**
|
|
39
|
+
`project` (that scans every project) and narrow afterwards.
|
|
40
|
+
|
|
41
|
+
## 3. Pick the tool
|
|
42
|
+
|
|
43
|
+
| Need | Tool | Note |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| A topic, a decision, a bug, "what do you know about X" | `search_memory` | `query` is tokenized and **OR**-matched, ranked by relevance — not an exact phrase |
|
|
46
|
+
| Who the user is, the profile, the stack | `get_profile` | profile entries for one project |
|
|
47
|
+
| Everything about one project | `export_memories` | no limit; for dumps, not lookups |
|
|
48
|
+
| Is the server reachable? | `ping` | health check |
|
|
49
|
+
|
|
50
|
+
Traps that cost real time:
|
|
51
|
+
|
|
52
|
+
- **`search_memory` with `entry_type=profile` always returns 0 results.** Profiles come
|
|
53
|
+
from `get_profile`.
|
|
54
|
+
- `get_profile` with **any other** `entry_type` does not filter: it returns the 10 most
|
|
55
|
+
recent entries of that type.
|
|
56
|
+
- `tags` is **OR** as well: `["ci","release"]` returns entries carrying *either* tag.
|
|
57
|
+
- `search_memory` returns **at most 50** entries. For a whole project, use
|
|
58
|
+
`export_memories`.
|
|
59
|
+
- `source` entries are excluded from every filtered search: only `entry_type="source"`
|
|
60
|
+
with no other filter returns them.
|
|
61
|
+
|
|
62
|
+
## 4. Profile layers are documents, not index entries
|
|
63
|
+
|
|
64
|
+
A vault may keep a profile layer: a `profile/` directory inside a project, usually a
|
|
65
|
+
`README.md` map plus numbered documents. Those files are **not** in `memory.db`, so
|
|
66
|
+
`search_memory` never returns them — read them from the filesystem. `get_profile` returns
|
|
67
|
+
something else: internal metadata rows that live only in SQLite and write no Markdown, so
|
|
68
|
+
an index rebuild from Markdown cannot recover them.
|
|
69
|
+
|
|
70
|
+
## 5. Answer
|
|
71
|
+
|
|
72
|
+
- Cite project + entry (its description or first line). Quote; do not paraphrase a claim
|
|
73
|
+
into something stronger than it is.
|
|
74
|
+
- Never fill gaps: if the vault does not say it, it is not there.
|
|
75
|
+
- No results? Say so, and separate **"not in the vault"** from **"I searched badly"**:
|
|
76
|
+
retry without `project`, with other tags, or with `export_memories`.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: checkpoint
|
|
3
|
+
description: >-
|
|
4
|
+
Capture what a session produced into the memory vault as decision, fact, learning and
|
|
5
|
+
convention entries through the memory plugin's tools, then commit the vault. Use when
|
|
6
|
+
the user invokes /checkpoint, says "save this session", "run the checkpoint", "remember
|
|
7
|
+
this", or when a memory-auto [memory-checkpoint] prompt arrives.
|
|
8
|
+
whenToUse: "/checkpoint [topic] — with no argument it reviews the whole session"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Checkpoint — capture the session
|
|
12
|
+
|
|
13
|
+
Write into the vault through the memory plugin's tools (`mcp__memory__*` when the server
|
|
14
|
+
is named `memory`). Never edit `memory.db` or the entry Markdown by hand: the tools keep
|
|
15
|
+
the Markdown source and the derived index in sync.
|
|
16
|
+
|
|
17
|
+
The `memory-auto` plugin runs this **same** procedure automatically — that is what its
|
|
18
|
+
`[memory-checkpoint]` prompt asks for: same types, same selection rules. If the request
|
|
19
|
+
arrived as `[memory-checkpoint]`, this is the path. Do not write the same thing twice:
|
|
20
|
+
search first (step 3).
|
|
21
|
+
|
|
22
|
+
## 1. Resolve the project
|
|
23
|
+
|
|
24
|
+
`project` is the folder key under `<vault>/projects/`; a misspelled name silently creates a
|
|
25
|
+
new project:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
ls "${DSH_MEMORY_PATH:-$HOME/.memories}"/projects/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
It is not always the repo basename — it can be the `package.json` name, a curated name, or
|
|
32
|
+
a nested path (`<org>/<repo>`). Reuse the key the vault already uses for this work.
|
|
33
|
+
|
|
34
|
+
## 2. Decide what qualifies
|
|
35
|
+
|
|
36
|
+
| Type | Tool | What goes in |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| **decision** | `store_decision` | Architecture or design choices, **and why**. Pass `openspec_change_id` when the choice came from a change |
|
|
39
|
+
| **fact** | `store_fact` | Stable, verifiable statements: versions, constraints, paths, endpoints. `confidence` 0.0–1.0 |
|
|
40
|
+
| **learning** | `store_learning` | Non-obvious lessons, debugging insights, a solution that cost effort |
|
|
41
|
+
| **convention** | `store_convention` | Agreed style rules, naming patterns, standards |
|
|
42
|
+
| **source** | `store_source` | External references (article, transcript, PDF, video, link), stored under `raw/`. Immutable per URL |
|
|
43
|
+
|
|
44
|
+
**Do not store:** greetings, acknowledgements, restatements of the request, or anything you
|
|
45
|
+
cannot ground in this session. Few high-signal entries beat many weak ones. If nothing
|
|
46
|
+
qualifies, say so and write nothing — an empty checkpoint is a valid outcome.
|
|
47
|
+
|
|
48
|
+
## 3. Search before writing
|
|
49
|
+
|
|
50
|
+
`search_memory` on the topic first. Deduplication is a hash of
|
|
51
|
+
`(project, entry_type, content)` over normalized content: resending the same text with
|
|
52
|
+
different casing **updates** the existing entry; changing one word creates a **new** one.
|
|
53
|
+
|
|
54
|
+
## 4. Write
|
|
55
|
+
|
|
56
|
+
- `content`: **one paragraph** — no headings, no lists, no markdown structure.
|
|
57
|
+
- `tags`: lowercase-kebab, at least one (`architecture`, `python`, `testing`, `ci`…).
|
|
58
|
+
- `confidence` only has an effect on `store_fact`; the other tools accept and ignore it.
|
|
59
|
+
- `store_profile` is **not** used here: it replaces a project's whole profile entry (one
|
|
60
|
+
per project, no Markdown written). The profile layer has its own procedure.
|
|
61
|
+
|
|
62
|
+
## 5. Close
|
|
63
|
+
|
|
64
|
+
The vault is its own git repository — commit it:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
cd "${DSH_MEMORY_PATH:-$HOME/.memories}"
|
|
68
|
+
git add -A && git commit -m "memory(<project>): <what changed and why>"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Follow whatever commit convention the vault's own history uses, and do not push unless
|
|
72
|
+
asked: with no remote configured, the commit is the end of the line.
|
|
73
|
+
|
|
74
|
+
Report 5–10 lines: what was written (tool + description), what was left out and why, and
|
|
75
|
+
what could not be verified.
|