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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
local DEFAULT_NAME = "Tasks"
|
|
2
|
+
|
|
3
|
+
local function styled_line(text, done)
|
|
4
|
+
if done then
|
|
5
|
+
return {
|
|
6
|
+
spans = {
|
|
7
|
+
{ text = " ✓ ", fg = "green", modifiers = { "bold" } },
|
|
8
|
+
{ text = text, fg = "dark_gray", modifiers = { "strike" } },
|
|
9
|
+
},
|
|
10
|
+
}
|
|
11
|
+
end
|
|
12
|
+
return {
|
|
13
|
+
spans = {
|
|
14
|
+
{ text = " ○ ", fg = "dark_gray" },
|
|
15
|
+
{ text = text, fg = "white" },
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
local function emit(state)
|
|
21
|
+
local tasks = state.tasks or {}
|
|
22
|
+
local done = 0
|
|
23
|
+
for _, t in ipairs(tasks) do
|
|
24
|
+
if t[2] then done = done + 1 end
|
|
25
|
+
end
|
|
26
|
+
local total = #tasks
|
|
27
|
+
local name = state.name or DEFAULT_NAME
|
|
28
|
+
|
|
29
|
+
local lines = {}
|
|
30
|
+
for _, t in ipairs(tasks) do
|
|
31
|
+
table.insert(lines, styled_line(t[1], t[2]))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
local output = {
|
|
35
|
+
content = string.format("%d/%d done", done, total),
|
|
36
|
+
state = cjson.encode(state),
|
|
37
|
+
pane = {
|
|
38
|
+
source = "task_list",
|
|
39
|
+
title = string.format("%s (%d/%d)", name, done, total),
|
|
40
|
+
visible_rows = 8,
|
|
41
|
+
scroll = 0,
|
|
42
|
+
lines = lines,
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
return cjson.encode(output)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
local function get_texts(params)
|
|
49
|
+
local texts = params.texts or {}
|
|
50
|
+
return texts
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
local function get_indices(params)
|
|
54
|
+
if params.indices then
|
|
55
|
+
return params.indices
|
|
56
|
+
end
|
|
57
|
+
if params.index then
|
|
58
|
+
return { params.index }
|
|
59
|
+
end
|
|
60
|
+
return {}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
local function execute(params, ctx)
|
|
64
|
+
local action = params.action or ""
|
|
65
|
+
|
|
66
|
+
if action == "kill" then
|
|
67
|
+
ctx.state.clear("task_list")
|
|
68
|
+
return cjson.encode({
|
|
69
|
+
content = "Task list cleared.",
|
|
70
|
+
pane = {
|
|
71
|
+
source = "task_list",
|
|
72
|
+
title = DEFAULT_NAME,
|
|
73
|
+
lines = {},
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if action == "create" then
|
|
79
|
+
local texts = get_texts(params)
|
|
80
|
+
if #texts == 0 then
|
|
81
|
+
return "ERROR: Provide texts for 'create'."
|
|
82
|
+
end
|
|
83
|
+
if #texts > 15 then
|
|
84
|
+
return "ERROR: Maximum 15 tasks allowed."
|
|
85
|
+
end
|
|
86
|
+
local name = params.name or DEFAULT_NAME
|
|
87
|
+
local tasks = {}
|
|
88
|
+
for _, t in ipairs(texts) do
|
|
89
|
+
table.insert(tasks, { t, false })
|
|
90
|
+
end
|
|
91
|
+
local state = { name = name, tasks = tasks }
|
|
92
|
+
ctx.state.set("task_list", cjson.encode(state))
|
|
93
|
+
return emit(state)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
if action == "complete" then
|
|
97
|
+
local raw = ctx.state.get("task_list")
|
|
98
|
+
if not raw or raw == "" then
|
|
99
|
+
return "ERROR: No task list state found. Create one first with action=create."
|
|
100
|
+
end
|
|
101
|
+
local state = cjson.decode(raw)
|
|
102
|
+
if not state then
|
|
103
|
+
return "ERROR: Invalid state JSON."
|
|
104
|
+
end
|
|
105
|
+
local indices = get_indices(params)
|
|
106
|
+
if #indices == 0 then
|
|
107
|
+
return "ERROR: Provide index or indices."
|
|
108
|
+
end
|
|
109
|
+
local tasks = state.tasks or {}
|
|
110
|
+
local bad = {}
|
|
111
|
+
for _, idx in ipairs(indices) do
|
|
112
|
+
if idx < 1 or idx > #tasks then
|
|
113
|
+
table.insert(bad, idx)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
if #bad > 0 then
|
|
117
|
+
return string.format("ERROR: Invalid task index/indices: %s", cjson.encode(bad))
|
|
118
|
+
end
|
|
119
|
+
for _, idx in ipairs(indices) do
|
|
120
|
+
tasks[idx][2] = true
|
|
121
|
+
end
|
|
122
|
+
state.tasks = tasks
|
|
123
|
+
ctx.state.set("task_list", cjson.encode(state))
|
|
124
|
+
|
|
125
|
+
local all_done = true
|
|
126
|
+
for _, t in ipairs(tasks) do
|
|
127
|
+
if not t[2] then all_done = false break end
|
|
128
|
+
end
|
|
129
|
+
if all_done then
|
|
130
|
+
local summary = ""
|
|
131
|
+
for i, t in ipairs(tasks) do
|
|
132
|
+
if i > 1 then summary = summary .. ", " end
|
|
133
|
+
summary = summary .. t[1]
|
|
134
|
+
end
|
|
135
|
+
return cjson.encode({
|
|
136
|
+
content = string.format("All tasks completed: %s", summary),
|
|
137
|
+
pane = {
|
|
138
|
+
source = "task_list",
|
|
139
|
+
title = DEFAULT_NAME,
|
|
140
|
+
lines = {},
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
end
|
|
144
|
+
return emit(state)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
return "ERROR: Action must be create, complete, or kill."
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
bone.register_tool({
|
|
151
|
+
name = "task_list",
|
|
152
|
+
description = "Manage a named visible task list. State is held by the host; no state arg needed. Actions: create (pass texts and optional name, max 15 tasks), complete (pass index/indices), kill.",
|
|
153
|
+
safety = "read_only",
|
|
154
|
+
parameters = {
|
|
155
|
+
type = "object",
|
|
156
|
+
properties = {
|
|
157
|
+
action = {
|
|
158
|
+
type = "string",
|
|
159
|
+
description = "create, complete, or kill",
|
|
160
|
+
},
|
|
161
|
+
name = {
|
|
162
|
+
type = "string",
|
|
163
|
+
description = "Optional task list name for create.",
|
|
164
|
+
},
|
|
165
|
+
texts = {
|
|
166
|
+
type = "array",
|
|
167
|
+
description = "Task strings for create.",
|
|
168
|
+
items = { type = "string" },
|
|
169
|
+
},
|
|
170
|
+
index = {
|
|
171
|
+
type = "number",
|
|
172
|
+
description = "Single 1-based task index for complete.",
|
|
173
|
+
},
|
|
174
|
+
indices = {
|
|
175
|
+
type = "array",
|
|
176
|
+
description = "Multiple 1-based task indices for complete.",
|
|
177
|
+
items = { type = "number" },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
required = { "action" },
|
|
181
|
+
additionalProperties = false,
|
|
182
|
+
},
|
|
183
|
+
display = {
|
|
184
|
+
show = false,
|
|
185
|
+
show_result = false,
|
|
186
|
+
args = { "action", "name", "texts", "index", "indices" },
|
|
187
|
+
},
|
|
188
|
+
execute = execute,
|
|
189
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
local function execute(params, ctx)
|
|
2
|
+
local query = params.query
|
|
3
|
+
local num_results = params.num_results or 5
|
|
4
|
+
num_results = math.max(1, math.min(10, num_results))
|
|
5
|
+
|
|
6
|
+
-- Escape for safe embedding in double-quoted shell env var
|
|
7
|
+
local safe_query = query:gsub('\\', '\\\\'):gsub('"', '\\"')
|
|
8
|
+
|
|
9
|
+
local cmd = string.format(
|
|
10
|
+
"export TOOL_QUERY=\"%s\"; export TOOL_NUM_RESULTS=%d; uv run --with ddgs -- python3 -c 'import json, os, sys; from ddgs import DDGS; query = os.environ[\"TOOL_QUERY\"]; num = max(1, min(10, int(os.environ.get(\"TOOL_NUM_RESULTS\", \"5\")))); [print(json.dumps(r)) for r in DDGS().text(query, max_results=num)]'",
|
|
11
|
+
safe_query, num_results
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
local result = ctx.shell(cmd, { timeout_ms = 300000 })
|
|
15
|
+
if result.stderr and #result.stderr > 0 then
|
|
16
|
+
return "ERROR: " .. result.stderr
|
|
17
|
+
end
|
|
18
|
+
return result.stdout or ""
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
bone.register_tool({
|
|
22
|
+
name = "web_search",
|
|
23
|
+
description = "Search the web for information using DuckDuckGo. Returns titles, URLs and summaries. Useful for looking up documentation, current events, technical topics, and general knowledge.",
|
|
24
|
+
parameters = {
|
|
25
|
+
type = "object",
|
|
26
|
+
properties = {
|
|
27
|
+
query = {
|
|
28
|
+
type = "string",
|
|
29
|
+
description = "The search query",
|
|
30
|
+
},
|
|
31
|
+
num_results = {
|
|
32
|
+
type = "number",
|
|
33
|
+
description = "Number of results to return (default 5, max 10)",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
required = { "query" },
|
|
37
|
+
additionalProperties = false,
|
|
38
|
+
},
|
|
39
|
+
safety = "read_only",
|
|
40
|
+
execute = execute,
|
|
41
|
+
})
|
package/install.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* bone-rs install script
|
|
4
|
+
* Extracts bundled Lua files to ~/.bone-rust/ (persists across npm updates).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const packageDir = path.join(__dirname, '..');
|
|
12
|
+
const luaDir = path.join(packageDir, 'defaults', 'lua');
|
|
13
|
+
const boneDir = path.join(os.homedir(), '.bone-rust');
|
|
14
|
+
const luaTarget = path.join(boneDir, 'lua');
|
|
15
|
+
|
|
16
|
+
// Create ~/.bone-rust/ if it doesn't exist
|
|
17
|
+
if (!fs.existsSync(boneDir)) {
|
|
18
|
+
fs.mkdirSync(boneDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Copy bundled Lua files to ~/.bone-rust/lua/ (only if target doesn't exist)
|
|
22
|
+
function copyRecursive(src, dest) {
|
|
23
|
+
const stat = fs.statSync(src);
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
if (!fs.existsSync(dest)) {
|
|
26
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
for (const entry of fs.readdirSync(src)) {
|
|
29
|
+
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
if (!fs.existsSync(dest)) {
|
|
33
|
+
fs.copyFileSync(src, dest);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (fs.existsSync(luaDir)) {
|
|
39
|
+
copyRecursive(luaDir, luaTarget);
|
|
40
|
+
console.log('✓ Lua files installed to ~/.bone-rust/lua/');
|
|
41
|
+
} else {
|
|
42
|
+
console.log('⚠ No bundled Lua files found');
|
|
43
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bone-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Rust+Lua terminal AI coding assistant",
|
|
5
|
+
"bin": {
|
|
6
|
+
"bone": "bin/bone.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin/",
|
|
10
|
+
"defaults/",
|
|
11
|
+
"install.js",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md",
|
|
14
|
+
"package.json"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"install": "node install.js"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ai",
|
|
21
|
+
"coding-assistant",
|
|
22
|
+
"terminal",
|
|
23
|
+
"llm",
|
|
24
|
+
"rust",
|
|
25
|
+
"lua",
|
|
26
|
+
"tui"
|
|
27
|
+
],
|
|
28
|
+
"author": "Vincent Miranda <vincentmiranda65@gmail.com>",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/vincentm65/bone.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/vincentm65/bone/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/vincentm65/bone",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=14.0.0"
|
|
40
|
+
},
|
|
41
|
+
"os": [
|
|
42
|
+
"darwin",
|
|
43
|
+
"linux",
|
|
44
|
+
"win32"
|
|
45
|
+
]
|
|
46
|
+
}
|