From 584169d49f23d3a1c3cf10480c5c6b837b402dd7 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste THERY Date: Wed, 28 Oct 2026 00:03:09 +0700 Subject: [PATCH 1/3] feat(hooks): install the index refresh into husky scripts husky 9 points core.hooksPath at .husky/_, whose shims it regenerates on every install, and husky 8 at .husky itself. `rgr hooks install` refused both as a tracked hooks folder. It now recognizes husky, writes the Ragmir block into the .husky/ scripts husky runs, reports manager=husky, and asks to commit those scripts. Other tracked hooks folders still need --force. --- docs/cli-reference.md | 6 ++- packages/ragmir-core/src/cli.ts | 9 ++++- packages/ragmir-core/src/git-hooks.test.ts | 43 ++++++++++++++++++++-- packages/ragmir-core/src/git-hooks.ts | 28 +++++++++++++- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 1bf8aec..6c4da57 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -99,8 +99,10 @@ rgr hooks uninstall Each hook gets one marked block that runs `node .ragmir/run.cjs ingest` in the background from the knowledge base folder; existing hook content is kept, and a final `exit` stays last. Incremental ingestion only re-embeds changed files. `hooks install` resolves `core.hooksPath` and linked -worktrees, and refuses a hooks folder tracked in the working tree (for example `.husky/`) unless -`--force` is given. Hooks need Git; on Windows they run in Git's bundled shell. +worktrees. With husky, it edits the `.husky/` scripts that husky runs (`manager=husky`) rather +than the `.husky/_` shims it regenerates; commit those scripts to share the refresh. Another hooks +folder tracked in the working tree (for example `.githooks/`) needs `--force`. Hooks need Git; on +Windows they run in Git's bundled shell. ## Safe upgrades diff --git a/packages/ragmir-core/src/cli.ts b/packages/ragmir-core/src/cli.ts index bb190e0..31e9c54 100644 --- a/packages/ragmir-core/src/cli.ts +++ b/packages/ragmir-core/src/cli.ts @@ -1268,7 +1268,10 @@ const hooksCommand = program hooksCommand .command("install") .description("Add a background `rgr ingest` to the Git hooks of this repository.") - .option("--force", "Also edit hooks in a directory tracked by the working tree, such as .husky.") + .option( + "--force", + "Also edit hooks in a directory tracked by the working tree, such as .githooks; husky is supported.", + ) .option("--json", "Print machine-readable JSON.") .action(async (options: { force?: boolean; json?: boolean }, command: Command) => { const report = await installGitHooks({ @@ -1698,7 +1701,11 @@ function printGitHooks( ) } console.log(`hooksDir=${report.hooksDir}`) + console.log(`manager=${report.manager}`) console.log(`installed=${report.installed.join(",") || "none"}`) + if (action === "installed" && report.manager === "husky" && report.changed.length > 0) { + console.log("Commit the changed .husky scripts so the refresh follows the repository.") + } if (report.missing.length > 0) { console.log(`missing=${report.missing.join(",")}`) } diff --git a/packages/ragmir-core/src/git-hooks.test.ts b/packages/ragmir-core/src/git-hooks.test.ts index ffc7a8e..b504c8f 100644 --- a/packages/ragmir-core/src/git-hooks.test.ts +++ b/packages/ragmir-core/src/git-hooks.test.ts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process" import { existsSync } from "node:fs" -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises" +import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises" import os from "node:os" import path from "node:path" import { afterEach, describe, expect, it } from "vitest" @@ -62,12 +62,49 @@ describe("git hooks", () => { it("refuses to edit a hooks directory tracked in the working tree unless forced", async () => { const root = await gitRepository() - execFileSync("git", ["config", "core.hooksPath", ".husky"], { cwd: root }) + execFileSync("git", ["config", "core.hooksPath", ".githooks"], { cwd: root }) await expect(installGitHooks({ cwd: root })).rejects.toThrow(/inside the working tree/u) const forced = await installGitHooks({ cwd: root, force: true }) - expect(forced.trackedHooksDir).toBe(true) + expect(forced).toMatchObject({ manager: "git", trackedHooksDir: true }) + expect(existsSync(path.join(root, ".githooks", "post-merge"))).toBe(true) + }) + + it("adds the block to husky 9 scripts instead of the shims husky regenerates", async () => { + const root = await gitRepository() + const shims = path.join(root, ".husky", "_") + await mkdir(shims, { recursive: true }) + await writeFile(path.join(shims, "h"), "#!/usr/bin/env sh\n", "utf8") + await writeFile(path.join(shims, ".gitignore"), "*", "utf8") + await writeFile(path.join(root, ".husky", "pre-commit"), "pnpm lint\n", "utf8") + execFileSync("git", ["config", "core.hooksPath", ".husky/_"], { cwd: root }) + + const installed = await installGitHooks({ cwd: root }) + const postCommit = await readFile(path.join(root, ".husky", "post-commit"), "utf8") + const removed = await uninstallGitHooks({ cwd: root }) + + expect(installed).toMatchObject({ + manager: "husky", + hooksDir: path.join(await realpath(root), ".husky"), + installed: ["post-checkout", "post-commit", "post-merge", "post-rewrite"], + }) + expect(postCommit).toContain("node .ragmir/run.cjs ingest") + expect(existsSync(path.join(shims, "post-commit"))).toBe(false) + expect(await readFile(path.join(root, ".husky", "pre-commit"), "utf8")).toBe("pnpm lint\n") + expect(removed.changed).toHaveLength(4) + expect(existsSync(path.join(root, ".husky", "post-commit"))).toBe(false) + }) + + it("recognizes husky 8, whose hooks path is the scripts folder itself", async () => { + const root = await gitRepository() + await mkdir(path.join(root, ".husky", "_"), { recursive: true }) + await writeFile(path.join(root, ".husky", "_", "husky.sh"), "#!/usr/bin/env sh\n", "utf8") + execFileSync("git", ["config", "core.hooksPath", ".husky"], { cwd: root }) + + const installed = await installGitHooks({ cwd: root }) + + expect(installed).toMatchObject({ manager: "husky", trackedHooksDir: true }) expect(existsSync(path.join(root, ".husky", "post-merge"))).toBe(true) }) }) diff --git a/packages/ragmir-core/src/git-hooks.ts b/packages/ragmir-core/src/git-hooks.ts index f846695..9248f6d 100644 --- a/packages/ragmir-core/src/git-hooks.ts +++ b/packages/ragmir-core/src/git-hooks.ts @@ -27,6 +27,8 @@ export interface GitHooksOptions { export interface GitHooksReport { hooksDir: string workTree: string + /** `husky` when husky owns the hooks: Ragmir then edits its tracked `.husky/` scripts. */ + manager: GitHooksManager /** The hooks directory lives inside the working tree, so its files are usually committed. */ trackedHooksDir: boolean installed: GitHookName[] @@ -41,7 +43,7 @@ export interface GitHooksReport { */ export async function installGitHooks(options: GitHooksOptions = {}): Promise { const location = await gitHooksLocation(options.cwd) - if (location.trackedHooksDir && options.force !== true) { + if (location.trackedHooksDir && location.manager !== "husky" && options.force !== true) { throw new Error( `Git hooks resolve to ${location.hooksDir}, inside the working tree (for example a husky directory). Add the Ragmir block to that tool's hook files, or rerun with --force to edit them.`, ) @@ -90,9 +92,12 @@ export async function gitHooksStatus(options: GitHooksOptions = {}): Promise ) // Resolves core.hooksPath and linked worktrees the way Git itself does. const hooksPath = (await git(projectRoot, ["rev-parse", "--git-path", "hooks"])).trim() - const hooksDir = path.resolve(projectRoot, hooksPath) + const gitHooksDir = path.resolve(projectRoot, hooksPath) + const huskyDir = huskyScriptsDir(gitHooksDir) + const hooksDir = huskyDir ?? gitHooksDir const gitDir = path.resolve( projectRoot, (await git(projectRoot, ["rev-parse", "--git-common-dir"])).trim(), @@ -116,11 +123,27 @@ async function gitHooksLocation(cwd = process.cwd()): Promise return { hooksDir, workTree, + manager: huskyDir === null ? "git" : "husky", trackedHooksDir: insideWorkTree && !insideGitDir, projectPath: canonicalSourcePath(path.relative(workTree, projectRoot)) || ".", } } +/** + * The folder of husky's hook scripts when husky owns the Git hooks. husky 9 points core.hooksPath at + * `.husky/_`, which it regenerates on every install and whose shims run `.husky/`; husky 8 + * points it at `.husky` itself. Hooks belong in the `.husky/` scripts in both cases. + */ +function huskyScriptsDir(gitHooksDir: string): string | null { + if (path.basename(gitHooksDir) === "_" && path.basename(path.dirname(gitHooksDir)) === ".husky") { + return existsSync(path.join(gitHooksDir, "h")) ? path.dirname(gitHooksDir) : null + } + if (path.basename(gitHooksDir) === ".husky") { + return existsSync(path.join(gitHooksDir, "_", "husky.sh")) ? gitHooksDir : null + } + return null +} + function ragmirHookBlock(projectPath: string): string { const runner = `${RAGMIR_DIR}/${RGR_RUNNER_FILENAME}` return [ @@ -190,6 +213,7 @@ async function reportFor( return { hooksDir: location.hooksDir, workTree: location.workTree, + manager: location.manager, trackedHooksDir: location.trackedHooksDir, installed, missing, From 548d943badd348c37c9a9929e9d8d0a5417bfb35 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste THERY Date: Wed, 28 Oct 2026 00:03:10 +0700 Subject: [PATCH 2/3] fix(mcp): name the resolved allowed roots when refusing a workspace The refusal printed the raw mcpAllowedRoots values, so an umbrella base configured with "." told agents it served "workspaces under .". It now prints the resolved folders the check uses. --- packages/ragmir-core/src/mcp-scope.ts | 5 ++++- packages/ragmir-core/src/mcp.test.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ragmir-core/src/mcp-scope.ts b/packages/ragmir-core/src/mcp-scope.ts index 3ec1502..414b28b 100644 --- a/packages/ragmir-core/src/mcp-scope.ts +++ b/packages/ragmir-core/src/mcp-scope.ts @@ -122,7 +122,10 @@ export function workspaceScopeRefusal( name: string, config: Config, ): string { - return `This Ragmir server serves the "${name}" knowledge base only to workspaces under ${config.mcpAllowedRoots.join(", ")} (mcpAllowedRoots). Your workspace is ${verdict.workspaceRoots.join(", ")}. Register a project-scoped Ragmir server for this workspace instead.` + const allowedRoots = config.mcpAllowedRoots.map((root) => + resolveAllowedRoot(config.projectRoot, root), + ) + return `This Ragmir server serves the "${name}" knowledge base only to workspaces under ${allowedRoots.join(", ")} (mcpAllowedRoots). Your workspace is ${verdict.workspaceRoots.join(", ")}. Register a project-scoped Ragmir server for this workspace instead.` } function resolveAllowedRoot(projectRoot: string, value: string): string { diff --git a/packages/ragmir-core/src/mcp.test.ts b/packages/ragmir-core/src/mcp.test.ts index d5a6fbc..2f2386a 100644 --- a/packages/ragmir-core/src/mcp.test.ts +++ b/packages/ragmir-core/src/mcp.test.ts @@ -608,7 +608,7 @@ describe("MCP workspace scope and session", () => { }) expect(refused.isError).toBe(true) - expect(textContent(refused)).toContain("only to workspaces under .") + expect(textContent(refused)).toContain(`only to workspaces under ${root} (mcpAllowedRoots)`) expect(allowed.isError).not.toBe(true) expect(allowed._meta?.["ragmir/workspace"]).toMatchObject({ relation: "related" }) }) From 10806e8aa5a1c72742db972edfbb97fb53dfb51e Mon Sep 17 00:00:00 2001 From: Jean-Baptiste THERY Date: Wed, 28 Oct 2026 00:03:10 +0700 Subject: [PATCH 3/3] docs: add community, early quick start, and social badges to both READMEs Compared with eight successful open-source READMEs (Vite, Zod, tRPC, Biome, Orama, Context7, mem0, Chroma), every convention most of them share is now present: the GitHub README shows a three-line quick start in its first lines and a community section, and both READMEs carry downloads and stars badges; the npm README gains the centered logo header. --- README.md | 16 +++++++++++++++ packages/ragmir-core/README.md | 36 +++++++++++++++++++++++----------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a681450..bc8a456 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Linux, macOS, and Windows License: AGPL-3.0 Node.js 22.12+ + npm downloads + GitHub stars

@@ -31,6 +33,12 @@ Code, Codex, or your own application queries it through MCP, the `rgr` CLI, or a and every passage comes back with a citation the agent can open and check. Your agent plans, reasons, and writes with the model you choose; Ragmir finds the evidence. +```bash +npm install -D @jcode.labs/ragmir +npx rgr setup --agents claude,codex # index the project, connect your agents +npx rgr search "how are permissions checked?" --compact +``` + | Without Ragmir | With Ragmir | | --- | --- | | Code search misses the specs, spreadsheets, and scanned PDFs around the code. | One local index for code and documents, with optional local OCR. | @@ -282,6 +290,14 @@ then run `rgr ingest`. Monorepos can keep separate bases, selected with `rgr bas - [Troubleshooting](./docs/troubleshooting.md) - [Synthetic examples](./packages/ragmir-core/examples/sovereign-rag-demo/README.md) +## Community + +Ask questions, propose ideas, and report bugs in +[GitHub Issues](https://github.com/jcode-works/jcode-ragmir/issues); report vulnerabilities +privately through [SECURITY](./SECURITY.md). Follow releases on +[GitHub](https://github.com/jcode-works/jcode-ragmir/releases), and if Ragmir saves your agents +time, [star the repository](https://github.com/jcode-works/jcode-ragmir) so other teams find it. + ## Contributing Issues and pull requests are welcome; read [CONTRIBUTING](./CONTRIBUTING.md) and the diff --git a/packages/ragmir-core/README.md b/packages/ragmir-core/README.md index 33f700d..cf2a1c7 100644 --- a/packages/ragmir-core/README.md +++ b/packages/ragmir-core/README.md @@ -1,11 +1,23 @@ -# @jcode.labs/ragmir - -[![npm version](https://img.shields.io/npm/v/@jcode.labs/ragmir?color=cb3837&logo=npm)](https://www.npmjs.com/package/@jcode.labs/ragmir) -[![CI](https://github.com/jcode-works/jcode-ragmir/actions/workflows/ci.yml/badge.svg)](https://github.com/jcode-works/jcode-ragmir/actions/workflows/ci.yml) -[![License: AGPL-3.0](https://img.shields.io/badge/license-AGPL--3.0-2f9e44)](https://github.com/jcode-works/jcode-ragmir/blob/main/LICENSE) -[![Node.js 22.12+](https://img.shields.io/badge/node-%E2%89%A522.12-339933?logo=node.js&logoColor=white)](https://nodejs.org) - -**Cited project context for coding agents. Local, fast, and offline by default.** +

+ + Ragmir: local retrieval with exact citations for coding agents + +

+ +

@jcode.labs/ragmir

+ +

+ Cited project context for coding agents. Local, fast, and offline by default. +

+ +

+ npm version + npm downloads + CI + License: AGPL-3.0 + Node.js 22.12+ + GitHub stars +

Ragmir indexes the code, specifications, and office documents you choose, then returns precise passages with checkable citations through a TypeScript library, the `rgr` CLI, or a local MCP @@ -143,9 +155,11 @@ Read the [full documentation](https://github.com/jcode-works/jcode-ragmir#readme [CLI reference](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/cli-reference.md), [migration guide](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/migration.md), and [troubleshooting guide](https://github.com/jcode-works/jcode-ragmir/blob/main/docs/troubleshooting.md). -Issues and contributions are welcome through -[GitHub](https://github.com/jcode-works/jcode-ragmir) and -[CONTRIBUTING](https://github.com/jcode-works/jcode-ragmir/blob/main/CONTRIBUTING.md). +Ask questions and report bugs in +[GitHub Issues](https://github.com/jcode-works/jcode-ragmir/issues); contributions are welcome +through [CONTRIBUTING](https://github.com/jcode-works/jcode-ragmir/blob/main/CONTRIBUTING.md). If +Ragmir saves your agents time, [star the repository](https://github.com/jcode-works/jcode-ragmir) +so other teams find it. Ragmir is open source under [AGPL-3.0-only](https://github.com/jcode-works/jcode-ragmir/blob/main/LICENSE). JCode Works also offers a [commercial license](https://github.com/jcode-works/jcode-ragmir/blob/main/COMMERCIAL-LICENSE.md).