kloudy 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MCP-DEFAULT.md +12 -12
- package/README.md +307 -307
- package/bin/entry.mjs +22 -22
- package/bin/kloudy.mjs +70 -70
- package/package.json +68 -68
- package/schemas/SDF-LICENSE.txt +21 -21
- package/schemas/sdf-document-0.2.schema.json +411 -411
- package/schemas/sdf-webpage-v1.schema.json +176 -176
- package/src/activity.mjs +23 -23
- package/src/approval.mjs +25 -25
- package/src/autoupdate.mjs +2 -0
- package/src/client.mjs +96 -96
- package/src/connect-card.mjs +7 -11
- package/src/connect-ui.mjs +2 -2
- package/src/connection.mjs +14 -14
- package/src/editor-install.mjs +22 -4
- package/src/hosted.mjs +40 -35
- package/src/httpk.mjs +64 -64
- package/src/install.mjs +35 -35
- package/src/introduce.mjs +33 -33
- package/src/mcp.mjs +72 -72
- package/src/models.mjs +55 -55
- package/src/preferences-ui.mjs +1 -1
- package/src/public.mjs +41 -41
- package/src/remote.mjs +59 -27
- package/src/sample-cards.mjs +1 -1
- package/src/sample-ui.mjs +1 -1
- package/src/starter.mjs +43 -43
- package/src/strings.mjs +51 -51
- package/src/wrap.mjs +58 -58
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/remote.mjs
CHANGED
|
@@ -1,27 +1,59 @@
|
|
|
1
|
-
import {readFileSync} from 'node:fs';
|
|
2
|
-
const packageVersion=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
|
|
3
|
-
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
-
import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
-
import {Server} from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
-
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
-
import {ListToolsRequestSchema,CallToolRequestSchema} from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
-
import {REMOTE} from './editor-install.mjs';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
import {readFileSync} from 'node:fs';
|
|
2
|
+
const packageVersion=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
|
|
3
|
+
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
|
+
import {Server} from '@modelcontextprotocol/sdk/server/index.js';
|
|
6
|
+
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import {ListToolsRequestSchema,CallToolRequestSchema,ListResourcesRequestSchema,ReadResourceRequestSchema,ToolListChangedNotificationSchema,ResourceListChangedNotificationSchema} from '@modelcontextprotocol/sdk/types.js';
|
|
8
|
+
import {REMOTE} from './editor-install.mjs';
|
|
9
|
+
import {autoupdateEnabled} from './autoupdate.mjs';
|
|
10
|
+
const transports=new WeakMap();
|
|
11
|
+
export async function connectRemote({key=process.env.KLOUDY_MCP_TOKEN,fetcher=fetch}={}){
|
|
12
|
+
if(key!==undefined&&!/^\S{20,512}$/.test(key))throw Error('Invalid key');
|
|
13
|
+
const client=new Client({name:'kloudy',version:packageVersion},{capabilities:{}});
|
|
14
|
+
const transport=new StreamableHTTPClientTransport(new URL(REMOTE),{requestInit:{headers:key?{Authorization:'Bearer '+key}:{},redirect:'error'},fetch:(url,options)=>fetcher(url,{...options,redirect:'error'}),reconnectionOptions:{maxRetries:0}});
|
|
15
|
+
transports.set(client,transport);
|
|
16
|
+
try{await client.connect(transport,{timeout:25000});return client;}catch(e){await client.close().catch(()=>{});throw e;}
|
|
17
|
+
}
|
|
18
|
+
export async function askRemote(question,options){
|
|
19
|
+
if(typeof question!=='string'||!question.trim()||question.length>1000)throw Error('Use a question of 1–1000 characters');
|
|
20
|
+
const client=await connectRemote(options);
|
|
21
|
+
try{return await client.callTool({name:'kloudy',arguments:{operation:'ask',goal:question}},undefined,{timeout:25000});}finally{await client.close();}
|
|
22
|
+
}
|
|
23
|
+
export async function serveRemote(options={}){
|
|
24
|
+
const enabled=autoupdateEnabled(options.autoupdate??process.env.KLOUDY_MCP_AUTOUPDATE);
|
|
25
|
+
let remote=await connectRemote(options),reconnecting;
|
|
26
|
+
const server=new Server({name:'kloudy',version:packageVersion},{capabilities:enabled?{tools:{listChanged:true},resources:{listChanged:true}}:{tools:{}},instructions:'Use Kloudy when mentioned, or to discover MCP tools and SDKs. Public discovery is free; account actions need an existing authorized key.'});
|
|
27
|
+
function forward(client){
|
|
28
|
+
if(!enabled)return;
|
|
29
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema,()=>server.sendToolListChanged());
|
|
30
|
+
client.setNotificationHandler(ResourceListChangedNotificationSchema,()=>server.sendResourceListChanged());
|
|
31
|
+
}
|
|
32
|
+
forward(remote);
|
|
33
|
+
async function invoke(method,params){
|
|
34
|
+
const current=remote;
|
|
35
|
+
const run=client=>method==='callTool'?client.callTool(params,undefined,{timeout:25000}):client[method](params);
|
|
36
|
+
try{return await run(current);}catch(error){
|
|
37
|
+
// Only an explicit HTTP 404 on an established session permits one retry.
|
|
38
|
+
// Ambiguous network/timeout errors and session-less 404s are never retried.
|
|
39
|
+
if(!enabled||error.code!==404||!transports.get(current)?.sessionId)throw error;
|
|
40
|
+
if(remote===current){
|
|
41
|
+
reconnecting??=(async()=>{
|
|
42
|
+
const next=await connectRemote(options);forward(next);remote=next;
|
|
43
|
+
await current.close().catch(()=>{});
|
|
44
|
+
await server.sendToolListChanged();await server.sendResourceListChanged();
|
|
45
|
+
})().finally(()=>{reconnecting=undefined;});
|
|
46
|
+
await reconnecting;
|
|
47
|
+
}
|
|
48
|
+
return run(remote);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
server.setRequestHandler(ListToolsRequestSchema,req=>invoke('listTools',req.params));
|
|
52
|
+
server.setRequestHandler(CallToolRequestSchema,req=>invoke('callTool',req.params));
|
|
53
|
+
if(enabled){
|
|
54
|
+
server.setRequestHandler(ListResourcesRequestSchema,req=>invoke('listResources',req.params));
|
|
55
|
+
server.setRequestHandler(ReadResourceRequestSchema,req=>invoke('readResource',req.params));
|
|
56
|
+
}
|
|
57
|
+
server.onclose=()=>{void remote.close();};
|
|
58
|
+
await server.connect(options.transport??new StdioServerTransport());return server;
|
|
59
|
+
}
|
package/src/sample-cards.mjs
CHANGED
|
@@ -42,6 +42,6 @@ export class SampleQueue{
|
|
|
42
42
|
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
43
43
|
export function samplePreferencesMarkup(flow,{surface='site',notice=''}={}){
|
|
44
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>${
|
|
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
46
|
}
|
|
47
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>`;}
|