From c7543c978abbda8028369d4414d853a2e465de92 Mon Sep 17 00:00:00 2001 From: Daniel Winkler Date: Fri, 7 Aug 2026 15:00:50 +1000 Subject: [PATCH 1/4] OMP neovim server bridge --- .omp/tools/nvim_buffers.mjs | 185 ++++++++++++++++++++++++++++++++++++ lua/nvim_omp/init.lua | 55 +++++++++++ plugin/31_nvim_omp.lua | 8 ++ 3 files changed, 248 insertions(+) create mode 100644 .omp/tools/nvim_buffers.mjs create mode 100644 lua/nvim_omp/init.lua create mode 100644 plugin/31_nvim_omp.lua diff --git a/.omp/tools/nvim_buffers.mjs b/.omp/tools/nvim_buffers.mjs new file mode 100644 index 0000000..82e9ce6 --- /dev/null +++ b/.omp/tools/nvim_buffers.mjs @@ -0,0 +1,185 @@ +// omp custom tool: read this machine's running Neovim buffers over its RPC +// socket. Read-only and on-demand — the model calls it when it needs buffer +// content; nothing is injected into prompts automatically. +// +// Install options +// 1. Project scope (this project): keep the file in .omp/tools/ — omp picks it +// up when a session's cwd is inside this repository. +// 2. All projects: mkdir -p ~/.omp/agent/tools +// ln -s "$PWD/.omp/tools/nvim_buffers.mjs" ~/.omp/agent/tools/ +// then restart `omp` (custom tools are loaded at session bootstrap). +// +// Socket contract (must match lua/nvim_omp/init.lua): +// path = $NVIM_OMP_SOCKET or /nvim/omp.sock +// The nvim instance running this config starts the listener at startup. +// Only one instance can own the socket; a second instance never replaces a +// live owner. Remove a stale socket manually only after confirming no nvim +// process owns it. + +import os from "node:os"; +import path from "node:path"; + +// Safe default cap so a large buffer cannot flood the conversation. +const DEFAULT_MAX_LINES = 2000; + +export default function (pi) { + // Resolve socket path with the same rule as lua/nvim_omp/init.lua. + const socketPath = () => { + const env = process.env.NVIM_OMP_SOCKET; + if (env) return env; + const state = + process.env.XDG_STATE_HOME || + path.join(os.homedir(), ".local", "state"); + return path.join(state, "nvim", "omp.sock"); + }; + + // Evaluate expr in the remote nvim via `nvim --server`; returns parsed JSON. + const rpc = async (expr, signal) => { + const bin = process.env.NVIM_BIN || "nvim"; + const { code, stdout, stderr, killed } = await pi.exec( + bin, + ["--server", socketPath(), "--remote-expr", expr], + { signal }, + ); + if (killed) throw new Error("cancelled"); + if (code !== 0) { + const why = String(stderr || "").trim(); + throw new Error( + why.includes("E247") + ? `No Neovim RPC socket at ${socketPath()}. ` + + "Start nvim (or vv) first — the 31_nvim_omp.lua plugin binds the socket at startup." + : `nvim RPC failed (${code}): ${why}`, + ); + } + const text = String(stdout ?? "").trim(); + if (!text) throw new Error("empty response from nvim RPC"); + return JSON.parse(text); + }; + + // Resolve a buffer argument (number, string, or partial path) to a buffer + // number. Returns null when nothing matches. + const resolveBuf = async (buffer, signal) => { + if (buffer === undefined || buffer === null || buffer === "") { + return rpc("json_encode(nvim_get_current_buf())", signal); + } + if (typeof buffer === "number") return buffer; + const bufnum = Number(buffer); + if (Number.isInteger(bufnum) && bufnum > 0) return bufnum; + const list = await rpc( + "json_encode(map(nvim_list_bufs(), {i, v -> " + + "{'bufnr': v, 'name': nvim_buf_get_name(v)}}))", + signal, + ); + const q = String(buffer); + const hit = + list.find( + (b) => b.name === q || b.name.endsWith("/" + q) || b.name.endsWith(q), + ) || + list.find((b) => b.name.includes(q)); + return hit ? hit.bufnr : null; + }; + + const tools = []; + + // List open buffers ----------------------------------------------- + tools.push({ + name: "nvim_buffers", + label: "Neovim Buffers", + description: + "List the buffers currently open in the user's running Neovim " + + "(number, name, current/loaded state, modified). Use before " + + "nvim_buffer to pick a buffer.", + parameters: pi.arktype({}), + async execute(_id, _params, _onUpdate, _ctx, _signal) { + const list = await rpc( + "json_encode(map(nvim_list_bufs(), {i, v -> {'nr': v, " + + "'name': nvim_buf_get_name(v), " + + "'current': v == nvim_get_current_buf(), " + + "'loaded': nvim_buf_is_loaded(v), " + + "'modified': getbufvar(v, '&modified')}}))", + ); + if (!list.length) { + return { content: [{ type: "text", text: "No buffers open." }] }; + } + const lines = list.map( + (b) => + `${b.nr}\t${b.name || "[no name]"}\t` + + `${b.current ? "current " : ""}` + + `${b.loaded ? "" : "unloaded "}` + + `${b.modified ? "modified" : ""}`.trim(), + ); + return { + content: [ + { + type: "text", + text: `Open buffers (${list.length}):\n` + lines.join("\n"), + }, + ], + }; + }, + }); + + // Read a single buffer, line-capped ------------------------------- + tools.push({ + name: "nvim_buffer", + label: "Neovim Buffer", + description: + "Read lines of a Neovim buffer. `buffer` accepts the current buffer " + + "(default), a buffer number from nvim_buffers, or a file name/path " + + "open in Neovim. `maxLines` caps the returned lines (default 2000); " + + "pass a larger value explicitly to read more of a long buffer.", + parameters: pi.arktype({ + buffer: "string? | number?", + maxLines: "number?", + }), + async execute(_id, params, _onUpdate, _ctx, signal) { + const buf = await resolveBuf(params.buffer, signal); + if (buf === null) { + return { + content: [ + { + type: "text", + text: `No buffer matches '${params.buffer}'. List open buffers with nvim_buffers.`, + }, + ], + }; + } + const maxLines = + Number.isInteger(params.maxLines) && params.maxLines > 0 + ? Math.min(params.maxLines, 100000) + : DEFAULT_MAX_LINES; + + const expr = + `json_encode({'name': nvim_buf_get_name(${buf}), ` + + `'total': nvim_buf_line_count(${buf}), ` + + `'lines': nvim_buf_get_lines(${buf}, 0, ${maxLines}, 0)})`; + const { name, total, lines } = await rpc(expr, signal); + if (!total) { + return { + content: [ + { type: "text", text: `Buffer ${buf} (${name}) is empty.` }, + ], + }; + } + const truncated = + total > lines.length + ? `\n... ${total - lines.length} more lines ` + + `(raise maxLines to read them)` + : ""; + const numbered = lines.map((l, i) => `${i + 1}: ${l}`).join("\n"); + return { + content: [ + { + type: "text", + text: + `Buffer ${buf} (${name}), ${total} lines:\n` + + numbered + + truncated, + }, + ], + }; + }, + }); + + return tools; +} \ No newline at end of file diff --git a/lua/nvim_omp/init.lua b/lua/nvim_omp/init.lua new file mode 100644 index 0000000..4400d49 --- /dev/null +++ b/lua/nvim_omp/init.lua @@ -0,0 +1,55 @@ +-- omp bridge: expose an RPC socket so the omp harness can read this Neovim +-- instance's buffers on demand. Read-only on the agent side; this module only +-- owns the socket lifecycle. +-- +-- Contract +-- * Socket path: $NVIM_OMP_SOCKET if set, else /omp.sock +-- (~/.local/state/nvim/omp.sock on macOS, ~/.local/state/nvim/omp.sock +-- elsewhere). The omp-side tool in .omp/tools/nvim_buffers.mjs resolves the +-- *same* path, so the two sides agree without configuration. +-- * The agent reads buffers by evaluating read-only nvim API expressions over +-- the socket with `nvim --server --remote-expr 'json_encode(...)'`. +-- Nothing here writes buffers or executes model-supplied commands. +local M = {} + +local SOCKET_NAME = "omp.sock" + +-- Resolve the deterministic socket path. Copies the rule on the omp side; keep +-- the two files in sync when changing the fallback or env override. +function M.socket_path() + local env = vim.env.NVIM_OMP_SOCKET + if env and env ~= "" then + return env + end + return vim.fn.stdpath("state") .. "/" .. SOCKET_NAME +end + +-- Start the RPC listener. Returns the live socket path, or nil. +-- A second instance cannot bind the same address. This function deliberately +-- never removes an existing socket file: a failed liveness probe must not +-- disconnect another live Neovim instance. +-- If a crash leaves a stale socket, remove it manually only after confirming +-- that no Neovim process owns the path, then restart vv. +function M.start() + if vim.v.headless == 1 then + -- Headless runs (tests, CI) get no socket; nothing should depend on one. + return nil + end + + local path = M.socket_path() + local ok, res = pcall(vim.fn.serverstart, path) + if ok and type(res) == "string" and res ~= "" then + -- serverstart returns the bound address string (e.g. "/tmp/omp.sock"). + vim.notify("nvim_omp: RPC socket ready at " .. path, vim.log.levels.INFO) + return path + end + + vim.notify( + "nvim_omp: could not bind RPC socket at " .. path + .. "; another instance may own it or a stale socket needs manual cleanup.", + vim.log.levels.WARN + ) + return nil +end + +return M diff --git a/plugin/31_nvim_omp.lua b/plugin/31_nvim_omp.lua new file mode 100644 index 0000000..44076cf --- /dev/null +++ b/plugin/31_nvim_omp.lua @@ -0,0 +1,8 @@ +-- omp bridge entrypoint: start the RPC socket as early as possible so the omp +-- harness can read this instance's buffers on demand (see lua/nvim_omp/init.lua +-- and .omp/tools/nvim_buffers.mjs). Safe to fail silently — the socket is a +-- convenience, not a dependency of the editor. +local ok, nvim_omp = pcall(require, "nvim_omp") +if ok then + nvim_omp.start() +end \ No newline at end of file From 99f7f29fa63b713818a3257fc8373d5910fd7c01 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 9 Aug 2026 04:20:39 +0000 Subject: [PATCH 2/4] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/b7c2ada' (2026-08-05) → 'github:nixos/nixpkgs/f13ff45' (2026-08-07) • Updated input 'plugins-bloocky': 'github:atiladefreitas/bloocky/a052c3b' (2026-08-06) → 'github:atiladefreitas/bloocky/2f493d6' (2026-08-07) • Updated input 'plugins-dooing': 'github:atiladefreitas/dooing/6748316' (2026-08-05) → 'github:atiladefreitas/dooing/2871aaf' (2026-08-07) --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 195ab40..05df499 100644 --- a/flake.lock +++ b/flake.lock @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1785967620, - "narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=", + "lastModified": 1786106723, + "narHash": "sha256-zDSUbpoeo/9ZmD2+wXnzxoo1+uhL8vxc0b8yuYMKYq0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436", + "rev": "f13ff45afd1bb73e640eaa08a7066dbed07e3238", "type": "github" }, "original": { @@ -39,11 +39,11 @@ "plugins-bloocky": { "flake": false, "locked": { - "lastModified": 1786042339, - "narHash": "sha256-8QcoC1bS8pRcwBkW3mqD2vgP9Mu0aCdIy6umZO6mUGM=", + "lastModified": 1786129261, + "narHash": "sha256-u/s7wCZBZYIax5V/xKEqfs4/y3uCGnaLWY6CkUQRyTk=", "owner": "atiladefreitas", "repo": "bloocky", - "rev": "a052c3b1a8126e04b194bb3db3c4c6ca641c0e4e", + "rev": "2f493d637bd9f385b812d49beb6a93564051bb77", "type": "github" }, "original": { @@ -71,11 +71,11 @@ "plugins-dooing": { "flake": false, "locked": { - "lastModified": 1785971574, - "narHash": "sha256-GtEC5kWcH0DmiYXkzxzNjjfYLfyEb7zpTLQWdi7n3zs=", + "lastModified": 1786125118, + "narHash": "sha256-Iil8eCCz61T1Hz1y2cij71Hti+QGXbjH49eGCPMqTbA=", "owner": "atiladefreitas", "repo": "dooing", - "rev": "6748316bc6b4601797fb952a83694cc0a5ef6da2", + "rev": "2871aaf400c49187aa4216e7cf7abfa37cd32840", "type": "github" }, "original": { From b630932348d8b16e8f186584d1256f7a9c2002c9 Mon Sep 17 00:00:00 2001 From: Daniel Winkler Date: Tue, 11 Aug 2026 12:33:49 +1000 Subject: [PATCH 3/4] Fixed dooing keymap --- plugin/30_dooing.lua | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/plugin/30_dooing.lua b/plugin/30_dooing.lua index 0ec1a09..f47ecc2 100644 --- a/plugin/30_dooing.lua +++ b/plugin/30_dooing.lua @@ -13,16 +13,32 @@ end local nix = require('config.nix') local later = MiniDeps.later - +local now = MiniDeps.now later(function() if not nix.get_cat('general', false) then return end - require('dooing').setup({ + local dooing = require('dooing') + require("dooing").setup({ keymaps = { - toggle_window = 'cd', - open_project_todo = 'cD', - show_due_notification = 'cN', + toggle_window = "cd", + open_project_todo = "cD", + show_due_notification = "cN", + create_nested_task = "cn", -- Create nested subtask under current todo + toggle_priority = "a", + }, + calendar = { + week_start_day = "monday", }, }) -end) \ No newline at end of file + -- Remove Dooing's old defaults. + vim.keymap.del("n", "td") + vim.keymap.del("n", "tD") + vim.keymap.del("n", "tN") + + -- Restore mappings overwritten by Dooing. + local helpers = require('keymap.helpers') + local nmap_leader = helpers.nmap_leader + -- t is for 'terminal' + nmap_leader("td", 'lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()', 'Open DuckDB') +end) From 93b88ee0195deeb42a2cd694160a01b356880479 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 11 Aug 2026 04:23:06 +0000 Subject: [PATCH 4/4] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/b7c2ada' (2026-08-05) → 'github:nixos/nixpkgs/f13ff45' (2026-08-07) • Updated input 'plugins-bloocky': 'github:atiladefreitas/bloocky/a052c3b' (2026-08-06) → 'github:atiladefreitas/bloocky/2f493d6' (2026-08-07) • Updated input 'plugins-dooing': 'github:atiladefreitas/dooing/6748316' (2026-08-05) → 'github:atiladefreitas/dooing/2871aaf' (2026-08-07) • Updated input 'rixpkgs': 'github:dwinkler1/rixpkgs/a54080a' (2026-08-03) → 'github:dwinkler1/rixpkgs/9518d16' (2026-08-11) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index 195ab40..410cdd9 100644 --- a/flake.lock +++ b/flake.lock @@ -22,11 +22,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1785967620, - "narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=", + "lastModified": 1786106723, + "narHash": "sha256-zDSUbpoeo/9ZmD2+wXnzxoo1+uhL8vxc0b8yuYMKYq0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436", + "rev": "f13ff45afd1bb73e640eaa08a7066dbed07e3238", "type": "github" }, "original": { @@ -39,11 +39,11 @@ "plugins-bloocky": { "flake": false, "locked": { - "lastModified": 1786042339, - "narHash": "sha256-8QcoC1bS8pRcwBkW3mqD2vgP9Mu0aCdIy6umZO6mUGM=", + "lastModified": 1786129261, + "narHash": "sha256-u/s7wCZBZYIax5V/xKEqfs4/y3uCGnaLWY6CkUQRyTk=", "owner": "atiladefreitas", "repo": "bloocky", - "rev": "a052c3b1a8126e04b194bb3db3c4c6ca641c0e4e", + "rev": "2f493d637bd9f385b812d49beb6a93564051bb77", "type": "github" }, "original": { @@ -71,11 +71,11 @@ "plugins-dooing": { "flake": false, "locked": { - "lastModified": 1785971574, - "narHash": "sha256-GtEC5kWcH0DmiYXkzxzNjjfYLfyEb7zpTLQWdi7n3zs=", + "lastModified": 1786125118, + "narHash": "sha256-Iil8eCCz61T1Hz1y2cij71Hti+QGXbjH49eGCPMqTbA=", "owner": "atiladefreitas", "repo": "dooing", - "rev": "6748316bc6b4601797fb952a83694cc0a5ef6da2", + "rev": "2871aaf400c49187aa4216e7cf7abfa37cd32840", "type": "github" }, "original": { @@ -108,11 +108,11 @@ }, "rixpkgs": { "locked": { - "lastModified": 1785776890, - "narHash": "sha256-CzzgB1teVbroR/QhGAnsQToUYCDXEZZtFTcISAooASM=", + "lastModified": 1786421894, + "narHash": "sha256-BQLZ0kJYdZ3FmC2Vq7PJFmx4wnyifoCdz+5CIkdbDxI=", "owner": "dwinkler1", "repo": "rixpkgs", - "rev": "a54080a58b57d5785dc85901850b817bef2aaf83", + "rev": "9518d16a41413cf08eecde557b337b87f1422e91", "type": "github" }, "original": {