rlm-navigator 0.1.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/README.md +186 -0
- package/bin/cli.js +319 -0
- package/daemon/__init__.py +0 -0
- package/daemon/requirements.txt +10 -0
- package/daemon/rlm_daemon.py +418 -0
- package/daemon/rlm_repl.py +465 -0
- package/daemon/squeezer.py +414 -0
- package/package.json +17 -0
- package/server/package.json +20 -0
- package/server/src/index.ts +684 -0
- package/server/tsconfig.json +19 -0
- package/templates/CLAUDE_SNIPPET.md +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# RLM Navigator
|
|
2
|
+
|
|
3
|
+
Token-efficient codebase navigation for AI-assisted coding. Treats codebases as navigable hierarchical trees of AST skeletons — the AI sees structure first, drills into implementations only when needed. A file-watching daemon caches AST structures, a stateful REPL with dependency-aware staleness tracking provides targeted analysis, and automatic output truncation keeps every tool response within budget.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
AI coding assistants treat source files as opaque text blobs. Every interaction starts the same way: read the whole file, scan for the relevant section, discard the rest. This is fundamentally wasteful because source code is *structured* — it has a hierarchy (modules → classes → methods → statements) that can be navigated without reading implementations.
|
|
8
|
+
|
|
9
|
+
The cost compounds quickly:
|
|
10
|
+
|
|
11
|
+
- **Context Bloat**: A 500-line file consumes ~2,000 tokens even when you only need one function. Across a multi-file task, the window fills with irrelevant code that the model must attend to on every generation step.
|
|
12
|
+
- **Context Rot**: LLM attention degrades over long contexts. Important instructions and earlier findings get diluted as the window fills with raw source. The model "forgets" what it already learned — not because the tokens are gone, but because attention is spread too thin.
|
|
13
|
+
- **Exploration Loops**: Without structural summaries, the AI has no compact representation of what a file contains. It re-reads files it already saw, or reads adjacent files speculatively, burning tokens on redundant I/O.
|
|
14
|
+
- **Stale Data**: Results stored in variables go stale when underlying files change. Without tracking, the AI operates on outdated information — a silent correctness problem that's worse than wasted tokens.
|
|
15
|
+
|
|
16
|
+
The root cause is a mismatch between how code is organized (hierarchical, structured) and how AI tools access it (flat, full-text). RLM Navigator closes this gap by exposing code structure as a first-class navigation primitive.
|
|
17
|
+
|
|
18
|
+
## Solution
|
|
19
|
+
|
|
20
|
+
RLM Navigator provides 10 MCP tools that enforce a surgical navigation workflow:
|
|
21
|
+
|
|
22
|
+
**Navigation tools:**
|
|
23
|
+
|
|
24
|
+
| Tool | Purpose |
|
|
25
|
+
|------|---------|
|
|
26
|
+
| `get_status` | Check daemon health |
|
|
27
|
+
| `rlm_tree` | See directory structure (replaces ls/find) |
|
|
28
|
+
| `rlm_map` | See file signatures only (replaces cat/read) |
|
|
29
|
+
| `rlm_drill` | Read specific symbol implementation |
|
|
30
|
+
| `rlm_search` | Find symbols across files |
|
|
31
|
+
|
|
32
|
+
**REPL tools** (stateful Python environment with pickle persistence):
|
|
33
|
+
|
|
34
|
+
| Tool | Purpose |
|
|
35
|
+
|------|---------|
|
|
36
|
+
| `rlm_repl_init` | Initialize the stateful REPL |
|
|
37
|
+
| `rlm_repl_exec` | Execute Python code (variables persist across calls) |
|
|
38
|
+
| `rlm_repl_status` | Check variables, buffers, execution count + staleness warnings |
|
|
39
|
+
| `rlm_repl_reset` | Clear all REPL state |
|
|
40
|
+
| `rlm_repl_export` | Export accumulated buffers |
|
|
41
|
+
|
|
42
|
+
Built-in REPL helpers: `peek()` (read lines), `grep()` (regex search), `chunk_indices()` / `write_chunks()` (file chunking), `add_buffer()` (accumulate findings). All helpers automatically track file dependencies — when source files change, stale variables and buffers are flagged in `repl_status` and `repl_exec` output.
|
|
43
|
+
|
|
44
|
+
The workflow: **tree → map → drill → edit**. For complex analysis: **init → exec with helpers → export buffers**. Each step loads only what's needed.
|
|
45
|
+
|
|
46
|
+
## Architecture
|
|
47
|
+
|
|
48
|
+
```mermaid
|
|
49
|
+
graph TB
|
|
50
|
+
MCP["MCP Server<br/>(TypeScript)"]
|
|
51
|
+
Client["Claude Code<br/>(AI Client)"]
|
|
52
|
+
Daemon["Python Daemon"]
|
|
53
|
+
FS["File System<br/>+ AST Cache"]
|
|
54
|
+
|
|
55
|
+
MCP -->|stdio| Client
|
|
56
|
+
MCP -->|TCP/JSON| Daemon
|
|
57
|
+
Daemon -->|watchdog| FS
|
|
58
|
+
Daemon -->|tree-sitter| FS
|
|
59
|
+
Daemon -->|cache| FS
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx rlm-navigator@latest install
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This copies the daemon and server into a local `.rlm/` directory, installs dependencies, builds the MCP server, and registers with Claude Code. The daemon **auto-starts** when Claude Code connects — no separate terminal needed.
|
|
69
|
+
|
|
70
|
+
### Other Commands
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx rlm-navigator status # Check daemon health
|
|
74
|
+
npx rlm-navigator uninstall # Remove from project
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Manual / Development Setup
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# 1. Install Python deps
|
|
81
|
+
pip install -r daemon/requirements.txt
|
|
82
|
+
|
|
83
|
+
# 2. Build MCP server
|
|
84
|
+
cd server && npm install && npm run build
|
|
85
|
+
|
|
86
|
+
# 3. Register with Claude Code
|
|
87
|
+
claude mcp add rlm-navigator -- node /path/to/server/build/index.js
|
|
88
|
+
|
|
89
|
+
# 4. Start the daemon (in a separate terminal)
|
|
90
|
+
python daemon/rlm_daemon.py --root /path/to/your/project
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Legacy install scripts (`install.sh`, `install.ps1`) are still available for development.
|
|
94
|
+
|
|
95
|
+
## Supported Languages
|
|
96
|
+
|
|
97
|
+
Tree-sitter powered parsing for: **Python, JavaScript, TypeScript, Go, Rust, Java, C, C++**
|
|
98
|
+
|
|
99
|
+
Unsupported file types get a graceful fallback (first 20 lines + line count).
|
|
100
|
+
|
|
101
|
+
## Benchmarks
|
|
102
|
+
|
|
103
|
+
`benchmark.py` supports four modes that measure different aspects of token efficiency.
|
|
104
|
+
|
|
105
|
+
### Workflow: Navigation Overhead
|
|
106
|
+
|
|
107
|
+
Compares "grep + full file reads" vs "tree → search → map → drill". Benchmarked against [tiangolo/fastapi](https://github.com/tiangolo/fastapi):
|
|
108
|
+
|
|
109
|
+
| Query | Approach | Files | Tokens | Reduction | Efficiency |
|
|
110
|
+
|---|---|---|---|---|---|
|
|
111
|
+
| `authenticate` | Traditional | 42 full reads | 47,131 | — | — |
|
|
112
|
+
| `authenticate` | RLM (full repo tree) | 9 maps | 19,109 | 59% | 2.5x |
|
|
113
|
+
| `authenticate` | RLM (targeted tree) | 9 maps | **8,364** | **82%** | **5.6x** |
|
|
114
|
+
| `OAuth2PasswordBearer` | Traditional | 20 full reads | 25,725 | — | — |
|
|
115
|
+
| `OAuth2PasswordBearer` | RLM (targeted tree) | 1 map | **3,267** | **87%** | **7.9x** |
|
|
116
|
+
|
|
117
|
+
Self-benchmark (this repo, query `squeeze`):
|
|
118
|
+
|
|
119
|
+
| Approach | Files | Tokens | Reduction |
|
|
120
|
+
|---|---|---|---|
|
|
121
|
+
| Traditional | 6 full reads | 22,139 | — |
|
|
122
|
+
| RLM | 5 maps + 5 drills | **3,358** | **85% (6.6x)** |
|
|
123
|
+
|
|
124
|
+
Scoping `rlm_tree` to the relevant subdirectory (`--tree-path fastapi/security`) is critical for large repos — it reduces tree overhead from ~11K tokens to 205, making the difference between 2-3x and **6-8x** savings.
|
|
125
|
+
|
|
126
|
+
### REPL: Targeted Analysis
|
|
127
|
+
|
|
128
|
+
Compares full file reads vs REPL-assisted grep + peek windows. Self-benchmark (query `handle_request`):
|
|
129
|
+
|
|
130
|
+
| Approach | Tokens | Reduction |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| Traditional (4 full reads) | 15,594 | — |
|
|
133
|
+
| REPL (grep + peek) | **16** | **~100% (974x)** |
|
|
134
|
+
|
|
135
|
+
The REPL's `grep()` returns only matching lines with file/line references — no need to read surrounding context unless you choose to `peek()` a specific range.
|
|
136
|
+
|
|
137
|
+
### Truncation: Response Capping
|
|
138
|
+
|
|
139
|
+
Measures how much the 8,000-char truncation cap saves across all tool responses. For well-structured codebases where skeletons are concise, truncation rarely activates — but for large files or verbose tree outputs it prevents runaway token consumption.
|
|
140
|
+
|
|
141
|
+
### Chunks: Skeleton vs Full-File vs Per-Chunk
|
|
142
|
+
|
|
143
|
+
Compares the cost of reading a file three ways: full text, skeleton only, and chunked windows. Self-benchmark (`daemon/rlm_daemon.py`, 397 lines):
|
|
144
|
+
|
|
145
|
+
| Approach | Tokens | Savings vs Full |
|
|
146
|
+
|---|---|---|
|
|
147
|
+
| Full file read | 3,332 | — |
|
|
148
|
+
| Skeleton (`rlm_map`) | 492 | **85%** |
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Run benchmarks yourself
|
|
152
|
+
python benchmark.py --root /path/to/project --query "symbol" # workflow
|
|
153
|
+
python benchmark.py --root /path/to/project --query "symbol" --mode truncation # truncation
|
|
154
|
+
python benchmark.py --root /path/to/project --query "symbol" --mode repl # repl
|
|
155
|
+
python benchmark.py --root /path/to/project --file "src/file.py" --mode chunks # chunks
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
| Environment Variable | Default | Description |
|
|
161
|
+
|---------------------|---------|-------------|
|
|
162
|
+
| `RLM_DAEMON_PORT` | `9177` | TCP port for daemon communication |
|
|
163
|
+
| `RLM_MAX_RESPONSE` | `8000` | Max chars before output truncation |
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Run tests
|
|
169
|
+
cd daemon && python -m pytest tests/ -v
|
|
170
|
+
|
|
171
|
+
# Start daemon in dev mode
|
|
172
|
+
python daemon/rlm_daemon.py --root .
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## How It Works
|
|
176
|
+
|
|
177
|
+
1. **Daemon** watches your project with `watchdog`, parses files with `tree-sitter`, caches AST skeletons. File change events propagate to both the skeleton cache and the REPL's dependency tracker.
|
|
178
|
+
2. **REPL** provides a pickle-persisted Python environment with codebase helpers (peek, grep, chunking, buffers). Tracks file dependencies per variable/buffer via mtime snapshots — when files change, staleness warnings surface automatically.
|
|
179
|
+
3. **MCP Server** bridges Claude Code to the daemon via TCP JSON protocol, with automatic output truncation and staleness warning formatting.
|
|
180
|
+
4. **Skill** enforces the navigation workflow (tree → map → drill → edit) and the chunk-delegate-synthesize workflow for large analyses.
|
|
181
|
+
5. **Sub-agent** (Haiku) analyzes file chunks with structured output — relevance rankings, missing items, and suggested next queries.
|
|
182
|
+
|
|
183
|
+
## Inspired By
|
|
184
|
+
|
|
185
|
+
- [brainqub3/claude_code_RLM](https://github.com/brainqub3/claude_code_RLM) — RLM for document navigation
|
|
186
|
+
- Tree-sitter — universal AST parsing
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync, spawnSync } = require("child_process");
|
|
7
|
+
const net = require("net");
|
|
8
|
+
const readline = require("readline");
|
|
9
|
+
|
|
10
|
+
const CWD = process.cwd();
|
|
11
|
+
const RLM_DIR = path.join(CWD, ".rlm");
|
|
12
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function copyDirSync(src, dest) {
|
|
19
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
20
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
21
|
+
const srcPath = path.join(src, entry.name);
|
|
22
|
+
const destPath = path.join(dest, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
// Skip node_modules, build, __pycache__, .venv
|
|
25
|
+
if (["node_modules", "build", "__pycache__", ".venv", ".git"].includes(entry.name)) continue;
|
|
26
|
+
copyDirSync(srcPath, destPath);
|
|
27
|
+
} else {
|
|
28
|
+
fs.copyFileSync(srcPath, destPath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findPython() {
|
|
34
|
+
for (const cmd of ["python", "python3"]) {
|
|
35
|
+
try {
|
|
36
|
+
const result = spawnSync(cmd, ["--version"], { stdio: "pipe" });
|
|
37
|
+
if (result.status === 0) return cmd;
|
|
38
|
+
} catch {
|
|
39
|
+
// not found
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ask(question) {
|
|
46
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
rl.question(question, (answer) => {
|
|
49
|
+
rl.close();
|
|
50
|
+
resolve(answer.trim());
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function run(cmd, opts = {}) {
|
|
56
|
+
console.log(` $ ${cmd}`);
|
|
57
|
+
try {
|
|
58
|
+
execSync(cmd, { stdio: "inherit", ...opts });
|
|
59
|
+
return true;
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(` Command failed: ${cmd}`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Install
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
async function install() {
|
|
71
|
+
console.log("=== RLM Navigator — Per-Project Install ===\n");
|
|
72
|
+
|
|
73
|
+
if (fs.existsSync(RLM_DIR)) {
|
|
74
|
+
console.log(".rlm/ already exists. Run 'rlm-navigator uninstall' first to reinstall.");
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Check Python
|
|
79
|
+
const python = findPython();
|
|
80
|
+
if (!python) {
|
|
81
|
+
console.error("Error: Python not found. Install Python 3.8+ and ensure 'python' or 'python3' is on PATH.");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 1. Copy daemon/ and server/ into .rlm/
|
|
86
|
+
console.log("[1/5] Copying daemon and server into .rlm/ ...");
|
|
87
|
+
copyDirSync(path.join(PKG_ROOT, "daemon"), path.join(RLM_DIR, "daemon"));
|
|
88
|
+
copyDirSync(path.join(PKG_ROOT, "server"), path.join(RLM_DIR, "server"));
|
|
89
|
+
console.log(" Done.\n");
|
|
90
|
+
|
|
91
|
+
// 2. Install Python deps
|
|
92
|
+
console.log("[2/5] Installing Python dependencies ...");
|
|
93
|
+
const reqFile = path.join(RLM_DIR, "daemon", "requirements.txt");
|
|
94
|
+
if (!run(`${python} -m pip install -r "${reqFile}"`)) {
|
|
95
|
+
console.error("\nFailed to install Python dependencies. Check pip is available.");
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
console.log("");
|
|
99
|
+
|
|
100
|
+
// 3. Build MCP server
|
|
101
|
+
console.log("[3/5] Building MCP server ...");
|
|
102
|
+
if (!run("npm install", { cwd: path.join(RLM_DIR, "server") })) {
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (!run("npm run build", { cwd: path.join(RLM_DIR, "server") })) {
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log("");
|
|
109
|
+
|
|
110
|
+
// 4. CLAUDE.md integration
|
|
111
|
+
console.log("[4/5] Integrating CLAUDE.md ...");
|
|
112
|
+
const snippetPath = path.join(PKG_ROOT, "templates", "CLAUDE_SNIPPET.md");
|
|
113
|
+
const snippet = fs.readFileSync(snippetPath, "utf-8");
|
|
114
|
+
const claudeMdPath = path.join(CWD, "CLAUDE.md");
|
|
115
|
+
|
|
116
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
117
|
+
fs.writeFileSync(claudeMdPath, snippet);
|
|
118
|
+
console.log(" Created CLAUDE.md with RLM Navigator instructions.\n");
|
|
119
|
+
} else {
|
|
120
|
+
const existing = fs.readFileSync(claudeMdPath, "utf-8");
|
|
121
|
+
if (existing.includes("<!-- rlm-navigator:start -->")) {
|
|
122
|
+
console.log(" CLAUDE.md already contains RLM Navigator block — skipped.\n");
|
|
123
|
+
} else {
|
|
124
|
+
fs.writeFileSync(claudeMdPath, existing + "\n" + snippet);
|
|
125
|
+
console.log(" Appended RLM Navigator block to CLAUDE.md.\n");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 5. .gitignore
|
|
130
|
+
const gitignorePath = path.join(CWD, ".gitignore");
|
|
131
|
+
const answer = await ask("Add .rlm/ to .gitignore? [Y/n] ");
|
|
132
|
+
if (answer.toLowerCase() !== "n") {
|
|
133
|
+
if (fs.existsSync(gitignorePath)) {
|
|
134
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
135
|
+
if (!content.includes(".rlm")) {
|
|
136
|
+
fs.appendFileSync(gitignorePath, "\n# RLM Navigator (local install)\n.rlm/\n");
|
|
137
|
+
console.log(" Added .rlm/ to .gitignore.\n");
|
|
138
|
+
} else {
|
|
139
|
+
console.log(" .rlm already in .gitignore.\n");
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
fs.writeFileSync(gitignorePath, "# RLM Navigator (local install)\n.rlm/\n");
|
|
143
|
+
console.log(" Created .gitignore with .rlm/ entry.\n");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 6. Register MCP server
|
|
148
|
+
console.log("[5/5] Registering MCP server with Claude Code ...");
|
|
149
|
+
const mcpServerPath = path.join(RLM_DIR, "server", "build", "index.js");
|
|
150
|
+
const claudeAvailable = spawnSync("claude", ["--version"], { stdio: "pipe" }).status === 0;
|
|
151
|
+
|
|
152
|
+
if (claudeAvailable) {
|
|
153
|
+
const registerCmd = `claude mcp add rlm-navigator --env RLM_PROJECT_ROOT="${CWD}" -- node "${mcpServerPath}"`;
|
|
154
|
+
if (!run(registerCmd)) {
|
|
155
|
+
console.log(" Manual registration command:");
|
|
156
|
+
console.log(` claude mcp add rlm-navigator --env RLM_PROJECT_ROOT="${CWD}" -- node "${mcpServerPath}"`);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
console.log(" Claude CLI not found. Register manually:");
|
|
160
|
+
console.log(` claude mcp add rlm-navigator --env RLM_PROJECT_ROOT="${CWD}" -- node "${mcpServerPath}"`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log("\n=== Installation complete ===");
|
|
164
|
+
console.log("\nThe daemon will auto-start when Claude Code connects.");
|
|
165
|
+
console.log("Run 'npx rlm-navigator status' to check daemon health.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Uninstall
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
function uninstall() {
|
|
173
|
+
console.log("=== RLM Navigator — Uninstall ===\n");
|
|
174
|
+
|
|
175
|
+
// 1. Remove MCP registration
|
|
176
|
+
const claudeAvailable = spawnSync("claude", ["--version"], { stdio: "pipe" }).status === 0;
|
|
177
|
+
if (claudeAvailable) {
|
|
178
|
+
console.log("Removing MCP server registration ...");
|
|
179
|
+
run("claude mcp remove rlm-navigator");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 2. Remove .rlm/
|
|
183
|
+
if (fs.existsSync(RLM_DIR)) {
|
|
184
|
+
console.log("Removing .rlm/ directory ...");
|
|
185
|
+
fs.rmSync(RLM_DIR, { recursive: true, force: true });
|
|
186
|
+
console.log(" Done.\n");
|
|
187
|
+
} else {
|
|
188
|
+
console.log(".rlm/ not found — nothing to remove.\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 3. Remove CLAUDE.md snippet
|
|
192
|
+
const claudeMdPath = path.join(CWD, "CLAUDE.md");
|
|
193
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
194
|
+
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
195
|
+
if (content.includes("<!-- rlm-navigator:start -->")) {
|
|
196
|
+
const cleaned = content.replace(
|
|
197
|
+
/\n?<!-- rlm-navigator:start -->[\s\S]*?<!-- rlm-navigator:end -->\n?/,
|
|
198
|
+
""
|
|
199
|
+
);
|
|
200
|
+
if (cleaned.trim() === "") {
|
|
201
|
+
fs.unlinkSync(claudeMdPath);
|
|
202
|
+
console.log("Removed empty CLAUDE.md.");
|
|
203
|
+
} else {
|
|
204
|
+
fs.writeFileSync(claudeMdPath, cleaned);
|
|
205
|
+
console.log("Removed RLM Navigator block from CLAUDE.md.");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log("\n=== Uninstall complete ===");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Status
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
function status() {
|
|
218
|
+
console.log("=== RLM Navigator — Status ===\n");
|
|
219
|
+
|
|
220
|
+
// Check .rlm/
|
|
221
|
+
if (!fs.existsSync(RLM_DIR)) {
|
|
222
|
+
console.log("Not installed (.rlm/ not found).");
|
|
223
|
+
console.log("Run: npx rlm-navigator install");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.log(".rlm/ directory: present");
|
|
227
|
+
|
|
228
|
+
// Check port file
|
|
229
|
+
const portFile = path.join(RLM_DIR, "port");
|
|
230
|
+
let port = 9177;
|
|
231
|
+
if (fs.existsSync(portFile)) {
|
|
232
|
+
port = parseInt(fs.readFileSync(portFile, "utf-8").trim(), 10);
|
|
233
|
+
console.log(`Port file: ${port}`);
|
|
234
|
+
} else {
|
|
235
|
+
console.log("Port file: not found (daemon may not be running)");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// TCP health check
|
|
239
|
+
const client = new net.Socket();
|
|
240
|
+
const timer = setTimeout(() => {
|
|
241
|
+
client.destroy();
|
|
242
|
+
console.log("Daemon: OFFLINE (connection timed out)");
|
|
243
|
+
}, 2000);
|
|
244
|
+
|
|
245
|
+
client.connect(port, "127.0.0.1", () => {
|
|
246
|
+
client.on("data", (chunk) => {
|
|
247
|
+
clearTimeout(timer);
|
|
248
|
+
const msg = chunk.toString("utf-8");
|
|
249
|
+
client.destroy();
|
|
250
|
+
if (msg.includes("ALIVE")) {
|
|
251
|
+
console.log("Daemon: ONLINE");
|
|
252
|
+
} else {
|
|
253
|
+
console.log("Daemon: responded but unexpected message");
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
client.on("error", () => {
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
console.log("Daemon: OFFLINE (connection refused)");
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Help
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
function help() {
|
|
269
|
+
console.log(`
|
|
270
|
+
rlm-navigator — Token-efficient codebase navigation for AI coding
|
|
271
|
+
|
|
272
|
+
Usage:
|
|
273
|
+
npx rlm-navigator <command>
|
|
274
|
+
|
|
275
|
+
Commands:
|
|
276
|
+
install Install RLM Navigator into the current project (.rlm/)
|
|
277
|
+
uninstall Remove RLM Navigator from the current project
|
|
278
|
+
status Check daemon health and installation status
|
|
279
|
+
help Show this help message
|
|
280
|
+
|
|
281
|
+
The install command:
|
|
282
|
+
- Copies daemon and server into .rlm/
|
|
283
|
+
- Installs Python dependencies
|
|
284
|
+
- Builds the MCP server
|
|
285
|
+
- Registers with Claude Code
|
|
286
|
+
- Integrates CLAUDE.md with navigation instructions
|
|
287
|
+
- Daemon auto-starts when Claude Code connects
|
|
288
|
+
`.trim());
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Main
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
const command = process.argv[2] || "help";
|
|
296
|
+
|
|
297
|
+
switch (command) {
|
|
298
|
+
case "install":
|
|
299
|
+
install().catch((err) => {
|
|
300
|
+
console.error("Install failed:", err.message);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
});
|
|
303
|
+
break;
|
|
304
|
+
case "uninstall":
|
|
305
|
+
uninstall();
|
|
306
|
+
break;
|
|
307
|
+
case "status":
|
|
308
|
+
status();
|
|
309
|
+
break;
|
|
310
|
+
case "help":
|
|
311
|
+
case "--help":
|
|
312
|
+
case "-h":
|
|
313
|
+
help();
|
|
314
|
+
break;
|
|
315
|
+
default:
|
|
316
|
+
console.error(`Unknown command: ${command}`);
|
|
317
|
+
help();
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
File without changes
|