@j-o-r/hello-dave 0.0.6 → 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 (44) hide show
  1. package/CHANGELOG.md +14 -34
  2. package/README.md +240 -0
  3. package/README.md.bak +218 -0
  4. package/{examples → agents}/ask_agent.js +5 -5
  5. package/{examples → agents}/codeserver.sh +14 -14
  6. package/{examples → agents}/daisy_agent.js +5 -5
  7. package/{examples → agents}/docs_agent.js +5 -5
  8. package/{examples → agents}/gpt_agent.js +5 -5
  9. package/{examples → agents}/grok_agent.js +5 -5
  10. package/{examples → agents}/memory_agent.js +5 -5
  11. package/{examples → agents}/npm_agent.js +5 -5
  12. package/{examples → agents}/prompt_agent.js +5 -5
  13. package/agents/spawn_agent.js +137 -0
  14. package/{examples → agents}/test_agent.js +6 -6
  15. package/{examples → agents}/todo_agent.js +5 -5
  16. package/bin/codeDave +58 -0
  17. package/bin/dave.js +3 -5
  18. package/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 -129
  27. package/lib/wsCli.js +50 -19
  28. package/lib/wsIO.js +10 -6
  29. package/package.json +3 -3
  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/bin/spawn_agent.js +0 -293
  42. package/lib/genericToolset.js.bak_syntax +0 -402
  43. /package/{examples → agents}/code_agent.js +0 -0
  44. /package/{examples → agents}/readme_agent.js +0 -0
@@ -1,293 +0,0 @@
1
- #!/usr/bin/env node
2
- import { AgentManager } from '@j-o-r/hello-dave';
3
- import { parseArgs } from '@j-o-r/sh';
4
-
5
- const name = 'spawn_agent';
6
- const api = 'xai';
7
- let secret = '';
8
-
9
- const args = parseArgs();
10
-
11
- let input; // Directcall input (positional ONLY - NO pipe)
12
- if (args._.length === 1 && typeof args._[0] === 'string' && args._[0].trim() !== '') {
13
- input = args._[0].trim();
14
- }
15
-
16
- const help = args['help'] || false;
17
- const connect = args['connect'] ? args['connect'] : undefined;
18
- const serve = args['serve'] ? parseInt(args['serve']) : undefined;
19
-
20
- /** @type {import('lib/API/x.ai/responses.js').XAIOptions} */
21
- const options = { tools: [] };
22
- options.tools.push({
23
- type: 'web_search'
24
- });
25
-
26
- if (args['secret']) {
27
- secret = args['secret'];
28
- }
29
- if (args['model'] || true) {
30
- options.model = args['model'] || 'grok-4-fast-reasoning';
31
- }
32
- if (args['temperature']) {
33
- options.temperature = parseFloat(args['temperature']);
34
- }
35
- if (args['tokens']) {
36
- options.max_output_tokens = parseInt(args['tokens']);
37
- }
38
- if (args['top_p']) {
39
- options.top_p = parseFloat(args['top_p']);
40
- }
41
- const reasoning = true;
42
- if (reasoning) {
43
- options.reasoning = {
44
- effort: 'medium',
45
- summary: 'auto'
46
- }
47
- }
48
- const toolsetMode = 'auto';
49
- const contextWindow = args['context'] ? parseInt(args['context']) : 250000;
50
-
51
- function printHelp() {
52
- console.log(`
53
- '${name} --help' You are looking at it.
54
-
55
- ## USAGE MODES:
56
-
57
- ### 1. Direct Call (One-Shot, Positional ONLY):
58
- ./examples/${name}.js "Create a code agent" [--options]
59
-
60
- ### 2. Interactive CLI (no positional arg):
61
- ./examples/${name}.js [--options]
62
-
63
- ### 3. WS Server (no positional arg):
64
- ./examples/${name}.js --serve 8080 [--secret mysecret] [--options]
65
-
66
- ### 4. WS Client (no positional arg):
67
- ./examples/${name}.js --connect ws://127.0.0.1:8080/ws --secret mysecret [--options]
68
-
69
- ### 5. Hybrid (Server + Client, no positional arg):
70
- ./examples/${name}.js --serve 8081 --connect ws://other:8080/ws [--secret ...] [--options]
71
-
72
- ## SERVER OPTIONS EXPLAINED:
73
- --serve [port]: Starts WebSocket SERVER at ws://127.0.0.1:[port]/ws. Allows other agents (--connect) to connect and use this agent as a remote TOOL (e.g., 'spawn_agent'). Runs indefinitely until Ctrl+C.
74
-
75
- --connect [ws_url]: Connects as CLIENT to remote WS server at [ws_url] (e.g., ws://127.0.0.1:8080/ws). Gains access to remote agent's tools. Interactive CLI available.
76
-
77
- --secret [string]: SHARED AUTH TOKEN (min 3 chars). SERVER rejects clients without matching --secret. CLIENTS must provide server's secret to connect. Use same secret for chains.
78
-
79
- Note: Server/Client/Hybrid IGNORES positional input arg (use CLI modes instead). Hybrid: This agent serves AND uses remote tools.
80
-
81
- ## OPTIONS:
82
- --model [grok-4-fast-reasoning|...] (default: grok-4-fast-reasoning)
83
- --temperature [float] (-2 to +2)
84
- --tokens [number] (max output tokens)
85
- --top_p [float]
86
- --context [number] (default: 250000)
87
-
88
- ## SERVER TOOLS (when no input):
89
- Exposes as 'spawn_agent' tool for chaining.
90
- `);
91
- process.exit()
92
- }
93
-
94
- if (help) {
95
- printHelp();
96
- }
97
-
98
- const tool_call_name = 'spawn_agent';
99
- const tool_call_description = `
100
- Portable creator for CLI/WS agents (bin/<name>.js) + PM2 multi-agent launchers. Validates/tests/deploys. No local docs needed.
101
- `.trim();
102
-
103
- const REPO_URL = 'https://codeberg.org/duin/hello-dave';
104
- const DOCS_BASE = `${REPO_URL}/src/branch/main/docs/`;
105
- const EXAMPLES_URL = `${REPO_URL}/src/branch/main/examples/`;
106
-
107
- const promptParts = [
108
- 'You are AgentCreator, expert in @j-o-r/hello-dave. Create production-ready CLI agents (bin/<name>.js) **portably** anywhere (no local docs needed).',
109
- '',
110
- '**AGENT NAME RULE ⚠️**: name MUST be lowercase a-z0-9_ min 2 chars (/^[a-z_0-9_]{2,}$/). EX: myagent, codeagent, ws_tool.',
111
- '',
112
- '**IMPORTS CRITICAL ⚠️**: **NAMED imports ONLY** - NO default! `import { AgentManager } from \'@j-o-r/hello-dave\';` `import { parseArgs } from \'@j-o-r/sh\';`',
113
- '',
114
- '**PORTABILITY**: Auto-install deps: `@j-o-r/hello-dave @j-o-r/sh`. Absolute imports. NO local file refs (docs/examples).',
115
- '',
116
- '**MANDATORY MEMORY PROTOCOL (For EVERY query/task - NO EXCEPTIONS):**',
117
- 'Connected clients (todo_agent, readme_agent, npm_agent, docs_agent, **memory_agent**) are available as tools. **ALWAYS** use **memory_agent** FIRST and LAST:',
118
- '1. **RECALL PHASE (Step 1 of EVERY response)**:',
119
- ' - Call `memory_agent "Recall relevant memories: tasks, errors, prefs for [keywords from user query]. List recent if empty."`',
120
- ' - Incorporate recalled info (e.g., avoid known errors, resume tasks, apply prefs like temp=0.2).',
121
- '2. **CORE ACTION**:',
122
- ' - Perform coding assistance (tools: execute_bash_script, javascript_interpreter, read_file/write_file, web_search, etc.).',
123
- ' - Delegate to specialists if relevant:',
124
- ' - todo_agent: Tasks/TODO.md',
125
- ' - readme_agent: README.md',
126
- ' - npm_agent: NPM modules/package.json',
127
- ' - docs_agent: Documentation',
128
- ' - memory_agent: Persistent storage',
129
- '3. **WRITE PHASE (Step 3 of EVERY response)**:',
130
- ' - Call `memory_agent "Store updates: [new tasks/errors/prefs/decisions from this interaction]. E.g., \'Task: [next step]\', \'Error: [issue]\', \'Pref: [setting]\'"`',
131
- ' - This ensures persistence across sessions/agents, prevents loops/token burn.',
132
- '',
133
- '**PRIORITIES**:',
134
- '- Safety: CWD-only, no escapes, validate before write.',
135
- '- Efficiency: Use memories to skip repeats (e.g., "Known error: git conflict → skip").',
136
- '- Step-by-step: Clear responses with code blocks.',
137
- 'FOLLOW MEMORY PROTOCOL RIGIDLY - it\'s how multi-agent coordination works!',
138
- '',
139
- '**INITIAL INSPECT** (execute_bash_script):',
140
- '1. `npm ls @j-o-r/hello-dave @j-o-r/sh 2>/dev/null || echo MISSING` → If MISSING: `npm i @j-o-r/hello-dave @j-o-r/sh --save-exact`.',
141
- '2. `mkdir -p bin examples`.',
142
- '3. `cat package.json` (project name). `ls -la bin/`.',
143
- '',
144
- '**TOOL RULES (CRITICAL - NO MIX)**:',
145
- '- **XAI Natives** (pre-setup): `options.tools.push({ type: \'web_search\' });` etc.',
146
- '- **Generics** (post-setup): `agent.addGenericToolcall(\'read_file\');` w/ `toolsetMode: \'auto\'`.',
147
- 'Default: web_search + execute_bash_script/read_file/write_file.',
148
- '',
149
- '**PROCESS (EXACT)**:',
150
- '1. INSPECT & SETUP.',
151
- '2. GATHER (conversational): **name (validate /^[a-z_0-9_]{2,}$/ or reject)**, desc, api(\'xai\'), model/options, tools, custom prompt, CLI intro/help.',
152
- '3. GENERATE & VALIDATE: Use blueprint. Write temp_validate.js, `node --check temp_validate.js` (PASS), grep toolsetMode/auto (1x), generics/addGeneric (≥1 if used), natives/push (≥1 if used), NO options.tools.function.read_file. rm temp.',
153
- '4. REVIEW: Show ```js``` + "✅ node --check | Tools OK". Edit if needed.',
154
- '5. DEPLOY: `write_file ./bin/${name}.js FULL_CODE`, `chmod +x ./bin/${name}.js`.',
155
- '6. **TEST**: `execute_bash_script \'./bin/${name}.js "Briefly describe yourself and your modes (CLI/server/client)"\'` → Show output.',
156
- '7. **SERVER/CLIENT GUIDE**: "Test server: ./bin/${name}.js --serve 8081 (find free port). Client: echo \'query\' | ./bin/${name}.js --connect ws://localhost:8081/ws --secret SECRET".',
157
- '8. **CodeServer MULTI-AGENT**: If requested (e.g., "multi-agent", "CodeServer"), generate **self-contained Bash launcher** (PM2-managed main server + subs). Write to examples/<Name>Server (e.g., examples/CodeServer). Use blueprint below. PM2: Assume installed (`npm i -g pm2`).',
158
- '',
159
- '**AGENT BLUEPRINT** (COPY/REPLACE - Plain backticks ` NO ESCAPES):',
160
- `\`#!/usr/bin/env node
161
- import { AgentManager } from '@j-o-r/hello-dave';
162
- import { parseArgs } from '@j-o-r/sh';
163
-
164
- const name = 'AGENTNAME'; // ← REPLACE w/ lowercase /^[a-z_0-9_]{2,}$/
165
- const api = 'xai';
166
- let secret = '';
167
-
168
- const args = parseArgs();
169
- const help = args['help'] || false;
170
- const connect = args['connect'] ? args['connect'] : undefined;
171
- const serve = args['serve'] ? parseInt(args['serve']) : undefined;
172
-
173
- const options = { tools: [] };
174
-
175
- // NATIVES HERE e.g. options.tools.push({ type: 'web_search' });
176
-
177
- if (args['secret']) { secret = args['secret']; }
178
- if (args['model'] || true) { options.model = args['model'] || 'grok-4-fast-reasoning'; }
179
- if (args['temperature']) { options.temperature = parseFloat(args['temperature']); } else { options.temperature = 0.8; }
180
- options.reasoning = { effort: 'medium', summary: 'auto' };
181
-
182
- const contextWindow = args['context'] ? parseInt(args['context']) : 500000;
183
- const toolsetMode = 'auto';
184
-
185
- function printHelp() {
186
- console.log(\`AGENTNAME --help: AGENTDESC
187
-
188
- Full modes: CLI, WS Server (--serve), WS Client (--connect), Hybrid.
189
-
190
- Repo docs: https://codeberg.org/duin/hello-dave (agent-manager.md, codeserver-pattern.md).
191
-
192
- USAGE:
193
- ./bin/AGENTNAME # Interactive CLI
194
- echo "query" | ./bin/AGENTNAME # One-shot
195
- ./bin/AGENTNAME --serve 8081 # WS Server
196
- ./bin/AGENTNAME --connect ws://host:8081/ws --secret KEY # WS Client
197
- ./bin/AGENTNAME --serve 8081 --connect ws://other:8080/ws # Hybrid
198
-
199
- OPTIONS: --model ... --secret ...
200
-
201
- EXAMPLES:
202
- Chain: Server A --connect B → delegates to B.
203
- Multi-Agent: Generate/use PM2 launcher (CodeServer pattern).\`);
204
- process.exit();
205
- }
206
-
207
- if (help) printHelp();
208
-
209
- const prompt = \`You are AGENTNAME, AGENTDESC.
210
-
211
- CUSTOMPROMPT\`.trim();
212
-
213
- const agent = new AgentManager({ name, secret });
214
- agent.setup({ prompt, api, options, toolsetMode, contextWindow });
215
-
216
- // GENERICS HERE e.g. agent.addGenericToolcall('execute_bash_script');
217
-
218
- const cliIntro = \`🤖 AGENTNAME ready! Modes: CLI/Server/Client/Hybrid.
219
-
220
- CLIINTRO\`.trim();
221
-
222
- const toolName = 'agent_' + name;
223
- const toolDescription = \`AGENTDESC. Supports WS chaining/multi-agent.\`.trim();
224
-
225
- await agent.start(serve, connect, cliIntro, toolName, toolDescription);\``,
226
- '',
227
- '**CODE_SERVER BLUEPRINT** (Bash for multi-agent - Self-contained, like CodeServer pattern):',
228
- `\`#!/bin/bash
229
- # Portable Multi-Agent Launcher (PM2-managed: main server + subs)
230
- # Run: npm i -g pm2 (once)
231
- if [ $# -lt 1 ] || [ $# -gt 2 ]; then
232
- echo "Usage: $0 <PORT> [SECRET] # PORT:1024-65535, SECRET:>=3 chars (def:123)"
233
- exit 1
234
- fi
235
- PORT="$1"
236
- SECRET="\${2:-123}"
237
- if [ \${#SECRET} -lt 3 ]; then echo "SECRET too short"; exit 1; fi
238
- if ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1024 ] || [ "$PORT" -gt 65535 ]; then echo "Bad PORT"; exit 1; fi
239
-
240
- SCRIPT_DIR="\$( cd "\$( dirname "\${BASH_SOURCE[0]}" )" && pwd )"
241
- PROJECT_DIR="\$( cd "\$( dirname "\${SCRIPT_DIR}" )" && pwd )"
242
- FOLDER=\$(basename "$PROJECT_DIR")
243
-
244
- echo "Starting <MainAgent>Server on port \${PORT} in '\$FOLDER' w/ SECRET '\$SECRET'..."
245
-
246
- # Cleanup old PM2
247
- pm2 delete "\${FOLDER}_mainagent_\${PORT}" 2>/dev/null || true
248
- pm2 delete "\${FOLDER}_sub1agent_\${PORT}" 2>/dev/null || true # Add per sub
249
- # ... more subs
250
-
251
- # Main server
252
- pm2 start "\${SCRIPT_DIR}/bin/mainagent.js" --name "\${FOLDER}_mainagent_\${PORT}" -- --serve "\${PORT}" --tools javascript --secret "\${SECRET}"
253
- # Subs as clients
254
- pm2 start "\${SCRIPT_DIR}/bin/sub1agent.js" --name "\${FOLDER}_sub1agent_\${PORT}" -- --connect "ws://127.0.0.1:\${PORT}/ws" --secret "\${SECRET}"
255
- # ... more: pm2 start ... sub2agent.js ...
256
-
257
- echo "Processes: pm2 list | grep '\${FOLDER}_\${PORT}'"
258
- echo "Connect: npx @j-o-r/hello-dave dave --connect ws://127.0.0.1:\${PORT}/ws --secret '\${SECRET}'"\``,
259
- '',
260
- 'Enthusiastic, step-by-step. Match portable style. **ALWAYS validate name regex before generate/deploy.** Current date: 2026. Models: grok-4-fast-reasoning/grok-beta. CodeServer pattern: https://codeberg.org/duin/hello-dave/src/branch/main/examples/CodeServer'
261
- ];
262
-
263
- const prompt = promptParts.join('\\n');
264
-
265
- const agent = new AgentManager({ name, secret });
266
- agent.setup({
267
- prompt,
268
- api,
269
- options,
270
- toolsetMode,
271
- contextWindow
272
- });
273
- const toolset = agent.getToolset();
274
- if (toolset) {
275
- agent.addGenericToolcall('execute_bash_script');
276
- agent.addGenericToolcall('read_file');
277
- agent.addGenericToolcall('write_file');
278
- agent.addGenericToolcall('javascript_interpreter');
279
- }
280
- const cliIntro = `🤖 ${name} (${options.model}) ready! (context: ${contextWindow})
281
-
282
- Portable Agent Creator: Builds CLI/WS agents + CodeServer launchers **anywhere** (auto-deps, online docs).
283
-
284
- Examples:
285
- - "Create code-review agent: name=coderev, desc=Git diff analyzer, tools=read_file execute_bash_script web_search"
286
- - "Generate CodeServer launcher for coderev + tododave + npmdave"`.trim();
287
-
288
- if (input) {
289
- const RES = await agent.directCall(input);
290
- console.log(RES);
291
- } else {
292
- await agent.start(serve, connect, cliIntro, tool_call_name, tool_call_description);
293
- }
@@ -1,402 +0,0 @@
1
- import { SH, bashEscape } from '@j-o-r/sh'
2
- import { ToolSet, env } from './index.js'
3
- import path from 'node:path';
4
- import { promises as fs } from 'node:fs';
5
- import { fileURLToPath } from 'node:url';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- const user = await env();
11
- const environment = `
12
- Name: ${user.name}
13
- System: ${user.system}
14
- City: ${user.city}
15
- Region: ${user.region}
16
- Country: ${user.country}
17
- Timezone: ${user.timezone}
18
- ExternalIp: ${user.external_ip}
19
- `.trim();
20
- const tools = new ToolSet('auto');
21
-
22
- /**
23
- * reduce the error output to essential info only, if possible
24
- * @param {string} errorStr
25
- * @returns {string}
26
- */
27
- const getJSError = (errorStr) => {
28
- let result = '';
29
- const linematch = errorStr.match(/\[eval\]:(\d+)/);
30
- const lineNumber = linematch ? linematch[1] : '';
31
- const match = errorStr.split(/\" \"\[eval\]:\d+/s);
32
- if (match.length > 1) {
33
- // Remove last 10 lines
34
- const res = match[1].split('\n').slice(0, -10).join('\n');
35
- result = `Error: line ${lineNumber}\n${res} `
36
- } else {
37
- result = errorStr;
38
- }
39
- return result;
40
- }
41
-
42
- tools.add(
43
- 'javascript_interpreter',
44
- `Execute ESM ES6 javascript on \`node\`.`,
45
- {
46
- type: 'object',
47
- properties: {
48
- script: {
49
- type: 'string',
50
- description: `ES6 ESM Javascript eval. 'console.log' to capture the response. cwd: ${user.cwd}`,
51
- }
52
- },
53
- required: ['script']
54
- },
55
- async (params) => {
56
- let response = '';
57
- try {
58
- const delim = `JS_STDIN_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,6)}`;
59
- response = await SH`cat <<'${delim}' | node --input-type=module -
60
- ${params.script}
61
- ${delim}
62
- `.run();
63
- } catch (e) {
64
- const errorStr = e.toString();
65
- response = getJSError(errorStr);
66
- }
67
- return response;
68
- }
69
- );
70
- tools.add(
71
- 'get_user_env', // name
72
- 'Get the user location, name and OS environment', // desciption
73
- {
74
- type: 'object',
75
- properties: {
76
- }
77
- },
78
- async (_params) => {
79
- return environment;
80
- }
81
- );
82
-
83
- tools.add(
84
- 'execute_bash_script',
85
- 'Execute a bash script or command. (char escaping not needed)',
86
- {
87
- type: 'object',
88
- properties: {
89
- 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})` }
90
- },
91
- required: ['bash_script']
92
- },
93
- async (params) => {
94
- const delim = `END_SCRIPT_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
95
- return await SH`bash <<'${delim}'
96
- ${params.bash_script}
97
- ${delim}
98
- `.run()
99
- }
100
- );
101
-
102
- tools.add(
103
- 'send_email',
104
- 'Send an email.',
105
- {
106
- type: 'object',
107
- properties: {
108
- to: { type: 'string', description: 'Recipient email' },
109
- subject: { type: 'string', description: 'Subject' },
110
- body: { type: 'string', description: 'Message body' }
111
- },
112
- required: ['to', 'subject', 'body']
113
- },
114
- async (params) => {
115
- const delim = `END_EMAIL_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
116
- return await SH`msmtp ${params.to} <<'${delim}'
117
- To: ${params.to}
118
- Subject: ${params.subject}
119
-
120
- ${params.body}
121
- ${delim}
122
- `.run();
123
- }
124
- );
125
- tools.add(
126
- 'open_link',
127
- 'Open an url or file in the local user environment. (xdg-open)',
128
- {
129
- type: 'object',
130
- properties: {
131
- url: { type: 'string', description: 'file | URL' }
132
- },
133
- required: ['url']
134
- },
135
- async (params) => {
136
- return await SH`xdg-open ${[params.url]}`.run();
137
- }
138
- );
139
- tools.add(
140
- 'execute_remote_script',
141
- 'Execute bash script on a remote machine via SSH.',
142
- {
143
- type: 'object',
144
- properties: {
145
- url: { type: 'string', description: 'SSH URL, e.g., ssh://user@host or ssh://user@host:port' },
146
- script: { type: 'string', description: 'RAW script code to execute remotely (no escaping needed)' }
147
- },
148
- required: ['url', 'script']
149
- },
150
- async (params) => {
151
- const { url, script } = params;
152
- if (!url.startsWith('ssh://')) throw new Error('Invalid SSH URL');
153
- const withoutProto = url.slice(6);
154
- const parts = withoutProto.split(':');
155
- let port = 22;
156
- let userHost = withoutProto;
157
- if (parts.length > 1) {
158
- userHost = parts[0];
159
- port = parseInt(parts[1]);
160
- }
161
- const [user, host] = userHost.split('@');
162
- if (!user || !host) throw new Error('Invalid SSH URL format: use ssh://user@host[:port]');
163
-
164
- const delim = `END_SCRIPT_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
165
- return await SH`ssh -p ${port} ${user}@${host} bash <<'${delim}'
166
- ${script}
167
- ${delim}
168
- `.run();
169
-
170
- }
171
- );
172
- tools.add(
173
- 'history_search',
174
- `Search previous LLM chat sessions or list them hierarchically.
175
- Example query: "(todo|task)" or "package.json".
176
- Searches filenames & content in .cache/[app]/[prompt]/sessions/*.ndjson (case-insensitive regex, with context).
177
- Omit query (or use empty string) to list sessions (see utils/list_sessions.sh).`,
178
- {
179
- type: 'object',
180
- properties: {
181
- query: {
182
- type: 'string',
183
- description: `Search query or regex (quoted for multi-word/regex). Omit or empty for list mode.`
184
- }
185
- },
186
- required: []
187
- },
188
- async (params) => {
189
- const history_search = path.resolve(__dirname, '..', 'utils', 'search_sessions.sh');
190
- const list_sessions = path.resolve(__dirname, '..', 'utils', 'list_sessions.sh');
191
- if (typeof params.query === 'string' && params.query.trim() !== '') {
192
- const escapedQuery = bashEscape(params.query);
193
- return await SH`${history_search} "${escapedQuery}"`.run();
194
- } else {
195
- return await SH`${list_sessions}`.run();
196
- }
197
- }
198
- );
199
- tools.add(
200
- 'read_file',
201
- 'Read the raw content of a file strictly within the current working directory (CWD). Paths must be relative (no leading /, no ..).',
202
- {
203
- type: 'object',
204
- properties: {
205
- file: {
206
- type: 'string',
207
- description: `Relative path to the file within CWD, e.g., 'path/to/file.txt' (no escaping needed). cwd: ${user.cwd}`
208
- }
209
- },
210
- required: ['file']
211
- },
212
- async (params) => {
213
- const file = params.file?.trim();
214
- if (typeof file !== 'string' || !file) {
215
- throw new Error('Valid relative file path required.');
216
- }
217
- if (file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
218
- throw new Error('Path must be relative within CWD only (no `/`, `..`, or `\\\\`).');
219
- }
220
- const resolvedPath = path.resolve(process.cwd(), file);
221
- if (!resolvedPath.startsWith(process.cwd())) {
222
- throw new Error(`Path '${file}' escapes CWD scope.`);
223
- }
224
- try {
225
- const content = await fs.readFile(resolvedPath, 'utf8');
226
- return content;
227
- } catch (e) {
228
- throw new Error(`Failed to read '${file}': ${e.message}`);
229
- }
230
- }
231
- );
232
-
233
- tools.add(
234
- 'write_file',
235
- '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.',
236
- {
237
- type: 'object',
238
- properties: {
239
- file: {
240
- type: 'string',
241
- description: `Relative path to the file within CWD, e.g., 'path/to/file.txt' (no escaping needed).`
242
- },
243
- content: {
244
- type: 'string',
245
- description: `Raw content to write (as-is, no char escaping needed; supports newlines, $, |, <, >, &, ", ', \\, etc.).`
246
- }
247
- },
248
- required: ['file', 'content']
249
- },
250
- async (params) => {
251
- const file = params.file?.trim();
252
- const content = params.content ?? '';
253
- if (typeof file !== 'string' || !file) {
254
- throw new Error('Valid relative file path required.');
255
- }
256
- if (file.startsWith('/') || file.includes('..') || file.includes('\\\\')) {
257
- throw new Error('Path must be relative within CWD only (no `/`, `..`, or `\\\\`).');
258
- }
259
- const resolvedPath = path.resolve(process.cwd(), file);
260
- if (!resolvedPath.startsWith(process.cwd())) {
261
- throw new Error(`Path '${file}' escapes CWD scope.`);
262
- }
263
- try {
264
- await fs.writeFile(resolvedPath, content, 'utf8');
265
-
266
- // AUTO-VALIDATE: Multi-lang syntax check via utils/syntax_check.sh + chmod shebang
267
- const syntaxChecker = path.resolve(process.cwd(), 'utils/syntax_check.sh');
268
- const hasShebang = content.startsWith('#!');
269
-
270
- let validationMsg = '';
271
- try {
272
- await SH`${syntaxChecker} ${[resolvedPath]}`.run();
273
- validationMsg = ' ✓ Multi-lang syntax OK';
274
- } catch (e) {
275
- const errPreview = content.slice(0, 1000).split('\n').slice(0, 20).join('\n');
276
- 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.`);
277
- }
278
- if (hasShebang) {
279
- await SH`chmod +x ${[resolvedPath]}`.run();
280
- validationMsg += ' ✓ chmod +x';
281
- }
282
-
283
- return `Successfully wrote to '${file}' (${Buffer.byteLength(content, 'utf8')} bytes).${validationMsg}`;
284
- } catch (e) {
285
- throw new Error(`Failed to write '${file}': ${e.message}`);
286
- }
287
- }
288
- );
289
-
290
- tools.add(
291
- 'syntax_check',
292
- 'Standalone syntax validation for files (JS/Python/Bash/JSON/etc.) via utils/syntax_check.sh. Detects lang from ext/shebang.',
293
- {
294
- type: 'object',
295
- properties: {
296
- file: {
297
- type: 'string',
298
- description: `Relative path to the file within CWD.`
299
- }
300
- },
301
- required: ['file']
302
- },
303
- async (params) => {
304
- const file = params.file?.trim();
305
- if (typeof file !== 'string' || !file) {
306
- throw new Error('Valid relative file path required.');
307
- }
308
- const resolvedPath = path.resolve(process.cwd(), file);
309
- if (!resolvedPath.startsWith(process.cwd())) {
310
- throw new Error(`Path '${file}' escapes CWD scope.`);
311
- }
312
- const syntaxChecker = path.resolve(process.cwd(), 'utils/syntax_check.sh');
313
- return await SH`${syntaxChecker} ${[resolvedPath]}`.run();
314
- }
315
- );
316
-
317
- tools.add(
318
- 'memory_write',
319
- `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.`,
320
- {
321
- type: 'object',
322
- properties: {
323
- category: {
324
- type: 'string',
325
- enum: ['tasks', 'errors', 'prefs'],
326
- description: 'Category: one of "tasks", "errors", "prefs"'
327
- },
328
- content: {
329
- type: 'string',
330
- description: 'Detailed content (e.g., "Pending task: implement runWithMemory optimization", "Error: loop burning tokens", "Pref: temperature=0.2 for decisions")'
331
- }
332
- },
333
- required: ['category', 'content']
334
- },
335
- async ({category, content}) => {
336
- if (!['tasks', 'errors', 'prefs'].includes(category)) {
337
- throw new Error(`Invalid category '${category}'. Must be 'tasks', 'errors', or 'prefs'.`);
338
- }
339
- const memoryPath = path.join(process.cwd(), '.cache', 'memory.ndjson');
340
- const dir = path.dirname(memoryPath);
341
- try {
342
- await fs.mkdir(dir, { recursive: true });
343
- } catch (e) {
344
- // Dir likely exists
345
- }
346
- const entry = {
347
- timestamp: new Date().toISOString(),
348
- category,
349
- content: content.trim()
350
- };
351
- await fs.appendFile(memoryPath, JSON.stringify(entry) + '\n', 'utf8');
352
- return `✓ Memory stored: [${category}] ${content.length > 50 ? content.slice(0, 47) + '...' : content}`;
353
- }
354
- );
355
-
356
- tools.add(
357
- 'memory_recall',
358
- `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.`,
359
- {
360
- type: 'object',
361
- properties: {
362
- query: {
363
- type: 'string',
364
- description: 'Keyword, category (tasks/errors/prefs), or phrase to filter (case-insensitive). Empty/omit lists last 20.'
365
- }
366
- },
367
- required: []
368
- },
369
- async (params = {}) => {
370
- const memoryPath = path.join(process.cwd(), '.cache', 'memory.ndjson');
371
- let content;
372
- try {
373
- content = await fs.readFile(memoryPath, 'utf8');
374
- } catch (e) {
375
- return 'No memories stored yet. Use memory_write first.';
376
- }
377
- const lines = content.trim().split('\n').filter(l => l.trim());
378
- const memories = [];
379
- for (const line of lines) {
380
- try {
381
- memories.push(JSON.parse(line));
382
- } catch {
383
- // Skip invalid lines
384
- }
385
- }
386
- if (!params.query || !params.query.toString().trim()) {
387
- const recent = memories.slice(-20).reverse();
388
- return recent.length
389
- ? 'Recent memories:\n' + recent.map(m => `• ${m.timestamp.slice(0, 19).replace('T', ' ')} [${m.category}] ${m.content}`).join('\n')
390
- : 'No memories stored.';
391
- }
392
- const q = params.query.toString().toLowerCase();
393
- const matches = memories.filter(m =>
394
- m.category.toLowerCase().includes(q) || m.content.toLowerCase().includes(q)
395
- );
396
- return matches.length
397
- ? `Matches for "${params.query}":\n` + matches.slice(0, 20).map(m => `• ${m.timestamp.slice(0, 19).replace('T', ' ')} [${m.category}] ${m.content}`).join('\n')
398
- : `No memories match "${params.query}".`;
399
- }
400
- );
401
-
402
- export default tools
File without changes