Skip to content

About

Lightweight code indexing and exploration powered by Lua, with multi-language symbol extraction, call graphs, and incremental updates.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

CodeOutline

English | 简体中文

CodeOutline is a code exploration tool and LSP server, powered by Lua.

  • Code exploration (code_explore) for coding agents: the MCP tool codeoutline_explore returns symbols, line-numbered source, call paths and callers/callees. Use codeoutline explore for the same exploration from a terminal.
  • LSP server for editors: codeoutline lsp --stdio provides symbols, definitions, references, hover, call hierarchy, completion and signature help. Install the bundled clients for VS Code, Cursor or Zed to use it in an editor.

CodeOutline answers symbol queries with line-numbered source, call paths, and callers/callees, refreshes incrementally, and caches the index on disk. Editors get symbols, definitions, references, call hierarchy, and completion from the same index through LSP. The MCP protocol with both transports (stdio and Streamable HTTP) and the language server are implemented in Lua, so neither depends on Node; in the npm package, Node only locates and starts the runtime for your platform.

Installation and setup

Requires Node.js 20 or newer. npm downloads the runtime for your platform automatically; no compiler or Lua installation is needed. Install it globally rather than configuring clients with npx -y: npx keeps a full npm process running beside every server it starts.

npm install -g codeoutline
codeoutline --version
codeoutline --help

To fully uninstall a global npm installation, run codeoutline remove. It stops the shared service, removes integrations and default user data, and then uninstalls the npm package. npm hides the install script's output by default. Use --foreground-scripts to see the uninstall reminder during setup.

To choose each detected AI tool and Zed, VS Code or Cursor editor during global installation, use npm's foreground mode. Each prompt defaults to installation. The shared service then starts automatically, without another install command:

npm install -g codeoutline --foreground-scripts

Installation offers two stages in order, confirming each detected application:

  1. Detect AI tools and ask whether to install code exploration (codeoutline_explore over MCP) for each tool, such as Install code_explore (MCP) for Codex (codex)? [Y/n].
  2. Detect Zed, VS Code and Cursor and ask whether to install their LSP clients. Each editor gets its own confirmation, such as Install CodeOutline LSP for Zed (zed)? [Y/n].

Every installation prompt defaults to yes: press Enter (or enter y/yes) to install it, or n/no to skip just that application. Press Enter for every item to install all detected integrations. EOF skips remaining applications instead of accepting their defaults. Explicit --target/--editors lists and environment selections bypass the per-application prompts. Interactive removal defaults to no. The installer then starts/registers the shared service. After restarting the selected applications, no additional codeoutline install command is required. npm runs lifecycle scripts in the background by default. The launcher only prompts when it inherits terminal input and output; it does not reopen the controlling console while npm owns it. Without --foreground-scripts, in CI or when no terminal is available, it uses noninteractive setup and installs detected integrations automatically unless explicitly overridden. Ordinary project-local npm install codeoutline does not configure global integrations; use the global installation command above for this setup flow. You can also choose or install clients later:

codeoutline editors                             # detect and choose interactively
codeoutline editors --list                      # only show detected editors
codeoutline editors --editors all               # install for all detected editors
codeoutline editors --editors vscode            # install for VS Code only
codeoutline editors --editors cursor            # install for Cursor only
codeoutline editors --editors zed               # install for Zed only
codeoutline editors --editors vscode,cursor      # install for selected editors

Set CODEOUTLINE_EDITORS to the same selection for unattended global npm installs; none skips clients. Without a terminal, installation never waits for a choice. Local npm installs do not change editor installations. Clients are bundled in release packages; no Rust toolchain or marketplace download is needed. Reload VS Code/Cursor or restart Zed afterward. Detection uses PATH and common installation locations; for a custom location, add the editor CLI to PATH. Zed uses its default user-data directory. Existing editor settings are preserved. The VS Code/Cursor client starts with full navigation enabled; set codeoutline.mode to auxiliary for workspace symbols only. See LSP.md for server capabilities.

Recommended: one shared HTTP service

A stdio server is a child process of its client, so every agent session (each Claude Code session, each Cursor window) starts its own copy and builds its own in-memory index. One HTTP service serves all sessions from a single process and a single index.

A global npm installation starts that service in the background and registers it to start at login: a Run registry value on Windows, a LaunchAgent on macOS, and a systemd user unit on Linux; without a systemd user manager (WSL, containers), start codeoutline daemon at login yourself. The service installs updates and restarts itself into them. Register agents once:

codeoutline install --yes                         # detect agents, configure MCP, start service
codeoutline install --target cursor,claude,codex   # select agents explicitly
codeoutline agents                               # choose interactively, without restarting service
codeoutline agents --list                         # inspect detection and config paths
codeoutline agents --target all                   # configure all detected agents
codeoutline agents --print codex                  # preview one agent's config, no writes
codeoutline agents --target cursor --remove       # remove only CodeOutline's MCP entry

Supported target IDs: claude, cursor, codex, gemini, opencode, hermes, kiro, antigravity, copilot-vscode, copilot-cli, copilot-jetbrains. Detection uses existing profile directories/configs and CLI commands on PATH; explicit IDs work even when detection misses an application. all and auto select detected targets. none skips registration. install --yes does not install LSP clients unless --editors is also specified.

Configuration defaults to user scope. Use --location local --project PATH for project scope; Hermes, Antigravity and Copilot CLI/JetBrains support user scope only. --port selects the shared service port for both registration and startup. agents edits configs only; use install to start/register the service too. Restart the agents after setup so they discover codeoutline_explore and the other MCP tools. Agent approval controls still apply; no prompt hooks or auto-approval policies are changed.

For Claude, Codex, Gemini and OpenCode, setup also adds a short CODEOUTLINE_START/END block to their scoped CLAUDE.md, AGENTS.md or GEMINI.md. If a complete CODEGRAPH_START/END block exists there, it is saved verbatim as codegraph.md in the same directory and removed from active instructions. An unrelated/different existing backup or malformed markers stop setup before config writes. Repeated setup is idempotent; unrelated instructions are preserved. Uninstall removes our block and restores the saved CodeGraph block only to a file that originally supplied it, when the codegraph CLI is still on PATH and no CodeGraph block is already present. The backup remains available for manual recovery, including when CodeGraph is gone. Project instructions are changed only with explicit project-scoped setup/cleanup. codegraph.md is not a default instruction filename, but explicit imports or manual reads can still load it.

The installer preserves unrelated settings and JSONC comments and stores the original nonempty file in <config>.codeoutline.bak before its first change. Repeating the same installation leaves configs unchanged. A different existing codeoutline entry is kept unless --force is passed. Complex TOML/YAML layouts that cannot be edited safely require manual setup using agents --print ID. For unattended global npm installation, set CODEOUTLINE_AGENTS=auto, all, none, or comma-separated IDs. Without a terminal or an explicit selection, global npm setup selects all detected agents and editors. Set both CODEOUTLINE_AGENTS=none and CODEOUTLINE_EDITORS=none to skip integration installation, or --ignore-scripts to skip the entire npm setup lifecycle. Local npm installs skip registration.

For manual setup instead:

Claude Code (added globally, available in every project):

claude mcp add --transport http codeoutline --scope user http://127.0.0.1:19876/mcp

Cursor (~/.cursor/mcp.json):

{ "mcpServers": { "codeoutline": { "url": "http://127.0.0.1:19876/mcp" } } }

codeoutline install does the same for a native installation, or after installing with --ignore-scripts; --port selects a port other than 19876. Set CODEOUTLINE_AUTOSTART=0 when installing to skip the login entry. The service logs to ~/.codeoutline/logs.

For a global npm installation, run codeoutline remove to clean integrations, purge the default user data and then uninstall the npm package. It waits for the runtime to exit before invoking npm; cleanup failure preserves the package for retry. Use codeoutline remove --project PATH to also remove a project's MCP registrations.

For separate cleanup and package removal, first run codeoutline uninstall, then npm uninstall -g codeoutline. The first command stops the service, removes its login entry, user-level Agent MCP entries and the VS Code/Cursor/Zed clients. Use --purge to also remove ~/.codeoutline caches, updates and logs, or --service-only to keep integrations. Cleanup also removes codeoutline.* settings from VS Code/Cursor user settings and named profiles, including language overrides, preserving other settings and comments. Use --project PATH to also remove MCP registrations in a specific project. Close/reload editors to release running LSP processes before deleting package files. Workspace settings, custom editor data directories, remote installations and original configuration backups require separate cleanup. npm 7+ has no uninstall hooks; running only npm uninstall cannot clean integrations or stop the service. The package includes a best-effort preuninstall compatibility hook for package managers that invoke it. It only cleans global uninstall integrations, preserves user data, and skips upgrade/local-install lifecycles. Use codeoutline remove for reliable full cleanup on current npm.

The service listens on 127.0.0.1:19876 and serves projects anywhere on the machine, so switching between projects on different drives needs no configuration. Only local processes can reach it, and they already have your file access. A shared service has no project of its own: it uses the client's roots when the client provides them, and otherwise the agent passes projectPath, which the tools ask for when it is missing.

To run an HTTP service yourself instead, for example restricted to some directories, start codeoutline serve --http and repeat --allow-root for each directory tree to allow; a tree that is unavailable at startup, such as an unmounted drive, is reported and becomes usable once it appears. Such a service installs updates but keeps running the old version until restarted; on Windows, supervise the native launcher rather than the npm one; see DISTRIBUTION.md.

Alternative: stdio, one process per session

Use stdio when you do not want a resident service. Each session then starts and stops its own server.

claude mcp add codeoutline --scope user -- codeoutline serve --stdio

On Windows, Claude Code cannot start the codeoutline.cmd shim directly; use -- cmd /c codeoutline serve --stdio. For Cursor, set "command": "codeoutline", "args": ["serve", "--stdio"]. A client that starts codeoutline with no arguments, as 0.1.7 recommended, gets the same stdio server.

When no project is specified, the service uses, in order: the projectPath argument of the call, the --project option, the client's roots (the first directory returned by roots/list), and the directory the service was started in. Clients usually start a stdio service in the project directory, so a global configuration needs no project path. Access defaults to --project or the start directory; repeat --allow-root to allow more.

Seven tools are provided:

Tool Purpose
codeoutline_explore Query source, call paths, and callers/callees by symbol or file name (query; optional projectPath, budget)
codeoutline_callers Find recursive callers and affected files for an exact symbol/qualified name (query; optional projectPath, depth default 3, max_nodes default 200, budget)
codeoutline_affected Analyze changed files with shared reverse traversal, shortest paths and test candidates (files; optional projectPath, depth, max_nodes, budget)
codeoutline_type_hierarchy Find declared ancestors and descendants of a C#/Java/Kotlin/TS/C++ type (query; optional projectPath, direction, depth default 8, max_nodes default 200, budget)
codeoutline_type_references Find explicit type uses with identity resolution in C/C++, C#, Java, Kotlin, TS/ArkTS, Go and Rust (query; optional projectPath, max_nodes default 200, budget)
codeoutline_status Refresh and report index, cache, scanner, and parser statistics
codeoutline_rebuild Rebuild fully, bypassing the cache

Without Node, download the native archive for your platform from GitHub Releases, extract it, and run codeoutline (codeoutline.cmd on Windows).

Editor integration (LSP)

codeoutline lsp --stdio is a language server over the same indexer. Each editor window starts its own server process with its own index; it does not connect to the shared MCP service, and the MCP URL is not an LSP endpoint.

Feature Scope
Document and workspace symbols Workspace symbols match substrings, case-insensitively
Definition, hover, signature help Definition also goes from import lines (require, #include, module imports) to the files they load
References Functions, methods, constructors, macros and supported explicit type positions
Call hierarchy Functions, methods, constructors and macros; incoming and outgoing calls
Method implementations C#, Java, Kotlin and TypeScript interface/abstract methods and C++ virtual overrides; textDocument/implementation
Completion Locals, the file's definitions and imports, declarations from included C/C++ headers, and members after ., ->, ::, ?., or Lua :

Resolution is name-based, the same as codeoutline_explore; it is not a type checker. Rename, diagnostics, and formatting are not provided, so CodeOutline works best beside a language's own server or where none is available. Unsaved edits are included in every request. A .codeoutline.json file in the workspace root can map Lua call arguments to registration sites for Go to Definition. Behavior, limits, and options are described in LSP.md.

Thin clients are in this repository: one VSIX for VS Code and Cursor, and a Zed extension. They are not published to an extension marketplace yet; install them as described in their READMEs. Other editors can start the same command through their generic LSP support; only VS Code, Cursor, and Zed have been tested.

Clients start the server directly, without a shell:

  • Linux and macOS: the npm installation puts an executable codeoutline on PATH, so the client defaults (codeoutline with lsp --stdio) work. An editor started from the desktop may not inherit PATH entries added by your shell profile (nvm, for example); then configure the absolute path printed by command -v codeoutline.
  • Windows: npm installs codeoutline.cmd, a batch file. The VS Code/Cursor client finds the npm installation itself, and Zed starts codeoutline.cmd from PATH through cmd.exe, so neither needs settings. For clients that cannot start a batch file, configure node as the command, with the arguments <global npm folder>/codeoutline/launcher/codeoutline.cjs, lsp, --stdio; npm root -g prints the global folder, usually C:\Users\<you>\AppData\Roaming\npm\node_modules. For the native archive, run its bin/xnet.exe with <archive>/scripts/codeoutline/command.lua, LOG_STDERR=1, LOG_FILE=0, lsp, --stdio.

The server uses the newest installed version when it starts and does not install updates itself; an update installed by the background service takes effect the next time the editor starts the server. Started directly from the native archive, the server always runs that archive's version.

System requirements

Platform Requirements
Windows x64, Windows 10 / Server 2016 or newer (Windows 8 for the native archive); Windows 11 on ARM64 runs it under x64 emulation. No Visual C++ redistributable needed
Linux x64, glibc 2.28 or newer: RHEL / Rocky / Alma 8 and 9, CentOS Stream 9, Debian 10+, Ubuntu 20.04+. Alpine/musl is not supported
macOS macOS 11 or newer; one universal binary for Apple Silicon and Intel

On Linux, decoding GBK sources needs the GBK module of the system iconv. Minimal EL8/EL9 installs and container images lack it: dnf install glibc-gconv-extra. UTF-8 projects work without it, and codeoutline doctor reports when it is missing.

Supported languages

C, C++, C#, Go, Java, JavaScript / TypeScript (including JSX/TSX), ArkTS (.ets), Kotlin (.kt / .kts), Lua, Python, and Rust.

Receiver lookup shares one query-time entry point across call relationships, navigation and completion, with language-specific binding and member rules. It distinguishes Lua method receivers, Python parameters and rebinding, JS/TS/ArkTS lexical arrow this and dynamic function this, Kotlin extension/scope receivers, Rust self/Self, and declared receivers in C/C++, C#, Java and Go. Ordinary variables named self or cls retain their declared types. Detailed bindings stay in bounded, source-validated memory caches; unknown receivers do not become same-name method guesses. Runtime monkey-patching, arbitrary dynamic calls and unproven generic constraints remain outside this static analysis.

ArkTS reuses the JS/TS parser: component struct declarations are indexed as classes, with decorated fields, methods, local module imports and ArkUI calls. Declarative UI blocks remain calls rather than method declarations. This is heuristic indexing, not ArkTS type checking; SDK @kit.*/@ohos.* resolution, resource references and generated UI lifecycle/state behavior are not modeled. Zed requires a separate ArkTS language extension.

Kotlin includes declarations, constructor properties, extension functions, expression bodies, package imports (including aliases), and ordinary, generic and trailing-lambda calls. Resolution is heuristic: compiler-generated members, implicit lambda receivers, string-template expressions, Java interop and coroutine/framework dispatch are not modeled.

C/C++ bare calls check project macro visibility on demand: define/undef, indexed include order and finite conditions distinguish visible macros from ordinary functions. Unknown expansions remain unresolved; this does not run a preprocessor.

Unknown filename suffixes are handled on demand. LSP uses a supported editor language ID first, then bounded, conservative syntax detection. Explicit project-relative file paths in explore queries receive the same temporary parsing; lang: can supply a language hint. Detection ignores comments, strings, binary data and ambiguous syntax. Each explore query admits at most four such files of 1 MiB each and 2 MiB in total. No suffix association is learned. Except for the C/C++ include-driven admission below, these files are not added to the persistent index. Supply a full relative path for files outside the root directory; file discovery by basename uses the regular index.

C/C++ literal #include directives can admit project-local unknown-suffix source fragments such as .inl or .inc. The normal file listing supplies candidates and honors ignore rules; relative paths and project include-path configuration take precedence. Without configured search paths, only a unique suffix match is admitted. Fragments inherit a C/C++ parser hint, with C++ syntax detection, and are parsed once per language hint into compact cached records. Unreferenced unknown files, ignored files, external SDKs, binary data and clearly foreign-language sources are not admitted. Changes rediscover reachable fragments and remove orphaned include cycles; cache reloads preserve parser hints. Limits are 4,096 fragments, 64 MiB combined source and 32 discovery rounds, plus the normal per-file size limit; refresh statistics expose fragment_limited when a discovery limit is reached. Detailed bindings and macro environments remain query-time data. Inclusion alone does not restore a surrounding class, namespace or the compiler configuration of every translation unit.

Explore can suggest exact-name definitions for unresolved calls, including possible macro declarations. The separate Unconfirmed definition candidates section gives locations, matching reasons and bounded source snippets. These suggestions never add call edges, seeds, callers or impact paths; inspect them before inferring a relationship. Known import, local, receiver, qualification and include-policy barriers still apply. Lookup inspects at most four encountered sites and 256 same-name entries per ordinary site, with eight results per site and snippets from four source files totaling 2 MiB. Macro suggestions use a separate bounded pass with the existing macro limits. Output limits are reported; suggestions and their evidence are query-owned and are not stored in the index.

C++ lookup covers named type aliases, namespace aliases, using ns::Type, using ns::function, and bounded using namespace chains across included headers. Calls, navigation and completion share this lookup; local imports respect their declaration position and block scope, and ambiguous imported names remain unresolved. Import evidence is generated on demand rather than stored in the index. This is heuristic lookup: preprocessing/include order, compiler include-search configuration, dependent templates, ADL and full overload resolution are not modeled.

JS/TS package imports resolve local npm/Yarn workspaces (workspaces arrays or packages lists) and pnpm packages lists. Package discovery reuses indexed directories; manifests and resolution maps exist only during a query. The nearest workspace owns an import, pnpm configuration takes precedence, and relative imports and existing tsconfig paths retain their priority. Supported globs are *, **, ? and ! exclusions; pnpm accepts block string lists or JSON flow lists. Lookup covers scoped names, subpaths, exact/wildcard exports, ordered types/import/require/ node/default conditions, and legacy types/typings/module/main entries. An unavailable declared export stays unresolved rather than guessing a source directory. Duplicate package names also stay unresolved. Configuration changes apply on the next query without reindexing sources. Discovery is bounded to 4,096 directories, 512 packages, 1,024 configuration probes and 8 MiB of configuration per query (1 MiB per file); exceeded limits report incomplete results. This is source navigation, not full Node/TypeScript module resolution: dependency installation, lockfiles, package imports, custom conditions, arbitrary YAML, and CommonJS export analysis are not provided.

Go method queries share bounded, query-time method sets across agent exploration, callers, LSP definition/implementation lookup and completion. Ordinary interfaces match only when every required method has an identical resolved parameter/result signature, including variadic form. Value and pointer receivers, non-generic embeddings, shallower declarations, field shadowing and ambiguous promotions are handled separately. Implementation edges are possible structural matches, not proof of a runtime receiver. Empty interfaces do not expand to every type. Generic bodies, type-set constraints and unresolved signatures remain unknown. Detailed evidence is not stored in the index; see Go method queries for limits.

Go imports use the nearest in-project go.mod and go.work: declared module paths, workspace use lists, and relative local replace targets locate indexed packages. Workspace replacements override module replacements; version-specific replacements require an agreed direct requirement across workspace modules. Module boundaries, duplicate names and conflicting replacements prevent speculative matches. Package calls, navigation and completion share explicit aliases and declared package names (including imports ending in /v2). Imported packages exclude test files and non-Go files; unqualified function lookup stays within the current package. With no module metadata, directory-suffix lookup remains available only when it finds a unique package. Configuration is query-owned and reloaded on the next request. Limits are 1,024 config probes, 1 MiB per config, 8 MiB total, 256 workspace modules, 512 files, 8,192 scanned nodes and 64 matching declarations per package lookup (4,096 legacy directory candidates); exceeded limits report incomplete results. Lookup stays inside the indexed project and does not invoke Go or download dependencies. The full module version graph, host GOWORK/GOPATH, vendor/module caches, build tags, dot imports, lexical shadowing and full receiver/type inference are not modeled. Configuration accepts standard line/block directives and JSON-compatible quoted paths; absolute local paths and other Go string escape forms are outside this supported subset.

Rust Cargo lookup reads local path dependencies and inherited workspace.dependencies on demand, including renamed packages, hyphenated crate names and custom [lib] names/paths. Workspace dependency paths are relative to their defining workspace manifest; sibling packages are not inferred as dependencies. crate/self/super, declared file/inline modules, grouped use aliases and bounded named public re-exports share resolution across calls, navigation and module completion. Detailed import, path and visibility evidence reuses the bounded query source cache and is excluded from serialized indexes. External lookups require public items. Manifest edits apply on the next request. Limits include 1,024 manifest probes, 1 MiB per manifest, 8 MiB total, 512 dependencies per package, 8,192 nodes per module and 64 alias-source files per caller query; exceeded limits report incomplete results. Supported TOML includes basic/literal strings, arrays, inline dependency tables, dotted dependency properties and dependency subtables. Lookup stays within the indexed root and never invokes Cargo. Registry/ Git downloads, dev/build/target-specific dependencies, lockfile/feature/target selection, patch/replace overrides, glob re-exports, #[path]/generated modules, custom binary targets, block-local imports and full Rust type, privacy or macro analysis are not modeled. Without Cargo metadata, legacy Rust lookup remains.

Function values in supported JS/TS/ArkTS, Python and Java/Kotlin forms retain ref dependencies, including explicit imports and typed method references. A reference is not proof of callback execution; anonymous scopes and complex expressions have conservative limits.

C#, Java and Kotlin implementation relationships use declared inheritance and matching named parameter types. Explore and callers label these edges implementation: they describe possible implementations, not calls made by the method or a proven runtime receiver. Go to Implementation uses the same lookup; Go to Definition continues to return the declaration. Detailed method evidence is generated per query and excluded from the index. Candidate, result and hierarchy limits report incomplete results. Generic substitution, inherited-only interface implementations, and default-interface conflict resolution are not modeled.

Lua also follows local callback arrays populated by named local functions and table.insert, then directly invoked through the value variable of a builtin pairs/ipairs loop. These query-owned automatic:callback-loop edges reuse static registry bindings and preserve collection identity. See Indirect calls for mutation, scope and size limits.

JS/TS also infer limited Node EventEmitter deliveries from literal registrations and emissions on the same local object. These query-owned automatic:event edges feed explore/callers without configuration; see Indirect calls for import, scope, mutation and candidate limits.

TypeScript interface and explicitly abstract methods use the same bounded query path. Only explicit implements/extends relationships between ordinary top-level named types are linked; named/default/namespace imports, re-exports, workspace packages and tsconfig paths reuse module resolution. Parameter and return annotations must match exactly after resolving primitive or named types (including simple arrays); named type identity includes its declaring module. Both signatures must have explicit annotations. Generic heritage/methods, aliases, unions, optional/rest/default parameters, accessors, arrow fields, declaration merging, nested types, inferred returns and structural implementations are excluded. Class overload signatures are not treated as abstract contracts. These links do not perform TypeScript assignability checks.

C++ uses the same query path for non-template, single, non-virtual inheritance. It matches ordinary named/builtin parameter and return types, pointer/reference and cv decoration, and member const/volatile/reference qualifiers. Overrides can omit virtual/override; private virtual methods participate, while final blocks further overrides. Namespace imports, aliases and indirect includes use the existing bounded C++ lookup. Matching out-of-line definitions are preferred over class declarations; an unmatched body cannot stand in for an edited declaration. Pure virtual overrides remain contracts. Multiple/virtual inheritance, templates, covariant returns, function-pointer/array parameters and decorated type aliases in signatures are excluded. These links describe possible implementations, without proving virtual dispatch at a call site. Each query retains at most 128 alias-file arrays and 1 MiB of alias evidence from the shared source loader. Reaching either limit reports incomplete relationships and suppresses uncertain C++ links; alias data is never persisted.

C/C++ include lookup can use ordered project search paths in .codeoutline.json:

{
  "includePaths": ["shared/include"],
  "includePathRules": [
    { "directory": "apps/client", "paths": ["apps/client/include"] },
    { "directory": "apps/server", "paths": ["apps/server/include"] }
  ]
}

Quoted includes first search the including file's directory. Other searches use the longest matching directory rule, then project includePaths, then LSP initializationOptions.includePaths, in array order, stopping at the first existing header. Rule directories match whole path components; apps/client does not match apps/client-tools. Paths are relative to the workspace root or absolute; rule directories must stay inside the workspace (. matches the root). Each including file selects its own rule. Configuration is reloaded per query and shared by explore, callers, definition, implementation navigation and completion. Existing same-directory source files take priority when pairing a header with its implementation; C++ class-outside members must belong to the selected class, rather than another same-named class. With configured paths, a paired source must directly include the selected header; unsupported build/forced-include relationships remain declarations instead of guessed definitions.

An explicit includePaths (including []), a matching directory rule, or nonempty LSP search paths disables whole-project suffix guesses for that file. Without configuration, legacy suffix lookup remains. External or excluded headers can be opened from include lines but do not contribute indexed symbols or fall through to indexed duplicates. There are at most 128 rules and 128 effective directories per file; paths are literal, without globs. These are per-file lookup rules, not compiler build configurations, forced includes or full preprocessor/translation-unit analysis. Only query-owned configuration and bounded path caches are added; no include maps are persisted.

Command line

Explore, callers, type-hierarchy and type-references accept explicit query filters such as lang:lua path:service/ connect. They select starting symbols; dependency expansion still crosses files and languages, including Lua-to-C calls.

Project search ranking lets .codeoutline.json deprioritize directory prefixes when explore chooses starting symbols. Files remain indexed, and dependency expansion keeps the same visibility.

codeoutline serve --stdio [--project PATH] [--allow-root PATH ...]
codeoutline serve --http [--host HOST] [--port PORT] [--project PATH] [--allow-root PATH ...]
codeoutline lsp --stdio [--project PATH]
codeoutline explore --project PATH --query QUERY [--budget BYTES]
codeoutline status [--project PATH]
codeoutline rebuild [--project PATH]
codeoutline doctor [--project PATH]

status, rebuild, and doctor print JSON; a failed diagnostic exits nonzero. One-shot commands default to the current directory as the project. lsp --stdio is started by an editor and speaks LSP on stdin and stdout; it is not an interactive command. Its workspace comes from the editor, and --project is used only when the editor sends none.

The runtime logs only warnings and errors, to stderr, and writes no log files, so starting it in a project leaves nothing behind. For troubleshooting, set CODEOUTLINE_LOG_LEVEL (DEBUG, INFO, WARN, ERROR, ...) and CODEOUTLINE_LOG_DIR to write log files to that directory.

MCP service details

The protocol baseline is MCP 2025-06-18. Query budgets range from 256 to 262144 bytes (default 16000); truncated output stays valid UTF-8 and ends with a truncation notice. Paths always refer to the machine running the service.

In stdio mode, stdout carries only MCP messages and logs go to stderr. HTTP listens on the local machine by default; clients connect to http://127.0.0.1:19876/mcp. Remote listening requires a CODEOUTLINE_TOKEN of at least 16 bytes and hosts allowed explicitly with --allow-host; clients send Authorization: Bearer .... Unlike local HTTP, a remote service without --allow-root is limited to --project or its start directory. Repeat --allow-origin to allow full browser origins; unlisted origins are rejected. Protect the token with a TLS reverse proxy for remote deployments.

The main thread handles the protocol while a resident index thread serves queries one at a time. Requests with a progress token receive progress every second (over SSE for HTTP); there is no standalone GET/SSE endpoint (it returns 405). Limits: 64 sessions, 64 queued or running jobs, a 120-second job deadline, and sessions expire after 15 idle minutes. Cancellation takes effect at explicit checkpoints, so a single file's native parse cannot be interrupted (files are capped at 1.5 MB). Cancelled updates roll back to the last complete in-memory index; cancelling a read-only query does not discard it.

Indexing behavior and limits

The service starts parse workers on demand for full scans with at least 32 files that need reading, while the index thread commits their results. It uses 8 parse threads on Windows and 6 elsewhere; set CODEOUTLINE_INDEX_THREADS to override the count, or 0 to disable them. Workers are reused and shut down after 60 seconds idle, once all outstanding batches have returned, then recreated when needed.

The cache lives at .codeoutline/cache/<SHA-256 of path>.idx in the user's home directory. It is integrity-checked and rebuilt automatically if corrupt; every write goes to a separate temporary file that atomically replaces the old one, which survives a failed write. Up to 8 projects stay resident by default, and idle ones are evicted after 15 minutes. Directory aliases and links resolve to real absolute paths; on Windows, ASCII case is ignored.

For isolated cache or memory experiments, set CODEOUTLINE_MEMORY_CACHE to a cache file path. A nonempty value overrides the embedding API's cache_path and the default path; empty or unset values preserve normal selection. This is one file shared by all projects in that process, so use it only for a single-project run, not the shared multi-project service. Source validation and atomic writes still apply.

Source encodings are confirmed on demand: the first index pass does not check each file, only the files a query touches. UTF-8 (with or without BOM) is tried first, then strict GBK conversion; a GBK file is reparsed and its symbols updated before the query runs, without modifying the file. Encoding results are cached and invalidated when a file changes, and a matched file that cannot be decoded is reported as an error. When both encodings are valid, UTF-8 wins. GBK files that no query has touched yet may still be misparsed, so Chinese identifiers can be missed; querying the file name triggers its repair.

Files are listed with ripgrep (rg) when available, honoring every .gitignore. Without rg, a directory walk supports only the common rules of the root .gitignore (no negation, no nested ignore files) and skips hidden directories, node_modules, __pycache__, and symbolic links. Project root paths must not contain ", %, !, $, backticks, backslashes, or line breaks.

Building from source

The runtime comes from the xnet2lua submodule:

git submodule update --init --recursive

The build scripts compile the LuaJIT runtime (with xproc, MPSCQ, and HTTPS) and copy it to bin/ at the repository root. On Windows, run from an MSVC developer terminal; the result links the C runtime statically. On macOS, the result is a universal binary for macOS 11 or newer.

.\tools\build-runtime.ps1
sh tools/build-runtime.sh

When running from source, pass LOG_STDERR=1 explicitly; the installed codeoutline adds it for you:

.\bin\xnet.exe scripts/codeoutline/command.lua LOG_STDERR=1 serve --stdio --project C:/src/project

Tests:

.\bin\xnet.exe tests/lua/codeoutline_spec.lua
.\bin\xnet.exe tests/lua/stability_spec.lua
.\bin\xnet.exe tests/lua/parse_pool_spec.lua
.\bin\xnet.exe tests/lua/lsp_spec.lua
npm ci
npm test
npm run test:package

npm test checks both transports with the official MCP SDK; npm run test:package builds real packages and verifies an installation outside the checkout. Set XSCAN_PURE_LUA=1 to exercise the pure-Lua scanner. Packaging and publishing are described in DISTRIBUTION.md.

Embedding

package.path = 'scripts/?.lua;' .. package.path
local service = require('codeoutline.service')
local text, info = service.explore('C:/src/project', 'Session.save', { budget = 16000 })

get, explore, and status refresh automatically; forget clears only the in-memory index, and rebuild reindexes without the disk cache. service.configure adjusts the resident project count and idle timeout. When embedding on Android, the host must call xutils_android_init(JavaVM*) once before starting worker threads; otherwise GBK conversion reports an error.

Origin and license

The code was extracted from Codua and uses the codeoutline.* namespace. CodeOutline is licensed under BSD-2-Clause; third-party notices are in THIRD_PARTY.md.

About

Lightweight code indexing and exploration powered by Lua, with multi-language symbol extraction, call graphs, and incremental updates.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages