@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.
- package/LICENSE.md +44 -0
- package/README.md +148 -0
- package/package.json +57 -0
- package/src/asset-install.mjs +109 -0
- package/src/claude-native-memory.mjs +507 -0
- package/src/file-perms.mjs +53 -0
- package/src/hooks/_transcript-cache.mjs +223 -0
- package/src/hooks/compact-mcp-output.mjs +87 -0
- package/src/hooks/compact-tool-output.mjs +177 -0
- package/src/hooks/ensure-otel-sink.mjs +51 -0
- package/src/hooks/guard-effort-gate.mjs +209 -0
- package/src/hooks/guard-native-memory-write.mjs +129 -0
- package/src/hooks/session-start-vault-context.mjs +200 -0
- package/src/hooks/stop-vault-close-reminder.mjs +150 -0
- package/src/index.js +1547 -0
- package/src/mcp-merge.mjs +279 -0
- package/src/memory-rules.mjs +205 -0
- package/src/obscura-setup.mjs +272 -0
- package/src/ollama-setup.mjs +126 -0
- package/src/rules-merge.mjs +106 -0
- package/src/settings-io.mjs +193 -0
- package/src/settings-writers.mjs +150 -0
- package/src/skills-install.mjs +96 -0
- package/src/telemetry.mjs +154 -0
- package/src/token-saver.mjs +248 -0
- package/templates/agents/vkm-implementer.md +23 -0
- package/templates/output-styles/vkm-terse.md +23 -0
- package/templates/skills/vkm-discipline/SKILL.md +77 -0
- package/templates/skills/vkm-discipline/domains/coding.md +39 -0
- package/templates/skills/vkm-discipline/domains/data.md +37 -0
- package/templates/skills/vkm-discipline/domains/debugging.md +41 -0
- package/templates/skills/vkm-discipline/domains/design-ui.md +41 -0
- package/templates/skills/vkm-discipline/domains/expertise.md +26 -0
- package/templates/skills/vkm-discipline/domains/infra.md +35 -0
- package/templates/skills/vkm-discipline/domains/llm-artifacts.md +30 -0
- package/templates/skills/vkm-discipline/domains/security.md +37 -0
- package/templates/skills/vkm-discipline/domains/web-search.md +43 -0
- package/templates/skills/vkm-discipline/domains/writing.md +32 -0
- 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
|
+
});
|