@j-o-r/hello-dave 0.1.1 → 0.1.4

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 (173) hide show
  1. package/CHANGELOG.md +42 -25
  2. package/README.md +81 -221
  3. package/TODO.md +173 -35
  4. package/agents/agent_creator.js +105 -0
  5. package/agents/agent_creator.prompt.md +371 -0
  6. package/agents/ask_agent.js +64 -127
  7. package/agents/claude_agent.js +68 -0
  8. package/agents/code_agent.js +55 -135
  9. package/agents/code_agent.prompt.md +50 -0
  10. package/agents/echo_agent.js +76 -0
  11. package/agents/financial_expert.js +75 -0
  12. package/agents/gpt_agent.js +52 -103
  13. package/agents/gpt_code.js +81 -0
  14. package/agents/grok_agent.js +58 -114
  15. package/agents/minimax_agent.js +92 -0
  16. package/agents/mureka_agent.js +77 -0
  17. package/agents/planner_agent.js +172 -0
  18. package/agents/stability_agent.js +87 -0
  19. package/agents/test_agent.js +75 -157
  20. package/agents/weather_agent.js +73 -0
  21. package/agents/workflow_agent.js +189 -0
  22. package/bin/dave.js +436 -184
  23. package/docs/bin-dave.md +85 -35
  24. package/docs/cdn-ssh.md +100 -0
  25. package/docs/creating-agents.md +301 -0
  26. package/docs/creating-toolsets.md +336 -0
  27. package/docs/docs-organization.md +48 -0
  28. package/docs/project-overview.md +86 -51
  29. package/lib/API/elevenlabs.io/music.compose.md +441 -0
  30. package/lib/API/elevenlabs.io/music.create-composition-plan.md +370 -0
  31. package/lib/API/elevenlabs.io/music.stream.md +425 -0
  32. package/lib/API/lalal.ai/lalal.js +445 -0
  33. package/lib/API/lalal.ai/openapi.json +2614 -0
  34. package/lib/API/minimax/ImageToolset.js +82 -37
  35. package/lib/API/minimax/MusicToolset.js +125 -79
  36. package/lib/API/minimax/VideoToolset.js +170 -167
  37. package/lib/API/minimax/image.js +5 -1
  38. package/lib/API/minimax/music.js +210 -23
  39. package/lib/API/minimax/video.js +242 -53
  40. package/lib/API/mureka/MusicToolset.js +646 -0
  41. package/lib/API/mureka/README.md +41 -0
  42. package/lib/API/mureka/index.js +7 -0
  43. package/lib/API/mureka/music.js +658 -0
  44. package/lib/API/openai.com/index.js +7 -0
  45. package/lib/API/openai.com/{reponses/text.js → responses.js} +64 -18
  46. package/lib/API/openai.com/video.create.character.md +40 -0
  47. package/lib/API/openai.com/video.create.md +219 -0
  48. package/lib/API/openai.com/video.delete.md +44 -0
  49. package/lib/API/openai.com/video.download.md +31 -0
  50. package/lib/API/openai.com/video.edit.md +155 -0
  51. package/lib/API/openai.com/video.extend.md +166 -0
  52. package/lib/API/openai.com/video.fetch.character.md +43 -0
  53. package/lib/API/openai.com/video.js +784 -0
  54. package/lib/API/openai.com/video.list.md +201 -0
  55. package/lib/API/openai.com/video.remix.md +175 -0
  56. package/lib/API/openai.com/video.retrieve.md +139 -0
  57. package/lib/API/openai.com/videoToolset.js +616 -0
  58. package/lib/API/stability.ai/ImageToolset.js +131 -40
  59. package/lib/API/stability.ai/MusicToolset.js +79 -47
  60. package/lib/API/stability.ai/audio.js +63 -131
  61. package/lib/API/x.ai/chat.responses.md +1040 -0
  62. package/lib/API/x.ai/image.js +229 -59
  63. package/lib/API/x.ai/imageToolset.js +376 -0
  64. package/lib/API/x.ai/index.js +1 -1
  65. package/lib/API/x.ai/responses.js +9 -18
  66. package/lib/Agent.js +271 -0
  67. package/lib/Agent.js.old +284 -0
  68. package/lib/AgentLauncher.js +562 -0
  69. package/lib/Cli.js +87 -13
  70. package/lib/Prompt.js +23 -1
  71. package/lib/Session.js +5 -4
  72. package/lib/ToolSet.js +102 -6
  73. package/lib/agentLoader.js +369 -0
  74. package/lib/cdn.js +67 -231
  75. package/lib/{CdnToolset.js → cdnToolset.js} +47 -64
  76. package/lib/defaultToolsets.js +43 -0
  77. package/lib/fafs.js +1 -1
  78. package/lib/genericToolset.js +442 -119
  79. package/lib/handOffToolset.js +179 -0
  80. package/lib/index.js +34 -27
  81. package/lib/toolsetLoader.js +248 -0
  82. package/package.json +11 -5
  83. package/types/API/lalal.ai/lalal.d.ts +116 -0
  84. package/types/API/minimax/image.d.ts +2 -1
  85. package/types/API/minimax/music.d.ts +189 -26
  86. package/types/API/minimax/video.d.ts +100 -31
  87. package/types/API/mureka/index.d.ts +7 -0
  88. package/types/API/mureka/music.d.ts +472 -0
  89. package/types/API/openai.com/index.d.ts +7 -0
  90. package/types/API/openai.com/{reponses/text.d.ts → responses.d.ts} +11 -11
  91. package/types/API/openai.com/video.d.ts +409 -0
  92. package/types/API/openai.com/videoToolset.d.ts +24 -0
  93. package/types/API/stability.ai/audio.d.ts +14 -103
  94. package/types/API/stability.ai/image.d.ts +2 -2
  95. package/types/API/x.ai/image.d.ts +138 -26
  96. package/types/API/x.ai/imageToolset.d.ts +3 -0
  97. package/types/API/x.ai/index.d.ts +1 -1
  98. package/types/API/x.ai/responses.d.ts +4 -4
  99. package/types/Agent.d.ts +123 -0
  100. package/types/AgentLauncher.d.ts +222 -0
  101. package/types/Cli.d.ts +28 -8
  102. package/types/Prompt.d.ts +23 -5
  103. package/types/Session.d.ts +1 -1
  104. package/types/ToolSet.d.ts +10 -0
  105. package/types/agentLoader.d.ts +78 -0
  106. package/types/cdn.d.ts +15 -90
  107. package/types/defaultToolsets.d.ts +9 -0
  108. package/types/fafs.d.ts +1 -1
  109. package/types/genericToolset.d.ts +1 -1
  110. package/types/handOffToolset.d.ts +28 -0
  111. package/types/index.d.ts +19 -17
  112. package/types/toolsetLoader.d.ts +114 -0
  113. package/utils/format_log.js +101 -23
  114. package/utils/launch_agent.js +18 -0
  115. package/utils/list_sessions.sh +13 -5
  116. package/utils/search_sessions.sh +65 -29
  117. package/utils/toolsets.js +33 -0
  118. package/README.md.bak.1779452127 +0 -240
  119. package/agents/codeserver.sh +0 -47
  120. package/agents/daisy_agent.js +0 -173
  121. package/agents/docs_agent.js +0 -148
  122. package/agents/memory_agent.js +0 -263
  123. package/agents/minimax.js +0 -173
  124. package/agents/npm_agent.js +0 -202
  125. package/agents/prompt_agent.js +0 -133
  126. package/agents/readme_agent.js +0 -148
  127. package/agents/spawn_agent.js +0 -160
  128. package/agents/stability.js +0 -173
  129. package/agents/todo_agent.js +0 -175
  130. package/bin/codeDave +0 -58
  131. package/docs/agent-dave-websocket-protocol.md +0 -180
  132. package/docs/agent-manager.md +0 -244
  133. package/docs/codeserver-pattern.md +0 -191
  134. package/docs/generic-toolset.md +0 -326
  135. package/docs/howtos/agent-networking.md +0 -253
  136. package/docs/howtos/spawn-agents.md.bak +0 -200
  137. package/docs/howtos/spawn-agents.md.bak_new +0 -200
  138. package/docs/multi-agent-clusters.md +0 -265
  139. package/docs/music-toolsets.md +0 -137
  140. package/docs/path-resolution-best-practices.md +0 -104
  141. package/docs/plans/minimax-music-generation.md +0 -80
  142. package/docs/plans/unified-agent-architecture.md +0 -146
  143. package/docs/plans/websocket-streaming-plan.md.bak +0 -317
  144. package/docs/prompt/spawn_agent.md +0 -175
  145. package/docs/prompt/spawn_agent.md.bak +0 -201
  146. package/docs/prompt/task_clarification_and_documentation.md +0 -35
  147. package/docs/prompt-class.md +0 -141
  148. package/docs/todo-archive-infra-2026-04-21.md +0 -15
  149. package/docs/todo-archive-v0.0.8.md +0 -1
  150. package/docs/todo-archive-v0.1.0.md +0 -32
  151. package/docs/todo-archive.md +0 -44
  152. package/docs/tools-syntax-validation.md +0 -121
  153. package/docs/toolset.md +0 -164
  154. package/docs/xai-responses.md +0 -111
  155. package/docs/xai_collections.md +0 -106
  156. package/lib/API/x.ai/ImageToolset.js +0 -165
  157. package/lib/API/x.ai/text.js +0 -415
  158. package/lib/AgentClient.js +0 -248
  159. package/lib/AgentManager.js +0 -245
  160. package/lib/AgentServer.js +0 -404
  161. package/lib/wsCli.js +0 -287
  162. package/lib/wsIO.js +0 -90
  163. package/types/API/x.ai/text.d.ts +0 -286
  164. package/types/AgentClient.d.ts +0 -109
  165. package/types/AgentManager.d.ts +0 -100
  166. package/types/AgentServer.d.ts +0 -89
  167. package/types/wsCli.d.ts +0 -17
  168. package/types/wsIO.d.ts +0 -30
  169. package/utils/test.sh +0 -46
  170. /package/docs/{suggestions.md → _notes/token-counts.md} +0 -0
  171. /package/lib/API/openai.com/{reponses/MESSAGES.md → MESSAGES.md} +0 -0
  172. /package/types/API/{x.ai/ImageToolset.d.ts → mureka/MusicToolset.d.ts} +0 -0
  173. /package/types/{CdnToolset.d.ts → cdnToolset.d.ts} +0 -0
@@ -1,5 +1,6 @@
1
- import { SH, bashEscape } from '@j-o-r/sh'
2
- import { ToolSet, env } from './index.js'
1
+ import { SH } from '@j-o-r/sh'
2
+ import ToolSet from './ToolSet.js';
3
+ import { env } from './fafs.js'
3
4
  import path from 'node:path';
4
5
  import { promises as fs } from 'node:fs';
5
6
  import { fileURLToPath } from 'node:url';
@@ -16,7 +17,22 @@ const syntaxCheckSh = path.join(utilsDir, 'syntax_check.sh');
16
17
  // Do we have a SSH access point?
17
18
  const SSH_EP = process.env.SSH_EP || '';
18
19
 
19
- const user = await env();
20
+ let user;
21
+ try {
22
+ user = await env();
23
+ } catch (e) {
24
+ user = {
25
+ name: process.env.USER || '',
26
+ system: process.platform,
27
+ city: '',
28
+ region: '',
29
+ country: '',
30
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || '',
31
+ external_ip: '',
32
+ cwd: process.cwd()
33
+ };
34
+ }
35
+
20
36
  const environment = `
21
37
  Name: ${user.name}
22
38
  System: ${user.system}
@@ -29,45 +45,50 @@ ExternalIp: ${user.external_ip}
29
45
  const tools = new ToolSet('auto');
30
46
 
31
47
  /**
32
- * Single source of truth for cleaning over-escaped strings from LLMs.
48
+ * Preserve an arbitrary text argument exactly as a string.
49
+ *
50
+ * Important: do not JSON.parse arbitrary text arguments. A valid user payload
51
+ * can itself be a JavaScript/Bash/string literal such as `"hello"`; parsing it
52
+ * would remove the quotes and corrupt the code or content.
53
+ *
54
+ * @param {*} value - Candidate text value.
55
+ * @returns {string} Exact string value, or an empty string for non-strings.
56
+ */
57
+ const exactText = (value) => typeof value === 'string' ? value : '';
58
+
59
+ /**
60
+ * Normalize scalar selector arguments such as paths, URLs, email addresses, and
61
+ * subjects where accidental extra wrapping quotes are almost always unintended.
62
+ *
63
+ * This helper is intentionally not used for executable scripts or file content.
64
+ *
65
+ * @param {*} value - Candidate scalar value.
66
+ * @returns {*} Normalized scalar, or the original non-string value.
33
67
  */
34
- const guessOverEscaping = (s) => {
35
- if (typeof s !== 'string') return s;
68
+ const cleanScalar = (value) => {
69
+ if (typeof value !== 'string') return value;
36
70
 
37
- let current = s.trim();
71
+ let current = value.trim();
38
72
  const seen = new Set();
39
- let iterations = 0;
40
- const MAX_ITER = 6;
73
+ const MAX_ITER = 3;
41
74
 
42
- while (iterations < MAX_ITER && !seen.has(current)) {
75
+ for (let i = 0; i < MAX_ITER && !seen.has(current); i++) {
43
76
  seen.add(current);
44
- iterations++;
45
-
46
- try {
47
- const parsed = JSON.parse(current);
48
- if (typeof parsed === 'string') {
49
- current = parsed;
50
- continue;
51
- }
52
- } catch (e) { }
53
77
 
54
- if (current.startsWith('\\"') && current.endsWith('\\"')) {
78
+ if (current.startsWith('\\"') && current.endsWith('\\"') && current.length > 4) {
55
79
  current = current.slice(2, -2);
56
80
  continue;
57
81
  }
58
82
 
59
- if (current.startsWith('"') && current.endsWith('"') && current.length > 2) {
60
- const inner = current.slice(1, -1).trim();
61
- if (!inner.startsWith('{') && !inner.startsWith('[')) {
62
- current = inner;
63
- continue;
64
- }
65
- }
66
-
67
- if (!current.includes('\n') && (current.match(/\\"/g) || []).length >= 1) {
68
- const lessEscaped = current.replace(/\\"/g, '"');
69
- if (lessEscaped !== current) {
70
- current = lessEscaped;
83
+ if (current.startsWith('"') && current.endsWith('"') && current.length > 1) {
84
+ try {
85
+ const parsed = JSON.parse(current);
86
+ if (typeof parsed === 'string') {
87
+ current = parsed;
88
+ continue;
89
+ }
90
+ } catch (_) {
91
+ current = current.slice(1, -1);
71
92
  continue;
72
93
  }
73
94
  }
@@ -78,187 +99,489 @@ const guessOverEscaping = (s) => {
78
99
  return current;
79
100
  };
80
101
 
102
+ /**
103
+ * Extract concise JavaScript error text from Node stdin-module output.
104
+ * Keeps the user-facing error message, source location, and first code-frame
105
+ * lines while dropping Node internals and version banners.
106
+ *
107
+ * @param {string} errorStr - Raw error text.
108
+ * @returns {string} Condensed error text.
109
+ */
81
110
  const getJSError = (errorStr) => {
82
- let result = '';
83
- const linematch = errorStr.match(/\[eval\]:(\d+)/);
84
- const lineNumber = linematch ? linematch[1] : '';
85
- const match = errorStr.split(/" "\[eval\]:\d+/s);
86
- if (match.length > 1) {
87
- const res = match[1].split('\n').slice(0, -10).join('\n');
88
- result = `Error: line ${lineNumber}\n${res} `;
111
+ const raw = String(errorStr || '').trim();
112
+ const withoutCommandPrefix = raw.replace(/^(?:Error: )?Command failed with code \d+:\s*/s, '');
113
+ const cleaned = withoutCommandPrefix
114
+ .split('\n')
115
+ .filter((line) => !/^\s*at (?:node:internal|ModuleJob\.|ModuleLoader\.|compileSourceTextModule|process\.|asyncRunEntryPoint|Object\.|evalModuleEntryPoint|Socket\.)/.test(line))
116
+ .filter((line) => !/^Node\.js v/.test(line))
117
+ .join('\n')
118
+ .trim();
119
+
120
+ const lines = cleaned.split('\n');
121
+ const location = lines.find((line) => /^file:\/\//.test(line));
122
+ const errorLine = lines.find((line) => /^(?:[A-Za-z]+Error|Error): /.test(line));
123
+ const frame = [];
124
+
125
+ if (location) frame.push(location);
126
+ for (const line of lines) {
127
+ if (line === location || line === errorLine) continue;
128
+ if (frame.length >= 3) break;
129
+ if (line.trim() !== '') frame.push(line);
130
+ }
131
+
132
+ const message = errorLine || lines.find((line) => line.trim() !== '') || 'JavaScript execution failed';
133
+ const prefixedMessage = message.startsWith('Error:') ? message : `Error: ${message}`;
134
+ return [prefixedMessage, ...frame].join('\n').trim();
135
+ };
136
+
137
+ /**
138
+ * Stringify a structured tool response with stable formatting.
139
+ * Structured responses preserve exact paths, URLs, commands, and other handles
140
+ * so the assistant can repeat essential references in normal conversation before
141
+ * raw function-call history is pruned.
142
+ *
143
+ * @param {Record<string, *>} payload - JSON-serializable tool response payload.
144
+ * @returns {string} Formatted JSON function response.
145
+ */
146
+ const json = (payload) => JSON.stringify(payload, null, 2);
147
+
148
+ /**
149
+ * Convert an unknown thrown value into a message.
150
+ *
151
+ * @param {*} error - Unknown error value.
152
+ * @returns {string} Error message.
153
+ */
154
+ const errorMessage = (error) => error instanceof Error ? error.message : String(error);
155
+
156
+ /**
157
+ * Return true when child is cwd or a path inside cwd.
158
+ *
159
+ * @param {string} cwd - Real absolute CWD path.
160
+ * @param {string} child - Absolute candidate path.
161
+ * @returns {boolean} Whether child is inside cwd.
162
+ */
163
+ const isInsideCwd = (cwd, child) => {
164
+ const relative = path.relative(cwd, child);
165
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
166
+ };
167
+
168
+ /**
169
+ * Resolve a user-supplied path to a CWD-contained absolute path.
170
+ * For writes, parent directories are created safely and final symlinks are rejected
171
+ * if they resolve outside CWD.
172
+ *
173
+ * @param {string} file - User-supplied relative path.
174
+ * @param {{ forWrite?: boolean }} [options] - Resolution options.
175
+ * @returns {Promise<{ file: string, resolved: string }>} Clean relative and absolute paths.
176
+ */
177
+ const resolveCwdPath = async (file, options = {}) => {
178
+ const cleaned = cleanScalar(file);
179
+ if (!cleaned || typeof cleaned !== 'string' || path.isAbsolute(cleaned)) {
180
+ throw new Error('Relative CWD path only');
181
+ }
182
+
183
+ const cwd = await fs.realpath(process.cwd());
184
+ const resolved = path.resolve(cwd, cleaned);
185
+ if (!isInsideCwd(cwd, resolved)) {
186
+ throw new Error('Path escapes CWD');
187
+ }
188
+
189
+ if (options.forWrite) {
190
+ await fs.mkdir(path.dirname(resolved), { recursive: true });
191
+ const parentReal = await fs.realpath(path.dirname(resolved));
192
+ if (!isInsideCwd(cwd, parentReal)) {
193
+ throw new Error('Path escapes CWD');
194
+ }
195
+
196
+ try {
197
+ const existingReal = await fs.realpath(resolved);
198
+ if (!isInsideCwd(cwd, existingReal)) {
199
+ throw new Error('Path escapes CWD');
200
+ }
201
+ } catch (e) {
202
+ if (e?.code !== 'ENOENT') throw e;
203
+ }
89
204
  } else {
90
- result = errorStr;
205
+ const real = await fs.realpath(resolved);
206
+ if (!isInsideCwd(cwd, real)) {
207
+ throw new Error('Path escapes CWD');
208
+ }
91
209
  }
92
- return result;
210
+
211
+ return { file: path.relative(cwd, resolved), resolved };
93
212
  };
94
213
 
214
+ /**
215
+ * Return a structured failure response for a tool.
216
+ *
217
+ * @param {string} tool - Tool name.
218
+ * @param {*} error - Unknown error value.
219
+ * @param {Record<string, *>} [extra] - Additional response fields.
220
+ * @returns {string} JSON error payload.
221
+ */
222
+ const toolError = (tool, error, extra = {}) => json({
223
+ tool,
224
+ success: false,
225
+ ...extra,
226
+ error: errorMessage(error)
227
+ });
228
+
95
229
  /**
96
230
  * @module lib/genericToolset
97
- * Secure utility tools.
231
+ * Secure utility tools.
98
232
  */
99
233
 
100
234
  tools.add(
101
235
  'javascript_interpreter',
102
- 'Execute ESM ES6 JavaScript on node.',
236
+ `Execute ESM ES6 JavaScript on node. Tool-specific calling rules: pass script as raw ESM JavaScript exactly as intended; preserve quotes, backticks, dollar braces, and newlines; do not JSON.parse, strip quotes, escape, shell-wrap, or otherwise transform the code. Use console.log for output. Returns structured JSON with success, output/error, and a note. If output contains exact paths, URLs, ids, or values needed later, include them verbatim in your assistant response before old tool calls are pruned.`,
103
237
  {
104
238
  type: 'object',
105
239
  properties: {
106
240
  script: {
107
241
  type: 'string',
108
- description: `ES6 ESM code. Use console.log for output. cwd: ${user.cwd}`
242
+ description: `Raw ES6 ESM code to execute with Node from stdin. Preserve the code exactly, including JavaScript string literal quotes such as "hello", template literals, dollar braces, and newlines. Use console.log for output. cwd: ${user.cwd}`
109
243
  }
110
244
  },
111
245
  required: ['script']
112
246
  },
113
247
  async (params) => {
114
- let response = '';
248
+ const tool = 'javascript_interpreter';
115
249
  try {
116
- const script = guessOverEscaping(params.script);
250
+ const script = exactText(params.script);
117
251
  const delim = `JS_STDIN_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
118
- response = await SH`cat <<'${delim}' | node --input-type=module -
252
+ const output = await SH`cat <<'${delim}' | node --input-type=module -
119
253
  ${script}
120
254
  ${delim}
121
255
  `.run();
256
+ return json({
257
+ tool,
258
+ success: true,
259
+ output,
260
+ note: 'Assistant: preserve exact paths, URLs, ids, and values from output in your normal response if needed for follow-up.'
261
+ });
122
262
  } catch (e) {
123
- response = getJSError(e.toString());
263
+ const error = getJSError(errorMessage(e));
264
+ return json({
265
+ tool,
266
+ success: false,
267
+ output: error,
268
+ error,
269
+ note: 'Assistant: report JavaScript errors compactly: message, relevant line/location, no Node internals unless asked.'
270
+ });
124
271
  }
125
- return response;
126
272
  }
127
273
  );
128
274
 
129
275
  tools.add(
130
276
  'get_user_env',
131
- 'Get user environment info.',
277
+ 'Get user environment info. Tool-specific behavior: call with an empty object; do not invent missing fields. Returns name/location/system/cwd context as text; mention only user-relevant facts in your assistant response.',
132
278
  { type: 'object', properties: {} },
133
- async () => environment
279
+ async () => json({
280
+ tool: 'get_user_env',
281
+ environment,
282
+ note: 'Assistant: use these environment facts only when relevant.'
283
+ })
134
284
  );
135
285
 
136
286
  tools.add(
137
287
  'execute_bash_script',
138
- 'Execute raw bash script or command (no escaping needed). Supports timeout.',
288
+ 'Execute raw bash script or command. Tool-specific calling rules: pass bash_script as raw Bash exactly as intended; preserve quotes, dollar signs, backticks, heredocs, and newlines; do not JSON.parse, escape, re-quote, or wrap the script. Use shellcheck=false for exploratory scripts unless the user explicitly requests linting; if shellcheck=true fails, report the ShellCheck error and do not imply the script ran. For timeout tests, report both timeout and timeoutSec from the tool result. Returns structured JSON with success, timeout, timeoutSec, and output. If output includes important paths, generated files, URLs, ids, or command results needed later, include those exact references in your assistant response before old tool calls are pruned.',
139
289
  {
140
290
  type: 'object',
141
291
  properties: {
142
292
  bash_script: {
143
293
  type: 'string',
144
- description: `Write raw bash exactly...`
294
+ description: 'Raw Bash to execute. Preserve exactly as intended, including quotes, variables, command substitutions, heredocs, and newlines. Do not add extra wrapping quotes or escaping.'
145
295
  },
146
- timeout: { type: 'number', default: 360 },
147
- strict: { type: 'boolean', default: false }
296
+ timeout: {
297
+ type: 'number',
298
+ default: 360,
299
+ description: 'Timeout in seconds. For timeout tests, report both timeout and timeoutSec from the result.'
300
+ },
301
+ shellcheck: {
302
+ type: 'boolean',
303
+ default: false,
304
+ description: 'Run shellcheck before executing the script. Defaults to false; keep false for exploratory scripts unless the user explicitly asks for linting. If shellcheck fails, the script does not run.'
305
+ }
148
306
  },
149
307
  required: ['bash_script']
150
308
  },
151
309
  async (params) => {
152
- let bash_script = params.bash_script;
153
- if (typeof bash_script !== 'string') bash_script = '';
154
- // Check if the bash_script string has a shebang on the first line '#!'
155
- // If not, add a bash shebang
156
- const firstLine = bash_script.split('\n')[0] || '';
157
- if (!firstLine.trim().startsWith('#!')) {
158
- bash_script = '#!/bin/bash\n' + bash_script;
310
+ const tool = 'execute_bash_script';
311
+ let timeoutSec = Number(params.timeout ?? 360);
312
+ try {
313
+ let bashScript = params.bash_script;
314
+ if (typeof bashScript !== 'string') bashScript = '';
315
+ if (!Number.isFinite(timeoutSec) || timeoutSec < 0) throw new Error('Invalid timeout');
316
+ const timeout = timeoutSec * 1000;
317
+ const shouldRunShellcheck = Boolean(params.shellcheck ?? false);
318
+ if (shouldRunShellcheck) {
319
+ const valid = SH`shellcheck -s bash -`.runSync(bashScript).stdout.toString().trim();
320
+ if (valid !== '') throw new Error(valid);
321
+ }
322
+ const output = await SH`bash`.options({ timeout }).run(bashScript);
323
+ return json({
324
+ tool,
325
+ success: true,
326
+ timeout: false,
327
+ timeoutSec,
328
+ output,
329
+ note: 'Assistant: preserve exact paths, URLs, ids, filenames, and command results from output in your normal response if needed for follow-up.'
330
+ });
331
+ } catch (e) {
332
+ const message = errorMessage(e);
333
+ return toolError(tool, e, {
334
+ timeout: /timeout|timed out|killed/i.test(message),
335
+ timeoutSec
336
+ });
159
337
  }
160
- const timeoutSec = Number(params.timeout ?? 360);
161
- const prams = params.strict ? '' : '-S error';
162
- if (isNaN(timeoutSec) || timeoutSec < 0) throw new Error('Invalid timeout');
163
- const timeout = timeoutSec * 1000;
164
- const valid = SH`shellcheck ${prams} -`.runSync(bash_script).stdout.toString().trim();
165
- if (valid !== '') throw new Error(valid);
166
- return await SH`bash`.options({ timeout }).run(bash_script);
167
338
  }
168
339
  );
169
340
 
170
341
  tools.add(
171
- 'send_email', 'Send email via msmtp.', { type: 'object', properties: { to: { type: 'string' }, subject: { type: 'string' }, body: { type: 'string' } }, required: ['to', 'subject', 'body'] },
342
+ 'send_email', 'Send email via msmtp. Tool-specific calling rules: to and subject are scalar fields, so do not add extra wrapping quotes; body is exact text and must preserve newlines, quotes, and formatting. Returns structured JSON with recipient and subject. After success, mention the recipient and subject exactly in your assistant response.', {
343
+ type: 'object',
344
+ properties: {
345
+ to: { type: 'string', description: 'Scalar recipient email address. Do not add extra wrapping quotes or newlines.' },
346
+ subject: { type: 'string', description: 'Scalar email subject. Do not add extra wrapping quotes or newlines.' },
347
+ body: { type: 'string', description: 'Exact email body text. Preserve newlines, quotes, and formatting exactly.' }
348
+ },
349
+ required: ['to', 'subject', 'body']
350
+ },
172
351
  async (params) => {
173
- const to = guessOverEscaping(params.to);
174
- const subject = guessOverEscaping(params.subject);
175
- const body = guessOverEscaping(params.body);
176
- const delim = `END_EMAIL_${Date.now().toString(36)}`;
177
- return await SH`msmtp ${params.to} <<'${delim}'
352
+ const tool = 'send_email';
353
+ try {
354
+ const to = cleanScalar(params.to);
355
+ const subject = cleanScalar(params.subject);
356
+ const body = exactText(params.body);
357
+ if (!to || /[\r\n]/.test(to) || /[\r\n]/.test(subject)) {
358
+ throw new Error('Email recipient and subject must be non-empty and must not contain newlines');
359
+ }
360
+ const delim = `END_EMAIL_${Date.now().toString(36)}`;
361
+ const output = await SH`msmtp ${to} <<'${delim}'
178
362
  To: ${to}
179
363
  Subject: ${subject}
180
364
 
181
365
  ${body}
182
366
  ${delim}
183
367
  `.run();
368
+ return json({
369
+ tool,
370
+ success: true,
371
+ to,
372
+ subject,
373
+ output,
374
+ note: 'Assistant: tell the user the email was sent and include the recipient and subject exactly.'
375
+ });
376
+ } catch (e) {
377
+ return toolError(tool, e);
378
+ }
184
379
  }
185
380
  );
186
381
 
187
382
  tools.add(
188
- 'open_link', 'Open URL/file with xdg-open.', { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] },
189
- async (params) => await SH`xdg-open ${[guessOverEscaping(params.url)]}`.run()
383
+ 'open_link', 'Open URL/file with xdg-open. Tool-specific calling rules: url is a scalar URL/file value; pass it without extra wrapping quotes such as "https://...". Returns structured JSON with the opened URL/file. After success, mention the exact URL/file if it is useful for follow-up.', {
384
+ type: 'object',
385
+ properties: {
386
+ url: { type: 'string', description: 'Scalar URL or file path to open. Do not add extra wrapping quotes.' }
387
+ },
388
+ required: ['url']
389
+ },
390
+ async (params) => {
391
+ const tool = 'open_link';
392
+ try {
393
+ const url = cleanScalar(params.url);
394
+ const output = await SH`xdg-open ${[url]}`.run();
395
+ return json({
396
+ tool,
397
+ success: true,
398
+ url,
399
+ output,
400
+ note: 'Assistant: mention the opened URL/file exactly if relevant for follow-up.'
401
+ });
402
+ } catch (e) {
403
+ return toolError(tool, e);
404
+ }
405
+ }
190
406
  );
191
407
 
192
408
  tools.add(
193
- 'execute_remote_script', `Run bash on remote via SSH. ${SSH_EP}`,
409
+ 'execute_remote_script', `Run bash on remote via SSH. Tool-specific calling rules: script is raw Bash and must preserve quotes, dollar signs, backticks, heredocs, and newlines exactly; url is a scalar ssh:// URL and should not have extra wrapping quotes. ${SSH_EP} Returns structured JSON with target, timeout, and output. If output contains important paths, URLs, ids, or files needed later, include them verbatim in your assistant response.`,
194
410
  {
195
411
  type: 'object',
196
412
  properties: {
197
- url: { type: 'string', description: `ssh://user@host[:port] default: ${SSH_EP}` },
198
- script: { type: 'string' },
199
- timeout: { type: 'number', default: 30 }
413
+ url: { type: 'string', description: `Scalar SSH URL ssh://user@host[:port]. Do not add extra wrapping quotes. Default: ${SSH_EP}` },
414
+ script: { type: 'string', description: 'Raw Bash to execute remotely. Preserve exactly, including quotes, variables, heredocs, and newlines.' },
415
+ timeout: {
416
+ type: 'number',
417
+ default: 30,
418
+ description: 'Remote execution timeout in seconds.'
419
+ }
200
420
  },
201
- required: ['url', 'script']
421
+ required: ['script']
202
422
  },
203
423
  async (params) => {
204
- let { url, script } = params;
205
- if (!url || url === '') url = SSH_EP;
206
- url = guessOverEscaping(url);
207
- script = guessOverEscaping(script);
208
- const timeoutMs = (Number(params.timeout ?? 30)) * 1000;
209
- if (!url.startsWith('ssh://')) throw new Error('ssh://user@host[:port]');
210
- const withoutProto = url.slice(6);
211
- const parts = withoutProto.split(':');
212
- let port = 22, userHost = withoutProto;
213
- if (parts.length > 1) { userHost = parts[0]; port = parseInt(parts[1]); }
214
- const [user, host] = userHost.split('@');
215
- return await SH`ssh -p ${port} ${user}@${host} bash`.options({ timeout: timeoutMs }).run(script);
424
+ const tool = 'execute_remote_script';
425
+ try {
426
+ let { url, script } = params;
427
+ if (!url || url === '') url = SSH_EP;
428
+ url = cleanScalar(url);
429
+ script = exactText(script);
430
+ const timeoutSec = Number(params.timeout ?? 30);
431
+ if (!Number.isFinite(timeoutSec) || timeoutSec < 0) throw new Error('Invalid timeout');
432
+ const parsed = new URL(url);
433
+ if (parsed.protocol !== 'ssh:') throw new Error('Expected ssh://user@host[:port]');
434
+ const remoteUser = decodeURIComponent(parsed.username);
435
+ const host = parsed.hostname;
436
+ const port = parsed.port ? Number(parsed.port) : 22;
437
+ if (!remoteUser || !host || !Number.isInteger(port) || port < 1 || port > 65535) {
438
+ throw new Error('Invalid SSH URL');
439
+ }
440
+ const output = await SH`ssh -p ${port} ${remoteUser}@${host} bash`.options({ timeout: timeoutSec * 1000 }).run(script);
441
+ return json({
442
+ tool,
443
+ success: true,
444
+ url,
445
+ host,
446
+ user: remoteUser,
447
+ port,
448
+ timeoutSec,
449
+ output,
450
+ note: 'Assistant: preserve exact remote paths, URLs, ids, filenames, and command results from output in your normal response if needed for follow-up.'
451
+ });
452
+ } catch (e) {
453
+ return toolError(tool, e);
454
+ }
216
455
  }
217
456
  );
218
457
 
219
458
  tools.add(
220
- 'history_search', 'Search/list chat sessions in .cache/.',
221
- { type: 'object', properties: { query: { type: 'string' } } },
459
+ 'history_search', 'Search/list chat sessions in .cache/. Tool-specific calling rules: query is a scalar search string; pass it without extra wrapping quotes. Omit query or pass an empty string to list sessions. Returns structured JSON with query and results. If a session id/path is useful for follow-up, include it exactly in your assistant response.',
460
+ {
461
+ type: 'object',
462
+ properties: {
463
+ query: { type: 'string', description: 'Scalar search query. Do not add extra wrapping quotes. Omit or use empty string to list sessions.' }
464
+ }
465
+ },
222
466
  async (params) => {
223
- if (typeof params.query === 'string' && params.query.trim()) {
224
- return await SH`${searchSessionsSh} "${bashEscape(guessOverEscaping(params.query))}"`.run();
467
+ const tool = 'history_search';
468
+ try {
469
+ const query = typeof params.query === 'string' ? cleanScalar(params.query) : '';
470
+ const output = query.trim()
471
+ ? await SH`${searchSessionsSh} ${[query]}`.run()
472
+ : await SH`${listSessionsSh}`.run();
473
+ return json({
474
+ tool,
475
+ success: true,
476
+ query,
477
+ output,
478
+ note: 'Assistant: if a session id or path is useful for follow-up, mention it exactly in your normal response.'
479
+ });
480
+ } catch (e) {
481
+ return toolError(tool, e);
225
482
  }
226
- return await SH`${listSessionsSh}`.run();
227
483
  }
228
484
  );
229
485
 
230
486
  tools.add(
231
- 'read_file', 'Read file from CWD (relative path only).',
232
- { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
487
+ 'read_file', 'Read file from CWD (relative path only). Tool-specific calling rules: file is a scalar relative path inside CWD; do not use absolute paths, parent-directory escapes, or extra wrapping quotes such as "src/file.js". Returns structured JSON with file path, byte count, and content. If this file is relevant for later edits, mention the exact relative path in your assistant response.',
488
+ {
489
+ type: 'object',
490
+ properties: {
491
+ file: { type: 'string', description: 'Scalar relative path inside CWD. Do not use absolute paths, parent-directory escapes, or extra wrapping quotes.' }
492
+ },
493
+ required: ['file']
494
+ },
233
495
  async (params) => {
234
- let file = guessOverEscaping(params.file?.trim());
235
- if (!file || file.startsWith('/') || file.includes('..')) throw new Error('Relative CWD path only');
236
- return await fs.readFile(path.resolve(process.cwd(), file), 'utf8');
496
+ const tool = 'read_file';
497
+ try {
498
+ const { file, resolved } = await resolveCwdPath(params.file);
499
+ const content = await fs.readFile(resolved, 'utf8');
500
+ return json({
501
+ tool,
502
+ success: true,
503
+ file,
504
+ absolutePath: resolved,
505
+ bytes: Buffer.byteLength(content, 'utf8'),
506
+ content,
507
+ note: 'Assistant: if this file matters for follow-up work, mention the exact relative file path in your normal response.'
508
+ });
509
+ } catch (e) {
510
+ return toolError(tool, e);
511
+ }
237
512
  }
238
513
  );
239
514
 
240
515
  tools.add(
241
- 'write_file', 'Write/validate file in CWD.',
242
- { type: 'object', properties: { file: { type: 'string' }, content: { type: 'string' } }, required: ['file', 'content'] },
516
+ 'write_file', 'Write/validate file in CWD. Tool-specific calling rules: file is a scalar relative path inside CWD; do not use absolute paths, parent-directory escapes, or extra wrapping quotes. content is exact file content; preserve quotes, backticks, dollar braces, and newlines; do not JSON.parse, strip quotes, or reformat unless asked. Returns structured JSON with exact relative path and byte count. After success, mention the written file path exactly in your assistant response before old tool calls are pruned.',
517
+ {
518
+ type: 'object',
519
+ properties: {
520
+ file: { type: 'string', description: 'Scalar relative path inside CWD. Do not use absolute paths, parent-directory escapes, or extra wrapping quotes.' },
521
+ content: { type: 'string', description: 'Exact file content. Preserve quotes, backticks, dollar braces, and newlines exactly; do not JSON.parse, strip quotes, or reformat unless asked.' }
522
+ },
523
+ required: ['file', 'content']
524
+ },
243
525
  async (params) => {
244
- let fileParam = guessOverEscaping(params.file?.trim());
245
- let content = guessOverEscaping(params.content || '');
246
- if (!fileParam || fileParam.startsWith('/') || fileParam.includes('..')) throw new Error('Relative CWD path only');
247
- const resolved = path.resolve(process.cwd(), fileParam);
248
- await fs.mkdir(path.dirname(resolved), { recursive: true });
249
- await fs.writeFile(resolved, content, 'utf8');
250
- return `Wrote ${fileParam} (${Buffer.byteLength(content, 'utf8')} bytes).`;
526
+ const tool = 'write_file';
527
+ try {
528
+ const { file, resolved } = await resolveCwdPath(params.file, { forWrite: true });
529
+ const content = exactText(params.content);
530
+ await fs.writeFile(resolved, content, 'utf8');
531
+ const bytes = Buffer.byteLength(content, 'utf8');
532
+ return json({
533
+ tool,
534
+ success: true,
535
+ file,
536
+ absolutePath: resolved,
537
+ bytes,
538
+ message: `Wrote ${file} (${bytes} bytes).`,
539
+ note: 'Assistant: tell the user the exact written file path and byte count in your normal response.'
540
+ });
541
+ } catch (e) {
542
+ return toolError(tool, e);
543
+ }
251
544
  }
252
545
  );
253
546
 
254
547
  tools.add(
255
- 'syntax_check', 'Syntax validate file via utils/syntax_check.sh.',
256
- { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] },
548
+ 'syntax_check', 'Syntax validate file via utils/syntax_check.sh. Tool-specific calling rules: file is a scalar relative path inside CWD; do not use absolute paths, parent-directory escapes, or extra wrapping quotes. Returns structured JSON with exact file path, status, and output. After success/failure, mention the file path and validation status exactly.',
549
+ {
550
+ type: 'object',
551
+ properties: {
552
+ file: { type: 'string', description: 'Scalar relative path inside CWD. Do not use absolute paths, parent-directory escapes, or extra wrapping quotes.' }
553
+ },
554
+ required: ['file']
555
+ },
257
556
  async (params) => {
258
- const file = guessOverEscaping(params.file?.trim());
259
- const resolved = path.resolve(process.cwd(), file);
260
- return await SH`${syntaxCheckSh} ${resolved}`.run() || 'Syntax OK';
557
+ const tool = 'syntax_check';
558
+ let file = '';
559
+ let resolved = '';
560
+ try {
561
+ ({ file, resolved } = await resolveCwdPath(params.file));
562
+ const output = await SH`${syntaxCheckSh} ${resolved}`.run();
563
+ return json({
564
+ tool,
565
+ success: true,
566
+ file,
567
+ absolutePath: resolved,
568
+ status: 'ok',
569
+ output: output || 'Syntax OK',
570
+ note: 'Assistant: mention the exact file path and validation status in your normal response.'
571
+ });
572
+ } catch (e) {
573
+ return json({
574
+ tool,
575
+ success: false,
576
+ file: file || undefined,
577
+ absolutePath: resolved || undefined,
578
+ status: 'failed',
579
+ output: errorMessage(e),
580
+ error: errorMessage(e),
581
+ note: 'Assistant: mention the exact file path and validation status in your normal response.'
582
+ });
583
+ }
261
584
  }
262
585
  );
263
586
 
264
- export default tools;
587
+ export default tools;