typebulb 0.44.3 → 0.45.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
@@ -42,7 +42,8 @@ typebulb agent:{claude|pi} Open a named harness's mirror in the foreground
42
42
  typebulb call <file> <fn> […] Invoke one server.ts export headlessly: prints its return as JSON to stdout, logs/errors to stderr (needs --trust)
43
43
  typebulb send <file> [msg] Push a message into a running bulb's page (its tb.onMessage handlers); the client-side twin of call, no --trust.
44
44
  With --wait, a handler's non-undefined return prints on stdout (JSON; a bare string raw)
45
- typebulb send <file> tb:snapshot Print the live page's rendered outline (roles, names, visible text)
45
+ typebulb send <file> tb:snapshot Print the live page's rendered outline (roles, names, visible text), headed by a viewport/content fit line
46
+ typebulb send <file> tb:rect … Print a named control's rect ('tb:rect button "Pass"' → {x,y,width,height} + viewport)
46
47
  typebulb send <file> tb:click … Click a control by role+name ('tb:click button "Pass"'); the reply is a fresh snapshot
47
48
  typebulb send <file> tb:set … Set a form control ('tb:set combobox "level" = hard'), firing input+change
48
49
  typebulb get <file> <kind> Print one block's content (data, insight, code, …) to stdout
@@ -266,7 +267,8 @@ That one launch *is* the loop: the server watches the file, so every save recomp
266
267
 
267
268
  - **Structured selftest** — a handler that returns `{ count, verdict }` beats one that logs prose: `typebulb send <file> selftest --wait` prints the object as JSON, and you assert on fields instead of parsing `logs`. At most one handler, in one page, may return a value; a slow check needs `--wait=<ms>` above the 5s default.
268
269
  - **Slow work settles in the handler** — a handler may be async, and the reply waits for its promise: keep a done-promise, `await` it, and return the finished state, with `--wait=<ms>` sized to the work (instead of polling flags in a sleep loop, or snapshot-polling from outside). The sharp edge: settle the promise on *every* exit of the run — success, failure, supersession, in a `finally` — and start idle with an already-resolved one, or the handler hangs and reads as a broken bulb. One case stays two-step: a `tb:*` gesture that kicks off slow work replies with the immediate frame (runtime-answered), so follow it with this settle probe.
269
- - **Rendered truth** — `typebulb send <file> tb:snapshot` prints the page's accessibility outline (roles, names, visible text) without disturbing its state. Use it when logs say ok but the screen might not, and as the first probe on a live page in a state you can't reproduce — a save would hot-reload and destroy it. (`tb:` messages are answered by the runtime, never your handlers, and imply `--wait`.)
270
+ - **Rendered truth** — `typebulb send <file> tb:snapshot` prints the page's accessibility outline (roles, names, visible text) without disturbing its state. Use it when logs say ok but the screen might not, and as the first probe on a live page in a state you can't reproduce — a save would hot-reload and destroy it. (`tb:` messages are answered by the runtime, never your handlers, and imply `--wait`.) Its first line is the page's geometry — viewport, content size, and a fits-or-overflows verdict — so an unwanted scrollbar shows up in the first read.
271
+ - **Measuring layout** — `typebulb send <file> 'tb:rect button "Pass"'` prints that control's viewport-relative rect as JSON (`{x, y, width, height, viewport}`, integers): how big something ended up, whether two things align, whether one is offscreen — arithmetic on rects, no probe handler. Only what the outline names is measurable; give a structural container an `aria-label` to measure it (a one-line edit that also improves the outline).
270
272
  - **Acting on the page** — `typebulb send <file> 'tb:click button "Pass"'` clicks the one control matching that role and name (exact, else a unique case-insensitive substring) and replies with a fresh snapshot; `tb:set combobox "strength" = hard` is the same for form controls (checkboxes and radios take `tb:click`). A disabled, readonly, or covered target is an error naming it — that silence is the bug class these verbs catch. Needs exactly one page open, and the reply is the immediate frame (slow work: follow up with `tb:snapshot`). Only what the outline names is targetable: real `<button>`s and labeled controls, not an `onClick` `<div>`.
271
273
  - **Poking state** — for state beyond what a form control expresses (`tb:set` covers those), author a set-handler up front: a `tb.onMessage` branch that takes a data payload (JSON arrives parsed), applies it to your state — committing the change if your framework needs an explicit step — and returns the new state: `typebulb send <file> '{"set":"speed","value":2}' --wait` prints it. In React, register it in an effect so it closes over the setters (the returned unsubscribe is the cleanup).
272
274
  - **A page must be open** — the CLI runs no browser of its own, so every client-side check waits on a real window (and `--no-open` means there isn't one). `send` says which case it is: nobody has ever connected (share the link), or a page dropped and hasn't returned (it's stale — reload it). Never open a window at the user: the server logs `[page] connected` when a page attaches, so end your turn with the link, arming `typebulb wait <file> --match "[page] connected"` in the background first — the user opening the page is your wake-up.
@@ -283,7 +285,7 @@ The host owns a bulb's **width**; you own its **height**.
283
285
 
284
286
  **Width is the host's.** Standalone, a bulb fills its browser window; in the agent mirror, an embed fits the conversation column by default, with a per-embed *spread* toggle to the full transcript width — and a cap so a tall embed doesn't run away down the transcript. Don't set a width or guess how much room you'll get. `max-width` is the one width worth setting — a readability cap that only declines excess, so it's safe at any granted width. It's also what *spread* runs into: a dense visualization that earns the full transcript width should omit it.
285
287
 
286
- **Height follows your content.** Set a height that adapts — content-driven or viewport-filling — never a fixed pixel value, which neither grows to fill a broken-out window nor shrinks to its content. Prose, a form, a chart flow to their natural height: set none. A full-bleed surface with no natural height of its own gets `height: 100dvh` **and** a pixel floor like `min-height: 420px`. Both are needed — `100dvh` fills its own window if the bulb is broken out, and the floor holds a definite band when embedded. Without the floor a bare `100dvh` collapses to zero embedded, because the mirror sizes an embed to its content height and `100dvh` gives it nothing to measure against.
288
+ **Height follows your content.** Set a height that adapts — content-driven or viewport-filling — never a fixed pixel value, which neither grows to fill a broken-out window nor shrinks to its content. Prose, a form, a chart flow to their natural height: set none. A full-bleed surface with no natural height of its own gets `height: 100dvh` **and** a pixel floor like `min-height: 420px`. Both are needed — `100dvh` fills its own window if the bulb is broken out, and the floor holds a definite band when embedded. Without the floor a bare `100dvh` collapses to zero embedded, because the mirror sizes an embed to its content height and `100dvh` gives it nothing to measure against. Chrome-plus-panel layouts (a header and controls above a board that should take the rest, no scrollbar) are the same case composed: make the `100dvh` element a flex column and give the panel `flex: 1; min-height: 0` — the remainder is sized by containment, never by measuring.
287
289
 
288
290
  **When embedded, keep vertical space on the root in `padding`, not `margin`.** The mirror measures an embed by `document.body.scrollHeight`, and the runtime makes `body` a block formatting context so a root child's vertical margin (yours, or a UA default like `<h1>`'s) is contained rather than escaping the measurement — so you no longer have to get this exactly right. It's still cleaner to keep the horizontal `auto` for centering and move the vertical space to padding:
289
291
 
package/SKILL.md CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: typebulb
3
3
  description: "Author and run Typebulb bulbs — single-file markdown apps (TypeScript/TSX) that run locally via `npx typebulb` (full power: filesystem, database, `server.ts`, `tb.ai`) or render live inline in your coding agent's session through Typebulb's agent mirror (embedded, client-only). A bulb can be a visual widget (chart, simulation, diagram, calculator, UI), a full-stack tool with a Node backend, or an AI app that calls models at runtime. Covers the bulb format, the `tb.*` API, trust, and the local run/embed workflow. Use when the user wants a bulb, a quick local tool (visual, backend-backed, or AI-powered), or something visual rendered inline in the conversation."
4
- version: 0.44.3
4
+ version: 0.45.0
5
5
  ---
6
6
 
7
- > Generated from typebulb v0.44.3. `npx typebulb agent` prints the running version alongside the path to its packaged SKILL.md: if that version is newer than this one, replace this file with that one.
7
+ > Generated from typebulb v0.45.0. `npx typebulb agent` prints the running version alongside the path to its packaged SKILL.md: if that version is newer than this one, replace this file with that one.
8
8
 
9
9
  # typebulb
10
10
 
@@ -50,7 +50,8 @@ typebulb agent:{claude|pi} Open a named harness's mirror in the foreground
50
50
  typebulb call <file> <fn> […] Invoke one server.ts export headlessly: prints its return as JSON to stdout, logs/errors to stderr (needs --trust)
51
51
  typebulb send <file> [msg] Push a message into a running bulb's page (its tb.onMessage handlers); the client-side twin of call, no --trust.
52
52
  With --wait, a handler's non-undefined return prints on stdout (JSON; a bare string raw)
53
- typebulb send <file> tb:snapshot Print the live page's rendered outline (roles, names, visible text)
53
+ typebulb send <file> tb:snapshot Print the live page's rendered outline (roles, names, visible text), headed by a viewport/content fit line
54
+ typebulb send <file> tb:rect … Print a named control's rect ('tb:rect button "Pass"' → {x,y,width,height} + viewport)
54
55
  typebulb send <file> tb:click … Click a control by role+name ('tb:click button "Pass"'); the reply is a fresh snapshot
55
56
  typebulb send <file> tb:set … Set a form control ('tb:set combobox "level" = hard'), firing input+change
56
57
  typebulb get <file> <kind> Print one block's content (data, insight, code, …) to stdout
@@ -274,7 +275,8 @@ That one launch *is* the loop: the server watches the file, so every save recomp
274
275
 
275
276
  - **Structured selftest** — a handler that returns `{ count, verdict }` beats one that logs prose: `typebulb send <file> selftest --wait` prints the object as JSON, and you assert on fields instead of parsing `logs`. At most one handler, in one page, may return a value; a slow check needs `--wait=<ms>` above the 5s default.
276
277
  - **Slow work settles in the handler** — a handler may be async, and the reply waits for its promise: keep a done-promise, `await` it, and return the finished state, with `--wait=<ms>` sized to the work (instead of polling flags in a sleep loop, or snapshot-polling from outside). The sharp edge: settle the promise on *every* exit of the run — success, failure, supersession, in a `finally` — and start idle with an already-resolved one, or the handler hangs and reads as a broken bulb. One case stays two-step: a `tb:*` gesture that kicks off slow work replies with the immediate frame (runtime-answered), so follow it with this settle probe.
277
- - **Rendered truth** — `typebulb send <file> tb:snapshot` prints the page's accessibility outline (roles, names, visible text) without disturbing its state. Use it when logs say ok but the screen might not, and as the first probe on a live page in a state you can't reproduce — a save would hot-reload and destroy it. (`tb:` messages are answered by the runtime, never your handlers, and imply `--wait`.)
278
+ - **Rendered truth** — `typebulb send <file> tb:snapshot` prints the page's accessibility outline (roles, names, visible text) without disturbing its state. Use it when logs say ok but the screen might not, and as the first probe on a live page in a state you can't reproduce — a save would hot-reload and destroy it. (`tb:` messages are answered by the runtime, never your handlers, and imply `--wait`.) Its first line is the page's geometry — viewport, content size, and a fits-or-overflows verdict — so an unwanted scrollbar shows up in the first read.
279
+ - **Measuring layout** — `typebulb send <file> 'tb:rect button "Pass"'` prints that control's viewport-relative rect as JSON (`{x, y, width, height, viewport}`, integers): how big something ended up, whether two things align, whether one is offscreen — arithmetic on rects, no probe handler. Only what the outline names is measurable; give a structural container an `aria-label` to measure it (a one-line edit that also improves the outline).
278
280
  - **Acting on the page** — `typebulb send <file> 'tb:click button "Pass"'` clicks the one control matching that role and name (exact, else a unique case-insensitive substring) and replies with a fresh snapshot; `tb:set combobox "strength" = hard` is the same for form controls (checkboxes and radios take `tb:click`). A disabled, readonly, or covered target is an error naming it — that silence is the bug class these verbs catch. Needs exactly one page open, and the reply is the immediate frame (slow work: follow up with `tb:snapshot`). Only what the outline names is targetable: real `<button>`s and labeled controls, not an `onClick` `<div>`.
279
281
  - **Poking state** — for state beyond what a form control expresses (`tb:set` covers those), author a set-handler up front: a `tb.onMessage` branch that takes a data payload (JSON arrives parsed), applies it to your state — committing the change if your framework needs an explicit step — and returns the new state: `typebulb send <file> '{"set":"speed","value":2}' --wait` prints it. In React, register it in an effect so it closes over the setters (the returned unsubscribe is the cleanup).
280
282
  - **A page must be open** — the CLI runs no browser of its own, so every client-side check waits on a real window (and `--no-open` means there isn't one). `send` says which case it is: nobody has ever connected (share the link), or a page dropped and hasn't returned (it's stale — reload it). Never open a window at the user: the server logs `[page] connected` when a page attaches, so end your turn with the link, arming `typebulb wait <file> --match "[page] connected"` in the background first — the user opening the page is your wake-up.
@@ -291,7 +293,7 @@ The host owns a bulb's **width**; you own its **height**.
291
293
 
292
294
  **Width is the host's.** Standalone, a bulb fills its browser window; in the agent mirror, an embed fits the conversation column by default, with a per-embed *spread* toggle to the full transcript width — and a cap so a tall embed doesn't run away down the transcript. Don't set a width or guess how much room you'll get. `max-width` is the one width worth setting — a readability cap that only declines excess, so it's safe at any granted width. It's also what *spread* runs into: a dense visualization that earns the full transcript width should omit it.
293
295
 
294
- **Height follows your content.** Set a height that adapts — content-driven or viewport-filling — never a fixed pixel value, which neither grows to fill a broken-out window nor shrinks to its content. Prose, a form, a chart flow to their natural height: set none. A full-bleed surface with no natural height of its own gets `height: 100dvh` **and** a pixel floor like `min-height: 420px`. Both are needed — `100dvh` fills its own window if the bulb is broken out, and the floor holds a definite band when embedded. Without the floor a bare `100dvh` collapses to zero embedded, because the mirror sizes an embed to its content height and `100dvh` gives it nothing to measure against.
296
+ **Height follows your content.** Set a height that adapts — content-driven or viewport-filling — never a fixed pixel value, which neither grows to fill a broken-out window nor shrinks to its content. Prose, a form, a chart flow to their natural height: set none. A full-bleed surface with no natural height of its own gets `height: 100dvh` **and** a pixel floor like `min-height: 420px`. Both are needed — `100dvh` fills its own window if the bulb is broken out, and the floor holds a definite band when embedded. Without the floor a bare `100dvh` collapses to zero embedded, because the mirror sizes an embed to its content height and `100dvh` gives it nothing to measure against. Chrome-plus-panel layouts (a header and controls above a board that should take the rest, no scrollbar) are the same case composed: make the `100dvh` element a flex column and give the panel `flex: 1; min-height: 0` — the remainder is sized by containment, never by measuring.
295
297
 
296
298
  **When embedded, keep vertical space on the root in `padding`, not `margin`.** The mirror measures an embed by `document.body.scrollHeight`, and the runtime makes `body` a block formatting context so a root child's vertical margin (yours, or a UA default like `<h1>`'s) is contained rather than escaping the measurement — so you no longer have to get this exactly right. It's still cleaner to keep the horizontal `auto` for centering and move the vertical space to padding:
297
299
 
@@ -1401,9 +1401,20 @@ const something = require('module-name') // NOT SUPPORTED!
1401
1401
  if (lines.length > MAX_LINES) { lines.length = MAX_LINES; lines.push('- \u2026 (truncated)'); }
1402
1402
  return { lines, targets };
1403
1403
  };
1404
+ // The scrolling element's client box is the usable area minus scrollbars \u2014 the only basis on
1405
+ // which "content \u2264 viewport" is the same verdict the scrollbar's presence gives the user.
1406
+ const pageGeometry = () => {
1407
+ const de = document.scrollingElement || document.documentElement;
1408
+ const over = [];
1409
+ if (de.scrollHeight > de.clientHeight) over.push('vertically by ' + (de.scrollHeight - de.clientHeight) + 'px');
1410
+ if (de.scrollWidth > de.clientWidth) over.push('horizontally by ' + (de.scrollWidth - de.clientWidth) + 'px');
1411
+ return '- page: viewport ' + de.clientWidth + '\xD7' + de.clientHeight
1412
+ + ', content ' + de.scrollWidth + '\xD7' + de.scrollHeight
1413
+ + ' \u2014 ' + (over.length ? 'overflows ' + over.join(', ') : 'fits');
1414
+ };
1404
1415
  const snapshot = () => {
1405
1416
  const { lines } = collectOutline();
1406
- return lines.length ? lines.join('\\n') : '(empty page)';
1417
+ return pageGeometry() + '\\n' + (lines.length ? lines.join('\\n') : '(empty page)');
1407
1418
  };
1408
1419
 
1409
1420
  // tb:click / tb:set (TB-Interrogation-Actuation.md). A target is role + name exactly as the
@@ -1475,6 +1486,20 @@ const something = require('module-name') // NOT SUPPORTED!
1475
1486
  }
1476
1487
  return postFrameSnapshot();
1477
1488
  };
1489
+ // tb:rect \u2014 a read in the actuation grammar: the named control's viewport-relative rect plus the
1490
+ // viewport itself, so the reply is self-interpreting. No scrollIntoView \u2014 a read disturbs nothing.
1491
+ const readRect = (rest) => {
1492
+ const t = parseActuation('tb:rect <role> "<name>"', rest);
1493
+ if (t.rest) throw new Error('usage: tb:rect <role> "<name>"');
1494
+ const el = resolveTarget(t.role, t.name);
1495
+ const r = el.getBoundingClientRect();
1496
+ const de = document.scrollingElement || document.documentElement;
1497
+ return {
1498
+ x: Math.round(r.x), y: Math.round(r.y),
1499
+ width: Math.round(r.width), height: Math.round(r.height),
1500
+ viewport: { width: de.clientWidth, height: de.clientHeight }
1501
+ };
1502
+ };
1478
1503
  const actSet = (rest) => {
1479
1504
  const t = parseActuation('tb:set <role> "<name>" = <value>', rest);
1480
1505
  const vm = /^=\\s*([\\s\\S]*)$/.exec(t.rest.trim());
@@ -1531,7 +1556,8 @@ ${vCt}
1531
1556
  if (p === 'tb:snapshot') return snapshot();
1532
1557
  if (p === 'tb:click' || p.indexOf('tb:click ') === 0) return actClick(p.slice(9));
1533
1558
  if (p === 'tb:set' || p.indexOf('tb:set ') === 0) return actSet(p.slice(7));
1534
- throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:click, tb:set)');
1559
+ if (p === 'tb:rect' || p.indexOf('tb:rect ') === 0) return readRect(p.slice(8));
1560
+ throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:rect, tb:click, tb:set)');
1535
1561
  };
1536
1562
  const value = parseMsg(env.payload);
1537
1563
  const calls = typeof env.payload === 'string' && env.payload.indexOf('tb:') === 0
@@ -1401,9 +1401,20 @@ const something = require('module-name') // NOT SUPPORTED!
1401
1401
  if (lines.length > MAX_LINES) { lines.length = MAX_LINES; lines.push('- \u2026 (truncated)'); }
1402
1402
  return { lines, targets };
1403
1403
  };
1404
+ // The scrolling element's client box is the usable area minus scrollbars \u2014 the only basis on
1405
+ // which "content \u2264 viewport" is the same verdict the scrollbar's presence gives the user.
1406
+ const pageGeometry = () => {
1407
+ const de = document.scrollingElement || document.documentElement;
1408
+ const over = [];
1409
+ if (de.scrollHeight > de.clientHeight) over.push('vertically by ' + (de.scrollHeight - de.clientHeight) + 'px');
1410
+ if (de.scrollWidth > de.clientWidth) over.push('horizontally by ' + (de.scrollWidth - de.clientWidth) + 'px');
1411
+ return '- page: viewport ' + de.clientWidth + '\xD7' + de.clientHeight
1412
+ + ', content ' + de.scrollWidth + '\xD7' + de.scrollHeight
1413
+ + ' \u2014 ' + (over.length ? 'overflows ' + over.join(', ') : 'fits');
1414
+ };
1404
1415
  const snapshot = () => {
1405
1416
  const { lines } = collectOutline();
1406
- return lines.length ? lines.join('\\n') : '(empty page)';
1417
+ return pageGeometry() + '\\n' + (lines.length ? lines.join('\\n') : '(empty page)');
1407
1418
  };
1408
1419
 
1409
1420
  // tb:click / tb:set (TB-Interrogation-Actuation.md). A target is role + name exactly as the
@@ -1475,6 +1486,20 @@ const something = require('module-name') // NOT SUPPORTED!
1475
1486
  }
1476
1487
  return postFrameSnapshot();
1477
1488
  };
1489
+ // tb:rect \u2014 a read in the actuation grammar: the named control's viewport-relative rect plus the
1490
+ // viewport itself, so the reply is self-interpreting. No scrollIntoView \u2014 a read disturbs nothing.
1491
+ const readRect = (rest) => {
1492
+ const t = parseActuation('tb:rect <role> "<name>"', rest);
1493
+ if (t.rest) throw new Error('usage: tb:rect <role> "<name>"');
1494
+ const el = resolveTarget(t.role, t.name);
1495
+ const r = el.getBoundingClientRect();
1496
+ const de = document.scrollingElement || document.documentElement;
1497
+ return {
1498
+ x: Math.round(r.x), y: Math.round(r.y),
1499
+ width: Math.round(r.width), height: Math.round(r.height),
1500
+ viewport: { width: de.clientWidth, height: de.clientHeight }
1501
+ };
1502
+ };
1478
1503
  const actSet = (rest) => {
1479
1504
  const t = parseActuation('tb:set <role> "<name>" = <value>', rest);
1480
1505
  const vm = /^=\\s*([\\s\\S]*)$/.exec(t.rest.trim());
@@ -1531,7 +1556,8 @@ ${mCt}
1531
1556
  if (p === 'tb:snapshot') return snapshot();
1532
1557
  if (p === 'tb:click' || p.indexOf('tb:click ') === 0) return actClick(p.slice(9));
1533
1558
  if (p === 'tb:set' || p.indexOf('tb:set ') === 0) return actSet(p.slice(7));
1534
- throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:click, tb:set)');
1559
+ if (p === 'tb:rect' || p.indexOf('tb:rect ') === 0) return readRect(p.slice(8));
1560
+ throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:rect, tb:click, tb:set)');
1535
1561
  };
1536
1562
  const value = parseMsg(env.payload);
1537
1563
  const calls = typeof env.payload === 'string' && env.payload.indexOf('tb:') === 0
@@ -2440,7 +2440,7 @@ var Patcher = class {
2440
2440
 
2441
2441
  // cli/agents/pi/server/piPatcherExtension.ts
2442
2442
  var DESCRIPTION = true ? "Apply unified diffs to files \u2014 tolerant of form, strict about intent. Repairs sloppy AI-generated diffs (mangled headers, whitespace drift, missing prefixes; line numbers can be wrong or absent \u2014 hunks anchor by fuzzy-matched context lines) and applies all hunks atomically when the intent is unambiguous; fails with a precise, typed error when it isn't. Use for multi-hunk edits in one step, or when exact string-replacement editing fails on whitespace or invisible characters." : "Apply unified diffs to files.";
2443
- var BUILD_TAG = true ? "matchu-patchu-pi 0.3.5, built 2026-07-30 16:00:57" : "matchu-patchu-pi dev";
2443
+ var BUILD_TAG = true ? "matchu-patchu-pi 0.3.5, built 2026-07-30 17:52:54" : "matchu-patchu-pi dev";
2444
2444
  var errorBlocks = (errors) => {
2445
2445
  const parts = [`Patch failed with ${errors.length} error(s):`];
2446
2446
  for (const e of errors) parts.push("", e.toString());
package/dist/index.js CHANGED
@@ -894,9 +894,20 @@ ${gE(t)}`)}var gh=v(()=>{Bi()});var wt,Fn,Nl,qn=v(()=>{wt="https://esm.sh",Fn="h
894
894
  if (lines.length > MAX_LINES) { lines.length = MAX_LINES; lines.push('- \u2026 (truncated)'); }
895
895
  return { lines, targets };
896
896
  };
897
+ // The scrolling element's client box is the usable area minus scrollbars \u2014 the only basis on
898
+ // which "content \u2264 viewport" is the same verdict the scrollbar's presence gives the user.
899
+ const pageGeometry = () => {
900
+ const de = document.scrollingElement || document.documentElement;
901
+ const over = [];
902
+ if (de.scrollHeight > de.clientHeight) over.push('vertically by ' + (de.scrollHeight - de.clientHeight) + 'px');
903
+ if (de.scrollWidth > de.clientWidth) over.push('horizontally by ' + (de.scrollWidth - de.clientWidth) + 'px');
904
+ return '- page: viewport ' + de.clientWidth + '\xD7' + de.clientHeight
905
+ + ', content ' + de.scrollWidth + '\xD7' + de.scrollHeight
906
+ + ' \u2014 ' + (over.length ? 'overflows ' + over.join(', ') : 'fits');
907
+ };
897
908
  const snapshot = () => {
898
909
  const { lines } = collectOutline();
899
- return lines.length ? lines.join('\\n') : '(empty page)';
910
+ return pageGeometry() + '\\n' + (lines.length ? lines.join('\\n') : '(empty page)');
900
911
  };
901
912
 
902
913
  // tb:click / tb:set (TB-Interrogation-Actuation.md). A target is role + name exactly as the
@@ -968,6 +979,20 @@ ${gE(t)}`)}var gh=v(()=>{Bi()});var wt,Fn,Nl,qn=v(()=>{wt="https://esm.sh",Fn="h
968
979
  }
969
980
  return postFrameSnapshot();
970
981
  };
982
+ // tb:rect \u2014 a read in the actuation grammar: the named control's viewport-relative rect plus the
983
+ // viewport itself, so the reply is self-interpreting. No scrollIntoView \u2014 a read disturbs nothing.
984
+ const readRect = (rest) => {
985
+ const t = parseActuation('tb:rect <role> "<name>"', rest);
986
+ if (t.rest) throw new Error('usage: tb:rect <role> "<name>"');
987
+ const el = resolveTarget(t.role, t.name);
988
+ const r = el.getBoundingClientRect();
989
+ const de = document.scrollingElement || document.documentElement;
990
+ return {
991
+ x: Math.round(r.x), y: Math.round(r.y),
992
+ width: Math.round(r.width), height: Math.round(r.height),
993
+ viewport: { width: de.clientWidth, height: de.clientHeight }
994
+ };
995
+ };
971
996
  const actSet = (rest) => {
972
997
  const t = parseActuation('tb:set <role> "<name>" = <value>', rest);
973
998
  const vm = /^=\\s*([\\s\\S]*)$/.exec(t.rest.trim());
@@ -1024,7 +1049,8 @@ ${Gi}
1024
1049
  if (p === 'tb:snapshot') return snapshot();
1025
1050
  if (p === 'tb:click' || p.indexOf('tb:click ') === 0) return actClick(p.slice(9));
1026
1051
  if (p === 'tb:set' || p.indexOf('tb:set ') === 0) return actSet(p.slice(7));
1027
- throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:click, tb:set)');
1052
+ if (p === 'tb:rect' || p.indexOf('tb:rect ') === 0) return readRect(p.slice(8));
1053
+ throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:rect, tb:click, tb:set)');
1028
1054
  };
1029
1055
  const value = parseMsg(env.payload);
1030
1056
  const calls = typeof env.payload === 'string' && env.payload.indexOf('tb:') === 0
@@ -1444,11 +1470,14 @@ Usage:
1444
1470
  With --wait, a handler's non-undefined return
1445
1471
  prints on stdout (JSON; a bare string raw).
1446
1472
  msg 'tb:snapshot' instead prints the page's
1447
- rendered outline (roles, names, visible text).
1473
+ rendered outline (roles, names, visible text),
1474
+ headed by a viewport/content fit line.
1475
+ 'tb:rect <role> "<name>"' prints that control's
1476
+ rect ({x,y,width,height} + viewport).
1448
1477
  'tb:click <role> "<name>"' clicks the control
1449
- that outline names; 'tb:set \u2026 = <value>' fills
1450
- a form control. Both reply with a fresh
1451
- snapshot and need exactly one page open.
1478
+ the outline names; 'tb:set \u2026 = <value>' fills
1479
+ a form control. Both gestures reply with a
1480
+ fresh snapshot and need exactly one page open.
1452
1481
  typebulb get <file> <kind> Print one block's content to stdout (kind: code,
1453
1482
  css, html, data, infer, insight, config, notes).
1454
1483
  No content (absent or empty): exit 2;
@@ -2171,5 +2200,5 @@ ${AC}
2171
2200
  `);let c=0,l=Promise.resolve();Nt({target:r,onChange:()=>{let d=++c;l=l.then(async()=>{if(d===c)try{console.log("Re-running..."),await a()}catch(u){console.error("Error:",u)}})}})}}Ot();Ke();import{Console as RC}from"node:console";Ie();async function rw(r,e,t,n,s){LC();try{let p=kr(await Q(),r)[0];p&&(Ni(p.pid,Je(p.pid).offset),console.error(`note: 'call' boots a fresh server.ts instance; the running server (${p.url}) is not contacted \u2014 state shared with it must live on disk.`))}catch{}let i=Ae(t),{bulb:o,config:a}=await De(r);o.server||ws("This bulb has no **server.ts** block; nothing to call."),Tt(i,r,o.server),s&&await Mr(r,s);let c;try{c=await es(o.server,r,n,a.dependencies,s)}catch(p){ws(p instanceof Error?p.message:String(p))}let l=ca(c,e.fn);if(!l){let p=[...Object.keys(c).filter(h=>typeof c[h]=="function"),...Object.keys(aa)];ws(`Function '${e.fn}' not found. Available: ${p.length?p.join(", "):"(none)"}.`)}let d=await _C(e);if(la(l)){try{for await(let p of l(...d))await tw(JSON.stringify(p,nw)+`
2172
2201
  `)}catch(p){ws(p instanceof Error?p.stack??p.message:String(p))}return ew(0)}let u;try{u=await l(...d)}catch(p){ws(p instanceof Error?p.stack??p.message:String(p))}let f=$C(u);f!==void 0&&await tw(f+`
2173
2202
  `),ew(0)}function ew(r){process.exitCode=r,setTimeout(()=>process.exit(r),2e3).unref?.()}async function _C(r){if(r.hasArgsFlag){let e=r.argsJson??"";return e==="-"&&(e=await MC()),IC(e)}return NC(r.positional)}function NC(r){return r.map(e=>{try{return JSON.parse(e)}catch{return e}})}function IC(r){let e;try{e=JSON.parse(r)}catch(t){throw new Error(`--args must be a JSON array: ${t instanceof Error?t.message:String(t)}`)}if(!Array.isArray(e))throw new Error(`--args must be a JSON array, got ${e===null?"null":typeof e}`);return e}function $C(r){if(r!==void 0)return JSON.stringify(r,nw,2)}function nw(r,e){return typeof e=="bigint"?e.toString():e}function ws(r){process.stderr.write(r+`
2174
- `),process.exit(1)}function LC(){let r=new RC(process.stderr,process.stderr);console.log=r.log.bind(r),console.info=r.info.bind(r),console.debug=r.debug.bind(r),console.dir=r.dir.bind(r)}function tw(r){return new Promise((e,t)=>{process.stdout.write(r,n=>n?t(n):e())})}async function MC(){let r=[];for await(let e of process.stdin)r.push(e);return Buffer.concat(r).toString("utf-8")}var sw="0.44.3";for(let r of[process.stdout,process.stderr])r.on("error",e=>{if(e?.code!=="EPIPE")throw e});function iw(r,e){r||(console.error(`This bulb runs server-side Node code (server.ts), which --trust must authorize:
2203
+ `),process.exit(1)}function LC(){let r=new RC(process.stderr,process.stderr);console.log=r.log.bind(r),console.info=r.info.bind(r),console.debug=r.debug.bind(r),console.dir=r.dir.bind(r)}function tw(r){return new Promise((e,t)=>{process.stdout.write(r,n=>n?t(n):e())})}async function MC(){let r=[];for await(let e of process.stdin)r.push(e);return Buffer.concat(r).toString("utf-8")}var sw="0.45.0";for(let r of[process.stdout,process.stderr])r.on("error",e=>{if(e?.code!=="EPIPE")throw e});function iw(r,e){r||(console.error(`This bulb runs server-side Node code (server.ts), which --trust must authorize:
2175
2204
  ${e}`),process.exit(1))}async function BC(){let r=Zy(process.argv.slice(2));if(r.version&&(console.log(`typebulb ${sw}`),process.exit(0)),r.help&&(eb(),process.exit(0)),Xy(),r.subcommand==="logs"){await Pb(r.file||void 0,{follow:r.follow,clear:r.clear,run:r.run,lines:r.lines});return}if(r.subcommand==="wait"){await Cb(r.file||void 0,{match:r.match,timeoutSec:r.timeoutSec});return}if(r.subcommand==="stop"){r.stopScope?await _b(r.stopScope):await Rb(r.file||void 0);return}if(r.subcommand==="send"){await $b(r.file,r.sendMessage,r.sendWaitMs??0);return}if(r.subcommand==="models"){await Eb(r.mode);return}if(r.subcommand==="slug"){Ab(r.slugName);return}if(r.subcommand==="pull"){await kg(r.file||void 0,{force:r.force,mode:r.mode});return}if(r.subcommand==="push"){await Eg(r.file||void 0,{force:r.force,mode:r.mode});return}if(r.subcommand==="agent"){await(r.agentTarget?Xb(r):xb(sw));return}if(r.subcommand==="trust"||r.subcommand==="untrust"){await vb(r.file||void 0,r.subcommand==="trust");return}let e;if(!r.file||r.file==="."){let c=await eg(process.cwd());c||(console.error("No .bulb.md file found in current directory"),process.exit(1)),e=c}else e=ir.resolve(r.file);if(await ow.access(e).then(()=>!0,()=>!1)||(Vr().includes(r.file)&&(console.error(`To open the ${r.file} agent mirror, run: npx typebulb agent:${r.file}`),process.exit(1)),console.error(`File not found: ${e}`),process.exit(1)),e.endsWith(".bulb.md")||(console.error("File must have .bulb.md extension"),process.exit(1)),r.subcommand==="get"){await Mb(e,r.blockKind);return}if(r.subcommand==="put"){await jb(e,r.blockPairs);return}let n=r.file&&r.file!=="."?r.file:ir.relative(process.cwd(),e)||ir.basename(e),s=`npx typebulb --trust ${n.includes(" ")?`"${n}"`:n}`;if(r.subcommand==="predict"){await wb(e,s);return}let i=!r.noTrust&&Qt(e);i&&!r.trust&&console.error("trust: granted from memory (run `typebulb untrust` to revoke)"),r.trust=r.noTrust?!1:r.trust||i;let o;try{o=await De(e)}catch{}let a;if(r.local){o&&!(r.local.name in(o.config.dependencies??{}))&&(console.error(`--replace: '${r.local.name}' is not a dependency in this bulb's config.json; nothing to replace.`),process.exit(1)),o&&r.subcommand!=="call"&&(!o.bulb.code||r.server)&&console.warn("warning: --replace has no effect in server mode (the override is client-only).");try{a=await bd(r.local)}catch(c){console.error(c instanceof Error?c.message:String(c)),process.exit(1)}console.error(`replace: ${a.name} \u2192 ${ir.relative(process.cwd(),a.dir)||"."}`)}if(r.subcommand==="check"){await bb(e,a);return}if(r.subcommand==="call"){iw(r.trust,s),await rw(e,{fn:r.fn,positional:r.callArgs,argsJson:r.argsJson,hasArgsFlag:r.hasArgsFlag},r.mode,a,r.batch);return}if(o&&o.bulb.server&&(Li(o.bulb)||r.server)){iw(r.trust,s),await Zb(e,r.watch,r.mode,a,r.batch);return}await Yb(e,r,s,a)}BC().catch(r=>{console.error("Error:",r.message),process.exit(1)});