bone-agent 0.1.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/LICENSE +21 -0
- package/README.md +19 -0
- package/bin/bone-linux-x64 +0 -0
- package/bin/bone.js +57 -0
- package/defaults/lua/commands/compact.lua +360 -0
- package/defaults/lua/commands/customize.lua +143 -0
- package/defaults/lua/commands/memory.lua +164 -0
- package/defaults/lua/commands/review.lua +31 -0
- package/defaults/lua/commands/usage.lua +118 -0
- package/defaults/lua/tools/ask_user.lua +306 -0
- package/defaults/lua/tools/cron.lua +253 -0
- package/defaults/lua/tools/subagent.lua +350 -0
- package/defaults/lua/tools/task_list.lua +189 -0
- package/defaults/lua/tools/web_search.lua +41 -0
- package/install.js +43 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Vincent Miranda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# bone-agent
|
|
2
|
+
|
|
3
|
+
A Rust+Lua terminal AI coding assistant.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g bone-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bone
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Config
|
|
18
|
+
|
|
19
|
+
User config lives in `~/.bone-rust/`. Lua files are extracted there on first install and persist across npm updates.
|
|
Binary file
|
package/bin/bone.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bone-rs - npm wrapper for the Rust binary
|
|
4
|
+
* Finds the correct platform binary and executes it.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
// Resolve package directory from package.json location
|
|
13
|
+
let packageDir;
|
|
14
|
+
try {
|
|
15
|
+
packageDir = path.dirname(require.resolve('../package.json'));
|
|
16
|
+
} catch {
|
|
17
|
+
// Fallback: walk up from this file
|
|
18
|
+
packageDir = __dirname;
|
|
19
|
+
while (packageDir !== path.dirname(packageDir)) {
|
|
20
|
+
if (fs.existsSync(path.join(packageDir, 'package.json'))) break;
|
|
21
|
+
packageDir = path.dirname(packageDir);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getBinaryPath() {
|
|
26
|
+
const plat = os.platform();
|
|
27
|
+
const arch = os.arch();
|
|
28
|
+
const ext = plat === 'win32' ? '.exe' : '';
|
|
29
|
+
const key = `${plat}-${arch}`;
|
|
30
|
+
|
|
31
|
+
const binPath = path.join(packageDir, 'bin', `bone-${key}${ext}`);
|
|
32
|
+
if (fs.existsSync(binPath)) return binPath;
|
|
33
|
+
|
|
34
|
+
// Fallback for arm64 on darwin (Apple Silicon) — try x64 Rosetta binary
|
|
35
|
+
if (plat === 'darwin' && arch === 'arm64') {
|
|
36
|
+
const x64Path = path.join(packageDir, 'bin', `bone-darwin-x64${ext}`);
|
|
37
|
+
if (fs.existsSync(x64Path)) return x64Path;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const binPath = getBinaryPath();
|
|
44
|
+
if (!binPath) {
|
|
45
|
+
console.error(
|
|
46
|
+
`bone-rs: no binary found for ${os.platform()}-${os.arch()}. ` +
|
|
47
|
+
`Supported: linux-x64, darwin-x64, darwin-arm64, win-x64`
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = spawnSync(binPath, process.argv.slice(2), {
|
|
53
|
+
stdio: 'inherit',
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
-- /compact — manual context compaction and automatic before-turn reduction.
|
|
2
|
+
--
|
|
3
|
+
-- Implemented entirely in Lua. Remove or edit this file to disable or
|
|
4
|
+
-- customize compaction behaviour.
|
|
5
|
+
--
|
|
6
|
+
-- Requires: ctx.conversation.history(), ctx.agent.run(), ctx.usage.snapshot(),
|
|
7
|
+
-- action = "conversation.replace", bone.on("before_turn", ...)
|
|
8
|
+
|
|
9
|
+
-- ---------------------------------------------------------------------------
|
|
10
|
+
-- Configuration — read from config/general.yaml.
|
|
11
|
+
-- ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
local function config_int(ctx, key)
|
|
14
|
+
if not ctx.config or not ctx.config.get then
|
|
15
|
+
return nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
local value = ctx.config.get("general", key)
|
|
19
|
+
if value == nil then
|
|
20
|
+
return nil
|
|
21
|
+
end
|
|
22
|
+
if type(value) == "string" then
|
|
23
|
+
value = value:gsub("^%s+", ""):gsub("%s+$", "")
|
|
24
|
+
if value == "" then
|
|
25
|
+
return nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
local number = tonumber(value)
|
|
30
|
+
if not number or number < 1 or number ~= math.floor(number) then
|
|
31
|
+
return nil
|
|
32
|
+
end
|
|
33
|
+
return number
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
local function compact_config(ctx)
|
|
37
|
+
return {
|
|
38
|
+
auto_tokens = config_int(ctx, "auto_compact_tokens"),
|
|
39
|
+
keep_messages = config_int(ctx, "auto_compact_keep_messages"),
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
-- ---------------------------------------------------------------------------
|
|
44
|
+
-- Helpers
|
|
45
|
+
-- ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
local function truncate_utf8(s, max_bytes)
|
|
48
|
+
if #s <= max_bytes then
|
|
49
|
+
return s
|
|
50
|
+
end
|
|
51
|
+
for cut = max_bytes, math.max(max_bytes - 4, 1), -1 do
|
|
52
|
+
local chunk = s:sub(1, cut)
|
|
53
|
+
local ok, len = pcall(utf8.len, chunk)
|
|
54
|
+
if ok and len then
|
|
55
|
+
return chunk .. "..."
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
return "..."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
--- Build a summary prompt for the model to condense older messages.
|
|
62
|
+
local function summarization_prompt(older, recent_count)
|
|
63
|
+
local parts = {
|
|
64
|
+
"You are a context summarizer. Summarize the conversation below into a compact description.",
|
|
65
|
+
"",
|
|
66
|
+
"Instructions:",
|
|
67
|
+
"- Capture key facts, decisions, and user preferences.",
|
|
68
|
+
"- Include file paths, code changes, and errors when relevant.",
|
|
69
|
+
"- Write a concise summary in plain prose, no markdown headings.",
|
|
70
|
+
"",
|
|
71
|
+
"The last " .. recent_count .. " user/assistant messages, plus any matching tool results, are preserved verbatim and will follow this summary.",
|
|
72
|
+
"",
|
|
73
|
+
"--- Conversation to summarize ---",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for _, msg in ipairs(older) do
|
|
77
|
+
local role = msg.role or "unknown"
|
|
78
|
+
local content = truncate_utf8(msg.content or "", 1997)
|
|
79
|
+
parts[#parts + 1] = string.format("[%s] %s", role, content)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return table.concat(parts, "\n")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
--- Count the approximate token count of a string (chars / 4).
|
|
86
|
+
local function estimate_tokens(s)
|
|
87
|
+
return math.ceil(#s / 4)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
-- ---------------------------------------------------------------------------
|
|
91
|
+
-- Core compaction logic
|
|
92
|
+
-- ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
local function sanitize_tool_chains(messages)
|
|
95
|
+
-- Pass 1: collect tool_call_ids that have results.
|
|
96
|
+
local result_ids = {}
|
|
97
|
+
for _, msg in ipairs(messages) do
|
|
98
|
+
if msg.role == "tool" and msg.tool_call_id then
|
|
99
|
+
result_ids[msg.tool_call_id] = true
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
-- Pass 2: filter assistant tool_calls; collect which ids are kept.
|
|
104
|
+
local kept_call_ids = {}
|
|
105
|
+
local filtered = {}
|
|
106
|
+
for _, msg in ipairs(messages) do
|
|
107
|
+
if msg.role == "assistant" and msg.tool_calls then
|
|
108
|
+
local calls = {}
|
|
109
|
+
for _, call in ipairs(msg.tool_calls) do
|
|
110
|
+
if call.id and result_ids[call.id] then
|
|
111
|
+
calls[#calls + 1] = call
|
|
112
|
+
kept_call_ids[call.id] = true
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
if #calls > 0 then
|
|
116
|
+
local copy = {}
|
|
117
|
+
for k, v in pairs(msg) do copy[k] = v end
|
|
118
|
+
copy.tool_calls = calls
|
|
119
|
+
filtered[#filtered + 1] = copy
|
|
120
|
+
elseif msg.content and msg.content ~= "" then
|
|
121
|
+
local copy = {}
|
|
122
|
+
for k, v in pairs(msg) do copy[k] = v end
|
|
123
|
+
copy.tool_calls = nil
|
|
124
|
+
filtered[#filtered + 1] = copy
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
filtered[#filtered + 1] = msg
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
-- Pass 3: filter tool results to only those whose call id was kept.
|
|
132
|
+
local result = {}
|
|
133
|
+
for _, msg in ipairs(filtered) do
|
|
134
|
+
if msg.role == "tool" then
|
|
135
|
+
if msg.tool_call_id and kept_call_ids[msg.tool_call_id] then
|
|
136
|
+
result[#result + 1] = msg
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
result[#result + 1] = msg
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
--- Run compaction on the current transcript. Returns the replacement messages
|
|
147
|
+
--- table, or nil on failure / when history is already small enough.
|
|
148
|
+
local function compact(history, ctx, keep_messages)
|
|
149
|
+
if not history or #history == 0 then
|
|
150
|
+
return nil
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
-- Filter to user+assistant for the keep window; tool messages between
|
|
154
|
+
-- user/assistant pairs are fragile to reorder, so for v1 we drop them
|
|
155
|
+
-- from the replacement and let the model see only user/assistant.
|
|
156
|
+
local keep = {}
|
|
157
|
+
local older = {}
|
|
158
|
+
|
|
159
|
+
-- Pass 1: walk backward to find which user/assistant messages are in the
|
|
160
|
+
-- keep window, and collect tool_call_ids from kept assistants so we can
|
|
161
|
+
-- correctly route tool results (a tool result should be kept only if its
|
|
162
|
+
-- matching assistant is in keep).
|
|
163
|
+
local keep_indices = {}
|
|
164
|
+
local kept_call_ids = {}
|
|
165
|
+
local kept = 0
|
|
166
|
+
for i = #history, 1, -1 do
|
|
167
|
+
local msg = history[i]
|
|
168
|
+
if msg.role == "user" or msg.role == "assistant" then
|
|
169
|
+
kept = kept + 1
|
|
170
|
+
if kept <= keep_messages then
|
|
171
|
+
keep_indices[i] = true
|
|
172
|
+
if msg.tool_calls then
|
|
173
|
+
for _, call in ipairs(msg.tool_calls) do
|
|
174
|
+
if call.id then
|
|
175
|
+
kept_call_ids[call.id] = true
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
-- Pass 2: assign messages to keep or older using the collected data.
|
|
184
|
+
for i = #history, 1, -1 do
|
|
185
|
+
local msg = history[i]
|
|
186
|
+
if keep_indices[i] then
|
|
187
|
+
keep[#keep + 1] = msg
|
|
188
|
+
elseif msg.role == "tool" and msg.tool_call_id and kept_call_ids[msg.tool_call_id] then
|
|
189
|
+
-- This tool result belongs to an assistant in keep. Keep it
|
|
190
|
+
-- regardless of its position (it may trail the last kept user msg).
|
|
191
|
+
keep[#keep + 1] = msg
|
|
192
|
+
else
|
|
193
|
+
table.insert(older, 1, msg)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
-- If nothing to compact, skip.
|
|
198
|
+
if #older == 0 then
|
|
199
|
+
return nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
-- Build the summary via ctx.agent.run().
|
|
203
|
+
local prompt = summarization_prompt(older, keep_messages)
|
|
204
|
+
local run_result = ctx.agent.run(prompt, { timeout_ms = 120000 })
|
|
205
|
+
if not run_result.ok then
|
|
206
|
+
ctx.ui.notify("compact: summarization failed: " .. (run_result.error or "unknown"), "warn")
|
|
207
|
+
return nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
local summary = (run_result.content or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
|
211
|
+
if #summary == 0 then
|
|
212
|
+
ctx.ui.notify("compact: empty summary, skipping", "warn")
|
|
213
|
+
return nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
-- Build replacement messages: synthetic user summary + preserved messages
|
|
217
|
+
-- (the keep array was built backward, so reverse it).
|
|
218
|
+
local messages = {}
|
|
219
|
+
messages[#messages + 1] = {
|
|
220
|
+
role = "user",
|
|
221
|
+
content = "[Context summary]\n" .. summary,
|
|
222
|
+
}
|
|
223
|
+
for i = #keep, 1, -1 do
|
|
224
|
+
messages[#messages + 1] = keep[i]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
return sanitize_tool_chains(messages)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
-- ---------------------------------------------------------------------------
|
|
231
|
+
-- Auto-compaction: before_turn hook
|
|
232
|
+
-- ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
local last_auto_context = {}
|
|
235
|
+
|
|
236
|
+
bone.on("before_turn", function(event, ctx)
|
|
237
|
+
-- Safety: skip if usage or conversation APIs are unavailable.
|
|
238
|
+
if not ctx.usage or not ctx.usage.snapshot then
|
|
239
|
+
return nil
|
|
240
|
+
end
|
|
241
|
+
if not ctx.conversation or not ctx.conversation.history then
|
|
242
|
+
return nil
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
-- Check that the compact command is enabled (respects /config toggle).
|
|
246
|
+
local compact_enabled = ctx.config.get("commands", "compact")
|
|
247
|
+
if compact_enabled ~= true then
|
|
248
|
+
return nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
local config = compact_config(ctx)
|
|
252
|
+
if not config.auto_tokens or not config.keep_messages then
|
|
253
|
+
return nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
local snapshot = ctx.usage.snapshot()
|
|
257
|
+
if not snapshot then
|
|
258
|
+
return nil
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
local context_length = snapshot.context_length or 0
|
|
262
|
+
if context_length < config.auto_tokens then
|
|
263
|
+
return nil
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
local conv = ctx.conversation.current and ctx.conversation.current() or nil
|
|
267
|
+
local context_key = conv and conv.id or "default"
|
|
268
|
+
local previous_context = last_auto_context[context_key]
|
|
269
|
+
-- Avoid repeated runs when context_length hasn't changed meaningfully.
|
|
270
|
+
if previous_context and math.abs(context_length - previous_context) < 50 then
|
|
271
|
+
return nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
local history = ctx.conversation.history()
|
|
275
|
+
if not history then
|
|
276
|
+
return nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
local messages = compact(history, ctx, config.keep_messages)
|
|
280
|
+
if not messages then
|
|
281
|
+
return nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
local compacted_tokens = estimate_tokens(cjson.encode(messages))
|
|
285
|
+
last_auto_context[context_key] = compacted_tokens
|
|
286
|
+
|
|
287
|
+
ctx.ui.notify(
|
|
288
|
+
string.format(
|
|
289
|
+
"compacting: %d messages → %d (context: %d → ~%d tokens)",
|
|
290
|
+
#history, #messages, context_length, compacted_tokens
|
|
291
|
+
),
|
|
292
|
+
"info"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
action = "conversation.replace",
|
|
297
|
+
messages = messages,
|
|
298
|
+
}
|
|
299
|
+
end)
|
|
300
|
+
|
|
301
|
+
-- ---------------------------------------------------------------------------
|
|
302
|
+
-- Manual /compact command
|
|
303
|
+
-- ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
bone.register_command("compact", {
|
|
306
|
+
description = "Manually compact conversation context by summarizing older messages",
|
|
307
|
+
handler = function(_, ctx)
|
|
308
|
+
if not ctx.conversation or not ctx.conversation.history then
|
|
309
|
+
return {
|
|
310
|
+
display = "Conversation history not available in this context.",
|
|
311
|
+
submit = false,
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
local config = compact_config(ctx)
|
|
316
|
+
if not config.keep_messages then
|
|
317
|
+
return {
|
|
318
|
+
display = "Compaction requires auto_compact_keep_messages in general config.",
|
|
319
|
+
submit = false,
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
local history = ctx.conversation.history()
|
|
324
|
+
if not history or #history == 0 then
|
|
325
|
+
return { display = "Nothing to compact.", submit = false }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
-- Check if there's enough to compact: need more than configured keep messages.
|
|
329
|
+
local user_assistant_count = 0
|
|
330
|
+
for _, msg in ipairs(history) do
|
|
331
|
+
if msg.role == "user" or msg.role == "assistant" then
|
|
332
|
+
user_assistant_count = user_assistant_count + 1
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
if user_assistant_count <= config.keep_messages then
|
|
336
|
+
return {
|
|
337
|
+
display = string.format(
|
|
338
|
+
"History is already small (%d user+assistant messages; threshold: %d).",
|
|
339
|
+
user_assistant_count, config.keep_messages
|
|
340
|
+
),
|
|
341
|
+
submit = false,
|
|
342
|
+
}
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
local messages = compact(history, ctx, config.keep_messages)
|
|
346
|
+
if not messages then
|
|
347
|
+
return { display = "Compaction produced no changes.", submit = false }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
display = string.format(
|
|
352
|
+
"Compacted: %d messages → %d (~%d tokens).",
|
|
353
|
+
#history, #messages, estimate_tokens(cjson.encode(messages))
|
|
354
|
+
),
|
|
355
|
+
action = "conversation.replace",
|
|
356
|
+
messages = messages,
|
|
357
|
+
submit = false,
|
|
358
|
+
}
|
|
359
|
+
end,
|
|
360
|
+
})
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
-- /customize — quick-start guide for asking bone to customize itself.
|
|
2
|
+
|
|
3
|
+
local guide = [[
|
|
4
|
+
Customize bone
|
|
5
|
+
══════════════
|
|
6
|
+
|
|
7
|
+
Ask for the outcome you want in plain language. Bone can inspect the
|
|
8
|
+
config, explain the options, make the change, and tell you how to reload.
|
|
9
|
+
|
|
10
|
+
── Configs ──────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
YAML files that control bone's behavior. Stored in your config
|
|
13
|
+
directory (~/.bone-rust/).
|
|
14
|
+
|
|
15
|
+
providers.yaml — LLM providers and models. Change which
|
|
16
|
+
provider to use, switch models, set defaults.
|
|
17
|
+
Example: swap "openai" for "anthropic", or
|
|
18
|
+
change the default model from gpt-4o to claude-sonnet-4-20250514.
|
|
19
|
+
|
|
20
|
+
command-policy.yaml — Shell command safety tiers. Controls which
|
|
21
|
+
commands auto-run vs. require approval.
|
|
22
|
+
Example: mark git commands as "safe" to skip
|
|
23
|
+
approval, or make all file writes require "danger" clearance.
|
|
24
|
+
|
|
25
|
+
config/*.yaml — Feature toggles and thresholds.
|
|
26
|
+
Example: set auto-compaction token limits, adjust
|
|
27
|
+
memory update frequency, or change the max tool
|
|
28
|
+
nesting depth.
|
|
29
|
+
|
|
30
|
+
── Commands ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
Lua scripts in lua/commands/ that add slash commands like /compact
|
|
33
|
+
or /memory. Run on demand from the chat.
|
|
34
|
+
|
|
35
|
+
What you can change:
|
|
36
|
+
• Rename or remove bundled commands (e.g. drop /compact entirely).
|
|
37
|
+
• Change command behavior — tighten compaction thresholds, alter
|
|
38
|
+
what /memory summarizes, or change output formatting.
|
|
39
|
+
• Add new commands — a /release checklist, a /git-status summary,
|
|
40
|
+
a /find-dead-code helper.
|
|
41
|
+
|
|
42
|
+
Commands get a full ctx with shell access, file I/O, agent spawning,
|
|
43
|
+
and session history.
|
|
44
|
+
|
|
45
|
+
── Tools ────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
Lua scripts in lua/tools/ that extend what the LLM can do. Each
|
|
48
|
+
tool has a name, description, typed parameters, and an execute
|
|
49
|
+
function.
|
|
50
|
+
|
|
51
|
+
What you can change:
|
|
52
|
+
• Modify existing tools — tighten web_search result limits, add
|
|
53
|
+
filtering to task_list, change cron's default timeout.
|
|
54
|
+
• Add new tools — a GitHub issue search tool, a database query
|
|
55
|
+
wrapper, a project-specific linter runner.
|
|
56
|
+
• Change safety level — mark your custom tool "safe" for auto-run
|
|
57
|
+
or "danger" for approval-gated execution.
|
|
58
|
+
• Control TUI display — show/hide panes, customize what args and
|
|
59
|
+
results appear in the interface.
|
|
60
|
+
|
|
61
|
+
Tools are the LLM's primary interface to the outside world: shell,
|
|
62
|
+
filesystem, other tools, subagents, and more.
|
|
63
|
+
|
|
64
|
+
── Lua (init.lua) ──────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
A startup script in the config directory that runs once when bone
|
|
67
|
+
launches. Use it to register custom tools, subagents, commands, and
|
|
68
|
+
event hooks.
|
|
69
|
+
|
|
70
|
+
What you can do:
|
|
71
|
+
• Register subagents — declare a researcher, reviewer, or test
|
|
72
|
+
verifier with its own system prompt, provider, and model.
|
|
73
|
+
• Register event hooks — run code before each turn, after errors,
|
|
74
|
+
or on other lifecycle events.
|
|
75
|
+
• Set up one-time initialization — create config files, seed
|
|
76
|
+
templates, or log startup diagnostics.
|
|
77
|
+
|
|
78
|
+
Errors in init.lua are non-fatal — bone logs a warning and continues
|
|
79
|
+
without Lua support.
|
|
80
|
+
|
|
81
|
+
── ctx (the context object) ────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
Passed to every tool and command handler. Gives your Lua code access
|
|
84
|
+
to bone's internals. Not all fields are available everywhere.
|
|
85
|
+
|
|
86
|
+
Key capabilities:
|
|
87
|
+
• ctx.config — read values from YAML config files (read-only).
|
|
88
|
+
• ctx.fs, ctx.read_file, ctx.write_file — filesystem operations.
|
|
89
|
+
• ctx.shell, ctx.shell_streaming — run commands through the approval
|
|
90
|
+
pipeline.
|
|
91
|
+
• ctx.tools.call — invoke other registered tools by name.
|
|
92
|
+
• ctx.agent.run, ctx.agent.spawn — create and manage subagents.
|
|
93
|
+
• ctx.state — session-scoped key-value store for persisting data
|
|
94
|
+
across tool calls.
|
|
95
|
+
• ctx.conversation — read the active chat transcript.
|
|
96
|
+
• ctx.usage.snapshot — check token counts and costs.
|
|
97
|
+
• ctx.ui.notify, ctx.ui.status — send messages to the user.
|
|
98
|
+
|
|
99
|
+
Context availability varies:
|
|
100
|
+
• Tools get the full ctx (shell, files, tools, agents, etc.).
|
|
101
|
+
• Commands get most of the same, but no live event emission.
|
|
102
|
+
• Event hooks get a minimal ctx — only config_dir, ui.notify, and
|
|
103
|
+
config.dir. They cannot run shell commands or read files.
|
|
104
|
+
|
|
105
|
+
── Prompt examples ─────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
Make reviews stricter about security and race conditions.
|
|
108
|
+
Add a command that prepares a release checklist.
|
|
109
|
+
Create a tool that searches my issue tracker.
|
|
110
|
+
Use a quieter, more direct assistant style.
|
|
111
|
+
Ask before running commands that modify files.
|
|
112
|
+
Show me the config files involved before changing anything.
|
|
113
|
+
Add a subagent for test verification.
|
|
114
|
+
Remember that I prefer small targeted fixes.
|
|
115
|
+
|
|
116
|
+
Helpful phrases:
|
|
117
|
+
|
|
118
|
+
Explain the current behavior first, then change it.
|
|
119
|
+
Keep this project-agnostic.
|
|
120
|
+
Make the smallest change that works.
|
|
121
|
+
Remove anything unused after the change.
|
|
122
|
+
Verify it with the right command when done.
|
|
123
|
+
|
|
124
|
+
Common areas to customize:
|
|
125
|
+
|
|
126
|
+
Providers and models
|
|
127
|
+
Tools and command approval
|
|
128
|
+
Slash commands
|
|
129
|
+
Subagents
|
|
130
|
+
Memory and assistant style
|
|
131
|
+
Status, usage, and UI settings
|
|
132
|
+
|
|
133
|
+
If you are unsure what to ask, start with:
|
|
134
|
+
|
|
135
|
+
Look at my bone config and suggest practical customizations for how I work.
|
|
136
|
+
]]
|
|
137
|
+
|
|
138
|
+
bone.register_command("customize", {
|
|
139
|
+
description = "Quick-start guide to customizing bone",
|
|
140
|
+
handler = function()
|
|
141
|
+
return { display = guide, submit = false }
|
|
142
|
+
end,
|
|
143
|
+
})
|