Skip to main content
Glama
README.md
# swiss-knife

[![test](https://github.com/vinmh/swiss-knife/actions/workflows/test.yml/badge.svg)](https://github.com/vinmh/swiss-knife/actions/workflows/test.yml)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
![python 3.13+](https://img.shields.io/badge/python-3.13%2B-blue)

Structured-output CLI and MCP tools that cut agent token usage and tool-call count.

Coding agents burn tokens on chains of small shell calls: three git commands to prepare a commit, a full file read to learn one function signature, a `pkill -f` that matches the agent's own shell. swiss-knife bundles these recurring operations into single deterministic commands that print compact JSON, so an agent (or a human) gets the answer in one call — and gets the same answer every time.

## Installation

```sh
uv tool install git+https://github.com/vinmh/swiss-knife
# or run without installing:
uvx --from git+https://github.com/vinmh/swiss-knife swiss-knife --help
```

## Quick start

```sh
$ swiss-knife file-outline src/app.py
{
  "file": "src/app.py",
  "language": "python",
  "symbols": [
    {"kind": "class", "name": "App", "signature": "class App:", "line": 10, ...}
  ]
}
```

## Output contract

Every subcommand prints a single JSON object to stdout and exits with:

| Code | Meaning |
|------|---------|
| `0`  | success |
| `1`  | negative outcome, JSON still valid — test failures, wait timeout, non-zero wrapped command, kill survivors |
| `2`  | unrecoverable error; JSON has an `"error"` key |

## Commands

### `file-outline <path>`

Tree-sitter extraction of a source file's symbols — functions, classes, methods, types — with signatures and line numbers, no bodies. Read the structure of a 2000-line file for a few hundred tokens. Languages: Python, JavaScript/TypeScript, Go, Rust, C.

```sh
swiss-knife file-outline src/server.go
swiss-knife file-outline weird-extension.txt --language python
```

### `commit-context`

Staged diff, branch/tracking info, untracked files, and recent log in one call — everything needed to write a commit message or PR description.

```sh
swiss-knife commit-context --repo . -n 20
```

### `test-summary <command...>`

Wraps a test runner and parses its output into `{passed, failed, skipped, failures: [{test, file, line, message}]}`. Framework auto-detected from the project; override with `--framework`. Supports pytest, Jest, Vitest, `go test`, `cargo test`, RSpec, and Unity (C).

```sh
swiss-knife test-summary uv run pytest
swiss-knife test-summary --framework jest -- npx jest
```

### `proc find|stop <pattern>`

Process matching against full command lines (like `pgrep -f`/`pkill -f`) that can never match its own machinery: the invoking shell chain and any command line containing a swiss-knife invocation are always excluded. `find` reports listening ports and process age; `stop` signals matches and confirms each outcome (`terminated`, `survived`, `vanished`, `permission_denied`, `signal_unsupported`).

```sh
swiss-knife proc find "vite dev"
swiss-knife proc stop "node server.js" --signal TERM --wait 5
```

### `wait-for`

Bounded condition polling — every wait has a timeout, so it can never loop forever. Conditions (all must hold simultaneously): `--port`, `--file`, `--touched` (mtime at/after command start), `--gone`/`--present` (process cmdline pattern), `--http` (URL answers).

```sh
swiss-knife wait-for --port 5173 --http http://localhost:5173/health -t 30
swiss-knife wait-for --gone "vite dev" -t 10
```

### `run-logged [--] <command...>`

Runs a command with stdout+stderr captured to a log file, avoiding three ad-hoc invocation hazards: pipes filling up and blocking, fatal errors hidden by quiet flags, and `producer | grep -q` dying of SIGPIPE under pipefail. Returns exit code, log tail, and optional `--grep` matches over the full log. On timeout the whole process tree is terminated. Put `--` before commands that take their own flags.

```sh
swiss-knife run-logged --timeout 300 --grep "ERROR|WARN" -- npm run build
```

Logs default to a `swiss-knife` directory under the system temp dir; override with `SWISS_KNIFE_LOG_DIR`.

### `session-brief`

One call bundling the context an agent needs to resume work in a project directory: git state, `.claude/HANDOFF.md`, parsed `PLAN.md` phase statuses, the Claude Code project memory index, the latest [pi](https://github.com/badlogic/pi-mono) session for the directory, and any `AGENTS.md` files from the directory, its parents, and pi's global agent dir.

```sh
swiss-knife session-brief --cwd ~/code/myproject
```

### `mcp`

Serves every command above as an MCP tool over stdio (see next section).

## MCP server

Register with your MCP client — for Claude Code:

```sh
claude mcp add swiss-knife -- swiss-knife mcp
```

or in `.mcp.json`:

```json
{
  "mcpServers": {
    "swiss-knife": {
      "command": "swiss-knife",
      "args": ["mcp"]
    }
  }
}
```

Tools: `commit_context`, `file_outline`, `test_summary`, `proc_find`, `proc_stop`, `wait_for_condition`, `run_logged_command`, `session_brief`. Each returns the same JSON the CLI prints.

**Caveat:** the stdio server handles one call at a time, and `wait_for_condition` / `run_logged_command` block it for up to their full timeout with no progress notifications. If your MCP client's per-call timeout is shorter, the call fails client-side while the operation keeps running. Prefer the CLI for long builds and servers; the MCP tools shine for the fast, bounded ones.

## Teaching your agent to use it

Agents only reach for these tools if their instructions say to. Paste this into your `CLAUDE.md` (or `AGENTS.md`) and adjust to taste:

```markdown
## swiss-knife

CLI toolkit for common operations — prefer these over chains of separate tool calls.
All commands print JSON to stdout. Exit 0 = success, 1 = negative outcome
(test failures, wait-for timeout, non-zero command, kill survivors), 2 = error
(JSON has an "error" key).

- `swiss-knife file-outline <path>` — file structure (functions, classes,
  signatures, line numbers) without reading the full file. Use before reading
  any file over ~200 lines, then read only the relevant ranges.
  Supports Python, JS/TS, Go, Rust, C.
- `swiss-knife commit-context --repo <path>` — staged diff, branch info,
  untracked files, and recent log in one call. Use instead of separate git
  commands when preparing commits or PRs.
- `swiss-knife test-summary <command...>` — run tests, get structured JSON
  (passed/failed/skipped/failures with file:line). Auto-detects the framework;
  override with `--framework`.
- `swiss-knife proc find|stop <pattern>` — find or terminate processes by
  command-line pattern. ALWAYS use instead of `pkill -f`/`pgrep -f`: it can
  never match the invoking shell, reports listening ports and process age,
  and confirms each kill.
- `swiss-knife wait-for [--port N] [--file P] [--gone PAT] [--http URL] -t SECS`
  — poll until conditions hold. Use instead of hand-rolled `until ...; sleep`
  loops; it always terminates.
- `swiss-knife run-logged [--timeout S] [--grep RE] [--] <command...>` — run a
  command with stdout+stderr captured to a log file; returns exit code, tail,
  and grep matches. Use for builds/servers/long commands instead of pipes.
  Put `--` before commands with their own flags.
- `swiss-knife session-brief` — git state, HANDOFF.md, PLAN.md phase statuses,
  and agent-harness session context in one call. Run at session start or after
  compaction instead of separate git/file reads.

If the swiss-knife MCP server is registered, prefer its tools (`file_outline`,
`commit_context`, `test_summary`, `proc_find`, `proc_stop`, `session_brief`)
over the shell for these fast, bounded calls. Use the CLI instead for anything
long-running — `run-logged` builds/servers and `wait-for` with a long timeout —
because those MCP tools block the server and can exceed the client's per-call
timeout while the operation keeps running.
```

## Platform support

| Platform | Status |
|----------|--------|
| Linux    | supported, CI-tested |
| macOS    | supported, CI-tested |
| Windows  | supported, CI-tested |

Platform caveats:

- **macOS**: listing another user's listening ports requires root; without it those processes report `listen_ports: []`.
- **Windows**: `proc stop --signal INT` reports `signal_unsupported` (console interrupts cannot be delivered to arbitrary processes); use `TERM` or `KILL`.

## Development

```sh
git clone https://github.com/vinmh/swiss-knife
cd swiss-knife
uv sync
uv run pytest
```

Layout: each command is a package under `src/swiss_knife/` with two layers — `core.py` (pure functions returning dicts, no I/O to stdout, no Click) and a thin wrapper in `cli.py` that prints JSON and maps outcomes to exit codes. The MCP server in `mcp_server/server.py` wraps the same core functions.

### Contributing

- Tests are mandatory for any behaviour change. `proc`, `wait-for`, and `run-logged` tests spawn real processes and sockets — no mocks. Use `sys.executable -c` for spawned test processes (portable, and the script text stays visible in the cmdline).
- Keep JSON output schemas stable; they are the public API. New keys are fine, renames are not.
- Type hints on all public function signatures.
- tree-sitter queries live in `.scm` files under `file_outline/queries/`, not inline strings.
- Run `uv run pytest -q` before submitting; CI runs the suite on Linux, macOS, and Windows.

## License

[MIT](LICENSE)