claudinho-nvim 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +56 -0
  2. package/dist/index.js +242 -0
  3. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # claudinho-nvim
2
+
3
+ Most plugins for claude code in neovim are really just one single thing, they run claude code inside of a terminal session. I find this bad for two of reasons personally:
4
+ - The terminal buffer is not as capable as actual terminal, or even as tmux
5
+ - Having to write some command to mention code in Claude Code is not a very good expereicen, and then you need yet another shortcut
6
+
7
+ So this is Opus's attempt at a better experience for this. It includes a Claude Code plugin that can be installed, and it will communicate with the neovim plugin. The neovim plugin will exist as a tool that Claude can access whatever is the state of your neovim. That means, splits, buffers open, selection, draft code, etc.
8
+
9
+ This is borrowing from the idea that [amp.nvim](https://github.com/sourcegraph/amp.nvim) implements, which I believe is now discontinued, and it was used as a basis for this project.
10
+
11
+ ## Tools
12
+
13
+ | Tool | Description |
14
+ |------|-------------|
15
+ | `nvim_get_buffers` | List all open buffers with path, modified status, and which is current |
16
+ | `nvim_get_buffer_content` | Read lines from a buffer (defaults to current buffer) |
17
+ | `nvim_get_cursor` | Get cursor position, current file, and Vim mode |
18
+ | `nvim_get_window_layout` | Get all windows/splits with their buffers and dimensions |
19
+ | `nvim_get_visual_selection` | Get the last visual selection text and range |
20
+ | `nvim_get_diagnostics` | Get LSP diagnostics, optionally filtered by buffer or severity |
21
+
22
+ ## Setup
23
+
24
+ Install dependencies and build:
25
+
26
+ ```sh
27
+ pnpm install
28
+ pnpm build
29
+ ```
30
+
31
+ Add to your Claude Code settings (`~/.claude.json`):
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "claudinho": {
37
+ "command": "npx -y",
38
+ "args": ["claudinho-nvim"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ Claude Code must be launched from a Neovim `:terminal` so the `$NVIM` environment variable is available.
45
+
46
+ ## Development
47
+
48
+ ```sh
49
+ pnpm start # run from source with tsx
50
+ pnpm build # bundle into dist/index.js
51
+ pnpm typecheck # type check without emitting
52
+ pnpm lint # run biome linter
53
+ pnpm lint:fix # auto-fix lint issues
54
+ pnpm version # bump version from changesets
55
+ pnpm release # build and publish to npm
56
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,242 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ import { attach } from "neovim";
6
+ //#region src/nvim-client.ts
7
+ let client = null;
8
+ function getNvimClient() {
9
+ if (client) return client;
10
+ const socketPath = process.env.NVIM;
11
+ if (!socketPath) throw new Error("$NVIM environment variable not set. Claude Code must be running inside a Neovim terminal.");
12
+ client = attach({ socket: socketPath });
13
+ return client;
14
+ }
15
+ //#endregion
16
+ //#region src/tools/buffers.ts
17
+ function registerBufferTools(server) {
18
+ server.registerTool("nvim_get_buffers", { description: "List all open buffers in Neovim with metadata" }, async () => {
19
+ const nvim = getNvimClient();
20
+ const buffers = await nvim.buffers;
21
+ const currentBuf = await nvim.buffer;
22
+ const result = await Promise.all(buffers.map(async (buf) => {
23
+ const [name, loaded, modified] = await Promise.all([
24
+ buf.name,
25
+ buf.loaded,
26
+ nvim.call("getbufvar", [buf.id, "&modified"])
27
+ ]);
28
+ return {
29
+ bufnr: buf.id,
30
+ name,
31
+ loaded,
32
+ modified: modified === 1,
33
+ current: buf.id === currentBuf.id
34
+ };
35
+ }));
36
+ return { content: [{
37
+ type: "text",
38
+ text: JSON.stringify(result, null, 2)
39
+ }] };
40
+ });
41
+ server.registerTool("nvim_get_buffer_content", {
42
+ description: "Get the content of a buffer. Defaults to the current buffer.",
43
+ inputSchema: {
44
+ bufnr: z.number().optional().describe("Buffer number. Omit for current buffer."),
45
+ startLine: z.number().optional().describe("Start line (0-indexed, inclusive)"),
46
+ endLine: z.number().optional().describe("End line (0-indexed, exclusive)")
47
+ }
48
+ }, async ({ bufnr, startLine, endLine }) => {
49
+ const nvim = getNvimClient();
50
+ const buf = bufnr ? (await nvim.buffers).find((b) => b.id === bufnr) : await nvim.buffer;
51
+ if (!buf) return {
52
+ content: [{
53
+ type: "text",
54
+ text: `Buffer ${bufnr} not found`
55
+ }],
56
+ isError: true
57
+ };
58
+ const [name, lines] = await Promise.all([buf.name, buf.getLines({
59
+ start: startLine ?? 0,
60
+ end: endLine ?? -1,
61
+ strictIndexing: false
62
+ })]);
63
+ return { content: [{
64
+ type: "text",
65
+ text: JSON.stringify({
66
+ bufnr: buf.id,
67
+ name,
68
+ totalLines: lines.length,
69
+ lines
70
+ }, null, 2)
71
+ }] };
72
+ });
73
+ }
74
+ //#endregion
75
+ //#region src/tools/cursor.ts
76
+ function registerCursorTools(server) {
77
+ server.registerTool("nvim_get_cursor", { description: "Get the current cursor position, file, and Vim mode" }, async () => {
78
+ const nvim = getNvimClient();
79
+ const [win, mode] = await Promise.all([nvim.window, nvim.mode]);
80
+ const [buf, cursor] = await Promise.all([win.buffer, win.cursor]);
81
+ const name = await buf.name;
82
+ return { content: [{
83
+ type: "text",
84
+ text: JSON.stringify({
85
+ bufnr: buf.id,
86
+ file: name,
87
+ line: cursor[0],
88
+ col: cursor[1],
89
+ mode: mode.mode
90
+ }, null, 2)
91
+ }] };
92
+ });
93
+ server.registerTool("nvim_get_window_layout", { description: "Get all windows/splits with their buffers, cursor positions, and dimensions" }, async () => {
94
+ const windows = await getNvimClient().windows;
95
+ const result = await Promise.all(windows.map(async (win) => {
96
+ const [buf, cursor, height, width, position] = await Promise.all([
97
+ win.buffer,
98
+ win.cursor,
99
+ win.height,
100
+ win.width,
101
+ win.position
102
+ ]);
103
+ const name = await buf.name;
104
+ return {
105
+ windowId: win.id,
106
+ bufnr: buf.id,
107
+ file: name,
108
+ cursor: {
109
+ line: cursor[0],
110
+ col: cursor[1]
111
+ },
112
+ height,
113
+ width,
114
+ position: {
115
+ row: position[0],
116
+ col: position[1]
117
+ }
118
+ };
119
+ }));
120
+ return { content: [{
121
+ type: "text",
122
+ text: JSON.stringify(result, null, 2)
123
+ }] };
124
+ });
125
+ }
126
+ //#endregion
127
+ //#region src/tools/diagnostics.ts
128
+ const SEVERITY_MAP = {
129
+ error: 1,
130
+ warn: 2,
131
+ info: 3,
132
+ hint: 4
133
+ };
134
+ const SEVERITY_NAMES = [
135
+ "",
136
+ "error",
137
+ "warn",
138
+ "info",
139
+ "hint"
140
+ ];
141
+ const GET_DIAGNOSTICS_LUA = `
142
+ local bufnr, severity = ...
143
+ if bufnr == 0 then bufnr = nil end
144
+ if severity == 0 then severity = nil end
145
+ local opts = {}
146
+ if severity then opts.severity = severity end
147
+ local diags = vim.diagnostic.get(bufnr, opts)
148
+ local results = {}
149
+ for _, d in ipairs(diags) do
150
+ table.insert(results, {
151
+ bufnr = d.bufnr,
152
+ file = vim.api.nvim_buf_get_name(d.bufnr),
153
+ line = d.lnum + 1,
154
+ col = d.col + 1,
155
+ message = d.message,
156
+ severity = d.severity,
157
+ source = d.source or "",
158
+ })
159
+ end
160
+ return vim.json.encode(results)
161
+ `;
162
+ function registerDiagnosticsTools(server) {
163
+ server.registerTool("nvim_get_diagnostics", {
164
+ description: "Get LSP diagnostics (errors, warnings, etc.) for a buffer or all buffers",
165
+ inputSchema: {
166
+ bufnr: z.number().optional().describe("Buffer number. Omit for all buffers."),
167
+ severity: z.enum([
168
+ "error",
169
+ "warn",
170
+ "info",
171
+ "hint"
172
+ ]).optional().describe("Filter by severity level")
173
+ }
174
+ }, async ({ bufnr, severity }) => {
175
+ const nvim = getNvimClient();
176
+ const severityNum = severity ? SEVERITY_MAP[severity] : 0;
177
+ const raw = await nvim.lua(GET_DIAGNOSTICS_LUA, [bufnr ?? 0, severityNum]);
178
+ const result = JSON.parse(raw).map((d) => ({
179
+ ...d,
180
+ severity: SEVERITY_NAMES[d.severity] || String(d.severity)
181
+ }));
182
+ return { content: [{
183
+ type: "text",
184
+ text: JSON.stringify(result, null, 2)
185
+ }] };
186
+ });
187
+ }
188
+ //#endregion
189
+ //#region src/tools/selection.ts
190
+ const GET_SELECTION_LUA = `
191
+ local buf = vim.api.nvim_get_current_buf()
192
+ local start_pos = vim.fn.getpos("'<")
193
+ local end_pos = vim.fn.getpos("'>")
194
+
195
+ if start_pos[2] == 0 and end_pos[2] == 0 then
196
+ return vim.json.encode({ hasSelection = false })
197
+ end
198
+
199
+ local start_line = start_pos[2] - 1
200
+ local start_col = start_pos[3] - 1
201
+ local end_line = end_pos[2] - 1
202
+ local end_col = end_pos[3]
203
+
204
+ local lines = vim.api.nvim_buf_get_text(buf, start_line, start_col, end_line, end_col, {})
205
+ local name = vim.api.nvim_buf_get_name(buf)
206
+
207
+ return vim.json.encode({
208
+ hasSelection = true,
209
+ bufnr = buf,
210
+ file = name,
211
+ startLine = start_pos[2],
212
+ startCol = start_pos[3],
213
+ endLine = end_pos[2],
214
+ endCol = end_pos[3],
215
+ text = table.concat(lines, "\\n"),
216
+ })
217
+ `;
218
+ function registerSelectionTools(server) {
219
+ server.registerTool("nvim_get_visual_selection", { description: "Get the current or last visual selection text and range" }, async () => {
220
+ const raw = await getNvimClient().lua(GET_SELECTION_LUA, []);
221
+ const result = JSON.parse(raw);
222
+ return { content: [{
223
+ type: "text",
224
+ text: JSON.stringify(result, null, 2)
225
+ }] };
226
+ });
227
+ }
228
+ //#endregion
229
+ //#region src/index.ts
230
+ const server = new McpServer({
231
+ name: "claudinho",
232
+ version: "0.1.0"
233
+ });
234
+ registerBufferTools(server);
235
+ registerCursorTools(server);
236
+ registerSelectionTools(server);
237
+ registerDiagnosticsTools(server);
238
+ const transport = new StdioServerTransport();
239
+ await server.connect(transport);
240
+ console.error("claudinho MCP server running");
241
+ //#endregion
242
+ export {};
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "claudinho-nvim",
3
+ "version": "0.2.0",
4
+ "description": "Claude Code MCP plugin for Neovim integration",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/gabrielmfern/claudinho.git"
9
+ },
10
+ "homepage": "https://github.com/gabrielmfern/claudinho",
11
+ "bugs": {
12
+ "url": "https://github.com/gabrielmfern/claudinho/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "bin": "dist/index.js",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "1.29.0",
26
+ "neovim": "5.4.0",
27
+ "zod": "3.25.76"
28
+ },
29
+ "devDependencies": {
30
+ "@biomejs/biome": "2.4.10",
31
+ "@changesets/changelog-github": "0.6.0",
32
+ "@changesets/cli": "2.30.0",
33
+ "@types/node": "25.5.2",
34
+ "tsdown": "0.21.7",
35
+ "tsx": "4.21.0",
36
+ "typescript": "6.0.2"
37
+ },
38
+ "scripts": {
39
+ "start": "tsx src/index.ts",
40
+ "build": "tsdown",
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "biome check src",
43
+ "lint:fix": "biome check --write src",
44
+ "version": "changeset version",
45
+ "release": "pnpm build && changeset publish"
46
+ }
47
+ }