nvim-control 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '08c676f066c44ce25e9ad128f91a2dd67580a159562e2b2a4b3e63715fb09d09'
4
+ data.tar.gz: dd970515d69b6eaf5689b0b210e91727fa3e150e5cd0a83df2109a7a5fab412c
5
+ SHA512:
6
+ metadata.gz: 9159f7f3a2588fcd78e5b4adf116957929a0592d032b7ee646a194e69e91b612e454e3a82fc27d074152d38363cb1a1026001c40788243473e89304d798e3043
7
+ data.tar.gz: 6e4bd4ebac7638f01180fa1c871969046fd1f9e05e328fd3a15aeadefb3a1be537c86e2842131a7be720b6a66d8c92c7517921155bc6dead9607322dbba5b532
data/AGENTS.md ADDED
@@ -0,0 +1,87 @@
1
+ # AGENTS.md
2
+ ## Project overview
3
+ `nvim-control` is a Ruby gem that bridges running Neovim instances and agentic
4
+ coding tools. By default, it extracts live context from the editor via a Unix
5
+ socket connection and outputs JSON with cursor position, current file, visual
6
+ selection and diagnostics. It can also run explicit control actions, such as Ex
7
+ commands and key input, against the running editor.
8
+
9
+ **Version**: 1.0.0
10
+ **Ruby**: >= 4.0.0
11
+ **Dependencies**: `neovim` gem (~> 0.10.0), `logger` gem (~> 1.7)
12
+
13
+ ## Commands
14
+ - **Test all**: `bundle exec rspec`
15
+ - **Test unit**: `bundle exec rspec --exclude-pattern "spec/integration/**/*"`
16
+ - **Test integration**: `bundle exec rspec spec/integration/`
17
+ - **Test single file**: `bundle exec rspec path/to/spec.rb`
18
+ - **Lint**: `bundle exec rubocop`
19
+ - **Run tool**: `bin/nvim-control`
20
+ - **Build gem**: `gem build nvim-control.gemspec`
21
+
22
+ ## Architecture
23
+ ### Entry point
24
+ `bin/nvim-control` - CLI executable that dispatches to `NvimControl::CLI.run`
25
+ and prints JSON to stdout.
26
+
27
+ ### Core components (`lib/nvim_control/`)
28
+ - `cli.rb` - Parses arguments and dispatches to the read flow (`Fetcher`) or a
29
+ control action (`Controller`)
30
+ - `fetcher.rb` - Read flow with a static `fetch` method. Orchestrates data
31
+ extraction, handles all error types, and returns JSON responses
32
+ - `controller.rb` - Control flow that runs explicit Ex commands or key input
33
+ against Neovim and returns JSON with a `context_after` snapshot
34
+ - `connector.rb` - Manages Neovim socket connection using the `neovim` gem.
35
+ Reads socket path from `NVIM_CONTROL_SOCKET` env var or defaults to
36
+ `nvim-control.sock` in current directory
37
+ - `data_extractor.rb` - Static methods to extract cursor position, current file
38
+ path, visual selection (with text content), and diagnostics from Neovim
39
+ - `errors.rb` - Custom error classes: `ConnectionError` (socket failures) and
40
+ `OperationError` (Neovim operation failures)
41
+ - `version.rb` - Gem version constant
42
+
43
+ ### Data Flow
44
+ 1. User runs `nvim-control` CLI
45
+ 2. `CLI.run` dispatches to `Fetcher.fetch` (read) or `Controller.run` (control)
46
+ 3. `Connector` attaches to Neovim via Unix socket
47
+ 4. `DataExtractor` methods query Neovim for context data
48
+ 5. JSON output returned to stdout (or error JSON on failure)
49
+
50
+ ### JSON output format
51
+ Success:
52
+ ```json
53
+ {
54
+ "cursor": { "line": 43, "col": 3 },
55
+ "file": "/path/to/file.rb",
56
+ "selection": null,
57
+ "diagnostics": []
58
+ }
59
+ ```
60
+
61
+ Error:
62
+ ```json
63
+ {
64
+ "error": "Connection failed",
65
+ "details": "Failed to connect to Neovim socket: No such file"
66
+ }
67
+ ```
68
+
69
+ ## Code style
70
+ - Ruby 4.0, line length max 80 chars
71
+ - Use double quotes for strings, frozen string literals enabled
72
+ - Classes in `NvimControl` module with descriptive names
73
+ - Private constants at class bottom with `private_constant`
74
+ - Error handling with custom error classes in `errors.rb`
75
+ - Use `attr_reader` for private instance variables
76
+ - RSpec with expect syntax, monkey patching disabled
77
+ - Follow Rubocop rules in `.rubocop.yml`
78
+
79
+ ## Testing
80
+ - Unit tests mock the Neovim client
81
+ - Integration tests require a running Neovim instance with socket
82
+ - Test files mirror lib structure: `spec/lib/nvim_control/`
83
+
84
+ ## Integration examples
85
+ The gem is designed for use with agentic coding tools like Claude Code,
86
+ OpenCode, Amp Code, Codex, and Gemini. See README.md for integration examples
87
+ including Claude Code skill configuration and OpenCode custom tool setup.
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ # 1.0.0
2
+ - Initial release.
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2026 Mathias Jean Johansen
2
+
3
+ Permission to use, copy, modify, and distribute this software for any purpose
4
+ with or without fee is hereby granted, provided that the above copyright notice
5
+ and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
9
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13
+ PERFORMANCE OF THIS SOFTWARE.
data/MIGRATION.md ADDED
@@ -0,0 +1,239 @@
1
+ # Migration guide: `nvim-context` → `nvim-control`
2
+
3
+ A step-by-step runbook to publish the renamed gem, deprecate the old one,
4
+ rename the GitHub repo, and rewire your dotfiles. Ordered to minimise the
5
+ window where tooling is broken.
6
+
7
+ > This is a working artifact. Delete it when done — do not commit it to the
8
+ > repo.
9
+
10
+ ---
11
+
12
+ ## 0. Pre-flight (do these first)
13
+
14
+ 1. **Push the branch work to `main`** (you have 10 unpushed commits):
15
+ ```sh
16
+ cd ~/Code/majjoha/nvim-context
17
+ git log --format='%G? %h %s' origin/main..HEAD # confirm all signed (G)
18
+ git push origin main
19
+ ```
20
+ 2. **Confirm RubyGems ownership of the old gem** (needed for deprecation):
21
+ ```sh
22
+ gem signin # if not already signed in
23
+ gem owner nvim-context # confirm your email is listed
24
+ ```
25
+ 3. **Confirm the gem name is free** (should be — verified earlier):
26
+ ```sh
27
+ gem list -r -e nvim-control # expect no remote match
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 1. Rename the GitHub repository
33
+
34
+ GitHub auto-redirects the old URL, but you should update the remote.
35
+
36
+ 1. Rename via the API (or Settings → rename in the web UI):
37
+ ```sh
38
+ gh repo rename nvim-control --repo majjoha/nvim-context
39
+ ```
40
+ 2. Update your local remote:
41
+ ```sh
42
+ cd ~/Code/majjoha/nvim-context
43
+ git remote set-url origin git@github.com:majjoha/nvim-control.git
44
+ git remote -v # confirm
45
+ ```
46
+ 3. **Optionally rename the local working directory** to match (cosmetic):
47
+ ```sh
48
+ cd ~/Code/majjoha
49
+ mv nvim-context nvim-control
50
+ ```
51
+ If you do this, update any shell bookmarks / mise trust paths that point
52
+ at the old directory.
53
+
54
+ > GitHub keeps a redirect from `majjoha/nvim-context` → `majjoha/nvim-control`,
55
+ > so the README's `gh`/marketplace URLs and the CI badge resolve either way.
56
+ > The redirect breaks only if someone later creates a NEW repo at the old
57
+ > name — unlikely for a personal namespace.
58
+
59
+ ---
60
+
61
+ ## 2. Publish the new gem (`nvim-control`)
62
+
63
+ 1. Build from a clean checkout (active Ruby must be 4.0.5):
64
+ ```sh
65
+ cd ~/Code/majjoha/nvim-control # or nvim-context if you didn't rename
66
+ ruby -v # expect 4.0.5
67
+ gem build nvim-control.gemspec # → nvim-control-1.0.0.gem
68
+ ```
69
+ 2. Push to RubyGems:
70
+ ```sh
71
+ gem push nvim-control-1.0.0.gem
72
+ ```
73
+ - If you have MFA on (the gemspec sets `rubygems_mfa_required`), you'll be
74
+ prompted for an OTP.
75
+ 3. Verify:
76
+ ```sh
77
+ gem list -r -e nvim-control # shows 1.0.0
78
+ ```
79
+ 4. Clean up the local artifact (it's gitignored, but tidy anyway):
80
+ ```sh
81
+ trash nvim-control-1.0.0.gem
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 3. Deprecate / archive the old gem (`nvim-context`)
87
+
88
+ You cannot delete a published gem version after 30 minutes, and `yank`
89
+ should be reserved for broken releases — not renames. Two non-destructive
90
+ options; **A is recommended.**
91
+
92
+ ### Option A — Deprecation release (recommended)
93
+ Publish one final `nvim-context` version whose post-install message points
94
+ users to `nvim-control`. This keeps existing installs working while signalling
95
+ the move.
96
+
97
+ 1. In a scratch copy of the OLD gem (e.g. `git stash` aside or a tag checkout),
98
+ bump the version to `1.0.1` and add a `post_install_message` to the
99
+ gemspec:
100
+ ```ruby
101
+ s.version = "1.0.1"
102
+ s.post_install_message = <<~MSG
103
+ nvim-context has been renamed to nvim-control.
104
+ Please switch: gem install nvim-control
105
+ See https://github.com/majjoha/nvim-control
106
+ MSG
107
+ ```
108
+ 2. Build and push that `1.0.1`:
109
+ ```sh
110
+ gem build nvim-context.gemspec
111
+ gem push nvim-context-1.0.1.gem
112
+ ```
113
+
114
+ ### Option B — Yank (only if you want it gone from installs)
115
+ ```sh
116
+ gem yank nvim-context -v 1.0.0
117
+ ```
118
+ This removes 1.0.0 from the index (anyone with it stays working, but new
119
+ installs fail). Use ONLY if you're sure nothing depends on it. Not reversible
120
+ to the same version number.
121
+
122
+ > Recommendation: **Option A.** It's the polite, non-breaking path for a
123
+ > rename and matches ecosystem convention.
124
+
125
+ ---
126
+
127
+ ## 4. Rewire your dotfiles
128
+
129
+ Your dotfiles (`~/.dotfiles`, a git repo on `main`) reference the old name in
130
+ 7 active spots. Do this AFTER step 2 (the new gem must be published, since the
131
+ mise `gem:` backend installs from RubyGems).
132
+
133
+ ### 4a. mise (the gem install)
134
+ `~/.dotfiles/.config/mise/config.toml` line 41:
135
+ ```diff
136
+ - "gem:nvim-context" = "latest"
137
+ + "gem:nvim-control" = "latest"
138
+ ```
139
+ Then re-resolve and install:
140
+ ```sh
141
+ cd ~/.dotfiles
142
+ mise install # installs gem:nvim-control, drops gem:nvim-context
143
+ mise uninstall "gem:nvim-context@1.0.0" # remove the old one if it lingers
144
+ ```
145
+ The `mise.lock` entries (lines ~1237-1239) update automatically on
146
+ `mise install`; commit the regenerated lock.
147
+
148
+ Verify the binary resolves globally now:
149
+ ```sh
150
+ which nvim-control # should resolve via mise's gem-backend bindir
151
+ nvim-control read # connection-error JSON is fine (no socket)
152
+ ```
153
+
154
+ ### 4b. OpenCode command file
155
+ Rename the command so the slash-command matches:
156
+ ```sh
157
+ cd ~/.dotfiles/.config/opencode/command
158
+ git mv nvim-context.md nvim-control.md
159
+ ```
160
+ Edit `nvim-control.md` body: `!`nvim-context`` → `!`nvim-control``.
161
+
162
+ ### 4c. Neovim tmux-agent
163
+ `~/.dotfiles/.config/nvim/lua/tmux_agent.lua:208`:
164
+ ```diff
165
+ - M.send_text("/nvim-context " .. query)
166
+ + M.send_text("/nvim-control " .. query)
167
+ ```
168
+ (This sends a slash-command; ensure the OpenCode command rename in 4b matches.)
169
+
170
+ ### 4d. Agent instructions + walkthrough skill
171
+ - `~/.dotfiles/.config/agents/AGENTS.md:290` — `nvim-context` → `nvim-control`.
172
+ - `~/.dotfiles/.config/agents/skills/walkthrough/SKILL.md` — 4 spots:
173
+ - line 13: `Bash(nvim-context *)` → `Bash(nvim-control *)`
174
+ - line 34: `nvim-context read` → `nvim-control read`
175
+ - line 39: `NVIM_CONTEXT_SOCKET` → `NVIM_CONTROL_SOCKET`
176
+ - line 115: `nvim-context ex "..."` → `nvim-control ex "..."`
177
+ - `~/.dotfiles/.config/agents/skills/walkthrough/scripts/walkthrough-goto.sh`
178
+ — lines 70, 83, 84: `nvim-context` → `nvim-control`.
179
+
180
+ ### 4e. Claude permissions
181
+ `~/.dotfiles/.claude/settings.json:45`:
182
+ ```diff
183
+ - "Bash(nvim-context:*)",
184
+ + "Bash(nvim-control:*)",
185
+ ```
186
+
187
+ ### 4f. The `v` socket function and `nvim-control.sock`
188
+ Already updated to `nvim-control.sock` in `~/.dotfiles/.config/zsh/aliases.zsh`
189
+ (checked earlier — no change needed). Confirm the `v` function and the
190
+ jump-to-server `find ... -name nvim-control.sock` still match.
191
+
192
+ > Bulk option for 4c-4e (review the diff before committing):
193
+ > ```sh
194
+ > cd ~/.dotfiles
195
+ > grep -rIl 'nvim-context\|nvim_context\|NVIM_CONTEXT' .config .claude/settings.json \
196
+ > | grep -v '.claude/plans/' \
197
+ > | xargs sed -i '' \
198
+ > -e 's/nvim-context/nvim-control/g' \
199
+ > -e 's/NVIM_CONTEXT_SOCKET/NVIM_CONTROL_SOCKET/g'
200
+ > ```
201
+ > Then handle the file RENAMES (4b) separately with `git mv`, and re-read the
202
+ > diff — sed is blunt; verify nothing unintended changed (e.g. prose).
203
+
204
+ ### 4g. Disposable plan file (skip)
205
+ `~/.dotfiles/.claude/plans/jaunty-seeking-sifakis.md` references the old name
206
+ but is a disposable planning artifact — leave it or delete it; don't bother
207
+ rewriting.
208
+
209
+ ### 4h. Commit the dotfiles
210
+ ```sh
211
+ cd ~/.dotfiles
212
+ git add -A
213
+ git status # review — confirm no stray changes
214
+ git commit -m "Rename nvim-context references to nvim-control."
215
+ ```
216
+
217
+ ---
218
+
219
+ ## 5. Verify end to end
220
+
221
+ ```sh
222
+ # In a project dir, start a server and exercise the tool by name:
223
+ cd ~/some-project
224
+ nvim --listen "$PWD/nvim-control.sock" & # or use your `v` function
225
+ NVIM_CONTROL_SOCKET="$PWD/nvim-control.sock" nvim-control read
226
+ NVIM_CONTROL_SOCKET="$PWD/nvim-control.sock" nvim-control ex "echo 'hi'"
227
+ ```
228
+ Both should return JSON. Then confirm the agent skills resolve the new
229
+ binary (run a walkthrough / tmux-agent flow).
230
+
231
+ ---
232
+
233
+ ## Order summary (why this sequence)
234
+ 1. Push commits → 2. Rename repo → 3. Publish new gem → 4. Deprecate old gem
235
+ → 5. Rewire dotfiles (needs new gem live) → 6. Verify.
236
+
237
+ The only hard dependency: **dotfiles mise rewire (4a) requires the new gem
238
+ published (step 2)**, because the `gem:` backend pulls from RubyGems. Repo
239
+ rename (1) and gem publish (2) are independent and can swap order.
data/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # `nvim-control`
2
+ ![CI](https://github.com/majjoha/nvim-control/workflows/CI/badge.svg)
3
+ [![Gem Version](https://badge.fury.io/rb/nvim-control.svg)](
4
+ https://badge.fury.io/rb/nvim-control)
5
+
6
+ `nvim-control` is a bridge between running Neovim instances and agentic coding
7
+ tools. By default, it extracts context from the editor via a Unix socket
8
+ connection and outputs JSON with the cursor position, current file, visual
9
+ selection and diagnostics. It can also execute explicit control actions such as
10
+ running Ex commands or sending key input back to Neovim.
11
+
12
+ It allows agentic coding tools running outside Neovim to answer questions such
13
+ as:
14
+ - What does this line do?
15
+ - Can you convert the current line to uppercase?
16
+ - What does the method under my cursor do?
17
+ - Are these lines idiomatic Ruby?
18
+
19
+ ## Motivation
20
+ While the Neovim community provides several plugins for integrating agentic
21
+ coding assistants into the editor (see the [AI section in the Awesome Neovim
22
+ repository](https://github.com/rockerboo/awesome-neovim?tab=readme-ov-file#ai)),
23
+ it seems that few tools offer a way to let any agentic coding tool running
24
+ *outside* Neovim retrieve the state of the editor in an agnostic manner.
25
+
26
+ The goal with `nvim-control` is to separate concerns, so Amp Code, Claude Code,
27
+ Codex, etc., can query the current state of a Neovim session by calling this
28
+ tool. When editor mutations are needed, they must go through explicit control
29
+ subcommands instead of piggybacking on the default read-only flow. See the
30
+ [Integration with agentic tools](#integration-with-agentic-tools) section below
31
+ for suggestions on how to set this up.
32
+
33
+ ## Installation
34
+ ```sh
35
+ gem install nvim-control
36
+ ```
37
+
38
+ ## Setup
39
+ When starting Neovim ensure that you open it using the `--listen` flag and pass
40
+ a path to the socket as follows:
41
+
42
+ ```sh
43
+ nvim --listen $(pwd)/nvim-control.sock
44
+ ```
45
+
46
+ Alternatively, you can set the `NVIM_CONTROL_SOCKET` environment variable to
47
+ specify the socket path:
48
+
49
+ ```sh
50
+ export NVIM_CONTROL_SOCKET=/tmp/nvim-control.sock
51
+ nvim --listen $NVIM_CONTROL_SOCKET
52
+ ```
53
+
54
+ If no environment variable is set, the tool defaults to `nvim-control.sock` in
55
+ the current directory.
56
+
57
+ ## Usage
58
+ ### Read context
59
+ Once Neovim is running, you can retrieve the current context by running
60
+ `nvim-control` or `nvim-control read`.
61
+
62
+ This will output JSON containing the current file, cursor position, visual
63
+ selection (if any), and diagnostics in this format:
64
+
65
+ ```json
66
+ {
67
+ "cursor": {
68
+ "line": 43,
69
+ "col": 3
70
+ },
71
+ "file": "/path/to/current/file.rb",
72
+ "selection": null,
73
+ "diagnostics": []
74
+ }
75
+ ```
76
+
77
+ ### Control Neovim explicitly
78
+ Control actions are opt-in subcommands. They also return JSON, including a
79
+ `context_after` snapshot so agents can verify what changed.
80
+
81
+ Supported control subcommands:
82
+ - `nvim-control ex "..."` runs an Ex command.
83
+ - `nvim-control keys "..."` sends raw key input.
84
+
85
+ Example response:
86
+ ```json
87
+ {
88
+ "ok": true,
89
+ "action": "ex",
90
+ "command": "write",
91
+ "result": null,
92
+ "context_after": {
93
+ "cursor": { "line": 43, "col": 3 },
94
+ "file": "/path/to/current/file.rb",
95
+ "selection": null,
96
+ "diagnostics": []
97
+ }
98
+ }
99
+ ```
100
+
101
+ ### Useful examples
102
+ Read the current editor state:
103
+ ```sh
104
+ nvim-control
105
+ ```
106
+
107
+ Save the current buffer:
108
+ ```sh
109
+ nvim-control ex "write"
110
+ ```
111
+
112
+ Open the quickfix list after diagnostics or a grep:
113
+ ```sh
114
+ nvim-control ex "copen"
115
+ ```
116
+
117
+ Open a vertical split:
118
+ ```sh
119
+ nvim-control ex "vsplit"
120
+ ```
121
+
122
+ Open a diff split against another file:
123
+ ```sh
124
+ nvim-control ex "diffsplit other.rb"
125
+ ```
126
+
127
+ Send keys to write the file from command-line mode:
128
+ ```sh
129
+ nvim-control keys $':write\r'
130
+ ```
131
+
132
+ Send keys to open the quickfix list:
133
+ ```sh
134
+ nvim-control keys $':copen\r'
135
+ ```
136
+
137
+ ### Integration with agentic tools
138
+ #### Amp Code
139
+ ```sh
140
+ amp skill add majjoha/nvim-control/nvim-read
141
+ amp skill add majjoha/nvim-control/nvim-control
142
+ ```
143
+
144
+ #### Claude Code
145
+ ```sh
146
+ # Add the repository as a marketplace
147
+ /plugin marketplace add majjoha/nvim-control
148
+
149
+ # Install the plugin
150
+ /plugin install nvim-control@nvim-control
151
+ ```
152
+
153
+ The plugin provides the `nvim-read` skill which gives Claude Code access to
154
+ your live Neovim editor state. Install the `nvim-control` skill as well if you
155
+ want the agent to run explicit Neovim commands.
156
+
157
+ #### Codex
158
+ ```sh
159
+ $skill-installer install \
160
+ https://github.com/majjoha/nvim-control/tree/main/.codex/skills/nvim-read
161
+ $skill-installer install \
162
+ https://github.com/majjoha/nvim-control/tree/main/.codex/skills/nvim-control
163
+ ```
164
+
165
+ #### Gemini
166
+ TBD.
167
+
168
+ #### OpenCode
169
+ <details>
170
+ <summary><code>~/.config/opencode/command/nvim-read.md</code></summary>
171
+ <pre>
172
+ ---
173
+ description: Show current Neovim context
174
+ ---
175
+
176
+ !`nvim-control`
177
+ </pre>
178
+ </details>
179
+
180
+ <details>
181
+ <summary><code>~/.config/opencode/command/nvim-control.md</code></summary>
182
+ <pre>
183
+ ---
184
+ description: Run an explicit Neovim control command
185
+ ---
186
+
187
+ !`nvim-control ex "$ARGUMENTS"`
188
+ </pre>
189
+ </details>
190
+
191
+ ## Disclaimer
192
+ Since building software with AI can still be divisive, it might be worth
193
+ pointing out here that `nvim-control` itself has been built using OpenCode and
194
+ Claude Code, but with human guidance and continuous review of its work.
195
+
196
+ ## License
197
+ See [LICENSE](https://github.com/majjoha/nvim-control/blob/main/LICENSE).
data/bin/nvim-control ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+
5
+ require_relative "../lib/nvim_control"
6
+
7
+ exit(NvimControl::CLI.run(argv: ARGV))
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module NvimControl
6
+ class CLI
7
+ class << self
8
+ def run(argv:, stdout: $stdout)
9
+ case argv
10
+ in [] | [READ_ACTION]
11
+ read_context(stdout: stdout)
12
+ in [action, payload] if Controller.supported_action?(action)
13
+ run_control(action: action, payload: payload, stdout: stdout)
14
+ else
15
+ write_invalid_arguments(stdout: stdout)
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def read_context(stdout:)
22
+ write_response(Fetcher.fetch, stdout: stdout)
23
+ end
24
+
25
+ def run_control(action:, payload:, stdout:)
26
+ response = Controller.run(action: action, payload: payload)
27
+
28
+ write_response(response, stdout: stdout)
29
+ end
30
+
31
+ def write_invalid_arguments(stdout:)
32
+ stdout.puts(
33
+ JSON.generate(
34
+ ok: false,
35
+ error: "Invalid arguments",
36
+ details: usage
37
+ )
38
+ )
39
+ ERROR_EXIT_CODE
40
+ end
41
+
42
+ def write_response(response, stdout:)
43
+ stdout.puts(response)
44
+
45
+ successful_response?(response) ? SUCCESS_EXIT_CODE : ERROR_EXIT_CODE
46
+ end
47
+
48
+ def successful_response?(response)
49
+ payload = JSON.parse(response)
50
+
51
+ return false unless payload.is_a?(Hash)
52
+ return false if payload.key?("error")
53
+ return false if payload["ok"] == false
54
+
55
+ true
56
+ rescue JSON::ParserError
57
+ false
58
+ end
59
+
60
+ def usage
61
+ actions = ([READ_ACTION] + Controller.supported_actions).join("|")
62
+
63
+ "Usage: nvim-control [#{actions}] [payload]"
64
+ end
65
+
66
+ READ_ACTION = "read"
67
+ SUCCESS_EXIT_CODE = 0
68
+ ERROR_EXIT_CODE = 1
69
+ private_constant :READ_ACTION,
70
+ :SUCCESS_EXIT_CODE,
71
+ :ERROR_EXIT_CODE
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "neovim"
4
+
5
+ module NvimControl
6
+ class Connector
7
+ def initialize(client: nil)
8
+ @socket_path = ENV["NVIM_CONTROL_SOCKET"] || DEFAULT_SOCKET_PATH
9
+ @client = client || begin
10
+ Neovim.attach_unix(socket_path)
11
+ rescue StandardError => e
12
+ raise ConnectionError,
13
+ "Failed to connect to Neovim socket: #{e.message}",
14
+ e.backtrace
15
+ end
16
+ end
17
+
18
+ def connect
19
+ yield client if block_given?
20
+ rescue StandardError => e
21
+ raise OperationError,
22
+ "Failed during Neovim operation: #{e.message}",
23
+ e.backtrace
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :client, :socket_path
29
+
30
+ DEFAULT_SOCKET_PATH = File.expand_path("nvim-control.sock")
31
+ private_constant :DEFAULT_SOCKET_PATH
32
+ end
33
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module NvimControl
6
+ class Controller
7
+ class << self
8
+ def run(action:, payload:)
9
+ return invalid_action_response unless supported_action?(action)
10
+
11
+ JSON.generate(action_response(action: action, payload: payload))
12
+ rescue ConnectionError => e
13
+ format_error("Connection failed", e.message)
14
+ rescue OperationError => e
15
+ format_error("Control action failed", e.message)
16
+ rescue StandardError => e
17
+ format_error("Unexpected error", e.message)
18
+ end
19
+
20
+ def supported_action?(action)
21
+ ACTIONS.key?(action)
22
+ end
23
+
24
+ def supported_actions
25
+ ACTIONS.keys
26
+ end
27
+
28
+ private
29
+
30
+ def action_response(action:, payload:)
31
+ Connector.new.connect do |client|
32
+ execute_action(client: client, action: action, payload: payload)
33
+ end
34
+ end
35
+
36
+ def execute_action(client:, action:, payload:)
37
+ action_metadata = ACTIONS.fetch(action)
38
+
39
+ {
40
+ ok: true,
41
+ action: action,
42
+ action_metadata.fetch(:payload_key) => payload,
43
+ result: action_metadata.fetch(:runner).call(client, payload),
44
+ context_after: DataExtractor.context(client: client)
45
+ }
46
+ end
47
+
48
+ def invalid_action_response
49
+ JSON.generate(
50
+ ok: false,
51
+ error: "Unsupported action",
52
+ details: "Supported actions: #{supported_actions.join(', ')}"
53
+ )
54
+ end
55
+
56
+ def format_error(error, details)
57
+ JSON.generate({ ok: false, error: error, details: details })
58
+ end
59
+
60
+ ACTIONS = {
61
+ "ex" => {
62
+ payload_key: :command,
63
+ runner: ->(client, payload) { client.command(payload) }
64
+ }.freeze,
65
+ "keys" => {
66
+ payload_key: :keys,
67
+ runner: ->(client, payload) { client.input(payload) }
68
+ }.freeze
69
+ }.freeze
70
+ private_constant :ACTIONS
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NvimControl
4
+ class DataExtractor
5
+ def self.context(client:)
6
+ {
7
+ cursor: cursor(client: client),
8
+ file: file(client: client),
9
+ selection: visual_selection(client: client),
10
+ diagnostics: diagnostics(client: client)
11
+ }
12
+ end
13
+
14
+ def self.cursor(client:)
15
+ cursor = client.current.window.cursor
16
+ { line: cursor[0], col: cursor[1] }
17
+ rescue StandardError => e
18
+ raise OperationError,
19
+ "Failed to get cursor info: #{e.message}",
20
+ e.backtrace
21
+ end
22
+
23
+ def self.file(client:)
24
+ client.current.buffer.name
25
+ rescue StandardError => e
26
+ raise OperationError,
27
+ "Failed to get file info: #{e.message}",
28
+ e.backtrace
29
+ end
30
+
31
+ def self.visual_selection(client:)
32
+ return nil unless visual_mode?(client)
33
+
34
+ marks = visual_marks(client)
35
+ text = selected_text(client, marks)
36
+ build_selection_info(marks, text)
37
+ rescue StandardError
38
+ nil
39
+ end
40
+
41
+ def self.diagnostics(client:)
42
+ client.eval("vim.diagnostic.get(0)").map do |diagnostic|
43
+ {
44
+ line: diagnostic["lnum"] + 1,
45
+ col: diagnostic["col"] + 1,
46
+ message: diagnostic["message"],
47
+ severity: diagnostic["severity"]
48
+ }
49
+ end
50
+ rescue StandardError
51
+ []
52
+ end
53
+
54
+ class << self
55
+ private
56
+
57
+ VISUAL_BLOCK_MODE = "\x16"
58
+ private_constant :VISUAL_BLOCK_MODE
59
+
60
+ def visual_mode?(client)
61
+ ["v", "V", VISUAL_BLOCK_MODE].include?(client.eval("mode()"))
62
+ end
63
+
64
+ def visual_marks(client)
65
+ {
66
+ start: client.eval("getpos(\"'<\")"),
67
+ end: client.eval("getpos(\"'>\")")
68
+ }
69
+ end
70
+
71
+ def selected_text(client, marks)
72
+ start_line = marks[:start][1]
73
+ end_line = marks[:end][1]
74
+ client.current.buffer.get_lines(start_line - 1, end_line, true)
75
+ end
76
+
77
+ def build_selection_info(marks, text)
78
+ {
79
+ start: { line: marks[:start][1], col: marks[:start][2] },
80
+ end: { line: marks[:end][1], col: marks[:end][2] },
81
+ text: text.join("\n")
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NvimControl
4
+ class ConnectionError < StandardError; end
5
+ class OperationError < StandardError; end
6
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module NvimControl
6
+ class Fetcher
7
+ class << self
8
+ def fetch
9
+ context = build_context
10
+ JSON.generate(context)
11
+ rescue ConnectionError => e
12
+ format_error("Connection failed", e.message)
13
+ rescue OperationError => e
14
+ format_error("Context extraction failed", e.message)
15
+ rescue StandardError => e
16
+ format_error("Unexpected error", e.message)
17
+ end
18
+
19
+ private
20
+
21
+ def build_context
22
+ Connector.new.connect { |client| DataExtractor.context(client: client) }
23
+ end
24
+
25
+ def format_error(error, details)
26
+ JSON.generate({ error: error, details: details })
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NvimControl
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nvim_control/errors"
4
+ require_relative "nvim_control/connector"
5
+ require_relative "nvim_control/fetcher"
6
+ require_relative "nvim_control/data_extractor"
7
+ require_relative "nvim_control/controller"
8
+ require_relative "nvim_control/cli"
9
+ require_relative "nvim_control/version"
10
+
11
+ module NvimControl
12
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nvim-control
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Mathias Jean Johansen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: neovim
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.10.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.10.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: logger
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.7'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.7'
40
+ description: |
41
+ `nvim-control` bridges running Neovim instances and agentic coding tools
42
+ via Unix socket connections. It reads live editor state (cursor position,
43
+ current file, visual selections, and diagnostics) as JSON and runs explicit
44
+ control actions such as Ex commands and key input. This lets agents both
45
+ answer questions like "What does this line do?" and drive the editor on
46
+ request.
47
+ email: mathias@mjj.io
48
+ executables:
49
+ - nvim-control
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - AGENTS.md
54
+ - CHANGELOG.md
55
+ - CLAUDE.md
56
+ - LICENSE
57
+ - MIGRATION.md
58
+ - README.md
59
+ - bin/nvim-control
60
+ - lib/nvim_control.rb
61
+ - lib/nvim_control/cli.rb
62
+ - lib/nvim_control/connector.rb
63
+ - lib/nvim_control/controller.rb
64
+ - lib/nvim_control/data_extractor.rb
65
+ - lib/nvim_control/errors.rb
66
+ - lib/nvim_control/fetcher.rb
67
+ - lib/nvim_control/version.rb
68
+ homepage: https://github.com/majjoha/nvim-control
69
+ licenses:
70
+ - ISC
71
+ metadata:
72
+ rubygems_mfa_required: 'true'
73
+ source_code_uri: https://github.com/majjoha/nvim-control
74
+ changelog_uri: https://github.com/majjoha/nvim-control/blob/main/CHANGELOG.md
75
+ bug_tracker_uri: https://github.com/majjoha/nvim-control/issues
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 4.0.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 4.0.10
91
+ specification_version: 4
92
+ summary: Bridge between running Neovim instances and agentic coding tools.
93
+ test_files: []