typebulb 0.10.4 → 0.10.5

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
@@ -226,7 +226,7 @@ A local `.bulb.md` can be re-imported into typebulb.com. If it has a `**server.t
226
226
 
227
227
  The agent viewer currently supports Claude Code only. `npx typebulb agent:claude` gives the user a great scratchpad experience:
228
228
 
229
- * a view over the Claude Code session, where assistant messages containing bulbs render as sandboxed embedded bulbs inline in the conversation, alongside KaTeX math and mermaid diagrams.
229
+ * a view over the Claude Code session, where assistant messages containing bulbs render as sandboxed embedded bulbs inline in the conversation, alongside KaTeX math, mermaid diagrams and svg.
230
230
  * run and stop any bulb in their project.
231
231
  * promote any embedded bulb to a `.bulb.md` file in the `typebulbs/` folder.
232
232
 
@@ -820,6 +820,16 @@ function decodeHtmlEntities(s: string): string {
820
820
  // ```mermaid``` fences render to inline SVG. A parse error falls through to the
821
821
  // default code-block rendering, so an unsupported diagram degrades to readable
822
822
  // source rather than a broken message.
823
+ // A hover-revealed copy pill for a rendered fence. Every fenced block — code, svg, mermaid, an
824
+ // unrecognised language — flows through the fence rule below, and the thing worth copying is always
825
+ // its raw body, so one primitive covers them all. The source rides the button's own data-src
826
+ // (escaped going in; getAttribute auto-unescapes coming out, so multi-line bodies round-trip), which
827
+ // decouples the delegated handler from each block's container structure. `.copyable` is the shared
828
+ // marker the rule stamps on every block so one CSS rule can reveal the pill on hover.
829
+ function copyButton(src: string): string {
830
+ return `<button class="overlay-pill copy-src" type="button" title="Copy source" data-src="${md.utils.escapeHtml(src)}">copy</button>`
831
+ }
832
+
823
833
  const defaultFence: MdRenderRule = md.renderer.rules.fence
824
834
  ?? ((tokens, idx, opts, _env, self) => self.renderToken(tokens, idx, opts))
825
835
  md.renderer.rules.fence = (tokens: MdToken[], idx: number, opts: unknown, env: unknown, self: MdRenderer) => {
@@ -840,7 +850,8 @@ md.renderer.rules.fence = (tokens: MdToken[], idx: number, opts: unknown, env: u
840
850
  // surface is stripped. Lets the agent draw anything (smiley, plot from an
841
851
  // equation) without an iframe, since it's static markup, not executed code.
842
852
  if (lang === 'svg' && live) {
843
- return `<div class="embed svg-embed">${DOMPurify.sanitize(t.content, { USE_PROFILES: { svg: true, svgFilters: true } })}</div>`
853
+ const safe = DOMPurify.sanitize(t.content, { USE_PROFILES: { svg: true, svgFilters: true } })
854
+ return `<div class="embed svg-embed copyable">${safe}${copyButton(t.content)}</div>`
844
855
  }
845
856
  if (lang === 'mermaid' && live) {
846
857
  try {
@@ -861,10 +872,13 @@ md.renderer.rules.fence = (tokens: MdToken[], idx: number, opts: unknown, env: u
861
872
  font: 'system-ui, -apple-system, "Segoe UI", sans-serif',
862
873
  transparent: true,
863
874
  })
864
- return `<div class="mermaid">${svg}</div>`
875
+ return `<div class="mermaid copyable">${svg}${copyButton(t.content)}</div>`
865
876
  } catch { /* fall through to a plain code block */ }
866
877
  }
867
- return defaultFence(tokens, idx, opts, env, self)
878
+ // Every other fence (code, an unrecognised language, or a non-live svg/mermaid in a user turn) is a
879
+ // copyable source block: the default <pre><code> wrapped so it gets the same hover copy pill. The
880
+ // wrapper — not the <pre> — is the `.md` flow child now; see `.code-block` CSS for the rhythm.
881
+ return `<div class="code-block copyable">${defaultFence(tokens, idx, opts, env, self)}${copyButton(t.content)}</div>`
868
882
  }
869
883
 
870
884
  // Author classDef/style colors render as literal fill/stroke/color that override
@@ -926,6 +940,16 @@ function fitSvgEmbeds(root: Element) {
926
940
  // served from the project root, so the browser would GET the path and 404. Links
927
941
  // with a scheme (http, mailto, …) fall through to the default new-tab behavior.
928
942
  function onMarkdownClick(e: Event) {
943
+ // Per-fence copy: the source is on the button's own data-src (see copyButton), so no container walk.
944
+ const copyBtn = (e.target as Element | null)?.closest<HTMLButtonElement>('.copy-src')
945
+ if (copyBtn) {
946
+ e.preventDefault()
947
+ navigator.clipboard?.writeText(copyBtn.dataset.src ?? '')
948
+ copyBtn.classList.add('done')
949
+ copyBtn.textContent = 'copied'
950
+ setTimeout(() => { copyBtn.classList.remove('done'); copyBtn.textContent = 'copy' }, 1200)
951
+ return
952
+ }
929
953
  const anchor = (e.target as Element | null)?.closest('a')
930
954
  if (!anchor) return
931
955
  const href = anchor.getAttribute('href') ?? ''
@@ -3050,6 +3074,19 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
3050
3074
  }
3051
3075
  .overlay-pill:hover { color: var(--accent); border-color: var(--accent); }
3052
3076
 
3077
+ /* Per-fence copy pill (code / svg / mermaid) — one reveal rule for every copyable block, since the
3078
+ fence rule stamps `.copyable` on each. The svg/mermaid containers are already position:relative for
3079
+ the chop/breakout; the code-block wrapper needs it for the absolute pill. The pill is positioned
3080
+ (absolute), so it paints over the static fence content regardless of DOM order. */
3081
+ .md .copyable { position: relative; }
3082
+ .md .copyable:hover .copy-src,
3083
+ .md .copy-src:focus-visible { opacity: 1; }
3084
+ .md .copy-src.done { color: var(--accent); border-color: var(--accent); }
3085
+ /* The wrapper carries the code block's vertical rhythm (it, not the <pre>, is the `.md` flow child
3086
+ now, so the first/last-child margin resets land on it); the <pre> sits flush inside. */
3087
+ .md .code-block { margin: .6rem 0; }
3088
+ .md .code-block pre { margin: 0; }
3089
+
3053
3090
  /* Controls (code ⇄ run, then copy, then breakout) — a centered overlay straddling the
3054
3091
  bulb's top edge: clear of the running bulb below, free to overlap the prose above.
3055
3092
  Absolute, so toggling run↔code (which resizes the bulb below) never moves it, and so
@@ -3302,7 +3339,7 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
3302
3339
  "beautiful-mermaid": "^1.1.3",
3303
3340
  "dompurify": "^3.2.6",
3304
3341
  "highlight.js": "^11.10.0",
3305
- "typebulb": "^0.10.4"
3342
+ "typebulb": "^0.10.5"
3306
3343
  }
3307
3344
  }
3308
3345
  ```
package/dist/index.js CHANGED
@@ -668,6 +668,6 @@ ${r.trim()}
668
668
  `)}catch(f){console.error("Compile error:",f)}}),F=vt({bulbPath:r,emitter:u}),n){let{name:f,serveDir:b}=n;L=Fn({dir:b,onChange:()=>{console.log(`Local package '${f}' changed. Browser reloading...
669
669
  `),a.emit("reload")}})}}e.open&&await it(g);let _=async()=>{console.log(`
670
670
  Shutting down...`),h.close(),F?.(),L?.(),i(),await qt(process.pid);let u=ve.join(ve.dirname(r),".typebulb","server.mjs");await Bn.rm(u,{force:!0}).catch(()=>{}),process.exit(0)};process.on("SIGINT",_),process.on("SIGTERM",_)}import*as Un from"path";import{EventEmitter as Mo}from"events";async function Wn(r,e,t,n,s){let o=ge(t),i=!1,a=async()=>{let{bulb:c,config:l}=await W(r);i||(mt(o,r,c.server),i=!0),await Nt(c.server,s,n,l.dependencies)};if(console.log(`Running ${Un.basename(r)}...`),await a(),e){console.log(`Watching for changes...
671
- `);let c=new Mo;c.on("reload",async()=>{try{console.log("Re-running..."),await a()}catch(l){console.error("Error:",l)}}),vt({bulbPath:r,emitter:c})}}var Jn="0.10.4";async function jo(){let r=ur(process.argv.slice(2));if(r.version&&(console.log(`typebulb ${Jn}`),process.exit(0)),r.help&&(pr(),process.exit(0)),r.subcommand==="logs"){await _n(r.file||void 0,{follow:r.follow,lines:r.lines});return}if(r.subcommand==="stop"){await Cn(r.file||void 0);return}if(r.subcommand==="skill"){await Pn(Jn);return}if(r.subcommand==="models"){await wn(r.mode);return}if(r.subcommand==="agent"){if(!r.agentTarget){await gn();return}me(r.agentTarget)||(console.error(`Unknown agent '${r.agentTarget}'. Known: ${Wt().join(", ")}.`),process.exit(1));let l=await zt(process.cwd(),r.agentTarget);if(l){console.log(`Viewer '${r.agentTarget}' is already running for this project:
671
+ `);let c=new Mo;c.on("reload",async()=>{try{console.log("Re-running..."),await a()}catch(l){console.error("Error:",l)}}),vt({bulbPath:r,emitter:c})}}var Jn="0.10.5";async function jo(){let r=ur(process.argv.slice(2));if(r.version&&(console.log(`typebulb ${Jn}`),process.exit(0)),r.help&&(pr(),process.exit(0)),r.subcommand==="logs"){await _n(r.file||void 0,{follow:r.follow,lines:r.lines});return}if(r.subcommand==="stop"){await Cn(r.file||void 0);return}if(r.subcommand==="skill"){await Pn(Jn);return}if(r.subcommand==="models"){await wn(r.mode);return}if(r.subcommand==="agent"){if(!r.agentTarget){await gn();return}me(r.agentTarget)||(console.error(`Unknown agent '${r.agentTarget}'. Known: ${Wt().join(", ")}.`),process.exit(1));let l=await zt(process.cwd(),r.agentTarget);if(l){console.log(`Viewer '${r.agentTarget}' is already running for this project:
672
672
  ${l.url}`),r.open&&await it(l.url);return}r.file=r.agentTarget,r.subcommand="run"}if(r.subcommand==="trust"||r.subcommand==="untrust"){await on(r.file||void 0,r.subcommand==="trust");return}let e,t=!1;if(!r.file||r.file==="."){let l=await Hr(process.cwd());l||(console.error("No .bulb.md file found in current directory"),process.exit(1)),e=l}else e=q.resolve(r.file);if(!await Vn.access(e).then(()=>!0,()=>!1)){let l=r.agentTarget?me(r.file):void 0;l?(e=l,t=!0):Wt().includes(r.file)?(console.error(`To open the ${r.file} agent viewer, 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));let s=r.file&&r.file!=="."?r.file:q.relative(process.cwd(),e)||q.basename(e),o=`npx typebulb --trust ${s.includes(" ")?`"${s}"`:s}`;if(r.subcommand==="predict"){await sn(e,o);return}if(t)r.trust=!r.noTrust,r.trust&&console.log("trust: granted (built-in bulb)");else{let l=!r.noTrust&&ot(e);l&&!r.trust&&console.log("trust: granted from memory (run `typebulb untrust` to revoke)"),r.trust=r.noTrust?!1:r.trust||l}let i;try{i=await W(e)}catch{}let a;if(r.local){i&&!(r.local.name in(i.config.dependencies??{}))&&(console.error(`--replace: '${r.local.name}' is not a dependency in this bulb's config.json; nothing to replace.`),process.exit(1)),i&&(!i.bulb.code||r.server)&&console.warn("warning: --replace has no effect in server mode (the override is client-only).");try{a=await lr(r.local)}catch(l){console.error(l instanceof Error?l.message:String(l)),process.exit(1)}console.log(`replace: ${a.name} \u2192 ${q.relative(process.cwd(),a.dir)||"."}`)}if(r.subcommand==="check"){await nn(e,a);return}let c=t?process.cwd():q.dirname(e);if(i&&i.bulb.server&&(!i.bulb.code||r.server)){r.trust||(console.error(`This bulb runs server-side Node code (server.ts), which --trust must authorize:
673
673
  ${o}`),process.exit(1)),await Wn(e,r.watch,r.mode,a,c);return}await Ln(e,r,o,a,c)}jo().catch(r=>{console.error("Error:",r.message),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typebulb",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Local bulb runner CLI for Typebulb",
5
5
  "license": "MIT",
6
6
  "engines": { "node": ">=18" },