@j-o-r/hello-dave 0.0.5 → 0.0.7

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 (127) hide show
  1. package/CHANGELOG.md +13 -26
  2. package/README.md +161 -522
  3. package/README.md.bak +144 -449
  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/agents/memory_agent.js +263 -0
  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 -8
  15. package/{examples → agents}/todo_agent.js +5 -5
  16. package/bin/codeDave +58 -0
  17. package/bin/dave.js +114 -96
  18. package/lib/AgentClient.js +111 -67
  19. package/lib/AgentManager.js +111 -80
  20. package/lib/AgentServer.js +144 -104
  21. package/lib/Cli.js +126 -93
  22. package/lib/Prompt.js +38 -5
  23. package/lib/Session.js +102 -79
  24. package/lib/ToolSet.js +79 -60
  25. package/lib/fafs.js +54 -19
  26. package/lib/genericToolset.js +109 -213
  27. package/lib/wsCli.js +50 -19
  28. package/lib/wsIO.js +11 -17
  29. package/package.json +2 -2
  30. package/types/AgentClient.d.ts +69 -35
  31. package/types/AgentManager.d.ts +50 -56
  32. package/types/AgentServer.d.ts +63 -16
  33. package/types/Cli.d.ts +56 -10
  34. package/types/Prompt.d.ts +36 -4
  35. package/types/Session.d.ts +23 -9
  36. package/types/ToolSet.d.ts +49 -32
  37. package/types/fafs.d.ts +68 -25
  38. package/types/wsCli.d.ts +14 -0
  39. package/types/wsIO.d.ts +9 -5
  40. package/utils/search_sessions.sh +100 -53
  41. package/README.md.backup +0 -269
  42. package/README.md.bak.1774780058 +0 -338
  43. package/README.md.bak2 +0 -531
  44. package/bin/spawn_agent.js +0 -293
  45. package/docs.bak.1774780058/agent-manager.md +0 -167
  46. package/docs.bak.1774780058/agent-manager.md.bak +0 -137
  47. package/docs.bak.1774780058/agent-manager.md.bak2 +0 -157
  48. package/docs.bak.1774780058/codeserver-pattern.md +0 -191
  49. package/docs.bak.1774780058/path-resolution-best-practices.md +0 -104
  50. package/docs.bak.1774780058/project-overview.md +0 -67
  51. package/docs.bak.1774780058/project-overview.md.bak +0 -67
  52. package/docs.bak.1774780058/prompt-class.md +0 -141
  53. package/docs.bak.1774780058/prompt-class.md.bak +0 -142
  54. package/docs.bak.1774780058/tools-syntax-validation.md +0 -121
  55. package/docs.bak.1774780058/tools-syntax-validation.md.bak2 +0 -125
  56. package/docs.bak.1774780058/tools-syntax-validation.md.bak3 +0 -125
  57. package/docs.bak.1774780058/tools-syntax-validation.md.bak4 +0 -106
  58. package/docs.bak.1774780058/tools-syntax-validation.md.bak_path +0 -106
  59. package/docs.bak.1774780058/toolset.md +0 -164
  60. package/docs.bak.1774780058/toolset.md.bak +0 -94
  61. package/docs.bak.1774780058/toolset.md.bak3 +0 -161
  62. package/docs.bak.1774780058/toolset.md.bak4 +0 -161
  63. package/docs.bak.1774780058/toolset.md.bak5 +0 -161
  64. package/docs.bak.1774780058/toolset.md.bak6 +0 -163
  65. package/docs.bak.1774780058/toolset.md.bak_path +0 -163
  66. package/docs.bak.1774780058/toolset.md.bak_syntax +0 -161
  67. package/docs.bak.1774780058/xai-responses.md +0 -111
  68. package/docs.bak.1774780058/xai-responses.md.bak +0 -107
  69. package/docs.bak.1774780058/xai-responses.md.bak2 +0 -107
  70. package/docs.bak.1774780058/xai_collections.md +0 -106
  71. package/examples/memory_agent.js +0 -152
  72. package/examples.bak.1774780058/ask_agent.js +0 -114
  73. package/examples.bak.1774780058/code_agent.js +0 -149
  74. package/examples.bak.1774780058/coderev_agent.js +0 -72
  75. package/examples.bak.1774780058/codeserver.sh +0 -47
  76. package/examples.bak.1774780058/daisy_agent.js +0 -177
  77. package/examples.bak.1774780058/docs_agent.js +0 -119
  78. package/examples.bak.1774780058/gpt_agent.js +0 -109
  79. package/examples.bak.1774780058/grok_agent.js +0 -98
  80. package/examples.bak.1774780058/memory_agent.js +0 -112
  81. package/examples.bak.1774780058/npm_agent.js +0 -175
  82. package/examples.bak.1774780058/prompt_agent.js +0 -112
  83. package/examples.bak.1774780058/readme_agent.js +0 -144
  84. package/examples.bak.1774780058/spawn_agent.js +0 -263
  85. package/examples.bak.1774780058/test_agent.js +0 -162
  86. package/examples.bak.1774780058/todo_agent.js +0 -138
  87. package/lib/genericToolset.js.bak_syntax +0 -402
  88. package/scenarios.bak.1774780058/data/eval_node_message.json +0 -9
  89. package/scenarios.bak.1774780058/data/hist_oa.json +0 -66
  90. package/scenarios.bak.1774780058/data/o3_response1.json +0 -96
  91. package/scenarios.bak.1774780058/data/oa_reasoning_parse.json +0 -112
  92. package/scenarios.bak.1774780058/data/tool_oa.json +0 -96
  93. package/scenarios.bak.1774780058/data/tool_xai.json +0 -59
  94. package/scenarios.bak.1774780058/data/tool_xai2.json +0 -40
  95. package/scenarios.bak.1774780058/data/xai-response-1.json +0 -59
  96. package/scenarios.bak.1774780058/data/xai-response-2.json +0 -10
  97. package/scenarios.bak.1774780058/data/xai_reasoning_tools_resp.json +0 -59
  98. package/scenarios.bak.1774780058/data/xai_search_response.json +0 -58
  99. package/scenarios.bak.1774780058/environment.js +0 -10
  100. package/scenarios.bak.1774780058/example.js +0 -17
  101. package/scenarios.bak.1774780058/genericToolset.test.js +0 -182
  102. package/scenarios.bak.1774780058/grok.js +0 -113
  103. package/scenarios.bak.1774780058/memory-tools.js +0 -51
  104. package/scenarios.bak.1774780058/openai-o3.js +0 -137
  105. package/scenarios.bak.1774780058/openai-prompt.js +0 -155
  106. package/scenarios.bak.1774780058/openai-session.js +0 -148
  107. package/scenarios.bak.1774780058/openai.js +0 -102
  108. package/scenarios.bak.1774780058/prompt.js +0 -118
  109. package/scenarios.bak.1774780058/promptFishbowl.js +0 -76
  110. package/scenarios.bak.1774780058/search.brave.com.js +0 -25
  111. package/scenarios.bak.1774780058/sh.js +0 -15
  112. package/scenarios.bak.1774780058/test-wsio.js +0 -26
  113. package/scenarios.bak.1774780058/testToolset.js +0 -42
  114. package/scenarios.bak.1774780058/toolset.js +0 -16
  115. package/scenarios.bak.1774780058/toolset.test.js +0 -141
  116. package/scenarios.bak.1774780058/write_file_syntax.test.js +0 -145
  117. package/scenarios.bak.1774780058/write_file_validation/README.md +0 -30
  118. package/scenarios.bak.1774780058/write_file_validation/bad.js +0 -3
  119. package/scenarios.bak.1774780058/write_file_validation/good.js +0 -4
  120. package/scenarios.bak.1774780058/write_file_validation/test.sh +0 -43
  121. package/scenarios.bak.1774780058/wsClient.js +0 -69
  122. package/scenarios.bak.1774780058/xai_responses.integration.test.js +0 -57
  123. package/scenarios.bak.1774780058/xai_responses.test.js +0 -154
  124. package/scenarios.bak.1774780058/xaicoll.js +0 -50
  125. package/scenarios.bak.1774780058/xaifiles.js +0 -48
  126. /package/{examples → agents}/code_agent.js +0 -0
  127. /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,32 +77,29 @@ ${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).',
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 heredoc. System: ${user.system}`
102
+ }
96
103
  },
97
104
  required: ['bash_script']
98
105
  },
@@ -101,19 +108,19 @@ tools.add(
101
108
  return await SH`bash <<'${delim}'
102
109
  ${params.bash_script}
103
110
  ${delim}
104
- `.run()
111
+ `.run();
105
112
  }
106
113
  );
107
114
 
108
115
  tools.add(
109
116
  'send_email',
110
- 'Send an email.',
117
+ 'Send email via msmtp.',
111
118
  {
112
119
  type: 'object',
113
120
  properties: {
114
- to: { type: 'string', description: 'Recipient email' },
121
+ to: { type: 'string', description: 'Recipient' },
115
122
  subject: { type: 'string', description: 'Subject' },
116
- body: { type: 'string', description: 'Message body' }
123
+ body: { type: 'string', description: 'Body' }
117
124
  },
118
125
  required: ['to', 'subject', 'body']
119
126
  },
@@ -128,277 +135,166 @@ ${delim}
128
135
  `.run();
129
136
  }
130
137
  );
138
+
131
139
  tools.add(
132
140
  'open_link',
133
- 'Open an url or file in the local user environment. (xdg-open)',
141
+ 'Open URL/file with xdg-open.',
134
142
  {
135
143
  type: 'object',
136
144
  properties: {
137
- url: { type: 'string', description: 'file | URL' }
145
+ url: { type: 'string', description: 'URL or file path' }
138
146
  },
139
147
  required: ['url']
140
148
  },
141
- async (params) => {
142
- return await SH`xdg-open ${[params.url]}`.run();
143
- }
149
+ async (params) => await SH`xdg-open ${[params.url]}`.run()
144
150
  );
151
+
145
152
  tools.add(
146
153
  'execute_remote_script',
147
- 'Execute bash script on a remote machine via SSH.',
154
+ 'Run bash on remote via SSH.',
148
155
  {
149
156
  type: 'object',
150
157
  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)' }
158
+ url: { type: 'string', description: 'ssh://user@host[:port]' },
159
+ script: { type: 'string', description: 'Raw script' }
153
160
  },
154
161
  required: ['url', 'script']
155
162
  },
156
163
  async (params) => {
157
164
  const { url, script } = params;
158
- if (!url.startsWith('ssh://')) throw new Error('Invalid SSH URL');
165
+ if (!url.startsWith('ssh://')) throw new Error('ssh://user@host[:port]');
159
166
  const withoutProto = url.slice(6);
160
167
  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
- }
168
+ let port = 22, userHost = withoutProto;
169
+ if (parts.length > 1) { userHost = parts[0]; port = parseInt(parts[1]); }
167
170
  const [user, host] = userHost.split('@');
168
- if (!user || !host) throw new Error('Invalid SSH URL format: use ssh://user@host[:port]');
171
+ if (!user || !host) throw new Error('Invalid SSH URL');
169
172
 
170
173
  const delim = `END_SCRIPT_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
171
174
  return await SH`ssh -p ${port} ${user}@${host} bash <<'${delim}'
172
175
  ${script}
173
176
  ${delim}
174
177
  `.run();
175
-
176
178
  }
177
179
  );
180
+
178
181
  tools.add(
179
182
  '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).`,
183
+ 'Search/list chat sessions in .cache/.',
184
184
  {
185
185
  type: 'object',
186
186
  properties: {
187
- query: {
188
- type: 'string',
189
- description: `Search query or regex (quoted for multi-word/regex). Omit or empty for list mode.`
190
- }
187
+ query: { type: 'string', description: 'Query/regex or empty to list' }
191
188
  },
192
189
  required: []
193
190
  },
194
191
  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();
192
+ if (typeof params.query === 'string' && params.query.trim()) {
193
+ return await SH`${searchSessionsSh} "${bashEscape(params.query)}"`.run();
200
194
  }
195
+ return await SH`${listSessionsSh}`.run();
201
196
  }
202
197
  );
198
+
203
199
  tools.add(
204
200
  'read_file',
205
- 'Read the raw content of a file strictly within the current working directory (CWD). Paths must be relative (no leading /, no ..).',
201
+ 'Read file from CWD (relative path only).',
206
202
  {
207
203
  type: 'object',
208
204
  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
- }
205
+ file: { type: 'string', description: `Relative path. cwd: ${user.cwd}` }
213
206
  },
214
207
  required: ['file']
215
208
  },
216
209
  async (params) => {
217
210
  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}`);
211
+ if (typeof file !== 'string' || !file || file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
212
+ throw new Error('Relative CWD path only (no /, .., \\\\).');
233
213
  }
214
+ const resolved = path.resolve(process.cwd(), file);
215
+ if (!resolved.startsWith(process.cwd())) throw new Error('Escapes CWD.');
216
+ return await fs.readFile(resolved, 'utf8');
234
217
  }
235
218
  );
236
219
 
237
220
  tools.add(
238
221
  'write_file',
239
- '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). **AUTO-VALIDATES** JS/Python/Bash/JSON/etc. via `utils/syntax_check.sh`; chmod +x shebangs. Retries syntax errors force LLM fix.',
222
+ 'Write/validate file in CWD. Auto-syntax check + JS fix + chmod.',
240
223
  {
241
224
  type: 'object',
242
225
  properties: {
243
- file: {
244
- type: 'string',
245
- description: `Relative path to the file within CWD, e.g., 'path/to/file.txt' (no escaping needed).`
246
- },
247
- content: {
248
- type: 'string',
249
- description: `Raw content to write (as-is, no char escaping needed; supports newlines, $, |, <, >, &, ", ', \\, etc.).`
250
- }
226
+ file: { type: 'string', description: `Relative path. cwd: ${user.cwd}` },
227
+ content: { type: 'string', description: 'Raw content verbatim (no escaping).' }
251
228
  },
252
229
  required: ['file', 'content']
253
230
  },
254
231
  async (params) => {
255
232
  const file = params.file?.trim();
256
233
  const content = params.content ?? '';
257
- if (typeof file !== 'string' || !file) {
258
- throw new Error('Valid relative file path required.');
259
- }
260
- if (file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
261
- throw new Error('Path must be relative within CWD only (no `/`, `..`, or `\\\\`).');
234
+ if (typeof file !== 'string' || !file || file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
235
+ throw new Error('Relative CWD path only.');
262
236
  }
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');
237
+ const resolved = path.resolve(process.cwd(), file);
238
+ if (!resolved.startsWith(process.cwd())) throw new Error('Escapes CWD.');
239
+ const dir = path.dirname(resolved);
240
+ const ext = path.extname(file);
241
+ let finalContent = content, validationMsg = '';
269
242
 
270
- // AUTO-VALIDATE: Multi-lang syntax check via utils/syntax_check.sh + chmod shebang
271
- const hasShebang = content.startsWith('#!');
243
+ const temp1 = path.join(dir, `temp_${Date.now()}_${Math.random().toString(36).slice(2,6)}${ext || '.tmp'}`);
244
+ await fs.writeFile(temp1, content, 'utf8');
272
245
 
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.`);
246
+ try {
247
+ await SH`${syntaxCheckSh} ${[temp1]}`.run();
248
+ validationMsg = ' ✓ Syntax OK';
249
+ } catch (e) {
250
+ if (!['.js','.mjs'].includes(ext)) {
251
+ await fs.unlink(temp1).catch(()=>{});
252
+ throw new Error(`Syntax error: ${e.message}`);
280
253
  }
281
- if (hasShebang) {
282
- await SH`chmod +x ${[resolvedPath]}`.run();
283
- validationMsg += ' ✓ chmod +x';
254
+ let fixed = content.replace(/\\\\`/g, '\\`').replace(/\\\`/g, '`').replace(/\\`/g, '`').replace(/\\\$/g, '$').replace(/\\\${/g, '${');
255
+ const temp2 = path.join(dir, `temp_fix_${Date.now()}_${Math.random().toString(36).slice(2,6)}.js`);
256
+ await fs.writeFile(temp2, fixed, 'utf8');
257
+ try {
258
+ await SH`${syntaxCheckSh} ${[temp2]}`.run();
259
+ finalContent = fixed;
260
+ await fs.rename(temp2, resolved);
261
+ await fs.unlink(temp1).catch(()=>{});
262
+ validationMsg = ' ✓ Fixed JS';
263
+ } catch {
264
+ await fs.unlink(temp1).catch(()=>{});
265
+ await fs.unlink(temp2).catch(()=>{});
266
+ throw new Error(`Syntax error (fix failed): ${e.message}`);
284
267
  }
268
+ }
285
269
 
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}`);
270
+ if (validationMsg === ' ✓ Syntax OK') await fs.rename(temp1, resolved);
271
+
272
+ if (finalContent.startsWith('#!')) {
273
+ await SH`chmod +x ${[resolved]}`.run();
274
+ validationMsg += ' ✓ +x';
289
275
  }
276
+
277
+ return `Wrote ${file} (${Buffer.byteLength(finalContent, 'utf8')} bytes).${validationMsg}`;
290
278
  }
291
279
  );
292
280
 
293
281
  tools.add(
294
282
  'syntax_check',
295
- 'Standalone syntax validation for files (JS/Python/Bash/JSON/etc.) via utils/syntax_check.sh. Detects lang from ext/shebang.',
283
+ 'Syntax validate file via utils/syntax_check.sh.',
296
284
  {
297
285
  type: 'object',
298
286
  properties: {
299
- file: {
300
- type: 'string',
301
- description: `Relative path to the file within CWD.`
302
- }
287
+ file: { type: 'string', description: 'Relative CWD path' }
303
288
  },
304
289
  required: ['file']
305
290
  },
306
291
  async (params) => {
307
292
  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();
316
- }
317
- );
318
-
319
- tools.add(
320
- 'memory_write',
321
- `Persist agent memory for tasks, errors, or user preferences to .cache/memory.ndjson in CWD (${user.cwd}). Use to store decisions, tasks, errors, or prefs for later recall. Agent should use this to avoid repeating work or token burn on loops.`,
322
- {
323
- type: 'object',
324
- properties: {
325
- category: {
326
- type: 'string',
327
- enum: ['tasks', 'errors', 'prefs'],
328
- description: 'Category: one of "tasks", "errors", "prefs"'
329
- },
330
- content: {
331
- type: 'string',
332
- description: 'Detailed content (e.g., "Pending task: implement runWithMemory optimization", "Error: loop burning tokens", "Pref: temperature=0.2 for decisions")'
333
- }
334
- },
335
- required: ['category', 'content']
336
- },
337
- async ({ category, content }) => {
338
- if (!['tasks', 'errors', 'prefs'].includes(category)) {
339
- throw new Error(`Invalid category '${category}'. Must be 'tasks', 'errors', or 'prefs'.`);
340
- }
341
- const memoryPath = path.join(process.cwd(), '.cache', 'memory.ndjson');
342
- const dir = path.dirname(memoryPath);
343
- try {
344
- await fs.mkdir(dir, { recursive: true });
345
- } catch (e) {
346
- // Dir likely exists
347
- }
348
- const entry = {
349
- timestamp: new Date().toISOString(),
350
- category,
351
- content: content.trim()
352
- };
353
- await fs.appendFile(memoryPath, JSON.stringify(entry) + '\n', 'utf8');
354
- return `✓ Memory stored: [${category}] ${content.length > 50 ? content.slice(0, 47) + '...' : content}`;
355
- }
356
- );
357
-
358
- tools.add(
359
- 'memory_recall',
360
- `Retrieve stored agent memories (tasks, errors, prefs) from .cache/memory.ndjson in CWD (${user.cwd}). Use before acting to check prior decisions/tasks/errors/prefs. Query by keyword or category; empty lists recent.`,
361
- {
362
- type: 'object',
363
- properties: {
364
- query: {
365
- type: 'string',
366
- description: 'Keyword, category (tasks/errors/prefs), or phrase to filter (case-insensitive). Empty/omit lists last 20.'
367
- }
368
- },
369
- required: []
370
- },
371
- async (params = {}) => {
372
- const memoryPath = path.join(process.cwd(), '.cache', 'memory.ndjson');
373
- let content;
374
- try {
375
- content = await fs.readFile(memoryPath, 'utf8');
376
- } catch (e) {
377
- return 'No memories stored yet. Use memory_write first.';
378
- }
379
- const lines = content.trim().split('\n').filter(l => l.trim());
380
- const memories = [];
381
- for (const line of lines) {
382
- try {
383
- memories.push(JSON.parse(line));
384
- } catch {
385
- // Skip invalid lines
386
- }
387
- }
388
- if (!params.query || !params.query.toString().trim()) {
389
- const recent = memories.slice(-20).reverse();
390
- return recent.length
391
- ? 'Recent memories:\n' + recent.map(m => `• ${m.timestamp.slice(0, 19).replace('T', ' ')} [${m.category}] ${m.content}`).join('\n')
392
- : 'No memories stored.';
393
- }
394
- const q = params.query.toString().toLowerCase();
395
- const matches = memories.filter(m =>
396
- m.category.toLowerCase().includes(q) || m.content.toLowerCase().includes(q)
397
- );
398
- return matches.length
399
- ? `Matches for "${params.query}":\n` + matches.slice(0, 20).map(m => `• ${m.timestamp.slice(0, 19).replace('T', ' ')} [${m.category}] ${m.content}`).join('\n')
400
- : `No memories match "${params.query}".`;
293
+ if (typeof file !== 'string' || !file) throw new Error('Relative path required.');
294
+ const resolved = path.resolve(process.cwd(), file);
295
+ if (!resolved.startsWith(process.cwd())) throw new Error('Escapes CWD.');
296
+ return await SH`${syntaxCheckSh} ${[resolved]}`.run();
401
297
  }
402
298
  );
403
299
 
404
- export default tools
300
+ 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 = '';
@@ -38,13 +42,6 @@ export default async function wsio(connectUrl, secret = '', action, input = '')
38
42
  const ws = new WebSocket(url);
39
43
 
40
44
  let resolved = false;
41
- const TIMEOUT_MS = 30000; // 30s timeout
42
- const timeoutId = setTimeout(() => {
43
- if (!resolved) {
44
- ws.close();
45
- reject(new Error('Request timeout (30s)'));
46
- }
47
- }, TIMEOUT_MS);
48
45
 
49
46
  const actionId = Date.now();
50
47
 
@@ -69,7 +66,6 @@ export default async function wsio(connectUrl, secret = '', action, input = '')
69
66
  try {
70
67
  const parsed = JSON.parse(data.toString());
71
68
  if (parsed.id === actionId) {
72
- clearTimeout(timeoutId);
73
69
  resolved = true;
74
70
  ws.close(1000, 'Request complete');
75
71
  resolve(parsed);
@@ -80,17 +76,15 @@ export default async function wsio(connectUrl, secret = '', action, input = '')
80
76
  });
81
77
 
82
78
  ws.on('close', (code, reason) => {
83
- clearTimeout(timeoutId);
84
79
  if (!resolved) {
85
80
  reject(new Error(`Connection closed (${code}): ${reason || 'Unknown reason'}`));
86
81
  }
87
82
  });
88
83
 
89
84
  ws.on('error', (error) => {
90
- clearTimeout(timeoutId);
91
85
  if (!resolved) {
92
86
  reject(new Error(`WebSocket error: ${error.message}`));
93
87
  }
94
88
  });
95
89
  });
96
- }
90
+ }