@j-o-r/hello-dave 0.0.6 → 0.0.8

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 (66) hide show
  1. package/CHANGELOG.md +19 -33
  2. package/README.md +240 -0
  3. package/TODO.md +13 -0
  4. package/{examples → agents}/ask_agent.js +5 -5
  5. package/{examples → agents}/codeserver.sh +14 -14
  6. package/{examples → agents}/daisy_agent.js +5 -5
  7. package/{examples → agents}/docs_agent.js +5 -5
  8. package/{examples → agents}/gpt_agent.js +5 -5
  9. package/{examples → agents}/grok_agent.js +5 -5
  10. package/{examples → agents}/memory_agent.js +5 -5
  11. package/{examples → agents}/npm_agent.js +5 -5
  12. package/{examples → agents}/prompt_agent.js +5 -5
  13. package/agents/spawn_agent.js +137 -0
  14. package/{examples → agents}/test_agent.js +6 -6
  15. package/{examples → agents}/todo_agent.js +5 -5
  16. package/bin/codeDave +58 -0
  17. package/bin/dave.js +3 -5
  18. package/docs/agent-manager.md +244 -0
  19. package/docs/bin-dave.md +62 -0
  20. package/docs/codeserver-pattern.md +191 -0
  21. package/docs/generic-toolset.md +326 -0
  22. package/docs/howtos/agent-networking.md +253 -0
  23. package/docs/howtos/spawn-agents.md.bak +200 -0
  24. package/docs/howtos/spawn-agents.md.bak_new +200 -0
  25. package/docs/jsdoc-best-practices.md +278 -0
  26. package/docs/multi-agent-clusters.md +265 -0
  27. package/docs/multi-agent-clusters.md.bak +229 -0
  28. package/docs/path-resolution-best-practices.md +104 -0
  29. package/docs/project-overview.md +67 -0
  30. package/docs/prompt/spawn_agent.md +173 -0
  31. package/docs/prompt/spawn_agent.md.bak +201 -0
  32. package/docs/prompt-class.md +141 -0
  33. package/docs/suggestions.md +38 -0
  34. package/docs/todo-archive-v0.0.8.md +1 -0
  35. package/docs/todo-archive.md +44 -0
  36. package/docs/tools-syntax-validation.md +121 -0
  37. package/docs/toolset.md +164 -0
  38. package/docs/xai-responses.md +111 -0
  39. package/docs/xai_collections.md +106 -0
  40. package/lib/AgentClient.js +111 -67
  41. package/lib/AgentManager.js +111 -80
  42. package/lib/AgentServer.js +144 -104
  43. package/lib/Cli.js +126 -93
  44. package/lib/Prompt.js +38 -5
  45. package/lib/Session.js +102 -79
  46. package/lib/ToolSet.js +79 -60
  47. package/lib/fafs.js +54 -19
  48. package/lib/genericToolset.js +129 -136
  49. package/lib/wsCli.js +50 -19
  50. package/lib/wsIO.js +10 -6
  51. package/package.json +3 -3
  52. package/types/AgentClient.d.ts +69 -35
  53. package/types/AgentManager.d.ts +50 -56
  54. package/types/AgentServer.d.ts +63 -16
  55. package/types/Cli.d.ts +56 -10
  56. package/types/Prompt.d.ts +36 -4
  57. package/types/Session.d.ts +23 -9
  58. package/types/ToolSet.d.ts +49 -32
  59. package/types/fafs.d.ts +68 -25
  60. package/types/wsCli.d.ts +14 -0
  61. package/types/wsIO.d.ts +9 -5
  62. package/utils/search_sessions.sh +100 -53
  63. package/bin/spawn_agent.js +0 -293
  64. package/lib/genericToolset.js.bak_syntax +0 -402
  65. /package/{examples → agents}/code_agent.js +0 -0
  66. /package/{examples → agents}/readme_agent.js +0 -0
@@ -14,46 +14,56 @@ const listSessionsSh = path.join(utilsDir, 'list_sessions.sh');
14
14
  const syntaxCheckSh = path.join(utilsDir, 'syntax_check.sh');
15
15
 
16
16
  const user = await env();
17
- const environment = `
17
+ const environment = `
18
18
  Name: ${user.name}
19
19
  System: ${user.system}
20
20
  City: ${user.city}
21
21
  Region: ${user.region}
22
22
  Country: ${user.country}
23
23
  Timezone: ${user.timezone}
24
- ExternalIp: ${user.external_ip}
24
+ ExternalIp: ${user.external_ip}
25
25
  `.trim();
26
26
  const tools = new ToolSet('auto');
27
27
 
28
28
  /**
29
- * reduce the error output to essential info only, if possible
30
- * @param {string} errorStr
31
- * @returns {string}
32
- */
29
+ * Reduces verbose JavaScript evaluation error output to essential info (line number + core message).
30
+ * @param {string} errorStr - Full Node.js error string.
31
+ * @returns {string} Simplified error.
32
+ * @private
33
+ */
33
34
  const getJSError = (errorStr) => {
34
35
  let result = '';
35
36
  const linematch = errorStr.match(/\[eval\]:(\d+)/);
36
37
  const lineNumber = linematch ? linematch[1] : '';
37
38
  const match = errorStr.split(/\" \"\[eval\]:\d+/s);
38
39
  if (match.length > 1) {
39
- // Remove last 10 lines
40
40
  const res = match[1].split('\n').slice(0, -10).join('\n');
41
- result = `Error: line ${lineNumber}\n${res} `
41
+ result = `Error: line ${lineNumber}\n${res} `;
42
42
  } else {
43
43
  result = errorStr;
44
44
  }
45
45
  return result;
46
- }
46
+ };
47
47
 
48
+ /**
49
+ * @module lib/genericToolset
50
+ * Secure utility tools for code execution, file I/O, system ops. Pre-populated ToolSet.
51
+ *
52
+ * @example
53
+ * import tools from './lib/genericToolset.js';
54
+ * await tools.call('get_user_env', {});
55
+ *
56
+ * @see {@link module:./index~ToolSet}
57
+ */
48
58
  tools.add(
49
59
  'javascript_interpreter',
50
- `Execute ESM ES6 javascript on \`node\`.`,
60
+ 'Execute ESM ES6 JavaScript on node.',
51
61
  {
52
62
  type: 'object',
53
63
  properties: {
54
64
  script: {
55
65
  type: 'string',
56
- description: `ES6 ESM Javascript eval. 'console.log' to capture the response. cwd: ${user.cwd}`,
66
+ description: `ES6 ESM code. Use console.log for output. cwd: ${user.cwd}`
57
67
  }
58
68
  },
59
69
  required: ['script']
@@ -67,53 +77,60 @@ ${params.script}
67
77
  ${delim}
68
78
  `.run();
69
79
  } catch (e) {
70
- const errorStr = e.toString();
71
- response = getJSError(errorStr);
80
+ response = getJSError(e.toString());
72
81
  }
73
82
  return response;
74
83
  }
75
84
  );
85
+
76
86
  tools.add(
77
- 'get_user_env', // name
78
- 'Get the user location, name and OS environment', // desciption
79
- {
80
- type: 'object',
81
- properties: {
82
- }
83
- },
84
- async (_params) => {
85
- return environment;
86
- }
87
+ 'get_user_env',
88
+ 'Get user environment info.',
89
+ { type: 'object', properties: {} },
90
+ async () => environment
87
91
  );
88
92
 
89
93
  tools.add(
90
94
  'execute_bash_script',
91
- 'Execute a bash script or command. (char escaping not needed)',
95
+ 'Execute raw bash script or command (no escaping needed). Supports timeout to prevent hangs.',
92
96
  {
93
97
  type: 'object',
94
98
  properties: {
95
- 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})` }
99
+ bash_script: {
100
+ type: 'string',
101
+ description: `Raw bash verbatim. Supports all syntax safely via stdin. System: ${user.system}`
102
+ },
103
+ timeout: {
104
+ type: 'number',
105
+ default: 360,
106
+ description: 'Max execution time in seconds (default 360 seconds, 0 is no timeout). Uses @j-o-r/sh timeout (ms) with SIGTERM on expiry to prevent hangs from interactive prompts or slow commands.'
107
+ }
96
108
  },
97
109
  required: ['bash_script']
98
110
  },
99
111
  async (params) => {
100
- const delim = `END_SCRIPT_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
112
+ const timeoutSec = Number(params.timeout ?? 300);
113
+ if (isNaN(timeoutSec) || timeoutSec < 0) throw new Error('Invalid timeout');
114
+ const timeout = timeoutSec * 1000;
115
+ // return await SH`bash`.options({ timeout: timeoutMs }).run(params.bash_script);
116
+ const delim = `END_SCRIPT_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
101
117
  return await SH`bash <<'${delim}'
102
118
  ${params.bash_script}
103
119
  ${delim}
104
- `.run()
120
+ `.options({timeout}).run();
121
+
105
122
  }
106
123
  );
107
124
 
108
125
  tools.add(
109
126
  'send_email',
110
- 'Send an email.',
127
+ 'Send email via msmtp.',
111
128
  {
112
129
  type: 'object',
113
130
  properties: {
114
- to: { type: 'string', description: 'Recipient email' },
131
+ to: { type: 'string', description: 'Recipient' },
115
132
  subject: { type: 'string', description: 'Subject' },
116
- body: { type: 'string', description: 'Message body' }
133
+ body: { type: 'string', description: 'Body' }
117
134
  },
118
135
  required: ['to', 'subject', 'body']
119
136
  },
@@ -128,193 +145,169 @@ ${delim}
128
145
  `.run();
129
146
  }
130
147
  );
148
+
131
149
  tools.add(
132
150
  'open_link',
133
- 'Open an url or file in the local user environment. (xdg-open)',
151
+ 'Open URL/file with xdg-open.',
134
152
  {
135
153
  type: 'object',
136
154
  properties: {
137
- url: { type: 'string', description: 'file | URL' }
155
+ url: { type: 'string', description: 'URL or file path' }
138
156
  },
139
157
  required: ['url']
140
158
  },
141
- async (params) => {
142
- return await SH`xdg-open ${[params.url]}`.run();
143
- }
159
+ async (params) => await SH`xdg-open ${[params.url]}`.run()
144
160
  );
161
+
145
162
  tools.add(
146
163
  'execute_remote_script',
147
- 'Execute bash script on a remote machine via SSH.',
164
+ 'Run bash on remote via SSH. Supports timeout to prevent hangs.',
148
165
  {
149
166
  type: 'object',
150
167
  properties: {
151
- url: { type: 'string', description: 'SSH URL, e.g., ssh://user@host or ssh://user@host:port' },
152
- script: { type: 'string', description: 'RAW script code to execute remotely (no escaping needed)' }
168
+ url: { type: 'string', description: 'ssh://user@host[:port]' },
169
+ script: { type: 'string', description: 'Raw script' },
170
+ timeout: {
171
+ type: 'number',
172
+ default: 30,
173
+ description: 'Max execution time in seconds (default 30). Uses @j-o-r/sh timeout (ms) with SIGTERM on SSH process expiry.'
174
+ }
153
175
  },
154
176
  required: ['url', 'script']
155
177
  },
156
178
  async (params) => {
157
179
  const { url, script } = params;
158
- if (!url.startsWith('ssh://')) throw new Error('Invalid SSH URL');
180
+ const timeoutSec = Number(params.timeout ?? 30);
181
+ if (isNaN(timeoutSec) || timeoutSec <= 0) throw new Error('Invalid timeout');
182
+ const timeoutMs = timeoutSec * 1000;
183
+ if (!url.startsWith('ssh://')) throw new Error('ssh://user@host[:port]');
159
184
  const withoutProto = url.slice(6);
160
185
  const parts = withoutProto.split(':');
161
- let port = 22;
162
- let userHost = withoutProto;
163
- if (parts.length > 1) {
164
- userHost = parts[0];
165
- port = parseInt(parts[1]);
166
- }
186
+ let port = 22, userHost = withoutProto;
187
+ if (parts.length > 1) { userHost = parts[0]; port = parseInt(parts[1]); }
167
188
  const [user, host] = userHost.split('@');
168
- if (!user || !host) throw new Error('Invalid SSH URL format: use ssh://user@host[:port]');
169
-
170
- const delim = `END_SCRIPT_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
171
- return await SH`ssh -p ${port} ${user}@${host} bash <<'${delim}'
172
- ${script}
173
- ${delim}
174
- `.run();
175
-
189
+ if (!user || !host) throw new Error('Invalid SSH URL');
190
+ return await SH`ssh -p ${port} ${user}@${host} bash`.options({ timeout: timeoutMs }).run(script);
176
191
  }
177
192
  );
193
+
178
194
  tools.add(
179
195
  'history_search',
180
- `Search previous LLM chat sessions or list them hierarchically.
181
- Example query: "(todo|task)" or "package.json".
182
- Searches filenames & content in .cache/[app]/[prompt]/sessions/*.ndjson (case-insensitive regex, with context).
183
- Omit query (or use empty string) to list sessions (see utils/list_sessions.sh).`,
196
+ 'Search/list chat sessions in .cache/.',
184
197
  {
185
198
  type: 'object',
186
199
  properties: {
187
- query: {
188
- type: 'string',
189
- description: `Search query or regex (quoted for multi-word/regex). Omit or empty for list mode.`
190
- }
200
+ query: { type: 'string', description: 'Query/regex or empty to list' }
191
201
  },
192
202
  required: []
193
203
  },
194
204
  async (params) => {
195
- if (typeof params.query === 'string' && params.query.trim() !== '') {
196
- const escapedQuery = bashEscape(params.query);
197
- return await SH`${searchSessionsSh} "${escapedQuery}"`.run();
198
- } else {
199
- return await SH`${listSessionsSh}`.run();
205
+ if (typeof params.query === 'string' && params.query.trim()) {
206
+ return await SH`${searchSessionsSh} "${bashEscape(params.query)}"`.run();
200
207
  }
208
+ return await SH`${listSessionsSh}`.run();
201
209
  }
202
210
  );
211
+
203
212
  tools.add(
204
213
  'read_file',
205
- 'Read the raw content of a file strictly within the current working directory (CWD). Paths must be relative (no leading /, no ..).',
214
+ 'Read file from CWD (relative path only).',
206
215
  {
207
216
  type: 'object',
208
217
  properties: {
209
- file: {
210
- type: 'string',
211
- description: `Relative path to the file within CWD, e.g., 'path/to/file.txt' (no escaping needed). cwd: ${user.cwd}`
212
- }
218
+ file: { type: 'string', description: `Relative path. cwd: ${user.cwd}` }
213
219
  },
214
220
  required: ['file']
215
221
  },
216
222
  async (params) => {
217
223
  const file = params.file?.trim();
218
- if (typeof file !== 'string' || !file) {
219
- throw new Error('Valid relative file path required.');
220
- }
221
- if (file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
222
- throw new Error('Path must be relative within CWD only (no `/`, `..`, or `\\\\`).');
223
- }
224
- const resolvedPath = path.resolve(process.cwd(), file);
225
- if (!resolvedPath.startsWith(process.cwd())) {
226
- throw new Error(`Path '${file}' escapes CWD scope.`);
227
- }
228
- try {
229
- const content = await fs.readFile(resolvedPath, 'utf8');
230
- return content;
231
- } catch (e) {
232
- throw new Error(`Failed to read '${file}': ${e.message}`);
224
+ if (typeof file !== 'string' || !file || file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
225
+ throw new Error('Relative CWD path only (no /, .., \\\\).');
233
226
  }
227
+ const resolved = path.resolve(process.cwd(), file);
228
+ if (!resolved.startsWith(process.cwd())) throw new Error('Escapes CWD.');
229
+ return await fs.readFile(resolved, 'utf8');
234
230
  }
235
231
  );
236
232
 
237
233
  tools.add(
238
234
  'write_file',
239
- '**MANDATORY**: 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 in params**). **AUTO-VALIDATES** JS/Python/Bash/JSON/etc. via `utils/syntax_check.sh`; chmod +x shebangs. Retries syntax errors force LLM fix. **Always use write_file for safety (CWD-only, syntax-checked).**',
235
+ 'Write/validate file in CWD. Auto-syntax check + JS fix + chmod.',
240
236
  {
241
237
  type: 'object',
242
238
  properties: {
243
- file: {
244
- type: 'string',
245
- description: `Relative path to the file within CWD, e.g., 'path/to/file.txt' (no escaping needed). cwd: ${user.cwd}`
246
- },
247
- content: {
248
- type: 'string',
249
- description: "Raw content to write (LITERAL: NO char escaping needed in this param; supports newlines, $, |, <, >, &, \", ', \\, \` , etc. verbatim). **Generate valid syntax for target lang** (e.g., JS template literals use raw `${var}`, no invalid \\`; syntax_check.sh rejects bad syntax like escaped backticks in JS)."
250
- }
239
+ file: { type: 'string', description: `Relative path. cwd: ${user.cwd}` },
240
+ content: { type: 'string', description: 'Raw content verbatim (no escaping).' }
251
241
  },
252
242
  required: ['file', 'content']
253
243
  },
254
244
  async (params) => {
255
245
  const file = params.file?.trim();
256
246
  const content = params.content ?? '';
257
- if (typeof file !== 'string' || !file) {
258
- throw new Error('Valid relative file path required.');
247
+ if (typeof file !== 'string' || !file || file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
248
+ throw new Error('Relative CWD path only.');
259
249
  }
260
- if (file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
261
- throw new Error('Path must be relative within CWD only (no `/`, `..`, or `\\\\`).');
262
- }
263
- const resolvedPath = path.resolve(process.cwd(), file);
264
- if (!resolvedPath.startsWith(process.cwd())) {
265
- throw new Error(`Path '${file}' escapes CWD scope.`);
266
- }
267
- try {
268
- await fs.writeFile(resolvedPath, content, 'utf8');
250
+ const resolved = path.resolve(process.cwd(), file);
251
+ if (!resolved.startsWith(process.cwd())) throw new Error('Escapes CWD.');
252
+ const dir = path.dirname(resolved);
253
+ const ext = path.extname(file);
254
+ let finalContent = content, validationMsg = '';
269
255
 
270
- // AUTO-VALIDATE: Multi-lang syntax check via utils/syntax_check.sh + chmod shebang
271
- const hasShebang = content.startsWith('#!');
256
+ const temp1 = path.join(dir, `temp_${Date.now()}_${Math.random().toString(36).slice(2,6)}${ext || '.tmp'}`);
257
+ await fs.writeFile(temp1, content, 'utf8');
272
258
 
273
- let validationMsg = '';
274
- try {
275
- await SH`${syntaxCheckSh} ${[resolvedPath]}`.run();
276
- validationMsg = ' ✓ Multi-lang syntax OK';
277
- } catch (e) {
278
- const errPreview = content.slice(0, 1000).split('\n').slice(0, 20).join('\n');
279
- throw new Error(`❌ SYNTAX ERROR in '${file}' (syntax_check.sh failed):\n${e.message}\n\nPREVIEW:\n${errPreview}\n\n... Fix syntax (quotes/backticks) and retry write_file.`);
259
+ try {
260
+ await SH`${syntaxCheckSh} ${[temp1]}`.run();
261
+ validationMsg = ' ✓ Syntax OK';
262
+ } catch (e) {
263
+ if (!['.js','.mjs'].includes(ext)) {
264
+ await fs.unlink(temp1).catch(()=>{});
265
+ throw new Error(`Syntax error: ${e.message}`);
280
266
  }
281
- if (hasShebang) {
282
- await SH`chmod +x ${[resolvedPath]}`.run();
283
- validationMsg += ' ✓ chmod +x';
267
+ let fixed = content.replace(/\\\\`/g, '\\`').replace(/\\\`/g, '`').replace(/\\`/g, '`').replace(/\\\$/g, '$').replace(/\\\${/g, '${');
268
+ const temp2 = path.join(dir, `temp_fix_${Date.now()}_${Math.random().toString(36).slice(2,6)}.js`);
269
+ await fs.writeFile(temp2, fixed, 'utf8');
270
+ try {
271
+ await SH`${syntaxCheckSh} ${[temp2]}`.run();
272
+ finalContent = fixed;
273
+ await fs.rename(temp2, resolved);
274
+ await fs.unlink(temp1).catch(()=>{});
275
+ validationMsg = ' ✓ Fixed JS';
276
+ } catch {
277
+ await fs.unlink(temp1).catch(()=>{});
278
+ await fs.unlink(temp2).catch(()=>{});
279
+ throw new Error(`Syntax error (fix failed): ${e.message}`);
284
280
  }
281
+ }
285
282
 
286
- return `Successfully wrote to '${file}' (${Buffer.byteLength(content, 'utf8')} bytes).${validationMsg}`;
287
- } catch (e) {
288
- throw new Error(`Failed to write '${file}': ${e.message}`);
283
+ if (validationMsg === ' ✓ Syntax OK') await fs.rename(temp1, resolved);
284
+
285
+ if (finalContent.startsWith('#!')) {
286
+ await SH`chmod +x ${[resolved]}`.run();
287
+ validationMsg += ' ✓ +x';
289
288
  }
289
+
290
+ return `Wrote ${file} (${Buffer.byteLength(finalContent, 'utf8')} bytes).${validationMsg}`;
290
291
  }
291
292
  );
292
293
 
293
294
  tools.add(
294
295
  'syntax_check',
295
- 'Standalone syntax validation for files (JS/Python/Bash/JSON/etc.) via utils/syntax_check.sh. Detects lang from ext/shebang.',
296
+ 'Syntax validate file via utils/syntax_check.sh.',
296
297
  {
297
298
  type: 'object',
298
299
  properties: {
299
- file: {
300
- type: 'string',
301
- description: `Relative path to the file within CWD.`
302
- }
300
+ file: { type: 'string', description: 'Relative CWD path' }
303
301
  },
304
302
  required: ['file']
305
303
  },
306
304
  async (params) => {
307
305
  const file = params.file?.trim();
308
- if (typeof file !== 'string' || !file) {
309
- throw new Error('Valid relative file path required.');
310
- }
311
- const resolvedPath = path.resolve(process.cwd(), file);
312
- if (!resolvedPath.startsWith(process.cwd())) {
313
- throw new Error(`Path '${file}' escapes CWD scope.`);
314
- }
315
- return await SH`${syntaxCheckSh} ${[resolvedPath]}`.run();
306
+ if (typeof file !== 'string' || !file) throw new Error('Relative path required.');
307
+ const resolved = path.resolve(process.cwd(), file);
308
+ if (!resolved.startsWith(process.cwd())) throw new Error('Escapes CWD.');
309
+ return await SH`${syntaxCheckSh} ${[resolved]}`.run();
316
310
  }
317
311
  );
318
312
 
319
-
320
- export default tools
313
+ export default tools;
package/lib/wsCli.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env -S node
2
- /*
3
- * WebSocket client for a hello-dave server with auto-reconnect and improved structure.
4
- * user CLI
2
+ /**
3
+ * @fileoverview Interactive WebSocket CLI client for hello-dave agent servers.
4
+ * Features: auto-reconnect, keyboard shortcuts (ALT-C/R/I/S/M, CTRL-K),
5
+ * session management, message history, clipboard copy.
6
+ *
7
+ * Depends on @j-o-r/cli for terminal UI, @j-o-r/apiserver WebSocketClient, @j-o-r/sh for shell.
5
8
  */
6
9
  import cli from '@j-o-r/cli';
7
10
  import { WebSocketClient } from "@j-o-r/apiserver";
@@ -9,11 +12,17 @@ import { SH } from '@j-o-r/sh';
9
12
 
10
13
  const OPEN = 1; // WebSocket.OPEN
11
14
 
15
+ /**
16
+ * @typedef {Object} WsMessage
17
+ * @property {string} action - Action type (e.g., 'user_request')
18
+ * @property {string} content - Message content
19
+ * @property {number} id - Unique message ID
20
+ */
12
21
 
13
22
  /**
14
23
  * Copy text to the clipboard using xclip.
15
- * @param {string} text
16
- * @returns {Promise<void>}
24
+ * @param {string} text - Text to copy
25
+ * @returns {Promise&lt;void&gt;}
17
26
  */
18
27
  const copyToClipboard = async (text) => {
19
28
  if (typeof text !== 'string') return;
@@ -21,11 +30,28 @@ const copyToClipboard = async (text) => {
21
30
  const prams = ['-selection', 'clipboard'];
22
31
  await SH`xclip ${prams}`.options({ stdio: 'inherit' }).run(text);
23
32
  };
33
+
24
34
  /**
25
- * Launch a CLI to an Agent Server
26
- * @param {string} connectUrl - Websocket server endpoint to connect to
27
- * @param {string} [secret] - Secret websocket connection key
28
- */
35
+ * Launches an interactive CLI client connected to a hello-dave agent server via WebSocket.
36
+ * Establishes persistent connection with auto-reconnect, handles user input,
37
+ * keyboard shortcuts for common actions, and displays responses.
38
+ *
39
+ * Keyboard shortcuts:
40
+ * - ALT-C: Clear screen
41
+ * - ALT-R: Reset session
42
+ * - ALT-S: List/load sessions
43
+ * - ALT-I: Server info
44
+ * - ALT-M: Copy last message to clipboard
45
+ * - CTRL-K: Show help
46
+ * - CTRL-D: Exit (standard)
47
+ *
48
+ * @param {string} connectUrl - WebSocket server endpoint (e.g., 'ws://localhost:8080')
49
+ * @param {string} [secret=''] - Optional base64 secret for authenticated connections
50
+ * @returns {void}
51
+ * @example
52
+ * import wsCli from './lib/wsCli.js';
53
+ * wsCli('ws://localhost:8080', 'mysecret');
54
+ */
29
55
  export default (connectUrl, secret = '') => {
30
56
  let ws;
31
57
  let busy = false;
@@ -36,8 +62,12 @@ export default (connectUrl, secret = '') => {
36
62
  }
37
63
 
38
64
  /**
39
- * Connect to the WebSocket server (with handlers).
40
- * Sets up reconnection on close.
65
+ * Connects (or reconnects) to the WebSocket server.
66
+ * Sets up event handlers for messages, close (auto-reconnect), errors.
67
+ * Sends user_introduction on open.
68
+ *
69
+ * @private
70
+ * @returns {void}
41
71
  */
42
72
  const connect = () => {
43
73
  if (ws && ws.readyState === OPEN) {
@@ -94,10 +124,14 @@ export default (connectUrl, secret = '') => {
94
124
  };
95
125
 
96
126
  /**
97
- * Send a message and await response.
98
- * Handles response actions and UI updates.
99
- * @param {Object} message - Message object (action, content)
100
- * @returns {Promise<Object>} Response data
127
+ * Sends a message and awaits response by ID.
128
+ * Handles special response actions (e.g., server_response updates UI).
129
+ * Manages busy state and spinner.
130
+ *
131
+ * @private
132
+ * @param {WsMessage} message - Message to send (id auto-added)
133
+ * @returns {Promise&lt;WsMessage&gt;} Response data
134
+ * @throws {Error} If not connected, timeout (12h), or connection error
101
135
  */
102
136
  const sendMessage = async (message) => {
103
137
  if (!ws || ws.readyState !== OPEN) {
@@ -246,11 +280,8 @@ Available keys:
246
280
  };
247
281
 
248
282
  // Initialize
249
-
250
283
  cli.focus('log');
251
284
  cli.write('Connecting... (ALT-I for info, CTRL-K for keys, CTRL-D to exit)');
252
285
 
253
286
  connect();
254
-
255
- }
256
-
287
+ };
package/lib/wsIO.js CHANGED
@@ -14,17 +14,21 @@ import { WebSocket } from 'ws';
14
14
  * Sends intro + action, awaits matching response by ID, closes, returns response.
15
15
  *
16
16
  * @param {string} connectUrl - Websocket server endpoint to connect to
17
- * @param {string} [secret] - Secret websocket connection key
18
- * @param {'user_request'|'user_info'|'user_reset'} action - Action
19
- * @param {string} [input] - When action is 'user_request' input is the query
20
- * @returns {Promise<wsResponse>}
17
+ * @param {string} [secret=''] - Secret websocket connection key
18
+ * @param {'user_request'|'user_info'|'user_reset'} action - Action to perform
19
+ * @param {string} [input=''] - Input content (required for 'user_request')
20
+ * @returns {Promise&lt;wsResponse&gt;}
21
+ * @throws {Error} Invalid action or missing input for user_request
22
+ * @example
23
+ * const response = await wsio('ws://localhost:8080', 'secret', 'user_request', 'Hello!');
24
+ * console.log(response.content);
21
25
  */
22
26
  export default async function wsio(connectUrl, secret = '', action, input = '') {
23
27
  if (!['user_request', 'user_reset', 'user_info'].includes(action)) {
24
- throw new Error(`Invalid action: ${action}. Must be one of: user_input, user_reset, user_info`);
28
+ throw new Error(`Invalid action: ${action}. Must be one of: user_request, user_reset, user_info`);
25
29
  }
26
30
  if (action === 'user_request' && (!input || typeof input !== 'string' || input.trim() === '')) {
27
- throw new Error('Non-empty string input required for "user_input"');
31
+ throw new Error('Non-empty string input required for "user_request"');
28
32
  }
29
33
 
30
34
  let b64secret = '';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@j-o-r/hello-dave",
3
3
  "type": "module",
4
- "version": "0.0.6",
4
+ "version": "0.0.8",
5
5
  "description": "ESM toolkit for building AI agents with unified access to Grok (XAI), OpenAI, and Anthropic endpoints",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./types/index.d.ts",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "bin": {
16
16
  "dave": "bin/dave.js",
17
- "createAgent": "bin/spawn_agent.js"
17
+ "codeDave": "bin/codeDave"
18
18
  },
19
19
  "scripts": {
20
20
  "release": "npm run types && npm pack --pack-destination=release",
@@ -51,4 +51,4 @@
51
51
  "engines": {
52
52
  "node": ">=20"
53
53
  }
54
- }
54
+ }