Skip to content
napbatPublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

semctl

semctl is the command-line client for the semctx code-intelligence server — semantic + exact code search and a precise symbol/call/value-flow graph over your repositories, exposed both as a CLI and as an MCP stdio server for AI coding tools (Claude Code, Codex).

It's a thin HTTP+JSON client: all indexing and retrieval happen server-side, so semctl links no engine code and has an entirely public dependency graph.

Install

The one-liner downloads the prebuilt binary from the latest GitHub release, verifies its checksum, and hands off to semctl install — which puts the binary on your PATH and wires it into the AI tools it finds. No Rust toolchain needed.

Linux / macOS (shell):

curl -fsSL https://raw.githubusercontent.com/napbat/semctl/main/install-cli.sh | sh

Windows (PowerShell):

irm https://raw.githubusercontent.com/napbat/semctl/main/install-cli.ps1 | iex

From source

For a platform without a prebuilt binary — or if you'd rather build it — install with cargo (needs a Rust toolchain):

cargo install --git https://github.com/napbat/semctl --locked semctl

Then sign in and index a repo:

semctl install      # interactive: integrates Claude Code, Codex CLI, and Oh My Pi
semctl auth login   # OIDC device-code sign-in
semctl index        # register + sync the current repo for indexing

Commands

Command What it does
semctl search <query> Cross-domain semantic search.
semctl index Register the current Git worktree (or selected non-Git folder) and sync its files.
semctl graph … Exact code intelligence plus symbol search, type hierarchy, call graphs, cycles, unused, duplicates, and rich outlines.
semctl edit … Grammar-native rename/delete/body/insertion planning and verified local apply/undo.
semctl files … File catalog (tree / filtered list) and revision-pinned line/byte source reads.
semctl inspect … Projects/domains plus visible codebases and the effective server/tenant/index/graph context.
semctl mcp Run as an MCP stdio server (launched by the host, not by hand).
semctl daemon status / stop Report or end the shared local daemon that serves the MCP sessions.
semctl install Add/remove the editor/agent integrations.
semctl uninstall Reverse install: unwire the tools, remove from PATH, delete the binary (--purge also drops config + credentials).
semctl upgrade / update Update the binary and refresh each installed editor/agent integration.
semctl auth login / logout / whoami / tenants Account & session.

semctl index uploads the requested files in batches. A failed batch does not stop the other batches. A batch that failed because of a connection, gateway, or server error is sent once more, alone, after the other batches. Then the command completes the sync for the files that the server received. It exits with an error that names the files that the server did not receive. Run semctl index again to upload them. The MCP auto-index logs the same files, and semctl daemon status shows them for each checkout.

Grammar-native editing

The server's edit operations are planners: they resolve one qualified or positional symbol through semctx's grammar graph, reparse and re-resolve the proposed postimages, then return a JSON WorkspaceEditPlan. They never write a repository. For example:

semctl edit rename --path src/lib.rs --line 42 --column 8 NewName > plan.json
semctl edit apply plan.json
semctl edit undo <plan-id>

Apply and undo use one local transaction engine. Apply checks the checkout identity and current server graph generation. It verifies every planned path, preimage, edit range, and expected postimage before staging the complete plan. Open directory handles keep mutations bound to the approved checkout. Reapplying an unchanged completed plan is idempotent. Undo requires all current files to match the retained postimage hashes.

Each replacement verifies the bytes it displaced. Installation and rollback never replace a target created during the swap. Edits require filesystem hard-link support; semctl checks this before changing source files. A conflict retains private .semctl-*.edit recovery directories next to affected files and reports their paths. Inspect retained files before retrying. Directory renames can change those reported locations. Multiple file replacements are not globally atomic. Semctl detects writes observed before cleanup, but a writer can still change a displaced inode through an old open descriptor after the final check. Stop such writers when full isolation is required.

Undo history stays in the local config directory's edit-history/ folder. A plan-supplied formatter runs only with explicit --run-formatter approval. Formatter commands, arguments, and paths are bounded to the approved plan. Each formatter receives only a planned postimage through standard input. Semctl captures the formatted output before it writes any file. Prettier runs without project configuration, EditorConfig settings, or configured plugins. These restrictions keep formatter writes within the approved plan.

The MCP surface exposes edits as immediate actions. rename_symbol, safe_delete_symbol, replace_symbol_body, insert_before_symbol, and insert_after_symbol ask the server for its internal plan and consume it inside the same call; callers never shuttle plan JSON through MCP. Each action is write/destructive in the host approval UI, returns an editId, and can be reversed with the hash-guarded undo_edit action. Read-only additions include list_codebases, current_context, read_source, search_symbols, type_hierarchy, call_graph, cycles, unused, and duplicates; existing reference/search/outline tools expose the richer occurrence, scope, and nesting options instead of gaining aliases.

Every read-only MCP tool call ends within one deadline. The default is 25 seconds, below the 30 seconds after which Oh My Pi ends an MCP request. Set SEMCTX_TOOL_DEADLINE_SECS to change it. A value from 5 to 600 applies as given, a smaller or larger value is held to that range, and 0 or an unreadable value means the default. Inside the deadline, a request that meets a gateway failure or a refused connection is sent once more after one second, and only when it only reads. A call that has no answer in time returns an error result that names the cause and the next step. A retrieval call also waits at most 5 seconds for a first index before it reports that the index is still running. The edit actions have no deadline: a limit that fired after the files changed would report a failed edit that was applied.

The index_codebase tool does not wait for the first index. It registers the codebase, starts the scan and upload in the background, and returns at once with the codebase id and the path. The sync_status tool then reports the first-index phase: registering, syncing, embedding, ready, or failed. While a sync runs, it also reports a current sync: line with the latest step, such as the number of uploaded files. semctl daemon status reports the same phase and step for each checkout.

Coming from the old semctx CLI

The first semctl install automatically retires a previous semctx install: it migrates your config + credentials to ~/.config/semctl, removes the old binary and its PATH entry, and re-points the Claude Code / Codex plugin at semctl's source (the plugin itself stays the same). A cargo install'd semctx is left for you to remove with cargo uninstall semctx-cli.

Shared local daemon

semctl mcp can serve its session from one shared local process instead of serving it in the process the host started. One daemon serves every MCP session of one operating-system user and one configuration directory. It keeps one filesystem watcher, one reconcile queue, one scan cache, and one HTTP connection pool per checkout, however many sessions use that checkout. Nothing changes at the MCP tool boundary: the host still launches semctl mcp, and that process becomes a byte pump between the host and the daemon.

SEMCTX_MCP_DAEMON selects the role of a semctl mcp invocation:

Value What it does
auto (default) Attach to the daemon, start one when none is running, and serve the session in this process when that fails.
off Serve the session in this process, as every earlier version did.
require Attach to the daemon, start one when none is running, and exit with status 1 when that fails.

On Windows, a daemon that semctl mcp starts must belong to no job object before it opens the shared endpoint. The client asks Windows to release the daemon from the client's jobs. A job that forbids breakaway blocks that request, and an ancestor job can keep the daemon even when the request succeeds. The daemon rejects membership in any job, including a job that would not end the daemon, because Windows offers no simple way to read an ancestor job's limits. When the daemon still belongs to a job, auto serves this session in its own process and require reports the startup failure. Clients in a job can still attach to an existing daemon. Closing one client's job must not end other clients' sessions.

Some hosts run every process in a job that forbids breakaway. To share one daemon there, run semctl daemon run yourself as the same user and with the same configuration as the clients, from a process whose lifetime you control. A daemon started by hand skips the job check, and clients attach to it.

Two commands inspect and end a running daemon:

semctl daemon status        # version, pid, uptime, sessions, permits, checkouts, their first-index phase, and the progress of a running sync
semctl daemon status --json # the same facts as one JSON object
semctl daemon stop          # end every session and exit

Both exit with status 1 and report no daemon is running for this configuration when no daemon answers. A daemon also exits by itself after ten minutes with no session.

A running daemon keeps its loaded executable while it serves a session. It checks its executable file every 30 seconds. After an on-disk update, such as semctl upgrade, it exits as soon as its last session ends, and the next semctl mcp starts a daemon from the new executable. Existing sessions keep the old version until the host reconnects them. To switch at once, run semctl daemon stop and reconnect the host's MCP sessions.

On Windows, the daemon and its Git probes use CREATE_NO_WINDOW: redirecting standard streams alone does not prevent Windows from allocating a console for each probe. Git source-policy checks skip the separate exclusion-path query when the complete resolved configuration proves core.excludesFile is absent. Explicit values still use Git's path expansion and participate in policy fingerprints; ignore-source identity and byte checks remain in place.

Env What it sets
SEMCTX_DAEMON_IDLE_SECS Seconds with no session before the daemon exits. Default 600; 0 exits with the last session.
SEMCTX_DAEMON_AUTO_UPDATE 0 keeps an idle daemon running after its executable is updated on disk. Default on.
SEMCTX_DAEMON_SCAN_PERMITS Concurrent checkout scans. Default: half the available CPUs, at least 2 and at most 8.
SEMCTX_DAEMON_UPLOAD_PERMITS Concurrent upload requests across every checkout. Default 8.
SEMCTX_DAEMON_REMOTE_PERMITS Concurrent remote requests from tool calls. Default 64.

Each permit override is clamped to the range 1 to 1024.

A daemon inherits the environment of the client that started it, minus the seven per-session variables (SEMCTX_TOKEN, SEMCTX_SERVER, SEMCTX_TENANT, SEMCTX_CODEBASE, SEMCTX_MCP_RESYNC_SECS, SEMCTX_TOOL_DEADLINE_SECS, SEMCTX_MCP_UPDATE_CHECK). Every later session of that daemon therefore runs with the first client's proxy settings, its PATH, which decides which formatter an edit runs, and its Git configuration variables, which decide which rules a source policy reads. Run semctl daemon stop to change any of them: the next semctl mcp client starts a daemon with its own environment. The daemon's working directory is not inherited; each session carries its own in its attach request.

The endpoint and the daemon log live in the same place on each platform:

Platform Endpoint Log
Linux $XDG_RUNTIME_DIR/semctl/<id>.sock, else /tmp/semctl-<uid>/<id>.sock <same directory>/<id>.log
macOS $TMPDIR/semctl/<id>.sock, else /tmp/semctl-<uid>/<id>.sock <same directory>/<id>.log
Windows \\.\pipe\semctl-<id> %LOCALAPPDATA%\semctl\daemon-<id>.log

<id> is a hash of the configuration directory, the build version, and the user. A new version therefore starts its own daemon and never attaches to a daemon of another version.

On Unix the runtime directory and the socket are owner-only, semctl refuses a runtime directory it does not own or one that carries group or other permission bits, and the daemon checks each peer's user id before it reads a handshake. On Windows the pipe carries a security descriptor that grants the current user's SID alone, the first instance of the pipe name is the election, and the client opens the pipe at identification level so the server cannot impersonate it. Secrets travel only inside the handshake body: no token appears in an endpoint name, a command argument, a log line, or status output.

The full shared-daemon lifecycle suite is Unix-only. Native Windows regressions cover rejected startup in breakaway-denied and nested jobs, standalone fallback, attachment to an existing daemon, console-free Git probes, and source-policy syncing. Startup that survives its client's job needs a process outside Cargo's job, so that case runs only from the test binary directly (see tests/daemon_windows_jobs.rs). Windows named-pipe startup, MCP initialization, tool listing/calls, and client EOF have also been smoke-tested. The macOS daemon path remains compile-checked without runtime measurements.

Updating

semctl upgrade

For a binary installed the standard way (the install script), upgrade downloads the latest release and self-replaces in place. If semctl was installed with cargo install, it's cargo-managed — upgrade detects that and tells you to re-run cargo install … --force instead of clobbering it. In both cases, upgrade refreshes each installed integration through the same update path as semctl install. This refresh can update hooks, skills, and MCP configuration. The refresh also runs when the binary is already current. It does not install or remove an integration.

Release maintainers must run an asset rebuild from the existing version tag. The workflow refuses a rebuild when the selected commit differs from that tag. Publication verifies the tag again before it uploads assets.

Configuration

Config lives at ~/.config/semctl/config.toml (credentials in a sibling 0600 credentials.json); XDG_CONFIG_HOME overrides the base. A non-secret installation-id in the same directory lets the server distinguish concurrent local checkouts without receiving a host name or local path. Local codebases are mapped by canonical checkout path, not guessed from Git remote or folder name: servers without project-key support keep separate checkout slugs. Servers with project-key support decide project membership from complete remote identities. Ordinary checkout paths retain their existing source IDs. Native paths with invalid UTF-8, the Unicode replacement character, or a literal Unix backslash receive lossless source IDs and cache keys. Old ambiguous cache entries do not authorize a checkout or reuse its single-manifest codebase. These paths require indexing again because the old encoding could identify another checkout. An older ~/.config/semctx/ install remains readable. Legacy credentials migrate when their JWT issuer matches the configured server's advertised authority. Credentials without that evidence require a new login.

Each stored login records its resource server, authentication authority, and generation. Requests refuse credentials from another server. A server override therefore needs a login for that server, or an explicit SEMCTX_TOKEN for the invocation. Refresh tokens remain bound to the authority that issued them. Login, logout, refresh, migration, and purge share one state lock. The non-secret .semctl.state.lock and .semctl.state.generation files remain in the config base after purge. They prevent pending operations from restoring cleared login state. Blank server flags and environment values fall through to the next configured source.

sync-cache/ stores non-secret per-checkout hashes and content-filter decisions. Every scan reads and hashes current file bytes. Matching hashes permit reuse of filter decisions. Equal file sizes and timestamps never prove unchanged content. Source contents are never written to this cache, and semctl uninstall --purge removes it with the rest of the semctl config directory.

Use .semctlignore to exclude repository files from indexing. The legacy .semctxignore name remains supported. Both files use Git ignore syntax. A sync aborts when any effective exclusion source cannot be read or changes during the scan. This includes Git global exclusions, included configuration, and worktree info/exclude files.

Setting Flag Env
Server base URL --server SEMCTX_SERVER
Active tenant --tenant SEMCTX_TENANT
Active codebase --codebase SEMCTX_CODEBASE

Login clears the previous account's tenant and automatically selects a sole membership. Tenant selection applies only while that login remains current. If a saved tenant later becomes invalid, an authenticated request repairs it the same way and retries once. Explicit --tenant / SEMCTX_TENANT overrides are never replaced; with multiple memberships, run semctl auth tenants in an interactive terminal to pick by number, or choose non-interactively with semctl auth tenants --switch <slug>.

With nothing configured, semctl talks to the hosted server at https://semctx.napbat.ca.

Agent integration data flow

The MCP server sends retrieval queries and opted-in repository content to the configured semctx server. When trusted Claude/Codex hooks or the native Oh My Pi extension are enabled, retrieval-shaped user prompts are also sent to semctl hook for a bounded, best-effort candidate search. The OMP extension runs in-process but delegates retrieval, authentication, and nudge state to the semctl subprocess; it does not make network requests itself. Set SEMCTX_HOOK_DISABLE=1 to disable all hook behavior or SEMCTX_NUDGE_DISABLE=1 to disable only shell-search reminders.

Observed semctx use cools further reminders for the active prompt. A new prompt or context reset re-arms guidance immediately; within one prompt, three consecutive broad built-in searches re-arm it by default. Tune that streak with SEMCTX_NUDGE_REARM_BROAD, and tune the existing escalation ladder with SEMCTX_NUDGE_GRACE, SEMCTX_NUDGE_COOLDOWN, and SEMCTX_NUDGE_MAX (default cap: four successful nudges per clear/compact segment).

Development

Run mise install to install the repository's Rust and Bun toolchains. Install Python 3, a POSIX shell, and PowerShell 7 for the bootstrap tests. Run prek install once to install the pre-commit hook. The complete local and CI gate is:

prek run --all-files

The configured hooks run:

cargo fmt --check
cargo clippy --workspace --all-targets --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --document-private-items --locked
cargo test --workspace --locked
mise exec -c "bun test plugins/semctx/adapters/omp/index.test.ts"
python3 tests/bootstrap_installers.py

The installer test runs the sh and pwsh installer cases for each shell that is installed. It skips a missing shell and reports the skip. CI runs sh on Linux and pwsh on Windows. Use --shell all to require both shells.

plugins/semctx/ is the shared plugin root for every supported coding agent. Its skills/codebase-retrieval/ has one physical source. Claude/Codex consume the shared hook manifest; OMP loads adapters/omp/index.ts, which maps native lifecycle events onto the same semctl hook wire contract. See plugins/semctx/README.md before adding another agent integration. Do not copy or symlink shared skills.

The deterministic hook and OMP-extension cases run under the normal Rust and Bun test commands.

tests/hosts/ runs each supported coding agent (OMP, Claude Code, and Codex) with the plugin from this checkout and the semctl that Cargo builds from it. Each host talks to a local scripted model, so a session is deterministic and offline. semctl runs logged out against an unreachable server. The tests read its decisions from the SEMCTX_HOOK_TRACE file. One scenario list runs on every host. Each host adapter in tests/hosts/hosts.ts maps the scenarios to that host's native tool calls. These tests are slow, so CI does not run them. Run them locally:

SEMCTX_HOST_INTEGRATION=1 mise exec -c "bun test tests/hosts"

Without SEMCTX_HOSTS, the suite skips a host whose CLI is not on PATH. SEMCTX_HOSTS=omp,claude selects hosts, and a selected host whose CLI is missing fails. SEMCTX_HOST_COMMAND_<ID> replaces a host's launch command, for example SEMCTX_HOST_COMMAND_CODEX="bun x @openai/codex".

To add a host, add an adapter to HOST_ADAPTERS in tests/hosts/hosts.ts. If the host speaks a new model API, also add a wire format to tests/hosts/model_server.ts.

SEMCTX_HOOK_TRACE=<file> also helps with host debugging. Each semctl hook run appends one JSON line with the event, host, tool name, PreToolUse decision, and whether the hook emitted context. A trace line never contains prompt text, tool input, or context text.

To record fresh Codex, Claude, and OMP tool-event traces for the model-level golden prompts:

python3 scripts/run_skill_evals.py --host all

CI (.github/workflows/ci.yml) installs the current stable Rust toolchain and runs these checks. Use the same Rust version for local checks. New stable releases can add Clippy lints. Clippy uses pedantic. Warnings, including broken doc links, fail the build.

Releases are cut by bumping the version: set a new version in Cargo.toml (e.g. 0.1.0 → 0.1.1) and push to main. .github/workflows/release.yml detects the new version, builds the per-platform binaries, and publishes them as release v<version> after the shared CI gate passes for the same commit. The release tag points to that commit. semctl upgrade then picks it up. Pushes that do not change the version are a no-op.

License

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages