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/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 };