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.
@@ -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
+ }