@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/LICENSE.md ADDED
@@ -0,0 +1,44 @@
1
+ # Licencia de libre uso con atribución obligatoria — obsidian-memory-kit
2
+
3
+ _(Basada en la Licencia MIT, con una condición adicional de atribución visible.
4
+ Based on the MIT License, with one additional visible-attribution condition.)_
5
+
6
+ Copyright (c) 2026 Jorge David Hidalgo Oporta («Vahlame») — <vahlame@gmail.com>
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ 1. The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ 2. **Mandatory visible attribution / Atribución visible obligatoria:** any
19
+ copy, use, fork, or derivative work of this project must display the
20
+ visible attribution
21
+
22
+ **«© Jorge David Hidalgo Oporta (Vahlame) — kit de memoria original»**
23
+
24
+ together with a link to the original repository
25
+ (<https://github.com/Vahlame/obsidian-memory-kit>), for example in the
26
+ README or equivalent primary documentation.
27
+
28
+ _En español: toda copia, uso, fork o derivado de este proyecto debe incluir
29
+ la atribución visible indicada arriba junto con el enlace al repositorio
30
+ original, por ejemplo en el README o documentación principal equivalente._
31
+
32
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
+ SOFTWARE.
39
+
40
+ ---
41
+
42
+ _Este es el único proyecto de Vahlame de libre uso; todos los demás son
43
+ propietarios y todo uso se acuerda directamente con el titular:
44
+ <vahlame@gmail.com> · (+506) 8610-8287._
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @vkmikc/create-vkm-kit
2
+
3
+ Interactive initializer for **Obsidian-style, file-based agent memory** — a Markdown vault your
4
+ AI coding agent reads and writes across sessions, wired to your IDE over **MCP**.
5
+
6
+ It configures the [`basic-memory`](https://github.com/basicmachines-co/basic-memory) MCP server
7
+ (and, optionally, the `obsidian-memory-hybrid` retrieval sidecar — **BM25 + semantic + graph-aware**
8
+ search with opt-in precision levers (**cross-encoder reranker**, type-weighted graph, importance,
9
+ MMR diversification, passage-window), a **structured knowledge graph** (typed relations + categorized
10
+ observations), **memory reports** for hygiene, and an optional **sqlite-vec** acceleration) for
11
+ **Codex CLI**, **Claude Code** and/or **Cursor**, points it at your vault, and can build the local
12
+ search index — in one
13
+ command. `--full` turns **all** of it on by default.
14
+
15
+ Part of the [obsidian-memory-kit](https://github.com/Vahlame/obsidian-memory-kit)
16
+ kit. Full docs (English + Spanish), architecture and ADRs live there.
17
+
18
+ ## Quick start
19
+
20
+ ```bash
21
+ # Interactive wizard (pre-selects everything)
22
+ npm create @vkmikc/vkm-kit@latest
23
+
24
+ # ⚡ The whole stack, zero questions — vault defaults to ~/Documents/obsidian-memory-vault.
25
+ # FULL by default (hybrid + semantic + sqlite-vec + index + rules). Run from a clone of
26
+ # the kit (or add --repo-root <clone>) for the hybrid pieces; degrades to basic-memory otherwise.
27
+ npx @vkmikc/create-vkm-kit@latest -y
28
+
29
+ # …or point it at any folder (created if it doesn't exist)
30
+ npx @vkmikc/create-vkm-kit@latest ./my-vault -y
31
+
32
+ # Just plain basic-memory, nothing else:
33
+ npx @vkmikc/create-vkm-kit@latest ./my-vault -y --minimal
34
+ ```
35
+
36
+ The old npm name still works: `npx @vkmikc/create-obsidian-memory` is a shim that forwards to the new package.
37
+
38
+ **The install is the full stack BY DEFAULT (default since v3.8.1)** — hybrid + semantic + sqlite-vec + index +
39
+ rules, the same set `--full` ships. That wires the knowledge graph + memory reports (automatic once
40
+ hybrid is on), neural embeddings, and the sqlite-vec acceleration. Run it from a clone of the kit (or
41
+ pass `--repo-root <clone>`) to get the hybrid pieces; with no clone it **degrades to `basic-memory`
42
+ instead of aborting**, so a bare `npx` is always safe. `--full` (alias `--all`) is the same full stack
43
+ but also flips `--ide` to `codex,claude`. Use **`--minimal`** for plain `basic-memory`, or
44
+ `--no-<piece>` (e.g. `--no-semantic`, `--no-vec`) to drop one part.
45
+
46
+ The wizard asks for your vault path and which IDE(s) to wire; the `-y` form skips all prompts. Either
47
+ way it writes the MCP config and scaffolds a starter vault (`START_HERE.md`, `MEMORY.md`, `PROJECTS/`,
48
+ `SESSION_LOG.md`, `PRACTICES/`, `STACKS/`, `_meta/agent-profiles.md`).
49
+
50
+ ## One-command, non-interactive (CI / fresh PC)
51
+
52
+ ```bash
53
+ # Codex CLI + Claude Code, hybrid search + multilingual embeddings, backend installed,
54
+ # index built, rules written — everything, from a kit clone:
55
+ node packages/create-vkm-kit/src/index.js --full --vault "$HOME/my-vault" --repo-root .
56
+
57
+ # …or spell it out (and add cursor) instead of --full:
58
+ node packages/create-vkm-kit/src/index.js --non-interactive \
59
+ --vault "$HOME/my-vault" --ide codex,claude,cursor \
60
+ --with-hybrid --semantic --install-backend --build-index --rules all --repo-root .
61
+ ```
62
+
63
+ `--with-hybrid` (and `--full`'s hybrid step) needs a local clone of the kit — it wires the Node
64
+ bridge + Python backend — so run it from the clone or pass `--repo-root <clone>`. The plain
65
+ `basic-memory` path needs no clone, and `--full` falls back to it (with a warning) if no clone is
66
+ found.
67
+
68
+ ## Options
69
+
70
+ | Flag | Purpose |
71
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
72
+ | `--lang en` | English prompts (default is Spanish-first). |
73
+ | `--full`, `--all` | **One-shot max power, every feature on (implies `-y`).** Defaults `--ide` to `codex,claude`; turns on `--with-hybrid --semantic --vec --build-index --install-backend --obscura` + rules per agent. Degrades to `basic-memory` (no abort) without a kit clone. Opt out: `--no-semantic` / `--no-vec` / `--no-build-index` / `--no-install-backend` / `--no-rules`. |
74
+ | `--dry-run` | Print what would be written — no writes. |
75
+ | `-y`, `--yes`, `--non-interactive` | Headless mode (no prompts). |
76
+ | `[vault]` (positional) | Vault path as the first argument (e.g. `… ./my-vault -y`); same as `--vault`. |
77
+ | `--vault <path>` | Vault root (absolute or cwd-relative). Optional — defaults to `~/Documents/obsidian-memory-vault`, created if missing. |
78
+ | `--ide <list>` | IDEs to wire, comma-separated: `codex`, `claude`, `cursor` (default: `cursor`; with `--full`: `codex,claude`). |
79
+ | `--no-cursor-mcp` | Skip writing `~/.cursor/mcp.json`. |
80
+ | `--no-git-init` | Skip `git init` when the vault has no `.git`. |
81
+ | `--with-hybrid` | Also wire `obsidian-memory-hybrid` (needs a kit clone; use `--repo-root` or cwd walk). |
82
+ | `--repo-root <path>` | Root of the `obsidian-memory-kit` clone (hybrid bridge + Python source). |
83
+ | `--semantic` | With `--with-hybrid`: neural embeddings (fastembed multilingual; needs the `[semantic]` extra). |
84
+ | `--vec` | With `--with-hybrid`: sqlite-vec acceleration (needs the `[vec]` extra; sets `OBSIDIAN_MEMORY_SQLITE_VEC=1`). Ranking-identical with a safe fallback. On under `--full`; opt out with `--no-vec`. |
85
+ | `--rerank` | With `--with-hybrid`: cross-encoder reranker for a top-k precision boost (installs the `[rerank]` extra; sets `OBSIDIAN_MEMORY_RERANK=1`). **Opt-in only** — downloads a model on first use and needs a content-language-matched model (the multilingual default); **not** enabled by `--full`. |
86
+ | `--build-index` | After wiring, build the local FTS (+ semantic) index (needs the Python backend). |
87
+ | `--install-backend` | `pip install -e` the Python RAG backend (best-effort; on by default under `--full`). |
88
+ | `--with-gitleaks` | Install a gitleaks pre-commit hook in `<vault>/.git/hooks/`. |
89
+ | `--rules <list>` | Install the memory-rules block into `claude` (`~/.claude/CLAUDE.md`), `codex` (`~/.codex/AGENTS.md`), `agents` (`./AGENTS.md`), `cursor` (`.cursor/rules`). Or `all` / `none`. Idempotent; never clobbers your content. Headless writes nothing unless passed (or `--full`); interactive asks. |
90
+ | `--no-rules` | Don't write any rules file. |
91
+ | `--native-memory-override` | (Claude Code) Force the native-memory override on, even under `--minimal`. |
92
+ | `--no-native-memory-override` | (Claude Code) Leave Claude's native auto-memory untouched (don't write `autoMemoryEnabled:false` / the SessionStart hook). |
93
+ | `--help` | Show usage. |
94
+ | `--obscura`, `--no-obscura` | Install + wire **obscura-web** (stealth `obscura_fetch` + robust `obscura_search`); **on under `--full`**. Needs a kit clone; downloads + SHA-256-verifies the pinned obscura binary into `~/.vkm/obscura/`. See [ADR-0051](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/adr/0051-obscura-web-stealth-browser.md). |
95
+ | `--searxng-url <url>` | Point `obscura_search` at a SearXNG JSON instance directly (bypasses the on-demand manager — e.g. a remote / Docker SearXNG). |
96
+
97
+ - `codex` registers servers via the Codex CLI (`codex mcp add` → `~/.codex/config.toml`).
98
+ - `claude` registers servers via the Claude Code CLI (`claude mcp add -s user`).
99
+ - `cursor` writes `~/.cursor/mcp.json`.
100
+
101
+ ### Install the memory rules too
102
+
103
+ The initializer can also drop the **memory-protocol rules** (how the agent should use the vault), not just the MCP wiring. Use `--rules all` for full coverage:
104
+
105
+ ```bash
106
+ npx @vkmikc/create-vkm-kit@latest ./my-vault -y --ide cursor,claude --rules all
107
+ ```
108
+
109
+ It writes an **idempotent marked block** (`<!-- obsidian-memory:start --> … <!-- obsidian-memory:end -->`) into `~/.claude/CLAUDE.md`, `./AGENTS.md` and `.cursor/rules/obsidian-memory.mdc`, merging in place — **your own content is never touched**, and re-runs just refresh the block. Cursor's _global_ User Rules can't be auto-written (not a file), so paste that one from the install guide.
110
+
111
+ ### Make the vault Claude Code's _only_ memory (`--ide claude`)
112
+
113
+ Claude Code ships a **native auto-memory** (`~/.claude/projects/<path>/memory/MEMORY.md`) that the harness auto-loads and the base prompt tells the model to `Write` to. Left on, it competes with — and by default beats — the vault, especially while the `vault_*` MCP tools are deferred. So a Claude Code install also, **by default** (ADR-0029):
114
+
115
+ 1. Writes `"autoMemoryEnabled": false` into `~/.claude/settings.json` (idempotent merge — other keys/hooks are preserved; invalid JSON is backed up and skipped).
116
+ 2. Installs a cross-platform Node **`SessionStart` hook** (`~/.claude/hooks/session-start-vault-context.mjs`) that injects the vault map + reinforced reminders: vault is the only source of truth, first step is to `ToolSearch`-load deferred `vault_*` tools, recall = `vault_hybrid_search`, and the close ritual (`SESSION_LOG.md` + `PROJECTS/<project>.md`, each edit anchored on one CRLF line).
117
+
118
+ Re-runs don't duplicate the hook (a legacy `.ps1` variant is recognized and replaced). Opt out with `--minimal` or `--no-native-memory-override`; force on with `--native-memory-override`.
119
+
120
+ ## What the installed memory does
121
+
122
+ Beyond wiring the MCP, the kit installs a **memory protocol** (the rules block above) and scaffolds a vault designed to get smarter over time while staying token-cheap:
123
+
124
+ - **Passage-first recall** — the agent pulls the matching _section_ of a note (`vault_hybrid_search`), not the whole file, and checks the vault _before answering_ when a task continues prior work or names a known project/person/tool.
125
+ - **Graph-aware recall + autocomplete (v3.5)** — `vault_hybrid_search` can follow your `[[wikilinks]]` (opt-in `graph: true`), so a note linked from a strong hit surfaces even when its own words barely match; `vault_complete` resolves a prefix to the titles / filenames / `#tags` that exist. See [how it works](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/en/how-it-works.md#the-retrieval-stack-at-a-glance-old--new).
126
+ - **Measured, not just claimed (v3.7)** — retrieval quality is benchmarked against a fixed labelled corpus and gated in CI (recall@k / MRR / hit@1), so a recall regression fails the build instead of shipping silently. A missing or misspelled term also no longer drops a relevant note (lexical search falls back from strict AND to OR). See [`evals/retrieval`](https://github.com/Vahlame/obsidian-memory-kit/tree/main/evals/retrieval).
127
+ - **Structured knowledge graph (v3.8)** — typed relations (`- implements [[adr-0014]]`) and categorized observations (`- [decision] … #tag`) authored in plain Markdown (Basic-Memory-compatible) become queryable: `vault_relations` answers "what supersedes this / what links here?" both directions, `vault_observations` pulls every `[decision]` or `#tag` across the vault, and `vault_kg_suggest` proposes structure for a note (read-only — you confirm and edit). See [ADR-0023](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/adr/0023-structured-knowledge-graph.md).
128
+ - **Memory reports (v3.8)** — `vault_memory_report` is a read-only digest for periodic upkeep: automatic indices (observations by category, graph hub notes, top `#tags`), hygiene (oversized / stale / orphan notes, broken links, `SESSION_LOG` bloat) with concrete next steps, and opt-in near-duplicate pairs to review. It flags what to condense; it never rewrites a note. See [ADR-0024](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/adr/0024-memory-reports-and-compaction.md).
129
+ - **sqlite-vec acceleration (v3.8, opt-in, on under `--full`)** — for large vaults, the cosine scan runs inside SQLite over the same index file via the sqlite-vec extension; ranking is **identical** to the pure-Python path (verified by the bench + a parity test) with a transparent fallback. The in-file embedded answer — no Chroma/LanceDB server. See [ADR-0025](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/adr/0025-optional-sqlite-vec-acceleration.md).
130
+ - **Opt-in precision levers (v3.9)** — `vault_hybrid_search` gains default-off knobs that only ever reorder the fused candidates, never break search: a **cross-encoder reranker** (`rerank: true`, behind the `[rerank]` extra / `--rerank`; re-scores query + passage _together_ — the precision lever, and only helps with a strong, content-language-matched model), **type-weighted graph recall** (`graphTyped: true`, verb-weighted typed relations), **importance** (`importance: true`, in-degree/hub bias), **MMR** diversification (`mmr: true`), and **passage-window** expansion (`passageWindow: N`). The measured default path stays byte-identical. See [ADR-0026](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/adr/0026-cross-encoder-reranker.md) / [ADR-0027](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/adr/0027-type-weighted-graph-and-importance.md) / [ADR-0028](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/adr/0028-mmr-and-passage-window.md).
131
+ - **Self-check** — before non-trivial answers it sanity-checks its own assumptions and edge cases (scaled to the task, internal — no padding).
132
+ - **Coach, don't impose** — flags high-impact anti-patterns in your code as a _question_ and logs them to `PRACTICES/observations.md`; promotes to `confirmed-{good,bad}.md` only when you confirm.
133
+ - **Evolving memory** — records new tech in `STACKS/`, firm preferences in `MEMORY.md`, hypotheses → facts.
134
+ - **Model-aware** — reads `_meta/agent-profiles.md` to tune behavior to the active model's strengths (Claude, Cursor Composer, GPT, DeepSeek, Gemini…) and learns which model fits which task over time.
135
+ - **Token economy** — all of the above is bounded by passage-first reads, terse bullets and dedup, so smarter ≠ pricier.
136
+
137
+ Read the rules block before installing — it's printed in [`docs/en/install.md`](https://github.com/Vahlame/obsidian-memory-kit/blob/main/docs/en/install.md) (Step 4) and is what `--rules` writes (idempotently) into your agent configs.
138
+
139
+ ## Requirements
140
+
141
+ - **Node.js ≥ 20**
142
+ - **[`uv`](https://docs.astral.sh/uv/)** (for `uvx basic-memory mcp`)
143
+ - For `--with-hybrid` / `--build-index` / `--install-backend`: **Python ≥ 3.11** + `pip`, and a clone of the kit (`obsidian-memory-rag`).
144
+ - For `--ide claude`: the **Claude Code** CLI on `PATH`. For `--ide codex`: the **Codex** CLI on `PATH` (otherwise the installer prints the `codex mcp add` command + a `config.toml` block to paste).
145
+
146
+ ## License
147
+
148
+ Free use with mandatory visible attribution (MIT-based) — © Vahlame. See [`LICENSE.md`](./LICENSE.md) and the [repository](https://github.com/Vahlame/obsidian-memory-kit) for details.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@vkmikc/create-vkm-kit",
3
+ "version": "4.2.0",
4
+ "description": "vkm-kit installer: persistent Markdown+git memory for AI agents (MCP) with hybrid search, stealth web fetch/search (obscura + on-demand SearXNG), a cross-domain execution-discipline skill, token-saver hooks, a local usage doctor and a spec-builder — one npx command.",
5
+ "license": "SEE LICENSE IN LICENSE.md",
6
+ "homepage": "https://github.com/Vahlame/create-vkm-kit#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Vahlame/create-vkm-kit.git",
10
+ "directory": "packages/create-vkm-kit"
11
+ },
12
+ "keywords": [
13
+ "mcp",
14
+ "obsidian",
15
+ "cursor",
16
+ "claude-code",
17
+ "codex",
18
+ "agent-memory",
19
+ "ai-memory",
20
+ "basic-memory",
21
+ "rag",
22
+ "semantic-search",
23
+ "hybrid-search",
24
+ "knowledge-graph",
25
+ "web-search",
26
+ "web-fetch",
27
+ "stealth-browser",
28
+ "obscura",
29
+ "searxng",
30
+ "token-efficiency",
31
+ "vkm"
32
+ ],
33
+ "files": [
34
+ "src",
35
+ "templates"
36
+ ],
37
+ "type": "module",
38
+ "bin": {
39
+ "create-vkm-kit": "src/index.js",
40
+ "vkm": "src/index.js"
41
+ },
42
+ "scripts": {
43
+ "test": "node --test"
44
+ },
45
+ "dependencies": {
46
+ "execa": "^9.5.2",
47
+ "fs-extra": "^11.2.0",
48
+ "picocolors": "^1.1.1",
49
+ "prompts": "^2.4.2"
50
+ },
51
+ "engines": {
52
+ "node": ">=20"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }
@@ -0,0 +1,109 @@
1
+ // Managed-asset installer for the template files this kit copies into the user's Claude
2
+ // Code profile (output styles, skills, subagent templates) — anything that is a FILE the
3
+ // kit owns rather than a key inside settings.json.
4
+ //
5
+ // Ownership is tracked in a sidecar manifest (`~/.claude/vkm-kit.assets.json`) that records
6
+ // the SHA-256 of every file AS INSTALLED. That gives uninstall a stronger guarantee than
7
+ // the hooks' marker-string check: a file is deleted only when its current content hash still
8
+ // equals the recorded one — a user who edited an installed template keeps their file (with a
9
+ // skip report), and a file we never recorded is never touched at all. Install always
10
+ // overwrites (kit templates are kit-owned, same policy as the hook scripts; users who want
11
+ // to customize should copy under a different name).
12
+ //
13
+ // No logging here — functions return action reports ({installed, removed, skipped}) and the
14
+ // orchestrator owns the console. All I/O goes through the same crash-safe primitives as the
15
+ // rest of the installer (`atomicWriteJson` for the sidecar; copy is per-file so a failure
16
+ // leaves previous files intact).
17
+ import crypto from "node:crypto";
18
+ import path from "node:path";
19
+ import fse from "fs-extra";
20
+ import { atomicWriteJson, readSettingsSafe } from "./settings-io.mjs";
21
+
22
+ /** Sidecar manifest basename, kept under `~/.claude/`. */
23
+ export const ASSETS_SIDECAR_BASENAME = "vkm-kit.assets.json";
24
+
25
+ function sha256(bytes) {
26
+ return crypto.createHash("sha256").update(bytes).digest("hex");
27
+ }
28
+
29
+ /** Load the sidecar manifest (missing/corrupt → empty manifest; never throws). */
30
+ async function readSidecar(sidecarFp) {
31
+ const { existing, invalidJson } = await readSettingsSafe(sidecarFp);
32
+ if (invalidJson || typeof existing.assets !== "object" || existing.assets === null) {
33
+ return { version: 1, assets: {} };
34
+ }
35
+ return { version: 1, assets: { ...existing.assets } };
36
+ }
37
+
38
+ /** Persist the manifest; deletes the file entirely when no assets remain (no trace). */
39
+ async function writeSidecar(sidecarFp, manifest) {
40
+ if (Object.keys(manifest.assets).length === 0) {
41
+ await fse.remove(sidecarFp);
42
+ return;
43
+ }
44
+ await atomicWriteJson(sidecarFp, manifest);
45
+ }
46
+
47
+ /**
48
+ * Copy each `{src, dest}` pair into place and record its content hash in the sidecar.
49
+ * Overwrites existing files (kit-owned templates); re-runs converge on the same state.
50
+ * @param {object} opts
51
+ * @param {{ src: string, dest: string }[]} opts.files - absolute paths
52
+ * @param {string} opts.sidecarFp - absolute path of the sidecar manifest
53
+ * @param {boolean} [opts.dryRun]
54
+ * @returns {Promise<{ installed: string[] }>} dest paths written (or that would be)
55
+ */
56
+ export async function installManagedAssets({ files, sidecarFp, dryRun = false }) {
57
+ const installed = [];
58
+ if (dryRun) {
59
+ return { installed: files.map((f) => f.dest) };
60
+ }
61
+ const manifest = await readSidecar(sidecarFp);
62
+ for (const { src, dest } of files) {
63
+ const bytes = await fse.readFile(src);
64
+ await fse.ensureDir(path.dirname(dest));
65
+ await fse.copy(src, dest, { overwrite: true });
66
+ manifest.assets[dest] = { hash: sha256(bytes), installedAt: new Date().toISOString() };
67
+ installed.push(dest);
68
+ }
69
+ await writeSidecar(sidecarFp, manifest);
70
+ return { installed };
71
+ }
72
+
73
+ /**
74
+ * Remove managed assets: every file recorded in the sidecar (optionally narrowed to
75
+ * `dests`) is deleted IFF its current content hash still equals the recorded one. Modified
76
+ * files are skipped (and reported) but their manifest entry is dropped either way — after an
77
+ * uninstall the kit no longer claims them. Missing files just drop their entry.
78
+ * @param {object} opts
79
+ * @param {string} opts.sidecarFp
80
+ * @param {string[]} [opts.dests] - narrow removal to these dest paths (default: all recorded)
81
+ * @param {boolean} [opts.dryRun]
82
+ * @returns {Promise<{ removed: string[], skipped: string[] }>}
83
+ */
84
+ export async function removeManagedAssets({ sidecarFp, dests, dryRun = false }) {
85
+ const manifest = await readSidecar(sidecarFp);
86
+ const targets = dests ?? Object.keys(manifest.assets);
87
+ const removed = [];
88
+ const skipped = [];
89
+ for (const dest of targets) {
90
+ const entry = manifest.assets[dest];
91
+ if (!entry) continue;
92
+ let unmodified = false;
93
+ try {
94
+ unmodified = sha256(await fse.readFile(dest)) === entry.hash;
95
+ } catch {
96
+ // Missing/unreadable: nothing to delete; just release the claim below.
97
+ unmodified = false;
98
+ }
99
+ if (unmodified) {
100
+ if (!dryRun) await fse.remove(dest);
101
+ removed.push(dest);
102
+ } else if (await fse.pathExists(dest)) {
103
+ skipped.push(dest);
104
+ }
105
+ if (!dryRun) delete manifest.assets[dest];
106
+ }
107
+ if (!dryRun) await writeSidecar(sidecarFp, manifest);
108
+ return { removed, skipped };
109
+ }