kloudy 0.4.7 → 1.0.0

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.
Files changed (52) hide show
  1. package/LICENSE +202 -202
  2. package/MCP-DEFAULT.md +12 -12
  3. package/README.md +317 -313
  4. package/bin/entry.mjs +23 -22
  5. package/bin/kloudy.mjs +74 -71
  6. package/package.json +3 -2
  7. package/schemas/SDF-LICENSE.txt +21 -21
  8. package/schemas/kloudy.deployable-bot.schema.json +80 -0
  9. package/schemas/sdf-document-0.2.schema.json +411 -411
  10. package/schemas/sdf-webpage-v1.schema.json +176 -176
  11. package/src/activity.mjs +23 -23
  12. package/src/approval-ui.mjs +2 -2
  13. package/src/approval.mjs +25 -25
  14. package/src/autoupdate.mjs +2 -2
  15. package/src/bots.mjs +144 -0
  16. package/src/chooser-link.mjs +17 -17
  17. package/src/client.mjs +96 -96
  18. package/src/connect-card.mjs +32 -32
  19. package/src/connect-ui.mjs +3 -3
  20. package/src/connection.mjs +25 -25
  21. package/src/conversation.mjs +53 -53
  22. package/src/deployable-bots.json.gz +0 -0
  23. package/src/editor-install.mjs +79 -79
  24. package/src/host-interface.mjs +10 -10
  25. package/src/hosted.mjs +42 -42
  26. package/src/httpk.mjs +66 -66
  27. package/src/install.mjs +35 -35
  28. package/src/introduce.mjs +33 -33
  29. package/src/mcp.mjs +74 -74
  30. package/src/modalities.mjs +27 -0
  31. package/src/models.mjs +55 -55
  32. package/src/preferences-card.mjs +21 -21
  33. package/src/preferences-ui.mjs +2 -2
  34. package/src/product-actions.mjs +22 -22
  35. package/src/public.mjs +41 -41
  36. package/src/query.mjs +28 -28
  37. package/src/remote.mjs +9 -2
  38. package/src/replies.mjs +7 -7
  39. package/src/reply-templates.json +12 -12
  40. package/src/sample-actions.mjs +31 -31
  41. package/src/sample-card.mjs +11 -11
  42. package/src/sample-cards.mjs +47 -47
  43. package/src/sample-ui.mjs +2 -2
  44. package/src/sdf-budget.mjs +23 -23
  45. package/src/sdf.mjs +12 -12
  46. package/src/starter.mjs +43 -43
  47. package/src/status-indicator.css +5 -5
  48. package/src/strings.mjs +51 -51
  49. package/src/terminal-status.mjs +6 -6
  50. package/src/website-return.mjs +18 -0
  51. package/src/welcome.mjs +8 -8
  52. package/src/wrap.mjs +58 -58
@@ -1,22 +1,22 @@
1
- // Product-side validation only. The single HTTPK remains the execution authority.
2
- const bounded=(value,max)=>typeof value==='string'&&value.length>0&&value.length<=max;
3
- const identifier=value=>bounded(value,80)&&/^[a-zA-Z0-9_.:-]+$/.test(value);
4
- export function actionInput(action,inputs){
5
- if(!inputs||typeof inputs!=='object'||Array.isArray(inputs))throw Error('invalid_action_input');
6
- const schemas={
7
- 'chain.head':{chain:v=>bounded(v,80)},
8
- 'wallet.prepare':{chain:v=>bounded(v,100),destination_chain:v=>bounded(v,100),sender:v=>bounded(v,100),recipient:v=>bounded(v,100),amount_atomic:v=>bounded(v,100)&&/^[1-9]\d*$/.test(v)},
9
- 'bww.fetch':{project:identifier,fragment_ids:v=>Array.isArray(v)&&v.length>=1&&v.length<=8&&new Set(v).size===v.length&&v.every(identifier),allow_html:v=>typeof v==='boolean'},
10
- 'bww.publish':{project:identifier,fragment_id:identifier,title:v=>bounded(v,120),text:v=>bounded(v,8000),html:v=>v===null||typeof v==='string'&&v.length<=12000,advertisement:v=>v===null||v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).length===2&&bounded(v.provider,80)&&bounded(v.text,500)}
11
- };
12
- const schema=schemas[action];
13
- if(!schema||Object.keys(inputs).length!==Object.keys(schema).length||!Object.entries(schema).every(([key,validate])=>Object.hasOwn(inputs,key)&&validate(inputs[key])))throw Error('invalid_action_input');
14
- return {type:'action',action,inputs};
15
- }
16
- export function hostingParams(budget){
17
- if(budget===undefined)return {};
18
- if(!Number.isSafeInteger(budget)||budget<=0||budget>1_000_000_000_000)throw Error('invalid_budget');
19
- return {budget_usd_micros:budget};
20
- }
21
- export function connectionParams(chain){if(!bounded(chain,100))throw Error('invalid_chain');return {chain};}
22
- export function stableIntentKey(key){if(!bounded(key,128)||!/^[a-zA-Z0-9_.:-]+$/.test(key))throw Error('invalid_idempotency_key');return key;}
1
+ // Product-side validation only. The single HTTPK remains the execution authority.
2
+ const bounded=(value,max)=>typeof value==='string'&&value.length>0&&value.length<=max;
3
+ const identifier=value=>bounded(value,80)&&/^[a-zA-Z0-9_.:-]+$/.test(value);
4
+ export function actionInput(action,inputs){
5
+ if(!inputs||typeof inputs!=='object'||Array.isArray(inputs))throw Error('invalid_action_input');
6
+ const schemas={
7
+ 'chain.head':{chain:v=>bounded(v,80)},
8
+ 'wallet.prepare':{chain:v=>bounded(v,100),destination_chain:v=>bounded(v,100),sender:v=>bounded(v,100),recipient:v=>bounded(v,100),amount_atomic:v=>bounded(v,100)&&/^[1-9]\d*$/.test(v)},
9
+ 'bww.fetch':{project:identifier,fragment_ids:v=>Array.isArray(v)&&v.length>=1&&v.length<=8&&new Set(v).size===v.length&&v.every(identifier),allow_html:v=>typeof v==='boolean'},
10
+ 'bww.publish':{project:identifier,fragment_id:identifier,title:v=>bounded(v,120),text:v=>bounded(v,8000),html:v=>v===null||typeof v==='string'&&v.length<=12000,advertisement:v=>v===null||v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).length===2&&bounded(v.provider,80)&&bounded(v.text,500)}
11
+ };
12
+ const schema=schemas[action];
13
+ if(!schema||Object.keys(inputs).length!==Object.keys(schema).length||!Object.entries(schema).every(([key,validate])=>Object.hasOwn(inputs,key)&&validate(inputs[key])))throw Error('invalid_action_input');
14
+ return {type:'action',action,inputs};
15
+ }
16
+ export function hostingParams(budget){
17
+ if(budget===undefined)return {};
18
+ if(!Number.isSafeInteger(budget)||budget<=0||budget>1_000_000_000_000)throw Error('invalid_budget');
19
+ return {budget_usd_micros:budget};
20
+ }
21
+ export function connectionParams(chain){if(!bounded(chain,100))throw Error('invalid_chain');return {chain};}
22
+ export function stableIntentKey(key){if(!bounded(key,128)||!/^[a-zA-Z0-9_.:-]+$/.test(key))throw Error('invalid_idempotency_key');return key;}
package/src/public.mjs CHANGED
@@ -1,41 +1,41 @@
1
- import {validateSDF} from './sdf.mjs';
2
- import {KloudyError} from './client.mjs';
3
- const fail=(code,message)=>{throw new KloudyError(code,message);};
4
- async function request(path,options,fetcher){
5
- let response;try{response=await fetcher('https://kloudy.ai'+path,{...options,redirect:'error',signal:AbortSignal.timeout(25000)});}catch{fail('public_unavailable','Kloudy public reading is unavailable. No account action was submitted.');}
6
- if(!response.ok)fail(response.status===429?'rate_limited':'public_unavailable','The public resource could not be read. Check the URL or try again later.');
7
- const chunks=[];let size=0;for await(const chunk of response.body){size+=chunk.length;if(size>2097152)fail('invalid_response','Public response exceeded its size limit.');chunks.push(chunk);}
8
- try{return JSON.parse(Buffer.concat(chunks).toString());}catch{fail('invalid_response','Expected a JSON public response.');}
9
- }
10
- export async function readPublicPage(address,{fetcher=fetch}={}){
11
- let url;try{url=new URL(address);}catch{}
12
- if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
13
- const result=await request('/api/read',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href})},fetcher);
14
- let source;try{source=new URL(result.url);}catch{}
15
- if(!source||!['http:','https:'].includes(source.protocol)||source.username||source.password||typeof result.body!=='string'||typeof result.type!=='string')fail('invalid_response','Expected source-attributed public page content.');
16
- return {url:source.href,type:result.type,body:result.body,bytes:result.bytes??Buffer.byteLength(result.body)};
17
- }
18
- export async function readCard(path='/world',{fetcher=fetch}={}){
19
- if(typeof path!=='string'||!/^\/(?:[a-zA-Z0-9_-]|%20)*$/.test(path))fail('invalid_path','Use a Kloudy page path such as /world.');
20
- const card=await request('/cards/'+(path==='/'?'home':path.slice(1))+'.json',{headers:{accept:'application/json'}},fetcher);
21
- if(card?.interaction_mode!=='informational_only'||!Array.isArray(card.capabilities)||card.capabilities.length||typeof card.description!=='string'||card.description.length>5000||typeof card.name!=='string'||card.website!=='https://kloudy.ai'+path)fail('invalid_response','Expected an informational Kloudy card without execution permissions.');
22
- return card;
23
- }
24
- export async function convertPage(address,{fetcher=fetch,resolution="compact"}={}){
25
- let url;try{url=new URL(address);}catch{}
26
- if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
27
- if(!['compact','standard'].includes(resolution))fail('invalid_resolution','Choose compact or standard conversion.');
28
- const card=await request('/api/convert/sdf',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href,resolution})},fetcher);
29
- if(!validateSDF(card).valid)fail('invalid_response','Expected a valid bounded SDF webpage card.');
30
- return card;
31
- }
32
- export async function convertPages(addresses,options={}){
33
- if(!Array.isArray(addresses)||!addresses.length||addresses.length>8||addresses.some(a=>typeof a!=='string'||a.length>2048))fail('invalid_input','Convert 1–8 public addresses at a time.');
34
- const cards=[],reports=[],seen=new Map();
35
- for(const address of addresses){let canonical;try{const url=new URL(address);url.hash='';canonical=url.username||url.password?'invalid-address':url.href;}catch{canonical=address;}
36
- if(seen.has(canonical)){reports.push({...seen.get(canonical),duplicate:true});continue;}
37
- let report;try{const card=await convertPage(address,options);cards.push(card);report={source:canonical,status:'converted',card_id:card.id};}catch(error){report={source:canonical,status:'unreadable',reason:error.code||'conversion_failed',message:error.code?error.message:'Conversion could not finish.'};}
38
- reports.push(report);seen.set(canonical,report);
39
- }
40
- return {schema:'kloudy.sdf.card-list/1',cards,reports,ranking:'none',model_calls:0};
41
- }
1
+ import {validateSDF} from './sdf.mjs';
2
+ import {KloudyError} from './client.mjs';
3
+ const fail=(code,message)=>{throw new KloudyError(code,message);};
4
+ async function request(path,options,fetcher){
5
+ let response;try{response=await fetcher('https://kloudy.ai'+path,{...options,redirect:'error',signal:AbortSignal.timeout(25000)});}catch{fail('public_unavailable','Kloudy public reading is unavailable. No account action was submitted.');}
6
+ if(!response.ok)fail(response.status===429?'rate_limited':'public_unavailable','The public resource could not be read. Check the URL or try again later.');
7
+ const chunks=[];let size=0;for await(const chunk of response.body){size+=chunk.length;if(size>2097152)fail('invalid_response','Public response exceeded its size limit.');chunks.push(chunk);}
8
+ try{return JSON.parse(Buffer.concat(chunks).toString());}catch{fail('invalid_response','Expected a JSON public response.');}
9
+ }
10
+ export async function readPublicPage(address,{fetcher=fetch}={}){
11
+ let url;try{url=new URL(address);}catch{}
12
+ if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
13
+ const result=await request('/api/read',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href})},fetcher);
14
+ let source;try{source=new URL(result.url);}catch{}
15
+ if(!source||!['http:','https:'].includes(source.protocol)||source.username||source.password||typeof result.body!=='string'||typeof result.type!=='string')fail('invalid_response','Expected source-attributed public page content.');
16
+ return {url:source.href,type:result.type,body:result.body,bytes:result.bytes??Buffer.byteLength(result.body)};
17
+ }
18
+ export async function readCard(path='/world',{fetcher=fetch}={}){
19
+ if(typeof path!=='string'||!/^\/(?:[a-zA-Z0-9_-]|%20)*$/.test(path))fail('invalid_path','Use a Kloudy page path such as /world.');
20
+ const card=await request('/cards/'+(path==='/'?'home':path.slice(1))+'.json',{headers:{accept:'application/json'}},fetcher);
21
+ if(card?.interaction_mode!=='informational_only'||!Array.isArray(card.capabilities)||card.capabilities.length||typeof card.description!=='string'||card.description.length>5000||typeof card.name!=='string'||card.website!=='https://kloudy.ai'+path)fail('invalid_response','Expected an informational Kloudy card without execution permissions.');
22
+ return card;
23
+ }
24
+ export async function convertPage(address,{fetcher=fetch,resolution="compact"}={}){
25
+ let url;try{url=new URL(address);}catch{}
26
+ if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
27
+ if(!['compact','standard'].includes(resolution))fail('invalid_resolution','Choose compact or standard conversion.');
28
+ const card=await request('/api/convert/sdf',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href,resolution})},fetcher);
29
+ if(!validateSDF(card).valid)fail('invalid_response','Expected a valid bounded SDF webpage card.');
30
+ return card;
31
+ }
32
+ export async function convertPages(addresses,options={}){
33
+ if(!Array.isArray(addresses)||!addresses.length||addresses.length>8||addresses.some(a=>typeof a!=='string'||a.length>2048))fail('invalid_input','Convert 1–8 public addresses at a time.');
34
+ const cards=[],reports=[],seen=new Map();
35
+ for(const address of addresses){let canonical;try{const url=new URL(address);url.hash='';canonical=url.username||url.password?'invalid-address':url.href;}catch{canonical=address;}
36
+ if(seen.has(canonical)){reports.push({...seen.get(canonical),duplicate:true});continue;}
37
+ let report;try{const card=await convertPage(address,options);cards.push(card);report={source:canonical,status:'converted',card_id:card.id};}catch(error){report={source:canonical,status:'unreadable',reason:error.code||'conversion_failed',message:error.code?error.message:'Conversion could not finish.'};}
38
+ reports.push(report);seen.set(canonical,report);
39
+ }
40
+ return {schema:'kloudy.sdf.card-list/1',cards,reports,ranking:'none',model_calls:0};
41
+ }
package/src/query.mjs CHANGED
@@ -1,28 +1,28 @@
1
- import {KloudyError} from './client.mjs';
2
- import {validateSDF} from './sdf.mjs';
3
- const fail=message=>{throw new KloudyError('invalid_query',message);};
4
- const engineBytes=value=>Buffer.byteLength(JSON.stringify(value).replace(/[^\x00-\x7f]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0')));
5
- export function queryInput(need,{scope,limit=3}={}){
6
- if(typeof need!=='string'||!need.trim()||need.length>1000||/[\x00-\x1f]/.test(need)||!Number.isInteger(limit)||limit<1||limit>5)fail('Use a need of 1–1000 characters and a result limit of 1–5.');
7
- if(scope!==undefined){if(!scope||typeof scope!=='object'||Array.isArray(scope)||Object.keys(scope).some(k=>!['sources','discovered_via','service_area'].includes(k)))fail('Use the documented discovery scope.');
8
- if(scope.sources!==undefined&&(!Array.isArray(scope.sources)||scope.sources.length>8||new Set(scope.sources).size!==scope.sources.length||scope.sources.some(s=>typeof s!=='string'||s.length>512||!safeURL(s))))fail('Scope sources must be up to eight distinct public URLs.');
9
- if(scope.discovered_via!==undefined&&(!Array.isArray(scope.discovered_via)||!scope.discovered_via.length||scope.discovered_via.length>2||new Set(scope.discovered_via).size!==scope.discovered_via.length||scope.discovered_via.some(s=>!['seo','geo'].includes(s))))fail('Discovery scope is seo and/or geo.');
10
- if(scope.service_area!==undefined&&(typeof scope.service_area!=='string'||!scope.service_area.trim()||scope.service_area.length>80||/[\x00-\x1f]/.test(scope.service_area)))fail('Use a service area of 1–80 characters.');
11
- }
12
- const input={version:'kldy.query/1',need,limit,...(scope?{scope}:{})};if(engineBytes(input)>2048)fail('The complete query must fit in 2 KiB.');return input;
13
- }
14
- function safeURL(value){try{const u=new URL(value);return ['https:','http:'].includes(u.protocol)&&!u.username&&!u.password;}catch{return false;}}
15
- export function queryResult(result,limit){
16
- if(!result||Object.keys(result).some(k=>!['version','query_sha256','state','matches','execution_started','permission_granted','next','measurements'].includes(k)))throw new KloudyError('invalid_response','Expected card references, not additional payloads.');
17
- const allowed=['card_id','title','source_url','content_sha256','score','provenance','expires_at'];
18
- if(result?.version!=='kldy.query/1'||!['matched','no_match'].includes(result.state)||!Array.isArray(result.matches)||result.matches.length>limit||result.execution_started!==false||result.permission_granted!==false||result.measurements?.definitions_emitted!==0||result.matches.some(m=>!m||Object.keys(m).some(k=>!allowed.includes(k))||typeof m.card_id!=='string'||typeof m.title!=='string'||!safeURL(m.source_url)||!/^[a-f0-9]{64}$/.test(m.content_sha256)||!Number.isFinite(m.expires_at)))throw new KloudyError('invalid_response','Expected bounded KLDY card references without tool definitions or permission.');
19
- return result;
20
- }
21
- export function sdfIndex(cards,{discoveredVia,ttlSeconds=3600,now=Date.now()/1000}={}){
22
- if(!Array.isArray(cards)||cards.length>64||!['seo','geo'].includes(discoveredVia)||!Number.isInteger(ttlSeconds)||ttlSeconds<1||ttlSeconds>86400)fail('Provide up to 64 validated cards, actual SEO/GEO provenance, and a lifetime of 1–86400 seconds.');
23
- const seen=new Set(),entries=[];
24
- for(const card of cards){if(!validateSDF(card).valid)fail('Every indexed card must pass SDF validation.');if(seen.has(card.id))continue;seen.add(card.id);const fetched=Date.parse(card.source.fetched_at)/1000,expires=Math.min(now+ttlSeconds,fetched+86400);if(expires<=now||fetched>now+300)fail('Refresh expired or future-dated source content before indexing.');
25
- entries.push({id:card.id,source_url:card.source.url,discovered_via:discoveredVia,title:card.source.title.slice(0,120),summary:card.summary.brief,tags:[],capability_ids:[],service_areas:[],content_sha256:card.provenance.content_hash.slice(7),expires_at:expires});
26
- }
27
- const envelope={version:'bbe.sdf-index/1',cards:entries};if(engineBytes(envelope)>65536)fail('Split this index into smaller batches; the engine limit is 64 KiB.');return envelope;
28
- }
1
+ import {KloudyError} from './client.mjs';
2
+ import {validateSDF} from './sdf.mjs';
3
+ const fail=message=>{throw new KloudyError('invalid_query',message);};
4
+ const engineBytes=value=>Buffer.byteLength(JSON.stringify(value).replace(/[^\x00-\x7f]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0')));
5
+ export function queryInput(need,{scope,limit=3}={}){
6
+ if(typeof need!=='string'||!need.trim()||need.length>1000||/[\x00-\x1f]/.test(need)||!Number.isInteger(limit)||limit<1||limit>5)fail('Use a need of 1–1000 characters and a result limit of 1–5.');
7
+ if(scope!==undefined){if(!scope||typeof scope!=='object'||Array.isArray(scope)||Object.keys(scope).some(k=>!['sources','discovered_via','service_area'].includes(k)))fail('Use the documented discovery scope.');
8
+ if(scope.sources!==undefined&&(!Array.isArray(scope.sources)||scope.sources.length>8||new Set(scope.sources).size!==scope.sources.length||scope.sources.some(s=>typeof s!=='string'||s.length>512||!safeURL(s))))fail('Scope sources must be up to eight distinct public URLs.');
9
+ if(scope.discovered_via!==undefined&&(!Array.isArray(scope.discovered_via)||!scope.discovered_via.length||scope.discovered_via.length>2||new Set(scope.discovered_via).size!==scope.discovered_via.length||scope.discovered_via.some(s=>!['seo','geo'].includes(s))))fail('Discovery scope is seo and/or geo.');
10
+ if(scope.service_area!==undefined&&(typeof scope.service_area!=='string'||!scope.service_area.trim()||scope.service_area.length>80||/[\x00-\x1f]/.test(scope.service_area)))fail('Use a service area of 1–80 characters.');
11
+ }
12
+ const input={version:'kldy.query/1',need,limit,...(scope?{scope}:{})};if(engineBytes(input)>2048)fail('The complete query must fit in 2 KiB.');return input;
13
+ }
14
+ function safeURL(value){try{const u=new URL(value);return ['https:','http:'].includes(u.protocol)&&!u.username&&!u.password;}catch{return false;}}
15
+ export function queryResult(result,limit){
16
+ if(!result||Object.keys(result).some(k=>!['version','query_sha256','state','matches','execution_started','permission_granted','next','measurements'].includes(k)))throw new KloudyError('invalid_response','Expected card references, not additional payloads.');
17
+ const allowed=['card_id','title','source_url','content_sha256','score','provenance','expires_at'];
18
+ if(result?.version!=='kldy.query/1'||!['matched','no_match'].includes(result.state)||!Array.isArray(result.matches)||result.matches.length>limit||result.execution_started!==false||result.permission_granted!==false||result.measurements?.definitions_emitted!==0||result.matches.some(m=>!m||Object.keys(m).some(k=>!allowed.includes(k))||typeof m.card_id!=='string'||typeof m.title!=='string'||!safeURL(m.source_url)||!/^[a-f0-9]{64}$/.test(m.content_sha256)||!Number.isFinite(m.expires_at)))throw new KloudyError('invalid_response','Expected bounded KLDY card references without tool definitions or permission.');
19
+ return result;
20
+ }
21
+ export function sdfIndex(cards,{discoveredVia,ttlSeconds=3600,now=Date.now()/1000}={}){
22
+ if(!Array.isArray(cards)||cards.length>64||!['seo','geo'].includes(discoveredVia)||!Number.isInteger(ttlSeconds)||ttlSeconds<1||ttlSeconds>86400)fail('Provide up to 64 validated cards, actual SEO/GEO provenance, and a lifetime of 1–86400 seconds.');
23
+ const seen=new Set(),entries=[];
24
+ for(const card of cards){if(!validateSDF(card).valid)fail('Every indexed card must pass SDF validation.');if(seen.has(card.id))continue;seen.add(card.id);const fetched=Date.parse(card.source.fetched_at)/1000,expires=Math.min(now+ttlSeconds,fetched+86400);if(expires<=now||fetched>now+300)fail('Refresh expired or future-dated source content before indexing.');
25
+ entries.push({id:card.id,source_url:card.source.url,discovered_via:discoveredVia,title:card.source.title.slice(0,120),summary:card.summary.brief,tags:[],capability_ids:[],service_areas:[],content_sha256:card.provenance.content_hash.slice(7),expires_at:expires});
26
+ }
27
+ const envelope={version:'bbe.sdf-index/1',cards:entries};if(engineBytes(envelope)>65536)fail('Split this index into smaller batches; the engine limit is 64 KiB.');return envelope;
28
+ }
package/src/remote.mjs CHANGED
@@ -8,6 +8,7 @@ import {ListToolsRequestSchema,CallToolRequestSchema,ListResourcesRequestSchema,
8
8
  import {REMOTE} from './editor-install.mjs';
9
9
  import {autoupdateEnabled} from './autoupdate.mjs';
10
10
  import {chooserResponse,pendingChooserResponse,pendingChooserTool} from './chooser-link.mjs';
11
+ import {WebsiteSelectionReturn,websiteReturnTool} from './website-return.mjs';
11
12
  const transports=new WeakMap();
12
13
  // This proxy can relay Apps rendering, but cannot service sampling, roots or elicitation.
13
14
  export function forwardedCapabilities(value={}){
@@ -30,6 +31,7 @@ export async function askRemote(question,options){
30
31
  export async function serveRemote(options={}){
31
32
  const connect=options.connect??connectRemote,enabled=autoupdateEnabled(options.autoupdate??process.env.KLOUDY_MCP_AUTOUPDATE);
32
33
  let remote=await connect({...options,capabilities:{}}),ready=Promise.resolve(),closed=false,reconnecting,pendingChooser,requestGoal;
34
+ const attachment=options.websiteReturn??(options.connect?null:new WebsiteSelectionReturn());
33
35
  const upstream=remote.getServerCapabilities(),capabilities={};
34
36
  if(upstream.tools)capabilities.tools=enabled&&upstream.tools.listChanged?{listChanged:true}:{};
35
37
  if(upstream.resources)capabilities.resources=enabled&&upstream.resources.listChanged?{listChanged:true}:{};
@@ -58,9 +60,10 @@ export async function serveRemote(options={}){
58
60
  if(capabilities.tools){
59
61
  server.setRequestHandler(ListToolsRequestSchema,async req=>{
60
62
  const result=await invoke('listTools',req.params);
61
- return {...result,tools:[...result.tools.map(tool=>tool.name==='show_visual_setup_choices'?{...tool,inputSchema:{...tool.inputSchema,properties:{...tool.inputSchema.properties,goal:{type:'string',minLength:1,maxLength:1000,description:'Presentation request retained in this MCP process; never sent as a credential or URL.'}}}}:tool),...(result.tools.some(t=>t.name===pendingChooserTool.name)?[]:[pendingChooserTool])]};
63
+ return {...result,tools:[...result.tools.map(tool=>tool.name==='show_visual_setup_choices'?{...tool,inputSchema:{...tool.inputSchema,properties:{...tool.inputSchema.properties,goal:{type:'string',minLength:1,maxLength:1000,description:'Presentation request retained in this MCP process; never sent as a credential or URL.'}}}}:tool),...(result.tools.some(t=>t.name===pendingChooserTool.name)?[]:[pendingChooserTool]),websiteReturnTool]};
62
64
  });
63
65
  server.setRequestHandler(CallToolRequestSchema,async req=>{
66
+ if(req.params.name===websiteReturnTool.name){const args=req.params.arguments||{};if(Object.keys(args).some(k=>k!=='waitSeconds'))throw Error('Return lookup accepts a bounded wait only');const result=attachment?await attachment.receive(args):{state:'unavailable'};if(result.state?.startsWith('received')&&pendingChooser)pendingChooser={...pendingChooser,structuredContent:{...pendingChooser.structuredContent,authenticatedReturn:result}};return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};}
64
67
  if(req.params.name===pendingChooserTool.name){if(Object.keys(req.params.arguments||{}).length)throw Error('Pending chooser lookup takes no identity or credential fields');return pendingChooserResponse(pendingChooser);}
65
68
  if(req.params.name!=='show_visual_setup_choices')return invoke('callTool',req.params);
66
69
  const argumentsValue={...(req.params.arguments||{})};
@@ -68,6 +71,10 @@ export async function serveRemote(options={}){
68
71
  if(Object.hasOwn(argumentsValue,'goal')){if(typeof argumentsValue.goal!=='string'||!argumentsValue.goal.trim()||argumentsValue.goal.length>1000)throw Error('Use a presentation request of 1-1000 characters');nextGoal=argumentsValue.goal;delete argumentsValue.goal;}
69
72
  const result=await invoke('callTool',{...req.params,arguments:argumentsValue});
70
73
  const decorated=chooserResponse(result,{apps:!!forwardedCapabilities(server.getClientCapabilities()).experimental,goal:nextGoal});
74
+ if(!decorated.isError&&nextGoal&&attachment){
75
+ try{const link=await attachment.open(nextGoal),priorUrl=decorated.structuredContent.chooser.url;decorated.content=decorated.content.map(c=>c.type==='text'&&c.text.startsWith('Website chooser: ')?{...c,text:c.text.replace(priorUrl,link.chooserUrl)}:c);decorated.structuredContent.chooser={...decorated.structuredContent.chooser,url:link.chooserUrl,returnMethod:'paired-encrypted-website-selection'};decorated.content.push({type:'text',text:'Open '+link.chooserUrl+' in your signed-in account. Confirm the waiting IDE code '+link.pairCode+' and the existing project. Then return the selection; receive_website_selection waits on this same MCP connection. The locator grants no account authority.'});decorated.structuredContent.websiteReturn={...link,extensionRequired:false,engineAuthority:false};}
76
+ catch{decorated.structuredContent.websiteReturn={available:false,manualManifestFallback:true};}
77
+ }
71
78
  if(!decorated.isError){requestGoal=nextGoal;pendingChooser=decorated;}return decorated;
72
79
  });
73
80
  }
@@ -77,6 +84,6 @@ export async function serveRemote(options={}){
77
84
  server.setRequestHandler(ReadResourceRequestSchema,req=>invoke('readResource',req.params));
78
85
  server.setRequestHandler(ListResourceTemplatesRequestSchema,req=>invoke('listResourceTemplates',req.params));
79
86
  }
80
- server.onclose=()=>{closed=true;void ready.catch(()=>{}).then(()=>remote.close());};
87
+ server.onclose=()=>{closed=true;attachment?.close?.();void ready.catch(()=>{}).then(()=>remote.close());};
81
88
  await server.connect(options.transport??new StdioServerTransport());return server;
82
89
  }
package/src/replies.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import catalog from './reply-templates.json' with {type:'json'};
2
- // Plain text output only. Hosts must render as text, never trusted HTML.
3
- export function renderReply(state,slots){
4
- const template=catalog.templates[Object.hasOwn(catalog.templates,state)?state:'unknown'];
5
- if(!slots||Array.isArray(slots)||Object.keys(slots).length!==1||typeof slots.action!=='string'||!slots.action.trim()||slots.action.length>160||/[\u0000-\u001f\u007f]/.test(slots.action))throw Error('invalid_reply_slots');
6
- return {version:catalog.version,status:template.status,text:template.text.replace('{action}',()=>slots.action)};
7
- }
1
+ import catalog from './reply-templates.json' with {type:'json'};
2
+ // Plain text output only. Hosts must render as text, never trusted HTML.
3
+ export function renderReply(state,slots){
4
+ const template=catalog.templates[Object.hasOwn(catalog.templates,state)?state:'unknown'];
5
+ if(!slots||Array.isArray(slots)||Object.keys(slots).length!==1||typeof slots.action!=='string'||!slots.action.trim()||slots.action.length>160||/[\u0000-\u001f\u007f]/.test(slots.action))throw Error('invalid_reply_slots');
6
+ return {version:catalog.version,status:template.status,text:template.text.replace('{action}',()=>slots.action)};
7
+ }
@@ -1,12 +1,12 @@
1
- {
2
- "version": "kloudy.replies/1",
3
- "templates": {
4
- "awaiting_approval": {"status":"approval", "text":"{action} — Needs your approval."},
5
- "queued": {"status":"pending", "text":"{action} — Queued."},
6
- "running": {"status":"pending", "text":"{action} — Working."},
7
- "completed": {"status":"completed", "text":"{action} — Completed."},
8
- "failed": {"status":"failed", "text":"{action} — Failed."},
9
- "cancelled": {"status":"cancelled", "text":"{action} — Cancelled."},
10
- "unknown": {"status":"pending", "text":"{action} — Status not confirmed."}
11
- }
12
- }
1
+ {
2
+ "version": "kloudy.replies/1",
3
+ "templates": {
4
+ "awaiting_approval": {"status":"approval", "text":"{action} — Needs your approval."},
5
+ "queued": {"status":"pending", "text":"{action} — Queued."},
6
+ "running": {"status":"pending", "text":"{action} — Working."},
7
+ "completed": {"status":"completed", "text":"{action} — Completed."},
8
+ "failed": {"status":"failed", "text":"{action} — Failed."},
9
+ "cancelled": {"status":"cancelled", "text":"{action} — Cancelled."},
10
+ "unknown": {"status":"pending", "text":"{action} — Status not confirmed."}
11
+ }
12
+ }
@@ -1,31 +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>`;}
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>`;}
@@ -1,11 +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'}}]};}
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'}}]};}
@@ -1,47 +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 tools'],['hybrid','Mix of both'],['free_then_paid','Free tier, then ask']],
6
- target:[['local','This device'],['www','World Wide Web'],['bww','Bot Wide Web'],['mixed','A mix']],
7
- delivery:[['silent','Quietly'],['paid','Paid tools only'],['all','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 where people will use your projects.'},
16
- {key:'delivery',question:'How much should Kloudy show?',intro:'Quietly is the default. You can ask for updates instead.'}
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'?'Bot Wide Web sites need hosting: your computer, your cloud, or a paid host.':'New costs always need your approval. Ask which tools are connected anytime.'}</p><details><summary>How this works</summary><p>Quietly 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>`;}
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 tools'],['hybrid','Mix of both'],['free_then_paid','Free tier, then ask']],
6
+ target:[['local','This device'],['www','World Wide Web'],['bww','Bot Wide Web'],['mixed','A mix']],
7
+ delivery:[['silent','Quietly'],['paid','Paid tools only'],['all','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 where people will use your projects.'},
16
+ {key:'delivery',question:'How much should Kloudy show?',intro:'Quietly is the default. You can ask for updates instead.'}
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'?'Bot Wide Web sites need hosting: your computer, your cloud, or a paid host.':'New costs always need your approval. Ask which tools are connected anytime.'}</p><details><summary>How this works</summary><p>Quietly 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>`;}