Skip to content

Commit 2947d29

Browse files
committed
wip
1 parent b8a0ed1 commit 2947d29

8 files changed

Lines changed: 420 additions & 52 deletions

File tree

.codecompanion/rules.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Rules
2+
3+
In CodeCompanion, rules enable a user to quickly share context with an LLM or an agent.
4+
5+
Rules files are markdown files that contain context about a specific feature or behaviour, generally. They can be parsed by a parser which can extract text and file paths from them, adding them to the chat buffer as it goes.
6+
7+
## Init
8+
9+
@./lua/codecompanion/interactions/chat/rules/init.lua
10+
11+
This picks up the rules from a users config and contains methods which allows them to be added to the chat buffer's context.
12+
13+
## Helpers
14+
15+
@./lua/codecompanion/interactions/chat/rules/helpers.lua
16+
17+
This contains some helper functions that allow rules files to add context to the chat buffer.
18+
19+
## Parsers
20+
21+
@./lua/codecompanion/interactions/chat/rules/parsers/init.lua
22+
@./lua/codecompanion/interactions/chat/rules/parsers/claude.lua
23+
@./lua/codecompanion/interactions/chat/rules/parsers/codecompanion.lua
24+
@./lua/codecompanion/interactions/chat/rules/parsers/none.lua
25+
26+
These are the files that allow CodeCompanion to read a user's markdown rule file and extract its content according to the parser, ready for sharing in the chat buffer. For example, with the Claude parser, file paths are extracted and those files are then shared as buffer or file context with the LLM, alongside any text.
27+
28+
## Slash Command
29+
30+
@./lua/codecompanion/interactions/chat/slash_commands/builtin/rules.lua
31+
32+
This slash command allows users to select a given rule and load it into the chat buffer
33+
34+
## Config
35+
36+
The default config for rules is:
37+
38+
````lua
39+
rules = {
40+
default = {
41+
description = "Collection of common files for all projects",
42+
files = {
43+
".clinerules",
44+
".cursorrules",
45+
".goosehints",
46+
".rules",
47+
".windsurfrules",
48+
".github/copilot-instructions.md",
49+
"AGENT.md",
50+
"AGENTS.md",
51+
{ path = "CLAUDE.md", parser = "claude" },
52+
{ path = "CLAUDE.local.md", parser = "claude" },
53+
{ path = "~/.claude/CLAUDE.md", parser = "claude" },
54+
},
55+
is_preset = true,
56+
},
57+
CodeCompanion = {
58+
description = "CodeCompanion rules",
59+
parser = "claude",
60+
---@return boolean
61+
enabled = function()
62+
-- Don't show this to users who aren't working on CodeCompanion itself
63+
return vim.fn.getcwd():find("codecompanion", 1, true) ~= nil
64+
end,
65+
files = {
66+
["adapters"] = {
67+
description = "The adapters implementation",
68+
files = {
69+
".codecompanion/adapters/adapters.md",
70+
},
71+
},
72+
["chat"] = {
73+
description = "The chat buffer",
74+
files = {
75+
".codecompanion/chat.md",
76+
},
77+
},
78+
["acp"] = {
79+
description = "The ACP implementation",
80+
files = {
81+
".codecompanion/acp/acp.md",
82+
},
83+
},
84+
["acp-json-rpc"] = {
85+
description = "The JSON-RPC output for various ACP adapters",
86+
files = {
87+
".codecompanion/acp/claude_code_acp.md",
88+
},
89+
},
90+
["rules"] = {
91+
description = "Rules in the plugin",
92+
files = {
93+
".codecompanion/rules.md",
94+
},
95+
},
96+
["tests"] = {
97+
description = "Testing in the plugin",
98+
files = {
99+
".codecompanion/tests/test.md",
100+
},
101+
},
102+
["tools"] = {
103+
description = "Tools implementation in the plugin",
104+
files = {
105+
".codecompanion/tools.md",
106+
},
107+
},
108+
["ui"] = {
109+
description = "The chat UI implementation",
110+
files = {
111+
".codecompanion/ui.md",
112+
},
113+
},
114+
},
115+
is_preset = true,
116+
},
117+
parsers = {
118+
claude = "claude", -- Parser for CLAUDE.md files
119+
codecompanion = "codecompanion", -- Parser for CodeCompanion specific rules files
120+
none = "none", -- No parsing, just raw text
121+
},
122+
opts = {
123+
chat = {
124+
---The rule groups to load with every chat interaction
125+
---@type string|fun(): string
126+
autoload = "default",
127+
128+
---@type boolean | fun(chat: CodeCompanion.Chat): boolean
129+
enabled = true,
130+
131+
---The default parameters to use when loading buffer rules
132+
default_params = "diff", -- all|diff
133+
},
134+
135+
show_presets = true, -- Show the preset rules files?
136+
},
137+
},
138+
````

lua/codecompanion/config.lua

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,10 +497,11 @@ If you are providing code changes, use the insert_edit_into_file tool (if availa
497497
},
498498
},
499499
["rules"] = {
500-
path = "interactions.chat.slash_commands.builtin.rules",
501-
description = "Insert rules into the chat buffer",
500+
path = "interactions.shared.slash_commands.rules",
501+
description = "Insert rules",
502502
opts = {
503503
contains_code = true,
504+
interactions = { "chat", "cli" },
504505
},
505506
},
506507
["symbols"] = {
@@ -994,6 +995,12 @@ The user is working on a %s machine. Please respond with system specific command
994995
".codecompanion/acp/claude_code_acp.md",
995996
},
996997
},
998+
["rules"] = {
999+
description = "Rules in the plugin",
1000+
files = {
1001+
".codecompanion/rules.md",
1002+
},
1003+
},
9971004
["tests"] = {
9981005
description = "Testing in the plugin",
9991006
files = {
@@ -1017,6 +1024,7 @@ The user is working on a %s machine. Please respond with system specific command
10171024
},
10181025
parsers = {
10191026
claude = "claude", -- Parser for CLAUDE.md files
1027+
cli = "cli", -- Parser for CLI interactions (file paths only, no content)
10201028
codecompanion = "codecompanion", -- Parser for CodeCompanion specific rules files
10211029
none = "none", -- No parsing, just raw text
10221030
},

lua/codecompanion/interactions/chat/rules/init.lua

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ function Rules.new(args)
4444
return self
4545
end
4646

47-
---Collect all file paths based on the rules configuration
47+
---Resolve all file paths based on the rules configuration
4848
---@return string[] paths List of absolute file paths
49-
function Rules:collect_files()
49+
function Rules:resolve_paths()
5050
local collected_paths = {}
5151
local seen = {} -- Track duplicates
5252

@@ -226,7 +226,7 @@ end
226226
---@param args { chat: CodeCompanion.Chat }
227227
---@return nil
228228
function Rules:make(args)
229-
local paths = self:collect_files()
229+
local paths = self:resolve_paths()
230230
local files = self:read_files(paths)
231231
self.processed = self:parse_files(files)
232232
self:add_to_chat(args.chat)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--[[
2+
===============================================================================
3+
File: codecompanion.interactions/chat/rules/parsers/cli.lua
4+
Author: Oli Morris
5+
-------------------------------------------------------------------------------
6+
Description:
7+
Parses a markdown file and extracts any lines that start with "@" as
8+
file paths. Unlike the claude parser, this returns no content — only
9+
file references. Designed for CLI interactions where the agent reads
10+
files directly.
11+
===============================================================================
12+
--]]
13+
14+
---@param file CodeCompanion.Chat.Rules.ProcessedFile
15+
---@return CodeCompanion.Chat.Rules.Parser
16+
return function(file)
17+
local content = file.content or ""
18+
local included_files = {}
19+
20+
if content == "" then
21+
return { content = "" }
22+
end
23+
24+
local ok, parser = pcall(vim.treesitter.get_string_parser, content, "markdown")
25+
if not ok then
26+
return { content = "" }
27+
end
28+
29+
local tree = parser:parse()[1]
30+
if not tree then
31+
return { content = "" }
32+
end
33+
local root = tree:root()
34+
35+
local query = vim.treesitter.query.parse("markdown", "(paragraph) @p")
36+
local get_text = vim.treesitter.get_node_text
37+
38+
local seen = {}
39+
for id, node in query:iter_captures(root, content, 0, -1) do
40+
if query.captures[id] == "p" then
41+
local para = get_text(node, content)
42+
for line in para:gmatch("[^\n]+") do
43+
local path = line:match("^%s*@(%S+)")
44+
if path and not seen[path] then
45+
seen[path] = true
46+
table.insert(included_files, path)
47+
end
48+
end
49+
end
50+
end
51+
52+
return { content = "", meta = (#included_files > 0) and { included_files = included_files } or nil }
53+
end

lua/codecompanion/interactions/chat/slash_commands/builtin/rules.lua

Lines changed: 0 additions & 47 deletions
This file was deleted.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
local helpers = require("codecompanion.interactions.chat.rules.helpers")
2+
local rules = require("codecompanion.interactions.chat.rules")
3+
4+
---A picker for rules
5+
---@param chat? CodeCompanion.Chat
6+
---@param callback fun(selected: table)
7+
local function picker(chat, callback)
8+
vim.ui.select(helpers.list(chat), {
9+
prompt = "Select a rule",
10+
format_item = function(item)
11+
return item.name
12+
end,
13+
}, function(selected)
14+
if selected then
15+
callback(selected)
16+
end
17+
end)
18+
end
19+
20+
---@class CodeCompanion.SlashCommand.Rules: CodeCompanion.SlashCommand
21+
local SlashCommand = {}
22+
23+
---@param args CodeCompanion.SlashCommand
24+
function SlashCommand.new(args)
25+
local self = setmetatable({
26+
Chat = args.Chat,
27+
config = args.config,
28+
context = args.context,
29+
}, { __index = SlashCommand })
30+
31+
return self
32+
end
33+
34+
function SlashCommand:execute()
35+
picker(self.Chat, function(selected)
36+
self:output(selected)
37+
end)
38+
end
39+
40+
---Execute the slash command
41+
---@param selected table
42+
---@return nil
43+
function SlashCommand:output(selected)
44+
return rules
45+
.new({
46+
name = selected.name,
47+
files = selected.files,
48+
opts = selected.opts,
49+
parser = selected.parser,
50+
})
51+
:make({ chat = self.Chat, force = true })
52+
end
53+
54+
---Render the slash command for the CLI interaction
55+
---@param _ table
56+
---@param callback fun(paths: string[]) Called with a table of relative file paths
57+
---@return nil
58+
function SlashCommand.cli_render(_, callback)
59+
picker(nil, function(selected)
60+
local rule = rules.new({
61+
files = selected.files,
62+
name = selected.name,
63+
opts = selected.opts,
64+
parser = "cli",
65+
})
66+
67+
-- Strip file-level parsers so the group-level cli parser handles all files
68+
local files = rule:read_files(rule:resolve_paths())
69+
vim.iter(files):each(function(f)
70+
f.parser = nil
71+
end)
72+
rule.processed = rule:parse_files(files)
73+
74+
local added = {}
75+
local paths = {}
76+
77+
for _, file in ipairs(files) do
78+
local path = file.path
79+
if not added[path] then
80+
added[path] = true
81+
table.insert(paths, path)
82+
end
83+
end
84+
85+
for _, f in ipairs(rule.processed) do
86+
if f.meta and f.meta.included_files then
87+
for _, included in ipairs(f.meta.included_files) do
88+
if not added[included] then
89+
added[included] = true
90+
table.insert(paths, included)
91+
end
92+
end
93+
end
94+
end
95+
96+
callback(paths)
97+
end)
98+
end
99+
100+
return SlashCommand

0 commit comments

Comments
 (0)