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 +5 -3
- package/SKILL.md +7 -5
- package/dist/agents/claude/client.js +28 -2
- package/dist/agents/pi/client.js +28 -2
- package/dist/agents/pi/matchu-patchu.ts +1 -1
- package/dist/index.js +36 -7
- package/dist/render.js +89 -63
- package/dist/servers.js +28 -2
- package/package.json +1 -1
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.
|
|
4
|
+
version: 0.45.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
> Generated from typebulb v0.
|
|
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
|
-
|
|
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
|
package/dist/agents/pi/client.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1450
|
-
a form control. Both reply with a
|
|
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.
|
|
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)});
|