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.
- package/README.md +56 -0
- package/dist/index.js +242 -0
- 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
|
+
}
|