textweb 0.1.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/LICENSE +21 -0
- package/README.md +231 -0
- package/docs/index.html +761 -0
- package/mcp/index.js +275 -0
- package/package.json +34 -0
- package/src/apply.js +565 -0
- package/src/browser.js +134 -0
- package/src/cli.js +427 -0
- package/src/renderer.js +452 -0
- package/src/server.js +504 -0
- package/tools/crewai.py +128 -0
- package/tools/langchain.py +165 -0
- package/tools/system_prompt.md +37 -0
- package/tools/tool_definitions.json +154 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TextWeb CLI - Command-line interface for text-grid web rendering
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { AgentBrowser } = require('./browser');
|
|
8
|
+
const { createServer } = require('./server');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
|
|
11
|
+
// Parse command line arguments
|
|
12
|
+
function parseArgs() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const options = {
|
|
15
|
+
url: null,
|
|
16
|
+
interactive: false,
|
|
17
|
+
json: false,
|
|
18
|
+
serve: false,
|
|
19
|
+
cols: 100,
|
|
20
|
+
rows: 30,
|
|
21
|
+
port: 3000,
|
|
22
|
+
help: false
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
|
|
28
|
+
switch (arg) {
|
|
29
|
+
case '--interactive':
|
|
30
|
+
case '-i':
|
|
31
|
+
options.interactive = true;
|
|
32
|
+
break;
|
|
33
|
+
|
|
34
|
+
case '--json':
|
|
35
|
+
case '-j':
|
|
36
|
+
options.json = true;
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
case '--serve':
|
|
40
|
+
case '-s':
|
|
41
|
+
options.serve = true;
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case '--cols':
|
|
45
|
+
case '-c':
|
|
46
|
+
options.cols = parseInt(args[++i]) || 100;
|
|
47
|
+
break;
|
|
48
|
+
|
|
49
|
+
case '--rows':
|
|
50
|
+
case '-r':
|
|
51
|
+
options.rows = parseInt(args[++i]) || 30;
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case '--port':
|
|
55
|
+
case '-p':
|
|
56
|
+
options.port = parseInt(args[++i]) || 3000;
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case '--help':
|
|
60
|
+
case '-h':
|
|
61
|
+
options.help = true;
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
default:
|
|
65
|
+
if (!arg.startsWith('-') && !options.url) {
|
|
66
|
+
options.url = arg;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return options;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Show help message
|
|
76
|
+
function showHelp() {
|
|
77
|
+
console.log(`
|
|
78
|
+
TextWeb - Text-grid web renderer for AI agents
|
|
79
|
+
|
|
80
|
+
USAGE:
|
|
81
|
+
textweb <url> Render page and print to console
|
|
82
|
+
textweb --interactive <url> Start interactive REPL mode
|
|
83
|
+
textweb --json <url> Output as JSON (view + elements)
|
|
84
|
+
textweb --serve Start HTTP API server
|
|
85
|
+
|
|
86
|
+
OPTIONS:
|
|
87
|
+
--cols, -c <number> Grid width in characters (default: 100)
|
|
88
|
+
--rows, -r <number> Grid height in characters (default: 30)
|
|
89
|
+
--port, -p <number> Server port (default: 3000)
|
|
90
|
+
--interactive, -i Interactive REPL mode
|
|
91
|
+
--json, -j JSON output format
|
|
92
|
+
--serve, -s Start HTTP server
|
|
93
|
+
--help, -h Show this help message
|
|
94
|
+
|
|
95
|
+
EXAMPLES:
|
|
96
|
+
textweb https://example.com
|
|
97
|
+
textweb --interactive https://github.com
|
|
98
|
+
textweb --json --cols 120 https://news.ycombinator.com
|
|
99
|
+
textweb --serve --port 8080
|
|
100
|
+
|
|
101
|
+
INTERACTIVE COMMANDS:
|
|
102
|
+
click <ref> Click element by reference number
|
|
103
|
+
type <ref> <text> Type text into input element
|
|
104
|
+
scroll <direction> [amount] Scroll (up/down/left/right)
|
|
105
|
+
select <ref> <value> Select dropdown option
|
|
106
|
+
snapshot Re-render current page
|
|
107
|
+
query <selector> Find elements by CSS selector
|
|
108
|
+
region <r1> <c1> <r2> <c2> Read text from grid region
|
|
109
|
+
navigate <url> Navigate to new URL
|
|
110
|
+
screenshot [filename] Take screenshot (for debugging)
|
|
111
|
+
help Show interactive commands
|
|
112
|
+
quit, exit Exit interactive mode
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Main render function
|
|
117
|
+
async function render(url, options) {
|
|
118
|
+
const browser = new AgentBrowser({
|
|
119
|
+
cols: options.cols,
|
|
120
|
+
rows: options.rows,
|
|
121
|
+
headless: true
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
console.error(`Rendering: ${url}`);
|
|
126
|
+
const result = await browser.navigate(url);
|
|
127
|
+
|
|
128
|
+
if (options.json) {
|
|
129
|
+
console.log(JSON.stringify({
|
|
130
|
+
view: result.view,
|
|
131
|
+
elements: result.elements,
|
|
132
|
+
meta: result.meta
|
|
133
|
+
}, null, 2));
|
|
134
|
+
} else {
|
|
135
|
+
console.log(result.view);
|
|
136
|
+
|
|
137
|
+
// Show element references
|
|
138
|
+
const elCount = Object.keys(result.elements || {}).length;
|
|
139
|
+
if (elCount > 0) {
|
|
140
|
+
console.error(`\\nInteractive elements:`);
|
|
141
|
+
for (const [ref, element] of Object.entries(result.elements || {})) {
|
|
142
|
+
console.error(`[${ref}] ${element.tagName}: ${element.textContent || '(no text)'}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error(`Error: ${error.message}`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
} finally {
|
|
151
|
+
await browser.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Interactive REPL mode
|
|
156
|
+
async function interactive(url, options) {
|
|
157
|
+
const browser = new AgentBrowser({
|
|
158
|
+
cols: options.cols,
|
|
159
|
+
rows: options.rows,
|
|
160
|
+
headless: true
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const rl = readline.createInterface({
|
|
164
|
+
input: process.stdin,
|
|
165
|
+
output: process.stdout,
|
|
166
|
+
prompt: 'textweb> '
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
let result = null;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
console.log(`Starting interactive session...`);
|
|
173
|
+
if (url) {
|
|
174
|
+
console.log(`Navigating to: ${url}`);
|
|
175
|
+
result = await browser.navigate(url);
|
|
176
|
+
console.log(result.view);
|
|
177
|
+
console.log(`\\nElements: ${Object.keys(result.elements || {}).length} interactive elements found`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
console.log(`\\nType 'help' for commands, 'quit' to exit`);
|
|
181
|
+
rl.prompt();
|
|
182
|
+
|
|
183
|
+
rl.on('line', async (input) => {
|
|
184
|
+
const parts = input.trim().split(/\\s+/);
|
|
185
|
+
const command = parts[0].toLowerCase();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
switch (command) {
|
|
189
|
+
case 'help':
|
|
190
|
+
console.log(`
|
|
191
|
+
Interactive Commands:
|
|
192
|
+
click <ref> Click element [ref]
|
|
193
|
+
type <ref> <text> Type text into element [ref]
|
|
194
|
+
scroll <dir> [amount] Scroll direction (up/down/left/right)
|
|
195
|
+
select <ref> <value> Select option in dropdown [ref]
|
|
196
|
+
snapshot Re-render current page
|
|
197
|
+
query <selector> Find elements by CSS selector
|
|
198
|
+
region <r1> <c1> <r2> <c2> Read text from grid region
|
|
199
|
+
navigate <url> Navigate to new URL
|
|
200
|
+
screenshot [file] Take screenshot
|
|
201
|
+
elements List all interactive elements
|
|
202
|
+
url Show current URL
|
|
203
|
+
clear Clear screen
|
|
204
|
+
quit, exit Exit
|
|
205
|
+
`);
|
|
206
|
+
break;
|
|
207
|
+
|
|
208
|
+
case 'click':
|
|
209
|
+
if (parts.length < 2) {
|
|
210
|
+
console.log('Usage: click <ref>');
|
|
211
|
+
} else {
|
|
212
|
+
const ref = parseInt(parts[1]);
|
|
213
|
+
result = await browser.click(ref);
|
|
214
|
+
console.log(result.view);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
|
|
218
|
+
case 'type':
|
|
219
|
+
if (parts.length < 3) {
|
|
220
|
+
console.log('Usage: type <ref> <text>');
|
|
221
|
+
} else {
|
|
222
|
+
const ref = parseInt(parts[1]);
|
|
223
|
+
const text = parts.slice(2).join(' ');
|
|
224
|
+
result = await browser.type(ref, text);
|
|
225
|
+
console.log(result.view);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
|
|
229
|
+
case 'upload':
|
|
230
|
+
if (parts.length < 3) {
|
|
231
|
+
console.log('Usage: upload <ref> <filepath> [filepath2 ...]');
|
|
232
|
+
} else {
|
|
233
|
+
const ref = parseInt(parts[1]);
|
|
234
|
+
const files = parts.slice(2);
|
|
235
|
+
result = await browser.upload(ref, files);
|
|
236
|
+
console.log(result.view);
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case 'scroll':
|
|
241
|
+
if (parts.length < 2) {
|
|
242
|
+
console.log('Usage: scroll <direction> [amount]');
|
|
243
|
+
} else {
|
|
244
|
+
const direction = parts[1];
|
|
245
|
+
const amount = parseInt(parts[2]) || 5;
|
|
246
|
+
result = await browser.scroll(direction, amount);
|
|
247
|
+
console.log(result.view);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
|
|
251
|
+
case 'select':
|
|
252
|
+
if (parts.length < 3) {
|
|
253
|
+
console.log('Usage: select <ref> <value>');
|
|
254
|
+
} else {
|
|
255
|
+
const ref = parseInt(parts[1]);
|
|
256
|
+
const value = parts.slice(2).join(' ');
|
|
257
|
+
result = await browser.select(ref, value);
|
|
258
|
+
console.log(result.view);
|
|
259
|
+
}
|
|
260
|
+
break;
|
|
261
|
+
|
|
262
|
+
case 'snapshot':
|
|
263
|
+
result = await browser.snapshot();
|
|
264
|
+
console.log(result.view);
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case 'query':
|
|
268
|
+
if (parts.length < 2) {
|
|
269
|
+
console.log('Usage: query <selector>');
|
|
270
|
+
} else {
|
|
271
|
+
const selector = parts[1];
|
|
272
|
+
const matches = await browser.query(selector);
|
|
273
|
+
console.log(`Found ${matches.length} matches:`);
|
|
274
|
+
matches.forEach(match => {
|
|
275
|
+
console.log(`[${match.ref}] ${match.tagName}: ${match.textContent || '(no text)'}`);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
|
|
280
|
+
case 'region':
|
|
281
|
+
if (parts.length < 5) {
|
|
282
|
+
console.log('Usage: region <r1> <c1> <r2> <c2>');
|
|
283
|
+
} else {
|
|
284
|
+
const r1 = parseInt(parts[1]);
|
|
285
|
+
const c1 = parseInt(parts[2]);
|
|
286
|
+
const r2 = parseInt(parts[3]);
|
|
287
|
+
const c2 = parseInt(parts[4]);
|
|
288
|
+
const text = browser.readRegion(r1, c1, r2, c2);
|
|
289
|
+
console.log(`Region (${r1},${c1}) to (${r2},${c2}):`);
|
|
290
|
+
console.log(text);
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
case 'navigate':
|
|
295
|
+
if (parts.length < 2) {
|
|
296
|
+
console.log('Usage: navigate <url>');
|
|
297
|
+
} else {
|
|
298
|
+
const newUrl = parts[1];
|
|
299
|
+
console.log(`Navigating to: ${newUrl}`);
|
|
300
|
+
result = await browser.navigate(newUrl);
|
|
301
|
+
console.log(result.view);
|
|
302
|
+
}
|
|
303
|
+
break;
|
|
304
|
+
|
|
305
|
+
case 'screenshot':
|
|
306
|
+
const filename = parts[1] || 'screenshot.png';
|
|
307
|
+
await browser.screenshot({ path: filename });
|
|
308
|
+
console.log(`Screenshot saved to: ${filename}`);
|
|
309
|
+
break;
|
|
310
|
+
|
|
311
|
+
case 'elements':
|
|
312
|
+
if (result && Object.keys(result.elements || {}).length > 0) {
|
|
313
|
+
console.log(`Interactive elements (${Object.keys(result.elements || {}).length}):`);
|
|
314
|
+
for (const [ref, element] of Object.entries(result.elements || {})) {
|
|
315
|
+
console.log(`[${ref}] ${element.tagName}: ${element.textContent || '(no text)'}`);
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
console.log('No interactive elements found');
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
|
|
322
|
+
case 'url':
|
|
323
|
+
console.log(`Current URL: ${browser.getCurrentUrl() || 'Not navigated'}`);
|
|
324
|
+
break;
|
|
325
|
+
|
|
326
|
+
case 'clear':
|
|
327
|
+
console.clear();
|
|
328
|
+
break;
|
|
329
|
+
|
|
330
|
+
case 'quit':
|
|
331
|
+
case 'exit':
|
|
332
|
+
console.log('Goodbye!');
|
|
333
|
+
rl.close();
|
|
334
|
+
return;
|
|
335
|
+
|
|
336
|
+
case '':
|
|
337
|
+
// Empty command, just re-prompt
|
|
338
|
+
break;
|
|
339
|
+
|
|
340
|
+
default:
|
|
341
|
+
console.log(`Unknown command: ${command}. Type 'help' for available commands.`);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.error(`Error: ${error.message}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
rl.prompt();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
rl.on('close', async () => {
|
|
352
|
+
console.log('\\nClosing browser...');
|
|
353
|
+
await browser.close();
|
|
354
|
+
process.exit(0);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error(`Error: ${error.message}`);
|
|
359
|
+
await browser.close();
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Start HTTP server
|
|
365
|
+
async function serve(options) {
|
|
366
|
+
console.log(`Starting TextWeb HTTP server on port ${options.port}...`);
|
|
367
|
+
|
|
368
|
+
const server = createServer({
|
|
369
|
+
cols: options.cols,
|
|
370
|
+
rows: options.rows
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
server.listen(options.port, () => {
|
|
374
|
+
console.log(`TextWeb server running at http://localhost:${options.port}`);
|
|
375
|
+
console.log(`\\nAPI Endpoints:`);
|
|
376
|
+
console.log(` POST /navigate - Navigate to URL`);
|
|
377
|
+
console.log(` POST /click - Click element`);
|
|
378
|
+
console.log(` POST /type - Type text`);
|
|
379
|
+
console.log(` POST /scroll - Scroll page`);
|
|
380
|
+
console.log(` POST /select - Select dropdown option`);
|
|
381
|
+
console.log(` GET /snapshot - Get current state`);
|
|
382
|
+
console.log(` GET /health - Health check`);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Main entry point
|
|
387
|
+
async function main() {
|
|
388
|
+
const options = parseArgs();
|
|
389
|
+
|
|
390
|
+
if (options.help || (process.argv.length === 2)) {
|
|
391
|
+
showHelp();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (options.serve) {
|
|
396
|
+
await serve(options);
|
|
397
|
+
} else if (options.interactive) {
|
|
398
|
+
await interactive(options.url, options);
|
|
399
|
+
} else if (options.url) {
|
|
400
|
+
await render(options.url, options);
|
|
401
|
+
} else {
|
|
402
|
+
console.error('Error: No URL provided or server mode selected');
|
|
403
|
+
console.error('Use --help for usage information');
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Handle graceful shutdown
|
|
409
|
+
process.on('SIGINT', () => {
|
|
410
|
+
console.log('\\nShutting down...');
|
|
411
|
+
process.exit(0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
process.on('SIGTERM', () => {
|
|
415
|
+
console.log('\\nShutting down...');
|
|
416
|
+
process.exit(0);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Run CLI
|
|
420
|
+
if (require.main === module) {
|
|
421
|
+
main().catch(error => {
|
|
422
|
+
console.error(`Fatal error: ${error.message}`);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = { parseArgs, render, interactive, serve };
|