@luisarg/memory-mcp 0.1.5 → 0.1.7

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 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
- Requires `uv` on PATH the plugin spawns the server with `uv run`.
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`) | `$DSH_HOME/memory-vault` |
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, falling back to the harness home (`$DSH_HOME`, or
28
- `~/.dsh`) DSH does not chdir, so launching from any directory works.
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
- # defaults resuelven bajo el home del harness (cwd-independent):
4
- # $DSH_HOME/memory-vault-server y $DSH_HOME/memory-vault (~/.dsh por defecto)
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 ?? dshHomePath('memory-vault')
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')
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;qBAQa;iBAEI;EACf;EACA;;qBAGW,QAAQ,OAAO;wBAqCZ,MAAM,KAAK,SAAS,QAAQ"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cpSync, existsSync, mkdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, isAbsolute, join } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve } 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";
@@ -124,7 +124,14 @@ function resolveUnderHome(value, segment) {
124
124
  if (v.length === 0) return join(dshHome(), segment);
125
125
  return isAbsolute(v) ? v : join(dshHome(), v);
126
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
+ }
127
133
  /** Copy the bundled dir into `target` when `key` is missing there. */
134
+ /** Copy the bundled dir into `target` when `key` is missing there (user data). */
128
135
  function ensure(target, bundled, key) {
129
136
  if (existsSync(join(target, key))) return false;
130
137
  if (!existsSync(bundled)) return false;
@@ -132,26 +139,23 @@ function ensure(target, bundled, key) {
132
139
  cpSync(bundled, target, { recursive: true });
133
140
  return true;
134
141
  }
135
- /** Copy one bundled file into `target` when missing (upgrades add files 0.1.1 → 0.1.2). */
136
- function ensureFile(target, bundled, file) {
137
- const dest = join(target, file);
138
- if (existsSync(dest)) return false;
139
- const src = join(bundled, file);
140
- if (!existsSync(src)) return false;
141
- mkdirSync(target, { recursive: true });
142
- cpSync(src, dest);
143
- return true;
144
- }
145
142
  function apply(ctx, config) {
146
143
  ctx.inject(["skills"], (ctx) => {
147
144
  ctx.skills.registerProvider(() => skillsProvider);
148
145
  });
149
146
  const serverDir = resolveUnderHome(config.serverDir, "memory-vault-server");
150
- const memoryPath = resolveUnderHome(config.memoryPath, "memory-vault");
151
- if (ensure(serverDir, join(packageRoot, "server"), "server.py")) console.log(`[memory-mcp] installed memory-vault-server -> ${serverDir}`);
147
+ const memoryPath = resolveMemoryPath(config.memoryPath);
148
+ const bundledServer = join(packageRoot, "server");
149
+ if (existsSync(join(bundledServer, "server.py")) && resolve(bundledServer) !== resolve(serverDir)) {
150
+ const firstBoot = !existsSync(join(serverDir, "server.py"));
151
+ mkdirSync(serverDir, { recursive: true });
152
+ cpSync(bundledServer, serverDir, {
153
+ recursive: true,
154
+ force: true
155
+ });
156
+ if (firstBoot) console.log(`[memory-mcp] installed memory-vault-server -> ${serverDir}`);
157
+ }
152
158
  if (ensure(memoryPath, join(packageRoot, "vault"), "type-registry.yaml")) console.log(`[memory-mcp] installed vault starter -> ${memoryPath}`);
153
- const bundled = join(packageRoot, "server");
154
- for (const file of ["launcher.mjs", "requirements.txt"]) if (ensureFile(serverDir, bundled, file)) console.log(`[memory-mcp] installed ${file} -> ${serverDir}`);
155
159
  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)`);
156
160
  }
157
161
  //#endregion
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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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"}
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, resolve } 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. */\n/** Copy the bundled dir into `target` when `key` is missing there (user data). */\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 // 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. The vault is user data, so it is copied only when\n // missing; the server directory is our code, so it is refreshed on every boot.\n // 0.1.5 copied the server only when `server.py` was absent, which froze the\n // Python code of every existing install (and shipped fixes to nobody).\n // cpSync merges: a pip `.venv` or `__pycache__` left in the target survives.\n const bundledServer = join(packageRoot, 'server')\n // `resolve` because a checkout can legitimately point serverDir at the bundle\n // itself (tests and local dev); copying a directory onto itself throws EINVAL.\n if (existsSync(join(bundledServer, 'server.py')) && resolve(bundledServer) !== resolve(serverDir)) {\n const firstBoot = !existsSync(join(serverDir, 'server.py'))\n mkdirSync(serverDir, { recursive: true })\n cpSync(bundledServer, serverDir, { recursive: true, force: true })\n if (firstBoot) 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":";;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,kBAAkB;;AAG/B,MAAM,cAAc,CAAC,SAAS,YAAY;AAE1C,MAAM,cAAc,IAAI,IAAI,cAAc,YAAY,GAAG;AACzD,MAAM,aAAa;CAAE,gBAAgB;CAAM,eAAe;AAAK;AAQ/D,SAAS,SAAS,MAAmB;CACnC,OAAO,IAAI,IAAI,GAAG,KAAK,YAAY,WAAW;AAChD;AAEA,SAAS,aAAa,MAAiC;CACrD,OAAO;EAAE,MAAM;EAAa,MAAM,cAAc,IAAI,IAAI,GAAG,KAAK,IAAI,WAAW,CAAC;CAAE;AACpF;;AAGA,SAAS,iBAAiB,KAA8D;CACtF,MAAM,QAAQ,oCAAoC,KAAK,GAAG;CAC1D,IAAI,UAAU,MAAM,MAAM,IAAI,MAAM,kCAAkC;CACtE,MAAM,SAAkBA,MAAU,MAAM,EAAE;CAC1C,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GACvE,MAAM,IAAI,UAAU,6CAA6C;CAEnE,OAAO;EAAE,MAAM;EAAmC,MAAM,IAAI,MAAM,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK;CAAE;AAC5F;AAEA,SAAS,UAAU,MAA+B,KAAiC;CACjF,MAAM,QAAQ,KAAK;CACnB,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ,KAAA;AACjE;;AAGA,eAAe,UAAU,MAAmE;CAC1F,MAAM,EAAE,MAAM,SAAS,iBAAiB,MAAM,SAAS,SAAS,IAAI,GAAG,MAAM,CAAC;CAC9E,MAAM,WAAW,UAAU,MAAM,MAAM;CACvC,IAAI,aAAa,MACf,MAAM,IAAI,MAAM,UAAU,KAAK,2BAA2B,YAAY,SAAS,EAAE;CAEnF,MAAM,cAAc,UAAU,MAAM,aAAa;CACjD,IAAI,gBAAgB,KAAA,GAAW,MAAM,IAAI,MAAM,UAAU,KAAK,6BAA6B;CAC3F,MAAM,YAAY,UAAU,MAAM,WAAW;CAC7C,OAAO;EACL,aAAa;GAAE;GAAM;GAAa,GAAG,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;EAAE;EAClF;CACF;AACF;;AAGA,MAAa,iBAAgC;CAC3C,MAAM;CAEN,MAAM,OAA2C;EAC/C,OAAO,MAAM,QAAQ,IAAI,YAAY,IAAI,OAAO,SAAS;GACvD,MAAM,EAAE,gBAAgB,MAAM,UAAU,IAAI;GAC5C,OAAO;IACL,GAAG;IACH,MAAM,cAAc,SAAS,IAAI,CAAC;IAClC,YAAY;IACZ,QAAQ;IACR,UAAU;IACV,cAAc,aAAa,IAAI;IAC/B,MAAM;IACN,SAAS,SAAS,IAAI;GACxB;EACF,CAAC,CAAC;CACJ;CAEA,MAAM,IAAI,WAAiD;EAIzD,MAAM,SAAS,MAAM,UAAU,UAAU,IAAI,CAAC,CAAC,YAAY,KAAA,CAAS;EACpE,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;EACjC,MAAM,EAAE,aAAa,SAAS;EAC9B,OAAO;GACL,GAAG;GACH,MAAM,cAAc,SAAS,UAAU,IAAI,CAAC;GAC5C,YAAY;GACZ,QAAQ;GACR,UAAU;GACV,cAAc,aAAa,UAAU,IAAI;GACzC,SAAS;EACX;CACF;AACF;;;AC9GA,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,kBAAkB,OAAuB;CAChD,MAAM,IAAI,MAAM,KAAK;CACrB,IAAI,EAAE,WAAW,GAAG,OAAO,KAAK,QAAQ,GAAG,WAAW;CACtD,OAAO,WAAW,CAAC,IAAI,IAAI,KAAK,QAAQ,GAAG,CAAC;AAC9C;;;AAIA,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;AAEA,SAAgB,MAAM,KAAc,QAAgB;CAKlD,IAAI,OAAO,CAAC,QAAQ,IAAI,QAAQ;EAC9B,IAAI,OAAO,uBAAuB,cAAc;CAClD,CAAC;CAED,MAAM,YAAY,iBAAiB,OAAO,WAAW,qBAAqB;CAC1E,MAAM,aAAa,kBAAkB,OAAO,UAAU;CAOtD,MAAM,gBAAgB,KAAK,aAAa,QAAQ;CAGhD,IAAI,WAAW,KAAK,eAAe,WAAW,CAAC,KAAK,QAAQ,aAAa,MAAM,QAAQ,SAAS,GAAG;EACjG,MAAM,YAAY,CAAC,WAAW,KAAK,WAAW,WAAW,CAAC;EAC1D,UAAU,WAAW,EAAE,WAAW,KAAK,CAAC;EACxC,OAAO,eAAe,WAAW;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;EACjE,IAAI,WAAW,QAAQ,IAAI,iDAAiD,WAAW;CACzF;CACA,IAAI,OAAO,YAAY,KAAK,aAAa,OAAO,GAAG,oBAAoB,GACrE,QAAQ,IAAI,2CAA2C,YAAY;CAErE,IAAI,CAAC,WAAW,KAAK,WAAW,WAAW,CAAC,GAC1C,QAAQ,KACN,iDAAiD,UAAU,uGAE7D;AAEJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luisarg/memory-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "DSH MCP client for the memory vault",
6
6
  "repository": {
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
- """Return MEMORY_PATH from env, or default to `<project>/memory-vault/`."""
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
 
@@ -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 or repo memory-vault)",
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 = Path(os.environ.get("MEMORY_PATH", "")).resolve()
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
package/server/store.py CHANGED
@@ -175,7 +175,9 @@ def _write_okf_file(
175
175
  frontmatter_lines.append(f"project: {project}")
176
176
  if openspec_change_id:
177
177
  frontmatter_lines.append(f"openspec_change_id: {openspec_change_id}")
178
- if confidence is not None and entry_type in ("facts", "conventions"):
178
+ # Confidence is a fact-only field: `templates/fact.md` is the only OKF template
179
+ # that declares it, and `store_fact` is the only tool whose schema exposes it.
180
+ if confidence is not None and entry_type == "facts":
179
181
  frontmatter_lines.append(f"confidence: {confidence}")
180
182
  for key, value in (extra_fields or {}).items():
181
183
  frontmatter_lines.append(f"{key}: {value}")
@@ -15,15 +15,16 @@ The memory plugin exposes a Markdown knowledge base (OKF) over MCP; the vault's
15
15
  `memory.db` is a derived SQLite FTS5 index over that Markdown. Read it through the
16
16
  plugin's tools — `mcp__memory__*` when the server is named `memory` — never by hand.
17
17
 
18
- **`/brain` is read-only.** It writes nothing: capture is `/checkpoint`, and the profile
19
- layer is `/checkpoint-perfil`.
18
+ **`/brain` is read-only.** It writes nothing: capture is `/checkpoint`. The profile layer
19
+ that `/checkpoint-perfil` maintains is a user-level skill, not one this plugin ships — if
20
+ that command is not in the skill catalog, `store_profile` is the only profile writer here.
20
21
 
21
22
  ## 1. Locate the vault
22
23
 
23
24
  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.
25
+ `DSH_MEMORY_PATH` → `$HOME/.memories` (the OS home — `DSH_HOME` does not move it) → the
26
+ profile's own patch. The server receives the result as `MEMORY_PATH`. Paths are absolute
27
+ and do not depend on the cwd.
27
28
 
28
29
  ## 2. Resolve the project
29
30
 
@@ -31,7 +32,7 @@ are absolute and do not depend on the cwd.
31
32
  a new project — there is no validation against a list — so list before filtering:
32
33
 
33
34
  ```sh
34
- ls "${DSH_MEMORY_PATH:-${DSH_HOME:-$HOME/.dsh}/memory-vault}"/projects/
35
+ ls "${DSH_MEMORY_PATH:-$HOME/.memories}"/projects/
35
36
  ```
36
37
 
37
38
  The key is **not necessarily the repo basename**: it can be the `package.json` name, a
@@ -25,7 +25,7 @@ search first (step 3).
25
25
  new project:
26
26
 
27
27
  ```sh
28
- ls "${DSH_MEMORY_PATH:-${DSH_HOME:-$HOME/.dsh}/memory-vault}"/projects/
28
+ ls "${DSH_MEMORY_PATH:-$HOME/.memories}"/projects/
29
29
  ```
30
30
 
31
31
  It is not always the repo basename — it can be the `package.json` name, a curated name, or
@@ -64,7 +64,7 @@ different casing **updates** the existing entry; changing one word creates a **n
64
64
  The vault is its own git repository — commit it:
65
65
 
66
66
  ```sh
67
- cd "${DSH_MEMORY_PATH:-${DSH_HOME:-$HOME/.dsh}/memory-vault}"
67
+ cd "${DSH_MEMORY_PATH:-$HOME/.memories}"
68
68
  git add -A && git commit -m "memory(<project>): <what changed and why>"
69
69
  ```
70
70