@nodemint/projectmind
Advanced tools
+36
-0
@@ -6,2 +6,38 @@ # Changelog | ||
| ## [0.5.0] - 2026-07-04 | ||
| ### Added | ||
| - **Universal digest embedding — the real fix for "the agent didn't call | ||
| mind_digest."** `projectmind setup` now embeds the live digest content | ||
| directly into each agent's rules file (`CLAUDE.md`, `.cursorrules`, | ||
| `.windsurfrules`, `GEMINI.md`, `AGENTS.md`, | ||
| `.github/copilot-instructions.md`), between | ||
| `<!-- projectmind:digest:begin/end -->` markers, instead of only telling the | ||
| agent to call a tool. Rules files are loaded into every agent's context | ||
| unconditionally — no model choice involved — so this works identically | ||
| across every supported agent, not just Claude Code. | ||
| - `save()` now keeps every already-set-up rules file's embedded digest in | ||
| sync automatically on every map change (`mind_update`, the CLI, the git | ||
| post-commit hook, `projectmind watch` — anything that calls `save()`). | ||
| - New exports: `embedDigestBlock`, `committedDigest`, `RULES_FILES`, | ||
| `RULES_MARKER_BEGIN/END`, `DIGEST_BLOCK_BEGIN/END`. | ||
| ### Fixed | ||
| - A stray pair of literal null bytes in `dedupEdges`'s dedup key (an old typo: | ||
| `\0` was meant as a delimiter but had been written as a raw byte) made | ||
| `src/core/index.js` register as a binary file to some tools (e.g. `grep` | ||
| without `-a`). Functionally harmless — replaced with a plain space. | ||
| ### Security / privacy | ||
| - Only the **repo-committed** map is ever embedded in rules files — | ||
| `committedDigest()` never includes the gitignored local overlay, so | ||
| personal handoff notes or local-only nodes can never leak into a file you | ||
| commit. Covered by a dedicated regression test. | ||
| **Why this matters:** MCP tool descriptions and rules-file instructions are | ||
| nudges — no server can force a model to call a specific tool, and real | ||
| dogfooding showed a connected server + explicit instructions still wasn't | ||
| enough on plain orientation questions. Embedding the actual content sidesteps | ||
| that entirely: there's nothing left for the model to choose *not* to do. | ||
| ## [0.4.4] - 2026-07-04 | ||
@@ -8,0 +44,0 @@ |
+1
-1
| { | ||
| "name": "@nodemint/projectmind", | ||
| "version": "0.4.4", | ||
| "version": "0.5.0", | ||
| "mcpName": "io.github.Nodemint-dev/projectmind", | ||
@@ -5,0 +5,0 @@ "publishConfig": { |
+20
-3
@@ -221,8 +221,25 @@ # projectmind | ||
| ## One-command agent wiring | ||
| ## One-command agent wiring — and it doesn't depend on the model choosing to read it | ||
| `projectmind setup` idempotently writes the MCP config **and** a short workflow | ||
| `projectmind setup` idempotently writes the MCP config **and** a workflow | ||
| rules block for each agent — merging into existing configs, never clobbering | ||
| (an unparseable config is backed up and skipped): | ||
| (an unparseable config is backed up and skipped). | ||
| Here's the part that matters: MCP tools are a *nudge* — no server can force a | ||
| model to call one, so an agent can (and sometimes will) skip `mind_digest` on | ||
| a plain "explain this project" question, especially mid-task. But rules files | ||
| (`CLAUDE.md`, `.cursorrules`, `.windsurfrules`, `GEMINI.md`, `AGENTS.md`, | ||
| `.github/copilot-instructions.md`) are loaded into every agent's context | ||
| **unconditionally, with zero model choice involved** — that's the actual | ||
| mechanism, not a coincidence. So `setup` doesn't just write instructions to | ||
| call `mind_digest`; it **embeds the live digest itself** between | ||
| `<!-- projectmind:digest:begin/end -->` markers, and every subsequent | ||
| `mind_update`, CLI edit, git-commit hook run, or `watch` save **re-syncs it | ||
| automatically**. The project map is present in context from message one, on | ||
| every agent, with no tool call required at all — the same reliability | ||
| class as a context-injecting hook, without needing one. | ||
| (Only the *repo-committed* map is ever embedded — never your gitignored local | ||
| overlay or handoff notes, so nothing personal leaks into a file you commit.) | ||
| | Agent | MCP config | Rules file | | ||
@@ -229,0 +246,0 @@ |-------|-----------|------------| |
+63
-2
@@ -275,3 +275,3 @@ // projectmind core — the only module that touches map.json. | ||
| for (const e of edges) { | ||
| const k = `${e.from} ${e.to} ${e.rel}`; | ||
| const k = `${e.from} ${e.to} ${e.rel}`; | ||
| if (seen.has(k)) continue; | ||
@@ -298,4 +298,13 @@ seen.add(k); | ||
| atomicWrite(mapPath(r), serialize(m)); | ||
| const digestText = buildDigest(m, loadConfig(r).digest); | ||
| // digest.md reflects the committed (repo) map so PR diffs stay clean. | ||
| atomicWrite(digestPath(r), buildDigest(m, loadConfig(r).digest)); | ||
| atomicWrite(digestPath(r), digestText); | ||
| // Keep every already-set-up agent's rules file (CLAUDE.md, etc.) carrying | ||
| // the current digest inline. This is the universal, cross-agent fix for | ||
| // "the model didn't choose to call mind_digest": rules files are loaded | ||
| // into context by every agent unconditionally, with zero model choice | ||
| // involved, so embedding the actual content there (not just an | ||
| // instruction to fetch it) guarantees fresh orientation context without | ||
| // depending on the model deciding to call a tool. | ||
| syncRuleDigests(r, digestText); | ||
| } | ||
@@ -306,2 +315,54 @@ return m; | ||
| // --------------------------------------------------------------------------- | ||
| // Rules-file digest embedding. Only files that already opted in via | ||
| // `projectmind setup` (i.e. already contain RULES_MARKER_BEGIN) are ever | ||
| // touched — this never spontaneously creates a rules file for an agent the | ||
| // user hasn't set up, and never embeds anything before the user has run | ||
| // setup once. | ||
| // --------------------------------------------------------------------------- | ||
| export const RULES_MARKER_BEGIN = "<!-- projectmind:begin -->"; | ||
| export const RULES_MARKER_END = "<!-- projectmind:end -->"; | ||
| export const DIGEST_BLOCK_BEGIN = "<!-- projectmind:digest:begin -->"; | ||
| export const DIGEST_BLOCK_END = "<!-- projectmind:digest:end -->"; | ||
| export const RULES_FILES = [ | ||
| "CLAUDE.md", ".cursorrules", ".windsurfrules", "GEMINI.md", "AGENTS.md", | ||
| path.join(".github", "copilot-instructions.md"), | ||
| ]; | ||
| // Pure: given a rules file's current content and the digest text to embed, | ||
| // return the updated content. No-op if the static instructions block isn't | ||
| // present (agent never set up) or malformed (defensive; should not happen). | ||
| export function embedDigestBlock(content, digestText) { | ||
| if (!content.includes(RULES_MARKER_BEGIN)) return content; | ||
| const block = `${DIGEST_BLOCK_BEGIN}\n${digestText.trim()}\n${DIGEST_BLOCK_END}`; | ||
| if (content.includes(DIGEST_BLOCK_BEGIN) && content.includes(DIGEST_BLOCK_END)) { | ||
| const start = content.indexOf(DIGEST_BLOCK_BEGIN); | ||
| const end = content.indexOf(DIGEST_BLOCK_END) + DIGEST_BLOCK_END.length; | ||
| return content.slice(0, start) + block + content.slice(end); | ||
| } | ||
| const markerEndIdx = content.indexOf(RULES_MARKER_END); | ||
| if (markerEndIdx === -1) return content; | ||
| const insertAt = markerEndIdx + RULES_MARKER_END.length; | ||
| return content.slice(0, insertAt) + `\n\n${block}` + content.slice(insertAt); | ||
| } | ||
| // The digest text safe to embed in COMMITTED rules files — repo scope only, | ||
| // never merged with the local overlay (which may carry personal handoff | ||
| // notes). Same content as digest.md. | ||
| export function committedDigest(r = root()) { | ||
| return buildDigest(loadScope("repo", r), loadConfig(r).digest); | ||
| } | ||
| function syncRuleDigests(r, digestText) { | ||
| for (const relFile of RULES_FILES) { | ||
| const file = path.join(r, relFile); | ||
| let content; | ||
| try { content = fs.readFileSync(file, "utf8"); } catch { continue; } | ||
| const updated = embedDigestBlock(content, digestText); | ||
| if (updated !== content) { | ||
| try { fs.writeFileSync(file, updated); } catch { /* best effort */ } | ||
| } | ||
| } | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
| // Init | ||
@@ -308,0 +369,0 @@ // --------------------------------------------------------------------------- |
@@ -182,3 +182,3 @@ #!/usr/bin/env node | ||
| const server = new Server( | ||
| { name: "projectmind", version: "0.4.4" }, | ||
| { name: "projectmind", version: "0.5.0" }, | ||
| { capabilities: { tools: {} } } | ||
@@ -185,0 +185,0 @@ ); |
+22
-13
@@ -16,2 +16,3 @@ // Multi-agent wiring. Writes the MCP server config and a workflow rules block | ||
| import { execFileSync } from "node:child_process"; | ||
| import { committedDigest, embedDigestBlock, RULES_MARKER_BEGIN, RULES_MARKER_END } from "../core/index.js"; | ||
@@ -22,12 +23,10 @@ // `mcp` is a subcommand of the main bin, so plain npx works (same invocation | ||
| const RULES_BEGIN = "<!-- projectmind:begin -->"; | ||
| const RULES_END = "<!-- projectmind:end -->"; | ||
| const RULES_BODY = [ | ||
| RULES_BEGIN, | ||
| RULES_MARKER_BEGIN, | ||
| "## projectmind", | ||
| "Before running ls/find/glob/grep or reading files to explain, describe, or orient in this project (e.g. \"what is this project\", \"explain this codebase\", \"how is this structured\"), call `mind_digest` first — it answers most of that in a few hundred tokens.", | ||
| "The current project map is embedded below (auto-synced on every change — do not hand-edit between the digest markers). Use it instead of ls/find/glob/grep when you need to explain, describe, or orient in this project.", | ||
| "Use `mind_context({ files })` for a task-scoped subgraph, or `mind_query(<id>)` for one module's files/notes.", | ||
| "After a structural change, architectural decision, or newly learned convention, call `mind_update` (only the fields that changed).", | ||
| "Before the session ends (or when context is about to be compacted), call `mind_handoff` with a one-line note on what's in progress and what's next — it leads the next session's digest.", | ||
| RULES_END, | ||
| RULES_MARKER_END, | ||
| ].join("\n"); | ||
@@ -91,11 +90,20 @@ | ||
| function appendRulesBlock(r, relFile) { | ||
| // Appends the static instructions block if missing, then (re)embeds the | ||
| // current digest between digest markers regardless — so the very first | ||
| // setup run already carries real content, not just an instruction to fetch | ||
| // it, and every later setup run refreshes a possibly-stale embedded digest. | ||
| function appendRulesBlock(r, relFile, digestText) { | ||
| const file = path.join(r, relFile); | ||
| let content = ""; | ||
| try { content = fs.readFileSync(file, "utf8"); } catch { /* none */ } | ||
| if (content.includes(RULES_BEGIN)) return { file: relFile, status: "already" }; | ||
| const sep = content && !content.endsWith("\n") ? "\n\n" : content ? "\n" : ""; | ||
| let original = ""; | ||
| try { original = fs.readFileSync(file, "utf8"); } catch { /* none */ } | ||
| const already = original.includes(RULES_MARKER_BEGIN); | ||
| let content = original; | ||
| if (!already) { | ||
| const sep = original && !original.endsWith("\n") ? "\n\n" : original ? "\n" : ""; | ||
| content = `${original}${sep}${RULES_BODY}\n`; | ||
| } | ||
| const updated = embedDigestBlock(content, digestText); | ||
| fs.mkdirSync(path.dirname(file), { recursive: true }); | ||
| fs.writeFileSync(file, `${content}${sep}${RULES_BODY}\n`); | ||
| return { file: relFile, status: content ? "appended" : "created" }; | ||
| fs.writeFileSync(file, updated); | ||
| return { file: relFile, status: already ? "already" : (original ? "appended" : "created") }; | ||
| } | ||
@@ -106,2 +114,3 @@ | ||
| const keys = agents === "all" || !agents ? SUPPORTED_AGENTS : [].concat(agents).filter((k) => AGENTS[k]); | ||
| const digestText = committedDigest(r); | ||
| const results = []; | ||
@@ -112,3 +121,3 @@ for (const key of keys) { | ||
| if (a.json) results.push({ agent: key, label: a.label, ...mergeJsonConfig(path.join(r, a.json.file), a.json.key, a.json.file) }); | ||
| if (a.rules) results.push({ agent: key, label: a.label, ...appendRulesBlock(r, a.rules) }); | ||
| if (a.rules) results.push({ agent: key, label: a.label, ...appendRulesBlock(r, a.rules, digestText) }); | ||
| } | ||
@@ -115,0 +124,0 @@ return results; |
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
112104
6.37%1768
3.88%430
4.12%9
-10%