@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.
- package/CHANGELOG.md +19 -33
- package/README.md +240 -0
- package/TODO.md +13 -0
- package/{examples → agents}/ask_agent.js +5 -5
- package/{examples → agents}/codeserver.sh +14 -14
- package/{examples → agents}/daisy_agent.js +5 -5
- package/{examples → agents}/docs_agent.js +5 -5
- package/{examples → agents}/gpt_agent.js +5 -5
- package/{examples → agents}/grok_agent.js +5 -5
- package/{examples → agents}/memory_agent.js +5 -5
- package/{examples → agents}/npm_agent.js +5 -5
- package/{examples → agents}/prompt_agent.js +5 -5
- package/agents/spawn_agent.js +137 -0
- package/{examples → agents}/test_agent.js +6 -6
- package/{examples → agents}/todo_agent.js +5 -5
- package/bin/codeDave +58 -0
- package/bin/dave.js +3 -5
- package/docs/agent-manager.md +244 -0
- package/docs/bin-dave.md +62 -0
- package/docs/codeserver-pattern.md +191 -0
- package/docs/generic-toolset.md +326 -0
- package/docs/howtos/agent-networking.md +253 -0
- package/docs/howtos/spawn-agents.md.bak +200 -0
- package/docs/howtos/spawn-agents.md.bak_new +200 -0
- package/docs/jsdoc-best-practices.md +278 -0
- package/docs/multi-agent-clusters.md +265 -0
- package/docs/multi-agent-clusters.md.bak +229 -0
- package/docs/path-resolution-best-practices.md +104 -0
- package/docs/project-overview.md +67 -0
- package/docs/prompt/spawn_agent.md +173 -0
- package/docs/prompt/spawn_agent.md.bak +201 -0
- package/docs/prompt-class.md +141 -0
- package/docs/suggestions.md +38 -0
- package/docs/todo-archive-v0.0.8.md +1 -0
- package/docs/todo-archive.md +44 -0
- package/docs/tools-syntax-validation.md +121 -0
- package/docs/toolset.md +164 -0
- package/docs/xai-responses.md +111 -0
- package/docs/xai_collections.md +106 -0
- package/lib/AgentClient.js +111 -67
- package/lib/AgentManager.js +111 -80
- package/lib/AgentServer.js +144 -104
- package/lib/Cli.js +126 -93
- package/lib/Prompt.js +38 -5
- package/lib/Session.js +102 -79
- package/lib/ToolSet.js +79 -60
- package/lib/fafs.js +54 -19
- package/lib/genericToolset.js +129 -136
- package/lib/wsCli.js +50 -19
- package/lib/wsIO.js +10 -6
- package/package.json +3 -3
- package/types/AgentClient.d.ts +69 -35
- package/types/AgentManager.d.ts +50 -56
- package/types/AgentServer.d.ts +63 -16
- package/types/Cli.d.ts +56 -10
- package/types/Prompt.d.ts +36 -4
- package/types/Session.d.ts +23 -9
- package/types/ToolSet.d.ts +49 -32
- package/types/fafs.d.ts +68 -25
- package/types/wsCli.d.ts +14 -0
- package/types/wsIO.d.ts +9 -5
- package/utils/search_sessions.sh +100 -53
- package/bin/spawn_agent.js +0 -293
- package/lib/genericToolset.js.bak_syntax +0 -402
- /package/{examples → agents}/code_agent.js +0 -0
- /package/{examples → agents}/readme_agent.js +0 -0
package/lib/genericToolset.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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',
|
|
78
|
-
'Get
|
|
79
|
-
{
|
|
80
|
-
|
|
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
|
|
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: {
|
|
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
|
|
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
|
|
127
|
+
'Send email via msmtp.',
|
|
111
128
|
{
|
|
112
129
|
type: 'object',
|
|
113
130
|
properties: {
|
|
114
|
-
to: { type: 'string', description: 'Recipient
|
|
131
|
+
to: { type: 'string', description: 'Recipient' },
|
|
115
132
|
subject: { type: 'string', description: 'Subject' },
|
|
116
|
-
body: { type: 'string', description: '
|
|
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
|
|
151
|
+
'Open URL/file with xdg-open.',
|
|
134
152
|
{
|
|
135
153
|
type: 'object',
|
|
136
154
|
properties: {
|
|
137
|
-
url: { type: 'string', description: 'file
|
|
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
|
-
'
|
|
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: '
|
|
152
|
-
script: { type: 'string', description: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
-
'
|
|
235
|
+
'Write/validate file in CWD. Auto-syntax check + JS fix + chmod.',
|
|
240
236
|
{
|
|
241
237
|
type: 'object',
|
|
242
238
|
properties: {
|
|
243
|
-
file: {
|
|
244
|
-
|
|
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('
|
|
247
|
+
if (typeof file !== 'string' || !file || file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
|
|
248
|
+
throw new Error('Relative CWD path only.');
|
|
259
249
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
throw new Error(
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
4
|
-
*
|
|
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
|
|
24
|
+
* @param {string} text - Text to copy
|
|
25
|
+
* @returns {Promise<void>}
|
|
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
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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
|
-
*
|
|
40
|
-
* Sets up
|
|
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
|
-
*
|
|
98
|
-
* Handles response actions
|
|
99
|
-
*
|
|
100
|
-
*
|
|
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<WsMessage>} 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] -
|
|
20
|
-
* @returns {Promise
|
|
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<wsResponse>}
|
|
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:
|
|
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 "
|
|
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.
|
|
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
|
-
"
|
|
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
|
+
}
|