kloudy 0.4.1 → 0.4.3

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.
@@ -0,0 +1,31 @@
1
+ // These controllers require a trusted account/engine adapter. Never feed model output as an invoice.
2
+ const id=v=>typeof v==='string'&&/^[\w:.-]{1,128}$/.test(v);
3
+ const minor=v=>Number.isSafeInteger(v)&&v>=0&&v<=100000000;
4
+ const same=(a,b)=>a?.accountRef===b?.accountRef&&a?.projectId===b?.projectId&&a?.grantId===b?.grantId;
5
+ export function validateBill(b,binding,now=Date.now()){
6
+ if(!binding||!['accountRef','projectId','grantId'].every(k=>id(binding[k])))throw Error('Verify your project before paying.');
7
+ if(!b||!same(b,binding)||!['due','paid','paused'].includes(b.state)||!id(b.billId)||!id(b.revision)||b.currency!=='usd'||!minor(b.totalMinor)||!Number.isFinite(b.expiresAt)||b.expiresAt<=now||b.expiresAt>now+900000||!Array.isArray(b.lines)||b.lines.length>20||!b.lines.every(l=>typeof l.label==='string'&&l.label.length<=120&&minor(l.amountMinor))||b.lines.reduce((n,l)=>n+l.amountMinor,0)!==b.totalMinor)throw Error('Refresh this bill before paying.');
8
+ return structuredClone(b);
9
+ }
10
+ export class BillApproval{
11
+ constructor({adapter,binding,now=Date.now,key=()=>globalThis.crypto.randomUUID()}){this.adapter=adapter;this.binding=structuredClone(binding);this.now=now;this.key=key;this.generation=0;this.bill=null;this.pending=null;this.approvalKey=null;}
12
+ reset(){this.generation++;this.bill=null;this.approvalKey=null;}
13
+ async read(billId){if(!id(billId))throw Error('Invalid bill');this.reset();const generation=this.generation;const b=validateBill(await this.adapter.readBill(billId),this.binding,this.now());if(generation!==this.generation||b.billId!==billId)throw Error('Bill changed.');this.bill=b;this.approvalKey=this.key();return b;}
14
+ async pay({approvedBillId,approvedRevision}={}){
15
+ if(this.pending)return this.pending;
16
+ const b=validateBill(this.bill,this.binding,this.now());
17
+ if(b.state!=='due'&&b.state!=='paused'||approvedBillId!==b.billId||approvedRevision!==b.revision)throw Error('Approve the current bill before paying.');
18
+ const generation=this.generation,key=this.approvalKey;
19
+ this.pending=(async()=>{const current=await this.adapter.binding();if(!same(current,this.binding)||current.connected!==true)throw Error('Project authorization changed.');if(generation!==this.generation)throw Error('Bill changed.');validateBill(b,this.binding,this.now());
20
+ const result=await this.adapter.payBill({billId:b.billId,revision:b.revision,totalMinor:b.totalMinor,currency:b.currency,idempotencyKey:key});
21
+ if(generation!==this.generation)throw Error('Payment status needs a fresh receipt.');
22
+ if(result?.state!=='paid'||!same(result,this.binding)||result.billId!==b.billId||result.revision!==b.revision||result.totalMinor!==b.totalMinor||result.currency!==b.currency||!id(result.receiptId))throw Error('Payment is not confirmed. Check the receipt before retrying.');
23
+ this.bill={...b,state:'paid'};return result;
24
+ })().finally(()=>{this.pending=null;});return this.pending;
25
+ }
26
+ }
27
+ const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
28
+ const amount=n=>new Intl.NumberFormat('en-US',{style:'currency',currency:'USD'}).format(n/100);
29
+ export function billCardMarkup(b,binding,{now=Date.now(),busy=false}={}){b=validateBill(b,binding,now);return `<section class="sample-card"><header>Kloudy Sample card · Wallet</header><h1>${b.state==='paid'?'Payment received':'Your bill is ready'}</h1><p class="bill-amount">${amount(b.totalMinor)}</p>${b.state==='paid'?'':`<button class="sample-primary" data-bill-pay ${busy?'disabled':''}>Pay ${amount(b.totalMinor)}</button><p class="sample-note">Only this bill. Your balance is never charged without your approval.</p>`}<details><summary>Cost breakdown</summary><dl>${b.lines.map(l=>`<dt>${e(l.label)}</dt><dd>${amount(l.amountMinor)}</dd>`).join('')}</dl></details>${b.state==='paid'?'':'<a href="https://kloudy.ai/?view=wallet">Add money to wallet</a><p class="sample-note">Adding money does not pay this bill. Return here to approve it.</p>'}</section>`;}
30
+ export function attachmentCardMarkup(name){return `<section class="sample-card"><header>Kloudy Sample card</header><h1>Where should this tool stay?</h1><p>${e(String(name).slice(0,160))}</p><div class="sample-options"><button data-attachment="project">This project</button><button data-attachment="account">All my projects</button></div><p class="sample-note">The engine must confirm the link. Permissions remain scoped and revocable; no permanent grant or payment is created.</p></section>`;}
31
+ export function clarificationCardMarkup(question,choices){if(typeof question!=='string'||question.length>160||!Array.isArray(choices)||choices.length<2||choices.length>4||choices.some(c=>!id(c.id)||typeof c.label!=='string'||c.label.length>60)||new Set(choices.map(c=>c.id)).size!==choices.length)throw Error('Invalid clarification');return `<section class="sample-card"><header>Kloudy Sample card</header><h1>${e(question)}</h1><div class="sample-options">${choices.map(c=>`<button data-clarify="${e(c.id)}">${e(c.label)}</button>`).join('')}</div></section>`;}
@@ -0,0 +1,11 @@
1
+ import {sampleHTML} from './sample-ui.mjs';
2
+ export const SAMPLE_URI='ui://widget/kloudy-sample-v1.html';
3
+ export const sampleTool={name:'kloudy_sample_card',title:'Kloudy Sample card',description:'Show a compact Kloudy preference card when the user asks to choose tool types, project location or tool-update frequency. Also opens account-owned wallet/activity views or explains BWW conversion. Navigation and preference collection only: never grants access, installs tools or authorizes payment. Use once at first relevant setup, or when preferences are unclear; do not repeat for every tool.',annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:'object',properties:{kind:{type:'string',enum:['preferences','conversion','analytics','wallet']},surface:{type:'string',enum:['ide','chatbot','site'],default:'ide'}},required:['kind'],additionalProperties:false},_meta:{ui:{resourceUri:SAMPLE_URI},'openai/outputTemplate':SAMPLE_URI}};
4
+ export const sampleResource={uri:SAMPLE_URI,name:'Kloudy Sample card',description:'Compact preferences and account navigation. No execution or billing authority.',mimeType:'text/html;profile=mcp-app'};
5
+ export function sampleArguments(a){return a&&typeof a==='object'&&!Array.isArray(a)&&Object.keys(a).every(k=>['kind','surface'].includes(k))&&['preferences','conversion','analytics','wallet'].includes(a.kind)&&(a.surface===undefined||['ide','chatbot','site'].includes(a.surface));}
6
+ export function sampleMessage(method,params){return method==='resources/read'&&params?.uri===SAMPLE_URI&&Object.keys(params).length===1||method==='tools/call'&&params?.name==='kloudy_sample_card'&&Object.keys(params).every(k=>['name','arguments'].includes(k))&&sampleArguments(params.arguments);}
7
+ export function sampleResult(kind='preferences',surface='ide'){
8
+ const url={preferences:'https://kloudy.ai/samples?frontdoor='+surface,conversion:'https://kloudy.ai/samples?card=conversion',analytics:'https://kloudy.ai/bots',wallet:'https://kloudy.ai/?view=wallet'}[kind];
9
+ return {content:[{type:'text',text:`Kloudy Sample card\n\n[${kind==='preferences'?'Choose your tool preferences':'Open '+kind}](${url})\n\nSilent delivery is the default. New costs require approval. Opening a card does not connect a tool or make a payment.`}],structuredContent:{version:'kloudy.sample/1',kind,surface,url,executed:false,authorizesSpending:false}};
10
+ }
11
+ export function sampleContents(){return {contents:[{uri:SAMPLE_URI,mimeType:sampleResource.mimeType,text:sampleHTML,_meta:{ui:{prefersBorder:true,domain:'https://kloudy.ai',csp:{connectDomains:[],resourceDomains:[]}},'openai/widgetDescription':'Kloudy Sample card. Three short preference questions; no tool execution or payment.','openai/widgetPrefersBorder':true,'openai/widgetCSP':{connect_domains:[],resource_domains:[]},'openai/widgetDomain':'https://kloudy.ai'}}]};}
@@ -0,0 +1,47 @@
1
+ // Portable, non-authorizing presentation contract. No credential or payment execution.
2
+ export const SAMPLE_VERSION='kloudy.sample/1';
3
+ export const SAMPLE_TITLE='Kloudy Sample card';
4
+ export const preferenceChoices={
5
+ tools:[['open_source','Open source'],['paid','Paid · convenience'],['hybrid','Mix of both'],['free_then_paid','Free tier, then ask']],
6
+ target:[['local','Local only'],['www','World Wide Web'],['bww','Bot Wide Web'],['mixed','A mix']],
7
+ delivery:[['silent','Silent · recommended'],['paid','Cards for paid tools'],['all','Cards for every tool']]
8
+ };
9
+ export const preferenceDefaults=()=>({tools:null,target:null,delivery:'silent'});
10
+ export function validPreferences(value){return !!value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===3&&Object.entries(preferenceChoices).every(([key,options])=>(key!=='delivery'&&value[key]===null)||options.some(([id])=>id===value[key]));}
11
+ export function preferenceQuestion(step,surface='site'){
12
+ const external=['ide','chatbot','terminal'].includes(surface);
13
+ return [
14
+ {key:'tools',question:'Which tools do you prefer?',intro:external?'Kloudy gives your assistant tools to build your projects.':'Kloudy uses tools to build your projects.'},
15
+ {key:'target',question:'Where will your projects run?',intro:'Choose the places you want to serve.'},
16
+ {key:'delivery',question:'How much should Kloudy show?',intro:'Work quietly, or show the tools as they are offered.'}
17
+ ][step]||null;
18
+ }
19
+ export class SamplePreferences{
20
+ constructor(value=preferenceDefaults()){if(!validPreferences(value))throw Error('Invalid preferences');this.value=structuredClone(value);this.step=0;}
21
+ choose(id){const q=preferenceQuestion(this.step);if(!q||!preferenceChoices[q.key].some(([value])=>value===id))throw Error('Choose one of the offered options.');this.value[q.key]=id;this.step++;return this.value;}
22
+ back(){this.step=Math.max(0,this.step-1);}
23
+ }
24
+ export const cardKinds=['preferences','clarify','tools','attachment','signin','approval','payment','exhaustion','receipt','recovery','analytics','conversion'];
25
+ export function shouldShowCard(kind,preferences=preferenceDefaults(),{paid=false,requested=false}={}){
26
+ if(!cardKinds.includes(kind)||!validPreferences(preferences))return false;
27
+ if(requested||['signin','approval','payment','exhaustion','recovery','clarify','attachment'].includes(kind))return true;
28
+ if(kind==='tools')return preferences.delivery==='all'||(preferences.delivery==='paid'&&paid);
29
+ return false;
30
+ }
31
+ export class SampleQueue{
32
+ constructor(){this.current=null;this.waiting=[];this.seen=new Set();}
33
+ offer(card,preferences,options){if(!card||!cardKinds.includes(card.kind)||typeof card.id!=='string'||card.id.length>160||this.seen.has(card.id)||!shouldShowCard(card.kind,preferences,options))return false;
34
+ this.seen.add(card.id);if(this.seen.size>256)this.seen.delete(this.seen.values().next().value);
35
+ const urgent=['payment','approval','exhaustion','recovery'].includes(card.kind);
36
+ if(!this.current)this.current=card;else if(urgent&&!['payment','approval','exhaustion','recovery'].includes(this.current.kind)){this.waiting.unshift(this.current);this.current=card;}else this.waiting.push(card);
37
+ this.waiting=this.waiting.slice(0,10);return true;
38
+ }
39
+ dismiss(){this.current=this.waiting.shift()||null;return this.current;}
40
+ clear(){this.current=null;this.waiting=[];this.seen.clear();}
41
+ }
42
+ const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
43
+ export function samplePreferencesMarkup(flow,{surface='site',notice=''}={}){
44
+ const q=preferenceQuestion(flow.step,surface),complete=!q;
45
+ return `<section class="sample-card" aria-label="${SAMPLE_TITLE}"><header><span class="sample-orb" aria-hidden="true"></span><span>${SAMPLE_TITLE}</span><small>${complete?'Ready':(flow.step+1)+' / 3'}</small></header><h1 tabindex="-1">${complete?'Your way to build.':esc(q.question)}</h1>${complete?'':`<p>${esc(q.intro)}</p>`}${complete?`<dl>${Object.entries(preferenceChoices).map(([key,options])=>`<dt>${esc({tools:'Tools',target:'Where',delivery:'Updates'}[key])}</dt><dd>${esc(options.find(([id])=>id===flow.value[key])?.[1]||'Ask when needed')}</dd>`).join('')}</dl><button class="sample-primary" data-sample-save>Save preferences</button>`:`<div class="sample-options">${preferenceChoices[q.key].map(([id,label])=>`<button data-sample-choice="${id}" aria-pressed="${flow.value[q.key]===id}">${esc(label)}</button>`).join('')}</div>`}<p class="sample-note">${q?.key==='tools'?'Open source describes the tools; hosting or compute may still cost money.':q?.key==='target'?'BWW sites need hosting: your compute or a paid host.':'New costs always need your approval. Ask which tools are connected anytime.'}</p><details><summary>How this works</summary><p>Silent means fewer updates, not permission to spend. Free tiers ending always prompt a choice. These preferences do not install tools or authorize a connection.</p></details><footer>${flow.step?'<button data-sample-back>Back</button>':''}<button data-sample-later>Not now</button></footer><p role="status" class="sample-status">${esc(notice)}</p></section>`;
46
+ }
47
+ export function conversionCardMarkup(){return `<section class="sample-card"><header>${SAMPLE_TITLE}</header><h1>Let bots reach your site.</h1><p>Publish a BWW card so compatible assistants can fetch the part a user needs.</p><a class="sample-primary" href="https://kloudy.ai/hosting">Choose hosting</a><details><summary>Benefits and costs</summary><p>A converted site can offer structured fragments instead of a full browser visit. Actual compute, water and cost savings depend on measured usage; no fixed savings are promised.</p><p>Bring your PC or cloud, or use a paid host. If your compute goes offline, your site becomes unavailable. Concierge and ad bots are optional paid services.</p></details></section>`;}