diff --git a/.claude/skills/create-or-update-pr/SKILL.md b/.claude/skills/create-or-update-pr/SKILL.md new file mode 100644 index 0000000..53bbaf9 --- /dev/null +++ b/.claude/skills/create-or-update-pr/SKILL.md @@ -0,0 +1,197 @@ +--- +name: create-or-update-pr +description: Create a pull request for the current branch, or update the existing one if it already exists. Regenerates the title (Conventional Commits format) and the body (Objective + What was done) from the actual changes, in English. INVOKE when the user asks to "open a PR", "create a PR", "update the PR", "update PR title/description", "push and PR", or similar. Works even when a PR already exists — it edits rather than re-opens. +--- + +# Create or Update Pull Request + +This skill creates a new PR for the current branch, or updates the existing PR if one is already open. In both cases, it regenerates the **title** (Conventional Commits) and **body** (Objective + What was done) from the actual diff and commit history. All output is in **English**, regardless of the conversation language. + +## When to use + +Invoke this skill whenever the user wants to: + +- Open a PR for the current branch +- Update an existing PR's title or description +- Refresh the PR after new commits on the branch + +Do **not** invoke for: merging, reviewing, closing PRs, or operations against a different branch. + +## Preconditions + +Before doing any network action, verify: + +1. The repository has a GitHub remote (`git remote -v` shows github.com). If not, abort and tell the user. +2. The current branch is **not** the main/master branch. Detect the main branch with `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`. If the user is on it, abort and ask them to switch. +3. The `gh` CLI is authenticated (`gh auth status`). If not, abort with instructions. +4. There are commits on the current branch beyond the merge base with the default branch. If zero commits ahead, abort — nothing to PR. + +## Steps + +### 1. Gather branch + repo context (parallel) + +Run these commands in parallel: + +```sh +git branch --show-current # current branch +gh repo view --json defaultBranchRef -q .defaultBranchRef.name # default/base branch +gh pr list --head "" --state open --json number,title,body,url --limit 1 +git status --porcelain # uncommitted changes check +git rev-parse --abbrev-ref "@{upstream}" 2>&1 # is branch pushed? +``` + +### 2. Gather change context (parallel) + +Using the base branch discovered above: + +```sh +git log ..HEAD --oneline # commit list +git diff ...HEAD --stat # files changed + insertions/deletions +git diff ...HEAD # full diff (truncate if huge) +``` + +If the diff is very large (>1000 lines), prefer `--stat` plus reading key files directly over dumping everything. + +### 3. Handle uncommitted changes + +If `git status --porcelain` is non-empty: + +- **Do NOT** auto-commit. Ask the user: "You have uncommitted changes in . Commit them first, or should I proceed with just what's already committed?" +- Only continue once the user answers. + +### 4. Handle unpushed branch + +If the branch has no upstream (step 1 `rev-parse` errored with `no upstream`): + +- Push with `git push -u origin ` **only** if the user's request implies pushing (e.g. "open a PR", "create PR"). If they said "update the PR description" on an unpushed branch, that's inconsistent — ask. + +### 5. Draft the title (Conventional Commits) + +Based on the actual diff, draft a title following [Conventional Commits](https://www.conventionalcommits.org/): + +``` +()?: +``` + +Pick the `` that best matches the changes: + +| Type | When | +|---|---| +| `feat` | New user-facing capability | +| `fix` | Bug fix | +| `docs` | Documentation only | +| `refactor` | Code restructuring without behavior change | +| `perf` | Performance improvement | +| `test` | Adding/updating tests | +| `build` | Build system, deps, bundler config | +| `ci` | CI/CD workflow changes | +| `chore` | Tooling, repo plumbing, no user-facing impact | +| `style` | Formatting only (whitespace, semicolons) | +| `revert` | Reverting a prior commit | + +Scope rules: + +- Use the affected package/app name when changes are localized (e.g. `feat(mcp-json-diff): …`, `docs(playground): …`). +- If changes span multiple packages, omit the scope or use `repo`/`workspace`. +- Keep the summary under **72 characters** total. + +If the branch mixes multiple types (e.g. feat + docs), pick the dominant one by line count / impact. + +### 6. Draft the body (Objective + What was done) + +Always use this exact structure, in English: + +```md +## Objective + +<1-3 sentences stating WHY this change exists — the goal or problem it solves. Infer from commit messages, diff context, and any linked issues. If nothing is clear, say "Implements " or "Fixes ".> + +## What was done + +- +- +- + +## Test plan + +- [ ] +- [ ] +``` + +Body rules: + +- Keep bullets concrete and tied to real changes in the diff. No generic filler. +- Reference file paths using backticks (e.g. `` `tools/mcp-json-diff/src/index.ts` ``). +- If changes include a new public API, add a `## API changes` section with before/after. +- If the branch includes a breaking change, add `## Breaking changes` and prefix the title type with `!` (e.g. `feat!: …`). +- Do **not** add a summary of what the PR "intends to do" — describe what it actually does. +- Do **not** include marketing language, emojis (unless the project uses them), or Claude attribution. + +### 7. Create or update the PR + +**Use HEREDOCs** to pass multi-line bodies to `gh` to avoid shell escaping issues. + +#### If no open PR exists: + +```sh +gh pr create \ + --base \ + --head \ + --title "" \ + --body "$(cat <<'EOF' +## Objective + +… + +## What was done + +- … + +## Test plan + +- [ ] … +EOF +)" +``` + +#### If an open PR exists (from step 1): + +```sh +gh pr edit \ + --title "" \ + --body "$(cat <<'EOF' +… +EOF +)" +``` + +> Use `gh pr edit` — never close and re-open. Editing preserves PR number, comments, and review history. + +### 8. Report back + +Output to the user: + +- PR URL (from `gh pr create` output or `gh pr view --json url -q .url`) +- Whether it was **created** or **updated** +- The final title and a one-line summary of what the body contains + +## Important rules + +- **Never force-push** unless the user explicitly asks. +- **Never skip hooks** (`--no-verify`) unless the user explicitly asks. +- **Never commit or amend** as part of this skill — if there are uncommitted changes, ask the user. +- **Do not add** `Co-Authored-By: Claude …` or any AI-attribution footer to the PR body. This is a PR description, not a commit message. +- **Do not** include the "Generated with Claude Code" footer either — the user explicitly wants the PR body to look human-authored. +- Respect the user's repo conventions: if `git log` shows a consistent pattern (e.g. all commits use `chore:` for tooling), follow that pattern. + +## Edge cases + +| Situation | Action | +|---|---| +| PR exists but is **closed/merged** | Ask the user whether to open a new PR or reopen the old one. Default: open a new PR. | +| Branch is the default branch | Abort with an explanation. | +| No commits ahead of base | Abort — "There's nothing to PR." | +| Uncommitted changes | Ask the user — do not auto-commit. | +| Branch has no remote tracking | Push with `-u` only if the intent is to create a PR. | +| Multiple open PRs for same branch | Use the most recent; warn the user about the others. | +| Large diff (>1000 lines) | Use `--stat` + targeted reads instead of full diff; note in response that the body was generated from a summary view. | diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..4796766 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Objective + + + +## What was done + + + +- + +## Test plan + + + +- [ ] diff --git a/apps/example/README.md b/apps/example/README.md index 149beb3..fda8cc2 100644 --- a/apps/example/README.md +++ b/apps/example/README.md @@ -1,3 +1,46 @@ # Example -A simple node project that runs the "json-difference" lib. +Runnable Node scripts that exercise the [`json-difference`](../../libs/json-difference) library — one hand-crafted tiny case and one stress test against real fixtures. + +Useful as: +- A smoke test when iterating on the library +- Reference for how to import and call `getDiff` in a Node program + +## Scripts + +| File | Purpose | +|---|---| +| [`src/simple.ts`](./src/simple.ts) | Diffs two small inline objects, prints delta + execution time | +| [`src/stress.ts`](./src/stress.ts) | Diffs two large JSON fixtures from [`src/assets/`](./src/assets/), prints delta + execution time | + +## Running + +From the repository root: + +```sh +# Run both scripts (simple + stress) in parallel via tsx +yarn nx run example:test + +# Compile to dist/ (TypeScript + copy JSON fixtures) +yarn nx build example +``` + +Or run a single script directly with `tsx`: + +```sh +yarn tsx apps/example/src/simple.ts +yarn tsx apps/example/src/stress.ts +``` + +## Example output + +``` +diff {"added":[["special2",false]],"removed":[["special",true]],"edited":[["color/color1","black","red"],["color/color2","brown","blue"]]} +Execution time: 1ms +``` + +## Adding a case + +1. Drop a new `.ts` file under [`src/`](./src/) +2. Import from `json-difference` (already wired via the monorepo tsconfig paths) +3. Optionally register it in [`project.json`](./project.json) so it runs under `yarn nx run example:test` diff --git a/apps/playground/README.md b/apps/playground/README.md index 7b994da..3973d9c 100644 --- a/apps/playground/README.md +++ b/apps/playground/README.md @@ -1,3 +1,37 @@ -# GH Playground demo +# Playground -The playground is a live demo to help you better understand how the "json-difference" tool works on web application. +Interactive web demo of [`json-difference`](../../libs/json-difference). Paste two JSON payloads and see the computed delta in real time. + +🔗 **Live:** + +## Stack + +- [React 19](https://react.dev) +- [Chakra UI v3](https://chakra-ui.com) +- [Monaco Editor](https://microsoft.github.io/monaco-editor/) (via `react-monaco-editor`) for the JSON input panes +- [Vite](https://vitejs.dev) build +- Consumes the local `json-difference` library + +## Running locally + +From the repository root: + +```sh +yarn install +yarn nx serve playground +``` + +The app starts on `http://localhost:4200` by default. + +## Other targets + +```sh +yarn nx build playground # production build (outputs to apps/playground/dist) +yarn nx preview playground # preview the production build +yarn nx lint playground # lint +yarn nx type-check playground # type-check +``` + +## Deployment + +Deployed to GitHub Pages on each release. See the root [CD workflow](../../.github/workflows/cd.yml). diff --git a/libs/json-difference-cli/README.md b/libs/json-difference-cli/README.md new file mode 100644 index 0000000..488fefa --- /dev/null +++ b/libs/json-difference-cli/README.md @@ -0,0 +1,95 @@ +# json-difference-cli + +[![npm version](http://img.shields.io/npm/v/json-difference-cli.svg?style=flat)](https://www.npmjs.com/package/json-difference-cli) +[![Total Downloads](https://img.shields.io/npm/dt/json-difference-cli.svg)](https://www.npmjs.com/package/json-difference-cli) +[![MIT License](https://img.shields.io/npm/l/json-difference-cli.svg?style=flat)](https://github.com/lukascivil/jsondiffer/blob/master/LICENSE) + +Command-line wrapper around [`json-difference`](https://www.npmjs.com/package/json-difference). Computes the delta between two JSON strings and prints it to stdout. + +## Installation + +Global (recommended for CLI use): + +```sh +npm install -g json-difference-cli +# or +yarn global add json-difference-cli +``` + +One-off: + +```sh +npx json-difference-cli -o "{}" -m '{"a":1}' +``` + +## Usage + +```sh +jd -o -m +``` + +### Options + +| Flag | Alias | Required | Description | +|---|---|---|---| +| `-o` | `--original` | yes | Original (old) JSON string | +| `-m` | `--modified` | yes | Modified (new) JSON string | + +### Examples + +```sh +# Simple key change +jd -o '{"a":"b"}' -m '{"a":"c"}' +# delta ---> { added: [], removed: [], edited: [ [ 'a', 'b', 'c' ] ] } + +# Added / removed keys +jd -o '{"a":1}' -m '{"b":2}' +# delta ---> { added: [["b", 2]], removed: [["a", 1]], edited: [] } + +# Root type change +jd -o '{}' -m '[]' +# delta ---> { added: [], removed: [], edited: [["__root__", {}, []]] } +``` + +### Shell quoting tip + +JSON uses double quotes, so on macOS/Linux wrap the argument in single quotes: + +```sh +jd -o '{"foo":"bar"}' -m '{"foo":"baz"}' +``` + +On Windows PowerShell, escape the inner quotes or use single quotes: + +```powershell +jd -o '{\"foo\":\"bar\"}' -m '{\"foo\":\"baz\"}' +``` + +## Output format + +The tool prints the delta returned by [`getDiff`](https://www.npmjs.com/package/json-difference): + +```ts +{ + added: [[path, value], ...], + removed: [[path, value], ...], + edited: [[path, oldValue, newValue], ...] +} +``` + +See the [`json-difference` README](https://www.npmjs.com/package/json-difference) for the full path format and semantics. + +## Related packages + +- [`json-difference`](https://www.npmjs.com/package/json-difference) — the underlying library (use directly in code) +- [`mcp-json-diff`](https://github.com/lukascivil/json-difference/tree/master/tools/mcp-json-diff) — MCP server for AI agents + +## Links + +- Repository: +- Issues: +- [Changelog](./CHANGELOG.md) + +## License + +[MIT](../../LICENSE) © lukascivil diff --git a/libs/json-difference/README.md b/libs/json-difference/README.md new file mode 100644 index 0000000..535b755 --- /dev/null +++ b/libs/json-difference/README.md @@ -0,0 +1,142 @@ +# json-difference + +[![npm version](http://img.shields.io/npm/v/json-difference.svg?style=flat)](https://www.npmjs.com/package/json-difference) +[![Total Downloads](https://img.shields.io/npm/dt/json-difference.svg)](https://www.npmjs.com/package/json-difference) +[![MIT License](https://img.shields.io/npm/l/json-difference.svg?style=flat)](https://github.com/lukascivil/jsondiffer/blob/master/LICENSE) + +Computes the difference between two JSON structures and returns an intuitive path-based delta `{ added, removed, edited }`. Fast even on large payloads. + +🪶 Lightweight: **1.95 kB** (gzip: **0.79 kB**). Zero runtime dependencies. + +## Installation + +```sh +yarn add json-difference +# or +npm install json-difference +``` + +**Requirements:** Node.js `>=18.17`. + +## Usage + +```ts +import { getDiff } from 'json-difference' + +const oldStruct = { color: { color1: 'black', color2: 'brown' }, special: true } +const newStruct = { color: { color1: 'red', color2: 'blue' }, special2: false } + +getDiff(oldStruct, newStruct) +// { +// added: [["special2", false]], +// removed: [["special", true]], +// edited: [ +// ["color/color1", "black", "red"], +// ["color/color2", "brown", "blue"] +// ] +// } +``` + +### Lodash-style paths + +```ts +getDiff(oldStruct, newStruct, { isLodashLike: true }) +// edited: [["color.color1", "black", "red"], ...] +``` + +## API + +```ts +import { + getDiff, + getStructPaths, + getEditedPaths, + getPathsDiff, + // types + Delta, + EditedPath, + PathsDiff, + StructPaths, + JsonDiffOptions +} from 'json-difference' +``` + +### `getDiff(old, new, options?) => Delta` + +High-level entry point. Returns a full delta. + +```ts +interface Delta { + added: Array<[path: string, value: unknown]> + removed: Array<[path: string, value: unknown]> + edited: Array<[path: string, oldValue: unknown, newValue: unknown]> +} +``` + +Accepts objects, arrays, or JSON-encoded strings for both inputs. + +### `getStructPaths(json, isLodashLike?) => StructPaths` + +Flattens a JSON into a plain object mapping **dot/slash paths → leaf values**. Building block used internally by `getDiff`. + +```ts +getStructPaths({ a: { b: 1 }, c: [true] }) +// { "a/b": 1, "c/0[]": true } +``` + +### `getEditedPaths(oldPaths, newPaths) => EditedPath[]` + +Given two flattened path maps, returns only the paths whose value changed. + +### `getPathsDiff(pathsA, pathsB) => PathsDiff[]` + +Returns the paths present in `pathsA` but missing from `pathsB` (set difference). `getDiff` uses this with `(old, new)` for `removed` and with `(new, old)` for `added`. + +## Options + +| Option | Type | Default | Description | +|---|---|---|---| +| `isLodashLike` | `boolean` | `false` | Use lodash-style bracket notation (`a.b[0]`) instead of slash notation (`a/b/0[]`) | + +## Path format + +| Marker | Meaning | +|---|---| +| `__root__` | Root object/array was replaced entirely | +| `@{}` | Non-leaf node of type object | +| `@[]` | Non-leaf node of type array | + +### Reference operations + +| Original | Modified | Delta | +|---|---|---| +| `{}` | `[]` | `edited: [["__root__", {}, []]]` | +| `{"a":"b"}` | `{"a":"c"}` | `edited: [["a", "b", "c"]]` | +| `{}` | `{"a":"b"}` | `added: [["a", "b"]]` | +| `{"a":"b"}` | `{}` | `removed: [["a", "b"]]` | +| `[{}]` | `[]` | `removed: [["0[]", {}]]` | + +## Browser usage (CDN) + +```html + +``` + +## Related packages + +- [`json-difference-cli`](https://www.npmjs.com/package/json-difference-cli) — CLI wrapper (`jd -o … -m …`) +- [`mcp-json-diff`](https://github.com/lukascivil/json-difference/tree/master/tools/mcp-json-diff) — MCP server for AI agents (Claude, Cursor, etc.) + +## Links + +- Repository: +- Live playground: +- Issues: +- [Changelog](./CHANGELOG.md) + +## License + +[MIT](../../LICENSE) © lukascivil diff --git a/tools/mcp-json-diff/README.md b/tools/mcp-json-diff/README.md index 280e54a..b81187c 100644 --- a/tools/mcp-json-diff/README.md +++ b/tools/mcp-json-diff/README.md @@ -1,23 +1,28 @@ # mcp-json-diff -MCP server that exposes the `json-difference` library as tools for AI agents -(Claude Code, Claude Desktop, Cursor, etc.). Uses stdio transport. +An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that exposes the [`json-difference`](../../libs/json-difference) library as **tools** and **prompts** for AI agents (Claude Code, Claude Desktop, Cursor, Windsurf, …). Uses stdio transport. -## Tools +## Why + +LLMs are not deterministic at structured operations. Asking them to "compare these two JSONs" by hand works on small payloads but falls apart on large ones — missed fields, hallucinated paths, wrong diffs. This server lets the agent call the **real** `json-difference` lib and work from the exact delta. + +## Capabilities + +### Tools (agent-invoked, autonomous) | Tool | Description | |---|---| | `get_diff` | Full delta `{ added, removed, edited }` between two JSON structures | | `get_edited_paths` | Only the paths whose value changed | -| `get_struct_paths` | Flattens a single JSON into path → leaf-value map | -| `get_paths_diff` | Paths in A missing from B | +| `get_struct_paths` | Flatten a single JSON into path → leaf-value map | +| `get_paths_diff` | Paths present in A but missing from B | All JSON inputs accept objects, arrays, or JSON-encoded strings. Optional `isLodashLike: true` switches paths to bracket notation (`a[0].b`). -## Prompts +### Prompts (user-invoked, shortcut templates) -User-invocable templates (appear in Claude Desktop under `/` menu, in Claude Code as slash commands, etc.): +Pre-built, parameterized prompts that appear in MCP clients (attachment menu in Claude Desktop, `/mcp__*` slash commands in Claude Code, etc.): | Prompt | Arguments | Output | |---|---|---| @@ -26,32 +31,46 @@ User-invocable templates (appear in Claude Desktop under `/` menu, in Claude Cod | `explain-config-drift` | `baseline`, `actual` | Plain-language drift explanation + risk level | | `migration-guide` | `v1`, `v2` | Field change table + numbered steps + transform snippet | -Each prompt instructs the agent to call the `get_diff` tool internally, so you get deterministic delta + LLM narrative in one shot. +Each prompt instructs the agent to call `get_diff` internally — you get a deterministic delta plus an LLM narrative in one click. + +## Requirements + +- Node.js `>=18.17` +- A MCP-compatible client (Claude Desktop, Claude Code, Cursor, etc.) ## Running -Dev (via `tsx`, no build step): +Dev (no build step, via `tsx`): -```bash +```sh yarn nx run mcp-json-diff:serve ``` Production (compiled to `bin/`): -```bash +```sh yarn nx run mcp-json-diff:build node tools/mcp-json-diff/bin/src/index.js ``` -Browser inspector (opens MCP Inspector UI connected to this server): +Inspector UI (preview tools + prompts without an LLM): -```bash +```sh yarn nx run mcp-json-diff:test-browser ``` +Opens the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) in your browser, typically at `http://localhost:6274`. Great for verifying payloads before wiring a real client. + ## Client configuration -### Claude Code / Claude Desktop (`mcp.json` or `claude_desktop_config.json`) +### Claude Desktop + +Edit `claude_desktop_config.json`: + +- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json` +- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json` + +Using the compiled bin (most reliable — no `yarn`/`tsx` required at runtime): ```json { @@ -64,7 +83,7 @@ yarn nx run mcp-json-diff:test-browser } ``` -Or, running directly from TypeScript source with `tsx`: +Or running directly from TypeScript source with `tsx`: ```json { @@ -78,7 +97,23 @@ Or, running directly from TypeScript source with `tsx`: } ``` -## Example call +> Fully quit and reopen Claude Desktop (exit from the system tray on Windows / menu bar on macOS) — it only reads the config at startup. + +Prompts appear in the **attachment menu** (“+” next to the chat input), not as `/` slash commands. + +### Claude Code + +```sh +claude mcp add json-diff -- node h:/GitHub/jsondiffer/tools/mcp-json-diff/bin/src/index.js +``` + +Prompts become slash commands: `/mcp__json-diff__summarize-breaking-changes`, etc. + +### Cursor / Windsurf / others + +Same JSON shape — refer to the client’s MCP docs. The `command` + `args` + optional `cwd` fields are standard. + +## Example tool call Input: @@ -104,3 +139,34 @@ Output: } } ``` + +## Troubleshooting + +| Symptom | Likely cause / fix | +|---|---| +| Server not listed in Claude Desktop | Config JSON is invalid — validate with a JSON linter. Also fully quit and restart the app | +| `spawn yarn ENOENT` in logs | Claude Desktop doesn't inherit your shell PATH. Use `node` + absolute path to the built `bin/` file instead | +| Tools show up but prompts don't | Older client version — update. Prompts require a client that implements the full MCP spec | +| "Cannot find module" at startup | Run `yarn nx run mcp-json-diff:build` first if pointing at `bin/`, or install deps (`yarn install`) if using `tsx` | + +Logs: + +- **Claude Desktop (Windows):** `%APPDATA%\Claude\logs\mcp-server-json-diff.log` +- **Claude Desktop (macOS):** `~/Library/Logs/Claude/mcp-server-json-diff.log` + +## Development + +```sh +yarn nx run mcp-json-diff:type-check # TypeScript check +yarn nx run mcp-json-diff:build # Compile to bin/ +yarn nx run mcp-json-diff:serve # Run directly (tsx) +yarn nx run mcp-json-diff:test-browser # Open MCP Inspector UI +``` + +Source: [`src/index.ts`](./src/index.ts) — a single file registering tools and prompts against `@modelcontextprotocol/sdk`. + +## Links + +- Parent repo: +- MCP specification: +- MCP Inspector: