typebulb 0.57.1 → 0.57.2
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 +2 -2
- package/SKILL.md +4 -4
- package/dist/agents/claude/client.js +18 -1
- package/dist/agents/codex/client.js +18 -1
- package/dist/agents/pi/client.js +18 -1
- package/dist/index.js +277 -261
- package/dist/render.js +18 -1
- package/dist/servers.js +103 -86
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -251,7 +251,7 @@ The agent mirror turns that block into a live, sandboxed app, with a *breakout
|
|
|
251
251
|
|
|
252
252
|
`typebulb wait` turns a background task into a subscription. It blocks until the target server logs a new line (`--match <substr>` filters), prints it, and exits — and since an agent harness re-invokes the agent when a background task finishes, the exit *is* the wake-up (Claude Code and pi; Codex has no background wake — its recipe is the bounded foreground wait above, and this loop doesn't reach it). It resumes where your last `wait` or `call` on that target left off, so an event that lands while you're acting — or before the wait attaches — still fires it immediately; arm order doesn't matter. It parks until the event. Exit `2` means it gave up before any event arrived (re-arm if you still care, or move on); exit `3` means the server died.
|
|
253
253
|
|
|
254
|
-
**The turn-based loop** (a game, an approval flow): a bulb whose `server.ts` does `console.log` on each user action is the event channel. Per turn — act via `typebulb call`, arm `wait <file> --match <tag>` in the background, end your turn; on wake, read state with `typebulb call <file> <getState>` (never parse it from the log line) and repeat. **`call` always boots a fresh `server.ts` instance** — it never attaches to the running bulb's server — so any state shared between the page and your calls must live on disk (load/save it in each export), not in `server.ts` module memory. A bulb's uncaught browser errors land in the same log as `[runtime error] …`, so the wake channel also catches your bulb breaking. A
|
|
254
|
+
**The turn-based loop** (a game, an approval flow): a bulb whose `server.ts` does `console.log` on each user action is the event channel. Per turn — act via `typebulb call`, arm `wait <file> --match <tag>` in the background, end your turn; on wake, read state with `typebulb call <file> <getState>` (never parse it from the log line) and repeat. **`call` always boots a fresh `server.ts` instance** — it never attaches to the running bulb's server — so any state shared between the page and your calls must live on disk (load/save it in each export), not in `server.ts` module memory. A bulb's uncaught browser errors land in the same log as `[runtime error] …`, so the wake channel also catches your bulb breaking. A run whose page closes never completes, so the wait ends itself when that happens (exit 3, the code a stopped server gives) rather than parking on a tag nothing can log. For inline bulbs, the same subscription is `typebulb wait agent` on the mirror — see [Emitting an inline bulb](#emitting-an-inline-bulb).
|
|
255
255
|
|
|
256
256
|
**Keep every loop command argument-stable.** A harness that permission-matches exact command strings prompts the user on *every* event if varying data (a move, a payload) rides the command line. Keep it off: write the args to a fixed file and pipe them — `cat <bulb-folder>/args.json | typebulb call <file> <fn> --args -` — so each of the loop's commands is one constant string, approved once. `send` takes its message the same way (`typebulb send <file> -`), which is also how a large or quote-heavy payload avoids the shell. `wait` and a `getState` call are constant already.
|
|
257
257
|
|
|
@@ -281,7 +281,7 @@ That one launch *is* the loop: the server watches the file, so every save recomp
|
|
|
281
281
|
- **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 a `role` **and** an `aria-label` (`<div role="group" aria-label="board">`) to measure it — a label alone leaves it invisible.
|
|
282
282
|
- **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. The click is synthetic, so a handler needing user activation (a clipboard write, fullscreen) does nothing and reports nothing. Needs exactly one page open (if two are, `typebulb stop <file>` then relaunch is the reset), 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>`.
|
|
283
283
|
- **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).
|
|
284
|
-
- **A page must be open** — the CLI runs no browser of its own, so every client-side check waits on a real window. The CLI opens one where the agent mirror is open (in VS Code beside it, or in the user's browser): at launch, and again from `send
|
|
284
|
+
- **A page must be open** — the CLI runs no browser of its own, so every client-side check waits on a real window. The CLI opens one where the agent mirror is open (in VS Code beside it, or in the user's browser): at launch, and again from any `send` that finds none, which then waits for it to arrive before delivering. So a tab the user closed reopens and the message still lands. Run the bulb, edit, `send`; never relaunch for a page. **A `send` that reached no page exits 1** — believe it, and don't chain a `wait` behind one. With no mirror page open anywhere, nothing can open one: end your turn with the link, arming `typebulb wait <file> --match "[page] connected"` in the background first; the user opening it is your wake-up. Never open a window at the user yourself.
|
|
285
285
|
- **Reading a canvas** — `typebulb send <file> tb:png` writes the page's canvas to a PNG (a stable per-bulb path under `~/.typebulb/`, overwritten each read) and prints the path — rendered truth for a bulb whose output is drawn, with no probe handler and nothing disturbed. One canvas needs no name; several take `tb:png "<name>"` — an `aria-label` on the canvas, or `role="img" aria-label="…"` on the container when a chart library owns the canvas. A WebGL/WebGPU canvas without `preserveDrawingBuffer` reads back blank outside its own frame — capture during the draw instead.
|
|
286
286
|
|
|
287
287
|
### Emitting a server-only bulb
|
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 (sandboxed, 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/inline 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.57.
|
|
4
|
+
version: 0.57.2
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
> Generated from typebulb v0.57.
|
|
7
|
+
> Generated from typebulb v0.57.2. `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
|
|
|
@@ -259,7 +259,7 @@ The agent mirror turns that block into a live, sandboxed app, with a *breakout
|
|
|
259
259
|
|
|
260
260
|
`typebulb wait` turns a background task into a subscription. It blocks until the target server logs a new line (`--match <substr>` filters), prints it, and exits — and since an agent harness re-invokes the agent when a background task finishes, the exit *is* the wake-up (Claude Code and pi; Codex has no background wake — its recipe is the bounded foreground wait above, and this loop doesn't reach it). It resumes where your last `wait` or `call` on that target left off, so an event that lands while you're acting — or before the wait attaches — still fires it immediately; arm order doesn't matter. It parks until the event. Exit `2` means it gave up before any event arrived (re-arm if you still care, or move on); exit `3` means the server died.
|
|
261
261
|
|
|
262
|
-
**The turn-based loop** (a game, an approval flow): a bulb whose `server.ts` does `console.log` on each user action is the event channel. Per turn — act via `typebulb call`, arm `wait <file> --match <tag>` in the background, end your turn; on wake, read state with `typebulb call <file> <getState>` (never parse it from the log line) and repeat. **`call` always boots a fresh `server.ts` instance** — it never attaches to the running bulb's server — so any state shared between the page and your calls must live on disk (load/save it in each export), not in `server.ts` module memory. A bulb's uncaught browser errors land in the same log as `[runtime error] …`, so the wake channel also catches your bulb breaking. A
|
|
262
|
+
**The turn-based loop** (a game, an approval flow): a bulb whose `server.ts` does `console.log` on each user action is the event channel. Per turn — act via `typebulb call`, arm `wait <file> --match <tag>` in the background, end your turn; on wake, read state with `typebulb call <file> <getState>` (never parse it from the log line) and repeat. **`call` always boots a fresh `server.ts` instance** — it never attaches to the running bulb's server — so any state shared between the page and your calls must live on disk (load/save it in each export), not in `server.ts` module memory. A bulb's uncaught browser errors land in the same log as `[runtime error] …`, so the wake channel also catches your bulb breaking. A run whose page closes never completes, so the wait ends itself when that happens (exit 3, the code a stopped server gives) rather than parking on a tag nothing can log. For inline bulbs, the same subscription is `typebulb wait agent` on the mirror — see [Emitting an inline bulb](#emitting-an-inline-bulb).
|
|
263
263
|
|
|
264
264
|
**Keep every loop command argument-stable.** A harness that permission-matches exact command strings prompts the user on *every* event if varying data (a move, a payload) rides the command line. Keep it off: write the args to a fixed file and pipe them — `cat <bulb-folder>/args.json | typebulb call <file> <fn> --args -` — so each of the loop's commands is one constant string, approved once. `send` takes its message the same way (`typebulb send <file> -`), which is also how a large or quote-heavy payload avoids the shell. `wait` and a `getState` call are constant already.
|
|
265
265
|
|
|
@@ -289,7 +289,7 @@ That one launch *is* the loop: the server watches the file, so every save recomp
|
|
|
289
289
|
- **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 a `role` **and** an `aria-label` (`<div role="group" aria-label="board">`) to measure it — a label alone leaves it invisible.
|
|
290
290
|
- **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. The click is synthetic, so a handler needing user activation (a clipboard write, fullscreen) does nothing and reports nothing. Needs exactly one page open (if two are, `typebulb stop <file>` then relaunch is the reset), 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>`.
|
|
291
291
|
- **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).
|
|
292
|
-
- **A page must be open** — the CLI runs no browser of its own, so every client-side check waits on a real window. The CLI opens one where the agent mirror is open (in VS Code beside it, or in the user's browser): at launch, and again from `send
|
|
292
|
+
- **A page must be open** — the CLI runs no browser of its own, so every client-side check waits on a real window. The CLI opens one where the agent mirror is open (in VS Code beside it, or in the user's browser): at launch, and again from any `send` that finds none, which then waits for it to arrive before delivering. So a tab the user closed reopens and the message still lands. Run the bulb, edit, `send`; never relaunch for a page. **A `send` that reached no page exits 1** — believe it, and don't chain a `wait` behind one. With no mirror page open anywhere, nothing can open one: end your turn with the link, arming `typebulb wait <file> --match "[page] connected"` in the background first; the user opening it is your wake-up. Never open a window at the user yourself.
|
|
293
293
|
- **Reading a canvas** — `typebulb send <file> tb:png` writes the page's canvas to a PNG (a stable per-bulb path under `~/.typebulb/`, overwritten each read) and prints the path — rendered truth for a bulb whose output is drawn, with no probe handler and nothing disturbed. One canvas needs no name; several take `tb:png "<name>"` — an `aria-label` on the canvas, or `role="img" aria-label="…"` on the container when a chart library owns the canvas. A WebGL/WebGPU canvas without `preserveDrawingBuffer` reads back blank outside its own frame — capture during the draw instead.
|
|
294
294
|
|
|
295
295
|
### Emitting a server-only bulb
|
|
@@ -1854,7 +1854,20 @@ ${LCt("tbStreamUrl")}
|
|
|
1854
1854
|
} catch (err) { location.replace('about:blank'); }
|
|
1855
1855
|
}, 250);
|
|
1856
1856
|
});
|
|
1857
|
-
|
|
1857
|
+
// The stream attaches during this script's own eval, in <head> \u2014 before the bulb's module (and
|
|
1858
|
+
// its import graph) have run, so before a single \`tb.onMessage\` handler exists. The server now
|
|
1859
|
+
// delivers the instant the stream attaches (it holds a send for the page it opened), so a
|
|
1860
|
+
// message can beat the page's own code by the width of a CDN fetch. A page that is attached but
|
|
1861
|
+
// has not yet run is not a page with no handlers, so hold the message until the deferred module
|
|
1862
|
+
// scripts have executed (DOMContentLoaded waits for them). Page-side and bounded by one load:
|
|
1863
|
+
// the server still never buffers, and a page that is awake handles inline as before.
|
|
1864
|
+
let pageAwake = document.readyState !== 'loading';
|
|
1865
|
+
const beforeAwake = [];
|
|
1866
|
+
if (!pageAwake) document.addEventListener('DOMContentLoaded', () => {
|
|
1867
|
+
pageAwake = true;
|
|
1868
|
+
for (const run of beforeAwake.splice(0)) run();
|
|
1869
|
+
});
|
|
1870
|
+
const onMessageEvent = async (e) => {
|
|
1858
1871
|
// Wire envelope { id?, payload } (JSON \u2014 SSE-line-safe). tb:* payloads are the shim's reserved
|
|
1859
1872
|
// namespace (TB-Interrogation.md): answered by a built-in handler, never delivered to
|
|
1860
1873
|
// tb.onMessage. Each settled non-undefined return is a reply candidate \u2014 the CLI enforces the
|
|
@@ -1896,6 +1909,10 @@ ${LCt("tbStreamUrl")}
|
|
|
1896
1909
|
if (env.id !== undefined) {
|
|
1897
1910
|
try { fetch('/__send-reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: env.id, results, errors }) }); } catch {}
|
|
1898
1911
|
}
|
|
1912
|
+
};
|
|
1913
|
+
tbOn('message', (e) => {
|
|
1914
|
+
if (pageAwake) { void onMessageEvent(e); return; }
|
|
1915
|
+
beforeAwake.push(() => { void onMessageEvent(e); });
|
|
1899
1916
|
});
|
|
1900
1917
|
}
|
|
1901
1918
|
})();
|
|
@@ -1854,7 +1854,20 @@ ${xCt("tbStreamUrl")}
|
|
|
1854
1854
|
} catch (err) { location.replace('about:blank'); }
|
|
1855
1855
|
}, 250);
|
|
1856
1856
|
});
|
|
1857
|
-
|
|
1857
|
+
// The stream attaches during this script's own eval, in <head> \u2014 before the bulb's module (and
|
|
1858
|
+
// its import graph) have run, so before a single \`tb.onMessage\` handler exists. The server now
|
|
1859
|
+
// delivers the instant the stream attaches (it holds a send for the page it opened), so a
|
|
1860
|
+
// message can beat the page's own code by the width of a CDN fetch. A page that is attached but
|
|
1861
|
+
// has not yet run is not a page with no handlers, so hold the message until the deferred module
|
|
1862
|
+
// scripts have executed (DOMContentLoaded waits for them). Page-side and bounded by one load:
|
|
1863
|
+
// the server still never buffers, and a page that is awake handles inline as before.
|
|
1864
|
+
let pageAwake = document.readyState !== 'loading';
|
|
1865
|
+
const beforeAwake = [];
|
|
1866
|
+
if (!pageAwake) document.addEventListener('DOMContentLoaded', () => {
|
|
1867
|
+
pageAwake = true;
|
|
1868
|
+
for (const run of beforeAwake.splice(0)) run();
|
|
1869
|
+
});
|
|
1870
|
+
const onMessageEvent = async (e) => {
|
|
1858
1871
|
// Wire envelope { id?, payload } (JSON \u2014 SSE-line-safe). tb:* payloads are the shim's reserved
|
|
1859
1872
|
// namespace (TB-Interrogation.md): answered by a built-in handler, never delivered to
|
|
1860
1873
|
// tb.onMessage. Each settled non-undefined return is a reply candidate \u2014 the CLI enforces the
|
|
@@ -1896,6 +1909,10 @@ ${xCt("tbStreamUrl")}
|
|
|
1896
1909
|
if (env.id !== undefined) {
|
|
1897
1910
|
try { fetch('/__send-reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: env.id, results, errors }) }); } catch {}
|
|
1898
1911
|
}
|
|
1912
|
+
};
|
|
1913
|
+
tbOn('message', (e) => {
|
|
1914
|
+
if (pageAwake) { void onMessageEvent(e); return; }
|
|
1915
|
+
beforeAwake.push(() => { void onMessageEvent(e); });
|
|
1899
1916
|
});
|
|
1900
1917
|
}
|
|
1901
1918
|
})();
|
package/dist/agents/pi/client.js
CHANGED
|
@@ -1854,7 +1854,20 @@ ${MCt("tbStreamUrl")}
|
|
|
1854
1854
|
} catch (err) { location.replace('about:blank'); }
|
|
1855
1855
|
}, 250);
|
|
1856
1856
|
});
|
|
1857
|
-
|
|
1857
|
+
// The stream attaches during this script's own eval, in <head> \u2014 before the bulb's module (and
|
|
1858
|
+
// its import graph) have run, so before a single \`tb.onMessage\` handler exists. The server now
|
|
1859
|
+
// delivers the instant the stream attaches (it holds a send for the page it opened), so a
|
|
1860
|
+
// message can beat the page's own code by the width of a CDN fetch. A page that is attached but
|
|
1861
|
+
// has not yet run is not a page with no handlers, so hold the message until the deferred module
|
|
1862
|
+
// scripts have executed (DOMContentLoaded waits for them). Page-side and bounded by one load:
|
|
1863
|
+
// the server still never buffers, and a page that is awake handles inline as before.
|
|
1864
|
+
let pageAwake = document.readyState !== 'loading';
|
|
1865
|
+
const beforeAwake = [];
|
|
1866
|
+
if (!pageAwake) document.addEventListener('DOMContentLoaded', () => {
|
|
1867
|
+
pageAwake = true;
|
|
1868
|
+
for (const run of beforeAwake.splice(0)) run();
|
|
1869
|
+
});
|
|
1870
|
+
const onMessageEvent = async (e) => {
|
|
1858
1871
|
// Wire envelope { id?, payload } (JSON \u2014 SSE-line-safe). tb:* payloads are the shim's reserved
|
|
1859
1872
|
// namespace (TB-Interrogation.md): answered by a built-in handler, never delivered to
|
|
1860
1873
|
// tb.onMessage. Each settled non-undefined return is a reply candidate \u2014 the CLI enforces the
|
|
@@ -1896,6 +1909,10 @@ ${MCt("tbStreamUrl")}
|
|
|
1896
1909
|
if (env.id !== undefined) {
|
|
1897
1910
|
try { fetch('/__send-reply', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: env.id, results, errors }) }); } catch {}
|
|
1898
1911
|
}
|
|
1912
|
+
};
|
|
1913
|
+
tbOn('message', (e) => {
|
|
1914
|
+
if (pageAwake) { void onMessageEvent(e); return; }
|
|
1915
|
+
beforeAwake.push(() => { void onMessageEvent(e); });
|
|
1899
1916
|
});
|
|
1900
1917
|
}
|
|
1901
1918
|
})();
|