@vkmikc/create-vkm-kit 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE.md +44 -0
  2. package/README.md +148 -0
  3. package/package.json +57 -0
  4. package/src/asset-install.mjs +109 -0
  5. package/src/claude-native-memory.mjs +507 -0
  6. package/src/file-perms.mjs +53 -0
  7. package/src/hooks/_transcript-cache.mjs +223 -0
  8. package/src/hooks/compact-mcp-output.mjs +87 -0
  9. package/src/hooks/compact-tool-output.mjs +177 -0
  10. package/src/hooks/ensure-otel-sink.mjs +51 -0
  11. package/src/hooks/guard-effort-gate.mjs +209 -0
  12. package/src/hooks/guard-native-memory-write.mjs +129 -0
  13. package/src/hooks/session-start-vault-context.mjs +200 -0
  14. package/src/hooks/stop-vault-close-reminder.mjs +150 -0
  15. package/src/index.js +1547 -0
  16. package/src/mcp-merge.mjs +279 -0
  17. package/src/memory-rules.mjs +205 -0
  18. package/src/obscura-setup.mjs +272 -0
  19. package/src/ollama-setup.mjs +126 -0
  20. package/src/rules-merge.mjs +106 -0
  21. package/src/settings-io.mjs +193 -0
  22. package/src/settings-writers.mjs +150 -0
  23. package/src/skills-install.mjs +96 -0
  24. package/src/telemetry.mjs +154 -0
  25. package/src/token-saver.mjs +248 -0
  26. package/templates/agents/vkm-implementer.md +23 -0
  27. package/templates/output-styles/vkm-terse.md +23 -0
  28. package/templates/skills/vkm-discipline/SKILL.md +77 -0
  29. package/templates/skills/vkm-discipline/domains/coding.md +39 -0
  30. package/templates/skills/vkm-discipline/domains/data.md +37 -0
  31. package/templates/skills/vkm-discipline/domains/debugging.md +41 -0
  32. package/templates/skills/vkm-discipline/domains/design-ui.md +41 -0
  33. package/templates/skills/vkm-discipline/domains/expertise.md +26 -0
  34. package/templates/skills/vkm-discipline/domains/infra.md +35 -0
  35. package/templates/skills/vkm-discipline/domains/llm-artifacts.md +30 -0
  36. package/templates/skills/vkm-discipline/domains/security.md +37 -0
  37. package/templates/skills/vkm-discipline/domains/web-search.md +43 -0
  38. package/templates/skills/vkm-discipline/domains/writing.md +32 -0
  39. package/templates/skills/vkm-spec/SKILL.md +33 -0
package/src/index.js ADDED
@@ -0,0 +1,1547 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @vkmikc/create-vkm-kit — interactive initializer.
4
+ * Spanish-first CLI; pass --lang en for English labels.
5
+ *
6
+ * Source-of-truth lives in this `src/` directory. There is no `dist/` build
7
+ * step — `src/` is what npm publishes and what `bin` in package.json points
8
+ * to. (Pre-2026 the directory was named `dist/`, which falsely implied a
9
+ * compile step. Renamed for clarity; see CHANGELOG.)
10
+ */
11
+ import path from "node:path";
12
+ import pc from "picocolors";
13
+ import prompts from "prompts";
14
+ import { execa } from "execa";
15
+ import fse from "fs-extra";
16
+ import {
17
+ mergeBasicMemoryServer,
18
+ mergeObsidianHybridServer,
19
+ mergeObscuraWebServer,
20
+ resolveKitRepoRoot,
21
+ hybridMcpPathsFromKitRoot,
22
+ flagValue,
23
+ basicMemoryServer,
24
+ hybridServer,
25
+ obscuraWebServer,
26
+ claudeAddArgv,
27
+ claudeRemoveArgv,
28
+ codexAddArgv,
29
+ codexRemoveArgv,
30
+ codexTomlBlock,
31
+ SEMANTIC_EMBEDDER
32
+ } from "./mcp-merge.mjs";
33
+ import { installRules } from "./rules-merge.mjs";
34
+ import {
35
+ configureClaudeNativeMemory,
36
+ uninstallClaudeNativeMemory
37
+ } from "./claude-native-memory.mjs";
38
+ import { configureTokenSaver, uninstallTokenSaver } from "./token-saver.mjs";
39
+ import { configureTelemetry, uninstallTelemetry } from "./telemetry.mjs";
40
+ import { configureSkillAssets, uninstallSkillAssets } from "./skills-install.mjs";
41
+ import { maybeInstallOllama } from "./ollama-setup.mjs";
42
+ import { maybeInstallObscura } from "./obscura-setup.mjs";
43
+ import { restrictFileToOwner } from "./file-perms.mjs";
44
+ import { atomicWriteJson, stripLeadingUtf8Bom } from "./settings-io.mjs";
45
+
46
+ /** Cursor/VS Code workspace defaults: fewer `git` + `conhost` spikes on Windows (SCM polling). */
47
+ const VAULT_VSCODE_GIT_SETTINGS = {
48
+ "git.autoRepositoryDetection": false,
49
+ "git.autorefresh": false,
50
+ "git.autofetch": false,
51
+ "git.terminalAuthentication": false,
52
+ "git.decorations.enabled": false,
53
+ "git.timeline.enabled": false,
54
+ "git.blame.editorDecoration.enabled": false,
55
+ "git.blame.statusBarItem.enabled": false,
56
+ "git.showProgress": false,
57
+ "npm.autoDetect": "off",
58
+ "files.watcherExclude": {
59
+ "**/node_modules/**": true,
60
+ "**/bin/**": true,
61
+ "**/dist/**": true,
62
+ "**/.cursor/**": true,
63
+ "**/packages/**/node_modules/**": true,
64
+ "**/.pytest_cache/**": true,
65
+ "**/coverage/**": true,
66
+ "**/.venv/**": true,
67
+ "**/__pycache__/**": true,
68
+ "**/.obsidian/**": true,
69
+ "**/go.work.sum": true,
70
+ "**/.git/objects/**": true,
71
+ "**/.git/lfs/**": true
72
+ }
73
+ };
74
+
75
+ const messages = {
76
+ es: {
77
+ title: "create-vkm-kit",
78
+ vaultQ: "Ruta del vault (debe contener .obsidian o crearemos uno)",
79
+ createVault: "Crear un vault nuevo en ~/Documents/obsidian-memory-vault",
80
+ ides: "IDEs a configurar (espacio para MCP)",
81
+ gitleaks: "Activar hook pre-commit gitleaks",
82
+ age: "Activar cifrado age para datos sensibles (mas friccion)",
83
+ daemon: "Instalar obsidian-memoryd como servicio de usuario",
84
+ summary: "Listo. Pasos siguientes",
85
+ otherIdes: "Copia este bloque MCP en la config del IDE:",
86
+ ftsHint:
87
+ "Opcional (vaults grandes): MCP obsidian-memory-hybrid (tras pip install -e …/obsidian-memory-rag) o obsidian-memory-rag index manual; ver docs/es/instalacion.md (Verificación).",
88
+ hybridQ:
89
+ "¿Añadir MCP obsidian-memory-hybrid (FTS5 / BM25) además de basic-memory? (requiere clon del kit y pip install -e packages/obsidian-memory-rag)",
90
+ semanticQ:
91
+ "¿Usar embeddings neuronales (fastembed) para recall por significado? (requiere el extra [semantic])",
92
+ rulesQ:
93
+ "¿Instalar las reglas de memoria en CLAUDE.md / AGENTS.md / .cursor? (bloque marcado; no pisa tu contenido)"
94
+ },
95
+ en: {
96
+ title: "create-vkm-kit",
97
+ vaultQ: "Vault path (must contain .obsidian or we create a sample)",
98
+ createVault: "Create a new vault at ~/Documents/obsidian-memory-vault",
99
+ ides: "IDEs to wire for MCP",
100
+ gitleaks: "Enable gitleaks pre-commit hook",
101
+ age: "Enable age encryption (more friction)",
102
+ daemon: "Install obsidian-memoryd user service",
103
+ summary: "Done. Next steps",
104
+ otherIdes: "Paste this MCP block into each IDE's config:",
105
+ ftsHint:
106
+ "Optional (large vaults): obsidian-memory-hybrid MCP (after pip install -e …/obsidian-memory-rag) or manual obsidian-memory-rag index; see docs/en/install.md (Verification).",
107
+ hybridQ:
108
+ "Add obsidian-memory-hybrid MCP (FTS5 / BM25) in addition to basic-memory? (needs this repo clone + pip install -e packages/obsidian-memory-rag)",
109
+ semanticQ:
110
+ "Use neural embeddings (fastembed) for meaning-based recall? (needs the [semantic] extra)",
111
+ rulesQ:
112
+ "Install the memory rules into CLAUDE.md / AGENTS.md / .cursor? (marked block; won't clobber your content)"
113
+ }
114
+ };
115
+
116
+ function langFromArgs() {
117
+ const i = process.argv.indexOf("--lang");
118
+ if (i >= 0 && process.argv[i + 1] === "en") return "en";
119
+ return "es";
120
+ }
121
+
122
+ function dryRunFromArgs() {
123
+ return process.argv.includes("--dry-run");
124
+ }
125
+
126
+ /**
127
+ * The `--full` (alias `--all`) one-shot preset: max power, zero questions. Turns
128
+ * on hybrid + semantic + index build + backend install + rules, and defaults the
129
+ * wired IDEs to Codex + Claude Code. Implies non-interactive.
130
+ */
131
+ function fullPresetFromArgs() {
132
+ return process.argv.includes("--full") || process.argv.includes("--all");
133
+ }
134
+
135
+ function nonInteractiveFromArgs() {
136
+ return (
137
+ process.argv.includes("--non-interactive") ||
138
+ process.argv.includes("--yes") ||
139
+ process.argv.includes("-y") ||
140
+ fullPresetFromArgs()
141
+ );
142
+ }
143
+
144
+ // Long flags that consume the NEXT token as their value, so it isn't mistaken
145
+ // for the positional vault path.
146
+ const VALUE_FLAGS = new Set(["--vault", "--ide", "--repo-root", "--lang", "--rules"]);
147
+
148
+ /**
149
+ * First bare (non-flag) CLI argument = vault path shorthand, so you can write
150
+ * `create-vkm-kit ./my-vault` instead of `--vault ./my-vault`.
151
+ * @param {string[]} argv
152
+ */
153
+ function positionalVault(argv) {
154
+ const args = argv.slice(2);
155
+ for (let i = 0; i < args.length; i++) {
156
+ const a = args[i];
157
+ if (a === "--") continue;
158
+ if (a.startsWith("-")) {
159
+ if (VALUE_FLAGS.has(a)) i++; // skip this flag's value
160
+ continue;
161
+ }
162
+ return a;
163
+ }
164
+ return null;
165
+ }
166
+
167
+ /** Default vault when none is given: ~/Documents/obsidian-memory-vault. */
168
+ function defaultVaultPath(home) {
169
+ return path.join(home, "Documents", "obsidian-memory-vault");
170
+ }
171
+
172
+ /**
173
+ * Which agent-config files to install the memory-rules block into. Derived from
174
+ * --ide (wiring an IDE also drops its rules) unless overridden:
175
+ * --no-rules / --rules none → [] · --rules all → claude+agents+cursor+codex
176
+ * --rules a,b → explicit · --full or interactive default → agents + the
177
+ * global rules file for each wired agent (claude/codex/cursor)
178
+ * @param {string[]} argv
179
+ * @param {string[]} ides
180
+ * @param {{ defaultFromIde?: boolean, full?: boolean }} [opts]
181
+ * @returns {string[]}
182
+ */
183
+ function rulesTargetsFromArgs(argv, ides, { defaultFromIde = false, full = false } = {}) {
184
+ if (argv.includes("--no-rules")) return [];
185
+ const raw = flagValue(argv, "--rules");
186
+ const valid = ["claude", "agents", "cursor", "codex"];
187
+ if (raw) {
188
+ const v = raw.trim().toLowerCase();
189
+ if (v === "none") return [];
190
+ if (v === "all") return [...valid];
191
+ return v
192
+ .split(",")
193
+ .map((s) => s.trim())
194
+ .filter((s) => valid.includes(s));
195
+ }
196
+ // `--full` is "everything on": install the project AGENTS.md rules plus the
197
+ // global rules file for each wired agent, so recall works out of the box.
198
+ if (full) return rulesFromIdes(ides);
199
+ // Headless with no --rules → write nothing (don't surprise-touch global files).
200
+ // Interactive derives a default from the selected IDEs, then asks for confirmation.
201
+ if (!defaultFromIde) return [];
202
+ return rulesFromIdes(ides);
203
+ }
204
+
205
+ /** Project AGENTS.md + the global rules file for each wired agent. */
206
+ function rulesFromIdes(ides) {
207
+ const targets = ["agents"];
208
+ if (ides.includes("claude")) targets.push("claude");
209
+ if (ides.includes("codex")) targets.push("codex");
210
+ if (ides.includes("cursor")) targets.push("cursor");
211
+ return targets;
212
+ }
213
+
214
+ async function findVault(cwd, home) {
215
+ let cur = cwd;
216
+ for (let i = 0; i < 6; i++) {
217
+ if (await fse.pathExists(path.join(cur, ".obsidian"))) return cur;
218
+ const parent = path.dirname(cur);
219
+ if (parent === cur) break;
220
+ cur = parent;
221
+ }
222
+ const h = path.join(home, "Documents");
223
+ if (await fse.pathExists(path.join(h, ".obsidian"))) return h;
224
+ return null;
225
+ }
226
+
227
+ /**
228
+ * Merges kit Git/SCM tuning into `<vault>/.vscode/settings.json` (creates or updates).
229
+ * Kit keys win for known tuning; `files.watcherExclude` is merged with existing entries.
230
+ * @param {string} vault
231
+ * @param {boolean} dryRun
232
+ */
233
+ async function writeVaultGitWorkspaceSettings(vault, dryRun) {
234
+ const fp = path.join(vault, ".vscode", "settings.json");
235
+ if (dryRun) {
236
+ console.log(pc.cyan("[dry-run] would merge"), fp);
237
+ return;
238
+ }
239
+ await fse.ensureDir(path.dirname(fp));
240
+ const existedBefore = await fse.pathExists(fp);
241
+ let existing = {};
242
+ if (existedBefore) {
243
+ try {
244
+ const raw = (await fse.readFile(fp, "utf8")).trim().replace(/^\uFEFF/, "");
245
+ if (raw) existing = JSON.parse(raw);
246
+ } catch {
247
+ const bak = `${fp}.bak.${Date.now()}`;
248
+ await fse.copy(fp, bak);
249
+ console.warn(pc.yellow("Invalid JSON in vault .vscode/settings.json; backed up to"), bak);
250
+ existing = {};
251
+ }
252
+ }
253
+ const merged = { ...existing };
254
+ for (const [key, value] of Object.entries(VAULT_VSCODE_GIT_SETTINGS)) {
255
+ if (key === "files.watcherExclude" && value && typeof value === "object") {
256
+ const prev = existing[key] && typeof existing[key] === "object" ? existing[key] : {};
257
+ merged[key] = { ...prev, ...value };
258
+ } else {
259
+ merged[key] = value;
260
+ }
261
+ }
262
+ if (process.platform === "win32") {
263
+ const candidates = [
264
+ "C:\\Program Files\\Git\\cmd\\git.exe",
265
+ path.join(process.env.ProgramFiles || "C:\\Program Files", "Git", "cmd", "git.exe")
266
+ ];
267
+ const pf86 = process.env["ProgramFiles(x86)"];
268
+ if (pf86) candidates.push(path.join(pf86, "Git", "cmd", "git.exe"));
269
+ for (const g of candidates) {
270
+ if (g && (await fse.pathExists(g))) {
271
+ merged["git.path"] = g;
272
+ break;
273
+ }
274
+ }
275
+ }
276
+ await fse.writeFile(fp, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
277
+ console.log(pc.green(existedBefore ? "Merged" : "Wrote"), fp);
278
+ }
279
+
280
+ async function scaffoldNewVault(vault, lang, dryRun) {
281
+ await fse.ensureDir(path.join(vault, ".obsidian"));
282
+ const start =
283
+ lang === "en"
284
+ ? `---
285
+ type: index
286
+ tags: [start]
287
+ ---
288
+
289
+ # START_HERE
290
+
291
+ 1. Read \`MEMORY.md\`.
292
+ 2. Then \`PROJECTS/<your-repo>.md\` (match your workspace folder name).
293
+ 3. Log decisions in \`SESSION_LOG.md\`.
294
+ `
295
+ : `---
296
+ type: index
297
+ tags: [start]
298
+ ---
299
+
300
+ # START_HERE
301
+
302
+ 1. Lee \`MEMORY.md\`.
303
+ 2. Luego \`PROJECTS/<tu-repo>.md\` (ajusta el nombre a tu carpeta de proyecto).
304
+ 3. Cierra tareas en \`SESSION_LOG.md\`.
305
+ `;
306
+ await fse.writeFile(path.join(vault, "START_HERE.md"), start, "utf8");
307
+ await fse.writeFile(
308
+ path.join(vault, "MEMORY.md"),
309
+ lang === "en"
310
+ ? "# Global memory\n\nSeparate **facts** vs **hypotheses** explicitly.\n"
311
+ : "# Memoria global\n\nSepara **hechos** e **hipótesis** explícitamente.\n",
312
+ "utf8"
313
+ );
314
+ await fse.writeFile(path.join(vault, "SESSION_LOG.md"), "# SESSION_LOG\n\n", "utf8");
315
+ await fse.ensureDir(path.join(vault, "PROJECTS"));
316
+ await fse.writeFile(path.join(vault, "PROJECTS", ".gitkeep"), "", "utf8");
317
+ // Evolving-memory homes: STACKS/ (one note per tech) + PRACTICES/ (hypotheses → confirmed).
318
+ await fse.ensureDir(path.join(vault, "STACKS"));
319
+ await fse.writeFile(path.join(vault, "STACKS", ".gitkeep"), "", "utf8");
320
+ await fse.ensureDir(path.join(vault, "PRACTICES"));
321
+ const practices =
322
+ lang === "en"
323
+ ? {
324
+ obs: "# Observations (hypotheses)\n\nOne line each: `date · file:line · pattern · status: pending|confirmed|dismissed`. Promote confirmed ones to `confirmed-bad.md`.\n",
325
+ good: "# Confirmed good practices\n\nPatterns the user prefers — reinforce them when they apply.\n",
326
+ bad: "# Confirmed anti-patterns\n\nPatterns the user rejected — flag (ask, don't impose) when they reappear.\n"
327
+ }
328
+ : {
329
+ obs: "# Observaciones (hipótesis)\n\nUna línea cada una: `fecha · archivo:línea · patrón · status: pending|confirmed|dismissed`. Promueve las confirmadas a `confirmed-bad.md`.\n",
330
+ good: "# Buenas prácticas confirmadas\n\nPatrones que el usuario prefiere — refuérzalos cuando apliquen.\n",
331
+ bad: "# Anti-patrones confirmados\n\nPatrones que el usuario rechazó — avisa (pregunta, no impongas) si reaparecen.\n"
332
+ };
333
+ await fse.writeFile(path.join(vault, "PRACTICES", "observations.md"), practices.obs, "utf8");
334
+ await fse.writeFile(path.join(vault, "PRACTICES", "confirmed-good.md"), practices.good, "utf8");
335
+ await fse.writeFile(path.join(vault, "PRACTICES", "confirmed-bad.md"), practices.bad, "utf8");
336
+ // Project rules home: dated, sourced, reasoned hard rules (ADR-0039).
337
+ await fse.ensureDir(path.join(vault, "RULES"));
338
+ const rulesTemplate =
339
+ lang === "en"
340
+ ? `---
341
+ type: rules
342
+ tags: [rules, template]
343
+ ---
344
+
345
+ # RULES — <project>
346
+
347
+ Hard rules for [[PROJECTS/<project>]]. **Binding**: if a rule blocks the task, stop and ask.
348
+
349
+ > PROJECT rules only — knowledge invisible from the repo (domain contracts, deliberate traps, hard-won decisions, user mandates). Generic method is not stored: models already ship with it.
350
+
351
+ - **R1 — <imperative one-line rule>.** <minimal operational detail>
352
+ - why: <concrete story or reason — rules with a why get respected; bare ones get misapplied>
353
+ - source: \`<file/test/ADR that defines or enforces it>\` · last_verified: YYYY-MM-DD
354
+
355
+ ## Changes
356
+
357
+ - [decision] YYYY-MM-DD · <rule added/retired/edited + reason>
358
+ `
359
+ : `---
360
+ type: rules
361
+ tags: [rules, template]
362
+ ---
363
+
364
+ # RULES — <proyecto>
365
+
366
+ Reglas duras de [[PROJECTS/<proyecto>]]. **Vinculantes**: si una regla bloquea la tarea, para y consulta.
367
+
368
+ > Solo reglas de PROYECTO — conocimiento invisible desde el repo (contratos de dominio, trampas deliberadas, decisiones costosas, mandatos del usuario). El método genérico no se guarda: ya viene en los modelos.
369
+
370
+ - **R1 — <regla imperativa en una línea>.** <detalle mínimo operativo>
371
+ - porqué: <historia o razón concreta — las reglas con porqué se respetan; las secas se obedecen mal>
372
+ - fuente: \`<archivo/test/ADR que la define o la enforcea>\` · last_verified: YYYY-MM-DD
373
+
374
+ ## Cambios
375
+
376
+ - [decision] YYYY-MM-DD · <alta/retiro/edición de regla + motivo>
377
+ `;
378
+ await fse.writeFile(path.join(vault, "RULES", "TEMPLATE.md"), rulesTemplate, "utf8");
379
+ // Per-model adaptive layer: the rules point here; the agent reads only its own row.
380
+ await fse.ensureDir(path.join(vault, "_meta"));
381
+ const profilesDoc =
382
+ lang === "en"
383
+ ? `# Agent profiles (which model, and what it's good at)
384
+
385
+ This vault may be driven by different models, each with different strengths when deciding what to do.
386
+ On a non-trivial task, read **your** row, follow its tuning, and **append a one-line observation** when
387
+ a model clearly excelled or stumbled at a task type — over time this learns the best model per job.
388
+
389
+ > Starting defaults — general, and they evolve. Correct them with real observations below.
390
+
391
+ | Model | Decision-making strength | Lean on it for | Tune the memory |
392
+ | --- | --- | --- | --- |
393
+ | Claude Opus / Sonnet | Reliable instruction-following + tool use, structured multi-step | agentic refactors, careful reviews, self-critique | full self-check + coaching; long context OK but stay passage-first |
394
+ | Cursor Composer | Fast in-IDE multi-file edits | big mechanical edits across the codebase | action-first; lean on STACKS/ patterns; shorter deliberation |
395
+ | GPT (incl. reasoning models) | Planning + tool orchestration | decomposing fuzzy tasks, multi-tool flows | decompose explicitly; verify tool results before trusting them |
396
+ | DeepSeek (V3 / R1) | Deep, cheap code/math reasoning | hard logic / algorithm problems | afford a deeper self-check on tricky logic; keep notes terse |
397
+ | Gemini | Very large context, multimodal | long-document / many-file synthesis | may load more, but passage-first still wins on cost/latency |
398
+
399
+ ## Observations (evolving — append one line each)
400
+
401
+ Format: \`date · model · task type · what worked / what to avoid\`
402
+
403
+ <!-- example -->
404
+
405
+ - 2026-01-15 · Composer · auth/security change · missed an RLS edge case → add a Claude self-check pass for security-sensitive work
406
+ `
407
+ : `# Perfiles de agente (qué modelo, y en qué destaca)
408
+
409
+ Este vault lo pueden conducir distintos modelos, cada uno con fortalezas distintas al decidir qué hacer.
410
+ En una tarea no trivial, lee **tu** fila, sigue su ajuste y **añade una observación de una línea** cuando
411
+ un modelo destaque o falle claramente en un tipo de tarea — con el tiempo aprende el mejor modelo por trabajo.
412
+
413
+ > Defaults de partida — generales, y evolucionan. Corrígelos con observaciones reales abajo.
414
+
415
+ | Modelo | Fortaleza al decidir | Aprovéchalo para | Ajusta la memoria |
416
+ | --- | --- | --- | --- |
417
+ | Claude Opus / Sonnet | Sigue instrucciones + tool use fiable, multi-paso | refactors agénticos, revisiones, autocrítica | self-check completo + coaching; contexto largo OK pero passage-first |
418
+ | Cursor Composer | Edición multi-archivo rápida en el IDE | cambios mecánicos amplios | acción primero; apóyate en STACKS/; menos deliberación |
419
+ | GPT (incl. razonadores) | Planificación + orquestación de tools | descomponer tareas difusas, multi-tool | descompón explícito; verifica resultados de tools |
420
+ | DeepSeek (V3 / R1) | Razonamiento profundo de código/mates, barato | lógica / algoritmos difíciles | permite un self-check más profundo; notas concisas |
421
+ | Gemini | Contexto enorme, multimodal | síntesis de muchos archivos / docs largos | puede cargar más, pero passage-first sigue ganando en coste |
422
+
423
+ ## Observaciones (evolutivo — una línea cada una)
424
+
425
+ Formato: \`fecha · modelo · tipo de tarea · qué funcionó / qué evitar\`
426
+
427
+ <!-- example -->
428
+
429
+ - 2026-01-15 · Composer · cambio auth/seguridad · se le escapó un caso RLS → añade un self-check de Claude para trabajo sensible a seguridad
430
+ `;
431
+ await fse.writeFile(path.join(vault, "_meta", "agent-profiles.md"), profilesDoc, "utf8");
432
+ await fse.writeFile(path.join(vault, ".gitignore"), ".obsidian-memory-rag/\n", "utf8");
433
+ await writeVaultGitWorkspaceSettings(vault, dryRun);
434
+ }
435
+
436
+ // `stripLeadingUtf8Bom` and `atomicWriteJson` moved to `settings-io.mjs` (Phase 0
437
+ // groundwork) — imported above; same behavior, now shared with the other installer modules.
438
+
439
+ /**
440
+ * @param {string} home
441
+ * @param {string} vaultAbs
442
+ * @param {boolean} dryRun
443
+ * @param {{ withHybrid?: boolean, repoRoot?: string | null, semantic?: boolean, vec?: boolean, rerank?: boolean, pinFailures?: boolean, usage?: boolean }} [hybridOpts]
444
+ */
445
+ async function writeCursorMcp(home, vaultAbs, dryRun, hybridOpts = {}) {
446
+ const dir = path.join(home, ".cursor");
447
+ const fp = path.join(dir, "mcp.json");
448
+ let parsed = {};
449
+ /** @type {Buffer | null} */
450
+ let priorBytes = null;
451
+ if (await fse.pathExists(fp)) {
452
+ priorBytes = await fse.readFile(fp);
453
+ const text = stripLeadingUtf8Bom(priorBytes.toString("utf8"));
454
+ try {
455
+ parsed = JSON.parse(text);
456
+ } catch {
457
+ console.warn(
458
+ pc.yellow("Invalid JSON in mcp.json; will back up the original before overwriting")
459
+ );
460
+ }
461
+ }
462
+ let merged = mergeBasicMemoryServer(parsed, vaultAbs);
463
+ const {
464
+ withHybrid = false,
465
+ repoRoot = null,
466
+ semantic = false,
467
+ vec = false,
468
+ rerank = false,
469
+ pinFailures = false,
470
+ usage = false,
471
+ obscura = false,
472
+ obscuraBin = null,
473
+ searxngUrl = null
474
+ } = hybridOpts;
475
+ if (withHybrid && repoRoot) {
476
+ merged = mergeObsidianHybridServer(merged, vaultAbs, path.resolve(repoRoot), {
477
+ semantic,
478
+ vec,
479
+ rerank,
480
+ pinFailures,
481
+ usage
482
+ });
483
+ }
484
+ if (obscura && repoRoot) {
485
+ merged = mergeObscuraWebServer(merged, path.resolve(repoRoot), {
486
+ binPath: obscuraBin,
487
+ searxngUrl
488
+ });
489
+ }
490
+ if (dryRun) {
491
+ console.log(pc.cyan("[dry-run] would write"), fp);
492
+ console.log(JSON.stringify(merged, null, 2));
493
+ return;
494
+ }
495
+ await fse.ensureDir(dir);
496
+ // Always preserve the previous mcp.json before overwriting — agent-driven
497
+ // installs that go wrong should be 1 `mv` away from recovery, not a re-run
498
+ // of the IDE wizard. Backups are kept indefinitely; clean up manually.
499
+ if (priorBytes) {
500
+ const bak = `${fp}.bak.${Date.now()}`;
501
+ await fse.writeFile(bak, priorBytes);
502
+ await restrictFileToOwner(bak);
503
+ console.log(pc.dim("Backed up previous mcp.json to"), bak);
504
+ }
505
+ await atomicWriteJson(fp, merged);
506
+ console.log(pc.green("Wrote"), fp);
507
+ }
508
+
509
+ /**
510
+ * Install a gitleaks `pre-commit` hook in the vault repo so secrets caught at
511
+ * commit time block both the user's interactive commits AND the obsidian-memoryd
512
+ * daemon's auto-commits. Falls through with a warning if gitleaks is not on PATH
513
+ * at commit time — does not hard-fail commits when the tool isn't installed.
514
+ * @param {string} vault
515
+ * @param {boolean} enable
516
+ * @param {boolean} dryRun
517
+ */
518
+ async function maybeInstallGitleaksHook(vault, enable, dryRun) {
519
+ if (!enable) return;
520
+ const gitDir = path.join(vault, ".git");
521
+ if (!(await fse.pathExists(gitDir))) {
522
+ console.warn(pc.yellow("gitleaks hook: vault has no .git; skipping (re-run after git init)"));
523
+ return;
524
+ }
525
+ const hookPath = path.join(gitDir, "hooks", "pre-commit");
526
+ const script = `#!/usr/bin/env sh
527
+ # obsidian-memory vault: gitleaks pre-commit guard
528
+ # Refuses commits that introduce secrets. Install gitleaks per OS:
529
+ # macOS: brew install gitleaks
530
+ # Windows: winget install gitleaks
531
+ # Linux: see https://github.com/gitleaks/gitleaks#installing
532
+ if ! command -v gitleaks >/dev/null 2>&1; then
533
+ echo "gitleaks not installed; pre-commit guard skipped." >&2
534
+ echo " install: https://github.com/gitleaks/gitleaks" >&2
535
+ exit 0
536
+ fi
537
+ exec gitleaks protect --staged --no-banner --redact
538
+ `;
539
+ if (dryRun) {
540
+ console.log(pc.cyan("[dry-run] would install"), hookPath);
541
+ return;
542
+ }
543
+ await fse.ensureDir(path.dirname(hookPath));
544
+ await fse.writeFile(hookPath, script, "utf8");
545
+ if (process.platform !== "win32") {
546
+ try {
547
+ await fse.chmod(hookPath, 0o755);
548
+ } catch {
549
+ /* ignore */
550
+ }
551
+ }
552
+ console.log(pc.green("Installed gitleaks pre-commit hook at"), hookPath);
553
+ }
554
+
555
+ /**
556
+ * Parse `--ide codex,claude` → lowercased array. When `--ide` is omitted the
557
+ * default is `["cursor"]` for back-compat, except under `--full`, whose focus is
558
+ * Codex + Claude Code (`["codex", "claude"]`).
559
+ * @param {string[]} argv
560
+ * @param {{ full?: boolean }} [opts]
561
+ */
562
+ function idesFromArgs(argv, { full = false } = {}) {
563
+ const raw = flagValue(argv, "--ide");
564
+ if (!raw) return full ? ["codex", "claude"] : ["cursor"];
565
+ return raw
566
+ .split(",")
567
+ .map((s) => s.trim().toLowerCase())
568
+ .filter(Boolean);
569
+ }
570
+
571
+ /**
572
+ * Register the MCP servers in Claude Code via its CLI (`claude mcp add -s user`) —
573
+ * Claude Code does not read an mcp.json file. Idempotent: removes any same-scope
574
+ * entry first. Falls back to printing the command if `claude` isn't on PATH.
575
+ * @param {string} vaultAbs
576
+ * @param {boolean} dryRun
577
+ * @param {{ withHybrid?: boolean, repoRoot?: string|null, semantic?: boolean, vec?: boolean, rerank?: boolean, pinFailures?: boolean, usage?: boolean }} [hybridOpts]
578
+ */
579
+ async function registerClaudeCodeMcp(vaultAbs, dryRun, hybridOpts = {}) {
580
+ const {
581
+ withHybrid = false,
582
+ repoRoot = null,
583
+ semantic = false,
584
+ vec = false,
585
+ rerank = false,
586
+ pinFailures = false,
587
+ usage = false,
588
+ obscura = false,
589
+ obscuraBin = null,
590
+ searxngUrl = null
591
+ } = hybridOpts;
592
+ /** @type {Array<[string, object]>} */
593
+ const servers = [["basic-memory", basicMemoryServer(vaultAbs)]];
594
+ if (withHybrid && repoRoot) {
595
+ servers.push([
596
+ "obsidian-memory-hybrid",
597
+ hybridServer(vaultAbs, path.resolve(repoRoot), { semantic, vec, rerank, pinFailures, usage })
598
+ ]);
599
+ }
600
+ if (obscura && repoRoot) {
601
+ servers.push([
602
+ "obscura-web",
603
+ obscuraWebServer(path.resolve(repoRoot), { binPath: obscuraBin, searxngUrl })
604
+ ]);
605
+ }
606
+ for (const [name, server] of servers) {
607
+ const addArgv = claudeAddArgv(name, server);
608
+ if (dryRun) {
609
+ console.log(pc.cyan("[dry-run] claude"), addArgv.join(" "));
610
+ continue;
611
+ }
612
+ try {
613
+ await execa("claude", claudeRemoveArgv(name), { reject: false }); // ignore "not found"
614
+ const r = await execa("claude", addArgv, { reject: false });
615
+ if (r.exitCode === 0) console.log(pc.green("Claude Code MCP added:"), name);
616
+ else {
617
+ console.warn(pc.yellow(`claude mcp add ${name} exited ${r.exitCode}; run manually:`));
618
+ console.log(" claude " + addArgv.join(" "));
619
+ }
620
+ } catch {
621
+ console.warn(pc.yellow("`claude` CLI not found; run manually:"));
622
+ console.log(" claude " + addArgv.join(" "));
623
+ }
624
+ }
625
+ }
626
+
627
+ /**
628
+ * Register the MCP servers in Codex CLI via `codex mcp add` — Codex stores them
629
+ * in `~/.codex/config.toml` and merges the TOML itself, so we shell out instead
630
+ * of editing the file (same approach as Claude Code). Idempotent: removes any
631
+ * same-name entry first. Falls back to printing the command AND a ready-to-paste
632
+ * `config.toml` block if `codex` isn't on PATH.
633
+ * @param {string} vaultAbs
634
+ * @param {boolean} dryRun
635
+ * @param {{ withHybrid?: boolean, repoRoot?: string|null, semantic?: boolean, vec?: boolean, rerank?: boolean, pinFailures?: boolean, usage?: boolean }} [hybridOpts]
636
+ */
637
+ async function registerCodexMcp(vaultAbs, dryRun, hybridOpts = {}) {
638
+ const {
639
+ withHybrid = false,
640
+ repoRoot = null,
641
+ semantic = false,
642
+ vec = false,
643
+ rerank = false,
644
+ pinFailures = false,
645
+ usage = false,
646
+ obscura = false,
647
+ obscuraBin = null,
648
+ searxngUrl = null
649
+ } = hybridOpts;
650
+ /** @type {Array<[string, object]>} */
651
+ const servers = [["basic-memory", basicMemoryServer(vaultAbs)]];
652
+ if (withHybrid && repoRoot) {
653
+ servers.push([
654
+ "obsidian-memory-hybrid",
655
+ hybridServer(vaultAbs, path.resolve(repoRoot), { semantic, vec, rerank, pinFailures, usage })
656
+ ]);
657
+ }
658
+ if (obscura && repoRoot) {
659
+ servers.push([
660
+ "obscura-web",
661
+ obscuraWebServer(path.resolve(repoRoot), { binPath: obscuraBin, searxngUrl })
662
+ ]);
663
+ }
664
+ for (const [name, server] of servers) {
665
+ const addArgv = codexAddArgv(name, server);
666
+ if (dryRun) {
667
+ console.log(pc.cyan("[dry-run] codex"), addArgv.join(" "));
668
+ continue;
669
+ }
670
+ try {
671
+ await execa("codex", codexRemoveArgv(name), { reject: false }); // ignore "not found"
672
+ const r = await execa("codex", addArgv, { reject: false });
673
+ if (r.exitCode === 0) console.log(pc.green("Codex CLI MCP added:"), name);
674
+ else {
675
+ console.warn(pc.yellow(`codex mcp add ${name} exited ${r.exitCode}; add it manually:`));
676
+ console.log(" codex " + addArgv.join(" "));
677
+ console.log(
678
+ pc.dim(" …or paste into ~/.codex/config.toml:\n" + codexTomlBlock(name, server))
679
+ );
680
+ }
681
+ } catch {
682
+ console.warn(pc.yellow("`codex` CLI not found; add it manually:"));
683
+ console.log(" codex " + addArgv.join(" "));
684
+ console.log(
685
+ pc.dim(" …or paste into ~/.codex/config.toml:\n" + codexTomlBlock(name, server))
686
+ );
687
+ }
688
+ }
689
+ }
690
+
691
+ /**
692
+ * Install the Python RAG backend editable (`pip install -e <rag>[semantic]`) so
693
+ * the hybrid MCP + index build work without a separate manual step. Best-effort:
694
+ * prints the command if pip/python is missing or the install fails.
695
+ * @param {string|null} repoRoot
696
+ * @param {boolean} semantic
697
+ * @param {boolean} dryRun
698
+ * @param {boolean} [vec] - also install the `[vec]` extra (sqlite-vec; ADR-0025)
699
+ * @param {boolean} [rerank] - also install the `[rerank]` extra (cross-encoder; ADR-0026)
700
+ */
701
+ async function maybeInstallBackend(repoRoot, semantic, dryRun, vec = false, rerank = false) {
702
+ if (!repoRoot) return;
703
+ const ragPkg = path.join(repoRoot, "packages", "obsidian-memory-rag");
704
+ const extras = [
705
+ semantic ? "semantic" : null,
706
+ vec ? "vec" : null,
707
+ rerank ? "rerank" : null
708
+ ].filter(Boolean);
709
+ const spec = ragPkg + (extras.length ? `[${extras.join(",")}]` : "");
710
+ const py = process.platform === "win32" ? "python" : "python3";
711
+ const args = ["-m", "pip", "install", "-e", spec];
712
+ if (dryRun) {
713
+ console.log(pc.cyan("[dry-run] would install backend:"), py, args.join(" "));
714
+ return;
715
+ }
716
+ try {
717
+ const r = await execa(py, args, { reject: false });
718
+ if (r.exitCode === 0) {
719
+ console.log(
720
+ pc.green("Python backend installed"),
721
+ extras.length ? `(with [${extras.join(",")}])` : ""
722
+ );
723
+ } else {
724
+ console.warn(pc.yellow("Backend install skipped/failed — run it manually:"));
725
+ console.log(' pip install -e "' + spec + '"');
726
+ }
727
+ } catch {
728
+ console.warn(pc.yellow("python not found; install the backend later:"));
729
+ console.log(' pip install -e "' + spec + '"');
730
+ }
731
+ }
732
+
733
+ /**
734
+ * Optionally build the local FTS (+semantic) index so search works on first use.
735
+ * Best-effort: prints the pip command if the Python backend isn't importable.
736
+ * @param {string} vaultAbs
737
+ * @param {boolean} dryRun
738
+ * @param {{ repoRoot?: string|null, semantic?: boolean }} [opts]
739
+ */
740
+ async function maybeBuildIndex(vaultAbs, dryRun, { repoRoot = null, semantic = false } = {}) {
741
+ if (!repoRoot) return;
742
+ const pySrc = path.join(repoRoot, "packages", "obsidian-memory-rag", "src");
743
+ const args = ["-m", "obsidian_memory_rag", "index", "--vault", vaultAbs];
744
+ if (semantic) args.push("--semantic", "--embedder", SEMANTIC_EMBEDDER);
745
+ const py = process.platform === "win32" ? "python" : "python3";
746
+ if (dryRun) {
747
+ console.log(pc.cyan("[dry-run] would build index:"), py, args.join(" "));
748
+ return;
749
+ }
750
+ try {
751
+ const r = await execa(py, args, {
752
+ env: { ...process.env, PYTHONPATH: pySrc, PYTHONUTF8: "1" },
753
+ reject: false
754
+ });
755
+ if (r.exitCode === 0) console.log(pc.green("Index built"), semantic ? "(semantic)" : "(FTS)");
756
+ else {
757
+ const ragPkg = path.join(repoRoot, "packages", "obsidian-memory-rag");
758
+ console.warn(pc.yellow("Index build skipped/failed — install the backend first:"));
759
+ console.log(' pip install -e "' + ragPkg + (semantic ? '[semantic]"' : '"'));
760
+ }
761
+ } catch {
762
+ console.warn(pc.yellow("python not found; build the index later (see docs install-fresh-pc)."));
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Headless / CI path: no prompts. Requires --vault.
768
+ * @param {string[]} argv
769
+ */
770
+ async function runNonInteractive(argv) {
771
+ const cwd = process.cwd();
772
+ const home = process.env.HOME || process.env.USERPROFILE || cwd;
773
+ const lang = langFromArgs();
774
+ const dryRun = dryRunFromArgs();
775
+ const t = messages[lang];
776
+ // Vault path: --vault wins, else the first positional arg, else the default
777
+ // (~/Documents/obsidian-memory-vault). No flag required for the common case.
778
+ const vaultRaw = flagValue(argv, "--vault") || positionalVault(argv);
779
+ const usedDefault = !vaultRaw;
780
+ const vault = path.resolve(cwd, vaultRaw || defaultVaultPath(home));
781
+ // Create a starter vault if the path isn't one yet, instead of erroring — a
782
+ // one-shot install shouldn't require pre-creating the folder by hand.
783
+ let createdVault = false;
784
+ if (!(await fse.pathExists(path.join(vault, ".obsidian")))) {
785
+ if (dryRun) {
786
+ console.log(pc.cyan("[dry-run] would create starter vault at"), vault);
787
+ } else {
788
+ await scaffoldNewVault(vault, lang, dryRun);
789
+ createdVault = true;
790
+ }
791
+ }
792
+ const full = fullPresetFromArgs();
793
+ const noCursorMcp = argv.includes("--no-cursor-mcp");
794
+ const noGitInit = argv.includes("--no-git-init");
795
+ // The full stack is the DEFAULT (v3.8.1): a bare non-interactive install ships
796
+ // every feature (hybrid + semantic + index + backend + sqlite-vec + rules), exactly
797
+ // as `--full` did. `--minimal` opts back down to plain basic-memory; a granular
798
+ // `--no-<piece>` drops one part; an explicit `--with-hybrid`/`--semantic`/… forces a
799
+ // piece on even under `--minimal`. (`--full`/`--all` stay as aliases — they also flip
800
+ // the IDE default to codex,claude — but no longer gate the feature set.)
801
+ const minimal = argv.includes("--minimal");
802
+ const on = (optIn, optOut) => (argv.includes(optIn) || !minimal) && !argv.includes(optOut);
803
+ let wantHybrid = on("--with-hybrid", "--no-hybrid");
804
+ let wantSemantic = on("--semantic", "--no-semantic");
805
+ let wantBuildIndex = on("--build-index", "--no-build-index");
806
+ let wantInstallBackend = on("--install-backend", "--no-install-backend");
807
+ // sqlite-vec acceleration (ADR-0025): ranking-identical with a safe fallback, so
808
+ // shipping it by default costs nothing where the extension can't load.
809
+ let wantVec = on("--vec", "--no-vec");
810
+ // Cross-encoder reranker (ADR-0026): a heavy, model-dependent precision pass.
811
+ // Strictly opt-in — NOT part of the default/full stack — because it downloads a
812
+ // model on first use and only helps with a strong, content-language-matched model.
813
+ // `--rerank` installs the [rerank] extra and sets OBSIDIAN_MEMORY_RERANK=1.
814
+ let wantRerank = wantHybrid && argv.includes("--rerank");
815
+ // ADR-0038 retrieval levers: pin_failures (resurface recorded lessons on matching
816
+ // tasks) and usage boost (reinforce notes the agent demonstrably used). Pure
817
+ // ranking levers over data that is already collected by default (recall_log),
818
+ // bench-gated in CI — so the default stack ships them ON; --no-pin-failures /
819
+ // --no-usage-boost opt out.
820
+ let wantPinFailures = wantHybrid && on("--pin-failures", "--no-pin-failures");
821
+ let wantUsageBoost = wantHybrid && on("--usage-boost", "--no-usage-boost");
822
+ const wantGitleaks = argv.includes("--with-gitleaks");
823
+ const ides = idesFromArgs(argv, { full });
824
+ // Claude Code only: disable the native per-project auto-memory + install the SessionStart
825
+ // hook so the vault is the single source of truth (ADR-0029). On by default when Claude Code
826
+ // is wired; --minimal opts out (re-add with --native-memory-override), --no-native-memory-override
827
+ // forces it off.
828
+ const wantNativeOverride =
829
+ ides.includes("claude") && on("--native-memory-override", "--no-native-memory-override");
830
+ // ADR-0030: deterministic enforcement hooks (PreToolUse guard + Stop nudge) ride along
831
+ // with the native-memory override by default; --no-memory-enforcement opts out of just
832
+ // these two (keeping autoMemoryEnabled:false + the SessionStart hook).
833
+ const wantEnforce = wantNativeOverride && on("--memory-enforcement", "--no-memory-enforcement");
834
+ // ADR-0031: effort-gate hook (PreToolUse) also rides along by default, independently of
835
+ // --memory-enforcement; --no-effort-gate opts out of just this one.
836
+ const wantEffortGate = wantNativeOverride && on("--effort-gate", "--no-effort-gate");
837
+ // vkm-kit token-saver (ADR-0043): PostToolUse compaction hooks (noisy Bash output; MCP
838
+ // JSON whitespace) + permissions.deny rules for token-hungry build artifacts + the
839
+ // vkm-terse output style. Claude Code-only; same default-on / --no-X idiom. The terse
840
+ // style gets its own toggle — changing Claude's voice is a preference, not pure noise
841
+ // removal — but ships ON with the rest.
842
+ const wantTokenSaver = ides.includes("claude") && on("--token-saver", "--no-token-saver");
843
+ const wantTerseStyle = wantTokenSaver && on("--terse-style", "--no-terse-style");
844
+ // vkm-kit local telemetry (ADR-0044): OTEL env → local sink (127.0.0.1:4319) so
845
+ // `vkm-doctor` can report real token/cache usage. Needs a kit clone (sink script);
846
+ // silently skipped without one. Data never leaves the machine.
847
+ const wantTelemetry = ides.includes("claude") && on("--telemetry", "--no-telemetry");
848
+ // vkm-kit skills + subagent template (ADR-0049): /vkm-discipline + /vkm-spec skills and
849
+ // the vkm-implementer agent. Pure files under ~/.claude/, hash-tracked.
850
+ const wantSkills = ides.includes("claude") && on("--skills", "--no-skills");
851
+ const wantAgents = ides.includes("claude") && on("--agents", "--no-agents");
852
+ // Ollama + phi4-mini (ADR-0047, ~2.3GB): gated to explicit --full or --ollama — a bare
853
+ // headless install must not surprise anyone with a multi-GB download. --no-ollama wins.
854
+ const wantOllama = (full || argv.includes("--ollama")) && !argv.includes("--no-ollama");
855
+ // obscura-web (ADR-0051): stealth web fetch + robust search via the local obscura browser.
856
+ // Gated to explicit --full or --obscura (downloads a ~40MB third-party binary); --no-obscura wins.
857
+ const wantObscura = (full || argv.includes("--obscura")) && !argv.includes("--no-obscura");
858
+ let kitRoot = null;
859
+
860
+ if (wantHybrid) {
861
+ kitRoot = await resolveKitRepoRoot({ cwd, argv, pathExists: (p) => fse.pathExists(p) });
862
+ const { hybridJs, pythonSrc } = kitRoot
863
+ ? hybridMcpPathsFromKitRoot(kitRoot)
864
+ : { hybridJs: null, pythonSrc: null };
865
+ const ok = kitRoot && (await fse.pathExists(hybridJs)) && (await fse.pathExists(pythonSrc));
866
+ if (!ok) {
867
+ // Explicit --with-hybrid is a hard requirement (fail loud). But the default
868
+ // full stack is "best effort, max power": when there's no kit clone to source
869
+ // the bridge from (e.g. run via bare `npx` outside a clone), degrade to
870
+ // basic-memory instead of aborting the whole install.
871
+ if (!argv.includes("--with-hybrid")) {
872
+ console.warn(
873
+ pc.yellow(
874
+ "No kit clone found, so hybrid/semantic/index/vec are skipped — wiring basic-memory only."
875
+ )
876
+ );
877
+ console.log(
878
+ pc.dim(
879
+ " For full hybrid power, run from a clone of the kit or pass --repo-root <clone>."
880
+ )
881
+ );
882
+ wantHybrid = false;
883
+ wantSemantic = false;
884
+ wantBuildIndex = false;
885
+ wantInstallBackend = false;
886
+ wantVec = false;
887
+ wantRerank = false;
888
+ wantPinFailures = false;
889
+ wantUsageBoost = false;
890
+ kitRoot = null;
891
+ } else {
892
+ console.error(
893
+ pc.red(
894
+ "--with-hybrid: pass --repo-root <path-to-vkm-kit-clone> or run from that clone (cwd walk), and ensure the bridge + Python src exist."
895
+ )
896
+ );
897
+ process.exit(2);
898
+ }
899
+ }
900
+ }
901
+
902
+ // obscura-web needs the kit clone for its MCP bridge path (same as hybrid); resolve it if
903
+ // the hybrid block above didn't, then best-effort install the pinned+verified obscura binary.
904
+ let obscuraBin = null;
905
+ let wantObscuraFinal = wantObscura;
906
+ const searxngUrl = flagValue(argv, "--searxng-url");
907
+ if (wantObscuraFinal && !kitRoot) {
908
+ kitRoot = await resolveKitRepoRoot({ cwd, argv, pathExists: (p) => fse.pathExists(p) });
909
+ }
910
+ if (wantObscuraFinal && !kitRoot) {
911
+ console.warn(
912
+ pc.yellow(
913
+ "No kit clone found — obscura-web is skipped (run from a clone or pass --repo-root)."
914
+ )
915
+ );
916
+ wantObscuraFinal = false;
917
+ }
918
+ if (wantObscuraFinal) {
919
+ const r = await maybeInstallObscura(dryRun, { enable: true });
920
+ obscuraBin = r.binPath;
921
+ if (r.status === "manual" || r.status === "failed") {
922
+ console.log(
923
+ pc.dim(
924
+ " obscura-web MCP stays wired; its tools fall back to native WebFetch/WebSearch until obscura is installed."
925
+ )
926
+ );
927
+ }
928
+ }
929
+
930
+ console.log(
931
+ pc.cyan(t.title),
932
+ pc.dim(full ? "full preset" : minimal ? "minimal" : "full stack (default)")
933
+ );
934
+
935
+ const mcpSnippet = {
936
+ command: "uvx",
937
+ args: ["basic-memory", "mcp"],
938
+ env: { BASIC_MEMORY_HOME: vault }
939
+ };
940
+
941
+ const hybridOpts = {
942
+ withHybrid: wantHybrid,
943
+ repoRoot: kitRoot,
944
+ semantic: wantSemantic,
945
+ vec: wantVec,
946
+ rerank: wantRerank,
947
+ pinFailures: wantPinFailures,
948
+ usage: wantUsageBoost,
949
+ obscura: wantObscuraFinal,
950
+ obscuraBin,
951
+ searxngUrl
952
+ };
953
+ if (ides.includes("cursor") && !noCursorMcp) {
954
+ await writeCursorMcp(home, vault, dryRun, hybridOpts);
955
+ } else if (ides.includes("cursor") && noCursorMcp) {
956
+ console.log(pc.dim("Skipped Cursor mcp.json (--no-cursor-mcp)"));
957
+ }
958
+ if (ides.includes("claude")) {
959
+ await registerClaudeCodeMcp(vault, dryRun, hybridOpts);
960
+ }
961
+ // Always reconcile (not just install) whenever Claude Code is a wired target for this
962
+ // run: a symmetric call also STRIPS previously-installed pieces a `--no-X` flag now
963
+ // turns off, instead of only ever adding entries (see ADR-0030/0031 install/remove fix).
964
+ if (ides.includes("claude")) {
965
+ await configureClaudeNativeMemory(home, vault, dryRun, {
966
+ lang,
967
+ enable: wantNativeOverride,
968
+ enforce: wantEnforce,
969
+ effortGate: wantEffortGate
970
+ });
971
+ // Same symmetric-reconcile contract as above: `--no-token-saver` on a re-run strips a
972
+ // previously-installed token-saver instead of merely skipping it.
973
+ await configureTokenSaver(home, dryRun, {
974
+ hooks: wantTokenSaver,
975
+ terseStyle: wantTerseStyle,
976
+ denyRules: wantTokenSaver
977
+ });
978
+ await configureTelemetry(home, dryRun, { enable: wantTelemetry, kitRoot });
979
+ await configureSkillAssets(home, dryRun, { skills: wantSkills, agents: wantAgents });
980
+ if (wantOllama) {
981
+ await maybeInstallOllama(dryRun, { enable: true });
982
+ }
983
+ }
984
+ if (ides.includes("codex")) {
985
+ await registerCodexMcp(vault, dryRun, hybridOpts);
986
+ }
987
+ // Install the Python backend before building the index so the build can succeed
988
+ // on a fresh machine in the same run.
989
+ if (wantInstallBackend && kitRoot) {
990
+ await maybeInstallBackend(kitRoot, wantSemantic, dryRun, wantVec, wantRerank);
991
+ }
992
+ if (wantBuildIndex) {
993
+ await maybeBuildIndex(vault, dryRun, { repoRoot: kitRoot, semantic: wantSemantic });
994
+ }
995
+
996
+ // Rules ship by default too (part of "everything"); --no-rules / --minimal opt out.
997
+ const ruleTargets = rulesTargetsFromArgs(argv, ides, { full: !minimal });
998
+ if (ruleTargets.length) {
999
+ await installRules(ruleTargets, lang, { home, cwd, dryRun });
1000
+ }
1001
+
1002
+ if (!noGitInit && dryRun) {
1003
+ console.log(pc.cyan("[dry-run] would run"), "git init", pc.dim(`(cwd ${vault})`));
1004
+ } else if (!noGitInit && !(await fse.pathExists(path.join(vault, ".git")))) {
1005
+ await execa("git", ["init"], { cwd: vault, stdio: "inherit" });
1006
+ }
1007
+
1008
+ await writeVaultGitWorkspaceSettings(vault, dryRun);
1009
+ await maybeInstallGitleaksHook(vault, wantGitleaks, dryRun);
1010
+
1011
+ console.log(pc.green("\n" + t.summary));
1012
+ console.log(
1013
+ "- Vault:",
1014
+ vault + (usedDefault ? " (default)" : "") + (createdVault ? " (created)" : "")
1015
+ );
1016
+ console.log("- MCP:", JSON.stringify(mcpSnippet));
1017
+ if (wantHybrid && kitRoot) {
1018
+ console.log("- obsidian-memory-hybrid: merged (kit root", kitRoot + ")");
1019
+ const ragPkg = path.join(kitRoot, "packages", "obsidian-memory-rag");
1020
+ const extras = [
1021
+ wantSemantic ? "semantic" : null,
1022
+ wantVec ? "vec" : null,
1023
+ wantRerank ? "rerank" : null
1024
+ ].filter(Boolean);
1025
+ const extraSpec = extras.length ? `[${extras.join(",")}]` : "";
1026
+ console.log(pc.dim(' pip install -e "' + ragPkg + extraSpec + '"'));
1027
+ if (wantSemantic) {
1028
+ console.log(
1029
+ pc.dim(
1030
+ " embedder: fastembed (OBSIDIAN_MEMORY_EMBEDDER); build once with vault_fts_index semantic:true"
1031
+ )
1032
+ );
1033
+ }
1034
+ if (wantVec) {
1035
+ console.log(
1036
+ pc.dim(
1037
+ " sqlite-vec acceleration: OBSIDIAN_MEMORY_SQLITE_VEC=1 (ranking-identical; ADR-0025)"
1038
+ )
1039
+ );
1040
+ }
1041
+ if (wantRerank) {
1042
+ console.log(
1043
+ pc.dim(
1044
+ " cross-encoder reranker: OBSIDIAN_MEMORY_RERANK=1 (multilingual model downloads on first use; ADR-0026)"
1045
+ )
1046
+ );
1047
+ }
1048
+ }
1049
+ if (ides.includes("claude")) {
1050
+ console.log(
1051
+ "- Claude Code: MCP registered via `claude mcp add -s user` (verify: `claude mcp list`)"
1052
+ );
1053
+ }
1054
+ if (wantNativeOverride) {
1055
+ console.log(
1056
+ "- Claude Code native memory: DISABLED (autoMemoryEnabled:false) + SessionStart vault hook → ~/.claude/settings.json"
1057
+ );
1058
+ } else if (ides.includes("claude")) {
1059
+ console.log(
1060
+ "- Claude Code native memory: left/reverted to Claude's own default (any prior override + hooks from this kit were removed if present)"
1061
+ );
1062
+ }
1063
+ if (wantEnforce) {
1064
+ console.log(
1065
+ "- Deterministic enforcement (ADR-0030): PreToolUse guard (denies writes into native memory) + Stop nudge (close-ritual reminder) → ~/.claude/settings.json"
1066
+ );
1067
+ }
1068
+ if (wantEffortGate) {
1069
+ console.log(
1070
+ "- Effort gate (ADR-0031): PreToolUse hook (denies the 2nd+ substantive edit until the model proposes an effort level and the user replies) → ~/.claude/settings.json"
1071
+ );
1072
+ }
1073
+ if (wantTokenSaver) {
1074
+ console.log(
1075
+ "- Token-saver (ADR-0043): PostToolUse compaction hooks (Bash + mcp__.*) + permissions.deny noise rules" +
1076
+ (wantTerseStyle ? " + vkm-terse output style" : "") +
1077
+ " → ~/.claude/settings.json (kill switch: VKM_TOKEN_SAVER=0)"
1078
+ );
1079
+ }
1080
+ if (ides.includes("codex")) {
1081
+ console.log(
1082
+ "- Codex CLI: MCP registered via `codex mcp add` → ~/.codex/config.toml (verify: `codex mcp list`)"
1083
+ );
1084
+ }
1085
+ if (wantGitleaks) {
1086
+ console.log("- gitleaks pre-commit hook: installed (vault/.git/hooks/pre-commit)");
1087
+ }
1088
+ if (ruleTargets.length) {
1089
+ console.log("- Memory rules installed into:", ruleTargets.join(", "));
1090
+ if (ides.includes("cursor")) {
1091
+ console.log(
1092
+ pc.dim(
1093
+ " Cursor GLOBAL User Rules can't be auto-written — paste the block from install.md Step 4 into Settings → Rules → User Rules."
1094
+ )
1095
+ );
1096
+ }
1097
+ }
1098
+ console.log("-", t.ftsHint);
1099
+ }
1100
+
1101
+ async function main() {
1102
+ const argv = process.argv;
1103
+ if (argv.includes("--help")) {
1104
+ console.log(`Usage: create-vkm-kit [vault] [options]
1105
+
1106
+ Examples:
1107
+ create-vkm-kit # interactive wizard (pre-selects everything)
1108
+ create-vkm-kit ./my-vault -y # headless, FULL stack by default, into ./my-vault
1109
+ create-vkm-kit -y # headless, FULL stack, default vault path
1110
+ create-vkm-kit -y --minimal # headless, plain basic-memory only
1111
+
1112
+ The install is the FULL stack BY DEFAULT (hybrid + semantic + index + sqlite-vec +
1113
+ rules); run it from a clone of the kit (or pass --repo-root) for the hybrid pieces —
1114
+ it degrades to basic-memory (no abort) when no clone is found. Use --minimal for plain
1115
+ basic-memory, or --no-<piece> to drop one part.
1116
+
1117
+ Interactive (default):
1118
+ --lang en English prompts
1119
+ --dry-run Show what would be written (no writes)
1120
+
1121
+ Feature set:
1122
+ --full, --all Alias for the default full stack PLUS --ide codex,claude (implies -y).
1123
+ Since the full stack is now the default, --full mainly flips the IDE
1124
+ default to codex,claude. Opt out of a piece with --no-semantic /
1125
+ --no-vec / --no-pin-failures / --no-usage-boost / --no-build-index /
1126
+ --no-install-backend / --no-rules.
1127
+ --minimal Opposite of the default: install plain basic-memory only (no hybrid,
1128
+ semantic, index, vec, or rules). Re-add one piece with --with-hybrid /
1129
+ --semantic / --vec / --build-index / --install-backend / --rules.
1130
+
1131
+ Headless (CI / scripts) — add -y (aliases: --yes, --non-interactive):
1132
+ [vault] Vault path as the first argument (or --vault <path>); defaults to
1133
+ ~/Documents/obsidian-memory-vault and is created if it doesn't exist.
1134
+ --ide <list> IDEs to wire, comma-separated: codex, claude, cursor (default: cursor;
1135
+ with --full: codex,claude). codex uses the Codex CLI (codex mcp add →
1136
+ ~/.codex/config.toml); claude uses the Claude Code CLI (claude mcp add
1137
+ -s user); cursor writes ~/.cursor/mcp.json.
1138
+ --no-cursor-mcp Skip writing ~/.cursor/mcp.json
1139
+ --no-git-init Skip git init when .git is missing
1140
+ (Merges kit Git/SCM keys into <vault>/.vscode/settings.json — creates or updates.)
1141
+ --with-hybrid Also wire obsidian-memory-hybrid (needs kit clone; use --repo-root or cwd walk)
1142
+ --repo-root <path> Root of vkm-kit clone (hybrid bridge + Python src)
1143
+ --semantic With --with-hybrid: neural embeddings (fastembed multilingual; needs the [semantic] extra)
1144
+ --vec With --with-hybrid: sqlite-vec acceleration (needs the [vec] extra; sets
1145
+ OBSIDIAN_MEMORY_SQLITE_VEC=1). Ranking-identical, safe fallback. On under --full.
1146
+ --rerank With --with-hybrid: cross-encoder reranker (installs the [rerank] extra;
1147
+ sets OBSIDIAN_MEMORY_RERANK=1). Opt-in only (downloads a model on first use;
1148
+ uses the multilingual default) — NOT enabled by --full.
1149
+ --pin-failures With --with-hybrid: resurface recorded KNOWN_FAILURES lessons on matching
1150
+ tasks (sets OBSIDIAN_MEMORY_PIN_FAILURES=1; ADR-0038). ON by default;
1151
+ opt out with --no-pin-failures.
1152
+ --usage-boost With --with-hybrid: boost notes the agent demonstrably used, from the
1153
+ local recall_log telemetry (sets OBSIDIAN_MEMORY_USAGE_BOOST=1; ADR-0038).
1154
+ ON by default; opt out with --no-usage-boost.
1155
+ --build-index After wiring, build the local FTS (+semantic) index (needs the Python backend)
1156
+ --install-backend pip install -e the Python RAG backend (best-effort; on by default under --full)
1157
+ --with-gitleaks Install gitleaks pre-commit hook in <vault>/.git/hooks/
1158
+ --rules <list> Install the memory-rules block into agent configs: claude
1159
+ (~/.claude/CLAUDE.md, global), codex (~/.codex/AGENTS.md, global),
1160
+ agents (./AGENTS.md), cursor (.cursor/rules/obsidian-memory.mdc).
1161
+ Or 'all' / 'none'. Headless installs rules by DEFAULT (for each wired
1162
+ agent); pass --no-rules or --minimal to skip, or --rules <list> to
1163
+ choose. Interactive asks (deriving from --ide). Idempotent marked block
1164
+ (obsidian-memory:start/end) — never clobbers content.
1165
+ --no-rules Don't write any rules file.
1166
+
1167
+ Claude Code native-memory override (when --ide includes claude):
1168
+ By default a Claude Code install also DISABLES Claude's native per-project
1169
+ auto-memory (writes "autoMemoryEnabled": false into ~/.claude/settings.json) and
1170
+ installs a SessionStart hook (~/.claude/hooks/session-start-vault-context.mjs)
1171
+ that injects the vault map + "vault is the only source of truth" reminders. This
1172
+ makes the Obsidian vault win over the native memory out of the box (ADR-0029).
1173
+ Idempotent (merges settings.json; never duplicates the hook). --minimal opts out.
1174
+ --native-memory-override Force it on (even under --minimal).
1175
+ --no-native-memory-override Leave Claude's native auto-memory untouched.
1176
+
1177
+ Deterministic enforcement (ADR-0030, on by default with the override above) — makes
1178
+ the doctrine real for ANY model, old or new, not just ones that reliably read prose
1179
+ rules: a PreToolUse hook DENIES Write/Edit/MultiEdit/NotebookEdit into the native
1180
+ auto-memory directory, and a Stop hook nudges the close ritual once per turn when the
1181
+ session did substantive file work but never touched the vault.
1182
+ --memory-enforcement Force the two enforcement hooks on (even under --minimal).
1183
+ --no-memory-enforcement Keep autoMemoryEnabled:false + SessionStart, skip the
1184
+ PreToolUse guard and Stop nudge.
1185
+
1186
+ Effort gate (ADR-0031, on by default with the override above, independent of the
1187
+ enforcement pair) — makes a pause real instead of just announced: a PreToolUse hook
1188
+ DENIES a session's 2nd+ substantive edit (Write/Edit/MultiEdit/NotebookEdit) until the
1189
+ model has proposed an effort level (/effort low|medium|high|xhigh|max) and gotten an
1190
+ actual reply from the user. The first substantive edit of a session is always free.
1191
+ --effort-gate Force the effort-gate hook on (even under --minimal).
1192
+ --no-effort-gate Keep the override (+ enforcement, if on) without the
1193
+ effort-gate hook.
1194
+
1195
+ Token-saver (ADR-0043, on by default when --ide includes claude) — cuts token spend
1196
+ with zero quality loss: two PostToolUse hooks compact noisy tool output before it
1197
+ enters context (Bash: ANSI/progress/dup-line cleanup + head/tail windowing that HARD
1198
+ preserves error/warn/fail lines; mcp__.*: whitespace-only JSON compaction), plus
1199
+ permissions.deny rules for token-hungry artifacts (node_modules, dist, lockfiles) and
1200
+ the vkm-terse output style (~/.claude/output-styles/vkm-terse.md, activated via the
1201
+ outputStyle setting). Runtime kill switch without uninstalling: VKM_TOKEN_SAVER=0.
1202
+ --token-saver Force it on (even under --minimal).
1203
+ --no-token-saver Skip/remove hooks + deny rules + style.
1204
+ --terse-style Force just the output style on.
1205
+ --no-terse-style Keep the hooks + deny rules but not the output style.
1206
+
1207
+ Local telemetry + doctor (ADR-0044, on by default when --ide includes claude and a kit
1208
+ clone is available) — wires Claude Code's OTEL metrics export to a LOCAL sink
1209
+ (127.0.0.1:4319 → ~/.vkm/telemetry/, nothing leaves the machine) via a managed env
1210
+ block + a SessionStart hook that spawns the sink. Run \`vkm-doctor\` for token usage,
1211
+ cache-hit ratio and a broken-cache diagnosis.
1212
+ --telemetry / --no-telemetry Force on / remove.
1213
+
1214
+ Skills + subagent (ADR-0049, on by default when --ide includes claude): /vkm-discipline
1215
+ (dense minimal-line code at full quality + verification contract), /vkm-spec (idea →
1216
+ precise spec via one assemble_context call), and the vkm-implementer agent template.
1217
+ Hash-tracked files; uninstall never deletes one you edited.
1218
+ --skills / --no-skills Force on / remove the two skills.
1219
+ --agents / --no-agents Force on / remove the subagent template.
1220
+
1221
+ Obscura web (ADR-0051, opt-in via --obscura or --full) — routes web access through the
1222
+ local obscura headless browser (anti-detection) instead of the native tools: an MCP
1223
+ (obscura-web) exposing obscura_fetch (stealth URL fetch) and obscura_search (SearXNG JSON →
1224
+ obscura-rendered multi-engine SERP → native fallback). Soft enforcement — obscura is
1225
+ preferred, native WebFetch/WebSearch stay as the fallback. The pinned obscura binary is
1226
+ downloaded + SHA-256-verified into ~/.vkm/obscura/ (needs a kit clone for the MCP bridge).
1227
+ --obscura / --no-obscura Install + wire obscura-web (on under --full) / skip it.
1228
+ --searxng-url <url> Point obscura_search at a SearXNG JSON instance (e.g.
1229
+ http://127.0.0.1:8888) for the most robust, structured layer.
1230
+
1231
+ Uninstall: --no-native-memory-override / --no-memory-enforcement / --no-effort-gate on a
1232
+ re-run now ACTIVELY REMOVE the matching pieces this kit previously installed (not just
1233
+ skip adding them) — a symmetric install/remove path, so toggling a flag off actually
1234
+ reverses a prior toggle-on. For a full teardown of everything this kit's Claude Code
1235
+ integration installed in one shot, use:
1236
+ --uninstall Remove all 4 managed hook entries + the autoMemoryEnabled override from
1237
+ ~/.claude/settings.json, and delete the hook script files this kit
1238
+ installed under ~/.claude/hooks/ (the 4 hooks + a small shared helper
1239
+ module; each one only if a marker check confirms this kit wrote it —
1240
+ never a user's own same-named file). Does NOT touch MCP server
1241
+ registrations, the vault, or rules blocks — see docs/en/faq.md for those.
1242
+ Combine with --dry-run to preview. Exits immediately after (no other
1243
+ install steps run).
1244
+
1245
+ --help This message`);
1246
+ return;
1247
+ }
1248
+
1249
+ if (argv.includes("--uninstall")) {
1250
+ const cwd = process.cwd();
1251
+ const home = process.env.HOME || process.env.USERPROFILE || cwd;
1252
+ const dryRun = dryRunFromArgs();
1253
+ await uninstallClaudeNativeMemory(home, dryRun);
1254
+ await uninstallTokenSaver(home, dryRun);
1255
+ await uninstallTelemetry(home, dryRun);
1256
+ await uninstallSkillAssets(home, dryRun);
1257
+ return;
1258
+ }
1259
+
1260
+ if (nonInteractiveFromArgs()) {
1261
+ await runNonInteractive(argv);
1262
+ return;
1263
+ }
1264
+
1265
+ const lang = langFromArgs();
1266
+ const dryRun = dryRunFromArgs();
1267
+ const t = messages[lang];
1268
+ console.log(pc.cyan(t.title), pc.dim("v2 / v3"));
1269
+ if (dryRun) console.log(pc.dim("dry-run: Cursor mcp.json will not be written"));
1270
+
1271
+ const cwd = process.cwd();
1272
+ const home = process.env.HOME || process.env.USERPROFILE || cwd;
1273
+ const posVault = positionalVault(argv);
1274
+ let vault = posVault ? path.resolve(cwd, posVault) : await findVault(cwd, home);
1275
+
1276
+ // A positional path that isn't a vault yet → scaffold it, no prompt needed.
1277
+ if (vault && !(await fse.pathExists(path.join(vault, ".obsidian")))) {
1278
+ await scaffoldNewVault(vault, lang, dryRun);
1279
+ }
1280
+
1281
+ if (!vault) {
1282
+ const { ok } = await prompts({
1283
+ type: "confirm",
1284
+ name: "ok",
1285
+ message: t.createVault,
1286
+ initial: true
1287
+ });
1288
+ if (ok) {
1289
+ vault = defaultVaultPath(home);
1290
+ await scaffoldNewVault(vault, lang, dryRun);
1291
+ } else {
1292
+ const { p } = await prompts({
1293
+ type: "text",
1294
+ name: "p",
1295
+ message: t.vaultQ,
1296
+ initial: defaultVaultPath(home)
1297
+ });
1298
+ vault = p;
1299
+ }
1300
+ }
1301
+
1302
+ if (!vault) {
1303
+ console.error(pc.red("No vault path; aborted."));
1304
+ process.exit(1);
1305
+ }
1306
+ vault = path.resolve(cwd, vault);
1307
+
1308
+ const { ides } = await prompts({
1309
+ type: "multiselect",
1310
+ name: "ides",
1311
+ message: t.ides,
1312
+ choices: [
1313
+ { title: "Codex CLI", value: "codex", selected: true },
1314
+ { title: "Claude Code", value: "claude", selected: true },
1315
+ { title: "Cursor", value: "cursor", selected: false },
1316
+ { title: "VS Code / Cline", value: "cline", selected: false },
1317
+ { title: "Windsurf", value: "windsurf", selected: false },
1318
+ { title: "Zed", value: "zed", selected: false }
1319
+ ]
1320
+ });
1321
+
1322
+ const { gitleaks } = await prompts({
1323
+ type: "confirm",
1324
+ name: "gitleaks",
1325
+ message: t.gitleaks,
1326
+ initial: true
1327
+ });
1328
+
1329
+ const { age } = await prompts({
1330
+ type: "confirm",
1331
+ name: "age",
1332
+ message: t.age,
1333
+ initial: false
1334
+ });
1335
+
1336
+ const { daemon } = await prompts({
1337
+ type: "confirm",
1338
+ name: "daemon",
1339
+ message: t.daemon,
1340
+ initial: process.platform !== "win32"
1341
+ });
1342
+
1343
+ let hybridOpts = { withHybrid: false, repoRoot: null };
1344
+ if (ides?.includes("cursor") || ides?.includes("claude") || ides?.includes("codex")) {
1345
+ const kitRoot = await resolveKitRepoRoot({
1346
+ cwd,
1347
+ argv: process.argv,
1348
+ pathExists: (p) => fse.pathExists(p)
1349
+ });
1350
+ if (kitRoot) {
1351
+ const { hybrid } = await prompts({
1352
+ type: "confirm",
1353
+ name: "hybrid",
1354
+ message: t.hybridQ,
1355
+ initial: true
1356
+ });
1357
+ if (hybrid) {
1358
+ const { hybridJs, pythonSrc } = hybridMcpPathsFromKitRoot(kitRoot);
1359
+ if ((await fse.pathExists(hybridJs)) && (await fse.pathExists(pythonSrc))) {
1360
+ const { semantic } = await prompts({
1361
+ type: "confirm",
1362
+ name: "semantic",
1363
+ message: t.semanticQ,
1364
+ initial: true
1365
+ });
1366
+ // vec (sqlite-vec acceleration) rides along with hybrid: ranking-identical
1367
+ // and falls back safely, so the default install ships it (ADR-0025).
1368
+ // Same deal for the ADR-0038 retrieval levers (pin_failures + usage boost):
1369
+ // ranking-only, bench-gated, data already collected by default.
1370
+ hybridOpts = {
1371
+ withHybrid: true,
1372
+ repoRoot: kitRoot,
1373
+ semantic: Boolean(semantic),
1374
+ vec: true,
1375
+ pinFailures: true,
1376
+ usage: true
1377
+ };
1378
+ } else {
1379
+ console.warn(pc.yellow("Hybrid paths not found; skipping obsidian-memory-hybrid."));
1380
+ }
1381
+ }
1382
+ }
1383
+ }
1384
+
1385
+ const mcpSnippet = {
1386
+ command: "uvx",
1387
+ args: ["basic-memory", "mcp"],
1388
+ env: { BASIC_MEMORY_HOME: vault }
1389
+ };
1390
+
1391
+ if (ides?.includes("cursor")) {
1392
+ await writeCursorMcp(home, vault, dryRun, hybridOpts);
1393
+ }
1394
+ if (ides?.includes("claude")) {
1395
+ await registerClaudeCodeMcp(vault, dryRun, hybridOpts);
1396
+ }
1397
+ // Claude Code only: vault > native auto-memory (ADR-0029). On by default when Claude Code
1398
+ // is selected; opt out with --no-native-memory-override.
1399
+ const wantNativeOverride =
1400
+ Boolean(ides?.includes("claude")) && !process.argv.includes("--no-native-memory-override");
1401
+ // ADR-0030: deterministic enforcement hooks ride along by default; opt out with
1402
+ // --no-memory-enforcement (keeps autoMemoryEnabled:false + the SessionStart hook).
1403
+ const wantEnforce = wantNativeOverride && !process.argv.includes("--no-memory-enforcement");
1404
+ // ADR-0031: effort-gate hook also rides along by default, independently of
1405
+ // --memory-enforcement; opt out with --no-effort-gate.
1406
+ const wantEffortGate = wantNativeOverride && !process.argv.includes("--no-effort-gate");
1407
+ // Always reconcile (not just install) whenever Claude Code was selected: a symmetric call
1408
+ // also STRIPS previously-installed pieces `--no-native-memory-override`/`--no-memory-enforcement`/
1409
+ // `--no-effort-gate` now turn off, instead of only ever adding entries.
1410
+ // vkm-kit token-saver (ADR-0043) rides along in the wizard too; opt out with
1411
+ // --no-token-saver / --no-terse-style.
1412
+ const wantTokenSaver =
1413
+ Boolean(ides?.includes("claude")) && !process.argv.includes("--no-token-saver");
1414
+ const wantTerseStyle = wantTokenSaver && !process.argv.includes("--no-terse-style");
1415
+ if (ides?.includes("claude")) {
1416
+ await configureClaudeNativeMemory(home, vault, dryRun, {
1417
+ lang,
1418
+ enable: wantNativeOverride,
1419
+ enforce: wantEnforce,
1420
+ effortGate: wantEffortGate
1421
+ });
1422
+ await configureTokenSaver(home, dryRun, {
1423
+ hooks: wantTokenSaver,
1424
+ terseStyle: wantTerseStyle,
1425
+ denyRules: wantTokenSaver
1426
+ });
1427
+ await configureTelemetry(home, dryRun, {
1428
+ // Independent of wantTokenSaver — telemetry is its own ADR-0044 toggle
1429
+ // (matches the headless path's `wantTelemetry` below); coupling it to
1430
+ // token-saver meant --no-token-saver on a rerun silently stripped a
1431
+ // previously-installed telemetry sink the user never asked to touch.
1432
+ enable: Boolean(ides?.includes("claude")) && !process.argv.includes("--no-telemetry"),
1433
+ kitRoot: hybridOpts.repoRoot || null
1434
+ });
1435
+ await configureSkillAssets(home, dryRun, {
1436
+ skills: !process.argv.includes("--no-skills"),
1437
+ agents: !process.argv.includes("--no-agents")
1438
+ });
1439
+ }
1440
+ if (ides?.includes("codex")) {
1441
+ await registerCodexMcp(vault, dryRun, hybridOpts);
1442
+ }
1443
+
1444
+ let ruleTargets = rulesTargetsFromArgs(process.argv, ides || [], { defaultFromIde: true });
1445
+ if (ruleTargets.length && !process.argv.includes("--no-rules")) {
1446
+ const { rules } = await prompts({
1447
+ type: "confirm",
1448
+ name: "rules",
1449
+ message: t.rulesQ,
1450
+ initial: true
1451
+ });
1452
+ if (!rules) ruleTargets = [];
1453
+ }
1454
+ if (ruleTargets.length) {
1455
+ await installRules(ruleTargets, lang, { home, cwd, dryRun });
1456
+ }
1457
+
1458
+ const others = (ides || []).filter((x) => x !== "cursor" && x !== "claude" && x !== "codex");
1459
+ if (others.length) {
1460
+ console.log(pc.yellow(t.otherIdes), others.join(", "));
1461
+ console.log(JSON.stringify({ mcpServers: { "basic-memory": mcpSnippet } }, null, 2));
1462
+ }
1463
+
1464
+ if (dryRun) {
1465
+ console.log(pc.cyan("[dry-run] would run"), "git init", pc.dim(`(cwd ${vault})`));
1466
+ } else if (!(await fse.pathExists(path.join(vault, ".git")))) {
1467
+ await execa("git", ["init"], { cwd: vault, stdio: "inherit" });
1468
+ }
1469
+
1470
+ await writeVaultGitWorkspaceSettings(vault, dryRun);
1471
+ await maybeInstallGitleaksHook(vault, Boolean(gitleaks), dryRun);
1472
+
1473
+ console.log(pc.green("\n" + t.summary));
1474
+ console.log("- Vault:", vault);
1475
+ console.log("- MCP:", JSON.stringify(mcpSnippet));
1476
+ if (hybridOpts.withHybrid && hybridOpts.repoRoot) {
1477
+ console.log("- obsidian-memory-hybrid: enabled (kit", hybridOpts.repoRoot + ")");
1478
+ const ragPkg = path.join(hybridOpts.repoRoot, "packages", "obsidian-memory-rag");
1479
+ const extras = [hybridOpts.semantic ? "semantic" : null, hybridOpts.vec ? "vec" : null].filter(
1480
+ Boolean
1481
+ );
1482
+ console.log(
1483
+ pc.dim(' pip install -e "' + ragPkg + (extras.length ? `[${extras.join(",")}]` : "") + '"')
1484
+ );
1485
+ if (hybridOpts.semantic) {
1486
+ console.log(pc.dim(" embedder: fastembed; build once with vault_fts_index semantic:true"));
1487
+ }
1488
+ if (hybridOpts.vec) {
1489
+ console.log(pc.dim(" sqlite-vec acceleration: OBSIDIAN_MEMORY_SQLITE_VEC=1 (ADR-0025)"));
1490
+ }
1491
+ }
1492
+ if (ides?.includes("claude")) {
1493
+ console.log(
1494
+ "- Claude Code: MCP registered via `claude mcp add -s user` (verify: `claude mcp list`)"
1495
+ );
1496
+ }
1497
+ if (wantNativeOverride) {
1498
+ console.log(
1499
+ "- Claude Code native memory: DISABLED (autoMemoryEnabled:false) + SessionStart vault hook → ~/.claude/settings.json"
1500
+ );
1501
+ } else if (ides?.includes("claude")) {
1502
+ console.log(
1503
+ "- Claude Code native memory: left/reverted to Claude's own default (any prior override + hooks from this kit were removed if present)"
1504
+ );
1505
+ }
1506
+ if (wantEnforce) {
1507
+ console.log(
1508
+ "- Deterministic enforcement (ADR-0030): PreToolUse guard (denies writes into native memory) + Stop nudge (close-ritual reminder) → ~/.claude/settings.json"
1509
+ );
1510
+ }
1511
+ if (wantEffortGate) {
1512
+ console.log(
1513
+ "- Effort gate (ADR-0031): PreToolUse hook (denies the 2nd+ substantive edit until the model proposes an effort level and the user replies) → ~/.claude/settings.json"
1514
+ );
1515
+ }
1516
+ if (ides?.includes("codex")) {
1517
+ console.log(
1518
+ "- Codex CLI: MCP registered via `codex mcp add` → ~/.codex/config.toml (verify: `codex mcp list`)"
1519
+ );
1520
+ }
1521
+ console.log("-", t.ftsHint);
1522
+ if (gitleaks)
1523
+ console.log(
1524
+ "- gitleaks pre-commit hook: installed (vault/.git/hooks/pre-commit); install gitleaks CLI to activate"
1525
+ );
1526
+ if (age) console.log("- age: document keys outside repo");
1527
+ if (daemon)
1528
+ console.log(
1529
+ "- obsidian-memoryd:",
1530
+ "`obsidian-memoryd service install --user && obsidian-memoryd service start`"
1531
+ );
1532
+ if (ruleTargets.length) {
1533
+ console.log("- Memory rules installed into:", ruleTargets.join(", "));
1534
+ if (ides?.includes("cursor")) {
1535
+ console.log(
1536
+ pc.dim(
1537
+ " Cursor GLOBAL User Rules can't be auto-written — paste install.md Step 4 into Settings → Rules → User Rules."
1538
+ )
1539
+ );
1540
+ }
1541
+ }
1542
+ }
1543
+
1544
+ main().catch((e) => {
1545
+ console.error(e);
1546
+ process.exit(1);
1547
+ });