@j-o-r/hello-dave 0.0.2 → 0.0.3
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/README.md +288 -163
- package/README.md.backup +269 -0
- package/bin/dave.js +165 -0
- package/examples/CodeServer +43 -0
- package/{bin/hdAsk.js → examples/askDave.js} +50 -39
- package/{bin/hdCode.js → examples/codeDave.js} +47 -47
- package/examples/coderev.js +72 -0
- package/examples/daisy.js +177 -0
- package/examples/docsDave.js +119 -0
- package/examples/gpt.js +54 -72
- package/examples/grok.js +47 -68
- package/examples/npmDave.js +175 -0
- package/examples/promptDave.js +112 -0
- package/examples/readmeDave.js +144 -0
- package/examples/spawndave.js +240 -0
- package/examples/todoDave.js +132 -0
- package/lib/API/openai.com/reponses/text.js +12 -18
- package/lib/API/x.ai/collections.js +354 -0
- package/lib/API/x.ai/files.js +218 -0
- package/lib/API/x.ai/responses.js +494 -0
- package/lib/API/x.ai/text.js +1 -1
- package/lib/AgentClient.js +13 -6
- package/lib/AgentManager.js +79 -10
- package/lib/AgentServer.js +45 -21
- package/lib/Cli.js +7 -1
- package/lib/Prompt.js +4 -2
- package/lib/ToolSet.js +2 -1
- package/lib/genericToolset.js +124 -87
- package/lib/index.js +4 -2
- package/lib/wsCli.js +257 -0
- package/lib/wsIO.js +96 -0
- package/package.json +26 -20
- package/types/API/openai.com/reponses/text.d.ts +17 -3
- package/types/API/x.ai/collections.d.ts +167 -0
- package/types/API/x.ai/files.d.ts +84 -0
- package/types/API/x.ai/responses.d.ts +379 -0
- package/types/AgentClient.d.ts +5 -0
- package/types/AgentManager.d.ts +24 -31
- package/types/AgentServer.d.ts +5 -1
- package/types/Prompt.d.ts +4 -2
- package/types/ToolSet.d.ts +1 -0
- package/types/index.d.ts +4 -3
- package/types/wsCli.d.ts +3 -0
- package/types/wsIO.d.ts +26 -0
- package/utils/bars.js +40 -0
- package/utils/clear_sessions.sh +54 -0
- package/{bin/hdInspect.js → utils/format_log.js} +5 -0
- package/utils/list_sessions.sh +46 -0
- package/utils/search_sessions.sh +73 -0
- package/bin/hdClear.js +0 -13
- package/bin/hdConnect.js +0 -230
- package/bin/hdNpm.js +0 -114
- package/bin/hdPrompt.js +0 -108
- package/examples/claude-test.js +0 -89
- package/examples/claude.js +0 -143
- package/examples/gpt_code.js +0 -125
- package/examples/gpt_note_keeping.js +0 -117
- package/examples/grok_code.js +0 -114
- package/examples/grok_note_keeping.js +0 -111
- package/module.md +0 -189
package/lib/AgentManager.js
CHANGED
|
@@ -2,14 +2,15 @@ import { Prompt, Cli, AgentClient, AgentServer, API, ToolSet, Session, env } fro
|
|
|
2
2
|
import toolsPool from './genericToolset.js'
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {import('./API/x.ai/text.js').XOptions} XOptions
|
|
5
|
+
* @typedef {import('./API/x.ai/responses.js').XAIOptions} XAIOptions
|
|
5
6
|
* @typedef {import('./API/openai.com/reponses/text.js').OAOptions} OAOptions
|
|
6
7
|
* @typedef {import('./API/anthropic.com/text.js').ANTHOptions} ANTHOptions
|
|
7
8
|
*/
|
|
8
9
|
/**
|
|
9
10
|
* @typedef {Object} Setup
|
|
10
11
|
* @property {string} prompt - Initial system prompt
|
|
11
|
-
* @property {OAOptions|XOptions|ANTHOptions} options - Model options
|
|
12
|
-
* @property {'gpt'|'
|
|
12
|
+
* @property {XAIOptions|OAOptions|XOptions|ANTHOptions} options - Model options
|
|
13
|
+
* @property {'gpt'|'xai'|'claude'} api - AI endpoint to use
|
|
13
14
|
* @property {number} [contextWindow] - The size in tokens for the context/session
|
|
14
15
|
* @property {'auto'|'required'|void} [toolsetMode] -
|
|
15
16
|
* @property {boolean} [debug] - verbose output
|
|
@@ -17,6 +18,7 @@ import toolsPool from './genericToolset.js'
|
|
|
17
18
|
/**
|
|
18
19
|
* @typedef {Object} Options
|
|
19
20
|
* @property {string} name - Agent name
|
|
21
|
+
* @property {string} secret - Secret string for WS access authorisation (server and client secrets should match)
|
|
20
22
|
* @property {string} [cachePath] - path to session/record storage
|
|
21
23
|
*/
|
|
22
24
|
class AgentManager {
|
|
@@ -25,13 +27,24 @@ class AgentManager {
|
|
|
25
27
|
/** @type {Session} */
|
|
26
28
|
#sessionStorage;
|
|
27
29
|
#name = 'agent';
|
|
30
|
+
#secret = '';
|
|
28
31
|
#cachePath = '.cache/hello-dave';
|
|
29
32
|
#debug = false;
|
|
33
|
+
#model = '';
|
|
34
|
+
#contextWindow = 0;
|
|
35
|
+
#sysPrompt = '';
|
|
30
36
|
/**
|
|
31
37
|
* @param {Options} options
|
|
32
38
|
*/
|
|
33
39
|
constructor(options) {
|
|
34
40
|
this.#name = options.name || this.#name;
|
|
41
|
+
if (options.secret && typeof options.secret === 'string') {
|
|
42
|
+
this.#secret = Buffer.from(options.secret).toString('base64');
|
|
43
|
+
}
|
|
44
|
+
const validNameRegex = /^[a-z_0-9]{2,}$/;
|
|
45
|
+
if (!validNameRegex.test(this.#name)) {
|
|
46
|
+
throw new Error(`Invalid agent 'name' in options: '${this.#name}'. Must match /^[a-z_0-9]{2,}$/ (lowercase a-z, digits, underscores; min 2 chars).`);
|
|
47
|
+
}
|
|
35
48
|
this.#cachePath = options.cachePath || this.#cachePath;
|
|
36
49
|
}
|
|
37
50
|
|
|
@@ -47,6 +60,12 @@ class AgentManager {
|
|
|
47
60
|
toolsetMode = null,
|
|
48
61
|
debug = false
|
|
49
62
|
} = setup;
|
|
63
|
+
this.#sysPrompt = prompt;
|
|
64
|
+
this.#contextWindow = contextWindow;
|
|
65
|
+
if (options?.model) this.#model = options.model;
|
|
66
|
+
this.#sysPrompt = prompt;
|
|
67
|
+
this.#contextWindow = contextWindow;
|
|
68
|
+
if (options?.model) this.#model = options.model;
|
|
50
69
|
let toolset;
|
|
51
70
|
if (toolsetMode) {
|
|
52
71
|
toolset = new ToolSet(toolsetMode);
|
|
@@ -65,7 +84,7 @@ class AgentManager {
|
|
|
65
84
|
* Start a CLI for local usage
|
|
66
85
|
* @param {string} description - introduction message for the CLI user
|
|
67
86
|
*/
|
|
68
|
-
startCli(description) {
|
|
87
|
+
#startCli(description) {
|
|
69
88
|
if (!this.#prompt) throw new Error('Run setup first');
|
|
70
89
|
const cli = new Cli({ prompt: this.#prompt, session: this.#sessionStorage, description });
|
|
71
90
|
setTimeout(() => {
|
|
@@ -82,9 +101,9 @@ class AgentManager {
|
|
|
82
101
|
* @param {string} description - function_call description
|
|
83
102
|
* @param {string} [url='ws://127.0.0.1:8000/ws'] - ws URL
|
|
84
103
|
*/
|
|
85
|
-
attach(name, description, url = 'ws://127.0.0.1:8000/ws') {
|
|
104
|
+
#attach(name, description, url = 'ws://127.0.0.1:8000/ws') {
|
|
86
105
|
if (!this.#prompt) throw new Error('Run setup first');
|
|
87
|
-
return new AgentClient({ prompt: this.#prompt, name, description, url });
|
|
106
|
+
return new AgentClient({ prompt: this.#prompt, name, description, url, secret: this.#secret });
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
/**
|
|
@@ -95,12 +114,13 @@ class AgentManager {
|
|
|
95
114
|
* @param {string} description - function_call description
|
|
96
115
|
* @param {number} [port=8000] - Start the dynamic toolset server on this port
|
|
97
116
|
*/
|
|
98
|
-
enableServer(name, description = '', port = 8000) {
|
|
117
|
+
#enableServer(name, description = '', port = 8000) {
|
|
99
118
|
if (!this.#prompt) throw new Error('Run setup first');
|
|
100
119
|
if (!this.#prompt.toolset) throw new Error('No toolset defined');
|
|
101
120
|
const server = new AgentServer({
|
|
102
121
|
port,
|
|
103
122
|
name,
|
|
123
|
+
secret: this.#secret,
|
|
104
124
|
description,
|
|
105
125
|
prompt: this.#prompt,
|
|
106
126
|
session: this.#sessionStorage,
|
|
@@ -131,13 +151,62 @@ class AgentManager {
|
|
|
131
151
|
return env();
|
|
132
152
|
}
|
|
133
153
|
/**
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
* Add (copy) a pre-defined toolcall to the function_calls
|
|
155
|
+
* @param {'read_file'|'write_file'|'get_user_env'|'execute_bash_script'|'send_email'|'open_link'|'execute_remote_script'|'history_search'|'javascript_interpreter'} name - name /^[a-z_0-9]{2,}$/ e.g. 'execute_bash_script'
|
|
156
|
+
*/
|
|
136
157
|
addGenericToolcall(name) {
|
|
158
|
+
const validNameRegex = /^[a-z_0-9]{2,}$/;
|
|
159
|
+
if (!validNameRegex.test(name)) {
|
|
160
|
+
throw new Error(`Invalid 'name': '${name}'. Must match /^[a-z_0-9]{2,}$/ (lowercase a-z, digits, underscores; min 2 chars).`);
|
|
161
|
+
}
|
|
137
162
|
if (!this.#prompt.toolset) throw new Error('No ToolSet defined')
|
|
138
|
-
let tool
|
|
163
|
+
let tool = toolsPool.get(name);
|
|
139
164
|
if (!tool) throw new Error('Tool not defined')
|
|
140
|
-
|
|
165
|
+
this.#prompt.toolset.add(name, tool.description, tool.parameters, tool.method);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Smart launcher: Dispatches to CLI/server/attach/direct. Auto-gens cliIntro/desc if empty.
|
|
170
|
+
* @param {number} [servePort] - Server port (if no input).
|
|
171
|
+
* @param {string} [connectUrl] - WS to attach. e.g. ws://127.0.0.1:8091/ws
|
|
172
|
+
* @param {string} [cliIntro=""] - CLI intro (auto-gen if falsy).
|
|
173
|
+
* @param {string} [toolName=""] - name /^[a-z_0-9]{2,}$/ Tool name for server/attach (defaults to agent.name).
|
|
174
|
+
* @param {string} [toolDescription=""] - Tool desc (auto-gen if falsy).
|
|
175
|
+
*/
|
|
176
|
+
async start(servePort, connectUrl, cliIntro = "", toolName = "", toolDescription = "") {
|
|
177
|
+
if (!this.#prompt) throw new Error("Run setup first");
|
|
178
|
+
|
|
179
|
+
const validNameRegex = /^[a-z_0-9]{2,}$/;
|
|
180
|
+
|
|
181
|
+
toolName = toolName || this.#name;
|
|
182
|
+
// Validate 'toolName' (or fallback agent name)
|
|
183
|
+
if (!validNameRegex.test(toolName)) {
|
|
184
|
+
throw new Error(`Invalid 'toolName' (or agent #name): '${toolName}'. Must match /^[a-z_0-9]{2,}$/ (lowercase a-z, digits, underscores; min 2 chars).`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!toolDescription && this.#sysPrompt) {
|
|
188
|
+
toolDescription = this.#sysPrompt.slice(0, 100).trim() + "...";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!cliIntro) {
|
|
192
|
+
const introStr = this.#name + " " + (this.#model || "unknown-model") + " - context: " + this.#contextWindow;
|
|
193
|
+
cliIntro = introStr.trim();
|
|
194
|
+
}
|
|
195
|
+
if (servePort) {
|
|
196
|
+
console.log(toolName);
|
|
197
|
+
console.log(toolDescription);
|
|
198
|
+
this.#enableServer(toolName, toolDescription, servePort);
|
|
199
|
+
// A server can also be a client
|
|
200
|
+
if (connectUrl) {
|
|
201
|
+
this.#attach(toolName, toolDescription, connectUrl);
|
|
202
|
+
}
|
|
203
|
+
} else if (connectUrl) {
|
|
204
|
+
// Connect to a agent 'server' and enable via websocket / toolcalls
|
|
205
|
+
this.#attach(toolName, toolDescription, connectUrl);
|
|
206
|
+
} else {
|
|
207
|
+
// Terminal user CLI
|
|
208
|
+
this.#startCli(cliIntro);
|
|
209
|
+
}
|
|
141
210
|
}
|
|
142
211
|
}
|
|
143
212
|
|
package/lib/AgentServer.js
CHANGED
|
@@ -19,11 +19,12 @@ let ws;
|
|
|
19
19
|
* @property {messageContent} content
|
|
20
20
|
*/
|
|
21
21
|
/**
|
|
22
|
-
* @typedef {(conn: import('@j-o-r/apiserver/types/WebSocketServer.js').WebSocketConnection) => Promise<boolean>} AuthFunction
|
|
22
|
+
* @typedef {(conn: import('@j-o-r/apiserver/types/WebSocketServer.js').WebSocketConnection, req: any) => Promise<boolean>} AuthFunction
|
|
23
23
|
*/
|
|
24
24
|
/**
|
|
25
25
|
* @typedef {Object} AgentServerOptions
|
|
26
26
|
* @property {string} name
|
|
27
|
+
* @property {string} secret - server access secret
|
|
27
28
|
* @property {string} description
|
|
28
29
|
* @property {Prompt} prompt
|
|
29
30
|
* @property {Session} session
|
|
@@ -32,7 +33,7 @@ let ws;
|
|
|
32
33
|
* @property {boolean} [debug = false]
|
|
33
34
|
*/
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
const ACTIONS = [
|
|
36
37
|
// Agent / Toolset function calls
|
|
37
38
|
'agent_introduction', // Describe your task, your purpose and usage. This is a reaction on a 'introduce' requests
|
|
38
39
|
'agent_error', // Message error
|
|
@@ -42,6 +43,7 @@ let ws;
|
|
|
42
43
|
'user_introduction', // Websocket client connection on open
|
|
43
44
|
'user_request', // A client user request
|
|
44
45
|
'server_response', // A reponse to a user request
|
|
46
|
+
'server_error', // A reponse to a user request
|
|
45
47
|
'user_reset', // User requests a new session
|
|
46
48
|
'user_sessionlist', // Request a list of sessions
|
|
47
49
|
'user_loadsession', // Request to load a session
|
|
@@ -129,6 +131,7 @@ class AgentServer {
|
|
|
129
131
|
#debug = false;
|
|
130
132
|
#port = 8000;
|
|
131
133
|
#name = '';
|
|
134
|
+
#secret = ''; // access secret
|
|
132
135
|
#description = '';
|
|
133
136
|
/** @type {Prompt} */
|
|
134
137
|
#prompt
|
|
@@ -136,7 +139,9 @@ class AgentServer {
|
|
|
136
139
|
#session
|
|
137
140
|
/** @type {AuthFunction} */
|
|
138
141
|
#auth = async (conn) => {
|
|
139
|
-
|
|
142
|
+
const q = conn.client.query;
|
|
143
|
+
const key = q.get('wssrc_id');
|
|
144
|
+
return key === this.#secret;
|
|
140
145
|
}
|
|
141
146
|
/**
|
|
142
147
|
* @param {AgentServerOptions} options
|
|
@@ -152,16 +157,25 @@ class AgentServer {
|
|
|
152
157
|
this.#auth = options.auth;
|
|
153
158
|
}
|
|
154
159
|
this.#name = options.name;
|
|
160
|
+
this.#secret = options.secret;
|
|
155
161
|
this.#description = options.description;
|
|
156
162
|
this.#session = options.session;
|
|
157
163
|
this.#prompt = options.prompt;
|
|
158
|
-
const events = Object.keys(this.#prompt.EVENTS);
|
|
159
|
-
events.forEach((evt) => {
|
|
160
|
-
this.#prompt.on(evt, (_msg) => {
|
|
161
|
-
console.log(evt);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
164
|
this.#debug = options.debug;
|
|
165
|
+
// LOG EVENTS
|
|
166
|
+
const events = Object.keys(this.#prompt.EVENTS);
|
|
167
|
+
events.forEach((evt) => {
|
|
168
|
+
this.#prompt.on(evt, (_msg) => {
|
|
169
|
+
// log events
|
|
170
|
+
console.log(`** ${this.#name} e:${evt}**`);
|
|
171
|
+
if (evt === 'tool_request') {
|
|
172
|
+
console.log(`tool execute: ${this.#name} ${_msg.name} ${_msg.call_id}`);
|
|
173
|
+
} else if (evt === 'tool_error') {
|
|
174
|
+
console.log(`tool error:`);
|
|
175
|
+
console.log(JSON.stringify(_msg, null, ' '));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
165
179
|
// start
|
|
166
180
|
(async () => {
|
|
167
181
|
await this._start();
|
|
@@ -213,8 +227,8 @@ class AgentServer {
|
|
|
213
227
|
const key = `${conn.id}:${msg.id}`; // Assume client includes original 'id' in response
|
|
214
228
|
const pending = pendingResponses.get(key);
|
|
215
229
|
if (pending) {
|
|
216
|
-
|
|
217
|
-
|
|
230
|
+
try { clearInterval(pending.timer); } catch { }
|
|
231
|
+
pending.resolve(msg.content);
|
|
218
232
|
pendingResponses.delete(key);
|
|
219
233
|
}
|
|
220
234
|
return;
|
|
@@ -223,8 +237,8 @@ class AgentServer {
|
|
|
223
237
|
const key = `${conn.id}:${msg.id}`; // Assume client includes original 'id' in response
|
|
224
238
|
const pending = pendingResponses.get(key);
|
|
225
239
|
if (pending) {
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
try { clearInterval(pending.timer); } catch { }
|
|
241
|
+
pending.reject(msg.content);
|
|
228
242
|
pendingResponses.delete(key);
|
|
229
243
|
}
|
|
230
244
|
return;
|
|
@@ -245,13 +259,23 @@ class AgentServer {
|
|
|
245
259
|
}
|
|
246
260
|
// Bind to THIS prompt
|
|
247
261
|
console.log(`user_request:>>\n${msg.content}\n`);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
try {
|
|
263
|
+
const content = await this.#prompt.call(msg.content);
|
|
264
|
+
ws.sendToConnection(conn.id, JSON.stringify({
|
|
265
|
+
action: 'server_response',
|
|
266
|
+
id,
|
|
267
|
+
content
|
|
268
|
+
}));
|
|
269
|
+
console.log(`server_reponse:>>\n${content}\n`);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
console.log(`server_error:>>\n`);
|
|
272
|
+
console.error(e);
|
|
273
|
+
ws.sendToConnection(conn.id, JSON.stringify({
|
|
274
|
+
action: 'server_error',
|
|
275
|
+
id,
|
|
276
|
+
e
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
255
279
|
}
|
|
256
280
|
if (msg.action === 'user_reset') {
|
|
257
281
|
const id = msg.id;
|
|
@@ -322,7 +346,7 @@ class AgentServer {
|
|
|
322
346
|
}
|
|
323
347
|
});
|
|
324
348
|
await server.create('v1', { port, host: '127.0.0.1', ws, strict: DEBUG, verbose: DEBUG }, API);
|
|
325
|
-
console.log(`
|
|
349
|
+
console.log(`Websocket server: ws://127.0.0.1:${port}/ws`);
|
|
326
350
|
}
|
|
327
351
|
/**
|
|
328
352
|
* Send a reset messages to all clients
|
package/lib/Cli.js
CHANGED
|
@@ -138,24 +138,30 @@ class Cli {
|
|
|
138
138
|
]);
|
|
139
139
|
// Write messages
|
|
140
140
|
this.#prompt.on('message', (msg) => {
|
|
141
|
+
let write = false;
|
|
141
142
|
if (msg.role === 'reasoning') {
|
|
142
143
|
const content = this.#prompt.contentToString(msg.content);
|
|
143
144
|
cli.focus('util');
|
|
144
145
|
cli.write(`REASONING: ---------\n${content}\n---------\n`);
|
|
146
|
+
write = true;
|
|
145
147
|
} else if (msg.role === 'log') {
|
|
146
148
|
const content = this.#prompt.contentToString(msg.content);
|
|
147
149
|
cli.focus('log');
|
|
148
150
|
cli.write(content);
|
|
151
|
+
write = true;
|
|
149
152
|
} else if (msg.role === 'assistant') {
|
|
150
153
|
if (msg.content[0].type === 'text') {
|
|
151
154
|
const content = this.#prompt.contentToString(msg.content);
|
|
152
155
|
if (content.trim() !== '') {
|
|
153
156
|
cli.focus('assistant');
|
|
154
157
|
cli.write(content);
|
|
158
|
+
write = true;
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
|
-
|
|
162
|
+
if (write) {
|
|
163
|
+
cli.startSpinner();
|
|
164
|
+
}
|
|
159
165
|
});
|
|
160
166
|
this.#prompt.on('truncated', () => {
|
|
161
167
|
cli.focus('log');
|
package/lib/Prompt.js
CHANGED
|
@@ -11,15 +11,17 @@ import { pruneResolvedToolIOByCallIdExceptLast as pruneResolvedToolIOByCallIdExc
|
|
|
11
11
|
/**
|
|
12
12
|
* @typedef {import('./API/openai.com/reponses/text.js').request} OARequest
|
|
13
13
|
* @typedef {import('./API/x.ai/text.js').request} XRequest
|
|
14
|
+
* @typedef {import('./API/x.ai/responses.js').request} XAIRequest
|
|
14
15
|
* @typedef {import('./API/anthropic.com/text.js').request} ANTHRequest
|
|
15
16
|
*
|
|
16
17
|
* @typedef {import('./API/x.ai/text.js').XOptions} XOptions
|
|
18
|
+
* @typedef {import('./API/x.ai/responses.js').XAIOptions} XAIOptions
|
|
17
19
|
* @typedef {import('./API/openai.com/reponses/text.js').OAOptions} OAOptions
|
|
18
20
|
* @typedef {import('./API/anthropic.com/text.js').ANTHOptions} ANTHOptions
|
|
19
21
|
*
|
|
20
22
|
* @typedef {import('./ToolSet.js').default} ToolSet
|
|
21
|
-
* @typedef {OARequest|XRequest|ANTHRequest} request - The AI model to use for chat functionality.
|
|
22
|
-
* @typedef {OAOptions|XOptions|ANTHOptions} options - Model options
|
|
23
|
+
* @typedef {OARequest|XRequest|ANTHRequest|XAIRequest} request - The AI model to use for chat functionality.
|
|
24
|
+
* @typedef {OAOptions|XOptions|ANTHOptions|XAIOptions} options - Model options
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
27
|
/**
|
package/lib/ToolSet.js
CHANGED
|
@@ -126,6 +126,7 @@ class ToolSet {
|
|
|
126
126
|
* Execute a method
|
|
127
127
|
* @param {string} name
|
|
128
128
|
* @param {object} params
|
|
129
|
+
* @returns {Promise<*>}
|
|
129
130
|
*/
|
|
130
131
|
async call(name, params) {
|
|
131
132
|
if (!this.has(name)) {
|
|
@@ -158,8 +159,8 @@ class ToolSet {
|
|
|
158
159
|
try {
|
|
159
160
|
response = await this.call(call.function_request.name, JSON.parse(call.function_request.parameters));
|
|
160
161
|
} catch (error) {
|
|
161
|
-
prompt.emit(prompt.EVENTS.tool_error, { name: call.function_request.name, call_id: call.function_request.call_id, error });
|
|
162
162
|
response = `Error: ${error.name} - ${error.message}`;
|
|
163
|
+
prompt.emit(prompt.EVENTS.tool_error, { name: call.function_request.name, call_id: call.function_request.call_id, error: response });
|
|
163
164
|
}
|
|
164
165
|
const duration = new Date().getTime() - startTime;
|
|
165
166
|
prompt.emit(prompt.EVENTS.tool_response, { name: call.function_request.name, call_id: call.function_request.call_id, duration });
|
package/lib/genericToolset.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { SH } from '@j-o-r/sh'
|
|
1
|
+
import { SH, bashEscape } from '@j-o-r/sh'
|
|
2
2
|
import { ToolSet, env } from './index.js'
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
3
9
|
|
|
4
10
|
const user = await env();
|
|
5
11
|
const environment = `
|
|
@@ -33,27 +39,6 @@ const getJSError = (errorStr) => {
|
|
|
33
39
|
return result;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
/**
|
|
37
|
-
* bash escape a string suitable as an argument on the commandline
|
|
38
|
-
* for javascript code
|
|
39
|
-
* @param {string} x
|
|
40
|
-
* @retruns {string}
|
|
41
|
-
*/
|
|
42
|
-
const bashEscape = (x) => {
|
|
43
|
-
let str = String(x).trim();
|
|
44
|
-
// the trick is to double escape escape vars first
|
|
45
|
-
str = str.replace(/\\/g, '\\\\');
|
|
46
|
-
// then add escaping for oddities
|
|
47
|
-
// Replace literal escape sequences like \n, \t, \r in quoted strings
|
|
48
|
-
str = str.replace(/(['"])\\(.)\1/g, '$1\\\\$2$1');
|
|
49
|
-
// escape $ (is a BASH var)
|
|
50
|
-
str = str.replace(/\$/g, '\\$');
|
|
51
|
-
// Escape backticks
|
|
52
|
-
str = str.replace(/`/g, '\\`');
|
|
53
|
-
// Escape quotes
|
|
54
|
-
str = str.replace(/"/g, '\\"');
|
|
55
|
-
return str;
|
|
56
|
-
}
|
|
57
42
|
tools.add(
|
|
58
43
|
'javascript_interpreter',
|
|
59
44
|
`Execute ESM ES6 javascript on \`node\`.`,
|
|
@@ -67,37 +52,21 @@ tools.add(
|
|
|
67
52
|
},
|
|
68
53
|
required: ['script']
|
|
69
54
|
},
|
|
70
|
-
// @ts-ignore
|
|
71
55
|
async (params) => {
|
|
72
|
-
// @ts-ignore
|
|
73
|
-
const script = bashEscape(params.script);
|
|
74
56
|
let response = '';
|
|
75
57
|
try {
|
|
76
|
-
|
|
77
|
-
response = await SH`node -
|
|
58
|
+
const delim = `JS_STDIN_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,6)}`;
|
|
59
|
+
response = await SH`cat <<'${delim}' | node --input-type=module -
|
|
60
|
+
${params.script}
|
|
61
|
+
${delim}
|
|
62
|
+
`.run();
|
|
78
63
|
} catch (e) {
|
|
79
|
-
const errorStr = e.toString()
|
|
80
|
-
response = getJSError(errorStr)
|
|
64
|
+
const errorStr = e.toString();
|
|
65
|
+
response = getJSError(errorStr);
|
|
81
66
|
}
|
|
82
67
|
return response;
|
|
83
68
|
}
|
|
84
69
|
);
|
|
85
|
-
tools.add(
|
|
86
|
-
'bash_cmd',
|
|
87
|
-
`Execute a Bash command on ${user.system}.`,
|
|
88
|
-
{
|
|
89
|
-
type: 'object',
|
|
90
|
-
properties: {
|
|
91
|
-
command: {
|
|
92
|
-
type: 'string',
|
|
93
|
-
description: 'The bash command.',
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
required: ['command']
|
|
97
|
-
},
|
|
98
|
-
// @ts-ignore
|
|
99
|
-
async (params) => (await SH`${params.command}`.run())
|
|
100
|
-
);
|
|
101
70
|
tools.add(
|
|
102
71
|
'get_user_env', // name
|
|
103
72
|
'Get the user location, name and OS environment', // desciption
|
|
@@ -113,11 +82,11 @@ tools.add(
|
|
|
113
82
|
|
|
114
83
|
tools.add(
|
|
115
84
|
'execute_bash_script',
|
|
116
|
-
'Execute a bash script.',
|
|
85
|
+
'Execute a bash script or command. (char escaping not needed)',
|
|
117
86
|
{
|
|
118
87
|
type: 'object',
|
|
119
88
|
properties: {
|
|
120
|
-
bash_script: { type: 'string', description: `
|
|
89
|
+
bash_script: { type: 'string', description: `RAW bash script (LITERAL TEXT: NO ESCAPING—output $, |, <, >, &, ", ', \\, \` , newlines, $(()), [[ ]] verbatim. Heredoc delimiter handles safely. EX: echo "$((1+1)) | grep \'<&>\"hi$USER\"\' && ls -la\`. (${user.system})` }
|
|
121
90
|
},
|
|
122
91
|
required: ['bash_script']
|
|
123
92
|
},
|
|
@@ -129,27 +98,6 @@ ${delim}
|
|
|
129
98
|
`.run()
|
|
130
99
|
}
|
|
131
100
|
);
|
|
132
|
-
/*
|
|
133
|
-
### nu shell Assumptions and Prerequisites
|
|
134
|
-
- You can install it via your package manager (e.g., on Ubuntu: `sudo apt install nushell` or download from the [official Nushell releases](https://github.com/nushell/nushell/releases)).
|
|
135
|
-
- The tool will use a heredoc approach (similar to the Bash tool) to pass the script content to `nu` for execution. This ensures multi-line scripts work reliably.
|
|
136
|
-
- The output from `nu` will be returned as-is, preserving its structured format (e.g., tables or JSON), which is beneficial for LLM consumption.o
|
|
137
|
-
*/
|
|
138
|
-
tools.add(
|
|
139
|
-
'execute_nushell_script',
|
|
140
|
-
'Execute a Nushell script.',
|
|
141
|
-
{
|
|
142
|
-
type: 'object',
|
|
143
|
-
properties: {
|
|
144
|
-
nushell_script: { type: 'string', description: `The Nushell script to execute (${user.system})` }
|
|
145
|
-
},
|
|
146
|
-
required: ['nushell_script']
|
|
147
|
-
},
|
|
148
|
-
async (params) => {
|
|
149
|
-
const script = bashEscape(params.nushell_script);
|
|
150
|
-
return await SH`nu -c "${script}"`.run()
|
|
151
|
-
}
|
|
152
|
-
);
|
|
153
101
|
|
|
154
102
|
tools.add(
|
|
155
103
|
'send_email',
|
|
@@ -176,11 +124,11 @@ ${delim}
|
|
|
176
124
|
);
|
|
177
125
|
tools.add(
|
|
178
126
|
'open_link',
|
|
179
|
-
'Open an url in local user environment.',
|
|
127
|
+
'Open an url or file in the local user environment. (xdg-open)',
|
|
180
128
|
{
|
|
181
129
|
type: 'object',
|
|
182
130
|
properties: {
|
|
183
|
-
url: { type: 'string', description: '
|
|
131
|
+
url: { type: 'string', description: 'file | URL' }
|
|
184
132
|
},
|
|
185
133
|
required: ['url']
|
|
186
134
|
},
|
|
@@ -190,18 +138,17 @@ tools.add(
|
|
|
190
138
|
);
|
|
191
139
|
tools.add(
|
|
192
140
|
'execute_remote_script',
|
|
193
|
-
'Execute
|
|
141
|
+
'Execute bash script on a remote machine via SSH.',
|
|
194
142
|
{
|
|
195
143
|
type: 'object',
|
|
196
144
|
properties: {
|
|
197
|
-
type: { type: 'string', enum: ['bash','nu','python','javascript'], description: 'Type of script (default: bash)' },
|
|
198
145
|
url: { type: 'string', description: 'SSH URL, e.g., ssh://user@host or ssh://user@host:port' },
|
|
199
|
-
script: { type: 'string', description: '
|
|
146
|
+
script: { type: 'string', description: 'RAW script code to execute remotely (no escaping needed)' }
|
|
200
147
|
},
|
|
201
148
|
required: ['url', 'script']
|
|
202
149
|
},
|
|
203
150
|
async (params) => {
|
|
204
|
-
const { url,
|
|
151
|
+
const { url, script } = params;
|
|
205
152
|
if (!url.startsWith('ssh://')) throw new Error('Invalid SSH URL');
|
|
206
153
|
const withoutProto = url.slice(6);
|
|
207
154
|
const parts = withoutProto.split(':');
|
|
@@ -212,23 +159,113 @@ tools.add(
|
|
|
212
159
|
port = parseInt(parts[1]);
|
|
213
160
|
}
|
|
214
161
|
const [user, host] = userHost.split('@');
|
|
215
|
-
if (!user || !host) throw new Error('Invalid SSH URL format');
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
command = `nu -c "${escaped}"`;
|
|
220
|
-
} else {
|
|
221
|
-
const interpreters = { bash: 'bash', python: 'python3', javascript: 'node' };
|
|
222
|
-
const interpreter = interpreters[type];
|
|
223
|
-
if (!interpreter) throw new Error('Unsupported type');
|
|
224
|
-
const delim = `END_REMOTE_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
225
|
-
command = `${interpreter} <<'${delim}'
|
|
162
|
+
if (!user || !host) throw new Error('Invalid SSH URL format: use ssh://user@host[:port]');
|
|
163
|
+
|
|
164
|
+
const delim = `END_SCRIPT_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
165
|
+
return await SH`ssh -p ${port} ${user}@${host} bash <<'${delim}'
|
|
226
166
|
${script}
|
|
227
167
|
${delim}
|
|
228
|
-
|
|
168
|
+
`.run();
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
tools.add(
|
|
173
|
+
'history_search',
|
|
174
|
+
`Search previous LLM chat sessions or list them hierarchically.
|
|
175
|
+
Example query: "(todo|task)" or "package.json".
|
|
176
|
+
Searches filenames & content in .cache/[app]/[prompt]/sessions/*.ndjson (case-insensitive regex, with context).
|
|
177
|
+
Omit query (or use empty string) to list sessions (see utils/list_sessions.sh).`,
|
|
178
|
+
{
|
|
179
|
+
type: 'object',
|
|
180
|
+
properties: {
|
|
181
|
+
query: {
|
|
182
|
+
type: 'string',
|
|
183
|
+
description: `Search query or regex (quoted for multi-word/regex). Omit or empty for list mode.`
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
required: []
|
|
187
|
+
},
|
|
188
|
+
async (params) => {
|
|
189
|
+
const history_search = path.resolve(__dirname, '..', 'utils', 'search_sessions.sh');
|
|
190
|
+
const list_sessions = path.resolve(__dirname, '..', 'utils', 'list_sessions.sh');
|
|
191
|
+
if (typeof params.query === 'string' && params.query.trim() !== '') {
|
|
192
|
+
const escapedQuery = bashEscape(params.query);
|
|
193
|
+
return await SH`${history_search} "${escapedQuery}"`.run();
|
|
194
|
+
} else {
|
|
195
|
+
return await SH`${list_sessions}`.run();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
tools.add(
|
|
200
|
+
'read_file',
|
|
201
|
+
'Read the raw content of a file strictly within the current working directory (CWD). Paths must be relative (no leading /, no ..).',
|
|
202
|
+
{
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
file: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: `Relative path to the file within CWD, e.g., 'path/to/file.txt' (no escaping needed). cwd: ${user.cwd}`
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
required: ['file']
|
|
211
|
+
},
|
|
212
|
+
async (params) => {
|
|
213
|
+
const file = params.file?.trim();
|
|
214
|
+
if (typeof file !== 'string' || !file) {
|
|
215
|
+
throw new Error('Valid relative file path required.');
|
|
216
|
+
}
|
|
217
|
+
if (file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
|
|
218
|
+
throw new Error('Path must be relative within CWD only (no `/`, `..`, or `\\\\`).');
|
|
219
|
+
}
|
|
220
|
+
const resolvedPath = path.resolve(process.cwd(), file);
|
|
221
|
+
if (!resolvedPath.startsWith(process.cwd())) {
|
|
222
|
+
throw new Error(`Path '${file}' escapes CWD scope.`);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const content = await fs.readFile(resolvedPath, 'utf8');
|
|
226
|
+
return content;
|
|
227
|
+
} catch (e) {
|
|
228
|
+
throw new Error(`Failed to read '${file}': ${e.message}`);
|
|
229
229
|
}
|
|
230
|
-
return await SH`ssh -p ${port} ${user}@${host} '${command}'`.run()
|
|
231
230
|
}
|
|
232
231
|
);
|
|
233
232
|
|
|
233
|
+
tools.add(
|
|
234
|
+
'write_file',
|
|
235
|
+
'Write raw content to a file strictly within the current working directory (CWD). Paths must be relative (no leading /, no ..). Content written as-is (no escaping).',
|
|
236
|
+
{
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {
|
|
239
|
+
file: {
|
|
240
|
+
type: 'string',
|
|
241
|
+
description: `Relative path to the file within CWD, e.g., 'path/to/file.txt' (no escaping needed).`
|
|
242
|
+
},
|
|
243
|
+
content: {
|
|
244
|
+
type: 'string',
|
|
245
|
+
description: `Raw content to write (as-is, no char escaping needed; supports newlines, $, |, <, >, &, ", ', \\, etc.).`
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
required: ['file', 'content']
|
|
249
|
+
},
|
|
250
|
+
async (params) => {
|
|
251
|
+
const file = params.file?.trim();
|
|
252
|
+
const content = params.content ?? '';
|
|
253
|
+
if (typeof file !== 'string' || !file) {
|
|
254
|
+
throw new Error('Valid relative file path required.');
|
|
255
|
+
}
|
|
256
|
+
if (file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
|
|
257
|
+
throw new Error('Path must be relative within CWD only (no `/`, `..`, or `\\\\`).');
|
|
258
|
+
}
|
|
259
|
+
const resolvedPath = path.resolve(process.cwd(), file);
|
|
260
|
+
if (!resolvedPath.startsWith(process.cwd())) {
|
|
261
|
+
throw new Error(`Path '${file}' escapes CWD scope.`);
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
await fs.writeFile(resolvedPath, content, 'utf8');
|
|
265
|
+
return `Successfully wrote to '${file}' (${Buffer.byteLength(content, 'utf8')} bytes).`;
|
|
266
|
+
} catch (e) {
|
|
267
|
+
throw new Error(`Failed to write '${file}': ${e.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
);
|
|
234
271
|
export default tools
|