@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.
- package/CHANGELOG.md +13 -26
- package/README.md +161 -522
- package/README.md.bak +144 -449
- 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/agents/memory_agent.js +263 -0
- 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 -8
- package/{examples → agents}/todo_agent.js +5 -5
- package/bin/codeDave +58 -0
- package/bin/dave.js +114 -96
- 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 +109 -213
- package/lib/wsCli.js +50 -19
- package/lib/wsIO.js +11 -17
- package/package.json +2 -2
- 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/README.md.backup +0 -269
- package/README.md.bak.1774780058 +0 -338
- package/README.md.bak2 +0 -531
- package/bin/spawn_agent.js +0 -293
- package/docs.bak.1774780058/agent-manager.md +0 -167
- package/docs.bak.1774780058/agent-manager.md.bak +0 -137
- package/docs.bak.1774780058/agent-manager.md.bak2 +0 -157
- package/docs.bak.1774780058/codeserver-pattern.md +0 -191
- package/docs.bak.1774780058/path-resolution-best-practices.md +0 -104
- package/docs.bak.1774780058/project-overview.md +0 -67
- package/docs.bak.1774780058/project-overview.md.bak +0 -67
- package/docs.bak.1774780058/prompt-class.md +0 -141
- package/docs.bak.1774780058/prompt-class.md.bak +0 -142
- package/docs.bak.1774780058/tools-syntax-validation.md +0 -121
- package/docs.bak.1774780058/tools-syntax-validation.md.bak2 +0 -125
- package/docs.bak.1774780058/tools-syntax-validation.md.bak3 +0 -125
- package/docs.bak.1774780058/tools-syntax-validation.md.bak4 +0 -106
- package/docs.bak.1774780058/tools-syntax-validation.md.bak_path +0 -106
- package/docs.bak.1774780058/toolset.md +0 -164
- package/docs.bak.1774780058/toolset.md.bak +0 -94
- package/docs.bak.1774780058/toolset.md.bak3 +0 -161
- package/docs.bak.1774780058/toolset.md.bak4 +0 -161
- package/docs.bak.1774780058/toolset.md.bak5 +0 -161
- package/docs.bak.1774780058/toolset.md.bak6 +0 -163
- package/docs.bak.1774780058/toolset.md.bak_path +0 -163
- package/docs.bak.1774780058/toolset.md.bak_syntax +0 -161
- package/docs.bak.1774780058/xai-responses.md +0 -111
- package/docs.bak.1774780058/xai-responses.md.bak +0 -107
- package/docs.bak.1774780058/xai-responses.md.bak2 +0 -107
- package/docs.bak.1774780058/xai_collections.md +0 -106
- package/examples/memory_agent.js +0 -152
- package/examples.bak.1774780058/ask_agent.js +0 -114
- package/examples.bak.1774780058/code_agent.js +0 -149
- package/examples.bak.1774780058/coderev_agent.js +0 -72
- package/examples.bak.1774780058/codeserver.sh +0 -47
- package/examples.bak.1774780058/daisy_agent.js +0 -177
- package/examples.bak.1774780058/docs_agent.js +0 -119
- package/examples.bak.1774780058/gpt_agent.js +0 -109
- package/examples.bak.1774780058/grok_agent.js +0 -98
- package/examples.bak.1774780058/memory_agent.js +0 -112
- package/examples.bak.1774780058/npm_agent.js +0 -175
- package/examples.bak.1774780058/prompt_agent.js +0 -112
- package/examples.bak.1774780058/readme_agent.js +0 -144
- package/examples.bak.1774780058/spawn_agent.js +0 -263
- package/examples.bak.1774780058/test_agent.js +0 -162
- package/examples.bak.1774780058/todo_agent.js +0 -138
- package/lib/genericToolset.js.bak_syntax +0 -402
- package/scenarios.bak.1774780058/data/eval_node_message.json +0 -9
- package/scenarios.bak.1774780058/data/hist_oa.json +0 -66
- package/scenarios.bak.1774780058/data/o3_response1.json +0 -96
- package/scenarios.bak.1774780058/data/oa_reasoning_parse.json +0 -112
- package/scenarios.bak.1774780058/data/tool_oa.json +0 -96
- package/scenarios.bak.1774780058/data/tool_xai.json +0 -59
- package/scenarios.bak.1774780058/data/tool_xai2.json +0 -40
- package/scenarios.bak.1774780058/data/xai-response-1.json +0 -59
- package/scenarios.bak.1774780058/data/xai-response-2.json +0 -10
- package/scenarios.bak.1774780058/data/xai_reasoning_tools_resp.json +0 -59
- package/scenarios.bak.1774780058/data/xai_search_response.json +0 -58
- package/scenarios.bak.1774780058/environment.js +0 -10
- package/scenarios.bak.1774780058/example.js +0 -17
- package/scenarios.bak.1774780058/genericToolset.test.js +0 -182
- package/scenarios.bak.1774780058/grok.js +0 -113
- package/scenarios.bak.1774780058/memory-tools.js +0 -51
- package/scenarios.bak.1774780058/openai-o3.js +0 -137
- package/scenarios.bak.1774780058/openai-prompt.js +0 -155
- package/scenarios.bak.1774780058/openai-session.js +0 -148
- package/scenarios.bak.1774780058/openai.js +0 -102
- package/scenarios.bak.1774780058/prompt.js +0 -118
- package/scenarios.bak.1774780058/promptFishbowl.js +0 -76
- package/scenarios.bak.1774780058/search.brave.com.js +0 -25
- package/scenarios.bak.1774780058/sh.js +0 -15
- package/scenarios.bak.1774780058/test-wsio.js +0 -26
- package/scenarios.bak.1774780058/testToolset.js +0 -42
- package/scenarios.bak.1774780058/toolset.js +0 -16
- package/scenarios.bak.1774780058/toolset.test.js +0 -141
- package/scenarios.bak.1774780058/write_file_syntax.test.js +0 -145
- package/scenarios.bak.1774780058/write_file_validation/README.md +0 -30
- package/scenarios.bak.1774780058/write_file_validation/bad.js +0 -3
- package/scenarios.bak.1774780058/write_file_validation/good.js +0 -4
- package/scenarios.bak.1774780058/write_file_validation/test.sh +0 -43
- package/scenarios.bak.1774780058/wsClient.js +0 -69
- package/scenarios.bak.1774780058/xai_responses.integration.test.js +0 -57
- package/scenarios.bak.1774780058/xai_responses.test.js +0 -154
- package/scenarios.bak.1774780058/xaicoll.js +0 -50
- package/scenarios.bak.1774780058/xaifiles.js +0 -48
- /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,32 +77,29 @@ ${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).',
|
|
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 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
|
|
117
|
+
'Send email via msmtp.',
|
|
111
118
|
{
|
|
112
119
|
type: 'object',
|
|
113
120
|
properties: {
|
|
114
|
-
to: { type: 'string', description: 'Recipient
|
|
121
|
+
to: { type: 'string', description: 'Recipient' },
|
|
115
122
|
subject: { type: 'string', description: 'Subject' },
|
|
116
|
-
body: { type: 'string', description: '
|
|
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
|
|
141
|
+
'Open URL/file with xdg-open.',
|
|
134
142
|
{
|
|
135
143
|
type: 'object',
|
|
136
144
|
properties: {
|
|
137
|
-
url: { type: 'string', description: 'file
|
|
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
|
-
'
|
|
154
|
+
'Run bash on remote via SSH.',
|
|
148
155
|
{
|
|
149
156
|
type: 'object',
|
|
150
157
|
properties: {
|
|
151
|
-
url: { type: 'string', description: '
|
|
152
|
-
script: { type: 'string', description: '
|
|
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('
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
|
222
|
+
'Write/validate file in CWD. Auto-syntax check + JS fix + chmod.',
|
|
240
223
|
{
|
|
241
224
|
type: 'object',
|
|
242
225
|
properties: {
|
|
243
|
-
file: {
|
|
244
|
-
|
|
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('
|
|
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
|
|
264
|
-
if (!
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
throw new Error(
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
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();
|
|
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
|
|
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 = '';
|
|
@@ -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
|
+
}
|