@polycode-projects/seonix
v0.10.18
Published
SEONIX — a deterministic, offline, $0 code-graph tool that makes a cheap coding agent edit like an expensive one. Indexes a repo with Python ast + git (zero model calls) and renders a bounded edit-digest.
Maintainers
Readme
@polycode-projects/seonix
A deterministic, offline, $0 code-graph tool that makes a cheap coding agent
edit like an expensive one. SEONIX indexes a repository with Python's ast and
git log, with zero model calls, into a typed code graph, then renders a
bounded edit digest you inject once at session start. Your agent edits from
turn 1 without fishing through the repo.
Headline. Measured as mean input tokens per both-solved task (paired arms, identical model pin). Under the shipped score-gap default, two cells clear the ≥50% bar, both on exploration-bound complex tasks (a TypeScript middleware task on Haiku 4.5 and a C# service task on Sonnet 4.6), with several more in the 35 to 48% range. The token cut tracks the turn cut: seonix saves tokens by removing exploration turns, so tasks whose edit site is obvious change little. Indexing costs $0. The win is task- and model-conditional. Full method, numbers, and the measurement contract are in
BENCHMARKS.md.
- Website / live demo: https://seonix.polycode.co.uk
- Source + benchmarks: https://gitlab.com/polycode-projects/seonix
- Licence: AGPL-3.0-only
A cheaper model, matched
seonix on the cheaper Sonnet 4.6 used fewer input tokens than out-of-the-box
Opus 4.8 on six of eight tasks, at equal correctness. The full cross-model
table and the turns correlation (Pearson 0.91) are in
BENCHMARKS.md.
Install
npm i -g @polycode-projects/seonix # global — gives you the `seonix` bin on PATH
# or one-shot, no install:
npx @polycode-projects/seonix cli index_repository '{"repo_path":"/abs/path/to/repo"}'Runtime prerequisites: Node ≥ 24, Python 3, and git on PATH (the
indexer shells out to Python's ast and git log). A Java runtime is needed
only to index Java through JavaParser, and the .NET SDK only to index C# through
Roslyn; without them, indexing for that language falls back to the bundled
tree-sitter parser. Indexing is fully local and deterministic: no API keys, no
network calls, $0.
Quickstart
# 1. index your repo (deterministic, offline) — writes a machine-local .seonix/
cd /path/to/repo && seonix cli index_repository
# 2. self-locate the touched modules from the task text (no human-chosen target)
seonix cli seonix_search '{"query":"<your task>"}'
# 3. render the bounded edit digest for those modules
seonix cli digest '{"modules":["<top hits>"]}' > digest.txt
# 4. inject digest.txt once at session start, then run your coding agent
claude --prompt "Use the seonix digest below to localise and make the change. $(cat digest.txt)"Add .seonix/ to your .gitignore; it is machine-local and rebuilt from source.
The JSON argument is optional on every cli command: an omitted repo_path
resolves to the nearest ancestor of the current directory (current directory
first) containing a .seonix/, so query commands work from any subdirectory of
an indexed repo. index_repository falls back to the current directory when
nothing is indexed yet; query commands print a one-line error instead. An
explicit repo_path always wins.
Indexing multiple repositories
index_repository takes one of three argument forms:
# one repo (unchanged): artifacts to <repo_path>/.seonix/, module ids unprefixed
seonix cli index_repository '{"repo_path":"/abs/repo"}'
# n explicit repos, merged into ONE graph at <out_root>/.seonix/
seonix cli index_repository '{"repo_paths":["/abs/repo1","/abs/repo2"],"out_root":"/abs/dir"}'
# estate discovery: every immediate child directory of multi_root that contains a
# .git (dir or file) is indexed as a repo; artifacts to <multi_root>/.seonix/
seonix cli index_repository '{"multi_root":"/abs/estate"}'The forms are mutually exclusive. out_root defaults to the deepest common
ancestor directory of the paths (or the current directory if that ancestor is the
filesystem root). In the merged graph every module id and label is prefixed with
the repo's directory basename (mod:<repoName>/<relpath>); when two repos share a
basename the first in path sort order keeps the bare name and later ones get -2,
-3, … appended. Each repo's git history and .seonixignore are handled per repo,
exactly as in single mode — a .seonixignore at the multi_root parent is NOT read
(each sub-repo loads only its own). For an estate-wide exclude, put it in a seonix.toml
at the directory you index from ([index].exclude / secret_exclude); that propagates to
every sub-repo. Cross-repo call edges only arise where a symbol name
resolves uniquely across the whole merged graph; ambiguous names are dropped, as
within one repo. Query a merged graph by passing the merge root as repo_path
(e.g. seonix cli seonix_search '{"repo_path":"/abs/estate","query":"…"}').
The shape that ships
The product is the no-MCP digest path: render the digest, inject it once, run
with no server. The stdio MCP server still ships and is fully supported (bare
seonix starts it) — it's just not the recommended default, because even
mitigated the interactive tool loop never cleared the 50% bar: the cost lies in
the round-trip count rather than the resident schema. Use seonix_search
(locate, self-derives the target) in production; pass a known target (oracle)
only to measure the headroom.
CLI reference
Every invocation shape bin/cli.mjs dispatches on, verified against the current source.
Bare invocation — MCP stdio server
seonixNo flags, no config file. Starts the stdio MCP server against
<cwd>/.seonix/graph.json (override with SEONIX_GRAPH_FILE). MCP launchers
should start it with cwd = the indexed worktree.
cli index_repository — build the graph
seonix cli index_repository # no arg: index the repo you are in
seonix cli index_repository '{"repo_path":"/abs/repo"}'
seonix cli index_repository '{"repo_paths":["/abs/repo1","/abs/repo2"],"out_root":"/abs/dir"}'
seonix cli index_repository '{"multi_root":"/abs/estate"}'repo_path / repo_paths+out_root / multi_root are mutually exclusive — pass
at most one. When none is given, seonix.toml's repositories supplies the set
if present; otherwise the target defaults to the nearest ancestor of the current
directory carrying a .seonix/ (re-index), else the current directory itself
(first index). Shared JSON options:
ignores(bool, defaulttrue) — honour the repo's.seonixignore. Pass"ignores":falseto index everything regardless (the bench harness always passes this, so a subject repo's own.seonixignorenever shapes an arm's index).config(bool, defaulttrue) — readseonix.tomlfrom the target dir. Pass"config":falseto skip it entirely (byte-identical to no-file behaviour; the bench harness's index/locate/digest calls always pass this).sync(bool) — on amulti_rootcall,"sync":trueroutes through the same incremental path ascli syncinstead of a full re-index.history_depth(non-negative integer) — cap on the git-history pass (both the commit walk and the history-symbol pass).0skips git history entirely; absent uses the default depth. Precedence: this arg >seonix.toml[index].history_depth> default.
Writes <out_root-or-repo_path>/.seonix/graph.json (+ manifest for
multi_root/sync), prints indexed counts to stderr and a human summary MD to
stdout.
cli sync — incremental estate re-index
seonix cli sync '{"multi_root":"/abs/estate"}'Fingerprints every child repo of multi_root and re-extracts only the ones that
changed since the last run (per-repo cache under .seonix/cache/); one union
rebuild is exact-equal to a full re-index, and an unchanged estate is a byte-stable
no-op. Accepts ignores and history_depth as above. Writes
.seonix/manifest.json.
cli digest — the no-MCP injection payload
seonix cli digest '{"repo_path":"/abs/repo","modules":["path/to/mod.mjs","other/mod.py"]}'
seonix cli digest '{"repo_path":"/abs/repo","query":"add a truncate filter to the article view"}'Two ways to pick the modules: an explicit modules array, or a query string —
query auto-locates and score-gap-selects the modules in one call (modules
wins if both are given). Prints an architecture map + a size-adaptive context
bundle per module to stdout, headed by a machine-readable line the rig parses:
# seonix-digest tier=<NONE|TINY|MID|LARGE|FULL> topup=<bool> modules=<N>
[selected=<comma-list>] (selected= only appears in query mode).
Tuning flags (all optional, precedence explicit arg > seonix.toml [tune] >
shipped default):
top_k(int, query mode only, default 2) — how many modulesquerymode selects.score_gap(number, orfalseto disable) — the score-gap cutoff for query-mode selection.literal_mention(bool, defaulttrue) — credit verbatim dotted-name/path mentions in the raw query text.min(bool) — force the leanest bundle (no auto top-up).untuned(bool) — the pre-tuning escalation behaviour.max(bool) — the injection ceiling: every requested module gets a FULL (untrimmed) bundle instead of primary-full + capped trimmed secondaries.
cli seonix_locate — ranked module list (recall probe)
seonix cli seonix_locate '{"query":"…","repo_path":"/abs/repo"}'Prints <relpath>\t<score> per line, highest first — the raw ranking a caller
(or the rig) can threshold itself. Recall/precision levers, all optional
(precedence arg > seonix.toml [tune] > default, all default false unless noted):
demote_nonprod— demoteexamples//fixtures//test-*paths.call_adjacency— resolved-call adjacency bonus.impl_of_interface— C# impl-of-interface boost.beam_search(+beam_width) — multi-ply discriminative beam expansion.literal_mention(defaulttrue) — verbatim dotted-name/path mention matching against the raw query.raw_query— forward a separately-normalized raw problem text for literal matching (else derived fromquery).spiral(+spiral_depth,limit) — a bounded-radius, degree-ordered ego-neighbourhood walk from the lexical seeds that can surface lexically-invisible modules.spiral_depthis the hop radius (only honoured if>0);limitoverrides how many newly-reached nodes the spiral may surface (default 100 — deliberately above the internal 12-node default, since this CLI surface is a recall probe, not a token budget).
cli browser_link — NL request → Chronograph URL
seonix cli browser_link '{"query":"classes that change with render","base":"https://example.com"}'Maps a natural-language-ish request to the code-browser query grammar
deterministically (no model call), self-tests the resulting link against the
local temporal graph (must match ≥1 node and every cursor must resolve), then
prints the URL — or exits 1 with the failure reason(s) rather than printing a
dead link. Accepts repo_path (which repo's graph to test against), base (the
URL prefix the link is built on), at / b (the two temporal cursors — single
cursor / diff-cursor), scope (restrict the graph to a package-prefix set,
same values as viz --scope), and grammar:true (take query as grammar
verbatim, skipping NL mapping).
cli store_rebuild — force-rebuild the sqlite store
seonix cli store_rebuild '{"repo_path":"/abs/repo"}'Force-(re)builds the opt-in node:sqlite resident store (see
SEONIX_STORE below) from the current
graph.json. The store is a derived, rebuildable companion — read paths never
auto-build it, so this is the explicit way to (re)materialize it after an index
or a manual graph.json edit.
cli <toolName> — generic MCP tool fallback
seonix cli seonix_ask '{"query":"which modules import codegraph"}'
seonix cli seonix_context '{"symbol":"buildContextBundle","repo_path":"/abs/repo"}'Any sub-command name that isn't one of the above routes straight to the MCP tool
dispatcher, so any tool in the server's surface — cold or hot — is invokable from
Bash with no MCP connection. repo_path in the JSON arg selects the target
graph; omitted, it defaults to the nearest ancestor of the current directory
containing a .seonix/ (a clean one-line error when there is none). Every other
key is passed through as the tool's own arguments.
hook-augment — PreToolUse Grep/Glob augmenter
seonix hook-augment # reads a PreToolUse hook event JSON from stdinReads the hook event from stdin, extracts the Grep/Glob pattern, and — if the
local graph has a hit — writes {hookSpecificOutput:{hookEventName,
additionalContext}} to stdout so the agent sees seon typed-graph hits alongside
its own search. Silent (exit 0, no output) on any failure — empty/malformed
input, no graph, no hit — the never-block contract.
init [--dotnet] — seed a seonix.toml
seonix init # single-repo template
seonix init --dotnet # estate/.NET template (repositories + broader include_text/exclude)
seonix init --force # overwrite an existing seonix.tomlCopies the shipped template to ./seonix.toml (byte-for-byte). Refuses to
overwrite an existing file unless --force (exit 2).
config --effective [--repo <abs>]
seonix config --effective
seonix config --effective --repo /abs/repoPrints the merged {effective, sources} config (sorted keys) as JSON — every
knob's resolved value plus which source won it ("arg" | "seonix.toml" |
"default"). --repo defaults to cwd. Requires --effective; without it,
prints usage and exits 2.
viz — render the code-map / code-browser / timeline
seonix viz --focus <Symbol> --out graph.html
seonix viz --serve --port 8080 --focus <Symbol>Flags: --focus <sym> (default: highest-degree module), --depth N (default 2),
--hub N (hub-degree cap, default 40), --max N (node cap, default 200), --out
f.html (default seon-graph.html), --data-out f.json (split viewer + sidecar
data file instead of embedding), --graph <path> (override the graph.json
location), --repo-url <url> --ref <branch> (source links), --site-nav
(add the site's absolute home entry), --force-inline (embed the full graph inline
even past the size gate that would otherwise split it out with a message).
By default viz also writes a code-browser.html (Chronograph) and a
timeline.html next to --out with a working header nav; override their paths
with --browser-out f.html / --browser-data-out f.json / --timeline-out
f.html, or suppress both siblings with --graph-only. --scope
product|<prefixes> filters the Chronograph browser/timeline to a package
prefix set. --limit N caps the commit timeline to the newest N commits (0 =
unlimited; default 100 — an estate-scale timeline stays openable, with a "newest
N of M" header note when truncated).
--serve [--port N] serves the same viewer live against this repo's own index
(code browser at /code-browser.html, alias /browser; timeline at
/timeline.html; live re-annotates on HEAD change via /code-browser-version) —
port 0 (default) picks a free port.
chat — interactive tmct chat over the graph
seonix chat [--repo /abs/path]Opens tmct's natural-language client over seonix's graph (see Chat below).
The conversation layer is tmct (@polycode-projects/the-mechanical-code-talker);
seonix supplies the graph through tmct's provider seam.
SEONIX_STORE / SEONIX_GRAPH_FORMAT
Two opt-in/opt-out environment knobs, orthogonal to everything above:
SEONIX_STORE=sqlite turns on the resident node:sqlite store as a derived,
rebuildable companion to graph.json (rebuild it explicitly with cli
store_rebuild; unset, nothing sqlite loads). The variable must be exported
(export SEONIX_STORE=sqlite) — a plain shell assignment is invisible to the
seonix child process, and the JSON path runs instead. The index summary's
store: line states which mode actually ran (store: sqlite (graph.db +
graph.json) only when graph.db was built). SEONIX_GRAPH_FORMAT controls the
graph's on-disk wire format — the interned v2 form is the shipped default;
set SEONIX_GRAPH_FORMAT=1 to opt back out to the legacy v1 form.
Configuration (seonix.toml)
An optional seonix.toml at a repo (or estate) root steers indexing and
scoring. Absent file = shipped defaults, byte-for-byte today's behaviour.
seonix init (or seonix init --dotnet for an estate) seeds a commented
template — see templates/seonix.toml and
templates/seonix-dotnet.toml for the full annotated reference.
repositories— the repo set for an estate config: an inline array of paths, or a string naming a newline-delimited file (both resolved relative to the config file). Only consulted when no explicitrepo_path/repo_paths/multi_rootarg is given.out_root— where the merged graph artifact is written (default.).[index]languages— the AST languages to parse (javascript,typescript,python,csharp,java, …).exclude— additive glob excludes on top of.seonixignore(ordered; a leading!re-includes).secret_exclude— a hard, fail-safe-ON glob list (env files, keys, certs) that is never indexed and never grabbed as text, even as a text/structure source.history_depth— cap on the git-history pass (0 = skip entirely). Precedence: CLI arg > this key > default.include_text,include_structure,respect_gitignore,markdown_sections,vue— recognized and normalized, but not yet wired to a consumer (see "unwired keys" below).
[tune]— the locate/digest scoring knobs:score_gap_k,literal_mention(shipped defaulttrue),demote_non_prod,call_adjacency,impl_of_interface,beam_search(+beam_width),embed_rank,prose_layers.[tune.expansion]— graph-traversal expansion mode used by locate/the viewer's navigation:strategy(none|beam|spiral|ppr;noneis the byte-identical shipped default),nodes(breadth/token-budget cap),q(proportion of most-distinctive neighbours followed per step),depth(hop radius).[interfaces]— HTTP route↔handler edges the AST graph can't see on its own: aRoutenode per endpoint, linked to its handler by aservesedge, and (JS/TS + tree-sitter C#) an HTTP client call linked to the route it targets by acallsHttpedge. Off by default; turn it on withenabled = trueorSEONIX_INTERFACES=1(env wins). Tunable:frameworks(default["aspnet", "express"]),verbs,combine_controller_prefix,prefix_strip,match_threshold.[telemetry]—enabled = trueopts in (see Telemetry below).
Precedence, everywhere: explicit CLI arg > seonix.toml value > shipped
default. seonix cli … '{"config":false}' (always set on the bench path) skips
reading seonix.toml entirely, so a subject repo's config file can never shape
a benchmark arm. A seonix.toml key that parses and normalizes but has no
wired consumer yet (index.include_text, index.include_structure,
index.respect_gitignore, index.markdown_sections, index.vue) triggers a
one-time stderr warning per process rather than silently doing nothing.
seonix config --effective [--repo <abs>] prints the fully merged config plus,
per key, which source won it.
Telemetry
Opt-in, off by default, and the OFF path is byte-identical to no telemetry
existing at all — no file, no stdout/stderr change. Turn it on with
SEONIX_TELEMETRY=1 (env always wins both directions — SEONIX_TELEMETRY=0
force-disables even if the toml turns it on) or [telemetry] enabled = true in
seonix.toml.
When enabled, each cli invocation (digest / locate / the generic tool
fallback) appends fire-and-forget JSONL lines to
<repo>/.seonix/seonix-<uuidv7>.log — one line per query, schema-versioned
(v: 1) with an id (the invocation id, reusable across a host's own trace
via SEONIX_INVOCATION_ID), a monotonic seq, and per-surface fields
(query, response, perf, cost). A structural redactor drops any
text/content/snippet field at any depth and truncates long strings —
telemetry never records file contents, only ids/paths/scores/sizes/counts.
Writes are swallowed on error: telemetry can never throw, block a query, or
touch stdout (stdout is the MCP transport / the digest injected into the
no-MCP arm).
Visualise
seonix viz --focus <Symbol> --out graph.htmlRenders a focused, portable Cytoscape sub-graph of the typed code-map, the same
view that runs live on the website. By default it also writes a code-browser.html
and a timeline.html beside it with a working header nav; pass --graph-only
for just the single self-contained graph file.
Chat
seonix chat [--repo /abs/path]seonix chat opens an interactive natural-language client over the code graph. The
conversation engine is tmct (@polycode-projects/the-mechanical-code-talker), a
deterministic, no-LLM chatbot that seonix runs over its own graph through tmct's
provider seam. Every answer is grounded in the graph or an honest miss. With no
--repo the target defaults to the git root, so running it from a nested package dir
still covers the whole repo.
Ask structural questions in plain English — "which modules import walk.mjs", "where
is buildContextBundle defined", "when did walk.mjs change" — and route slash-commands
(/context, /impact, /callers, /arch, …) to the graph tools. On top of the
query layer, tmct adds conversational memory, provenance and trust scoring,
teach-and-recall, and multi-strategy interpretation. See the tmct docs for the full
command set.
seonix supplies the graph; tmct owns the conversation. The old opt-in
--with-claude/--with-copilot fallback is gone — tmct is no-LLM by contract.
Languages
Python (stdlib ast), JS/TS (the TypeScript compiler API, including CommonJS
require/module.exports), C# (Roslyn, with a tree-sitter fallback) and Java
(JavaParser, with a tree-sitter fallback), all merged into the one typed graph.
The C# Roslyn helper's source ships in the package under roslyn/. If the .NET
SDK is on your PATH, seonix builds it the first time it indexes a C# repo, one
build, cached after that. No dotnet on PATH just means it uses the tree-sitter
fallback, silently, no error. Run npm run build:roslyn yourself first if you
want the dll ready ahead of time, e.g. in a Docker image build. The Java
JavaParser helper is a jar built from the repository's java/ directory (mvn
package) and is not included in the npm package, so a package install indexes
Java with the tree-sitter fallback.
Licence & safety
AGPL-3.0-only. The network clause is intended for a hosted MCP tool, so that
hosted derivatives stay open. The seonix: ontology vocabulary is CC-BY-4.0 (NOTICE
credits SEON + FAMIX). SEONIX never writes to your global agent config and never
phones home; everything it writes lives under .seonix/.
