textweb 0.1.2 → 0.2.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 CHANGED
@@ -48,26 +48,6 @@ textweb --json https://example.com
48
48
 
49
49
  ~500 bytes. An LLM can read this, understand the layout, and say "click ref 9" to open the first link. No vision model needed.
50
50
 
51
- ## LLM Configuration
52
-
53
- TextWeb's job application pipeline uses a local or remote LLM for freeform questions. Configure via `.env` file or environment variables:
54
-
55
- ```bash
56
- cp .env.example .env
57
- # Edit .env with your settings
58
- ```
59
-
60
- | Variable | Default | Description |
61
- |----------|---------|-------------|
62
- | `TEXTWEB_LLM_URL` | `http://localhost:1234/v1` | OpenAI-compatible API endpoint |
63
- | `TEXTWEB_LLM_API_KEY` | *(empty)* | API key (optional for local LLMs) |
64
- | `TEXTWEB_LLM_MODEL` | `google/gemma-3-4b` | Model name |
65
- | `TEXTWEB_LLM_MAX_TOKENS` | `200` | Max response tokens |
66
- | `TEXTWEB_LLM_TEMPERATURE` | `0.7` | Sampling temperature |
67
- | `TEXTWEB_LLM_TIMEOUT` | `60000` | Request timeout (ms) |
68
-
69
- Works with LM Studio, Ollama, OpenAI, or any OpenAI-compatible endpoint. No API key needed for local models.
70
-
71
51
  ## Integration Options
72
52
 
73
53
  TextWeb works with any AI agent framework. Pick your integration:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "textweb",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "A text-grid web renderer for AI agents — see the web without screenshots",
5
5
  "main": "src/browser.js",
6
6
  "bin": {
package/src/browser.js CHANGED
@@ -60,7 +60,7 @@ class AgentBrowser {
60
60
  async click(ref) {
61
61
  const el = this._getElement(ref);
62
62
  await this.page.click(el.selector);
63
- await this.page.waitForLoadState('networkidle').catch(() => {});
63
+ await this._settle();
64
64
  return await this.snapshot();
65
65
  }
66
66
 
@@ -103,7 +103,7 @@ class AgentBrowser {
103
103
 
104
104
  async press(key) {
105
105
  await this.page.keyboard.press(key);
106
- await this.page.waitForLoadState('networkidle').catch(() => {});
106
+ await this._settle();
107
107
  return await this.snapshot();
108
108
  }
109
109
 
@@ -159,6 +159,56 @@ class AgentBrowser {
159
159
  }
160
160
  }
161
161
 
162
+ /**
163
+ * Get the current page URL
164
+ */
165
+ getCurrentUrl() {
166
+ return this.page ? this.page.url() : null;
167
+ }
168
+
169
+ /**
170
+ * Find elements matching a CSS selector
171
+ * Returns array of {tag, text, selector, visible} objects
172
+ */
173
+ async query(selector) {
174
+ if (!this.page) throw new Error('No page open. Call navigate() first.');
175
+ return await this.page.evaluate((sel) => {
176
+ const els = document.querySelectorAll(sel);
177
+ return Array.from(els).map((el, i) => ({
178
+ tag: el.tagName.toLowerCase(),
179
+ text: (el.textContent || '').trim().substring(0, 200),
180
+ selector: `${sel}:nth-child(${i + 1})`,
181
+ visible: el.offsetParent !== null,
182
+ href: el.href || null,
183
+ value: el.value || null,
184
+ }));
185
+ }, selector);
186
+ }
187
+
188
+ /**
189
+ * Take a screenshot (for debugging)
190
+ * @param {object} options - Playwright screenshot options (path, fullPage, type, etc.)
191
+ */
192
+ async screenshot(options = {}) {
193
+ if (!this.page) throw new Error('No page open. Call navigate() first.');
194
+ return await this.page.screenshot({
195
+ fullPage: true,
196
+ type: 'png',
197
+ ...options,
198
+ });
199
+ }
200
+
201
+ /**
202
+ * Wait for page to settle after an interaction.
203
+ * Races networkidle against a short timeout to avoid hanging on SPAs.
204
+ */
205
+ async _settle() {
206
+ await Promise.race([
207
+ this.page.waitForLoadState('networkidle').catch(() => {}),
208
+ new Promise(r => setTimeout(r, 3000)),
209
+ ]);
210
+ }
211
+
162
212
  _getElement(ref) {
163
213
  if (!this.lastResult) throw new Error('No snapshot. Navigate first.');
164
214
  const el = this.lastResult.elements[ref];
package/src/cli.js CHANGED
@@ -17,7 +17,6 @@ function parseArgs() {
17
17
  json: false,
18
18
  serve: false,
19
19
  cols: 100,
20
- rows: 30,
21
20
  port: 3000,
22
21
  help: false
23
22
  };
@@ -48,7 +47,9 @@ function parseArgs() {
48
47
 
49
48
  case '--rows':
50
49
  case '-r':
51
- options.rows = parseInt(args[++i]) || 30;
50
+ // Deprecated: height is dynamic (grows to fit content). Ignored.
51
+ console.error('Warning: --rows is deprecated. Height is dynamic (grows to fit content).');
52
+ args[++i]; // consume the value
52
53
  break;
53
54
 
54
55
  case '--port':
@@ -85,7 +86,7 @@ USAGE:
85
86
 
86
87
  OPTIONS:
87
88
  --cols, -c <number> Grid width in characters (default: 100)
88
- --rows, -r <number> Grid height in characters (default: 30)
89
+ --rows, -r <number> (deprecated, height is dynamic)
89
90
  --port, -p <number> Server port (default: 3000)
90
91
  --interactive, -i Interactive REPL mode
91
92
  --json, -j JSON output format
@@ -117,7 +118,7 @@ INTERACTIVE COMMANDS:
117
118
  async function render(url, options) {
118
119
  const browser = new AgentBrowser({
119
120
  cols: options.cols,
120
- rows: options.rows,
121
+
121
122
  headless: true
122
123
  });
123
124
 
@@ -156,7 +157,7 @@ async function render(url, options) {
156
157
  async function interactive(url, options) {
157
158
  const browser = new AgentBrowser({
158
159
  cols: options.cols,
159
- rows: options.rows,
160
+
160
161
  headless: true
161
162
  });
162
163
 
@@ -367,7 +368,7 @@ async function serve(options) {
367
368
 
368
369
  const server = createServer({
369
370
  cols: options.cols,
370
- rows: options.rows
371
+
371
372
  });
372
373
 
373
374
  server.listen(options.port, () => {
package/src/renderer.js CHANGED
@@ -7,8 +7,8 @@
7
7
  * Key design decisions:
8
8
  * - Overflow > truncation (never lose information)
9
9
  * - Measure actual font metrics from the page
10
- * - Table-aware layout
11
- * - Z-index compositing (back to front)
10
+ * - Row-grouping layout (elements grouped by Y position)
11
+ * - Dynamic height (grows to fit all content)
12
12
  */
13
13
 
14
14
  /**
@@ -277,44 +277,6 @@ async function extractElements(page) {
277
277
  });
278
278
  }
279
279
 
280
- /**
281
- * Place text onto the grid, allowing overflow (never truncate).
282
- * Text wraps to the next line at grid edge, continuing at the same start column.
283
- */
284
- function placeText(grid, zGrid, z, row, col, text, cols, rows) {
285
- let r = row;
286
- let c = col;
287
-
288
- for (let i = 0; i < text.length; i++) {
289
- // Grow grid vertically if needed (overflow — don't lose data)
290
- while (r >= grid.length) {
291
- grid.push(Array(cols).fill(' '));
292
- zGrid.push(Array(cols).fill(-1));
293
- }
294
-
295
- if (c >= cols) {
296
- // Wrap to next line at original column position
297
- r++;
298
- c = col;
299
- while (r >= grid.length) {
300
- grid.push(Array(cols).fill(' '));
301
- zGrid.push(Array(cols).fill(-1));
302
- }
303
- }
304
-
305
- const ch = text[i];
306
- if (ch === '\n') { r++; c = col; continue; }
307
-
308
- if (c >= 0 && c < cols && z >= zGrid[r][c]) {
309
- grid[r][c] = ch;
310
- zGrid[r][c] = z;
311
- }
312
- c++;
313
- }
314
-
315
- return r; // Return last row written to (useful for tracking grid growth)
316
- }
317
-
318
280
  /**
319
281
  * Detect row boundaries — groups of elements that share the same Y position
320
282
  * This prevents text from different elements on the same visual line from overlapping
package/src/server.js CHANGED
@@ -10,7 +10,7 @@ class TextWebServer {
10
10
  constructor(options = {}) {
11
11
  this.options = {
12
12
  cols: options.cols || 100,
13
- rows: options.rows || 30,
13
+ // rows is deprecated — height is dynamic
14
14
  timeout: options.timeout || 30000,
15
15
  ...options
16
16
  };
@@ -33,7 +33,6 @@ class TextWebServer {
33
33
  if (!this.browser) {
34
34
  this.browser = new AgentBrowser({
35
35
  cols: this.options.cols,
36
- rows: this.options.rows,
37
36
  headless: true,
38
37
  timeout: this.options.timeout
39
38
  });
package/.env.example DELETED
@@ -1,25 +0,0 @@
1
- # TextWeb LLM Configuration
2
- # Copy this to .env and fill in your values
3
-
4
- # Base URL for any OpenAI-compatible API
5
- # Default: http://localhost:1234/v1 (LM Studio)
6
- # Examples:
7
- # https://api.openai.com/v1
8
- # https://api.anthropic.com/v1
9
- # http://localhost:11434/v1 (Ollama)
10
- TEXTWEB_LLM_URL=http://localhost:1234/v1
11
-
12
- # API key (optional for local LLMs like LM Studio/Ollama)
13
- TEXTWEB_LLM_API_KEY=
14
-
15
- # Model name
16
- TEXTWEB_LLM_MODEL=google/gemma-3-4b
17
-
18
- # Max tokens for responses (default: 200)
19
- TEXTWEB_LLM_MAX_TOKENS=200
20
-
21
- # Temperature (default: 0.7)
22
- TEXTWEB_LLM_TEMPERATURE=0.7
23
-
24
- # Request timeout in milliseconds (default: 60000)
25
- TEXTWEB_LLM_TIMEOUT=60000