@luisarg/memory-mcp 0.1.3 → 0.1.5

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/dist/index.d.ts CHANGED
@@ -1,14 +1,12 @@
1
1
  import Schema from "@deepseek-ai/schemastery";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
-
4
3
  //#region src/index.d.ts
5
- declare const name = "memory-mcp";
6
- interface Config {
4
+ export declare const name = "memory-mcp";
5
+ export interface Config {
7
6
  memoryPath: string;
8
7
  serverDir: string;
9
8
  }
10
- declare const Config: Schema<Config>;
11
- declare function apply(ctx: Context, config: Config): void;
9
+ export declare const Config: Schema<Config>;
10
+ export declare function apply(ctx: Context, config: Config): void;
12
11
  //#endregion
13
- export { Config, apply, name };
14
12
  //# sourceMappingURL=index.d.ts.map
@@ -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;AAwCgB,cAxCH,MAwCc,EAxCN,MAwCuB,CAxChB,MAwCsB,CAAA;iBAAlC,KAAA,MAAW,iBAAiB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;qBAQa;iBAEI;EACf;EACA;;qBAGW,QAAQ,OAAO;wBAwCZ,MAAM,KAAK,SAAS,QAAQ"}
package/dist/index.js CHANGED
@@ -3,7 +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
-
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
7
109
  //#region src/index.ts
8
110
  const name = "memory-mcp";
9
111
  const Config = Schema.object({
@@ -41,6 +143,9 @@ function ensureFile(target, bundled, file) {
41
143
  return true;
42
144
  }
43
145
  function apply(ctx, config) {
146
+ ctx.inject(["skills"], (ctx) => {
147
+ ctx.skills.registerProvider(() => skillsProvider);
148
+ });
44
149
  const serverDir = resolveUnderHome(config.serverDir, "memory-vault-server");
45
150
  const memoryPath = resolveUnderHome(config.memoryPath, "memory-vault");
46
151
  if (ensure(serverDir, join(packageRoot, "server"), "server.py")) console.log(`[memory-mcp] installed memory-vault-server -> ${serverDir}`);
@@ -49,7 +154,7 @@ function apply(ctx, config) {
49
154
  for (const file of ["launcher.mjs", "requirements.txt"]) if (ensureFile(serverDir, bundled, file)) console.log(`[memory-mcp] installed ${file} -> ${serverDir}`);
50
155
  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)`);
51
156
  }
52
-
53
157
  //#endregion
54
158
  export { Config, apply, name };
159
+
55
160
  //# sourceMappingURL=index.js.map
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\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"}
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/** 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 = 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":"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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",
3
+ "version": "0.1.5",
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 && node ../../scripts/bundle-assets.mjs"
34
+ "prepare": "tsdown",
35
+ "typecheck": "tsc --noEmit"
34
36
  },
35
37
  "dependencies": {
36
- "@deepseek-ai/cordis": "4.0.1",
37
- "@deepseek-ai/schemastery": "3.18.1"
38
+ "@deepseek-ai/cordis": "4.0.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": {
40
- "typescript": "^5.9.2",
41
- "tsdown": "^0.15.6"
46
+ "@deepseek-ai/dsh-skill": "0.1.5-rc.2",
47
+ "typescript": "^7.0.2",
48
+ "tsdown": "^0.23.0",
49
+ "vitest": "^5.0.0"
42
50
  }
43
51
  }
package/server/server.py CHANGED
@@ -1,8 +1,13 @@
1
1
  """MCP server exposing memory store tools via the Model Context Protocol.
2
2
 
3
- Eight tools (per openspec/specs/memory-mcp-server/spec.md):
3
+ Ten tools (per openspec/specs/memory-mcp-server/spec.md):
4
4
  search_memory, store_decision, store_fact, store_learning,
5
- store_convention, store_profile, export_memories, get_profile, ping.
5
+ store_convention, store_profile, store_source, export_memories,
6
+ get_profile, ping.
7
+
8
+ Note: `type-registry.yaml` also declares `context` and `idea`. They are valid
9
+ `entries.entry_type` values and legal filters here, but no `store_*` tool
10
+ creates them — nothing writes them today.
6
11
 
7
12
  All reads are explicit (no background polling). Server validates storage
8
13
  accessibility at startup.
@@ -190,7 +195,13 @@ def _tool_definitions() -> list[Tool]:
190
195
  return [
191
196
  Tool(
192
197
  name="search_memory",
193
- description="Search memory entries across projects. Omit project to search all projects.",
198
+ description=(
199
+ "Search memory entries across projects. Omit project to search all projects. "
200
+ "The query is tokenized and OR-matched (any term hits), ranked by relevance. "
201
+ "Tags are OR-matched too: passing ['ci','release'] returns entries with either tag. "
202
+ "Filters narrow the result set; at most 50 entries are returned. "
203
+ "Source entries are excluded unless entry_type='source' with no other filter."
204
+ ),
194
205
  inputSchema={
195
206
  "type": "object",
196
207
  "properties": {
@@ -203,78 +214,143 @@ def _tool_definitions() -> list[Tool]:
203
214
  ),
204
215
  Tool(
205
216
  name="store_decision",
206
- description="Store a decision entry",
217
+ description=(
218
+ "Store a decision: an architectural or design choice that was made and why. "
219
+ "Deduplicated by content hash, so re-storing the same text updates instead of "
220
+ "duplicating."
221
+ ),
207
222
  inputSchema={
208
223
  "type": "object",
209
224
  "required": ["project", "content"],
210
225
  "properties": {
211
226
  "project": {"type": "string"},
212
- "content": {"type": "string"},
213
- "description": {"type": "string"},
214
- "tags": {"type": "array", "items": {"type": "string"}},
227
+ "content": {
228
+ "type": "string",
229
+ "description": "One paragraph, no headings or bullet lists.",
230
+ },
231
+ "description": {
232
+ "type": "string",
233
+ "description": "One-sentence queryable summary; derived from content if omitted.",
234
+ },
235
+ "tags": {
236
+ "type": "array",
237
+ "items": {"type": "string"},
238
+ "description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
239
+ },
215
240
  "openspec_change_id": {"type": "string"},
216
241
  },
217
242
  },
218
243
  ),
219
244
  Tool(
220
245
  name="store_fact",
221
- description="Store a fact entry",
246
+ description=(
247
+ "Store a fact: a stable, verifiable statement about the project (version, "
248
+ "constraint, path, endpoint). Deduplicated by content hash. Prefer one atomic "
249
+ "fact per call over a bundle of several."
250
+ ),
222
251
  inputSchema={
223
252
  "type": "object",
224
253
  "required": ["project", "content"],
225
254
  "properties": {
226
255
  "project": {"type": "string"},
227
- "content": {"type": "string"},
228
- "description": {"type": "string"},
229
- "tags": {"type": "array", "items": {"type": "string"}},
256
+ "content": {
257
+ "type": "string",
258
+ "description": "One paragraph, no headings or bullet lists.",
259
+ },
260
+ "description": {
261
+ "type": "string",
262
+ "description": "One-sentence queryable summary; derived from content if omitted.",
263
+ },
264
+ "tags": {
265
+ "type": "array",
266
+ "items": {"type": "string"},
267
+ "description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
268
+ },
230
269
  "confidence": {"type": "number", "minimum": 0.0, "maximum": 1.0},
231
270
  },
232
271
  },
233
272
  ),
234
273
  Tool(
235
274
  name="store_learning",
236
- description="Store a learning entry",
275
+ description=(
276
+ "Store a learning: a non-obvious lesson, debugging insight, or solution found — "
277
+ "something that cost effort and would otherwise be rediscovered. Deduplicated by "
278
+ "content hash."
279
+ ),
237
280
  inputSchema={
238
281
  "type": "object",
239
282
  "required": ["project", "content"],
240
283
  "properties": {
241
284
  "project": {"type": "string"},
242
- "content": {"type": "string"},
243
- "description": {"type": "string"},
244
- "tags": {"type": "array", "items": {"type": "string"}},
285
+ "content": {
286
+ "type": "string",
287
+ "description": "One paragraph, no headings or bullet lists.",
288
+ },
289
+ "description": {
290
+ "type": "string",
291
+ "description": "One-sentence queryable summary; derived from content if omitted.",
292
+ },
293
+ "tags": {
294
+ "type": "array",
295
+ "items": {"type": "string"},
296
+ "description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
297
+ },
245
298
  },
246
299
  },
247
300
  ),
248
301
  Tool(
249
302
  name="store_convention",
250
- description="Store a convention entry",
303
+ description=(
304
+ "Store a convention: an agreed style rule, naming pattern, or coding standard. "
305
+ "Deduplicated by content hash."
306
+ ),
251
307
  inputSchema={
252
308
  "type": "object",
253
309
  "required": ["project", "content"],
254
310
  "properties": {
255
311
  "project": {"type": "string"},
256
- "content": {"type": "string"},
257
- "description": {"type": "string"},
258
- "tags": {"type": "array", "items": {"type": "string"}},
312
+ "content": {
313
+ "type": "string",
314
+ "description": "One paragraph, no headings or bullet lists.",
315
+ },
316
+ "description": {
317
+ "type": "string",
318
+ "description": "One-sentence queryable summary; derived from content if omitted.",
319
+ },
320
+ "tags": {
321
+ "type": "array",
322
+ "items": {"type": "string"},
323
+ "description": "Lowercase-kebab tags, e.g. architecture/python/testing.",
324
+ },
259
325
  },
260
326
  },
261
327
  ),
262
328
  Tool(
263
329
  name="store_profile",
264
- description="Store or update a user profile entry for a project",
330
+ description=(
331
+ "Replace the tech profile for a project. One profile per project: the content "
332
+ "overwrites the previous one, it does not append. Pass the complete profile text."
333
+ ),
265
334
  inputSchema={
266
335
  "type": "object",
267
336
  "required": ["project", "content"],
268
337
  "properties": {
269
338
  "project": {"type": "string"},
270
- "content": {"type": "string"},
339
+ "content": {
340
+ "type": "string",
341
+ "description": "The full profile — replaces the stored one.",
342
+ },
271
343
  "tags": {"type": "array", "items": {"type": "string"}},
272
344
  },
273
345
  },
274
346
  ),
275
347
  Tool(
276
348
  name="store_source",
277
- description="Store a source reference (article, transcript, PDF, video, link)",
349
+ description=(
350
+ "Store an external source reference (article, transcript, PDF, video, link) under "
351
+ "raw/. Immutable: a later store with the same URL returns the existing entry, and "
352
+ "reusing a title slug with different content is rejected."
353
+ ),
278
354
  inputSchema={
279
355
  "type": "object",
280
356
  "required": ["url", "title", "description", "source_kind"],
@@ -294,7 +370,10 @@ def _tool_definitions() -> list[Tool]:
294
370
  ),
295
371
  Tool(
296
372
  name="export_memories",
297
- description="Export all memory entries for a project (no limit)",
373
+ description=(
374
+ "Export every stored entry for one project, newest first, with no result limit. "
375
+ "Use for a full project dump, not for lookups — search_memory is cheaper."
376
+ ),
298
377
  inputSchema={
299
378
  "type": "object",
300
379
  "required": ["project"],
@@ -306,7 +385,11 @@ def _tool_definitions() -> list[Tool]:
306
385
  ),
307
386
  Tool(
308
387
  name="get_profile",
309
- description="Retrieve the global tech profile for a project",
388
+ description=(
389
+ "Retrieve the stored tech profile for a project. Returns profile entries by "
390
+ "default; pass entry_type to query another type instead (then it is a "
391
+ "most-recent-first lookup capped at 10)."
392
+ ),
310
393
  inputSchema={
311
394
  "type": "object",
312
395
  "required": ["project"],
package/server/store.py CHANGED
@@ -770,12 +770,15 @@ class MemoryStore:
770
770
  project: str,
771
771
  entry_type: str | None = None,
772
772
  ) -> list[dict]:
773
- """Retrieve profile entries for a project."""
774
- sql = "SELECT * FROM entries WHERE project = ?"
775
- params: list = [project]
776
- if entry_type:
777
- sql += " AND entry_type = ?"
778
- params.append(entry_type)
773
+ """Retrieve the profile for a project.
774
+
775
+ Defaults to ``profile`` entries: the tool is named get_profile, so an
776
+ unfiltered call returning the 10 most recent rows of any type was a
777
+ silent wrong answer. Pass ``entry_type`` to use it as a recency query.
778
+ """
779
+ entry_type = entry_type or "profile"
780
+ sql = "SELECT * FROM entries WHERE project = ? AND entry_type = ?"
781
+ params: list = [project, entry_type]
779
782
  sql += " ORDER BY updated_at DESC LIMIT 10"
780
783
  rows = self.db.execute(sql, params).fetchall()
781
784
  return [dict(r) for r in rows]
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """Self-check: get_profile answers with the profile, not with recent noise.
3
+
4
+ Regression guard for the bug where an unfiltered get_profile returned the 10
5
+ most recently updated rows of ANY type, so a caller asking for the profile of a
6
+ busy project got unrelated decisions and facts.
7
+
8
+ Run: python3 memory-vault-server/test_get_profile.py
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import os
15
+ import shutil
16
+ import sys
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ SERVER_DIR = Path(__file__).resolve().parent
21
+ REPO_VAULT = SERVER_DIR.parent / "memory-vault"
22
+ sys.path.insert(0, str(SERVER_DIR))
23
+
24
+
25
+ def main() -> int:
26
+ with tempfile.TemporaryDirectory() as tmp:
27
+ # store.py resolves the type registry from MEMORY_PATH at import time,
28
+ # so seed the throwaway vault before importing it.
29
+ vault = Path(tmp)
30
+ (vault / "projects").mkdir()
31
+ shutil.copy(REPO_VAULT / "type-registry.yaml", vault / "type-registry.yaml")
32
+ os.environ["MEMORY_PATH"] = str(vault)
33
+ import store as store_mod
34
+
35
+ s = store_mod.MemoryStore(storage_path=vault)
36
+ s.initialize()
37
+
38
+ s.upsert_profile(project="proj", content="PROFILE: python + sqlite")
39
+ # Written afterwards, so these outrank the profile in updated_at:
40
+ # the old code returned them and called it a profile.
41
+ s.upsert_entry("decision", "proj", "DECISION: use sqlite for storage")
42
+ s.upsert_entry("fact", "proj", "FACT: python 3.11 is required")
43
+
44
+ got = s.get_profile(project="proj")
45
+ assert len(got) == 1, f"expected only the profile row, got {len(got)}: {got}"
46
+ assert got[0]["entry_type"] == "profile", got[0]["entry_type"]
47
+ assert "PROFILE" in got[0]["content"], got[0]["content"]
48
+
49
+ # entry_type=None must behave exactly like the default.
50
+ assert s.get_profile(project="proj", entry_type=None) == got
51
+
52
+ # Explicit entry_type still works (and is now a plain recency lookup).
53
+ facts = s.get_profile(project="proj", entry_type="fact")
54
+ assert len(facts) == 1 and facts[0]["entry_type"] == "fact", facts
55
+
56
+ # A project with no profile yields nothing rather than unrelated rows.
57
+ s.upsert_entry("fact", "other", "FACT: unrelated project")
58
+ assert s.get_profile(project="other") == [], "profile leaked across projects"
59
+
60
+ # The MCP tool handler agrees (binds the tool default to the store fix).
61
+ # Needs the pinned `mcp` version from requirements.txt; skip when the
62
+ # ambient one is older instead of failing the whole check.
63
+ try:
64
+ import server
65
+
66
+ found = json.loads(server.handle_get_profile(s, {"project": "proj"}))
67
+ assert len(found) == 1 and found[0]["entry_type"] == "profile", found
68
+ except ImportError as exc:
69
+ print(f"skip: MCP handler assertion ({exc})")
70
+
71
+ s._db.close()
72
+
73
+ print("ok: get_profile returns the profile, not the most recent rows")
74
+ return 0
75
+
76
+
77
+ if __name__ == "__main__":
78
+ raise SystemExit(main())
@@ -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` → `$DSH_HOME/memory-vault` (`~/.dsh/memory-vault` when `DSH_HOME` is
25
+ unset) → the profile's own patch. The server receives the result as `MEMORY_PATH`. Paths
26
+ are absolute 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:-${DSH_HOME:-$HOME/.dsh}/memory-vault}"/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:-${DSH_HOME:-$HOME/.dsh}/memory-vault}"/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:-${DSH_HOME:-$HOME/.dsh}/memory-vault}"
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.