@rizom/brain 0.1.1-alpha.14 → 0.1.1-alpha.15

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/dist/brain.js CHANGED
@@ -2724,7 +2724,7 @@ ${A.map((x)=>({url:`${f}${x.path}`,lastmod:Q,changefreq:x.path==="/"?"daily":"we
2724
2724
  <script src="https://unpkg.com/@sveltia/cms/dist/sveltia-cms.js"></script>
2725
2725
  </body>
2726
2726
  </html>
2727
- `;async function om2(A,f,Q){let w=A.siteConfig.url??"https://example.com",B=f.list(),x=wr0(w,A.environment);await GO.writeFile(JO(A.outputDir,"robots.txt"),x,"utf-8"),Q.info(`Generated robots.txt for ${A.environment} environment`);let c=Br0(B,w);await GO.writeFile(JO(A.outputDir,"sitemap.xml"),c,"utf-8"),Q.info(`Generated sitemap.xml with ${B.length} URLs`)}async function am2(A,f,Q,w){if(!Q.cms)return;let B=await f.messaging.send("git-sync:get-repo-info",{});if("noop"in B||!B.success||!B.data?.repo){w.warn("CMS enabled but git-sync repo info unavailable \u2014 skipping CMS generation");return}let x=f.entityService.getEntityTypes(),c=cr0({repo:B.data.repo,branch:B.data.branch,...f.siteUrl&&{baseUrl:f.siteUrl},entityTypes:x,getFrontmatterSchema:($)=>f.entities.getEffectiveFrontmatterSchema($),getAdapter:($)=>f.entities.getAdapter($),...Q.entityDisplay&&{entityDisplay:Q.entityDisplay}}),I=JO(A.outputDir,"admin");await GO.mkdir(I,{recursive:!0}),await GO.writeFile(JO(I,"config.yml"),u$(c),"utf-8"),await GO.writeFile(JO(I,"index.html"),Ir0,"utf-8"),w.info("Generated CMS admin page and config.yml")}function $r0(A){let{context:f,routeRegistry:Q,config:w,logger:B}=A;f.messaging.subscribe("site:build:completed",async(x)=>{try{let c=x.payload;return B.info(`Received site:build:completed event for ${c.environment} environment - generating SEO files`),await om2(c,Q,B),await am2(c,f,w,B),{success:!0}}catch(c){return B.error("Failed to generate SEO files",c),{success:!1}}})}B0();HA();var Dr0=X.object({environment:X.enum(["preview","production"]).optional(),outputDir:X.string(),workingDir:X.string().optional(),enableContentGeneration:X.boolean().optional(),siteConfig:X.object({title:X.string(),description:X.string(),url:X.string().optional(),analyticsScript:X.string().optional()}).optional()});HA();class dZA extends CQ{sendMessage;cfg;constructor(A,f,Q){super(A,{schema:Dr0,jobTypeName:"site-build"});this.sendMessage=f;this.cfg=Q}async process(A,f,Q){let w=A.environment??"preview",B=A.enableContentGeneration??!1;try{this.logger.debug("Starting site build job",{jobId:f,environment:w,outputDir:A.outputDir}),await Q.report({progress:0,total:100,message:`Starting site build for ${w} environment`});let x=Q.createSub({scale:{start:10,end:90}}),c=await this.cfg.siteBuilder.build({outputDir:A.outputDir,workingDir:A.workingDir,sharedImagesDir:this.cfg.sharedImagesDir,enableContentGeneration:B,environment:w,cleanBeforeBuild:!0,siteConfig:A.siteConfig??this.cfg.defaultSiteConfig,layouts:this.cfg.layouts,themeCSS:this.cfg.themeCSS,slots:this.cfg.slots,headScripts:this.cfg.getHeadScripts?.(),...this.cfg.staticAssets&&{staticAssets:this.cfg.staticAssets}},x.toCallback());if(await Q.report({progress:100,total:100,message:`Site build completed: ${c.routesBuilt} routes built`}),this.logger.debug("Site build job completed",{jobId:f,environment:w,routesBuilt:c.routesBuilt,success:c.success}),c.success){this.logger.info(`Emitting site:build:completed event for ${w} environment`);let I=w==="preview"?this.cfg.previewUrl??this.cfg.siteUrl:this.cfg.siteUrl;await this.sendMessage("site:build:completed",{outputDir:A.outputDir,environment:w,routesBuilt:c.routesBuilt,siteConfig:{...A.siteConfig??this.cfg.defaultSiteConfig,url:I},generateEntityUrl:($,D)=>s9.getInstance().generateUrl($,D)},{broadcast:!0})}return{success:c.success,routesBuilt:c.routesBuilt,outputDir:A.outputDir,environment:w,...c.errors&&{errors:c.errors},...c.warnings&&{warnings:c.warnings}}}catch(x){throw this.logger.error("Site build job failed",x),x}}summarizeDataForLog(A){return{environment:A.environment,outputDir:A.outputDir}}}HA();B0();var sm2=X.object({slot:X.enum(zJ).optional().default("primary"),limit:X.number().optional()});class oZA{routeRegistry;logger;id="site:navigation";name="Site Navigation DataSource";description="Provides navigation items for site menus";constructor(A,f){this.routeRegistry=A;this.logger=f;this.logger.debug("NavigationDataSource initialized")}async fetch(A,f,Q){let w=sm2.parse(A??{});this.logger.debug("NavigationDataSource fetch called",{params:w});let B=this.routeRegistry.getNavigationItems(w.slot),x=w.limit?B.slice(0,w.limit):B,c=x.map(($)=>({label:$.label,href:$.href}));this.logger.debug("NavigationDataSource returning",{slot:w.slot,itemCount:x.length,items:c});let I={navigation:c};return f.parse(I)}}B0();HA();var tm2=X.object({environment:X.enum(["preview","production"]).optional().describe("Build environment (defaults to production, or preview if configured)")});function Yr0(A,f){return[Tf(A,"build-site","Build a static site from registered routes",tm2,async(Q)=>{return f(Q.environment),{success:!0,message:`Site build requested${Q.environment?` for ${Q.environment}`:""} (debounced)`,data:{}}},{cli:{name:"build"}})]}HA();B0();var ur0=X.object({previewOutputDir:X.string().describe("Output directory for preview builds").default("./dist/site-preview"),productionOutputDir:X.string().describe("Output directory for production builds").default("./dist/site-production"),sharedImagesDir:X.string().describe("Shared directory for optimized images (used by both preview and production)").default("./dist/images"),workingDir:X.string().optional().describe("Working directory for builds").default("./.preact-work"),siteInfo:KD.default({title:"Brain",description:"A knowledge management system"}),themeCSS:X.string().describe("Custom CSS theme overrides to inject into builds").optional(),analyticsScript:X.string().describe("Analytics tracking script to inject into page head (e.g., Cloudflare Web Analytics)").optional(),templates:X.any().optional().describe("Template definitions to register"),routes:X.array(hJ).optional().describe("Routes to register"),layouts:X.record(X.any()).optional().describe("Layout components (at least 'default' required)"),autoRebuild:X.boolean().default(!0).describe("Automatically rebuild site when content changes"),rebuildDebounce:X.number().min(100).describe("Debounce time in ms before triggering site rebuild after content changes").default(5000),entityDisplay:X.record(X.object({label:X.string().describe("Display label for entity type (e.g., 'Essay')"),pluralName:X.string().optional().describe("URL path segment (defaults to label.toLowerCase() + 's')"),layout:X.string().optional().describe("Layout name for this entity type's generated routes (defaults to 'default')"),paginate:X.boolean().optional().describe("Enable pagination for list pages"),pageSize:X.number().optional().describe("Items per page (default: 10)"),navigation:X.object({show:X.boolean().optional().describe("Show in navigation"),slot:X.enum(zJ).optional().describe("Navigation slot (primary or secondary)"),priority:X.number().min(0).max(100).optional().describe("Navigation priority (0-100)")}).optional().describe("Navigation settings for this entity type")})).optional().describe("Display metadata per entity type \u2014 label, plural name, layout, pagination, navigation slot. Consulted when auto-generating routes for active entity plugins."),staticAssets:X.record(X.string()).optional().describe("Static files to write to the output directory at build time. Keys are output paths (e.g. '/canvases/tree.js'), values are file contents as strings. Typically supplied by a SitePackage via text imports."),cms:X.object({}).optional().describe("Sveltia CMS configuration (enables /admin/ CMS, requires git-sync plugin)")});var Ur0={name:"@brains/site-builder-plugin",private:!0,version:"0.0.1",description:"Static site generation plugin for Brain system",type:"module",exports:{".":"./src/index.ts"},scripts:{typecheck:"tsc --noEmit",test:"bun test",lint:"eslint .",clean:"rm -rf dist"},dependencies:{"@brains/plugins":"workspace:*","@brains/site-info":"workspace:*","@brains/ui-library":"workspace:*","@tailwindcss/postcss":"^4.1.13","@tailwindcss/typography":"^0.5.19",postcss:"^8.5.6",preact:"^10.27.2","preact-render-to-string":"^6.3.1",sharp:"^0.34.5",tailwindcss:"^4.1.11"},devDependencies:{"@brains/eslint-config":"workspace:*","@brains/test-utils":"workspace:*","@brains/typescript-config":"workspace:*","@types/bun":"^1.1.14","@types/sharp":"^0.32.0",eslint:"^8.56.0",typescript:"^5.3.3"}};class aZA extends mw{siteBuilder;pluginContext;_routeRegistry;_slotRegistry;profileService;layouts;rebuildManager;headScripts=new Map;get routeRegistry(){if(!this._routeRegistry)throw Error("RouteRegistry not initialized - plugin not registered");return this._routeRegistry}constructor(A={}){let f=A.layouts??{};super("site-builder",Ur0,{...A,layouts:f},ur0);this.layouts=f}async onRegister(A){if(this.pluginContext=A,this._routeRegistry=new SZA(A.logger),this._slotRegistry=new io,A.messaging.subscribe("plugin:site-builder:slot:register",async(f)=>{let{slotName:Q,pluginId:w,render:B,priority:x}=f.payload;return this._slotRegistry?.register(Q,{pluginId:w,render:B,...x!==void 0&&{priority:x}}),{success:!0}}),A.messaging.subscribe("plugin:site-builder:head-script:register",async(f)=>{let{pluginId:Q,script:w}=f.payload;return this.headScripts.set(Q,w),{success:!0}}),A.entities.registerDataSource(new oZA(this._routeRegistry,A.logger.child("NavigationDataSource"))),this.profileService=wx.getInstance(A.entityService,A.logger),A.messaging.subscribe("sync:initial:completed",async()=>{return await this.profileService?.initialize(),this.logger.info("AnchorProfileService initialized"),{success:!0}}),Qr0(A,this._routeRegistry,this.logger),this.config.templates)A.templates.register(this.config.templates);if(this.config.routes)To(this.config.routes,this.id,this.routeRegistry);if(this.siteBuilder=q7.getInstance(A.logger.child("SiteBuilder"),A,this.routeRegistry,this.profileService,this.config.entityDisplay),A.jobs.registerHandler("site-build",new dZA(this.logger.child("SiteBuildJobHandler"),A.messaging.send,{siteBuilder:this.siteBuilder,layouts:this.layouts,defaultSiteConfig:this.config.siteInfo,sharedImagesDir:this.config.sharedImagesDir,siteUrl:A.siteUrl,previewUrl:A.previewUrl,themeCSS:this.config.themeCSS,slots:this._slotRegistry,getHeadScripts:()=>this.getRegisteredHeadScripts(),...this.config.staticAssets&&{staticAssets:this.config.staticAssets}})),this.rebuildManager=new nZA(this.config,A,this.id,this.logger),this.config.autoRebuild)this.logger.debug("Auto-rebuild enabled"),this.rebuildManager.setupAutoRebuild();if(this.config.cms)this._routeRegistry.register({id:"cms-admin",path:"/admin/",title:"Admin",external:!0,navigation:{show:!0,slot:"secondary",label:"Admin",priority:100}});A.messaging.subscribe("entity:updated",async(f)=>{if(f.payload.entityType==="site-info"){let w=await this.getInstructions();if(w)A.registerInstructions(w)}return{success:!0}}),$r0({context:A,routeRegistry:this._routeRegistry,config:this.config,logger:this.logger})}getRegisteredHeadScripts(){return Array.from(this.headScripts.values())}async getTools(){if(!this.pluginContext||!this.rebuildManager)throw Error("Plugin context not initialized");let A=this.rebuildManager;return Yr0(this.id,(f)=>A.requestBuild(f))}async getResources(){let A=this.getContext();return[{uri:"brain://site",name:"Site Info",description:"Site metadata \u2014 title, description, domain, URLs",mimeType:"application/json",handler:async()=>{let f;try{f=await iY(A.entityService)}catch{f={title:"Brain",description:""}}return{contents:[{uri:"brain://site",mimeType:"application/json",text:JSON.stringify({...f,domain:A.domain,siteUrl:A.siteUrl,previewUrl:A.previewUrl},null,2)}]}}},{uri:"site://routes",name:"Site Routes",description:"All registered routes with sections and templates",mimeType:"application/json",handler:async()=>{let f=this.routeRegistry.list();return{contents:[{uri:"site://routes",mimeType:"application/json",text:JSON.stringify(f.map((Q)=>({id:Q.id,path:Q.path,title:Q.title,description:Q.description,sections:Q.sections.map((w)=>({id:w.id,template:w.template}))})),null,2)}]}}},{uri:"site://templates",name:"View Templates",description:"All registered view templates",mimeType:"application/json",handler:async()=>{let f=A.views.list();return{contents:[{uri:"site://templates",mimeType:"application/json",text:JSON.stringify(f.map((Q)=>({name:Q.name,description:Q.description,hasWebRenderer:!!Q.renderers.web})),null,2)}]}}}]}getSiteBuilder(){return this.siteBuilder}getSlotRegistry(){return this._slotRegistry}async getInstructions(){let A=this.getContext();try{let f=await iY(A.entityService);return`## Your Site
2727
+ `;async function om2(A,f,Q){let w=A.siteConfig.url??"https://example.com",B=f.list(),x=wr0(w,A.environment);await GO.writeFile(JO(A.outputDir,"robots.txt"),x,"utf-8"),Q.info(`Generated robots.txt for ${A.environment} environment`);let c=Br0(B,w);await GO.writeFile(JO(A.outputDir,"sitemap.xml"),c,"utf-8"),Q.info(`Generated sitemap.xml with ${B.length} URLs`)}async function am2(A,f,Q,w){if(!Q.cms)return;let B=await f.messaging.send("git-sync:get-repo-info",{});if("noop"in B||!B.success||!B.data?.repo){w.warn("CMS enabled but git-sync repo info unavailable \u2014 skipping CMS generation");return}let x=f.entityService.getEntityTypes(),c=cr0({repo:B.data.repo,branch:B.data.branch,...f.siteUrl&&{baseUrl:f.siteUrl},entityTypes:x,getFrontmatterSchema:($)=>f.entities.getEffectiveFrontmatterSchema($),getAdapter:($)=>f.entities.getAdapter($),...Q.entityDisplay&&{entityDisplay:Q.entityDisplay}}),I=JO(A.outputDir,"admin");await GO.mkdir(I,{recursive:!0}),await GO.writeFile(JO(I,"config.yml"),u$(c),"utf-8"),await GO.writeFile(JO(I,"index.html"),Ir0,"utf-8"),w.info("Generated CMS admin page and config.yml")}function $r0(A){let{context:f,routeRegistry:Q,config:w,logger:B}=A;f.messaging.subscribe("site:build:completed",async(x)=>{try{let c=x.payload;return B.info(`Received site:build:completed event for ${c.environment} environment - generating SEO files`),await om2(c,Q,B),await am2(c,f,w,B),{success:!0}}catch(c){return B.error("Failed to generate SEO files",c),{success:!1}}})}B0();HA();var Dr0=X.object({environment:X.enum(["preview","production"]).optional(),outputDir:X.string(),workingDir:X.string().optional(),enableContentGeneration:X.boolean().optional(),siteConfig:X.object({title:X.string(),description:X.string(),url:X.string().optional(),analyticsScript:X.string().optional()}).optional()});HA();class dZA extends CQ{sendMessage;cfg;constructor(A,f,Q){super(A,{schema:Dr0,jobTypeName:"site-build"});this.sendMessage=f;this.cfg=Q}async process(A,f,Q){let w=A.environment??"preview",B=A.enableContentGeneration??!1;try{this.logger.debug("Starting site build job",{jobId:f,environment:w,outputDir:A.outputDir}),await Q.report({progress:0,total:100,message:`Starting site build for ${w} environment`});let x=Q.createSub({scale:{start:10,end:90}}),c=await this.cfg.siteBuilder.build({outputDir:A.outputDir,workingDir:A.workingDir,sharedImagesDir:this.cfg.sharedImagesDir,enableContentGeneration:B,environment:w,cleanBeforeBuild:!0,siteConfig:A.siteConfig??this.cfg.defaultSiteConfig,layouts:this.cfg.layouts,themeCSS:this.cfg.themeCSS,slots:this.cfg.slots,headScripts:this.cfg.getHeadScripts?.(),...this.cfg.staticAssets&&{staticAssets:this.cfg.staticAssets}},x.toCallback());if(await Q.report({progress:100,total:100,message:`Site build completed: ${c.routesBuilt} routes built`}),this.logger.debug("Site build job completed",{jobId:f,environment:w,routesBuilt:c.routesBuilt,success:c.success}),c.success){this.logger.info(`Emitting site:build:completed event for ${w} environment`);let I=w==="preview"?this.cfg.previewUrl??this.cfg.siteUrl:this.cfg.siteUrl;await this.sendMessage("site:build:completed",{outputDir:A.outputDir,environment:w,routesBuilt:c.routesBuilt,siteConfig:{...A.siteConfig??this.cfg.defaultSiteConfig,url:I},generateEntityUrl:($,D)=>s9.getInstance().generateUrl($,D)},{broadcast:!0})}return{success:c.success,routesBuilt:c.routesBuilt,outputDir:A.outputDir,environment:w,...c.errors&&{errors:c.errors},...c.warnings&&{warnings:c.warnings}}}catch(x){throw this.logger.error("Site build job failed",x),x}}summarizeDataForLog(A){return{environment:A.environment,outputDir:A.outputDir}}}HA();B0();var sm2=X.object({slot:X.enum(zJ).optional().default("primary"),limit:X.number().optional()});class oZA{routeRegistry;logger;id="site:navigation";name="Site Navigation DataSource";description="Provides navigation items for site menus";constructor(A,f){this.routeRegistry=A;this.logger=f;this.logger.debug("NavigationDataSource initialized")}async fetch(A,f,Q){let w=sm2.parse(A??{});this.logger.debug("NavigationDataSource fetch called",{params:w});let B=this.routeRegistry.getNavigationItems(w.slot),x=w.limit?B.slice(0,w.limit):B,c=x.map(($)=>({label:$.label,href:$.href}));this.logger.debug("NavigationDataSource returning",{slot:w.slot,itemCount:x.length,items:c});let I={navigation:c};return f.parse(I)}}B0();HA();var tm2=X.object({environment:X.enum(["preview","production"]).optional().describe("Build environment (defaults to production, or preview if configured)")});function Yr0(A,f){return[Tf(A,"build-site","Build a static site from registered routes",tm2,async(Q)=>{return f(Q.environment),{success:!0,message:`Site build requested${Q.environment?` for ${Q.environment}`:""} (debounced)`,data:{}}},{cli:{name:"build"}})]}HA();B0();var ur0=X.object({previewOutputDir:X.string().describe("Output directory for preview builds").default("./dist/site-preview"),productionOutputDir:X.string().describe("Output directory for production builds").default("./dist/site-production"),sharedImagesDir:X.string().describe("Shared directory for optimized images (used by both preview and production)").default("./dist/images"),workingDir:X.string().optional().describe("Working directory for builds").default("./.preact-work"),siteInfo:KD.default({title:"Brain",description:"A knowledge management system"}),themeCSS:X.string().describe("Custom CSS theme overrides to inject into builds").optional(),analyticsScript:X.string().describe("Analytics tracking script to inject into page head (e.g., Cloudflare Web Analytics)").optional(),templates:X.any().optional().describe("Template definitions to register"),routes:X.array(hJ).optional().describe("Routes to register"),layouts:X.record(X.any()).optional().describe("Layout components (at least 'default' required)"),autoRebuild:X.boolean().default(!0).describe("Automatically rebuild site when content changes"),rebuildDebounce:X.number().min(100).describe("Debounce time in ms before triggering site rebuild after content changes").default(5000),entityDisplay:X.record(X.object({label:X.string().describe("Display label for entity type (e.g., 'Essay')"),pluralName:X.string().optional().describe("URL path segment (defaults to label.toLowerCase() + 's')"),layout:X.string().optional().describe("Layout name for this entity type's generated routes (defaults to 'default')"),paginate:X.boolean().optional().describe("Enable pagination for list pages"),pageSize:X.number().optional().describe("Items per page (default: 10)"),navigation:X.object({show:X.boolean().optional().describe("Show in navigation"),slot:X.enum(zJ).optional().describe("Navigation slot (primary or secondary)"),priority:X.number().min(0).max(100).optional().describe("Navigation priority (0-100)")}).optional().describe("Navigation settings for this entity type")})).optional().describe("Display metadata per entity type \u2014 label, plural name, layout, pagination, navigation slot. Consulted when auto-generating routes for active entity plugins."),staticAssets:X.record(X.string()).optional().describe("Static files to write to the output directory at build time. Keys are output paths (e.g. '/canvases/tree.js'), values are file contents as strings. Typically supplied by a SitePackage via text imports."),cms:X.object({}).optional().describe("Sveltia CMS configuration (enables /admin/ CMS, requires git-sync plugin)")});var Ur0={name:"@brains/site-builder-plugin",private:!0,version:"0.0.1",description:"Static site generation plugin for Brain system",type:"module",exports:{".":"./src/index.ts"},scripts:{typecheck:"tsc --noEmit",test:"bun test",lint:"eslint .",clean:"rm -rf dist"},dependencies:{"@brains/plugins":"workspace:*","@brains/site-info":"workspace:*","@brains/ui-library":"workspace:*","@tailwindcss/postcss":"^4.1.13","@tailwindcss/typography":"^0.5.19",postcss:"^8.5.6",preact:"^10.27.2","preact-render-to-string":"^6.3.1",sharp:"^0.34.5",tailwindcss:"^4.1.11"},devDependencies:{"@brains/eslint-config":"workspace:*","@brains/test-utils":"workspace:*","@brains/typescript-config":"workspace:*","@types/bun":"^1.1.14","@types/sharp":"^0.32.0",eslint:"^8.56.0",typescript:"^5.3.3"}};class aZA extends mw{siteBuilder;pluginContext;_routeRegistry;_slotRegistry;profileService;layouts;rebuildManager;headScripts=new Map;get routeRegistry(){if(!this._routeRegistry)throw Error("RouteRegistry not initialized - plugin not registered");return this._routeRegistry}constructor(A={}){let f=A.layouts??{};super("site-builder",Ur0,{...A,layouts:f},ur0);this.layouts=f}async onRegister(A){if(this.pluginContext=A,this._routeRegistry=new SZA(A.logger),this._slotRegistry=new io,A.messaging.subscribe("plugin:site-builder:slot:register",async(f)=>{let{slotName:Q,pluginId:w,render:B,priority:x}=f.payload;return this._slotRegistry?.register(Q,{pluginId:w,render:B,...x!==void 0&&{priority:x}}),{success:!0}}),A.messaging.subscribe("plugin:site-builder:head-script:register",async(f)=>{let{pluginId:Q,script:w}=f.payload;return this.headScripts.set(Q,w),{success:!0}}),A.entities.registerDataSource(new oZA(this._routeRegistry,A.logger.child("NavigationDataSource"))),this.profileService=wx.getInstance(A.entityService,A.logger),A.messaging.subscribe("sync:initial:completed",async()=>{return await this.profileService?.initialize(),this.logger.info("AnchorProfileService initialized"),{success:!0}}),Qr0(A,this._routeRegistry,this.logger),this.config.templates)A.templates.register(this.config.templates);if(this.config.routes)To(this.config.routes,this.id,this.routeRegistry);if(this.siteBuilder=q7.getInstance(A.logger.child("SiteBuilder"),A,this.routeRegistry,this.profileService,this.config.entityDisplay),A.jobs.registerHandler("site-build",new dZA(this.logger.child("SiteBuildJobHandler"),A.messaging.send,{siteBuilder:this.siteBuilder,layouts:this.layouts,defaultSiteConfig:this.config.siteInfo,sharedImagesDir:this.config.sharedImagesDir,siteUrl:A.siteUrl,previewUrl:A.previewUrl,themeCSS:this.config.themeCSS,slots:this._slotRegistry,getHeadScripts:()=>this.getRegisteredHeadScripts(),...this.config.staticAssets&&{staticAssets:this.config.staticAssets}})),this.rebuildManager=new nZA(this.config,A,this.id,this.logger),this.config.autoRebuild)this.logger.debug("Auto-rebuild enabled"),this.rebuildManager.setupAutoRebuild();if(this.config.cms)this._routeRegistry.register({id:"cms-admin",path:"/admin/",title:"Admin",external:!0,navigation:{show:!1,slot:"secondary",label:"Admin",priority:100}});A.messaging.subscribe("entity:updated",async(f)=>{if(f.payload.entityType==="site-info"){let w=await this.getInstructions();if(w)A.registerInstructions(w)}return{success:!0}}),$r0({context:A,routeRegistry:this._routeRegistry,config:this.config,logger:this.logger})}getRegisteredHeadScripts(){return Array.from(this.headScripts.values())}async getTools(){if(!this.pluginContext||!this.rebuildManager)throw Error("Plugin context not initialized");let A=this.rebuildManager;return Yr0(this.id,(f)=>A.requestBuild(f))}async getResources(){let A=this.getContext();return[{uri:"brain://site",name:"Site Info",description:"Site metadata \u2014 title, description, domain, URLs",mimeType:"application/json",handler:async()=>{let f;try{f=await iY(A.entityService)}catch{f={title:"Brain",description:""}}return{contents:[{uri:"brain://site",mimeType:"application/json",text:JSON.stringify({...f,domain:A.domain,siteUrl:A.siteUrl,previewUrl:A.previewUrl},null,2)}]}}},{uri:"site://routes",name:"Site Routes",description:"All registered routes with sections and templates",mimeType:"application/json",handler:async()=>{let f=this.routeRegistry.list();return{contents:[{uri:"site://routes",mimeType:"application/json",text:JSON.stringify(f.map((Q)=>({id:Q.id,path:Q.path,title:Q.title,description:Q.description,sections:Q.sections.map((w)=>({id:w.id,template:w.template}))})),null,2)}]}}},{uri:"site://templates",name:"View Templates",description:"All registered view templates",mimeType:"application/json",handler:async()=>{let f=A.views.list();return{contents:[{uri:"site://templates",mimeType:"application/json",text:JSON.stringify(f.map((Q)=>({name:Q.name,description:Q.description,hasWebRenderer:!!Q.renderers.web})),null,2)}]}}}]}getSiteBuilder(){return this.siteBuilder}getSlotRegistry(){return this._slotRegistry}async getInstructions(){let A=this.getContext();try{let f=await iY(A.entityService);return`## Your Site
2728
2728
  ${[`**Title:** ${f.title}`,`**Description:** ${f.description}`,A.domain&&`**Domain:** ${A.domain}`,A.siteUrl&&`**URL:** ${A.siteUrl}`].filter(Boolean).join(`
2729
2729
  `)}`}catch{return}}async onShutdown(){this.logger.debug("Shutting down site-builder plugin"),this.rebuildManager?.dispose(),q7.resetInstance(),this.logger.debug("Cleaned up all event subscriptions")}}function uW(A={}){return new aZA(A)}HA();var Al2=KD.extend({navigation:X.object({primary:X.array(X.object({label:X.string(),href:X.string(),priority:X.number()})),secondary:X.array(X.object({label:X.string(),href:X.string(),priority:X.number()}))}),copyright:X.string(),socialLinks:X.array(X.object({platform:X.enum(["github","instagram","linkedin","email","website"]).describe("Social media platform"),url:X.string().describe("Profile or contact URL"),label:X.string().optional().describe("Optional display label")})).optional().describe("Social media links from profile entity")});import{h as fl2,Fragment as Ql2}from"preact";function sZA({name:A,slots:f}){if(!f?.hasSlot(A))return null;let w=f.getSlot(A).map((B)=>B.render());return fl2(Ql2,{},...w)}B0();bD();bO();HA();var tZA=X.object({defaultPrompt:X.string().default("Write a blog post about my recent work and insights"),paginate:X.boolean().default(!0),pageSize:X.number().default(10)});B0();HA();var xl2=X.object({prompt:X.string().optional(),title:X.string().optional(),content:X.string().optional(),excerpt:X.string().optional(),coverImageId:X.string().optional(),seriesName:X.string().optional(),seriesIndex:X.number().optional(),skipAi:X.boolean().optional()}),OKQ=e9.extend({title:X.string().optional(),slug:X.string().optional()});class eZA extends G5{constructor(A,f){super(A,f,{schema:xl2,jobTypeName:"blog-generation",entityType:"post"})}async generate(A,f){let{prompt:Q,coverImageId:w,seriesName:B,seriesIndex:x,skipAi:c}=A,{title:I,content:$,excerpt:D}=A;if(c){if(!I)this.failEarly("Title is required when skipAi is true");$=$??`## Introduction
2730
2730
 
@@ -26115,7 +26115,7 @@ Please report this to https://github.com/markedjs/marked.\`;
26115
26115
  }
26116
26116
  })();
26117
26117
  })();
26118
- `;var Uo0=a0({name:"dashboard",description:"Extensible dashboard with plugin-contributed widgets",schema:Na,requiredPermission:"public",formatter:new Ea,dataSourceId:"dashboard:dashboard",layout:{component:XGA,interactive:uo0}});var Ho0={name:"@brains/dashboard",private:!0,version:"0.1.0",description:"Dashboard plugin with extensible widget system",type:"module",exports:{".":"./src/index.ts"},scripts:{precompile:"bun ../../scripts/compile-hydration.ts",typecheck:"tsc --noEmit",test:"bun test",lint:"eslint . --ext .ts,.tsx","lint:fix":"eslint . --ext .ts,.tsx --fix"},dependencies:{"@brains/plugins":"workspace:*","@brains/ui-library":"workspace:*","@brains/utils":"workspace:*",preact:"^10.27.2"},devDependencies:{"@brains/eslint-config":"workspace:*","@brains/test-utils":"workspace:*","@brains/typescript-config":"workspace:*","@types/bun":"latest","bun-types":"latest",eslint:"^8.56.0",typescript:"^5.3.3"}};var Xn2=X.object({version:X.string().default("1.0.0")}),Zn2=X.object({id:X.string(),pluginId:X.string(),title:X.string(),description:X.string().optional(),priority:X.number().default(50),section:X.enum(["primary","secondary","sidebar"]).default("primary"),rendererName:X.enum(lO),dataProvider:X.function().returns(X.promise(X.unknown()))}),Wn2=X.object({pluginId:X.string(),widgetId:X.string().optional()});class ZGA extends mw{dependencies=["site-builder"];widgetRegistry=null;datasource=null;constructor(A){super("dashboard",Ho0,A??{},Xn2)}async onRegister(A){this.widgetRegistry=new za(this.logger),this.datasource=new ha(this.widgetRegistry,this.logger),A.entities.registerDataSource(this.datasource),A.templates.register({dashboard:Uo0}),await A.messaging.send("plugin:site-builder:route:register",{pluginId:this.id,routes:[{id:"dashboard",path:"/dashboard",title:"System Dashboard",description:"Monitor your Brain system statistics and activity",layout:"default",navigation:{show:!0,label:"Dashboard",slot:"secondary",priority:100},sections:[{id:"main",template:`${this.id}:dashboard`}]}]}),A.messaging.subscribe("dashboard:register-widget",async(f)=>{try{let Q=Zn2.parse(f.payload),w={id:Q.id,pluginId:Q.pluginId,title:Q.title,description:Q.description,priority:Q.priority,section:Q.section,rendererName:Q.rendererName,dataProvider:Q.dataProvider};return this.widgetRegistry?.register(w),this.logger.debug("Widget registered via messaging",{widgetId:Q.id,pluginId:Q.pluginId}),{success:!0}}catch(Q){return this.logger.error("Failed to register widget",{error:v0(Q),payload:f.payload}),{success:!1,error:"Widget registration failed"}}}),A.messaging.subscribe("dashboard:unregister-widget",async(f)=>{let Q=Wn2.parse(f.payload);return this.widgetRegistry?.unregister(Q.pluginId,Q.widgetId),this.logger.debug("Widget unregistered via messaging",{pluginId:Q.pluginId,widgetId:Q.widgetId}),{success:!0}}),this.logger.info("Dashboard plugin registered")}async getTools(){return[]}getWidgetRegistry(){return this.widgetRegistry}}function nO(A){return new ZGA(A)}HA();B0();HA();import{h as Vn2}from"preact";HA();_c();B0();var Pz=X.enum(["draft","queued","published","failed"]),yO=X.object({subject:X.string(),status:Pz,entityIds:X.array(X.string()).optional(),scheduledFor:X.string().datetime().optional(),sentAt:X.string().datetime().optional(),buttondownId:X.string().optional(),sourceEntityType:X.string().optional()}),Ko0=yO.pick({subject:!0,status:!0,entityIds:!0,scheduledFor:!0,sentAt:!0,buttondownId:!0,sourceEntityType:!0}),pO=X2.extend({entityType:X.literal("newsletter"),metadata:Ko0});B0();class Fo0 extends C2{constructor(){super({entityType:"newsletter",schema:pO,frontmatterSchema:yO})}toMarkdown(A){let f=this.extractBody(A.content);return this.buildMarkdown(f,A.metadata)}fromMarkdown(A){let f=this.parseFrontmatter(A);return{entityType:"newsletter",content:A,metadata:f}}}var Xo0=new Fo0;B0();B0();HA();var Gn2=KU.extend({status:X.enum(["draft","queued","published","failed"]).optional()}),Jn2=FU.extend({query:Gn2.optional()});function Zo0(A){try{let{content:f}=H2(A.content,yO);return f}catch{return A.content}}class WGA extends $6{id="newsletter:entities";name="Newsletter Entity DataSource";description="Fetches and transforms newsletter entities for rendering";config={entityType:"newsletter",defaultSort:[{field:"created",direction:"desc"}],defaultLimit:10,lookupField:"id",enableNavigation:!0};constructor(A){super(A);this.logger.debug("NewsletterDataSource initialized")}parseQuery(A){let f=Jn2.parse(A);return{entityType:f.entityType??this.config.entityType,query:f.query??{}}}transformEntity(A){let f=Zo0(A),Q={id:A.id,subject:A.metadata.subject,status:A.metadata.status,excerpt:cU(f,150),created:A.created,url:`/newsletters/${A.id}`};if(A.metadata.sentAt)Q.sentAt=A.metadata.sentAt;return Q}buildListResult(A,f,Q){return{newsletters:A,totalCount:f?.totalItems??A.length,pagination:f}}async fetch(A,f,Q){let{query:w}=this.parseQuery(A),B=Q.entityService;if(w.id)return this.fetchSingleNewsletter(w.id,f,B);let x=w.status,c=x?{filter:{metadata:{status:x}}}:void 0,{items:I,pagination:$}=await this.fetchList(w,B,c);return f.parse(this.buildListResult(I,$,w))}async fetchSingleNewsletter(A,f,Q){let w=await Q.getEntity(this.config.entityType,A);if(!w)throw Error(`Newsletter not found: ${A}`);let B=await this.resolveNavigation(w,Q),x=[];if(w.metadata.entityIds?.length){let $=w.metadata.sourceEntityType??"post";x=(await Promise.all(w.metadata.entityIds.map(async(u)=>{let K=await Q.getEntity($,u);if(K){let F=K.metadata;return{id:u,title:F.title??u,url:`/${$}s/${F.slug??u}`}}return null}))).filter((u)=>u!==null)}let c=Zo0(w),I={id:w.id,subject:w.metadata.subject,status:w.metadata.status,content:c,created:w.created,updated:w.updated,sentAt:w.metadata.sentAt,scheduledFor:w.metadata.scheduledFor,newsletter:w,prevNewsletter:B.prev?{id:B.prev.id,subject:B.prev.subject,url:B.prev.url}:null,nextNewsletter:B.next?{id:B.next.id,subject:B.next.subject,url:B.next.url}:null,sourceEntities:x.length>0?x:void 0};return f.parse(I)}}B0();HA();var vn2=X.object({prompt:X.string().optional().describe("AI generation prompt"),sourceEntityIds:X.array(X.string()).optional().describe("Entity IDs to include in newsletter (e.g., blog posts)"),sourceEntityType:X.enum(["post"]).optional().describe("Type of source entities"),content:X.string().optional().describe("Direct content (skip AI)"),subject:X.string().optional().describe("Newsletter subject (AI-generated if not provided)"),addToQueue:X.boolean().optional().describe("Create as queued (true) or draft (false)")});class GGA extends G5{constructor(A,f){super(A,f,{schema:vn2,jobTypeName:"newsletter:generation",entityType:"newsletter"})}async generate(A,f){let Q=A.addToQueue??!1,{prompt:w,sourceEntityIds:B,sourceEntityType:x}=A,{content:c,subject:I}=A;if(c){if(!I)this.failEarly("Subject is required when providing content directly");await this.reportProgress(f,{progress:50,message:"Using provided content"})}else if(B&&B.length>0){let F=x??"post";await this.reportProgress(f,{progress:10,message:`Fetching ${B.length} source entities`});let J=(await Promise.all(B.map((q)=>this.context.entityService.getEntity(F,q)))).filter((q)=>q!=null);if(J.length===0)this.failEarly(`No source entities found for IDs: ${B.join(", ")}`);await this.reportProgress(f,{progress:30,message:`Generating newsletter from ${J.length} posts`});let k=`Create an engaging newsletter that highlights these blog posts:
26118
+ `;var Uo0=a0({name:"dashboard",description:"Extensible dashboard with plugin-contributed widgets",schema:Na,requiredPermission:"public",formatter:new Ea,dataSourceId:"dashboard:dashboard",layout:{component:XGA,interactive:uo0}});var Ho0={name:"@brains/dashboard",private:!0,version:"0.1.0",description:"Dashboard plugin with extensible widget system",type:"module",exports:{".":"./src/index.ts"},scripts:{precompile:"bun ../../scripts/compile-hydration.ts",typecheck:"tsc --noEmit",test:"bun test",lint:"eslint . --ext .ts,.tsx","lint:fix":"eslint . --ext .ts,.tsx --fix"},dependencies:{"@brains/plugins":"workspace:*","@brains/ui-library":"workspace:*","@brains/utils":"workspace:*",preact:"^10.27.2"},devDependencies:{"@brains/eslint-config":"workspace:*","@brains/test-utils":"workspace:*","@brains/typescript-config":"workspace:*","@types/bun":"latest","bun-types":"latest",eslint:"^8.56.0",typescript:"^5.3.3"}};var Xn2=X.object({version:X.string().default("1.0.0")}),Zn2=X.object({id:X.string(),pluginId:X.string(),title:X.string(),description:X.string().optional(),priority:X.number().default(50),section:X.enum(["primary","secondary","sidebar"]).default("primary"),rendererName:X.enum(lO),dataProvider:X.function().returns(X.promise(X.unknown()))}),Wn2=X.object({pluginId:X.string(),widgetId:X.string().optional()});class ZGA extends mw{dependencies=["site-builder"];widgetRegistry=null;datasource=null;constructor(A){super("dashboard",Ho0,A??{},Xn2)}async onRegister(A){this.widgetRegistry=new za(this.logger),this.datasource=new ha(this.widgetRegistry,this.logger),A.entities.registerDataSource(this.datasource),A.templates.register({dashboard:Uo0}),await A.messaging.send("plugin:site-builder:route:register",{pluginId:this.id,routes:[{id:"dashboard",path:"/dashboard",title:"System Dashboard",description:"Monitor your Brain system statistics and activity",layout:"default",navigation:{show:!1,label:"Dashboard",slot:"secondary",priority:100},sections:[{id:"main",template:`${this.id}:dashboard`}]}]}),A.messaging.subscribe("dashboard:register-widget",async(f)=>{try{let Q=Zn2.parse(f.payload),w={id:Q.id,pluginId:Q.pluginId,title:Q.title,description:Q.description,priority:Q.priority,section:Q.section,rendererName:Q.rendererName,dataProvider:Q.dataProvider};return this.widgetRegistry?.register(w),this.logger.debug("Widget registered via messaging",{widgetId:Q.id,pluginId:Q.pluginId}),{success:!0}}catch(Q){return this.logger.error("Failed to register widget",{error:v0(Q),payload:f.payload}),{success:!1,error:"Widget registration failed"}}}),A.messaging.subscribe("dashboard:unregister-widget",async(f)=>{let Q=Wn2.parse(f.payload);return this.widgetRegistry?.unregister(Q.pluginId,Q.widgetId),this.logger.debug("Widget unregistered via messaging",{pluginId:Q.pluginId,widgetId:Q.widgetId}),{success:!0}}),this.logger.info("Dashboard plugin registered")}async getTools(){return[]}getWidgetRegistry(){return this.widgetRegistry}}function nO(A){return new ZGA(A)}HA();B0();HA();import{h as Vn2}from"preact";HA();_c();B0();var Pz=X.enum(["draft","queued","published","failed"]),yO=X.object({subject:X.string(),status:Pz,entityIds:X.array(X.string()).optional(),scheduledFor:X.string().datetime().optional(),sentAt:X.string().datetime().optional(),buttondownId:X.string().optional(),sourceEntityType:X.string().optional()}),Ko0=yO.pick({subject:!0,status:!0,entityIds:!0,scheduledFor:!0,sentAt:!0,buttondownId:!0,sourceEntityType:!0}),pO=X2.extend({entityType:X.literal("newsletter"),metadata:Ko0});B0();class Fo0 extends C2{constructor(){super({entityType:"newsletter",schema:pO,frontmatterSchema:yO})}toMarkdown(A){let f=this.extractBody(A.content);return this.buildMarkdown(f,A.metadata)}fromMarkdown(A){let f=this.parseFrontmatter(A);return{entityType:"newsletter",content:A,metadata:f}}}var Xo0=new Fo0;B0();B0();HA();var Gn2=KU.extend({status:X.enum(["draft","queued","published","failed"]).optional()}),Jn2=FU.extend({query:Gn2.optional()});function Zo0(A){try{let{content:f}=H2(A.content,yO);return f}catch{return A.content}}class WGA extends $6{id="newsletter:entities";name="Newsletter Entity DataSource";description="Fetches and transforms newsletter entities for rendering";config={entityType:"newsletter",defaultSort:[{field:"created",direction:"desc"}],defaultLimit:10,lookupField:"id",enableNavigation:!0};constructor(A){super(A);this.logger.debug("NewsletterDataSource initialized")}parseQuery(A){let f=Jn2.parse(A);return{entityType:f.entityType??this.config.entityType,query:f.query??{}}}transformEntity(A){let f=Zo0(A),Q={id:A.id,subject:A.metadata.subject,status:A.metadata.status,excerpt:cU(f,150),created:A.created,url:`/newsletters/${A.id}`};if(A.metadata.sentAt)Q.sentAt=A.metadata.sentAt;return Q}buildListResult(A,f,Q){return{newsletters:A,totalCount:f?.totalItems??A.length,pagination:f}}async fetch(A,f,Q){let{query:w}=this.parseQuery(A),B=Q.entityService;if(w.id)return this.fetchSingleNewsletter(w.id,f,B);let x=w.status,c=x?{filter:{metadata:{status:x}}}:void 0,{items:I,pagination:$}=await this.fetchList(w,B,c);return f.parse(this.buildListResult(I,$,w))}async fetchSingleNewsletter(A,f,Q){let w=await Q.getEntity(this.config.entityType,A);if(!w)throw Error(`Newsletter not found: ${A}`);let B=await this.resolveNavigation(w,Q),x=[];if(w.metadata.entityIds?.length){let $=w.metadata.sourceEntityType??"post";x=(await Promise.all(w.metadata.entityIds.map(async(u)=>{let K=await Q.getEntity($,u);if(K){let F=K.metadata;return{id:u,title:F.title??u,url:`/${$}s/${F.slug??u}`}}return null}))).filter((u)=>u!==null)}let c=Zo0(w),I={id:w.id,subject:w.metadata.subject,status:w.metadata.status,content:c,created:w.created,updated:w.updated,sentAt:w.metadata.sentAt,scheduledFor:w.metadata.scheduledFor,newsletter:w,prevNewsletter:B.prev?{id:B.prev.id,subject:B.prev.subject,url:B.prev.url}:null,nextNewsletter:B.next?{id:B.next.id,subject:B.next.subject,url:B.next.url}:null,sourceEntities:x.length>0?x:void 0};return f.parse(I)}}B0();HA();var vn2=X.object({prompt:X.string().optional().describe("AI generation prompt"),sourceEntityIds:X.array(X.string()).optional().describe("Entity IDs to include in newsletter (e.g., blog posts)"),sourceEntityType:X.enum(["post"]).optional().describe("Type of source entities"),content:X.string().optional().describe("Direct content (skip AI)"),subject:X.string().optional().describe("Newsletter subject (AI-generated if not provided)"),addToQueue:X.boolean().optional().describe("Create as queued (true) or draft (false)")});class GGA extends G5{constructor(A,f){super(A,f,{schema:vn2,jobTypeName:"newsletter:generation",entityType:"newsletter"})}async generate(A,f){let Q=A.addToQueue??!1,{prompt:w,sourceEntityIds:B,sourceEntityType:x}=A,{content:c,subject:I}=A;if(c){if(!I)this.failEarly("Subject is required when providing content directly");await this.reportProgress(f,{progress:50,message:"Using provided content"})}else if(B&&B.length>0){let F=x??"post";await this.reportProgress(f,{progress:10,message:`Fetching ${B.length} source entities`});let J=(await Promise.all(B.map((q)=>this.context.entityService.getEntity(F,q)))).filter((q)=>q!=null);if(J.length===0)this.failEarly(`No source entities found for IDs: ${B.join(", ")}`);await this.reportProgress(f,{progress:30,message:`Generating newsletter from ${J.length} posts`});let k=`Create an engaging newsletter that highlights these blog posts:
26119
26119
 
26120
26120
  ${J.map((q)=>`## ${q.metadata.title}
26121
26121
 
@@ -29625,7 +29625,7 @@ footer.bg-footer button:not(.bg-theme-toggle) { color: var(--palette-black); }
29625
29625
  footer.bg-footer button:not(.bg-theme-toggle):hover { color: var(--palette-neon-green); }
29626
29626
  footer.bg-footer .bg-theme-toggle { background-color: transparent; border: 1px solid var(--palette-gray-700); color: var(--palette-gray-400); }
29627
29627
  footer.bg-footer .bg-theme-toggle:hover { background-color: transparent; border-color: var(--palette-neon-green); color: var(--palette-neon-green); }
29628
- `;var Es0=Ns0;import{readFileSync as xa2}from"fs";import{join as ca2}from"path";import{execSync as Ia2}from"child_process";import{parseArgs as ap2}from"util";var sp2={model:{type:"string"},domain:{type:"string"},"content-repo":{type:"string"},backend:{type:"string"},"push-to":{type:"string"},"ai-api-key":{type:"string"},"no-interactive":{type:"boolean"},preview:{type:"boolean"},deploy:{type:"boolean"},regen:{type:"boolean"},all:{type:"boolean"},only:{type:"string"},"dry-run":{type:"boolean"},remote:{type:"string"},token:{type:"string"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}};function eY(A,f){let Q=A[f];return typeof Q==="string"?Q:void 0}function tz(A,f){let Q=A[f];return typeof Q==="boolean"?Q:void 0}function Ls0(A){let{values:f,positionals:Q}=ap2({args:A,options:sp2,allowPositionals:!0,strict:!1});if(f.help)return{command:"help",args:[],flags:{help:!0}};if(f.version)return{command:"version",args:[],flags:{version:!0}};return{command:Q[0]??"help",args:Q.slice(1),flags:{model:eY(f,"model"),domain:eY(f,"domain"),"content-repo":eY(f,"content-repo"),backend:eY(f,"backend"),"push-to":eY(f,"push-to"),"ai-api-key":eY(f,"ai-api-key"),"no-interactive":tz(f,"no-interactive"),preview:tz(f,"preview"),deploy:tz(f,"deploy"),regen:tz(f,"regen"),all:tz(f,"all"),only:eY(f,"only"),"dry-run":tz(f,"dry-run"),remote:eY(f,"remote"),token:eY(f,"token")}}}import{mkdirSync as no2}from"fs";import{join as yo2}from"path";import{spawn as po2,execSync as ro2}from"child_process";var Us={name:"@rizom/brain",version:"0.1.1-alpha.14",description:"Brain runtime + CLI \u2014 scaffold, run, and manage AI brain instances",type:"module",bin:{brain:"./dist/brain.js"},exports:{"./cli":"./dist/brain.js","./site":{types:"./dist/site.d.ts",import:"./dist/site.js"},"./themes":{types:"./dist/themes.d.ts",import:"./dist/themes.js"}},files:["dist"],scripts:{build:"bun scripts/build.ts",prepublishOnly:"bun scripts/build.ts",typecheck:"tsc --noEmit",test:"bun test",lint:"eslint . --ext .ts"},dependencies:{"@clack/prompts":"^0.11.0","@modelcontextprotocol/sdk":"^1.24.0","@tailwindcss/postcss":"^4.1.13","@tailwindcss/typography":"^0.5.19",postcss:"^8.5.6",preact:"^10.27.2","preact-render-to-string":"^6.3.1",tailwindcss:"^4.1.11"},optionalDependencies:{"@libsql/client":"^0.15.7","@tailwindcss/oxide":"^4.1.4","better-sqlite3":"^11.8.1",lightningcss:"^1.29.2","react-devtools-core":"^6.1.1",sharp:"^0.34.5"},devDependencies:{"@brains/app":"workspace:*","@brains/eslint-config":"workspace:*","@brains/mcp-service":"workspace:*","@brains/plugins":"workspace:*","@brains/ranger":"workspace:*","@brains/relay":"workspace:*","@brains/rover":"workspace:*","@brains/site-composition":"workspace:*","@brains/site-default":"workspace:*","@brains/site-personal":"workspace:*","@brains/site-professional":"workspace:*","@brains/site-rizom":"workspace:*","@brains/site-yeehaa":"workspace:*","@brains/theme-brutalist":"workspace:*","@brains/theme-default":"workspace:*","@brains/theme-rizom":"workspace:*","@brains/typescript-config":"workspace:*","@brains/utils":"workspace:*","@types/bun":"latest",typescript:"^5.3.3"},publishConfig:{access:"public"},repository:{type:"git",url:"https://github.com/rizom-ai/brains.git",directory:"packages/brain-cli"},license:"Apache-2.0",author:"Yeehaa <yeehaa@rizom.ai> (https://rizom.ai)",homepage:"https://github.com/rizom-ai/brains/tree/main/packages/brain-cli#readme",bugs:"https://github.com/rizom-ai/brains/issues",engines:{bun:">=1.3.3"},keywords:["brain","ai","cli","mcp","agent","personal-ai","knowledge-management"]};import{mkdirSync as Vs0,writeFileSync as Ks,chmodSync as Fs,existsSync as Ms0,readFileSync as Os0}from"fs";import{basename as FJA,dirname as js0,join as E8,resolve as Zr2}from"path";Ij();import{existsSync as Cs0,readFileSync as wr2}from"fs";import{dirname as Br2,join as qs0}from"path";var Hs={rover:`# This env file uses @env-spec - see https://varlock.dev/env-spec for more info
29628
+ `;var Es0=Ns0;import{readFileSync as xa2}from"fs";import{join as ca2}from"path";import{execSync as Ia2}from"child_process";import{parseArgs as ap2}from"util";var sp2={model:{type:"string"},domain:{type:"string"},"content-repo":{type:"string"},backend:{type:"string"},"push-to":{type:"string"},"ai-api-key":{type:"string"},"no-interactive":{type:"boolean"},preview:{type:"boolean"},deploy:{type:"boolean"},regen:{type:"boolean"},all:{type:"boolean"},only:{type:"string"},"dry-run":{type:"boolean"},remote:{type:"string"},token:{type:"string"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}};function eY(A,f){let Q=A[f];return typeof Q==="string"?Q:void 0}function tz(A,f){let Q=A[f];return typeof Q==="boolean"?Q:void 0}function Ls0(A){let{values:f,positionals:Q}=ap2({args:A,options:sp2,allowPositionals:!0,strict:!1});if(f.help)return{command:"help",args:[],flags:{help:!0}};if(f.version)return{command:"version",args:[],flags:{version:!0}};return{command:Q[0]??"help",args:Q.slice(1),flags:{model:eY(f,"model"),domain:eY(f,"domain"),"content-repo":eY(f,"content-repo"),backend:eY(f,"backend"),"push-to":eY(f,"push-to"),"ai-api-key":eY(f,"ai-api-key"),"no-interactive":tz(f,"no-interactive"),preview:tz(f,"preview"),deploy:tz(f,"deploy"),regen:tz(f,"regen"),all:tz(f,"all"),only:eY(f,"only"),"dry-run":tz(f,"dry-run"),remote:eY(f,"remote"),token:eY(f,"token")}}}import{mkdirSync as no2}from"fs";import{join as yo2}from"path";import{spawn as po2,execSync as ro2}from"child_process";var Us={name:"@rizom/brain",version:"0.1.1-alpha.15",description:"Brain runtime + CLI \u2014 scaffold, run, and manage AI brain instances",type:"module",bin:{brain:"./dist/brain.js"},exports:{"./cli":"./dist/brain.js","./site":{types:"./dist/site.d.ts",import:"./dist/site.js"},"./themes":{types:"./dist/themes.d.ts",import:"./dist/themes.js"}},files:["dist"],scripts:{build:"bun scripts/build.ts",prepublishOnly:"bun scripts/build.ts",typecheck:"tsc --noEmit",test:"bun test",lint:"eslint . --ext .ts"},dependencies:{"@clack/prompts":"^0.11.0","@modelcontextprotocol/sdk":"^1.24.0","@tailwindcss/postcss":"^4.1.13","@tailwindcss/typography":"^0.5.19",postcss:"^8.5.6",preact:"^10.27.2","preact-render-to-string":"^6.3.1",tailwindcss:"^4.1.11"},optionalDependencies:{"@libsql/client":"^0.15.7","@tailwindcss/oxide":"^4.1.4","better-sqlite3":"^11.8.1",lightningcss:"^1.29.2","react-devtools-core":"^6.1.1",sharp:"^0.34.5"},devDependencies:{"@brains/app":"workspace:*","@brains/eslint-config":"workspace:*","@brains/mcp-service":"workspace:*","@brains/plugins":"workspace:*","@brains/ranger":"workspace:*","@brains/relay":"workspace:*","@brains/rover":"workspace:*","@brains/site-composition":"workspace:*","@brains/site-default":"workspace:*","@brains/site-personal":"workspace:*","@brains/site-professional":"workspace:*","@brains/site-rizom":"workspace:*","@brains/site-yeehaa":"workspace:*","@brains/theme-brutalist":"workspace:*","@brains/theme-default":"workspace:*","@brains/theme-rizom":"workspace:*","@brains/typescript-config":"workspace:*","@brains/utils":"workspace:*","@types/bun":"latest",typescript:"^5.3.3"},publishConfig:{access:"public"},repository:{type:"git",url:"https://github.com/rizom-ai/brains.git",directory:"packages/brain-cli"},license:"Apache-2.0",author:"Yeehaa <yeehaa@rizom.ai> (https://rizom.ai)",homepage:"https://github.com/rizom-ai/brains/tree/main/packages/brain-cli#readme",bugs:"https://github.com/rizom-ai/brains/issues",engines:{bun:">=1.3.3"},keywords:["brain","ai","cli","mcp","agent","personal-ai","knowledge-management"]};import{mkdirSync as Vs0,writeFileSync as Ks,chmodSync as Fs,existsSync as Ms0,readFileSync as Os0}from"fs";import{basename as FJA,dirname as js0,join as E8,resolve as Zr2}from"path";Ij();import{existsSync as Cs0,readFileSync as wr2}from"fs";import{dirname as Br2,join as qs0}from"path";var Hs={rover:`# This env file uses @env-spec - see https://varlock.dev/env-spec for more info
29629
29629
  #
29630
29630
  # @defaultRequired=false @defaultSensitive=false
29631
29631
  # ----------
package/dist/site.js.map CHANGED
@@ -536,7 +536,7 @@
536
536
  "import type { EntityPluginContext } from \"@brains/plugins\";\nimport { parseMarkdownWithFrontmatter } from \"@brains/plugins\";\nimport type { Logger } from \"@brains/utils\";\nimport type { SiteBuildCompletedPayload } from \"@brains/site-builder-plugin\";\nimport type { BlogPost } from \"../schemas/blog-post\";\nimport { blogPostFrontmatterSchema } from \"../schemas/blog-post\";\nimport type { BlogPostWithData } from \"../datasources/blog-datasource\";\nimport { generateRSSFeed } from \"../rss/feed-generator\";\nimport { promises as fs } from \"fs\";\nimport { join } from \"path\";\n\nexport function subscribeToSiteBuildCompleted(\n context: EntityPluginContext,\n logger: Logger,\n): void {\n context.messaging.subscribe<SiteBuildCompletedPayload, { success: boolean }>(\n \"site:build:completed\",\n async (message) => {\n try {\n const payload = message.payload;\n\n logger.info(\n `Received site:build:completed event for ${payload.environment} environment`,\n );\n\n await generateRSSAfterBuild(context, logger, payload);\n } catch (error) {\n logger.error(\"Failed to generate RSS feed\", error);\n }\n return { success: true };\n },\n );\n}\n\nasync function generateRSSAfterBuild(\n context: EntityPluginContext,\n logger: Logger,\n payload: SiteBuildCompletedPayload,\n): Promise<void> {\n const isPreview = payload.environment === \"preview\";\n logger.info(\n `Auto-generating RSS feed after site build (${isPreview ? \"all posts\" : \"published only\"})`,\n );\n\n const allPosts: BlogPost[] = await context.entityService.listEntities(\n \"post\",\n { limit: 1000 },\n );\n\n const filteredPosts: BlogPostWithData[] = allPosts\n .filter(\n (p) =>\n isPreview ||\n (p.metadata.status === \"published\" && p.metadata.publishedAt),\n )\n .map((entity) => {\n const parsed = parseMarkdownWithFrontmatter(\n entity.content,\n blogPostFrontmatterSchema,\n );\n return {\n ...entity,\n frontmatter: parsed.metadata,\n body: parsed.content,\n url: payload.generateEntityUrl(\"post\", entity.metadata.slug),\n };\n });\n\n if (filteredPosts.length === 0) {\n logger.info(\n `No ${isPreview ? \"\" : \"published \"}posts found, skipping RSS generation`,\n );\n return;\n }\n\n const siteUrl = payload.siteConfig.url ?? \"https://example.com\";\n const siteTitle = payload.siteConfig.title ?? \"Blog\";\n const siteDescription = payload.siteConfig.description ?? \"Latest blog posts\";\n\n const xml = generateRSSFeed(filteredPosts, {\n title: siteTitle,\n description: siteDescription,\n link: siteUrl,\n language: \"en-us\",\n includeAllPosts: isPreview,\n });\n\n const feedPath = join(payload.outputDir, \"feed.xml\");\n await fs.writeFile(feedPath, xml, \"utf-8\");\n\n logger.info(\n `RSS feed generated successfully with ${filteredPosts.length} posts at ${feedPath}`,\n );\n}\n",
537
537
  "import type { EntityPluginContext } from \"@brains/plugins\";\nimport { z } from \"@brains/utils\";\n\nconst generatePostInputSchema = z.object({\n prompt: z.string(),\n seriesName: z.string().optional(),\n});\n\nconst generateExcerptInputSchema = z.object({\n title: z.string(),\n content: z.string(),\n});\n\nexport function registerEvalHandlers(context: EntityPluginContext): void {\n context.eval.registerHandler(\"generatePost\", async (input: unknown) => {\n const parsed = generatePostInputSchema.parse(input);\n const generationPrompt = `${parsed.prompt}${parsed.seriesName ? `\\n\\nNote: This is part of a series called \"${parsed.seriesName}\".` : \"\"}`;\n\n return context.ai.generate<{\n title: string;\n content: string;\n excerpt: string;\n }>({\n prompt: generationPrompt,\n templateName: \"blog:generation\",\n });\n });\n\n context.eval.registerHandler(\"generateExcerpt\", async (input: unknown) => {\n const parsed = generateExcerptInputSchema.parse(input);\n\n return context.ai.generate<{\n excerpt: string;\n }>({\n prompt: `Title: ${parsed.title}\\n\\nContent:\\n${parsed.content}`,\n templateName: \"blog:excerpt\",\n });\n });\n}\n",
538
538
  "export { BlogPlugin, blogPlugin } from \"./plugin\";\nexport { blogConfigSchema, type BlogConfig } from \"./config\";\nexport {\n blogPostSchema,\n blogPostWithDataSchema,\n enrichedBlogPostSchema,\n blogPostFrontmatterSchema,\n type BlogPost,\n type BlogPostWithData,\n type EnrichedBlogPost,\n type BlogPostFrontmatter,\n} from \"./schemas/blog-post\";\nexport { blogPostAdapter, BlogPostAdapter } from \"./adapters/blog-post-adapter\";\nexport { parsePostData } from \"./datasources/parse-helpers\";\nexport { BlogListTemplate, type BlogListProps } from \"./templates/blog-list\";\nexport { BlogPostTemplate, type BlogPostProps } from \"./templates/blog-post\";\n",
539
- "import type {\n Plugin,\n Tool,\n Resource,\n ServicePluginContext,\n} from \"@brains/plugins\";\nimport { ServicePlugin, AnchorProfileService } from \"@brains/plugins\";\nimport { SiteBuilder } from \"./lib/site-builder\";\nimport { RouteRegistry } from \"./lib/route-registry\";\nimport { UISlotRegistry, type SlotRegistration } from \"./lib/ui-slot-registry\";\nimport { RebuildManager } from \"./lib/auto-rebuild\";\nimport { setupRouteHandlers } from \"./lib/route-handlers\";\nimport { registerConfigRoutes } from \"./lib/route-helpers\";\nimport { subscribeBuildCompleted } from \"./lib/seo-file-handler\";\nimport { SiteBuildJobHandler } from \"./handlers/siteBuildJobHandler\";\nimport { NavigationDataSource } from \"./datasources/navigation-datasource\";\nimport { fetchSiteInfo } from \"@brains/site-info\";\nimport { createSiteBuilderTools } from \"./tools/index\";\nimport type { SiteBuilderConfig, LayoutComponent } from \"./config\";\nimport { siteBuilderConfigSchema } from \"./config\";\n\nimport packageJson from \"../package.json\";\n\n/**\n * Site Builder Plugin\n * Provides static site generation capabilities\n */\nexport class SiteBuilderPlugin extends ServicePlugin<SiteBuilderConfig> {\n private siteBuilder?: SiteBuilder;\n private pluginContext?: ServicePluginContext;\n private _routeRegistry?: RouteRegistry;\n private _slotRegistry?: UISlotRegistry;\n private profileService?: AnchorProfileService;\n private layouts: Record<string, LayoutComponent>;\n private rebuildManager?: RebuildManager;\n private headScripts = new Map<string, string>();\n\n private get routeRegistry(): RouteRegistry {\n if (!this._routeRegistry) {\n throw new Error(\"RouteRegistry not initialized - plugin not registered\");\n }\n return this._routeRegistry;\n }\n\n constructor(config: Partial<SiteBuilderConfig> = {}) {\n const layouts = config.layouts ?? {};\n super(\n \"site-builder\",\n packageJson,\n {\n ...config,\n layouts,\n },\n siteBuilderConfigSchema,\n );\n this.layouts = layouts;\n }\n\n protected override async onRegister(\n context: ServicePluginContext,\n ): Promise<void> {\n this.pluginContext = context;\n\n // Initialize registries\n this._routeRegistry = new RouteRegistry(context.logger);\n this._slotRegistry = new UISlotRegistry();\n\n // Subscribe to slot registration messages from other plugins\n context.messaging.subscribe<\n SlotRegistration & { slotName: string },\n { success: boolean }\n >(\"plugin:site-builder:slot:register\", async (message) => {\n const { slotName, pluginId, render, priority } = message.payload;\n this._slotRegistry?.register(slotName, {\n pluginId,\n render,\n ...(priority !== undefined && { priority }),\n });\n return { success: true };\n });\n\n // Subscribe to head script registration messages from other plugins\n context.messaging.subscribe<\n { pluginId: string; script: string },\n { success: boolean }\n >(\"plugin:site-builder:head-script:register\", async (message) => {\n const { pluginId, script } = message.payload;\n // Use pluginId as key so re-registration replaces (no duplicates)\n this.headScripts.set(pluginId, script);\n return { success: true };\n });\n\n // Register data sources\n context.entities.registerDataSource(\n new NavigationDataSource(\n this._routeRegistry,\n context.logger.child(\"NavigationDataSource\"),\n ),\n );\n\n // Site-info entity type + datasource registered by SiteInfoPlugin (entities/site-info)\n this.profileService = AnchorProfileService.getInstance(\n context.entityService,\n context.logger,\n );\n\n context.messaging.subscribe(\"sync:initial:completed\", async () => {\n await this.profileService?.initialize();\n this.logger.info(\"AnchorProfileService initialized\");\n return { success: true };\n });\n\n // Wire up route message handlers and register config routes\n setupRouteHandlers(context, this._routeRegistry, this.logger);\n\n if (this.config.templates) {\n context.templates.register(this.config.templates);\n }\n\n if (this.config.routes) {\n registerConfigRoutes(this.config.routes, this.id, this.routeRegistry);\n }\n\n // Initialize site builder\n this.siteBuilder = SiteBuilder.getInstance(\n context.logger.child(\"SiteBuilder\"),\n context,\n this.routeRegistry,\n this.profileService,\n this.config.entityDisplay,\n );\n\n // Register site-build job handler\n context.jobs.registerHandler(\n \"site-build\",\n new SiteBuildJobHandler(\n this.logger.child(\"SiteBuildJobHandler\"),\n context.messaging.send,\n {\n siteBuilder: this.siteBuilder,\n layouts: this.layouts,\n defaultSiteConfig: this.config.siteInfo,\n sharedImagesDir: this.config.sharedImagesDir,\n siteUrl: context.siteUrl,\n previewUrl: context.previewUrl,\n themeCSS: this.config.themeCSS,\n slots: this._slotRegistry,\n getHeadScripts: (): string[] => this.getRegisteredHeadScripts(),\n ...(this.config.staticAssets && {\n staticAssets: this.config.staticAssets,\n }),\n },\n ),\n );\n\n // Set up rebuild manager (handles debounced builds and auto-rebuild)\n this.rebuildManager = new RebuildManager(\n this.config,\n context,\n this.id,\n this.logger,\n );\n\n if (this.config.autoRebuild) {\n this.logger.debug(\"Auto-rebuild enabled\");\n this.rebuildManager.setupAutoRebuild();\n }\n\n // Register CMS admin nav link if CMS is enabled\n if (this.config.cms) {\n this._routeRegistry.register({\n id: \"cms-admin\",\n path: \"/admin/\",\n title: \"Admin\",\n external: true,\n navigation: {\n show: true,\n slot: \"secondary\",\n label: \"Admin\",\n priority: 100,\n },\n });\n }\n\n // Re-register instructions when site-info changes so agent prompt stays fresh\n context.messaging.subscribe(\"entity:updated\", async (message) => {\n const payload = message.payload as {\n entityType: string;\n entityId: string;\n };\n if (payload.entityType === \"site-info\") {\n const instructions = await this.getInstructions();\n if (instructions) {\n context.registerInstructions(instructions);\n }\n }\n return { success: true };\n });\n\n // Subscribe to build-completed for SEO + CMS file generation\n subscribeBuildCompleted({\n context,\n routeRegistry: this._routeRegistry,\n config: this.config,\n logger: this.logger,\n });\n }\n\n /**\n * Get all head scripts registered by other plugins.\n * Used by the build pipeline to inject scripts into the HTML <head>.\n */\n public getRegisteredHeadScripts(): string[] {\n return Array.from(this.headScripts.values());\n }\n\n protected override async getTools(): Promise<Tool[]> {\n if (!this.pluginContext || !this.rebuildManager) {\n throw new Error(\"Plugin context not initialized\");\n }\n\n const rebuildManager = this.rebuildManager;\n return createSiteBuilderTools(this.id, (env) =>\n rebuildManager.requestBuild(env),\n );\n }\n\n protected override async getResources(): Promise<Resource[]> {\n const context = this.getContext();\n return [\n {\n uri: \"brain://site\",\n name: \"Site Info\",\n description: \"Site metadata — title, description, domain, URLs\",\n mimeType: \"application/json\",\n handler: async (): Promise<{\n contents: Array<{ uri: string; mimeType: string; text: string }>;\n }> => {\n let siteInfo;\n try {\n siteInfo = await fetchSiteInfo(context.entityService);\n } catch {\n siteInfo = { title: \"Brain\", description: \"\" };\n }\n return {\n contents: [\n {\n uri: \"brain://site\",\n mimeType: \"application/json\",\n text: JSON.stringify(\n {\n ...siteInfo,\n domain: context.domain,\n siteUrl: context.siteUrl,\n previewUrl: context.previewUrl,\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n },\n {\n uri: \"site://routes\",\n name: \"Site Routes\",\n description: \"All registered routes with sections and templates\",\n mimeType: \"application/json\",\n handler: async (): Promise<{\n contents: Array<{ uri: string; mimeType: string; text: string }>;\n }> => {\n const routes = this.routeRegistry.list();\n return {\n contents: [\n {\n uri: \"site://routes\",\n mimeType: \"application/json\",\n text: JSON.stringify(\n routes.map((route) => ({\n id: route.id,\n path: route.path,\n title: route.title,\n description: route.description,\n sections: route.sections.map((s) => ({\n id: s.id,\n template: s.template,\n })),\n })),\n null,\n 2,\n ),\n },\n ],\n };\n },\n },\n {\n uri: \"site://templates\",\n name: \"View Templates\",\n description: \"All registered view templates\",\n mimeType: \"application/json\",\n handler: async (): Promise<{\n contents: Array<{ uri: string; mimeType: string; text: string }>;\n }> => {\n const templates = context.views.list();\n return {\n contents: [\n {\n uri: \"site://templates\",\n mimeType: \"application/json\",\n text: JSON.stringify(\n templates.map((t) => ({\n name: t.name,\n description: t.description,\n hasWebRenderer: !!t.renderers.web,\n })),\n null,\n 2,\n ),\n },\n ],\n };\n },\n },\n ];\n }\n\n public getSiteBuilder(): SiteBuilder | undefined {\n return this.siteBuilder;\n }\n\n public getSlotRegistry(): UISlotRegistry | undefined {\n return this._slotRegistry;\n }\n\n protected override async getInstructions(): Promise<string | undefined> {\n const context = this.getContext();\n try {\n const siteInfo = await fetchSiteInfo(context.entityService);\n const parts = [\n `**Title:** ${siteInfo.title}`,\n `**Description:** ${siteInfo.description}`,\n context.domain && `**Domain:** ${context.domain}`,\n context.siteUrl && `**URL:** ${context.siteUrl}`,\n ].filter(Boolean);\n return `## Your Site\\n${parts.join(\"\\n\")}`;\n } catch {\n return undefined;\n }\n }\n\n protected override async onShutdown(): Promise<void> {\n this.logger.debug(\"Shutting down site-builder plugin\");\n this.rebuildManager?.dispose();\n SiteBuilder.resetInstance();\n this.logger.debug(\"Cleaned up all event subscriptions\");\n }\n}\n\n/**\n * Factory function to create the plugin\n */\nexport function siteBuilderPlugin(\n config: Partial<SiteBuilderConfig> = {},\n): Plugin {\n return new SiteBuilderPlugin(config);\n}\n",
539
+ "import type {\n Plugin,\n Tool,\n Resource,\n ServicePluginContext,\n} from \"@brains/plugins\";\nimport { ServicePlugin, AnchorProfileService } from \"@brains/plugins\";\nimport { SiteBuilder } from \"./lib/site-builder\";\nimport { RouteRegistry } from \"./lib/route-registry\";\nimport { UISlotRegistry, type SlotRegistration } from \"./lib/ui-slot-registry\";\nimport { RebuildManager } from \"./lib/auto-rebuild\";\nimport { setupRouteHandlers } from \"./lib/route-handlers\";\nimport { registerConfigRoutes } from \"./lib/route-helpers\";\nimport { subscribeBuildCompleted } from \"./lib/seo-file-handler\";\nimport { SiteBuildJobHandler } from \"./handlers/siteBuildJobHandler\";\nimport { NavigationDataSource } from \"./datasources/navigation-datasource\";\nimport { fetchSiteInfo } from \"@brains/site-info\";\nimport { createSiteBuilderTools } from \"./tools/index\";\nimport type { SiteBuilderConfig, LayoutComponent } from \"./config\";\nimport { siteBuilderConfigSchema } from \"./config\";\n\nimport packageJson from \"../package.json\";\n\n/**\n * Site Builder Plugin\n * Provides static site generation capabilities\n */\nexport class SiteBuilderPlugin extends ServicePlugin<SiteBuilderConfig> {\n private siteBuilder?: SiteBuilder;\n private pluginContext?: ServicePluginContext;\n private _routeRegistry?: RouteRegistry;\n private _slotRegistry?: UISlotRegistry;\n private profileService?: AnchorProfileService;\n private layouts: Record<string, LayoutComponent>;\n private rebuildManager?: RebuildManager;\n private headScripts = new Map<string, string>();\n\n private get routeRegistry(): RouteRegistry {\n if (!this._routeRegistry) {\n throw new Error(\"RouteRegistry not initialized - plugin not registered\");\n }\n return this._routeRegistry;\n }\n\n constructor(config: Partial<SiteBuilderConfig> = {}) {\n const layouts = config.layouts ?? {};\n super(\n \"site-builder\",\n packageJson,\n {\n ...config,\n layouts,\n },\n siteBuilderConfigSchema,\n );\n this.layouts = layouts;\n }\n\n protected override async onRegister(\n context: ServicePluginContext,\n ): Promise<void> {\n this.pluginContext = context;\n\n // Initialize registries\n this._routeRegistry = new RouteRegistry(context.logger);\n this._slotRegistry = new UISlotRegistry();\n\n // Subscribe to slot registration messages from other plugins\n context.messaging.subscribe<\n SlotRegistration & { slotName: string },\n { success: boolean }\n >(\"plugin:site-builder:slot:register\", async (message) => {\n const { slotName, pluginId, render, priority } = message.payload;\n this._slotRegistry?.register(slotName, {\n pluginId,\n render,\n ...(priority !== undefined && { priority }),\n });\n return { success: true };\n });\n\n // Subscribe to head script registration messages from other plugins\n context.messaging.subscribe<\n { pluginId: string; script: string },\n { success: boolean }\n >(\"plugin:site-builder:head-script:register\", async (message) => {\n const { pluginId, script } = message.payload;\n // Use pluginId as key so re-registration replaces (no duplicates)\n this.headScripts.set(pluginId, script);\n return { success: true };\n });\n\n // Register data sources\n context.entities.registerDataSource(\n new NavigationDataSource(\n this._routeRegistry,\n context.logger.child(\"NavigationDataSource\"),\n ),\n );\n\n // Site-info entity type + datasource registered by SiteInfoPlugin (entities/site-info)\n this.profileService = AnchorProfileService.getInstance(\n context.entityService,\n context.logger,\n );\n\n context.messaging.subscribe(\"sync:initial:completed\", async () => {\n await this.profileService?.initialize();\n this.logger.info(\"AnchorProfileService initialized\");\n return { success: true };\n });\n\n // Wire up route message handlers and register config routes\n setupRouteHandlers(context, this._routeRegistry, this.logger);\n\n if (this.config.templates) {\n context.templates.register(this.config.templates);\n }\n\n if (this.config.routes) {\n registerConfigRoutes(this.config.routes, this.id, this.routeRegistry);\n }\n\n // Initialize site builder\n this.siteBuilder = SiteBuilder.getInstance(\n context.logger.child(\"SiteBuilder\"),\n context,\n this.routeRegistry,\n this.profileService,\n this.config.entityDisplay,\n );\n\n // Register site-build job handler\n context.jobs.registerHandler(\n \"site-build\",\n new SiteBuildJobHandler(\n this.logger.child(\"SiteBuildJobHandler\"),\n context.messaging.send,\n {\n siteBuilder: this.siteBuilder,\n layouts: this.layouts,\n defaultSiteConfig: this.config.siteInfo,\n sharedImagesDir: this.config.sharedImagesDir,\n siteUrl: context.siteUrl,\n previewUrl: context.previewUrl,\n themeCSS: this.config.themeCSS,\n slots: this._slotRegistry,\n getHeadScripts: (): string[] => this.getRegisteredHeadScripts(),\n ...(this.config.staticAssets && {\n staticAssets: this.config.staticAssets,\n }),\n },\n ),\n );\n\n // Set up rebuild manager (handles debounced builds and auto-rebuild)\n this.rebuildManager = new RebuildManager(\n this.config,\n context,\n this.id,\n this.logger,\n );\n\n if (this.config.autoRebuild) {\n this.logger.debug(\"Auto-rebuild enabled\");\n this.rebuildManager.setupAutoRebuild();\n }\n\n // Register CMS admin route if CMS is enabled. Operator-only —\n // suppressed from public navigation; reachable via direct URL.\n if (this.config.cms) {\n this._routeRegistry.register({\n id: \"cms-admin\",\n path: \"/admin/\",\n title: \"Admin\",\n external: true,\n navigation: {\n show: false,\n slot: \"secondary\",\n label: \"Admin\",\n priority: 100,\n },\n });\n }\n\n // Re-register instructions when site-info changes so agent prompt stays fresh\n context.messaging.subscribe(\"entity:updated\", async (message) => {\n const payload = message.payload as {\n entityType: string;\n entityId: string;\n };\n if (payload.entityType === \"site-info\") {\n const instructions = await this.getInstructions();\n if (instructions) {\n context.registerInstructions(instructions);\n }\n }\n return { success: true };\n });\n\n // Subscribe to build-completed for SEO + CMS file generation\n subscribeBuildCompleted({\n context,\n routeRegistry: this._routeRegistry,\n config: this.config,\n logger: this.logger,\n });\n }\n\n /**\n * Get all head scripts registered by other plugins.\n * Used by the build pipeline to inject scripts into the HTML <head>.\n */\n public getRegisteredHeadScripts(): string[] {\n return Array.from(this.headScripts.values());\n }\n\n protected override async getTools(): Promise<Tool[]> {\n if (!this.pluginContext || !this.rebuildManager) {\n throw new Error(\"Plugin context not initialized\");\n }\n\n const rebuildManager = this.rebuildManager;\n return createSiteBuilderTools(this.id, (env) =>\n rebuildManager.requestBuild(env),\n );\n }\n\n protected override async getResources(): Promise<Resource[]> {\n const context = this.getContext();\n return [\n {\n uri: \"brain://site\",\n name: \"Site Info\",\n description: \"Site metadata — title, description, domain, URLs\",\n mimeType: \"application/json\",\n handler: async (): Promise<{\n contents: Array<{ uri: string; mimeType: string; text: string }>;\n }> => {\n let siteInfo;\n try {\n siteInfo = await fetchSiteInfo(context.entityService);\n } catch {\n siteInfo = { title: \"Brain\", description: \"\" };\n }\n return {\n contents: [\n {\n uri: \"brain://site\",\n mimeType: \"application/json\",\n text: JSON.stringify(\n {\n ...siteInfo,\n domain: context.domain,\n siteUrl: context.siteUrl,\n previewUrl: context.previewUrl,\n },\n null,\n 2,\n ),\n },\n ],\n };\n },\n },\n {\n uri: \"site://routes\",\n name: \"Site Routes\",\n description: \"All registered routes with sections and templates\",\n mimeType: \"application/json\",\n handler: async (): Promise<{\n contents: Array<{ uri: string; mimeType: string; text: string }>;\n }> => {\n const routes = this.routeRegistry.list();\n return {\n contents: [\n {\n uri: \"site://routes\",\n mimeType: \"application/json\",\n text: JSON.stringify(\n routes.map((route) => ({\n id: route.id,\n path: route.path,\n title: route.title,\n description: route.description,\n sections: route.sections.map((s) => ({\n id: s.id,\n template: s.template,\n })),\n })),\n null,\n 2,\n ),\n },\n ],\n };\n },\n },\n {\n uri: \"site://templates\",\n name: \"View Templates\",\n description: \"All registered view templates\",\n mimeType: \"application/json\",\n handler: async (): Promise<{\n contents: Array<{ uri: string; mimeType: string; text: string }>;\n }> => {\n const templates = context.views.list();\n return {\n contents: [\n {\n uri: \"site://templates\",\n mimeType: \"application/json\",\n text: JSON.stringify(\n templates.map((t) => ({\n name: t.name,\n description: t.description,\n hasWebRenderer: !!t.renderers.web,\n })),\n null,\n 2,\n ),\n },\n ],\n };\n },\n },\n ];\n }\n\n public getSiteBuilder(): SiteBuilder | undefined {\n return this.siteBuilder;\n }\n\n public getSlotRegistry(): UISlotRegistry | undefined {\n return this._slotRegistry;\n }\n\n protected override async getInstructions(): Promise<string | undefined> {\n const context = this.getContext();\n try {\n const siteInfo = await fetchSiteInfo(context.entityService);\n const parts = [\n `**Title:** ${siteInfo.title}`,\n `**Description:** ${siteInfo.description}`,\n context.domain && `**Domain:** ${context.domain}`,\n context.siteUrl && `**URL:** ${context.siteUrl}`,\n ].filter(Boolean);\n return `## Your Site\\n${parts.join(\"\\n\")}`;\n } catch {\n return undefined;\n }\n }\n\n protected override async onShutdown(): Promise<void> {\n this.logger.debug(\"Shutting down site-builder plugin\");\n this.rebuildManager?.dispose();\n SiteBuilder.resetInstance();\n this.logger.debug(\"Cleaned up all event subscriptions\");\n }\n}\n\n/**\n * Factory function to create the plugin\n */\nexport function siteBuilderPlugin(\n config: Partial<SiteBuilderConfig> = {},\n): Plugin {\n return new SiteBuilderPlugin(config);\n}\n",
540
540
  "import type {\n ProgressCallback,\n ServicePluginContext,\n IAnchorProfileService,\n ResolutionOptions,\n} from \"@brains/plugins\";\nimport { baseEntitySchema } from \"@brains/plugins\";\nimport { resolveEntityCoverImage, extractCoverImageId } from \"@brains/image\";\nimport type { Logger } from \"@brains/utils\";\nimport { ProgressReporter } from \"@brains/utils\";\nimport type { SectionDefinition, RouteDefinition } from \"@brains/plugins\";\nimport type {\n ISiteBuilder,\n SiteBuilderOptions,\n BuildResult,\n} from \"../types/site-builder-types\";\nimport { SiteBuilderOptionsSchema } from \"../types/site-builder-types\";\nimport type {\n StaticSiteBuilderFactory,\n BuildContext,\n} from \"./static-site-builder\";\nimport { createPreactBuilder } from \"./preact-builder\";\nimport { join } from \"path\";\nimport type { RouteRegistry } from \"./route-registry\";\nimport { DynamicRouteGenerator } from \"./dynamic-route-generator\";\n\nimport type { EntityDisplayMap } from \"../config\";\nimport { buildSiteInfo } from \"./build-site-info\";\nimport type { SiteInfo } from \"../types/site-info\";\nimport { z, pluralize, EntityUrlGenerator } from \"@brains/utils\";\nimport { ImageBuildService } from \"./image-build-service\";\n\n// Schema for entities with slug metadata (for auto-enrichment)\nconst entityWithSlugSchema = baseEntitySchema.extend({\n metadata: z\n .object({\n slug: z.string(),\n })\n .passthrough(), // Allow other metadata fields\n});\n\n// Type for enriched entity with url, typeLabel, listUrl, and listLabel\nexport type EnrichedEntity = z.infer<typeof entityWithSlugSchema> & {\n url: string;\n typeLabel: string;\n listUrl: string;\n listLabel: string;\n coverImageUrl?: string;\n coverImageWidth?: number;\n coverImageHeight?: number;\n coverImageSrcset?: string;\n coverImageSizes?: string;\n};\n\nexport class SiteBuilder implements ISiteBuilder {\n private static instance: SiteBuilder | null = null;\n private static defaultStaticSiteBuilderFactory: StaticSiteBuilderFactory =\n createPreactBuilder;\n private logger: Logger;\n private context: ServicePluginContext;\n private staticSiteBuilderFactory: StaticSiteBuilderFactory;\n private routeRegistry: RouteRegistry;\n private profileService: IAnchorProfileService;\n private entityDisplay: EntityDisplayMap | undefined;\n private imageBuildService: ImageBuildService | null = null;\n\n /**\n * Set the default static site builder factory for all instances\n */\n public static setDefaultStaticSiteBuilderFactory(\n factory: StaticSiteBuilderFactory,\n ): void {\n SiteBuilder.defaultStaticSiteBuilderFactory = factory;\n }\n\n public static getInstance(\n logger: Logger,\n context: ServicePluginContext,\n routeRegistry: RouteRegistry,\n profileService: IAnchorProfileService,\n entityDisplay?: EntityDisplayMap,\n ): SiteBuilder {\n SiteBuilder.instance ??= new SiteBuilder(\n logger,\n SiteBuilder.defaultStaticSiteBuilderFactory,\n context,\n routeRegistry,\n profileService,\n entityDisplay,\n );\n return SiteBuilder.instance;\n }\n\n public static resetInstance(): void {\n SiteBuilder.instance = null;\n }\n\n public static createFresh(\n logger: Logger,\n context: ServicePluginContext,\n routeRegistry: RouteRegistry,\n profileService: IAnchorProfileService,\n staticSiteBuilderFactory?: StaticSiteBuilderFactory,\n entityDisplay?: EntityDisplayMap,\n ): SiteBuilder {\n return new SiteBuilder(\n logger,\n staticSiteBuilderFactory ?? SiteBuilder.defaultStaticSiteBuilderFactory,\n context,\n routeRegistry,\n profileService,\n entityDisplay,\n );\n }\n\n private constructor(\n logger: Logger,\n staticSiteBuilderFactory: StaticSiteBuilderFactory,\n context: ServicePluginContext,\n routeRegistry: RouteRegistry,\n profileService: IAnchorProfileService,\n entityDisplay?: EntityDisplayMap,\n ) {\n this.logger = logger;\n this.context = context;\n this.staticSiteBuilderFactory = staticSiteBuilderFactory;\n this.routeRegistry = routeRegistry;\n this.profileService = profileService;\n this.entityDisplay = entityDisplay;\n\n // Configure the shared EntityUrlGenerator singleton\n EntityUrlGenerator.getInstance().configure(entityDisplay);\n }\n\n private async getSiteInfo(): Promise<SiteInfo> {\n return buildSiteInfo(\n this.context.entityService,\n this.profileService,\n this.routeRegistry,\n );\n }\n\n async build(\n options: SiteBuilderOptions,\n progress?: ProgressCallback,\n ): Promise<BuildResult> {\n // Parse options through schema to apply defaults\n const parsedOptions = SiteBuilderOptionsSchema.parse(options);\n\n const reporter = ProgressReporter.from(progress);\n const errors: string[] = [];\n const warnings: string[] = [];\n\n try {\n await reporter?.report({\n message: \"Starting site build\",\n progress: 0,\n total: 100,\n });\n\n // Generate dynamic routes from entities before building\n await reporter?.report({\n message: \"Generating dynamic routes\",\n progress: 10,\n total: 100,\n });\n\n const dynamicRouteGenerator = new DynamicRouteGenerator(\n this.context,\n this.routeRegistry,\n this.entityDisplay,\n );\n await dynamicRouteGenerator.generateEntityRoutes();\n\n // Create static site builder instance\n const workingDir =\n parsedOptions.workingDir ??\n join(parsedOptions.outputDir, \".preact-work\");\n const staticSiteBuilder = this.staticSiteBuilderFactory({\n logger: this.logger.child(\"StaticSiteBuilder\"),\n workingDir,\n outputDir: parsedOptions.outputDir,\n });\n\n // Clean stale build artifacts (preserves images/ for sharp cache)\n if (parsedOptions.cleanBeforeBuild) {\n await staticSiteBuilder.clean();\n }\n\n // Get all registered routes (now includes dynamically generated ones)\n const routes = this.routeRegistry.list();\n if (routes.length === 0) {\n warnings.push(\"No routes registered for site build\");\n }\n\n await reporter?.report({\n message: `Building ${routes.length} routes`,\n progress: 20,\n total: 100,\n });\n\n // Pre-resolve all images before rendering (Astro-like approach)\n await reporter?.report({\n message: \"Resolving images\",\n progress: 25,\n total: 100,\n });\n this.imageBuildService = new ImageBuildService(\n this.context.entityService,\n this.logger,\n parsedOptions.sharedImagesDir,\n );\n const imageIds = await this.collectAllImageIds();\n if (imageIds.length > 0) {\n await this.imageBuildService.resolveAll(imageIds);\n }\n\n // Create build context\n const siteConfig = parsedOptions.siteConfig;\n\n const buildContext: BuildContext = {\n routes,\n pluginContext: this.context,\n siteConfig: {\n title: siteConfig.title,\n description: siteConfig.description,\n ...(siteConfig.url && { url: siteConfig.url }),\n ...(siteConfig.copyright && { copyright: siteConfig.copyright }),\n ...(siteConfig.themeMode && { themeMode: siteConfig.themeMode }),\n ...(siteConfig.analyticsScript && {\n analyticsScript: siteConfig.analyticsScript,\n }),\n },\n headScripts: options.headScripts,\n ...(options.staticAssets && { staticAssets: options.staticAssets }),\n getContent: async (\n route: RouteDefinition,\n section: SectionDefinition,\n ) => {\n // In production, filter to only published content\n // In preview (or unspecified), show all content including drafts\n const publishedOnly = parsedOptions.environment === \"production\";\n return this.getContentForSection(section, route, publishedOnly);\n },\n getViewTemplate: (name: string) => {\n return this.context.views.get(name);\n },\n layouts: parsedOptions.layouts,\n getSiteInfo: async () => {\n return this.getSiteInfo();\n },\n ...(parsedOptions.themeCSS !== undefined && {\n themeCSS: parsedOptions.themeCSS,\n }),\n ...(options.slots && { slots: options.slots }),\n imageBuildService: this.imageBuildService,\n };\n\n // Run static site build (85% to 95% of overall progress)\n let buildStep = 0;\n const totalBuildSteps = routes.length + 4; // routes + start + tailwind + assets + hydration\n await staticSiteBuilder.build(buildContext, (message) => {\n buildStep++;\n // Map build steps to 85-95% range\n const stepProgress =\n 85 + Math.round((buildStep / totalBuildSteps) * 10);\n // Report progress without await to avoid blocking\n reporter\n ?.report({ message, progress: stepProgress, total: 100 })\n .catch(() => {\n // Ignore progress reporting errors\n });\n });\n\n await reporter?.report({\n message: \"Site build complete\",\n progress: 100,\n total: 100,\n });\n\n // Count files generated (at minimum, one HTML file per route)\n const filesGenerated = routes.length + 1; // routes + CSS file\n\n const result: BuildResult = {\n success: errors.length === 0,\n outputDir: parsedOptions.outputDir,\n filesGenerated,\n routesBuilt: routes.length,\n };\n\n if (errors.length > 0) {\n result.errors = errors;\n }\n\n if (warnings.length > 0) {\n result.warnings = warnings;\n }\n\n return result;\n } catch (error) {\n const buildError = new Error(\"Site build process failed\");\n this.logger.error(\"Site build failed\", {\n error: buildError,\n originalError: error,\n });\n\n errors.push(buildError.message);\n return {\n success: false,\n outputDir: parsedOptions.outputDir,\n filesGenerated: 0,\n routesBuilt: 0,\n errors,\n };\n }\n }\n\n /**\n * Get content for a section, either from provided content or from entity\n */\n private async getContentForSection(\n section: SectionDefinition,\n route: { id: string },\n publishedOnly: boolean,\n ): Promise<unknown> {\n // If no template, only static content is possible\n if (!section.template) {\n return section.content ?? null;\n }\n\n const templateName = section.template;\n const urlGenerator = EntityUrlGenerator.getInstance();\n\n // Check if this section uses dynamic content (DataSource)\n if (section.dataQuery) {\n // Use the context's resolveContent helper with DataSource params\n // DataSource will handle any necessary transformations internally\n const options: ResolutionOptions = {\n // Parameters for DataSource fetch\n dataParams: section.dataQuery,\n // Static fallback content from section definition\n fallback: section.content,\n // Filter to published-only content in production builds\n publishedOnly,\n };\n\n const content = await this.context.templates.resolve(\n templateName,\n options,\n );\n\n // Auto-enrich data with URLs, typeLabels, and coverImageUrls\n if (content) {\n return this.enrichWithUrls(content, urlGenerator);\n }\n\n return null;\n }\n\n // Use the context's resolveContent helper for static content\n const content = await this.context.templates.resolve(templateName, {\n // Saved content from entity storage\n savedContent: {\n entityType: \"site-content\",\n entityId: `${route.id}:${section.id}`,\n },\n // Static fallback content from section definition\n fallback: section.content,\n });\n\n // Auto-enrich data with URLs, typeLabels, and coverImageUrls\n if (content) {\n return this.enrichWithUrls(content, urlGenerator);\n }\n\n return null;\n }\n\n /**\n * Auto-enrich data with URL, typeLabel, and coverImageUrl fields\n * Recursively traverses data and adds url/typeLabel/coverImageUrl to any entity objects\n */\n private async enrichWithUrls(\n data: unknown,\n urlGenerator: EntityUrlGenerator,\n ): Promise<unknown> {\n if (data === null || data === undefined) {\n return data;\n }\n\n if (Array.isArray(data)) {\n return Promise.all(\n data.map((item) => this.enrichWithUrls(item, urlGenerator)),\n );\n }\n\n if (typeof data !== \"object\") {\n return data;\n }\n\n const obj = data as Record<string, unknown>;\n\n // Recursively enrich all nested objects first (in parallel)\n const enriched: Record<string, unknown> = {};\n const entries = Object.entries(obj);\n const enrichedValues = await Promise.all(\n entries.map(([, value]) => this.enrichWithUrls(value, urlGenerator)),\n );\n for (let i = 0; i < entries.length; i++) {\n const entry = entries[i];\n if (entry) {\n enriched[entry[0]] = enrichedValues[i];\n }\n }\n\n // Check if this object is an entity with slug metadata\n const entityCheck = entityWithSlugSchema.safeParse(obj);\n if (!entityCheck.success) {\n return enriched;\n }\n\n const entity = entityCheck.data;\n const entityType = entity.entityType;\n const slug = entity.metadata.slug;\n\n const config = this.entityDisplay?.[entityType];\n\n const typeLabel = config\n ? config.label\n : entityType.charAt(0).toUpperCase() + entityType.slice(1);\n\n // Compute listUrl and listLabel (plural) for breadcrumbs\n const pluralName = config\n ? (config.pluralName ?? config.label.toLowerCase() + \"s\")\n : pluralize(entityType);\n const listUrl = `/${pluralName}`;\n const listLabel = pluralName.charAt(0).toUpperCase() + pluralName.slice(1);\n\n // Resolve cover image: prefer pre-optimized from ImageBuildService, fall back to data URL\n const coverImageId = extractCoverImageId(entity);\n const preResolved = coverImageId\n ? this.imageBuildService?.get(coverImageId)\n : undefined;\n\n let coverImageFields: Partial<EnrichedEntity> = {};\n if (preResolved) {\n coverImageFields = {\n coverImageUrl: preResolved.src,\n coverImageWidth: preResolved.width,\n coverImageHeight: preResolved.height,\n ...(preResolved.srcset && {\n coverImageSrcset: preResolved.srcset,\n coverImageSizes: preResolved.sizes,\n }),\n };\n } else {\n // Fallback: resolve directly (returns data URL — post-processing will extract)\n const coverImage = await resolveEntityCoverImage(\n entity,\n this.context.entityService,\n );\n if (coverImage) {\n coverImageFields = {\n coverImageUrl: coverImage.url,\n coverImageWidth: coverImage.width,\n coverImageHeight: coverImage.height,\n };\n }\n }\n\n const enrichedEntity: EnrichedEntity = {\n ...enriched,\n ...entity,\n url: urlGenerator.generateUrl(entityType, slug),\n typeLabel,\n listUrl,\n listLabel,\n ...coverImageFields,\n };\n\n return enrichedEntity;\n }\n\n /**\n * Scan all entities for coverImageId references to pre-resolve before rendering.\n */\n private async collectAllImageIds(): Promise<string[]> {\n const imageIds = new Set<string>();\n\n try {\n // Get all entity types that have been registered\n const entityTypes = this.context.entityService.getEntityTypes();\n\n for (const entityType of entityTypes) {\n if (entityType === \"image\") continue; // Skip image entities themselves\n\n const entities =\n await this.context.entityService.listEntities(entityType);\n\n for (const entity of entities) {\n const coverImageId = extractCoverImageId(entity);\n if (coverImageId) {\n imageIds.add(coverImageId);\n }\n }\n }\n } catch (error) {\n this.logger.warn(\"Failed to collect image IDs for pre-resolution\", {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n\n return [...imageIds];\n }\n}\n",
541
541
  "import { z } from \"@brains/utils\";\nimport { baseEntitySchema } from \"@brains/entity-service\";\n\n/**\n * Supported image formats\n */\nexport const imageFormatSchema = z.enum([\n \"png\",\n \"jpg\",\n \"jpeg\",\n \"webp\",\n \"gif\",\n \"svg\",\n]);\nexport type ImageFormat = z.infer<typeof imageFormatSchema>;\n\n/**\n * Image entity metadata schema\n * All fields required (auto-detected on upload)\n * sourceUrl is optional - used for deduplication when importing from URLs\n */\nexport const imageMetadataSchema = z.object({\n title: z.string().optional(),\n alt: z.string().optional(),\n format: imageFormatSchema,\n width: z.number(),\n height: z.number(),\n sourceUrl: z.string().url().optional(),\n});\n\nexport type ImageMetadata = z.infer<typeof imageMetadataSchema>;\n\n/**\n * Image entity schema (extends BaseEntity)\n * Content field contains base64 data URL: data:image/png;base64,...\n */\nexport const imageSchema = baseEntitySchema.extend({\n entityType: z.literal(\"image\"),\n metadata: imageMetadataSchema,\n});\n\nexport type Image = z.infer<typeof imageSchema>;\n\n/**\n * Resolved image data for templates\n */\nexport const resolvedImageSchema = z.object({\n url: z.string(),\n alt: z.string(),\n title: z.string(),\n width: z.number(),\n height: z.number(),\n});\n\nexport type ResolvedImage = z.infer<typeof resolvedImageSchema>;\n",
542
542
  "import type { ImageFormat } from \"../schemas/image\";\n\n/**\n * Parsed data URL result\n */\nexport interface ParsedDataUrl {\n format: string;\n base64: string;\n}\n\n/**\n * Parse a data URL into format and base64 components\n * @throws Error if not a valid image data URL\n */\nexport function parseDataUrl(dataUrl: string): ParsedDataUrl {\n const match = dataUrl.match(/^data:image\\/([a-z+]+);base64,(.+)$/i);\n if (!match?.[1] || !match[2]) {\n throw new Error(\"Invalid image data URL\");\n }\n return {\n format: match[1].toLowerCase(),\n base64: match[2],\n };\n}\n\n/**\n * Create a data URL from base64 and format\n */\nexport function createDataUrl(\n base64: string,\n format: ImageFormat | string,\n): string {\n // Normalize jpg to jpeg for MIME type\n const mimeFormat = format === \"jpg\" ? \"jpeg\" : format;\n return `data:image/${mimeFormat};base64,${base64}`;\n}\n\n/**\n * Magic bytes for common image formats\n */\nconst IMAGE_MAGIC_BYTES: Record<string, string> = {\n // PNG: 89 50 4E 47 = iVBORw\n png: \"iVBORw\",\n // JPEG: FF D8 FF = /9j/\n jpg: \"/9j/\",\n // GIF: 47 49 46 38 = R0lGOD\n gif: \"R0lGOD\",\n // WebP: 52 49 46 46 = UklGR (RIFF header)\n webp: \"UklGR\",\n};\n\n/**\n * Detect image format from base64 magic bytes\n * @returns format string or null if unknown\n */\nexport function detectImageFormat(base64: string): ImageFormat | null {\n for (const [format, magic] of Object.entries(IMAGE_MAGIC_BYTES)) {\n if (base64.startsWith(magic)) {\n return format as ImageFormat;\n }\n }\n return null;\n}\n\n/**\n * Check if string is a valid image data URL\n */\nexport function isValidDataUrl(str: string): boolean {\n return /^data:image\\/[a-z+]+;base64,.+$/i.test(str);\n}\n\n// Re-export HTTP utilities from @brains/utils for convenience\nexport { isHttpUrl, fetchImageAsBase64 } from \"@brains/utils\";\n\n/**\n * Get image dimensions from base64 data\n * Parses image headers to extract width/height without full decode\n */\nexport function detectImageDimensions(\n base64: string,\n): { width: number; height: number } | null {\n const buffer = Buffer.from(base64, \"base64\");\n\n // PNG: width at bytes 16-19, height at bytes 20-23 (big-endian)\n if (\n buffer[0] === 0x89 &&\n buffer[1] === 0x50 &&\n buffer[2] === 0x4e &&\n buffer[3] === 0x47\n ) {\n const width = buffer.readUInt32BE(16);\n const height = buffer.readUInt32BE(20);\n return { width, height };\n }\n\n // JPEG: Need to scan for SOF0/SOF2 marker\n if (buffer[0] === 0xff && buffer[1] === 0xd8) {\n let offset = 2;\n while (offset < buffer.length - 8) {\n if (buffer[offset] !== 0xff) {\n offset++;\n continue;\n }\n const marker = buffer[offset + 1];\n // SOF0 (0xC0) or SOF2 (0xC2) - Start of Frame\n if (marker === 0xc0 || marker === 0xc2) {\n const height = buffer.readUInt16BE(offset + 5);\n const width = buffer.readUInt16BE(offset + 7);\n return { width, height };\n }\n // Skip to next marker\n const length = buffer.readUInt16BE(offset + 2);\n offset += 2 + length;\n }\n }\n\n // GIF: width at bytes 6-7, height at bytes 8-9 (little-endian)\n if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {\n const width = buffer.readUInt16LE(6);\n const height = buffer.readUInt16LE(8);\n return { width, height };\n }\n\n // WebP: RIFF header, check for VP8 chunk\n if (\n buffer.length >= 30 &&\n buffer[0] === 0x52 &&\n buffer[1] === 0x49 &&\n buffer[2] === 0x46 &&\n buffer[3] === 0x46\n ) {\n // VP8 (lossy): width/height at specific offsets\n if (buffer[12] === 0x56 && buffer[13] === 0x50 && buffer[14] === 0x38) {\n // VP8 chunk\n if (buffer[15] === 0x20) {\n // VP8 lossy\n // Frame header starts at offset 23\n const b26 = buffer[26] ?? 0;\n const b27 = buffer[27] ?? 0;\n const b28 = buffer[28] ?? 0;\n const b29 = buffer[29] ?? 0;\n const width = (b26 | (b27 << 8)) & 0x3fff;\n const height = (b28 | (b29 << 8)) & 0x3fff;\n return { width, height };\n }\n // VP8L (lossless)\n if (buffer[15] === 0x4c && buffer.length >= 25) {\n const bits = buffer.readUInt32LE(21);\n const width = (bits & 0x3fff) + 1;\n const height = ((bits >> 14) & 0x3fff) + 1;\n return { width, height };\n }\n }\n }\n\n return null;\n}\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rizom/brain",
3
- "version": "0.1.1-alpha.14",
3
+ "version": "0.1.1-alpha.15",
4
4
  "description": "Brain runtime + CLI — scaffold, run, and manage AI brain instances",
5
5
  "type": "module",
6
6
  "bin": {