web-terminal-agent 1.0.0
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/README.md +114 -0
- package/index.js +314 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# web-terminal-agent
|
|
2
|
+
|
|
3
|
+
A lightweight local agent that enables browser-based terminal access to your computer. Use CLI tools like Claude Code through a friendly web interface - no terminal experience required!
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx web-terminal-agent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
That's it! The agent will start and you can connect from the web interface.
|
|
12
|
+
|
|
13
|
+
## What is this?
|
|
14
|
+
|
|
15
|
+
This agent runs on your computer and creates a secure bridge between your browser and your local terminal. It allows web applications to:
|
|
16
|
+
|
|
17
|
+
- Execute shell commands on your machine
|
|
18
|
+
- Run CLI tools like Claude Code, Clawdbot, etc.
|
|
19
|
+
- Navigate your file system
|
|
20
|
+
- Install development tools
|
|
21
|
+
|
|
22
|
+
## Security
|
|
23
|
+
|
|
24
|
+
**Your security is our priority:**
|
|
25
|
+
|
|
26
|
+
- ✅ **Localhost only** - The agent only listens on `127.0.0.1` (your computer). It cannot be accessed from the internet or other devices.
|
|
27
|
+
- ✅ **You're in control** - You start and stop the agent whenever you want. Just press `Ctrl+C` to stop.
|
|
28
|
+
- ✅ **Transparent** - All commands are logged in your terminal so you can see exactly what's happening.
|
|
29
|
+
- ✅ **Open source** - The code is fully visible and auditable.
|
|
30
|
+
|
|
31
|
+
## Installation Options
|
|
32
|
+
|
|
33
|
+
### Option 1: Run without installing (Recommended)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx web-terminal-agent
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Option 2: Install globally
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g web-terminal-agent
|
|
43
|
+
web-terminal-agent
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Option 3: Clone and run
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/user/web-terminal-agent.git
|
|
50
|
+
cd web-terminal-agent
|
|
51
|
+
npm install
|
|
52
|
+
node index.js
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
By default, the agent runs on port `3456`. You can change this:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
PORT=8080 npx web-terminal-agent
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Node.js 18 or higher
|
|
66
|
+
- Works on Windows, macOS, and Linux
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
1. You run the agent on your computer
|
|
71
|
+
2. The agent starts a local server on `http://127.0.0.1:3456`
|
|
72
|
+
3. The web interface connects to this local server
|
|
73
|
+
4. Commands from the web interface are executed on your machine
|
|
74
|
+
5. Output is streamed back to the web interface in real-time
|
|
75
|
+
|
|
76
|
+
## API Endpoints
|
|
77
|
+
|
|
78
|
+
If you want to integrate with the agent programmatically:
|
|
79
|
+
|
|
80
|
+
- `GET /health` - Check if agent is running
|
|
81
|
+
- `GET /cwd` - Get current working directory
|
|
82
|
+
- `POST /execute` - Execute a command (body: `{ "command": "ls -la" }`)
|
|
83
|
+
- `GET /tools/:tool` - Check if a tool is installed
|
|
84
|
+
- `POST /tools/:tool/install` - Install a tool
|
|
85
|
+
- `WebSocket /ws` - Real-time command execution
|
|
86
|
+
|
|
87
|
+
## Troubleshooting
|
|
88
|
+
|
|
89
|
+
### Port already in use
|
|
90
|
+
|
|
91
|
+
If port 3456 is busy, use a different port:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
PORT=3457 npx web-terminal-agent
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Permission denied
|
|
98
|
+
|
|
99
|
+
On macOS/Linux, you might need to make the script executable:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
chmod +x ./node_modules/.bin/web-terminal-agent
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Connection refused in browser
|
|
106
|
+
|
|
107
|
+
Make sure:
|
|
108
|
+
1. The agent is running (you should see the banner in your terminal)
|
|
109
|
+
2. You're connecting to `http://localhost:3456` (not `127.0.0.1` in some browsers)
|
|
110
|
+
3. No firewall is blocking localhost connections
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Web Terminal Agent
|
|
5
|
+
*
|
|
6
|
+
* A lightweight local server that enables browser-based terminal access.
|
|
7
|
+
* Runs on localhost only for security.
|
|
8
|
+
*
|
|
9
|
+
* Usage: npx web-terminal-agent
|
|
10
|
+
* node index.js
|
|
11
|
+
* web-terminal-agent (if installed globally)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const express = require('express');
|
|
15
|
+
const cors = require('cors');
|
|
16
|
+
const { WebSocketServer } = require('ws');
|
|
17
|
+
const { spawn, exec } = require('child_process');
|
|
18
|
+
const http = require('http');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
// Configuration
|
|
23
|
+
const PORT = process.env.PORT || 3456;
|
|
24
|
+
const HOST = '127.0.0.1'; // Only listen on localhost for security
|
|
25
|
+
|
|
26
|
+
// ANSI color codes for terminal output
|
|
27
|
+
const colors = {
|
|
28
|
+
reset: '\x1b[0m',
|
|
29
|
+
green: '\x1b[32m',
|
|
30
|
+
yellow: '\x1b[33m',
|
|
31
|
+
cyan: '\x1b[36m',
|
|
32
|
+
red: '\x1b[31m',
|
|
33
|
+
dim: '\x1b[2m',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Print banner
|
|
37
|
+
function printBanner() {
|
|
38
|
+
console.log(`
|
|
39
|
+
${colors.green}╔═══════════════════════════════════════════════════════════╗
|
|
40
|
+
║ ║
|
|
41
|
+
║ ${colors.cyan}██╗ ██╗███████╗██████╗ ████████╗███████╗██████╗ ███╗ ███╗${colors.green} ║
|
|
42
|
+
║ ${colors.cyan}██║ ██║██╔════╝██╔══██╗ ╚══██╔══╝██╔════╝██╔══██╗████╗ ████║${colors.green} ║
|
|
43
|
+
║ ${colors.cyan}██║ █╗ ██║█████╗ ██████╔╝ ██║ █████╗ ██████╔╝██╔████╔██║${colors.green} ║
|
|
44
|
+
║ ${colors.cyan}██║███╗██║██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██╗██║╚██╔╝██║${colors.green} ║
|
|
45
|
+
║ ${colors.cyan}╚███╔███╔╝███████╗██████╔╝ ██║ ███████╗██║ ██║██║ ╚═╝ ██║${colors.green} ║
|
|
46
|
+
║ ${colors.cyan} ╚══╝╚══╝ ╚══════╝╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝${colors.green} ║
|
|
47
|
+
║ ║
|
|
48
|
+
║ ${colors.yellow}LOCAL AGENT v1.0.0${colors.green} ║
|
|
49
|
+
║ ║
|
|
50
|
+
╚═══════════════════════════════════════════════════════════════╝${colors.reset}
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Get current working directory display
|
|
55
|
+
let currentDir = process.cwd();
|
|
56
|
+
|
|
57
|
+
function getShortDir(dir) {
|
|
58
|
+
const home = os.homedir();
|
|
59
|
+
if (dir.startsWith(home)) {
|
|
60
|
+
return '~' + dir.slice(home.length);
|
|
61
|
+
}
|
|
62
|
+
return dir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Detect platform
|
|
66
|
+
const isWindows = process.platform === 'win32';
|
|
67
|
+
const shell = isWindows ? 'cmd.exe' : (process.env.SHELL || '/bin/bash');
|
|
68
|
+
const shellArgs = isWindows ? ['/c'] : ['-c'];
|
|
69
|
+
|
|
70
|
+
// Check if a command/tool is installed
|
|
71
|
+
async function checkToolInstalled(command) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const checkCmd = isWindows ? 'where' : 'which';
|
|
74
|
+
exec(`${checkCmd} ${command}`, (error) => {
|
|
75
|
+
resolve(!error);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get tool version
|
|
81
|
+
async function getToolVersion(command) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
exec(`${command} --version`, (error, stdout) => {
|
|
84
|
+
if (error) {
|
|
85
|
+
resolve(null);
|
|
86
|
+
} else {
|
|
87
|
+
resolve(stdout.trim().split('\n')[0]);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Install a tool
|
|
94
|
+
async function installTool(tool, ws) {
|
|
95
|
+
const installCommands = {
|
|
96
|
+
'claude-code': 'npm install -g @anthropic-ai/claude-code',
|
|
97
|
+
'clawdbot': 'npm install -g clawdbot',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const cmd = installCommands[tool];
|
|
101
|
+
if (!cmd) {
|
|
102
|
+
return { success: false, error: 'Unknown tool' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const proc = spawn(shell, [...shellArgs, cmd], {
|
|
107
|
+
cwd: currentDir,
|
|
108
|
+
env: process.env,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
proc.stdout.on('data', (data) => {
|
|
112
|
+
if (ws && ws.readyState === 1) {
|
|
113
|
+
ws.send(JSON.stringify({ type: 'output', content: data.toString() }));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
proc.stderr.on('data', (data) => {
|
|
118
|
+
if (ws && ws.readyState === 1) {
|
|
119
|
+
ws.send(JSON.stringify({ type: 'output', content: data.toString() }));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
proc.on('close', (code) => {
|
|
124
|
+
resolve({ success: code === 0 });
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Execute a command
|
|
130
|
+
function executeCommand(command, ws) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
// Handle cd command specially
|
|
133
|
+
if (command.trim().startsWith('cd ')) {
|
|
134
|
+
const newDir = command.trim().slice(3).trim();
|
|
135
|
+
const targetDir = path.resolve(currentDir, newDir.replace(/^~/, os.homedir()));
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
process.chdir(targetDir);
|
|
139
|
+
currentDir = process.cwd();
|
|
140
|
+
|
|
141
|
+
if (ws && ws.readyState === 1) {
|
|
142
|
+
ws.send(JSON.stringify({ type: 'cwd', content: getShortDir(currentDir) }));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
resolve({ success: true, cwd: getShortDir(currentDir) });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (ws && ws.readyState === 1) {
|
|
148
|
+
ws.send(JSON.stringify({ type: 'error', content: `cd: ${err.message}` }));
|
|
149
|
+
}
|
|
150
|
+
resolve({ success: false, error: err.message });
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const proc = spawn(shell, [...shellArgs, command], {
|
|
156
|
+
cwd: currentDir,
|
|
157
|
+
env: { ...process.env, FORCE_COLOR: '1' },
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
let stdout = '';
|
|
161
|
+
let stderr = '';
|
|
162
|
+
|
|
163
|
+
proc.stdout.on('data', (data) => {
|
|
164
|
+
const text = data.toString();
|
|
165
|
+
stdout += text;
|
|
166
|
+
if (ws && ws.readyState === 1) {
|
|
167
|
+
ws.send(JSON.stringify({ type: 'output', content: text }));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
proc.stderr.on('data', (data) => {
|
|
172
|
+
const text = data.toString();
|
|
173
|
+
stderr += text;
|
|
174
|
+
if (ws && ws.readyState === 1) {
|
|
175
|
+
ws.send(JSON.stringify({ type: 'error', content: text }));
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
proc.on('close', (code) => {
|
|
180
|
+
resolve({
|
|
181
|
+
success: code === 0,
|
|
182
|
+
stdout,
|
|
183
|
+
stderr,
|
|
184
|
+
exitCode: code,
|
|
185
|
+
cwd: getShortDir(currentDir),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
proc.on('error', (err) => {
|
|
190
|
+
if (ws && ws.readyState === 1) {
|
|
191
|
+
ws.send(JSON.stringify({ type: 'error', content: err.message }));
|
|
192
|
+
}
|
|
193
|
+
resolve({ success: false, error: err.message });
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Create Express app
|
|
199
|
+
const app = express();
|
|
200
|
+
app.use(cors({
|
|
201
|
+
origin: '*', // Allow all origins since we're on localhost
|
|
202
|
+
methods: ['GET', 'POST'],
|
|
203
|
+
}));
|
|
204
|
+
app.use(express.json());
|
|
205
|
+
|
|
206
|
+
// Health check endpoint
|
|
207
|
+
app.get('/health', (req, res) => {
|
|
208
|
+
res.json({
|
|
209
|
+
status: 'ok',
|
|
210
|
+
version: '1.0.0',
|
|
211
|
+
platform: process.platform,
|
|
212
|
+
cwd: getShortDir(currentDir),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Check tool status
|
|
217
|
+
app.get('/tools/:tool', async (req, res) => {
|
|
218
|
+
const { tool } = req.params;
|
|
219
|
+
const installed = await checkToolInstalled(tool);
|
|
220
|
+
const version = installed ? await getToolVersion(tool) : null;
|
|
221
|
+
|
|
222
|
+
res.json({
|
|
223
|
+
tool,
|
|
224
|
+
installed,
|
|
225
|
+
version,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Install tool
|
|
230
|
+
app.post('/tools/:tool/install', async (req, res) => {
|
|
231
|
+
const { tool } = req.params;
|
|
232
|
+
const result = await installTool(tool, null);
|
|
233
|
+
res.json(result);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Execute command (REST fallback)
|
|
237
|
+
app.post('/execute', async (req, res) => {
|
|
238
|
+
const { command } = req.body;
|
|
239
|
+
|
|
240
|
+
if (!command) {
|
|
241
|
+
return res.status(400).json({ error: 'Command is required' });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(`${colors.dim}[CMD]${colors.reset} ${command}`);
|
|
245
|
+
|
|
246
|
+
const result = await executeCommand(command, null);
|
|
247
|
+
res.json(result);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Get current directory
|
|
251
|
+
app.get('/cwd', (req, res) => {
|
|
252
|
+
res.json({ cwd: getShortDir(currentDir) });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Create HTTP server
|
|
256
|
+
const server = http.createServer(app);
|
|
257
|
+
|
|
258
|
+
// Create WebSocket server
|
|
259
|
+
const wss = new WebSocketServer({ server, path: '/ws' });
|
|
260
|
+
|
|
261
|
+
wss.on('connection', (ws) => {
|
|
262
|
+
console.log(`${colors.green}✓${colors.reset} Client connected via WebSocket`);
|
|
263
|
+
|
|
264
|
+
// Send initial state
|
|
265
|
+
ws.send(JSON.stringify({ type: 'cwd', content: getShortDir(currentDir) }));
|
|
266
|
+
|
|
267
|
+
ws.on('message', async (message) => {
|
|
268
|
+
try {
|
|
269
|
+
const data = JSON.parse(message.toString());
|
|
270
|
+
|
|
271
|
+
if (data.type === 'command') {
|
|
272
|
+
console.log(`${colors.dim}[CMD]${colors.reset} ${data.content}`);
|
|
273
|
+
await executeCommand(data.content, ws);
|
|
274
|
+
} else if (data.type === 'install') {
|
|
275
|
+
console.log(`${colors.dim}[INSTALL]${colors.reset} ${data.tool}`);
|
|
276
|
+
await installTool(data.tool, ws);
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error(`${colors.red}Error:${colors.reset}`, err.message);
|
|
280
|
+
ws.send(JSON.stringify({ type: 'error', content: err.message }));
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
ws.on('close', () => {
|
|
285
|
+
console.log(`${colors.yellow}○${colors.reset} Client disconnected`);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Start server
|
|
290
|
+
server.listen(PORT, HOST, () => {
|
|
291
|
+
printBanner();
|
|
292
|
+
|
|
293
|
+
console.log(`${colors.green}✓${colors.reset} Agent running on ${colors.cyan}http://${HOST}:${PORT}${colors.reset}`);
|
|
294
|
+
console.log(`${colors.green}✓${colors.reset} WebSocket available at ${colors.cyan}ws://${HOST}:${PORT}/ws${colors.reset}`);
|
|
295
|
+
console.log(`${colors.green}✓${colors.reset} Working directory: ${colors.cyan}${getShortDir(currentDir)}${colors.reset}`);
|
|
296
|
+
console.log('');
|
|
297
|
+
console.log(`${colors.dim}Press Ctrl+C to stop the agent${colors.reset}`);
|
|
298
|
+
console.log('');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Handle graceful shutdown
|
|
302
|
+
process.on('SIGINT', () => {
|
|
303
|
+
console.log(`\n${colors.yellow}Shutting down agent...${colors.reset}`);
|
|
304
|
+
server.close(() => {
|
|
305
|
+
console.log(`${colors.green}✓${colors.reset} Agent stopped`);
|
|
306
|
+
process.exit(0);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
process.on('SIGTERM', () => {
|
|
311
|
+
server.close(() => {
|
|
312
|
+
process.exit(0);
|
|
313
|
+
});
|
|
314
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "web-terminal-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Local agent for Web Terminal - enables browser-based terminal access to your computer. Run CLI tools like Claude Code through a friendly web interface.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"web-terminal-agent": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"terminal",
|
|
14
|
+
"web",
|
|
15
|
+
"agent",
|
|
16
|
+
"cli",
|
|
17
|
+
"shell",
|
|
18
|
+
"claude-code",
|
|
19
|
+
"browser-terminal",
|
|
20
|
+
"remote-terminal",
|
|
21
|
+
"web-terminal"
|
|
22
|
+
],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": ""
|
|
28
|
+
},
|
|
29
|
+
"homepage": "",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": ""
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"cors": "^2.8.5",
|
|
35
|
+
"express": "^4.18.2",
|
|
36
|
+
"ws": "^8.14.2"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"index.js",
|
|
43
|
+
"README.md"
|
|
44
|
+
]
|
|
45
|
+
}
|