@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.
Files changed (60) hide show
  1. package/README.md +288 -163
  2. package/README.md.backup +269 -0
  3. package/bin/dave.js +165 -0
  4. package/examples/CodeServer +43 -0
  5. package/{bin/hdAsk.js → examples/askDave.js} +50 -39
  6. package/{bin/hdCode.js → examples/codeDave.js} +47 -47
  7. package/examples/coderev.js +72 -0
  8. package/examples/daisy.js +177 -0
  9. package/examples/docsDave.js +119 -0
  10. package/examples/gpt.js +54 -72
  11. package/examples/grok.js +47 -68
  12. package/examples/npmDave.js +175 -0
  13. package/examples/promptDave.js +112 -0
  14. package/examples/readmeDave.js +144 -0
  15. package/examples/spawndave.js +240 -0
  16. package/examples/todoDave.js +132 -0
  17. package/lib/API/openai.com/reponses/text.js +12 -18
  18. package/lib/API/x.ai/collections.js +354 -0
  19. package/lib/API/x.ai/files.js +218 -0
  20. package/lib/API/x.ai/responses.js +494 -0
  21. package/lib/API/x.ai/text.js +1 -1
  22. package/lib/AgentClient.js +13 -6
  23. package/lib/AgentManager.js +79 -10
  24. package/lib/AgentServer.js +45 -21
  25. package/lib/Cli.js +7 -1
  26. package/lib/Prompt.js +4 -2
  27. package/lib/ToolSet.js +2 -1
  28. package/lib/genericToolset.js +124 -87
  29. package/lib/index.js +4 -2
  30. package/lib/wsCli.js +257 -0
  31. package/lib/wsIO.js +96 -0
  32. package/package.json +26 -20
  33. package/types/API/openai.com/reponses/text.d.ts +17 -3
  34. package/types/API/x.ai/collections.d.ts +167 -0
  35. package/types/API/x.ai/files.d.ts +84 -0
  36. package/types/API/x.ai/responses.d.ts +379 -0
  37. package/types/AgentClient.d.ts +5 -0
  38. package/types/AgentManager.d.ts +24 -31
  39. package/types/AgentServer.d.ts +5 -1
  40. package/types/Prompt.d.ts +4 -2
  41. package/types/ToolSet.d.ts +1 -0
  42. package/types/index.d.ts +4 -3
  43. package/types/wsCli.d.ts +3 -0
  44. package/types/wsIO.d.ts +26 -0
  45. package/utils/bars.js +40 -0
  46. package/utils/clear_sessions.sh +54 -0
  47. package/{bin/hdInspect.js → utils/format_log.js} +5 -0
  48. package/utils/list_sessions.sh +46 -0
  49. package/utils/search_sessions.sh +73 -0
  50. package/bin/hdClear.js +0 -13
  51. package/bin/hdConnect.js +0 -230
  52. package/bin/hdNpm.js +0 -114
  53. package/bin/hdPrompt.js +0 -108
  54. package/examples/claude-test.js +0 -89
  55. package/examples/claude.js +0 -143
  56. package/examples/gpt_code.js +0 -125
  57. package/examples/gpt_note_keeping.js +0 -117
  58. package/examples/grok_code.js +0 -114
  59. package/examples/grok_note_keeping.js +0 -111
  60. package/module.md +0 -189
@@ -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'|'grok'|'claude'} api - AI endpoint to use
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
- * @param {string} name - the toolname
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 = toolsPool.get(name);
163
+ let tool = toolsPool.get(name);
139
164
  if (!tool) throw new Error('Tool not defined')
140
- this.#prompt.toolset.add(name, tool.description, tool.parameters, tool.method);
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
 
@@ -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
- const ACTIONS = [
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
- return true;
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
- try { clearInterval(pending.timer); } catch {}
217
- pending.resolve(msg.content);
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
- try { clearInterval(pending.timer); } catch {}
227
- pending.reject(msg.content);
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
- const content = await this.#prompt.call(msg.content);
249
- ws.sendToConnection(conn.id, JSON.stringify({
250
- action: 'server_response',
251
- id,
252
- content
253
- }));
254
- console.log(`server_reponse:>>\n${content}\n`);
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(`ws running on port: ${port}`);
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
- cli.startSpinner();
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 });
@@ -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
- // Execute script
77
- response = await SH`node -e "${script}"`.run();
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: `The bash script to execute (${user.system})` }
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: 'Link to open' }
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 a script (bash, nu, python, javascript) on a remote machine via SSH. Default type: nu.',
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: 'The script code to execute remotely' }
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, type = 'bash', script } = params;
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
- let command;
217
- if (type === 'nu') {
218
- const escaped = bashEscape(script);
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