Skip to main content
Glama
README.md
# DevSentinel šŸ›”ļø

[![CI](https://github.com/pbs002-s/devsentinel/actions/workflows/ci.yml/badge.svg)](https://github.com/pbs002-s/devsentinel/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![MCP Server](https://img.shields.io/badge/MCP-Compliant-8A2BE2.svg)](https://modelcontextprotocol.io)
[![Node](https://img.shields.io/badge/Node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.6-blue.svg)](https://www.typescriptlang.org)

> **The flight controller for coding agents.**
> An MCP server that covers the three fundamental blind spots a coding agent cannot see on its own.

šŸ“– **[Explore the Interactive Landing Page & Visual Docs](https://pbs002-s.github.io/devsentinel/)** *(or open [`docs/index.html`](docs/index.html) locally)*

---

| Engine | Question it answers | Tools |
| :--- | :--- | :--- |
| **šŸ’„ BlastRadius** | *"What breaks across the repo if I change this?"* | `analyze_blast_radius`, `detect_breaking_changes` |
| **šŸ‘ļø PixelGuard** | *"Does the webpage actually look right?"* | `capture_ui_state`, `diff_ui_visuals` |
| **ā±ļø TimeMachine** | *"Can I undo this experiment safely?"* | `create_checkpoint`, `list_checkpoints`, `diff_checkpoint`, `rollback_checkpoint` |

Eight tools, three workflow prompts and two resources over one stdio connection. Built with Node.js + TypeScript (ESM) and `zod`-validated inputs; every tool returns deterministic, structured JSON.

## How it fits together

```
Claude Desktop / Claude Code
        │  JSON-RPC over stdio
        ā–¼
  devsentinel  ──▶ BlastRadius  ──▶ your source tree      (Babel AST + Python scanner)
                ā”œā–¶ PixelGuard   ──▶ headless Chromium     (Playwright + pngjs)
                └▶ TimeMachine  ──▶ .devsentinel/          (sha1 snapshots, git read-only)
```

One process, no daemon, no port, no network calls of its own. Chromium is loaded lazily — the six
non-visual tools never launch a browser. Nothing is written outside the workspace you point it at,
except the screenshots and checkpoints under `.devsentinel/`.

## Install

```bash
npm install
npm run build
npx playwright install chromium   # only needed for capture_ui_state
npm test
```

Add `.devsentinel/` to your `.gitignore` — that is where checkpoints and screenshots land.

## Register the server

### Claude Desktop

Copy `claude_desktop_config.example.json` into your Claude Desktop config and fix the two absolute paths:

- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "devsentinel": {
      "command": "node",
      "args": ["C:/absolute/path/to/devsentinel/dist/src/index.js"],
      "env": { "DEVSENTINEL_WORKSPACE": "C:/absolute/path/to/your/project" }
    }
  }
}
```

Restart Claude Desktop afterwards.

### Claude Code

```bash
claude mcp add devsentinel -e DEVSENTINEL_WORKSPACE=/path/to/your/project -- node /abs/path/to/devsentinel/dist/src/index.js
```

Use `--scope project` to commit the server into `.mcp.json` for the whole team, or `--scope user` to make it available in every project.

### Checking the wiring

```bash
node dist/src/index.js --version   # prints the version
node dist/src/index.js --help      # prints the workspace it resolved
```

Every tool also accepts an explicit `workspaceDir`, which wins over `DEVSENTINEL_WORKSPACE`, which in turn wins over the server's working directory. Human-readable output goes to stderr; stdout carries only the JSON-RPC stream.

## Tools

### `analyze_blast_radius({ filePath, symbolName?, workspaceDir? })`

Parses every `.ts/.tsx/.js/.jsx/.mjs/.cjs` file with Babel and every `.py` file with an import scanner, resolves each import specifier to a real file on disk, and reports who depends on the target:

```json
{
  "target": { "file": "src/math.ts", "symbol": "add", "exists": true },
  "scannedFiles": 11,
  "consumers": [
    { "file": "src/app.ts", "isTest": false, "importedAs": ["add"], "importKind": "esm", "referenceLines": [2, 3], "reexports": false },
    { "file": "test/math.test.ts", "isTest": true, "importedAs": ["add"], "importKind": "esm", "referenceLines": [2], "reexports": false }
  ],
  "testFiles": ["test/math.test.ts"],
  "transitiveConsumers": ["src/via-barrel.ts"],
  "riskScore": 34,
  "riskLevel": "medium",
  "summary": "src/math.ts:add is used by 4 file(s) (1 test file(s), 1 transitive). Risk: medium."
}
```

Handles ESM imports, `require()`, dynamic `import()`, barrel re-exports, and Python `import` / `from ... import`. `testFiles` is the list to run after the edit. Because specifiers are resolved to real files, a change to `add` never drags in the callers of `subtract` — something a text search cannot distinguish.

**`tsconfig` path aliases.** `tsconfig.json`, or `jsconfig.json` if there is no
`tsconfig.json`, is read for `compilerOptions.baseUrl` and `compilerOptions.paths`, so
`@/components/Button`, `~utils` and `@models/user` resolve to real files instead of
being dropped as package imports. Comments and trailing commas are handled, because
that is what these files actually contain; a malformed config degrades to the built-in
`~/` and `@/` handling rather than failing the scan. Bare specifiers are only tried
against `baseUrl` when `baseUrl` is actually set, so a `node_modules` import costs no
wasted lookups.

**Nested barrels.** Re-export chains are followed up to five hops with cycle detection,
so a symbol reached through `src/index.ts` → `src/feature/index.ts` →
`src/feature/impl.ts` still names the components that only ever import the outermost
barrel. Those arrive in `transitiveConsumers`, never duplicated into `consumers`. Pass
`includeTransitive: false` to skip the walk.

### `detect_breaking_changes({ filePath, newCode, workspaceDir? })`

Diffs the exported contract of the file on disk against the code you are about to write. Findings are graded:

- **breaking** — removed export, new required parameter, dropped parameter, changed parameter or return type, removed or newly-required interface/class member.
- **warning** — renamed parameter, new required member on an interface, changed type of an exported constant.
- **additive** — new export, new optional parameter or member.

`isBreaking` is true when the breaking list is non-empty, so an agent can gate on one field.

### `capture_ui_state({ url, selector?, viewport?, outputPath?, waitMs?, workspaceDir? })`

Loads the URL in headless Chromium, writes a PNG, and runs an in-page audit that reports:

- **horizontal-overflow** — the page scrolls sideways, with the offending elements and their widths.
- **clipped-content** — text cut off by an `overflow: hidden` or `text-overflow: ellipsis` box.
- **overlapping-text** — two unrelated text elements whose boxes overlap by more than 25% of the smaller one.
- **console-error**, **page-error**, **failed-request** — anything the browser complained about while loading.

`file://` URLs work, so a static HTML file can be audited without a dev server.

### `diff_ui_visuals({ beforeImagePath, afterImagePath, diffImagePath?, threshold?, workspaceDir? })`

Compares two PNGs pixel by pixel and writes a red-on-grey overlay of what moved. Returns `changedPixels`, `diffPercentage`, whether the dimensions changed, and a verdict (`identical` / `minor` / `significant` / `major`).

### `create_checkpoint({ label?, description?, workspaceDir? })`

Copies the current working state into `.devsentinel/checkpoints/<id>/`, with a SHA-1 per file. It never touches the git index, working tree, or stash, so it is safe to use mid-rebase or in a directory that is not a repo at all. In a git repo, `git ls-files` is used to pick files, so `.gitignore` is respected; otherwise a walk with a built-in ignore list is used. Files over 5MB are skipped, and a workspace over 250MB is refused.

### `list_checkpoints({ workspaceDir? })`

Checkpoints newest first, each with the files modified, added and deleted since it was taken.

### `diff_checkpoint({ checkpointId, filePath?, workspaceDir? })`

What changed in the workspace since a checkpoint, as unified line diffs — the question `list_checkpoints` leaves open once it has told you a file drifted.

```json
{
  "checkpointId": "20260909T101500000Z-0001",
  "label": "before auth refactor",
  "files": [
    {
      "path": "src/session.ts",
      "status": "modified",
      "additions": 1,
      "deletions": 0,
      "diff": "@@ -12,6 +12,7 @@\n export function createSession(\n   userId: string,\n+  ttlSeconds: number,\n ) {",
      "binary": false,
      "truncated": false
    }
  ],
  "totalAdditions": 1,
  "totalDeletions": 0,
  "summary": "1 file(s) changed since \"before auth refactor\": +1 / -0 line(s)."
}
```

`status` is `modified`, `added` or `deleted`. Pass `filePath` to narrow the report to one file. Binary files come back flagged with no text diff rather than as mojibake, and a change wider than 2000 lines per side degrades to a whole-block replace with `truncated: true`.

### `rollback_checkpoint({ checkpointId, deleteNewFiles?, workspaceDir? })`

Restores every file in the checkpoint and deletes files created since (pass `deleteNewFiles: false` to keep them). A safety checkpoint of the current state is always taken first and returned as `safetyCheckpointId`, so a rollback is itself undoable.

## Prompts

Tool descriptions say what each tool does but not what order to call them in, and
order is the whole point: a checkpoint taken after the edit is worthless, and a
baseline screenshot taken after the markup changed is not a baseline. Three MCP
prompts encode the sequences, and show up as slash commands in clients that support
them.

| Prompt | Arguments | What it drives |
| :--- | :--- | :--- |
| `safe_refactor_check` | `filePath`, `symbolName?` | Blast radius → checkpoint → contract check on the proposed code → edit → the tests the scan named |
| `visual_regression_audit` | `url`, `changeDescription?`, `selector?` | Baseline capture before the edit, re-capture after, pixel diff, and only the issues that are new |
| `experiment_sandbox` | `goal`, `rollbackIf?` | Checkpoint, a rollback trigger stated before any code is written, then diff and decide |

## Resources

| URI | Contents |
| :--- | :--- |
| `devsentinel://checkpoints/recent` | The checkpoint log with per-entry drift — the `list_checkpoints` payload, attachable as context |
| `devsentinel://workspace/config` | Resolved workspace root and where it came from, git branch and HEAD, and the tsconfig `baseUrl` and `paths` aliases imports resolve through |

Read `devsentinel://workspace/config` first when a scan comes back empty. That is
almost always the server resolving a different directory than you assume, and this
resource says which one.

## Agent configuration

[`CLAUDE.md`](CLAUDE.md) and [`.cursorrules`](.cursorrules) ship in this repository.
Copy the one matching your tool into your own project so its agent reaches for these
tools at the right moment without being asked each time.

## Suggested workflow

```
create_checkpoint          →  before a risky multi-file edit
analyze_blast_radius       →  who calls the thing you are about to change
detect_breaking_changes    →  does your new code break that contract
   ... make the edit, run the tests it named ...
capture_ui_state           →  screenshot + layout audit after a UI change
diff_ui_visuals            →  against the screenshot taken before
diff_checkpoint            →  what the experiment actually changed, line by line
rollback_checkpoint        →  if any of the above went badly
```

A prompt that puts it to work:

```
Before you touch src/auth/session.ts, run analyze_blast_radius on the
createSession symbol and tell me which tests I need to run.
```

## Project layout

```
CLAUDE.md         agent protocol for Claude Code and Claude Desktop
.cursorrules      the same protocol as Cursor IDE rules
src/
  index.ts        MCP server: tools, prompts, resources, CLI flags, shutdown
  blastradius.ts  Babel AST scanning, import resolution, signature diffing
  pixelguard.ts   Playwright capture, in-page audit, PNG diffing
  timemachine.ts  content-hashed snapshots, drift detection, unified diffs, rollback
  workspace.ts    file walking, git-aware listing, shared path helpers
test/             one suite per engine, plus an end-to-end MCP client
docs/index.html   the landing page (GitHub Pages)
```

## Development

```bash
npm run build      # tsc to dist/
npm run typecheck  # tsc --noEmit
npm test           # build, then node:test across all suites
npm start          # run the server on stdio directly
npm run clean      # remove dist/
```

**60 tests, 13 suites**, run on Node 18/20/22 in CI. Coverage by design:

| Suite | What it proves |
| :--- | :--- |
| `blastradius.test.ts` | ESM/CJS/Python fixtures, symbol precision, `tsconfig` alias resolution, nested and cyclic barrel chains, reference lines, every breaking-change class |
| `pixelguard.test.ts` | A real headless-Chromium audit of a deliberately broken page; pixel diffs with a known 4.00% changed area |
| `timemachine.test.ts` | Checkpoint → drift → rollback round trips, safety checkpoints, self-exclusion of the store, unified diffs across modified/added/deleted/binary files |
| `server.test.ts` | An MCP client speaking JSON-RPC over stdio: all eight tools, prompt rendering and tool ordering, resource reads, and error passthrough |

The Chromium tests skip themselves with a message if the browser is not installed, so `npm test` stays green on a machine without it.

## Production notes

- **Failure mode.** Tool errors come back as MCP tool errors with a usable message; the server stays alive. `SIGINT`/`SIGTERM` close the transport cleanly.
- **Input validation.** Every argument is `zod`-parsed at the boundary. Checkpoint paths are workspace-relative and rejected if they escape the workspace root.
- **Data safety.** `rollback_checkpoint` always snapshots the current state first. Nothing deletes a checkpoint automatically.
- **Privacy.** No telemetry, no network calls except the pages `capture_ui_state` is explicitly asked to open.
- **Cost.** A blast-radius scan is file I/O plus a Babel parse per file; there is no model call anywhere in this server.

### Known limits

- Import resolution covers relative, absolute, `~/` and `@/` specifiers and `tsconfig`/`jsconfig` `paths` aliases. `extends` chains are not followed; only the root config is read.
- Barrel re-export chains are followed five hops.
- Checkpoint diffs use an LCS line diff over the changed region only. A change wider than 2000 lines per side comes back as a block replace with `truncated: true`.
- Python analysis is regex-based, not a full parse: it covers module-level `def`, `class` and import statements.
- Overlap detection compares at most 300 text elements per page.
- Checkpoints copy file contents; they are not deltas, so a very large workspace is refused rather than slowly copied.

## Contributing

Issues and pull requests welcome. Keep `npm test` green, and add a test alongside any new behaviour — each engine has a suite to extend.

## License

MIT Ā© [Pritom Biswas](https://github.com/pbs002-s)