🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

rlm-navigator

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

rlm-navigator - npm Package Compare versions

Comparing version
1.1.6
to
1.2.0
+134
-0
bin/cli.js

@@ -35,2 +35,126 @@ #!/usr/bin/env node

// ---------------------------------------------------------------------------
// Enrichment Provider Data
// ---------------------------------------------------------------------------
const ENRICHMENT_PROVIDERS = [
{
name: "Anthropic",
key: "anthropic",
desc: "Claude Haiku 4.5",
api_key_env: "ANTHROPIC_API_KEY",
models: [
{ id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5 (recommended)" },
{ id: "claude-sonnet-4-5-20250514", label: "Claude Sonnet 4.5" },
],
},
{
name: "OpenAI",
key: "openai",
desc: "GPT-4o-mini, GPT-4o, GPT-4.1-mini",
api_key_env: "OPENAI_API_KEY",
models: [
{ id: "gpt-4o-mini", label: "GPT-4o-mini (recommended)" },
{ id: "gpt-4o", label: "GPT-4o" },
{ id: "gpt-4.1-mini", label: "GPT-4.1-mini" },
],
},
{
name: "OpenRouter",
key: "openrouter",
desc: "Multi-provider proxy",
api_key_env: "OPENROUTER_API_KEY",
models: [
{ id: "anthropic/claude-haiku-4-5", label: "Claude Haiku 4.5 (recommended)" },
{ id: "openai/gpt-4o-mini", label: "GPT-4o-mini" },
{ id: "google/gemini-2.0-flash", label: "Gemini 2.0 Flash" },
{ id: "meta-llama/llama-3.3-70b-instruct", label: "Llama 3.3 70B" },
],
},
];
function apiKeyInstructions(envVar) {
const isWindows = process.platform === "win32";
if (isWindows) {
return [
chalk.bold(" Set your API key (PowerShell):"),
chalk.dim(` $env:${envVar} = "your-key-here"`) + chalk.dim(" # current session"),
chalk.dim(` [System.Environment]::SetEnvironmentVariable("${envVar}", "your-key-here", "User")`) + chalk.dim(" # permanent"),
];
}
return [
chalk.bold(" Set your API key (bash/zsh):"),
chalk.dim(` export ${envVar}="your-key-here"`) + chalk.dim(" # current session"),
chalk.dim(` echo 'export ${envVar}="your-key-here"' >> ~/.bashrc`) + chalk.dim(" # permanent"),
];
}
async function configureEnrichment() {
console.log("");
console.log(chalk.bold.cyan(" Enrichment Provider"));
console.log(chalk.dim(" Enrichment adds semantic summaries to code skeletons using a small LLM."));
console.log(chalk.dim(" Requires an API key from your chosen provider."));
console.log("");
// Provider selection
for (let i = 0; i < ENRICHMENT_PROVIDERS.length; i++) {
const p = ENRICHMENT_PROVIDERS[i];
console.log(` ${chalk.cyan(i + 1 + ")")} ${chalk.white(p.name)} ${chalk.dim("(" + p.desc + ")")}`);
}
console.log(` ${chalk.cyan(ENRICHMENT_PROVIDERS.length + 1 + ")")} ${chalk.dim("Skip (no enrichment)")}`);
console.log("");
const providerAnswer = await ask(chalk.cyan(" ? ") + "Select provider " + chalk.dim(`[1-${ENRICHMENT_PROVIDERS.length + 1}] `) );
const providerIdx = parseInt(providerAnswer, 10) - 1;
if (isNaN(providerIdx) || providerIdx < 0 || providerIdx >= ENRICHMENT_PROVIDERS.length) {
console.log(chalk.dim(" Skipping enrichment configuration."));
return null;
}
const provider = ENRICHMENT_PROVIDERS[providerIdx];
// Model selection
console.log("");
console.log(chalk.bold(` ${provider.name} Models:`));
for (let i = 0; i < provider.models.length; i++) {
console.log(` ${chalk.cyan(i + 1 + ")")} ${provider.models[i].label}`);
}
console.log("");
const modelAnswer = await ask(chalk.cyan(" ? ") + "Select model " + chalk.dim(`[1-${provider.models.length}] `));
const modelIdx = parseInt(modelAnswer, 10) - 1;
const model = (modelIdx >= 0 && modelIdx < provider.models.length)
? provider.models[modelIdx]
: provider.models[0];
// API key instructions
console.log("");
const instructions = apiKeyInstructions(provider.api_key_env);
for (const line of instructions) {
console.log(line);
}
console.log("");
return {
provider: provider.key,
model: model.id,
api_key_env: provider.api_key_env,
};
}
function writeEnrichmentConfig(enrichment) {
const configPath = path.join(RLM_DIR, "config.json");
let config = {};
if (fs.existsSync(configPath)) {
try {
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
} catch (err) {
console.log(chalk.dim(` Warning: could not parse existing config.json, overwriting. (${err.message})`));
}
}
config.enrichment = enrichment || { provider: null, model: null, api_key_env: null };
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
}
function banner() {

@@ -414,2 +538,12 @@ console.log(BANNER);

// Enrichment provider selection
const enrichment = await configureEnrichment();
let enrichSpinner = step("Saving enrichment configuration...");
writeEnrichmentConfig(enrichment);
if (enrichment) {
enrichSpinner.succeed(`Enrichment: ${enrichment.provider} / ${enrichment.model}`);
} else {
enrichSpinner.succeed("Enrichment: skipped");
}
// .gitignore prompt (install-only)

@@ -416,0 +550,0 @@ let spinner = step("Checking .gitignore...");

+61
-4
"""Configuration for RLM Navigator — manages API keys, feature flags, and dependency detection."""
import json
import os

@@ -15,7 +16,12 @@

# Fallback defaults when no config file exists
_DEFAULT_ANTHROPIC_MODEL = "claude-haiku-4-5-20251001"
class RLMConfig:
"""Centralized configuration. Reads from environment variables."""
"""Centralized configuration. Reads .rlm/config.json then falls back to env vars."""
def __init__(self):
def __init__(self, root: str = "."):
self._root = root
# Legacy env vars (kept for backward compat + doc indexing)
self.anthropic_api_key: str | None = os.environ.get("ANTHROPIC_API_KEY")

@@ -26,7 +32,58 @@ self.openai_api_key: str | None = os.environ.get("CHATGPT_API_KEY")

self._anthropic_available = _UNCHECKED
# Enrichment config from .rlm/config.json
self._enrichment_provider: str | None = None
self._enrichment_model: str | None = None
self._enrichment_api_key_env: str | None = None
self._config_loaded = False
self._load_rlm_config()
def _load_rlm_config(self):
"""Read .rlm/config.json for enrichment provider settings."""
config_path = os.path.join(self._root, ".rlm", "config.json")
if not os.path.isfile(config_path):
return
try:
with open(config_path, "r", encoding="utf-8") as f:
data = json.load(f)
except (json.JSONDecodeError, OSError):
return
enrichment = data.get("enrichment")
if not isinstance(enrichment, dict):
return
self._enrichment_provider = enrichment.get("provider")
self._enrichment_model = enrichment.get("model")
self._enrichment_api_key_env = enrichment.get("api_key_env")
self._config_loaded = True
@property
def enrichment_provider(self) -> str | None:
if self._config_loaded:
return self._enrichment_provider
# Fallback: if ANTHROPIC_API_KEY is set, assume anthropic
if self.anthropic_api_key:
return "anthropic"
return None
@property
def enrichment_model(self) -> str | None:
if self._config_loaded:
return self._enrichment_model
# Fallback default
if self.anthropic_api_key:
return _DEFAULT_ANTHROPIC_MODEL
return None
@property
def enrichment_api_key(self) -> str | None:
if self._config_loaded:
if self._enrichment_api_key_env:
return os.environ.get(self._enrichment_api_key_env)
return None # Config loaded but no env var name -> no key
# Fallback: no config file, use ANTHROPIC_API_KEY directly
return self.anthropic_api_key
@property
def enrichment_enabled(self) -> bool:
"""Haiku enrichment requires Anthropic key + SDK."""
return self.anthropic_api_key is not None and self.anthropic_available
"""Enrichment requires a provider and a resolved API key."""
return self.enrichment_provider is not None and self.enrichment_api_key is not None

@@ -33,0 +90,0 @@ @property

+80
-28

@@ -1,7 +0,8 @@

"""Node enricher — generates semantic summaries for AST skeleton nodes using Haiku.
"""Node enricher — generates semantic summaries for AST skeleton nodes.
Parses skeleton output from squeezer.py, batches symbols, calls Haiku for 1-line
summaries, caches results by file path + mtime.
Parses skeleton output from squeezer.py, batches symbols, calls the configured
LLM provider for 1-line summaries, caches results by file path + mtime.
"""
import importlib
import queue

@@ -13,3 +14,69 @@ import re

# Haiku prompt template
# Lazy-loaded SDK modules and client instances
_sdk_cache: dict[str, object] = {}
_client_cache: dict[tuple, object] = {}
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
def _get_sdk(name: str):
"""Lazy-import an SDK module by name."""
if name not in _sdk_cache:
_sdk_cache[name] = importlib.import_module(name)
return _sdk_cache[name]
def _get_client(provider: str, api_key: str, base_url: str | None = None):
"""Get or create a cached SDK client instance."""
key = (provider, api_key, base_url)
if key not in _client_cache:
if provider == "anthropic":
sdk = _get_sdk("anthropic")
_client_cache[key] = sdk.Anthropic(api_key=api_key)
else:
sdk = _get_sdk("openai")
_client_cache[key] = sdk.OpenAI(api_key=api_key, base_url=base_url)
return _client_cache[key]
def _parse_enrichment_response(text: str) -> Optional[dict]:
"""Parse LLM response text into enrichment dict. Handles markdown fences."""
text = text.strip()
if text.startswith("```"):
text = text.split("\n", 1)[1].rsplit("```", 1)[0]
return json.loads(text)
def call_enrichment_api(prompt: str, config) -> Optional[str]:
"""Dispatch enrichment call to the configured provider. Returns raw text or None."""
provider = config.enrichment_provider
api_key = config.enrichment_api_key
model = config.enrichment_model
if not provider or not api_key or not model:
return None
if provider == "anthropic":
client = _get_client("anthropic", api_key)
response = client.messages.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return response.content[0].text
elif provider in ("openai", "openrouter"):
base_url = OPENROUTER_BASE_URL if provider == "openrouter" else None
client = _get_client(provider, api_key, base_url)
response = client.chat.completions.create(
model=model,
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
return None
# Enrichment prompt template
ENRICHMENT_PROMPT = """You are a code analyst. Given these code signatures from {filename}, provide a concise 1-line semantic summary for each symbol describing what it does (not what it is).

@@ -124,3 +191,3 @@

async def enrich_file(file_path: str, skeleton: str, config) -> Optional[dict]:
"""Call Haiku to generate enrichments for a file's skeleton.
"""Call enrichment API to generate enrichments for a file's skeleton.

@@ -139,14 +206,6 @@ Returns dict mapping symbol names to summaries, or None on failure.

try:
import anthropic
client = anthropic.Anthropic(api_key=config.anthropic_api_key)
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text.strip()
# Parse JSON from response (may be wrapped in ```json blocks)
if text.startswith("```"):
text = text.split("\n", 1)[1].rsplit("```", 1)[0]
return json.loads(text)
text = call_enrichment_api(prompt, config)
if text is None:
return None
return _parse_enrichment_response(text)
except Exception:

@@ -200,13 +259,6 @@ return None

try:
import anthropic
client = anthropic.Anthropic(api_key=self._config.anthropic_api_key)
response = client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
text = response.content[0].text.strip()
if text.startswith("```"):
text = text.split("\n", 1)[1].rsplit("```", 1)[0]
enrichments = json.loads(text)
text = call_enrichment_api(prompt, self._config)
if text is None:
return True
enrichments = _parse_enrichment_response(text)
self._cache.put(file_path, mtime, enrichments)

@@ -213,0 +265,0 @@ except Exception:

@@ -15,3 +15,5 @@ watchdog>=4.0

anthropic>=0.40
# OpenAI/OpenRouter enrichment provider support
openai>=1.0
# .env file support
python-dotenv>=1.0

@@ -654,3 +654,3 @@ """RLM Navigator Daemon — file watcher + TCP server + skeleton cache.

from config import RLMConfig
cfg = RLMConfig()
cfg = RLMConfig(root=root)
resp = {

@@ -765,3 +765,3 @@ "status": "alive",

cfg = RLMConfig()
cfg = RLMConfig(root=root)
tree = index_document(abs_path, cfg)

@@ -787,3 +787,3 @@ if tree is None:

from config import RLMConfig
cfg = RLMConfig()
cfg = RLMConfig(root=root)
tree = index_document(abs_path, cfg)

@@ -790,0 +790,0 @@ if not tree:

{
"name": "rlm-navigator",
"version": "1.1.6",
"version": "1.2.0",
"description": "Token-efficient codebase navigation for AI-assisted coding",

@@ -5,0 +5,0 @@ "bin": {