npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@devs30/local-deep-researcher

v0.7.1

Published

Fully local web research assistant (LangGraph.js port of langchain-ai/local-deep-researcher) - library, CLI and MCP server

Readme

@devs30/local-deep-researcher

npm CI license

A JavaScript/TypeScript port of langchain-ai/local-deep-researcher: a fully local web research assistant. It runs against a local LLM via Ollama by default, so no API keys are required to get a cited research report. Use it as a library, a CLI, an MCP server, or a LangGraph Studio graph.

It ships two research modes, each available in every interface:

| Mode | Engine | Who decides the next step | CLI | Model requirement | | ---------------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------- | ------------------------------- | | Workflow (default) | LangGraph.js state machine | The graph: fixed generate -> search -> grade -> summarize loop | local-deep-researcher "<topic>" | Any local LLM | | Agentic | createAgent (LangChain v1) loop | The LLM: it picks its own searches, page fetches and notes | local-deep-researcher agent "<topic>" | Tool-calling LLM (e.g. qwen3) |

How the workflow mode works

The assistant runs a small research loop as a LangGraph state machine:

generate query → web search → grade sources → summarize → reflect on gaps
                     ^                                          |
                     └───────────────── repeat ─────────────────┘
                                                                  |
                                                          finalize summary
  1. Generate query - the LLM turns your topic into a targeted search query.
  2. Web search - the query is run against the configured search provider.
  3. Grade sources - results are filtered in two stages: deterministic credibility heuristics (domain blocklist, thin/empty content, cross-loop URL dedup - no LLM cost), then a per-source binary LLM relevance check ("when in doubt, keep"). Rejected sources never reach the summary or final bibliography; every drop is logged to stderr with a reason. If a whole round is rejected, the research loop tries a different query next iteration - these empty rounds (all sources rejected or a failed search) get free retries that don't consume the budget. The budget counts only productive rounds (at least one source kept), up to a hard cap of 2 * (maxWebResearchLoops + 1) total rounds. Use --count-empty-loops to restore the v0.2.x behavior where empty rounds consume budget. Note that MCP progress loop counts all attempts and can exceed total when free retries happen.
  4. Summarize - the new results are folded into a running summary. Empty rounds skip this step and go straight to reflection, which receives the list of failed queries with a do-not-repeat instruction; a run where every round is empty produces an honestly empty report instead of a summary hallucinated from empty context.
  5. Reflect - the LLM looks for knowledge gaps and produces a follow-up query.
  6. Steps 2-5 repeat until the configured loop count is reached (default 3 loops), then the summary is finalized into a markdown report with a deduplicated source list. (Like the Python original, --max-loops N performs N+1 productive rounds; empty rounds get free retries up to a hard cap of 2 * (N + 1) total rounds.)

Behavior change vs the Python original (ollama-deep-researcher): source grading is ON by default and adds up to one LLM call per gathered source. Disable it with --no-grade-sources (CLI), GRADE_SOURCES=false (env), or gradeSources: false (library/MCP) to restore upstream-identical behavior.

Quickstart (CLI)

ollama pull gemma4:e4b
npx @devs30/local-deep-researcher "history of liquid rocket engines"

This prints a markdown report to stdout, with progress logged to stderr. No search API key is needed - the default search provider is DuckDuckGo. For the autonomous variant, see Agentic mode.

CLI flags

| Flag | Description | | ----------------------- | ----------------------------------------------------------------------------- | | --max-loops <n> | Research loops (default 3; N yields N+1 productive rounds) | | --count-empty-loops | Empty rounds (no sources kept) also consume the loop budget (v0.2.x behavior) | | --provider <name> | ollama | openai_compatible (default ollama) | | --model <name> | Model name (default gemma4:e4b, env LOCAL_LLM) | | --base-url <url> | LLM base URL (Ollama or OpenAI-compatible endpoint) | | --search-api <name> | duckduckgo | tavily | perplexity | searxng (default duckduckgo) | | --fetch-full-page | Fetch full page content for each source | | --no-grade-sources | Disable source grading (credibility + relevance filter) | | --blocklist <domains> | Comma-separated domains to always reject | | -o, --output <file> | Write the report to a file instead of stdout | | --json | Output {"summary", "sources"} JSON instead of markdown | | -q, --quiet | Suppress progress output on stderr | | -h, --help | Show help | | -v, --version | Show version |

local-deep-researcher mcp starts the MCP stdio server instead of running a one-off research task - see Use as an MCP server below.

Use as a Claude Code subagent

Copy .claude/agents/deep-researcher.md into your project's .claude/agents/ directory. Claude Code will pick it up automatically and delegate research tasks to it - it shells out to the CLI (npx -y @devs30/local-deep-researcher "<topic>" --quiet) and returns the markdown report verbatim.

Use as an MCP server (Claude Code / Codex)

Register the server with Claude Code:

claude mcp add deep-researcher -- npx -y @devs30/local-deep-researcher mcp

For Codex, add the server to ~/.codex/config.toml:

[mcp_servers.deep-researcher]
command = "npx"
args = ["-y", "@devs30/local-deep-researcher", "mcp"]

The server exposes two tools: deep_research, which takes topic (required), and optional max_loops, search_api, grade_sources, source_domain_blocklist and count_empty_loops parameters, and streams progress notifications while it runs; and deep_research_agent, documented in the Agentic mode section below.

Library API

import { research } from "@devs30/local-deep-researcher";

const report = await research(
  "history of liquid rocket engines",
  { maxWebResearchLoops: 3 },
  {
    onProgress: (event) => {
      // event.phase: "generating_query" | "searching" | "grading" | "summarizing" | "reflecting" | "finalizing"
      console.error(`[${event.loop}/${event.maxLoops}] ${event.phase}`);
    },
  },
);

console.log(report.markdown); // full "## Summary" + "### Sources:" report
console.log(report.summary); // the running summary text
console.log(report.sources); // [{ title, url }, ...] deduplicated sources

research(topic, options?, hooks?, deps?) returns a ResearchReport:

interface ResearchReport {
  summary: string;
  sources: Array<{ title: string; url: string }>;
  markdown: string;
}

options is a Partial<Configuration> (see Configuration below) and takes precedence over environment variables. deps lets you override the LLM factory or search provider - mainly useful for testing.

The agentic counterpart researchAgentic(topic, options?, hooks?, deps?) has the same signature and returns the same ResearchReport; see Agentic mode for its example and the extra progress fields (step/maxSteps).

Agentic mode

Alongside the fixed research loop above, this repo also ships an agentic mode: a single LLM agent decides its own searches, page fetches and notes in a tool-calling loop, instead of following the generate-query -> search -> grade -> summarize -> reflect steps. Once the agent stops (or hits the step cap), a separate one-shot LLM call writes the report from the gathered notes; the sources section is built deterministically from the notes' source URLs.

The step budget (--max-steps, default 20 model calls) is a hard cap, not a target: the agent stops on its own once its notes cover the question. Two guarantees shape the loop: while the agent has zero findings and unspent budget, it is automatically re-engaged with instructions to try different queries (it cannot give up early), and when nothing relevant exists it records an honest negative finding ("no evidence found") instead of failing - the run errors out only when the whole budget is spent without a single note.

Agentic mode requires an Ollama model with tool calling (e.g. qwen3) - the default gemma4:e4b does not support tools. A preflight check fails fast with a hint to set --agent-model / AGENT_LLM if the configured model can't call tools. In agentic mode the model itself chooses which URLs to fetch; fetching localhost and private-network addresses is blocked.

ollama pull qwen3
npx @devs30/local-deep-researcher agent "history of liquid rocket engines" --agent-model qwen3 --max-steps 15

CLI flags

| Flag | Description | | ---------------------- | --------------------------------------------------------------------------- | | --max-steps <n> | Max model calls in the agent loop (default 20, env MAX_AGENT_STEPS) | | --agent-model <name> | Tool-calling model for the agent loop (default: --model, env AGENT_LLM) |

The MCP server exposes a second tool alongside deep_research: deep_research_agent, which takes topic (required), and optional max_steps, agent_llm, search_api and source_domain_blocklist parameters.

import { researchAgentic } from "@devs30/local-deep-researcher";

const report = await researchAgentic("history of liquid rocket engines", {
  agentLlm: "qwen3",
  maxAgentSteps: 15,
});

console.log(report.markdown); // same ResearchReport shape as research()

researchAgentic(topic, options?, hooks?, deps?) returns the same ResearchReport shape as research(). agentLlm falls back to localLlm when unset; maxAgentSteps caps the number of model calls in the loop (default 20) - when the cap is hit, the report is written from whatever notes were gathered so far.

Existing workflow behavior (research() / local-deep-researcher <topic>) is unchanged; agentic mode is purely additive.

LangGraph Studio

This repo ships a langgraph.json with two graphs, so you can open them directly in LangGraph Studio from the repo root:

  • local_deep_researcher - the fixed research loop (src/graph.ts:graph)
  • local_deep_researcher_agent - agentic mode (src/agent.ts:agenticGraph)
npx @langchain/langgraph-cli dev

Configuration

All settings can be set via environment variables (see .env.example), and programmatic callers can also pass them directly as the options argument to research(). Programmatic options take precedence over environment variables, which take precedence over the defaults below.

| Config key | Env var | Default | | ------------------------- | ---------------------------- | ----------------------------------------------------- | | llmProvider | LLM_PROVIDER | ollama | | localLlm | LOCAL_LLM | gemma4:e4b | | agentLlm | AGENT_LLM | (none, falls back to localLlm) | | ollamaBaseUrl | OLLAMA_BASE_URL | http://localhost:11434 | | openaiCompatibleBaseUrl | OPENAI_COMPATIBLE_BASE_URL | (none, required if llmProvider=openai_compatible) | | openaiCompatibleApiKey | OPENAI_COMPATIBLE_API_KEY | (none) | | searchApi | SEARCH_API | duckduckgo | | maxWebResearchLoops | MAX_WEB_RESEARCH_LOOPS | 3 | | maxAgentSteps | MAX_AGENT_STEPS | 20 | | fetchFullPage | FETCH_FULL_PAGE | false | | gradeSources | GRADE_SOURCES | true | | sourceDomainBlocklist | SOURCE_DOMAIN_BLOCKLIST | (empty) | | countEmptyLoops | COUNT_EMPTY_LOOPS | false | | stripThinkingTokens | STRIP_THINKING_TOKENS | true | | tavilyApiKey | TAVILY_API_KEY | (none, required if searchApi=tavily) | | perplexityApiKey | PERPLEXITY_API_KEY | (none, required if searchApi=perplexity) | | searxngUrl | SEARXNG_URL | (none, required if searchApi=searxng) | | langsmithTracing | LANGSMITH_TRACING | false | | langsmithApiKey | LANGSMITH_API_KEY | (none, required if langsmithTracing=true) | | langsmithProject | LANGSMITH_PROJECT | local-deep-researcher (when tracing is enabled) | | langsmithEndpoint | LANGSMITH_ENDPOINT | (none, langsmith default endpoint) |

A .env file applies wherever the process is started from: the CLI and the MCP server load .env from the current working directory on startup (also when run via npx), and LangGraph Studio loads it as well (langgraph.json points at .env). The library API loads no file - programmatic callers set real environment variables or pass settings via the options argument.

.env.example is a ready-to-copy template for all of the variables above. It ships in the git repository only (not in the npm package): working from a clone, copy it to .env and adjust. With npx, create a .env in the directory you run the command from, or export the variables in your shell.

LangSmith tracing

Both research modes (workflow and agentic) can send full traces - every graph node, LLM call and tool call - to LangSmith. Tracing is built into LangChain/LangGraph; enabling it requires no code changes:

# .env
LANGSMITH_TRACING=true
LANGSMITH_API_KEY=lsv2_...
# LANGSMITH_PROJECT=local-deep-researcher   # optional, this is the default
# LANGSMITH_ENDPOINT=https://eu.api.smith.langchain.com   # optional, self-hosted / EU region

With npx, either export the variables in your shell (LANGSMITH_TRACING=true LANGSMITH_API_KEY=lsv2_... npx @devs30/local-deep-researcher "topic") or put them in a .env file in the directory you run the command from.

For the MCP server, pass them through the MCP client's env block, e.g. in .mcp.json:

{
  "mcpServers": {
    "local-deep-researcher": {
      "command": "npx",
      "args": ["-y", "@devs30/local-deep-researcher", "mcp"],
      "env": {
        "LANGSMITH_TRACING": "true",
        "LANGSMITH_API_KEY": "lsv2_..."
      }
    }
  }
}

The same settings also work programmatically via the options argument (research(topic, { langsmithTracing: true, langsmithApiKey: "..." })). When tracing is enabled without an API key, the run fails fast with a configuration error. When it is disabled (the default), no trace data leaves your machine.

Note that tracing is process-wide: an environment-level LANGSMITH_TRACING=true cannot be turned off per-call with langsmithTracing: false, and once a traced run has started in a process, the API key and endpoint are fixed for that process (only the project name can change between runs).

Known upstream issue (langchainjs#11189): with tracing enabled, agentic mode makes @langchain/core log harmless Error in handler LangChainTracer, ... No chain run to end lines (a duplicated-tracer bug with nested graphs). Traces in LangSmith are complete despite the warnings; the CLI and MCP server filter these lines out. EU-region accounts must set LANGSMITH_ENDPOINT=https://eu.api.smith.langchain.com, otherwise uploads fail with 403.

Search providers

  • DuckDuckGo (default) - no API key required.
  • Tavily - set SEARCH_API=tavily and TAVILY_API_KEY.
  • Perplexity - set SEARCH_API=perplexity and PERPLEXITY_API_KEY.
  • SearXNG - set SEARCH_API=searxng and SEARXNG_URL to your instance URL.

License

MIT. This is a port of langchain-ai/local-deep-researcher; credit to the original authors for the research loop design and prompts.