typebulb 0.10.2 → 0.10.4
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 +1 -1
- package/bulbs/claude.bulb.md +102 -56
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,7 +123,7 @@ typebulb skill Print this README as an Agent Skill on stdout
|
|
|
123
123
|
typebulb check [file.bulb.md] Type-check a bulb without running it
|
|
124
124
|
typebulb predict [file] Report the capability a bulb probably needs, without running it
|
|
125
125
|
typebulb models List AI models for tb.ai, filtered by your .env API keys
|
|
126
|
-
typebulb logs [file|pid] Print a running bulb's captured console (-f follow, -n N tail)
|
|
126
|
+
typebulb logs [file|pid] Print a running bulb's captured console (no arg: list running servers; -f follow, -n N tail)
|
|
127
127
|
typebulb stop [file|pid] Stop a running bulb (no arg: list this project's running servers)
|
|
128
128
|
typebulb trust [file] Remember a bulb as trusted (no arg: list trusted bulbs)
|
|
129
129
|
typebulb untrust <file> Forget a bulb's trust (back to sandboxed)
|
package/bulbs/claude.bulb.md
CHANGED
|
@@ -825,6 +825,12 @@ const defaultFence: MdRenderRule = md.renderer.rules.fence
|
|
|
825
825
|
md.renderer.rules.fence = (tokens: MdToken[], idx: number, opts: unknown, env: unknown, self: MdRenderer) => {
|
|
826
826
|
const t = tokens[idx]
|
|
827
827
|
const lang = (t.info ?? '').trim().toLowerCase()
|
|
828
|
+
// Live embeds (svg, mermaid) are an assistant medium — they render only in assistant turns. In a
|
|
829
|
+
// user turn (env.userMessage, set by userMarkdown) they fall through to a plain source code block:
|
|
830
|
+
// the svg chop and the mermaid lane-breakout both assume the transparent assistant flow and clash
|
|
831
|
+
// with the opaque, padded, rounded user card, and a user's pasted/quoted markup reads better as its
|
|
832
|
+
// literal source anyway. KaTeX/@mentions/links still render in user turns (those aren't gated here).
|
|
833
|
+
const live = !(env as { userMessage?: boolean })?.userMessage
|
|
828
834
|
// Note: no `bulb` case here. Live embeds are split out of the text before markdown runs
|
|
829
835
|
// (splitBulbSegments → BulbEmbed), so a ````bulb```` fence reaching markdown is illustrative
|
|
830
836
|
// source — it falls through to defaultFence like any other unrecognised fence.
|
|
@@ -833,10 +839,10 @@ md.renderer.rules.fence = (tokens: MdToken[], idx: number, opts: unknown, env: u
|
|
|
833
839
|
// it goes through DOMPurify's svg profile first — geometry survives, the script
|
|
834
840
|
// surface is stripped. Lets the agent draw anything (smiley, plot from an
|
|
835
841
|
// equation) without an iframe, since it's static markup, not executed code.
|
|
836
|
-
if (lang === 'svg') {
|
|
837
|
-
return `<div class="svg-embed">${DOMPurify.sanitize(t.content, { USE_PROFILES: { svg: true, svgFilters: true } })}</div>`
|
|
842
|
+
if (lang === 'svg' && live) {
|
|
843
|
+
return `<div class="embed svg-embed">${DOMPurify.sanitize(t.content, { USE_PROFILES: { svg: true, svgFilters: true } })}</div>`
|
|
838
844
|
}
|
|
839
|
-
if (lang === 'mermaid') {
|
|
845
|
+
if (lang === 'mermaid' && live) {
|
|
840
846
|
try {
|
|
841
847
|
// The library's internal theme vars share names with the bulb's (--fg/--bg/
|
|
842
848
|
// --muted/--border/--accent), so passing var(--fg) here would emit a
|
|
@@ -885,6 +891,36 @@ function themeMermaidNodes(root: Element) {
|
|
|
885
891
|
}
|
|
886
892
|
}
|
|
887
893
|
|
|
894
|
+
// Fit each raw ```svg``` embed to its content. A raw <svg> with only a viewBox has NO intrinsic size,
|
|
895
|
+
// so it defaults to width:100% and stretches to fill the column — which both makes the container's
|
|
896
|
+
// justify-content:center moot (where the art sits *inside* the viewBox is all that positions it, so a
|
|
897
|
+
// drawing parked off-centre in an oversized box reads as left/right-aligned) and blows a small drawing
|
|
898
|
+
// up to a column-wide wall. Two coupled steps fix both: (1) tighten the viewBox to the content's bbox,
|
|
899
|
+
// then (2) set the display width to that bbox's user-unit extent as px (1 unit ≈ 1px), capped by the
|
|
900
|
+
// CSS max-width:100%. Now it renders small when small (and the now-narrower svg actually centres), and
|
|
901
|
+
// a genuinely large drawing still caps at the column. getBBox is geometry-only — it ignores stroke,
|
|
902
|
+
// which paints up to half its width *outside* the geometry — so the viewBox is padded by the largest
|
|
903
|
+
// painted half-stroke (clipping depends on stroke width, not drawing size; a fixed % loses to a thick
|
|
904
|
+
// stroke on a small drawing). The +1 is a hairline guard for anti-aliasing / curve overshoot; pad is 0
|
|
905
|
+
// when nothing strokes (fills never overflow). Skips a node that isn't measurable (zero-area, or laid
|
|
906
|
+
// out inside a collapsed turn → getBBox throws / returns 0), leaving it as authored.
|
|
907
|
+
function fitSvgEmbeds(root: Element) {
|
|
908
|
+
for (const svg of root.querySelectorAll<SVGSVGElement>('.svg-embed svg')) {
|
|
909
|
+
try {
|
|
910
|
+
const b = svg.getBBox()
|
|
911
|
+
if (!b.width || !b.height) continue
|
|
912
|
+
let half = 0
|
|
913
|
+
for (const el of svg.querySelectorAll<SVGElement>('*')) {
|
|
914
|
+
const cs = getComputedStyle(el) // computed, so it catches attribute, CSS, and inherited widths
|
|
915
|
+
if (cs.stroke && cs.stroke !== 'none') half = Math.max(half, parseFloat(cs.strokeWidth) / 2)
|
|
916
|
+
}
|
|
917
|
+
const pad = half ? half + 1 : 0
|
|
918
|
+
svg.setAttribute('viewBox', `${b.x - pad} ${b.y - pad} ${b.width + pad * 2} ${b.height + pad * 2}`)
|
|
919
|
+
svg.style.width = `${b.width + pad * 2}px` // natural size (units≈px); max-width:100% caps it, height:auto keeps ratio
|
|
920
|
+
} catch { /* not laid out / unmeasurable — leave the authored viewBox */ }
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
888
924
|
// A relative-path link in assistant markdown is a local file citation (optionally
|
|
889
925
|
// with a #Lnnn line anchor): open it in the editor, not the browser — the bulb is
|
|
890
926
|
// served from the project root, so the browser would GET the path and 404. Links
|
|
@@ -904,10 +940,11 @@ function onMarkdownClick(e: Event) {
|
|
|
904
940
|
// Render markdown into the element via innerHTML. The click handler is bound natively here, not via
|
|
905
941
|
// domeleon's `onClick`: the anchors are raw innerHTML the vdom never sees, so a delegated handler
|
|
906
942
|
// wouldn't fire. onMarkdownClick is a stable ref, so re-mounts don't stack duplicate listeners.
|
|
907
|
-
const renderMarkdown = (text: string) => (el: Element) => {
|
|
943
|
+
const renderMarkdown = (text: string, env?: { userMessage?: boolean }) => (el: Element) => {
|
|
908
944
|
try {
|
|
909
|
-
el.innerHTML = md.render(text)
|
|
945
|
+
el.innerHTML = md.render(text, env)
|
|
910
946
|
themeMermaidNodes(el)
|
|
947
|
+
fitSvgEmbeds(el)
|
|
911
948
|
} catch {
|
|
912
949
|
;(el as HTMLElement).textContent = text
|
|
913
950
|
}
|
|
@@ -936,7 +973,7 @@ const atMentionLink = (m: string): string => {
|
|
|
936
973
|
const userMarkdown = (msg: Msg) => {
|
|
937
974
|
const segs = msg.segments ?? [msg.text]
|
|
938
975
|
const src = segs.join('\n\n---\n\n').replace(AT_MENTION, atMentionLink)
|
|
939
|
-
return renderMarkdown(src)
|
|
976
|
+
return renderMarkdown(src, { userMessage: true })
|
|
940
977
|
}
|
|
941
978
|
|
|
942
979
|
// Split an assistant message into ordered markdown chunks and live ````bulb```` sources, using
|
|
@@ -1821,7 +1858,7 @@ class BulbEmbed extends Component {
|
|
|
1821
1858
|
}
|
|
1822
1859
|
|
|
1823
1860
|
view() {
|
|
1824
|
-
const cls = ['bulb-embed', this.spread ? 'spread' : 'inline', this.showingCode ? 'code-open' : '', this.#compileError ? 'err' : '']
|
|
1861
|
+
const cls = ['embed', 'bulb-embed', this.spread ? 'spread' : 'inline', this.showingCode ? 'code-open' : '', this.#compileError ? 'err' : '']
|
|
1825
1862
|
const inner =
|
|
1826
1863
|
this.#compileError ? [`bulb: ${this.#compileError}`]
|
|
1827
1864
|
: !this.#frame ? ['compiling bulb…']
|
|
@@ -2745,10 +2782,11 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
|
|
|
2745
2782
|
flex: 1;
|
|
2746
2783
|
overflow-y: auto;
|
|
2747
2784
|
/* Content runs under the overlaid statusbar rather than stopping above it.
|
|
2748
|
-
|
|
2749
|
-
|
|
2785
|
+
Gutters are symmetric: the turn stripe is absolutely positioned, so it claims no layout
|
|
2786
|
+
width and the left gutter needs no extra room for it (see Invariants). The bottom is
|
|
2787
|
+
larger to clear the overlaid statusbar pills (~2.1rem:
|
|
2750
2788
|
.4rem inset + 1.7rem pill) so the last line isn't hidden behind them. */
|
|
2751
|
-
padding: 1.25rem 1.
|
|
2789
|
+
padding: 1.25rem 1.75rem 2.5rem 1.75rem;
|
|
2752
2790
|
display: flex;
|
|
2753
2791
|
flex-direction: column;
|
|
2754
2792
|
gap: 1rem;
|
|
@@ -2800,10 +2838,15 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
|
|
|
2800
2838
|
/* Offsets subtract --border-w: absolute positioning anchors to the
|
|
2801
2839
|
padding-edge (inside the border), so a user bubble's 1px border would
|
|
2802
2840
|
otherwise shave 1px off each stripe edge. */
|
|
2841
|
+
/* Stripe hugs the column (1rem into the left gutter); absolute, so it consumes no layout width —
|
|
2842
|
+
which is why the gutters can be symmetric (no asymmetric padding needed to "make room"), and
|
|
2843
|
+
thus why breakouts need no nudge. - var(--border-w): a user bubble's 1px border pushes the
|
|
2844
|
+
padding edge (the abs-pos anchor) 1px right, so without it that stripe sits 1px right of the
|
|
2845
|
+
assistant bubbles' — compensate. */
|
|
2803
2846
|
left: calc(-1rem - var(--border-w));
|
|
2804
2847
|
top: calc(-.5rem - var(--border-w));
|
|
2805
2848
|
bottom: calc(-.5rem - var(--border-w));
|
|
2806
|
-
width:
|
|
2849
|
+
width: 1px;
|
|
2807
2850
|
background: var(--turn-color, transparent);
|
|
2808
2851
|
}
|
|
2809
2852
|
/* Clip stripe at turn boundaries. A user bubble always starts a turn → no
|
|
@@ -2875,20 +2918,24 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
|
|
|
2875
2918
|
.md table { border-collapse: collapse; }
|
|
2876
2919
|
.md th, .md td { border: 1px solid var(--border); padding: .3rem .55rem; }
|
|
2877
2920
|
|
|
2878
|
-
/*
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2921
|
+
/* The full-lane breakout geometry, defined ONCE and shared by the two breakouts (the mermaid
|
|
2922
|
+
embed and a spread bulb). `100%` in --lane-ml resolves against each user's own containing block
|
|
2923
|
+
(prose width) at point of use, so both centre on the column axis — which is the viewport centre,
|
|
2924
|
+
since the gutters are symmetric. One home so the geometry can't drift between the two (the bug
|
|
2925
|
+
that let an off-centre nudge survive in one rule after being removed from the other). */
|
|
2926
|
+
.md { --lane-w: calc(100vw - 2rem); --lane-ml: calc((100% - var(--lane-w)) / 2); }
|
|
2927
|
+
|
|
2928
|
+
/* Full-lane breakout — the mermaid embed (whole element) and a SPREAD bulb's iframe step out of
|
|
2929
|
+
the prose column to the transcript width so wide content gets room. Both use --lane-w/--lane-ml
|
|
2930
|
+
and extend symmetrically from the column, which is viewport-centred (symmetric gutters), so
|
|
2931
|
+
neither needs an off-centre nudge (see Invariants in Specs-Bulbs/Claude-Bulb/Claude-Bulb.md). A
|
|
2932
|
+
bulb's *wrapper* never breaks out — only its inner surface does — so the wrapper and its controls
|
|
2933
|
+
stay at prose-right. position + z-index:0 lift the box over the turn stripe it crosses; the
|
|
2934
|
+
mermaid SVG is transparent so a bg backs it, the bulb iframe is opaque so it occludes the stripe. */
|
|
2888
2935
|
.md .mermaid {
|
|
2889
|
-
width:
|
|
2936
|
+
width: var(--lane-w);
|
|
2890
2937
|
margin: .6rem 0;
|
|
2891
|
-
margin-left:
|
|
2938
|
+
margin-left: var(--lane-ml);
|
|
2892
2939
|
margin-right: auto;
|
|
2893
2940
|
position: relative;
|
|
2894
2941
|
z-index: 0;
|
|
@@ -2912,8 +2959,9 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
|
|
|
2912
2959
|
.md .mermaid svg { max-width: none; height: auto; flex: 0 0 auto; }
|
|
2913
2960
|
|
|
2914
2961
|
/* Raw ```svg``` embeds: no breakout — these are drawings (smiley, plot), not wide
|
|
2915
|
-
diagrams, so keep them in the prose column, centred and capped at its width.
|
|
2916
|
-
|
|
2962
|
+
diagrams, so keep them in the prose column, centred and capped at its width. They
|
|
2963
|
+
share the `.embed` stripe-chop below (a drawing earns its own row like a bulb). */
|
|
2964
|
+
.md .svg-embed { justify-content: center; }
|
|
2917
2965
|
.md .svg-embed svg { max-width: 100%; height: auto; }
|
|
2918
2966
|
|
|
2919
2967
|
/* ````bulb```` embeds: a sandboxed nested app. createBulbFrame owns the iframe (auto-height,
|
|
@@ -2924,28 +2972,27 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
|
|
|
2924
2972
|
position:relative anchors the controls overlay in BOTH modes (spread re-declares it for the
|
|
2925
2973
|
z-index lift). .err is the compile-failure fallback; .bulb-err-strip is a runtime-error strip
|
|
2926
2974
|
under a live embed (both monospace, muted red). */
|
|
2927
|
-
|
|
2975
|
+
/* The stripe-chop, shared by every opaque in-column visual artifact (the inline bulb and a raw
|
|
2976
|
+
`svg` — each opts in by carrying `.embed`). An opaque bg over position:relative + z-index:0
|
|
2977
|
+
paints above .bubble::before; a -1rem left extension reaches the stripe in the gutter without a
|
|
2978
|
+
full breakout (padding-left:1rem keeps the content in the prose column, and the box's right edge
|
|
2979
|
+
stays at prose-right, so a bulb's controls anchored there don't move). The vertical gap is opaque
|
|
2980
|
+
padding, not margin: it occludes the stripe top and bottom so the cut is flush with the prose (no
|
|
2981
|
+
stub in a transparent gap), symmetric above/below to separate the artifact from surrounding text.
|
|
2982
|
+
margin top/bottom are zeroed — and the abutting prose margins too (below) — so the gap butts the
|
|
2983
|
+
text. Mermaid is NOT on this path: it's transparent and breaks out, occluding via box-shadow.
|
|
2984
|
+
Per-artifact layout (flex direction, centring) lives on .bulb-embed / .svg-embed. */
|
|
2985
|
+
.md .embed {
|
|
2928
2986
|
display: flex;
|
|
2929
|
-
flex-direction: column;
|
|
2930
2987
|
position: relative;
|
|
2931
2988
|
z-index: 0;
|
|
2932
|
-
/* Always cut the turn stripe, both modes: an opaque bg over position:relative + z-index:0
|
|
2933
|
-
paints above .bubble::before, and a -1rem left extension reaches the stripe in the gutter
|
|
2934
|
-
without a full breakout (padding-left:1rem keeps the content in the prose column; the box's
|
|
2935
|
-
right edge stays at prose-right, so the controls anchored there don't move). In spread mode the
|
|
2936
|
-
iframe's own breakout covers the stripe too. */
|
|
2937
2989
|
background: var(--bg);
|
|
2938
|
-
margin
|
|
2939
|
-
padding
|
|
2940
|
-
/* Symmetric vertical gap as opaque padding, not margin: it occludes the stripe top and bottom
|
|
2941
|
-
so it's cut flush with the prose (no stub in a transparent gap), and is the same amount above
|
|
2942
|
-
and below to separate the bulb from surrounding text. margin top/bottom are zeroed — and the
|
|
2943
|
-
abutting prose margins too (below) — so the gap butts against the text. */
|
|
2944
|
-
margin-top: 0;
|
|
2945
|
-
margin-bottom: 0;
|
|
2946
|
-
padding-top: 1.1rem;
|
|
2947
|
-
padding-bottom: 1.1rem;
|
|
2990
|
+
margin: 0 0 0 -1rem;
|
|
2991
|
+
padding: 1.1rem 0 1.1rem 1rem;
|
|
2948
2992
|
}
|
|
2993
|
+
/* A bulb stacks its controls/frame/code vertically; in spread mode the inner iframe's own
|
|
2994
|
+
breakout covers the stripe too (the wrapper still chops in column). */
|
|
2995
|
+
.md .bulb-embed { flex-direction: column; }
|
|
2949
2996
|
/* Inline (default): cap the embed's height so a tall bulb doesn't run away down the transcript.
|
|
2950
2997
|
Past the cap the embed scrolls internally (its own overflow, set by the host↔embed protocol —
|
|
2951
2998
|
the iframe element can't scroll its srcdoc from out here). spread removes the cap. */
|
|
@@ -2956,26 +3003,25 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
|
|
|
2956
3003
|
code toggle hides it via .code-open rather than unmounting, so the bulb keeps its state. */
|
|
2957
3004
|
.bulb-frame { display: contents; }
|
|
2958
3005
|
.md .bulb-embed.code-open .bulb-frame { display: none; }
|
|
2959
|
-
/* Spread: the inner surface
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
are opaque, so position/z-index:0 lift them over the turn stripe they now cross. */
|
|
3006
|
+
/* Spread: the inner surface (live iframe, or the code view behind the toggle) breaks out to the
|
|
3007
|
+
lane while the wrapper stays in the prose column, so the controls hold at prose-right. Shares the
|
|
3008
|
+
--lane-* geometry; centred on the column axis so toggling spread doesn't shift a column-fitting
|
|
3009
|
+
bulb sideways. `width !important` beats createBulbFrame's inline width:100% on the iframe
|
|
3010
|
+
(harmless on the code view, which has none); both opaque, so position/z-index:0 lift them over
|
|
3011
|
+
the turn stripe they now cross. */
|
|
2966
3012
|
.md .bulb-embed.spread iframe,
|
|
2967
3013
|
.md .bulb-embed.spread > .bulb-code {
|
|
2968
|
-
width:
|
|
2969
|
-
margin-left:
|
|
3014
|
+
width: var(--lane-w) !important;
|
|
3015
|
+
margin-left: var(--lane-ml);
|
|
2970
3016
|
margin-right: auto;
|
|
2971
3017
|
position: relative;
|
|
2972
3018
|
z-index: 0;
|
|
2973
3019
|
}
|
|
2974
|
-
/* Kill the transparent margin between
|
|
2975
|
-
the
|
|
3020
|
+
/* Kill the transparent margin between an embed and the prose it abuts (top and bottom):
|
|
3021
|
+
the embed's own opaque padding is the gap, and a leftover prose margin would let the
|
|
2976
3022
|
turn stripe show through it as a stub past the last/next line of text. */
|
|
2977
|
-
.md :has(+ .
|
|
2978
|
-
.md .
|
|
3023
|
+
.md :has(+ .embed) { margin-bottom: 0; }
|
|
3024
|
+
.md .embed + * { margin-top: 0; }
|
|
2979
3025
|
/* The rounded clip lives on the inner surfaces, not the embed (whose padding is now the
|
|
2980
3026
|
gap), so the bulb's own card — the iframe, or the code view behind the toggle — is
|
|
2981
3027
|
what carries the corners. */
|
|
@@ -3256,7 +3302,7 @@ a.server-port:hover { color: var(--accent); text-decoration: underline; }
|
|
|
3256
3302
|
"beautiful-mermaid": "^1.1.3",
|
|
3257
3303
|
"dompurify": "^3.2.6",
|
|
3258
3304
|
"highlight.js": "^11.10.0",
|
|
3259
|
-
"typebulb": "^0.10.
|
|
3305
|
+
"typebulb": "^0.10.4"
|
|
3260
3306
|
}
|
|
3261
3307
|
}
|
|
3262
3308
|
```
|
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.
|
|
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:
|
|
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)});
|