Commit Graph
716 Commits
Author SHA1 Message Date
Colby McHenry c9e207a0f2 feat(cli): add codegraph usage command to show AI balance and recent usage
Adds a `usage` subcommand that pings `/v1/usage` with the stored token and
displays balance, plan, 30-day explore/token counts, and allowance reset date.

Degrades quietly in all non-happy-path states — signed out, BYO endpoint, or
unreachable server — so managed reasoning remaining optional doesn't change.

Also extends `OffloadUsage` with the fields the endpoint already returns
(`unlimited`, `banned`, `tokensLast30`, `callsLast30`, `creditsLast30`) that
were previously untyped.
2026-06-18 01:22:58 -05:00
Colby McHenry 193722de45 feat(cli): replace offload subcommands with browser device-authorization login / logout
The old `offload` command family required users to paste a token manually (`offload login --token `) and exposed bring-your-own-endpoint plumbing (`set-endpoint`, `status`, `disable`) as top-level CLI surface. This replaces it with a standard OAuth device flow (RFC 8628 shape) against the CodeGraph dashboard.

`codegraph login` calls `/api/cli/device/start`, opens the browser to the returned URL, polls `/api/cli/device/token` until the user approves, then stores the minted token and enables managed reasoning. `codegraph logout` clears it. BYO-endpoint configuration moves entirely to env vars (`CODEGRAPH_OFFLOAD_URL` / `CODEGRAPH_OFFLOAD_KEY` / `CODEGRAPH_OFFLOAD_MODEL`), keeping the CLI surface minimal.
2026-06-18 00:15:40 -05:00
Colby McHenry 8aa05380a2 Merge branch 'feat/offload-byo' into codegraph-ai 2026-06-17 23:48:10 -05:00
Colby McHenry 3ba82681ea Merge branch 'fix/explore-corroboration-ranking' into codegraph-ai 2026-06-17 23:47:50 -05:00
Colby McHenryandClaude Opus 4.8 da5c6c2f79 feat(offload): managed tier (CodeGraph AI) — metered reasoning via org token [WIP]
Adds the managed offload mode: point codegraph_explore at the CodeGraph AI metered
gateway (https://ai.getcodegraph.com) with an org token instead of a BYO provider key.
Same synthesis client, pointed at codegraph-ai-proxy (a metered OpenAI-compatible gateway).

- credentials.ts — org token in ~/.codegraph/credentials.json (0600); unlike a BYO
  provider key it's a revocable org-scoped auth token (gh/npm-login style), kept out
  of config.json
- config.ts — managed branch in resolveOffload: default gateway URL + public model id
  (openai/gpt-oss-120b) + login token as bearer; managed requires a token to be enabled
- reasoner.ts — fetchUsage() reads the credit balance from /v1/usage
- bin/codegraph.ts — `codegraph offload login --token <t>` / `logout`; status shows the
  managed tier + live balance

Proven GREEN end-to-end against a local wrangler-dev of the proxy: org token validated,
credits prechecked, real Cerebras synthesis returned, and credits metered + charged
(250,000 → 248,473). Graceful degrade on upstream failure; balance via /v1/usage.
Phase 3 (codegraph login device flow) replaces the manual --token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 15:06:42 -05:00
Colby McHenryandClaude Opus 4.8 db4c9f3641 feat(offload): reasoning offload for codegraph_explore (bring-your-own endpoint)
codegraph_explore can now hand the source it retrieved to a reasoning model you
point at — any OpenAI-compatible endpoint (Cerebras, OpenAI, a local vLLM/Ollama)
with your own key — and return that model's tight, cited answer instead of the
raw source dump. The agent's main context gets the answer in far fewer tokens, at
the cost of one network round-trip.

Off by default. Configure with `codegraph offload set-endpoint <url> --model <m>
--key-env <ENV>` (or the CODEGRAPH_OFFLOAD_* env vars); status/disable manage it.
The API key is never written to disk — the config stores the NAME of an env var
and the key is read from it at call time. Strictly degradable: any failure
(no endpoint, network, timeout, empty answer) returns null and the call falls
back to the local source, so the offload can never surface an error to the agent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 14:18:22 -05:00
Colby McHenryandClaude Opus 4.8 798cd0e21c fix(explore): keep multi-term backend files from being buried by a denser frontend layer
codegraph_explore's file sort is primarily driven by Random-Walk-with-Restart
graph-centrality mass, seeded from the query's text matches. In a cross-layer
monorepo (an API server alongside a much larger, internally dense frontend that
mirrors the same domain words), that mass skews to the bigger layer — so a
backend service/handler that genuinely matches several query terms, even when
it's the #1 search hit, sorts below hits=0 frontend files and gets truncated out
of the response, and the agent reads it back.

Add a corroboration tier above the graph signal: a file that is BOTH an
entry/central file AND matched by >=2 distinct query terms is kept in. The
entry/central guard prevents an incidental multi-term file (a type/util file
that isn't the flow) from displacing a graph-central answer file — a blunt
hits-only tier regressed that case. Single-layer repos are unaffected. Gated by
CODEGRAPH_RANK_NO_MULTITERM=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 13:44:39 -05:00
Colby MchenryandGitHub f34f606342 feat(extraction): same-file value-reference edges for impact analysis — 15 languages (#897)
Adds same-file value-reference edges (reader symbol → const/var it reads) so impact analysis catches a constant's same-file consumers, closing the 'change this table, break its readers' hole. 15 languages validated S/M/L on public OSS: TS/JS/tsx, Go, Python, Rust, Ruby, C, Java, C#, PHP, Scala, Kotlin, Swift, Dart, Pascal/Delphi (+ Svelte/Vue/Astro inherited). Edges-only — node count identical on/off; default ON, CODEGRAPH_VALUE_REFS=0 opts out.
2026-06-16 12:16:00 -05:00
2f6316500d feat(extraction): enable same-file value-reference edges by default (TS/JS) (#895)
Value-reference edges (same-file `references` edges from a reader to the
file-scope const/var it reads) shipped behind CODEGRAPH_VALUE_REFS pending an
agent A/B. The A/B is in: on excalidraw the edges are correct and precise (node
count unchanged) and they transform the impact/blast-radius API — `impact` on a
const consumed by 103 readers goes from 1 affected symbol to the full radius.
That blast-radius API is what `codegraph impact` and CodeGraph Pro's verdict
engine consume, so the win is impact correctness; the agent path showed no
regression. Flip the default on; CODEGRAPH_VALUE_REFS=0 disables.

Also close the one precision gap the A/B surfaced: a bundled/Emscripten
`const Module` re-declared as an inner `var Module` / param produced false
positives (nested readers resolve to the inner binding). isGeneratedFile() is
path-only and can't catch content-minified bundles, so prune SHADOWED targets at
the syntax level — drop any value-ref target whose name is bound by more than one
`variable_declarator` in the file. On excalidraw this removes the 23 false
positives while preserving every real reader (impact unchanged at 170).

Adds regression coverage (there was none): same-file readers are edged, they
surface in the impact radius, shadowed consts are NOT edged, and
CODEGRAPH_VALUE_REFS=0 emits nothing.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 01:28:00 -05:00
b49147eab0 fix(cli): make codegraph index a full rebuild so it stops reporting 0 nodes (#874) (#894)
`codegraph index` ran extraction against the already-populated DB without
clearing it first. On an unchanged tree every file's content hash still
matched, so the orchestrator skipped re-inserting all of them and the run
reported its delta (after - before = 0) as "0 nodes, 0 edges" — which read as
if `index` had wiped the graph. `init` only ever differed because it runs on a
freshly created, empty DB.

Clear the existing graph before re-indexing so `index` rebuilds from scratch
and reports the same complete result as a fresh `init`. `--force` keeps its
role as the home-dir/root-path override; `sync` stays the incremental path.

Adds an end-to-end regression test driving the built binary (init -> index),
asserting the graph stays populated and the summary is never "0 nodes, 0 edges".

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:45:03 -05:00
ab107b325a fix(watcher): warn (don't degrade) on Linux inotify watch exhaustion (ENOSPC) (#893)
On the Linux per-directory watch path, hitting fs.inotify.max_user_watches
surfaces as ENOSPC — which the degrade logic added for #876 (EMFILE/ENFILE
only) did not catch, so it fell through to the silent "skip this directory"
branch: a large repo got a partial watch set with no hint why edits in
unwatched directories stopped auto-syncing.

ENOSPC is non-fatal — raise the limit and partial watching keeps working — so
it now warns ONCE, naming the exact knob (fs.inotify.max_user_watches, with the
sysctl to set it), instead of degrading. It also stops attempting further doomed
watches for the session (every inotify_add_watch would fail too). Installed
watches keep firing; `codegraph sync` / git sync hooks cover the remainder.

Validated on macOS (forced per-directory path) and real Linux (Docker) — the
new test asserts a single warning naming fs.inotify.max_user_watches, no
degrade, and a live partial watch.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 00:01:05 -05:00
beca7116a0 feat(mcp): surface degraded watcher state to the agent in tool responses (#892)
When live file watching permanently degrades (watch-resource exhaustion, or a
write lock held past the retry budget), getPendingFiles() goes empty — so the
existing per-file staleness banner can't fire even though the index is now
frozen and silently drifting stale. The agent kept getting clean-looking
responses off a no-longer-updating index.

Read-tool responses now lead with a whole-index banner ("CodeGraph auto-sync
is DISABLED…") whenever the watcher is degraded, and codegraph_status gets a
dedicated "Auto-sync disabled" section. Both carry the degrade reason and tell
the agent to Read files directly. Expose isWatcherDegraded() /
getWatcherDegradedReason() on the CodeGraph class, and document the new banner
in the MCP server instructions.

Completes the agent-notification half of #876 (the operator-facing onDegraded
wiring shipped in #891).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:47:31 -05:00
cea4d086f9 fix(watcher): degrade cleanly on watch exhaustion and prolonged lock contention (#891)
The live file watcher could stay "alive" after it had stopped being
trustworthy. EMFILE/ENFILE watch-resource exhaustion only logged (and was
silently tolerated on the Linux per-directory path), and prolonged
LockUnavailableError retried forever at the normal debounce cadence — both
left auto-sync dead while the index silently drifted stale. Especially bad
for long-running MCP/daemon sessions.

Add a one-way degrade(): on watch-resource exhaustion (any watch strategy)
or on lock contention past a bounded exponential-backoff budget, log once,
fire a new onDegraded callback, and stop. start() now returns false
consistently when the per-directory path degrades at startup — it previously
returned true on Linux, so the MCP server reported the watcher "active" when
it had degraded. Wire onDegraded into the MCP server so callers are actually
told, and expose isDegraded()/getDegradedReason().

Builds on the approach in #877 by @thismilktea. Validated on macOS
(recursive), Linux (per-directory, Docker) and Windows (recursive) — 30/30
watcher + watch-policy tests on each.

Closes #876

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 23:32:54 -05:00
Colby McHenry 1bd9431879 Merge branch 'feat/value-reference-edges' 2026-06-14 18:39:35 -05:00
Colby McHenryandClaude Opus 4.8 ec90ddf79a feat(extraction): same-file value-reference edges (flag-gated)
Emit 'references' edges from a symbol to the file-scope const/var it reads
(TS/JS), so impact analysis catches "change this table, affect its readers".
Off by default behind CODEGRAPH_VALUE_REFS pending the agent A/B; on a real PR:
+3.1% edges, 100% precision on the spot-checked target, 372/372 extraction tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 19:25:34 -05:00
github-actions[bot] a1489f77a6 docs(changelog): promote [Unreleased] into [1.0.1]
[skip ci] Auto-generated by Release workflow.
2026-06-13 21:08:42 +00:00
github-actions[bot] ceb66d86fa release: sync package-lock.json to 1.0.1
[skip ci] Auto-generated by Release workflow.
2026-06-13 21:08:31 +00:00
b35a292c90 chore(release): bump version to 1.0.1 (#869)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:06:55 -05:00
5cc155ddc4 fix(index): skip nested git worktrees instead of indexing them as duplicate repos (#848) (#868)
A git worktree nested in a project (e.g. Claude Code's gitignored
`.claude/worktrees/<name>/`) was swept into the index as an embedded repo: its
`.git` is a FILE pointing into the host repo's `.git/worktrees/`, and embedded-
repo discovery treated any `.git` (file or directory) as a distinct repo to
index. Each worktree then duplicated the entire graph — one report went from
~1,850 files to 24,533, with search/explore flooded by stale copies.

classifyGitDir() now distinguishes:
- `.git` directory       -> embedded clone, index (#193/#514/#622, unchanged)
- `.git` file → worktrees/ -> worktree, skip (#848)
- `.git` file → modules/   -> submodule, index (unchanged)

Applied at both embedded-repo entry points: findNestedGitRepos discovery (which
also covers the sync/change-detection path) and the untracked-subdir recursion
in collectGitFiles.

Verified: the reproduction drops from 6 files / betaHelper×3 to 3 files / ×1,
with a genuine embedded clone and submodules still indexed. Regression test added.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 16:01:00 -05:00
64ff7597d0 fix(cli): stop serve --mcp from confusing humans — hide it + explain on a TTY (#867)
`codegraph serve --mcp` is the stdio MCP server an AI agent launches for itself
(the installer wires it into every agent's MCP config), not a command a human
runs. Run by hand in a terminal it just hung waiting for JSON-RPC, looking
broken.

- Hide `serve` from `--help` (commander `{ hidden: true }`); it stays fully
  invocable, so agents are unaffected.
- When stdin is an interactive TTY (a person — never the agent's pipe or the
  detached daemon), print what it is and point to `codegraph status` /
  `codegraph daemon`, then exit instead of hanging.
- README: drop `serve --mcp` from the CLI Reference and stop the troubleshooting
  section from telling users to run it; keep the accurate "your agent launches
  it" note.

Verified: agent path intact (22 MCP handshake/daemon tests pass), `serve` absent
from --help, and the TTY path prints the message and exits cleanly.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 15:18:55 -05:00
f7441f2124 fix(resolution,cli): cross-file static method calls + affected path normalization (#825) (#865)
Cross-file `ClassName.staticMethod()` calls resolved to the class, not the
method: the import resolver matched the receiver `Foo` to the named class
import but dropped the `.bar` member, and createEdges then mis-promoted the
`calls` edge to `instantiates`. So callers/impact for the static method came
back empty. Descend from the resolved class into its `Container::member` so the
call links to the method; fall back to the class when no such member exists
(non-`::` languages and genuine class references are unaffected).

Also normalize `codegraph affected` inputs to the project-relative,
forward-slash form the index stores, so `./src/x.ts`, an absolute path, and a
Windows back-slash path all match (previously silently returned 0).

Validated on luxon (24 files): node/edge totals identical (no explosion), 69
mis-promoted `instantiates` edges become `calls`, and real static factories
(DateTime.fromISO, etc.) resolve their callers. Full suite: 1534 passed.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:48:52 -05:00
fb974552b0 docs(readme): point existing users to codegraph upgrade under the 1.0 banner (#866)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:48:48 -05:00
070ce4da2b feat(cli): codegraph version command + complete CLI Reference (#864)
* feat(cli): codegraph version command + complete CLI Reference

Add a `codegraph version` subcommand plus the `-v` and `-version`
spellings (commander already wires up `--version`/`-V`), so the version
is easy to reach however a user guesses at it. The `-v`/`-version` forms
are intercepted before commander parses — its version short flag is the
capital `-V`, and its parser rejects a multi-character single-dash flag.
A trailing `-v` on a subcommand still means `--verbose`.

Document the previously-missing commands in the README CLI Reference:
`daemon`/`daemons`, `unlock`, `telemetry`, `version`, and `help`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): reference #864 on the version-command entry

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 14:14:32 -05:00
Colby MchenryandGitHub ff288ac711 feat(cli): one interactive codegraph daemon command, replaces stop/list (#863)
Collapses the unreleased daemon controls into a single interactive command.
`codegraph daemon` (alias `daemons`) opens an arrow-key picker (current project's
daemon first, pre-selected), enter stops it, or pick "Stop all"; non-TTY prints a
plain list. Removes stop/list/ps; reuses the unchanged daemon-registry machinery;
the pick->stop loop is in daemon-manager.ts behind an injectable select (unit
tested). Validated live on macOS/Linux (real clack picker driven via pty) and
Windows (real runDaemonPicker + stopDaemonAt against a real daemon). Closes #845
follow-up.
2026-06-13 13:53:38 -05:00
Colby MchenryandGitHub 0f825649a1 feat(cli): codegraph stop / list to manage background daemons (#861)
Adds first-class daemon control (the #845 pain point: no clean way to stop a
runaway daemon). `codegraph stop [path]` stops the current/given project's
daemon (SIGTERM -> SIGKILL fallback, sweeps artifacts); `stop --all` stops every
daemon; `list`/`ps` shows running daemons (--json for scripts).

Discovery via a small self-healing registry: each daemon records its root under
~/.codegraph/daemons/ on start, removes it on graceful shutdown; readers prune
dead pids. Cross-platform by construction (files + process.kill). Validated live
on macOS, Linux (docker), and Windows (VM): registry unit 6/6 and real-daemon
stop/list 6/6 on each.
2026-06-13 12:59:41 -05:00
Colby MchenryandGitHub 2472508549 fix(installer,cli): refuse to index $HOME / filesystem root (#860)
Running the installer or `codegraph init`/`index` from $HOME auto-indexed the
entire home tree (installer indexes process.cwd() with no guard), producing a
multi-GB ~/.codegraph/codegraph.db; the install dir sharing the ~/.codegraph
name then made every home subdir resolve its root to $HOME. On pre-1.0 macOS the
per-file watcher over that tree exhausted kern.maxfiles and crashed the machine
(#845; the fd blowup was fixed in 1.0.0, this fixes the root cause).

Add unsafeIndexRootReason() and refuse the home dir, a parent of home, and
filesystem roots at the installer auto-index, `init`, and `index`. Overridable
with --force. Closes #845.
2026-06-13 12:35:18 -05:00
Colby MchenryandGitHub 484da77296 test(mcp): make liveness-watchdog kill assertions cross-platform (#859)
Validated the watchdog on the Windows VM: it kills a wedged process correctly,
but Windows has no real signals — process.kill(pid,'SIGKILL') maps to
TerminateProcess, seen as signal=null + non-zero code, not 'SIGKILL'. Assert
"killed" platform-agnostically and require the own exit code in the opt-out test.
Source watchdog unchanged. Windows: fatal-handler 8/8, liveness-watchdog 7/7,
mcp-daemon 9/9; mcp-initialize EPERM is pre-existing (identical with watchdog off).
2026-06-13 11:51:57 -05:00
Colby MchenryandGitHub 1702dfc544 fix(mcp): make the liveness watchdog a separate process, not a worker thread (#858)
The worker-thread watchdog from #856 didn't work in the real daemon — caught by
live-testing against a real serve --mcp. V8 isolates coordinate on global
safepoints, so a main thread wedged in a tight non-allocating loop (#850's
SourcePositionTableIterator::Advance) strands the watchdog worker before it can
SIGKILL.

A separate child process shares no isolate/heap with the parent, so the wedge
can't touch it; it kills via the kernel. Parent heartbeats to the child's stdin;
silence past the timeout -> SIGKILL; parent exit closes the pipe -> child exits.
Validated live (real daemon SIGKILLed in ~timeout); regression test covers the
non-allocating-wedge-under-heap-pressure case. API/install points/CHANGELOG
unchanged; the broken worker version was never released.
2026-06-13 10:40:55 -05:00
Colby MchenryandGitHub 576149e062 feat(mcp): worker-thread liveness watchdog to self-kill a wedged main thread (#856)
Belt-and-suspenders follow-up to #855. Any non-yielding sync loop on the main
thread wedges the event loop, and nothing running on that loop (timers, signal
handlers, PPID watchdog) can recover it — only another thread can.

A tiny worker thread (in the detached daemon + direct modes) watches a
shared-memory heartbeat the main thread bumps each event-loop turn; if it stops
advancing across enough consecutive checks (~CODEGRAPH_WATCHDOG_TIMEOUT_MS,
default 60s) the worker SIGKILLs the process so a fresh daemon starts on the next
connection. Counts consecutive stale checks (not wall-clock) so it's immune to
clock jumps / sleep; tuned never to fire on real work; opt out with
CODEGRAPH_NO_WATCHDOG=1.
2026-06-13 10:04:40 -05:00
Colby MchenryandGitHub 3476ac9a27 fix(mcp): exit on uncaught exception instead of orphaning/spinning at 100% CPU (#855)
The process-wide uncaughtException handler logged the error and kept running. For
the detached `serve --mcp` daemon that turned any escaped fault into an
unrecoverable orphan: nothing respawns it, and when logging the raw Error hit a
V8 source-position loop while lazily formatting `.stack`, the main thread wedged
at 100% CPU so even the PPID watchdog / idle-timer could no longer fire. Same
failure mode as #799, which only fixed the stdin-'error' trigger.

Restore Node's default fatal semantics: render a bounded, hang-proof line (name +
message only — never read `.stack`) then exit non-zero, so a fresh daemon starts
on the next connection. Extracted to src/bin/fatal-handler.ts with injectable
seams; unit-tested incl. the never-touch-stack invariant.

Closes #850.
2026-06-13 09:48:44 -05:00
06e03758af docs(readme): collapse the npm-install alternative into a details section (#844)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:01:12 -05:00
13027b0730 docs(readme): auto-sync becomes quick-start step 4 heading (#843)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 18:00:23 -05:00
eed0b5ae20 docs(readme): init indexes by default (drop -i) + bold auto-sync guarantee (#842)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 17:57:50 -05:00
github-actions[bot] 3286e9f104 docs(changelog): promote [Unreleased] into [1.0.0]
[skip ci] Auto-generated by Release workflow.
2026-06-12 18:22:16 +00:00
github-actions[bot] 238bd909cb release: sync package-lock.json to 1.0.0
[skip ci] Auto-generated by Release workflow.
2026-06-12 18:22:06 +00:00
b9eff08c77 chore(release): 1.0.0 — README banner + X account (#840)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:21:46 -05:00
06a410e9b4 feat(extraction): R language support (#828) (#839)
R has no declaration syntax — everything is an expression — so the
extractor works through the visitNode hook: functions in every
assignment form (incl. nested, attributed to their enclosing scope),
top-level variables/constants, library()/require() imports and
source() file references (claimed, Lua-style), S4/RefClass/R6/ggproto
classes with their methods and extends edges, setGeneric/setMethod.
Grammar vendored from r-lib/tree-sitter-r v1.2.0 (ABI 14; npm package
is a security placeholder, tree-sitter-wasms has no R).

Benchmarked on AnomalyDetection (8/8 named defs), dplyr (1027 fns),
ggplot2 (150 ggproto classes / 597 methods / 128 extends edges —
adding ggproto mid-bench flipped the large-repo A/B from a regression
to 2.4x faster than the no-codegraph arm).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:17:29 -05:00
2c7bbd5387 fix(extraction): C# record-struct kind fidelity + bodiless positional records (#831 follow-up) (#838)
The shipped grammar parses every record form as record_declaration (no
record_struct_declaration node), so 'record struct' mis-kinded as class.
classifyClassNode now distinguishes the value-type form by its struct
keyword child, and extractStruct accepts bodiless positional records
(the no-body gate is for C/C++ forward declarations) instead of
crashing mid-file on them.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 13:17:25 -05:00
ed117ef90b feat(index): multi-repo workspaces index as a whole (#514) (#837)
A workspace holding several independent git repos failed two ways:

- Enumeration: a super-repo whose .gitignore hides its child repos
  (/packages/) indexed 0 of their files — git never lists ignored dirs,
  and the #193 embedded-repo recursion only fired for UNTRACKED dirs.
  Gitignored embedded repos are now discovered (ignored-dirs listing +
  bounded .git search) and enumerated by their own git ls-files.
- Change detection: git status in the parent says nothing about embedded
  repos (untracked OR ignored), so codegraph sync missed every child
  change. Status now recurses per embedded repo.

ScopeIgnore is the new single source of truth for indexer + watcher
scope: parent rules for ordinary paths, the child repo's own rules for
paths inside it, built-in defaults uniformly on full paths (a git repo
inside node_modules is an npm git-dependency, not project code), and
ancestors of embedded roots are never pruned (the Linux per-directory
watcher must descend to reach them).

Non-git workspace roots already worked via the per-directory gitignore
walk — locked in by test.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 12:41:22 -05:00
13b2575dfe fix(installer): opencode global config goes to ~/.config/opencode on every platform (#535) (#836)
* fix(installer): opencode global config goes to ~/.config/opencode on every platform (#535)

opencode resolves its config dir with xdg-basedir (XDG_CONFIG_HOME ??
~/.config) unconditionally — it never reads %APPDATA%; that layout
belonged to the discontinued Go fork. Writing there on Windows meant
opencode never saw the MCP entry.

- globalConfigDir(): drop the win32 APPDATA branch; XDG resolution everywhere
- install/uninstall (global): sweep a stale codegraph entry + AGENTS.md block
  out of the legacy %APPDATA%/opencode location (siblings/comments untouched)
- detect(global): a legacy-only dir still counts as installed so the sweep
  is reachable
- tests are env-gated, not platform-gated, so the whole matrix runs on any
  OS; the suite previously pointed APPDATA and XDG_CONFIG_HOME at the same
  dir, which is exactly how the divergence stayed invisible

Supersedes the prefer-if-exists approach of #670 (greenfield installs --
before opencode's first run -- would still have fallen back to APPDATA).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(installer): match legacy sweep paths by dir prefix, not 'AppData' substring

On Windows os.tmpdir() lives under AppData\Local\Temp, so every harness
path contains 'AppData' and the substring assertions false-positive.
Caught on the real Windows VM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:43:18 -05:00
df6f4bec43 feat(explore): dynamic-dispatch boundary surfacing — announce where a flow ends instead of guessing edges (#687) (#835)
* feat(explore): announce dynamic-dispatch boundaries when a flow can't connect statically (#687)

When buildFlowFromNamedSymbols can't connect the agent's named symbols, scan
the disconnected symbols' bodies (query-time, deterministic, zero graph
mutation) for dynamic-dispatch forms — computed member calls, getattr,
reflection, typed message buses, runtime-keyed emits, Proxy — and announce
the exact site where the static path ends, with candidate runtime targets
when a dispatch key is statically visible. The honest alternative to
guessing edges: surface the boundary, don't fabricate the bridge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(agent-eval): ab-new-vs-baseline survives files added since the baseline ref

A single multi-file 'git checkout <ref> --' with one unknown pathspec checks
out nothing, so the baseline arm silently ran the NEW build. Check out
per-file and remove files that don't exist on the baseline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(playbook): boundary surfacing as the mechanism floor for non-gateable dispatch (#687)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(explore): render a direct synthesized hop between two named symbols (#687)

A 2-node chain populates pathIds but renders nothing (Flow needs >=3), and
the dynamic-links section skipped its edge as 'already in the main chain' —
so a custom EventBus emit→handler connection was invisible. Skip-as-in-chain
now applies only when a chain actually renders, and the boundary scan treats
short-chain endpoints as connected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 11:26:33 -05:00
848fde9f59 feat(telemetry): anonymous usage telemetry — documented schema, opt-out, public ingest worker (#834)
Adds anonymous usage statistics (commands/tools used, languages indexed,
connecting agents) with a strict, auditable allowlist. Never code, paths,
file/symbol names, queries, or IPs.

- src/telemetry/: zero-dep client — consent resolution (DO_NOT_TRACK >
  CODEGRAPH_TELEMETRY > stored choice > default-on), random machine UUID,
  in-memory counters → capped JSONL buffer → completed-day rollups; sync
  exit-append (survives process.exit) + opportunistic bounded sends; the
  first-run notice gates the first SEND, never local buffering, so the
  installer's consent toggle always precedes it. Off is off: no recording,
  no socket, buffered data deleted.
- codegraph telemetry status|on|off; per-command counting via preAction hook.
- MCP: tool counting after the reply is on the wire (session + proxy
  in-process fallback), agent attribution from initialize clientInfo,
  unref'd daemon flush interval. Zero hot-path cost, zero stdout.
- Installer: visible default-on consent toggle (asked once, never re-asked),
  install/index/uninstall lifecycle events.
- telemetry-worker/: public Cloudflare Worker behind telemetry.getcodegraph.com
  — allowlist validation, IP stripping, per-machine rate limit, forwards to
  PostHog as anonymous events. Ships nowhere with the npm package.
- TELEMETRY.md (field-by-field contract) + README section + design doc.
- 20 unit tests; suite-wide CODEGRAPH_TELEMETRY=0 guard so tests never
  pollute real telemetry. Full suite: 1448 passing.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 10:37:19 -05:00
7db4c1d2f8 test(mcp): unindexed suite teardown survives Windows file locking (#824)
The spawn-based tests failed on Windows with EPERM in afterEach — the
SIGKILL'd server child briefly holds the temp cwd/SQLite handles when
rmSync runs (the documented class that fails mcp-initialize/mcp-roots
teardowns). Await the child's exit (3s cap) and retry the removal
(maxRetries/retryDelay); assertions were already passing on Windows.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:43:32 -05:00
7ef2ea9c11 docs(readme): MCP Tools table reflects the 4-tool default surface (#818) (#823)
The table still listed all 8 tools; it now shows the default four
(explore/node/search/callers, with node's Read-parity file mode), the
CODEGRAPH_MCP_TOOLS re-enable path + CLI equivalents for the unlisted
four, and the inactive-when-unindexed behavior (#817).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:36:20 -05:00
01717854f5 fix(cli): codegraph node accepts Windows backslash paths in file mode (#822)
The file-vs-symbol heuristic only matched '/' — `codegraph node
src\auth\session.ts` on Windows fell through to symbol mode and found
nothing. Both separators now route to file mode, normalized to forward
slashes (the form the index stores). Symbols never contain either
separator in any indexed language.

Validated: macOS smoke (explore/node symbol/node file/unindexed
refusal) + Linux Docker (same smoke + full suite, 1428 passed).
Windows VM validation queued — the Parallels guest is currently
unreachable (control commands are Pro-gated; needs a manual start).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:30:56 -05:00
adcb862f8e fix(cli): explore/node not-indexed error stops agents from running init themselves (#821)
The message said "run 'codegraph init' first" — an instruction-shaped
error that invites an agent hitting it (e.g. a subagent following the
global instructions block into an unindexed repo) to index the project
uninvited: minutes of CPU and a surprise .codegraph/ the user never
asked for. Every other layer already encodes indexing-is-the-user's-
decision (the MCP NotIndexedError guidance, the inactive instructions,
the conditional block); the CLI now matches: continue with your usual
tools, do not run init yourself, the project owner can enable it.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:24:08 -05:00
e2f1fe4b2a fix(installer): instructions block is scope-neutral — conditional on .codegraph/ existing (#820)
A global install writes the block into user-scope files
(~/.claude/CLAUDE.md, ~/.codex/AGENTS.md) that apply to EVERY repo the
user opens — the unconditional "This repository is indexed" claim was
false in unindexed ones and would send subagents into failing codegraph
calls, the exact noise the unindexed-session policy (#817) eliminates.
Now: "In repositories indexed by CodeGraph (a .codegraph/ directory
exists) …" plus an explicit skip-entirely line for the no-index case.

Residual: the delegation A/B validated the assertive project-scoped
wording; the conditional form keeps the same active ingredients (the
codegraph name + both command surfaces in the relay-able slot) — fold a
re-check into the next delegation A/B run.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 00:20:29 -05:00
8170d181f2 feat(cli+installer): codegraph explore/node CLI + instructions-file block — subagent & non-MCP reach (#704) (#819)
Task-tool subagents never see the MCP initialize instructions and hold
the MCP tools only as deferred names they rarely think to load — so
delegated work bypassed codegraph almost entirely (measured ~1 of 9
forced-delegation runs touched it; the rest did 30-50 grep/read calls).
Two additions close the gap:

- CLI: `codegraph explore` and `codegraph node` call the same ToolHandler
  as the MCP tools and print identical output — the graph for any agent
  with a shell (subagents, Gemini CLI, raw Codex, humans).
- Installer: each agent target (claude/codex/gemini/opencode) writes a
  short marker-fenced CodeGraph section into its instructions file —
  the one channel subagents DO receive — naming both surfaces. Upsert
  self-heals the stale pre-#529 long block; uninstall strips it; re-runs
  are byte-equal unchanged. (#529's duplication argument bounded the
  size: four lines, commands only.)

A/B (excalidraw, sonnet/high, forced Explore-agent delegation): without
the block, subagent codegraph usage ~1/9 runs; with it, 4/4 — subagents
ToolSearch-load the MCP tools and run explore 5-7x, best runs with ZERO
Read/grep (80-95s vs 150-197s baseline). The block's mechanism: the
parent relays the note into the task prompt, making the deferred tool
names salient.

Contract tests updated to the new expectations (write + self-heal
replace the #529 strip-only behavior); README install/guidance sections
refreshed (they also still described the pre-#817/#818 tool surface).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:50:41 -05:00
c450fd95b7 feat(mcp): default tool surface trimmed to 4 — explore, node, search, callers (#818)
callees/impact/files/status stay fully functional (handlers, CLI, library
API untouched; CODEGRAPH_MCP_TOOLS re-enables any) but are no longer
LISTED by default. Evidence: codegraph_impact appears in zero recorded
eval runs ever; its blast-radius info already arrives inline on explore
(Blast radius section) and node (dependents note). callees is redundant
by construction (a symbol's body IS its callee list). files/status
"reduce to one grep" per the tiny-repo audit, and staleness banners
already inline pending-sync. callers stays: exhaustive call-site
enumeration (incl. callback registrations, per-definition sections) is
the one job explore/node don't replicate. Fewer tools = fewer mis-picks
+ ~300 schema tokens saved per session; presence itself steers.

server-instructions rewritten around the 4-tool surface ("what does X
call" → node body+trail; "what breaks" → callers + inline blast radius).
Tiny-repo gate unchanged (its trio ⊆ the default set); stale gate
comment corrected (context/trace are long gone — its "5 core tools" are
today's trio).

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 20:50:22 -05:00
f9fcc2cd6a feat(mcp): unindexed sessions go quiet — empty tools/list + inactive instructions, no-error policy (#769) (#817)
An MCP session in a workspace with no .codegraph/ previously got the full
"lean on codegraph for everything" playbook plus all 8 tools, then every
call returned isError — and one or two early errors teach an agent to
abandon codegraph for the whole session (maintainer-observed). Now the
initialize response picks an instructions variant by index state (cheap
sync walk-up, #172 respond-fast contract holds) and tools/list serves an
EMPTY list when unindexed: absence is the one signal an agent can't
misread. Indexing is deliberately the user's call — the inactive note
tells the agent not to run init itself.

No-error policy in the tool handler: expected/recoverable conditions
(NotIndexedError — cross-project query to an unindexed path, default-
project detection miss) return SUCCESS-shaped guidance instead of
isError; security refusals (PathRefusalError) stay hard errors without
retry encouragement; genuine internal failures keep isError but add a
retry-once note so a transient blip doesn't convert to permanent
abandonment. Principle recorded in CLAUDE.md.

Also: codegraph_search kind:"type" (advertised by its own schema enum)
silently matched nothing — now maps to type_alias; codegraph_explore's
query param no longer tells agents to run codegraph_search first
(contradicted explore's call-FIRST design); server-instructions
§Limitations rewords the unindexed case to stay-out-for-the-session.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 20:03:26 -05:00