Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ pnpm exec rgr setup --agents claude,codex,kimi,opencode,cline
pnpm exec rgr doctor
```

Setup writes local helper files for the selected agents. The MCP surface is intentionally bounded
and read-focused. Agents can request compact evidence first, then expand one returned citation
Setup links skills into the selected agents' native project folders and writes local MCP helpers
backed by a generated project runner. The MCP surface is intentionally bounded and read-focused.
Agents can request compact evidence first, then expand one returned citation
without opening a second index or reading arbitrary files. MCP clients can read `ragmir://context`
for a compact base, readiness, freshness, and capability overview before choosing a tool.

Expand Down
15 changes: 12 additions & 3 deletions docs/agent-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Ragmir gives agents cited local passages through one stdio MCP server. Prepare t
rgr setup --agents claude,codex,kimi,opencode,cline
```

The generated files live under ignored `.ragmir/`. They reference `rgr serve-mcp` in the target repository, never a hosted document service.
The canonical files live under ignored `.ragmir/`. Setup also links the selected skills into each
agent's native project directory and generates a local `.ragmir/run.cjs` MCP runner. The runner uses
the installed project binary first, then the current package installation, with a pinned npm fallback.

## Native helpers

Expand All @@ -18,13 +20,17 @@ The generated files live under ignored `.ragmir/`. They reference `rgr serve-mcp
| OpenCode | `.ragmir/opencode.jsonc` |
| Cline | `.ragmir/cline-mcp.json` |

Install native skill discovery when the agent supports it:
Setup installs project-scoped native skill discovery by default. Re-run installation when you want a
different scope or copy mode:

```bash
rgr install-agent --agents codex,claude
```

Use `--scope user` only when you intentionally want a user-wide installation. Project scope is the default. `--mode copy` is a fallback for filesystems that cannot follow symlinks.
Use `--scope user` only when you intentionally want a user-wide installation. Project scope is the
default. Codex skills use `.agents/skills/` for both project and user discovery. `--mode copy` is a
fallback for filesystems that cannot follow symlinks. Ragmir refuses to overwrite an unmanaged
same-name skill unless you explicitly pass `--force` after reviewing it.

## Monorepo bases

Expand Down Expand Up @@ -76,4 +82,7 @@ rgr bases --json
rgr search "known phrase" --compact
```

Doctor reports runner verification, native agents discovered, and integration warnings separately
from retrieval readiness.

If the client cannot set a working directory, launch the server with `RAGMIR_PROJECT_ROOT=/absolute/path/to/project`.
5 changes: 3 additions & 2 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ evaluate one heading or structured-data branch.
| Export | Use |
| --- | --- |
| `serveMcp(cwd?)` | Start the local stdio MCP server. |
| `installSkill(options?)` / `installAgentSkills(options?)` | Install bundled helper files. |
| `installSkill(options?)` / `installAgentSkills(options?)` | Install the canonical kit and ownership-safe native skill links. |
| `inspectAgentIntegration(cwd?)` | Verify the local runner and native skill discovery targets. |
| `parseAgentTargets(value)` / `SUPPORTED_AGENT_TARGETS` | Validate supported agent targets. |
| `detectPackageManager(cwd?)` | Detect the target project package manager. |
| `rgrCommand(cwd, args)` | Build the canonical local `rgr` command. |
| `rgrCommand(cwd, args)` | Prefer the generated project runner, then build the package-manager command. |

`kbCommand` and `ragmirCommand` remain compatibility helpers. New integration code should use
`rgrCommand` and the `rgr` CLI name.
Expand Down
4 changes: 3 additions & 1 deletion docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ rgr usage-report --days 30
rgr destroy-index --yes
```

- `install-skill` and `install-agent` install the bundled project helpers.
- `setup` installs canonical skills, native project links, a local runner, and selected MCP helpers.
- `install-skill` refreshes only the canonical kit; `install-agent` changes native scope or link mode.
- `install-agent --force` replaces a conflicting same-name skill only when explicitly requested.
- `serve-mcp` starts the local stdio MCP server.
- `route-prompt` classifies whether a prompt should use Ragmir without storing it.
- `evaluate` measures retrieval against a local golden-query file.
Expand Down
3 changes: 2 additions & 1 deletion packages/ragmir-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ npx rgr setup --agents claude,codex,kimi,opencode,cline
npx rgr doctor
```

Ragmir writes helper files for the selected clients and points them at the current project. MCP
Ragmir links the canonical skills into each selected client's native project folder, writes a local
runner plus MCP helpers, and points them at the current project. MCP
exposes status, search, ask, research, exact citation expansion, audit, evaluation, usage, and
security tools. Retrieval responses have a global byte ceiling and expose metadata-only output
metrics. The server does not expose index deletion.
Expand Down
40 changes: 36 additions & 4 deletions packages/ragmir-core/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,15 +225,17 @@ program

program
.command("setup")
.description("Initialize Ragmir, install the agent kit, run doctor, and ingest when safe.")
.description(
"Initialize Ragmir, expose native agent skills, generate MCP helpers, and ingest when safe.",
)
.option(
"--target-dir <path>",
"Directory where the skill folder should be copied.",
DEFAULT_SKILL_TARGET_DIR,
)
.option(
"--agents <list>",
`Agent MCP helpers to generate: all, ${SUPPORTED_AGENT_TARGETS.join(", ")}.`,
`Native agent skills and MCP helpers to install: all, ${SUPPORTED_AGENT_TARGETS.join(", ")}.`,
"all",
)
.option(
Expand All @@ -252,6 +254,10 @@ program
"Download the configured Transformers.js embedding model and enable higher-quality semantic retrieval.",
)
.option("--no-ingest", "Skip automatic indexing even when supported files are present.")
.option(
"--force-agent-skills",
"Replace same-name native skills after reviewing that they can be overwritten.",
)
.option("--json", "Print machine-readable JSON.")
.action(
async (
Expand All @@ -263,6 +269,7 @@ program
mcpArg: string[]
semantic?: boolean
ingest?: boolean
forceAgentSkills?: boolean
json?: boolean
},
command: Command,
Expand All @@ -277,6 +284,7 @@ program
addOption(setupOptions, "semantic", options.semantic)
addOption(setupOptions, "ingest", options.ingest)
addOption(setupOptions, "mcpCommand", options.mcpCommand)
addOption(setupOptions, "forceAgentSkills", options.forceAgentSkills)
if (options.mcpArg.length > 0) {
setupOptions.mcpArgs = options.mcpArg
}
Expand Down Expand Up @@ -1327,17 +1335,27 @@ program
)
.option("--scope <scope>", "Install scope: project or user.", "project")
.option("--mode <mode>", "Expose skills as links or physical copies: link or copy.", "link")
.option(
"--force",
"Replace same-name native skills after reviewing that they can be overwritten.",
)
.option("--json", "Print machine-readable JSON.")
.action(
async (
options: { agents: string; scope: string; mode: string; json?: boolean },
options: { agents: string; scope: string; mode: string; force?: boolean; json?: boolean },
command: Command,
) => {
const cwd = projectRoot(command)
const scope = parseAgentInstallScope(options.scope)
const mode = parseAgentInstallMode(options.mode)
const agents = parseAgentTargets(options.agents)
const result = await installAgentSkills({ cwd, agents, scope, mode })
const result = await installAgentSkills({
cwd,
agents,
scope,
mode,
force: options.force ?? false,
})
if (options.json) {
console.log(JSON.stringify(result, null, 2))
return
Expand Down Expand Up @@ -1651,6 +1669,15 @@ function printDoctor(report: Awaited<ReturnType<typeof doctor>>): void {
console.log(`packageManager=${report.packageManager}`)
console.log(`runCommand=${report.runCommand}`)
console.log(`agentKitInstalled=${report.agentKitInstalled}`)
console.log(`agentIntegration.ready=${report.agentIntegration.ready}`)
console.log(`agentIntegration.runnerReady=${report.agentIntegration.runnerReady}`)
console.log(`agentIntegration.runnerMode=${report.agentIntegration.runnerMode ?? "none"}`)
console.log(`agentIntegration.projectAgents=${report.agentIntegration.projectAgents.join(",")}`)
console.log(`agentIntegration.userAgents=${report.agentIntegration.userAgents.join(",")}`)
console.log(`agentIntegration.nativeAgents=${report.agentIntegration.nativeAgents.join(",")}`)
for (const warning of report.agentIntegration.warnings) {
console.log(pc.yellow(`agentIntegration.warning: ${warning}`))
}
console.log(`embeddingProvider=${report.embeddingProvider}`)
console.log(`transformersAllowRemoteModels=${report.transformersAllowRemoteModels}`)
console.log(`redactionEnabled=${report.redactionEnabled}`)
Expand Down Expand Up @@ -1808,6 +1835,11 @@ function printSetup(result: Awaited<ReturnType<typeof setupProject>>, title: str
console.log(` - ${helper.label} MCP helper: ${helper.path}`)
}
console.log(` - agent setup guide: ${result.agentKit.agentSetupPath}`)
for (const installation of result.agentInstallations) {
console.log(
` - ${installation.label} skills: ${installation.targetDir} (${installation.mode})`,
)
}
console.log("")
if (result.semantic) {
console.log(pc.cyan("Semantic retrieval:"))
Expand Down
16 changes: 14 additions & 2 deletions packages/ragmir-core/src/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it } from "vitest"
import { doctor } from "./doctor.js"
import { ingest } from "./ingest.js"
import { initProject } from "./init.js"
import { installSkill } from "./skill.js"
import { installAgentSkills, installSkill } from "./skill.js"

const tempDirs: string[] = []

Expand All @@ -25,6 +25,7 @@ describe("doctor", () => {
expect(uninitialized.ready).toBe(false)
expect(uninitialized.packageManager).toBe("pnpm")
expect(uninitialized.agentKitInstalled).toBe(false)
expect(uninitialized.agentIntegration.ready).toBe(false)
expect(uninitialized.nextSteps).toEqual([
"Run `pnpm exec rgr setup` to initialize Ragmir and install the agent kit.",
])
Expand Down Expand Up @@ -80,7 +81,18 @@ describe("doctor", () => {

await installSkill({ cwd: root })

expect((await doctor(root)).agentKitInstalled).toBe(true)
const kitOnly = await doctor(root)
expect(kitOnly.agentKitInstalled).toBe(true)
expect(["installed-package", "npm-cache"]).toContain(kitOnly.agentIntegration.runnerMode)
expect(kitOnly.agentIntegration.projectAgents).toEqual([])
expect(kitOnly.agentIntegration.ready).toBe(
kitOnly.agentIntegration.runnerReady && kitOnly.agentIntegration.nativeAgents.length > 0,
)

await installAgentSkills({ cwd: root, agents: ["codex"] })
const integrated = await doctor(root)
expect(integrated.agentIntegration.projectAgents).toContain("codex")
expect(integrated.agentIntegration.ready).toBe(integrated.agentIntegration.runnerReady)
})

it("does not report complete coverage when a supported file yields no text", async () => {
Expand Down
23 changes: 20 additions & 3 deletions packages/ragmir-core/src/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { RAGMIR_DIR } from "./defaults.js"
import { countSkippedByReason } from "./files.js"
import { getIndexFreshnessWarning, getLexicalScanWarning } from "./index-diagnostics.js"
import { audit } from "./ingest.js"
import { rgrCommand } from "./package-manager.js"
import { RGR_RUNNER_FILENAME, rgrCommand } from "./package-manager.js"
import { securityAudit } from "./security.js"
import {
AGENT_HELPER_CONFIG_FILENAMES,
AGENT_SETUP_FILENAME,
inspectAgentIntegration,
MCP_CONFIG_FILENAME,
SKILL_NAMES,
} from "./skill.js"
Expand All @@ -22,6 +23,7 @@ export async function doctor(cwd = process.cwd()): Promise<DoctorReport> {
const config = await loadConfig(cwd)
const command = await rgrCommand(config.projectRoot, [])
const agentKitInstalled = isAgentKitInstalled(config.projectRoot)
const agentIntegration = inspectAgentIntegration(config.projectRoot)
const [auditReport, securityReport, chunksIndexed, manifest, freshnessWarning] =
await Promise.all([
audit(config.projectRoot),
Expand Down Expand Up @@ -63,6 +65,8 @@ export async function doctor(cwd = process.cwd()): Promise<DoctorReport> {
warnings: securityReport.warnings.length,
embeddingProvider: config.embeddingProvider,
agentKitInstalled,
agentRunnerReady: agentIntegration.runnerReady,
nativeAgentCount: agentIntegration.nativeAgents.length,
freshnessWarning,
lexicalScanWarning,
run: (args) => command.display + (args.length > 0 ? ` ${args.join(" ")}` : ""),
Expand All @@ -74,6 +78,7 @@ export async function doctor(cwd = process.cwd()): Promise<DoctorReport> {
packageManager: command.packageManager,
runCommand: command.display,
agentKitInstalled,
agentIntegration,
rawDir: config.rawDir,
storageDir: config.storageDir,
embeddingProvider: config.embeddingProvider,
Expand Down Expand Up @@ -126,6 +131,8 @@ interface NextActionInput {
warnings: number
embeddingProvider: string
agentKitInstalled: boolean
agentRunnerReady: boolean
nativeAgentCount: number
freshnessWarning: string | null
lexicalScanWarning: string | null
run: (args: string[]) => string
Expand Down Expand Up @@ -205,13 +212,22 @@ function nextActions(input: NextActionInput): string[] {
steps.push(
`Run \`${input.run(["research", '"your topic"'])}\` for audit-backed multi-query evidence.`,
)
if (input.agentKitInstalled) {
if (input.agentKitInstalled && input.nativeAgentCount > 0) {
steps.push(
"Run `rgr install-agent --agents claude` or another targeted agent list for native skill discovery.",
"Restart or reload the selected agents so they discover the installed Ragmir skills.",
)
if (!input.agentRunnerReady) {
steps.push(
"Install @jcode.labs/ragmir in this project or rebuild the workspace package, then rerun `rgr doctor` to verify the local runner.",
)
}
steps.push(
"Wire the matching MCP helper from .ragmir/ when the agent should call Ragmir tools directly.",
)
} else if (input.agentKitInstalled) {
steps.push(
"Run `rgr install-agent --agents claude` or another targeted agent list, then rerun `rgr doctor`.",
)
} else {
steps.push(
`Run \`${input.run(["install-skill"])}\` if an AI agent should use the local knowledge base.`,
Expand All @@ -226,6 +242,7 @@ function isAgentKitInstalled(projectRoot: string): boolean {
const ragmirDir = path.join(projectRoot, RAGMIR_DIR)
const requiredPaths = [
...SKILL_NAMES.map((skillName) => path.join(ragmirDir, "skills", skillName, "SKILL.md")),
path.join(ragmirDir, RGR_RUNNER_FILENAME),
path.join(ragmirDir, MCP_CONFIG_FILENAME),
path.join(ragmirDir, AGENT_SETUP_FILENAME),
]
Expand Down
8 changes: 6 additions & 2 deletions packages/ragmir-core/src/gitignore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { RAGMIR_GITIGNORE_ENTRY } from "./defaults.js"

export const RAGMIR_GITIGNORE_ENTRIES = [RAGMIR_GITIGNORE_ENTRY]

export async function ensureRagmirGitignore(cwd = process.cwd()): Promise<boolean> {
export async function ensureRagmirGitignore(
cwd = process.cwd(),
additionalEntries: readonly string[] = [],
): Promise<boolean> {
const root = path.resolve(cwd)
const gitignorePath = path.join(root, ".gitignore")
const current = existsSync(gitignorePath) ? await readFile(gitignorePath, "utf8") : ""
Expand All @@ -15,7 +18,8 @@ export async function ensureRagmirGitignore(cwd = process.cwd()): Promise<boolea
.map((line) => line.trim())
.filter(Boolean),
)
const missingEntries = RAGMIR_GITIGNORE_ENTRIES.filter((entry) => !currentLines.has(entry))
const desiredEntries = [...new Set([...RAGMIR_GITIGNORE_ENTRIES, ...additionalEntries])]
const missingEntries = desiredEntries.filter((entry) => !currentLines.has(entry))

if (missingEntries.length === 0) {
return false
Expand Down
3 changes: 3 additions & 0 deletions packages/ragmir-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,18 @@ export type {
AgentHelperFile,
AgentInstallMode,
AgentInstallScope,
AgentIntegrationReport,
AgentSkillInstallation,
AgentTarget,
InstallAgentSkillsOptions,
InstallAgentSkillsResult,
InstallSkillOptions,
InstallSkillResult,
RagmirRunnerMode,
} from "./skill.js"
export {
bundledSkillPath,
inspectAgentIntegration,
installAgentSkills,
installSkill,
parseAgentTargets,
Expand Down
16 changes: 15 additions & 1 deletion packages/ragmir-core/src/package-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises"
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
import os from "node:os"
import path from "node:path"
import { afterEach, describe, expect, it } from "vitest"
Expand Down Expand Up @@ -50,6 +50,20 @@ describe("package manager detection", () => {
expect(await detectPackageManager(yarnRoot)).toBe("yarn")
})

it("prefers the generated project runner when it exists", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "ragmir-pm-"))
tempDirs.push(root)
const runnerPath = path.join(root, ".ragmir", "run.cjs")
await mkdir(path.dirname(runnerPath), { recursive: true })
await writeFile(runnerPath, "", "utf8")

await expect(rgrCommand(root, ["doctor"])).resolves.toMatchObject({
command: "node",
args: [runnerPath, "doctor"],
display: "node .ragmir/run.cjs doctor",
})
})

it("keeps existing command helpers as compatibility aliases", () => {
expect(ragmirCommand).toBe(rgrCommand)
expect(kbCommand).toBe(rgrCommand)
Expand Down
Loading