kloudy 0.4.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.
@@ -0,0 +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;}
package/src/public.mjs ADDED
@@ -0,0 +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
+ }
package/src/query.mjs ADDED
@@ -0,0 +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
+ }
package/src/remote.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import {Client} from '@modelcontextprotocol/sdk/client/index.js';
2
+ import {StreamableHTTPClientTransport} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3
+ import {Server} from '@modelcontextprotocol/sdk/server/index.js';
4
+ import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {ListToolsRequestSchema,CallToolRequestSchema} from '@modelcontextprotocol/sdk/types.js';
6
+ import {REMOTE} from './editor-install.mjs';
7
+ export async function connectRemote({key=process.env.KLOUDY_MCP_TOKEN,fetcher=fetch}={}){
8
+ if(key!==undefined&&!/^\S{20,512}$/.test(key))throw Error('Invalid key');
9
+ const client=new Client({name:'kloudy',version:'0.4.0'},{capabilities:{}});
10
+ 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}});
11
+ try{await client.connect(transport,{timeout:25000});return client;}catch(e){await client.close().catch(()=>{});throw e;}
12
+ }
13
+ export async function askRemote(question,options){
14
+ if(typeof question!=='string'||!question.trim()||question.length>1000)throw Error('Use a question of 1–1000 characters');
15
+ const client=await connectRemote(options);
16
+ try{return await client.callTool({name:'kloudy',arguments:{operation:'ask',goal:question}},undefined,{timeout:25000});}finally{await client.close();}
17
+ }
18
+ export async function serveRemote(options){
19
+ const remote=await connectRemote(options);
20
+ const server=new Server({name:'kloudy',version:'0.4.0'},{capabilities:{tools:{}},instructions:'Use Kloudy when mentioned, or to discover MCP tools and SDKs. Public discovery is free; account actions need an existing authorized key.'});
21
+ server.setRequestHandler(ListToolsRequestSchema,req=>remote.listTools(req.params));
22
+ server.setRequestHandler(CallToolRequestSchema,req=>remote.callTool(req.params,undefined,{timeout:25000}));
23
+ server.onclose=()=>{void remote.close();};
24
+ await server.connect(new StdioServerTransport());return server;
25
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,23 @@
1
+ import {Tiktoken} from 'js-tiktoken/lite';
2
+ import ranks from 'js-tiktoken/ranks/cl100k_base';
3
+ // A reproducible local reference encoding, not a claim about every model.
4
+ const tokenizer=new Tiktoken(ranks);
5
+ export const sdfTokens=card=>tokenizer.encode(JSON.stringify(card),[],[]).length;
6
+ export function compactSDF(card){
7
+ const ext=card.extensions['x-kloudy'];
8
+ ext.tokenizer='cl100k_base';ext.token_limit=750;
9
+ while(sdfTokens(card)>750){
10
+ ext.truncated=true;
11
+ if(card.links.length){card.links.pop();continue;}
12
+ if(card.sections.length>1){card.sections.pop();continue;}
13
+ if(card.summary.key_points.length){card.summary.key_points.pop();continue;}
14
+ const section=card.sections[0];
15
+ if(section.summary.length>80){section.summary=shorter(section.summary);continue;}
16
+ if(card.summary.brief.length>80){card.summary.brief=shorter(card.summary.brief);continue;}
17
+ if(card.source.title.length>40){const title=shorter(card.source.title);card.source.title=title;card.metadata.title=title;card.type_data.headline=title;continue;}
18
+ if(section.title.length>40){section.title=shorter(section.title);continue;}
19
+ throw Error('Source attribution cannot fit the 750-token compact card. Use standard resolution.');
20
+ }
21
+ return card;
22
+ }
23
+ function shorter(text){const cut=Array.from(text).slice(0,Math.floor(Array.from(text).length*.75)).join('');return cut.replace(/\s+\S*$/,'')+'…';}
package/src/sdf.mjs ADDED
@@ -0,0 +1,12 @@
1
+ import Ajv from 'ajv/dist/2020.js';
2
+ import addFormats from 'ajv-formats';
3
+ import root from '../schemas/sdf-document-0.2.schema.json' with {type:'json'};
4
+ import profile from '../schemas/sdf-webpage-v1.schema.json' with {type:'json'};
5
+ const ajv=new Ajv({strict:false,allErrors:false});addFormats(ajv);ajv.addSchema(root);
6
+ const validate=ajv.compile(profile);
7
+ export function validateSDF(card){
8
+ if(!validate(card))return {valid:false,error:'The card does not match the pinned SDF webpage schema.'};
9
+ for(const value of [card.source.url,...card.links.map(l=>l.href)]){let url;try{url=new URL(value);}catch{return {valid:false,error:'Invalid source or link.'};}if(!['https:','http:'].includes(url.protocol)||url.username||url.password)return {valid:false,error:'Unsafe source or link.'};}
10
+ if(!/^[0-9a-f]{64}$/.test(card.provenance.content_hash.slice(7)))return {valid:false,error:'Missing content hash.'};
11
+ return {valid:true,profile:'kloudy.sdf.webpage/1',upstream:'sdfprotocol/sdf@e4619ad0b951ced8542692c6b624cb86794cb863'};
12
+ }
@@ -0,0 +1,43 @@
1
+ import {mkdir,writeFile} from 'node:fs/promises';
2
+ import {resolve,join} from 'node:path';
3
+ import {KloudyError} from './client.mjs';
4
+ export const starterFiles={
5
+ 'kloudy.json':JSON.stringify({version:'kloudy.project/1',deploymentTarget:'bww',html:'provider-opt-in',publication:'requires-authorized-hosting'},null,2)+'\n',
6
+ 'package.json':JSON.stringify({name:'my-kloudy-app',version:'1.0.0',private:true,type:'module',engines:{node:'>=20.19.0'},scripts:{start:'node app.mjs',card:'node card.mjs',toolbox:'node toolbox.mjs'},dependencies:{'kloudy':'https://kloudy.ai/downloads/kloudy-open-0.4.0.tgz'}},null,2)+'\n',
7
+ 'app.mjs':`import {readPublicPage} from 'kloudy/public';
8
+ try {
9
+ const page=await readPublicPage(process.argv[2]||'https://example.com');
10
+ console.log(JSON.stringify({...page,body:page.body.slice(0,6000)},null,2));
11
+ } catch(error) { console.error(error.code+': '+error.message);process.exitCode=1; }
12
+ `,
13
+ 'card.mjs':`import {readCard} from 'kloudy/public';
14
+ try { console.log(JSON.stringify(await readCard(process.argv[2]||'/world'),null,2)); }
15
+ catch(error) { console.error(error.code+': '+error.message);process.exitCode=1; }
16
+ `,
17
+ 'toolbox.mjs':`import {withConnection} from 'kloudy/session';
18
+ // The native host/operator provides the existing scoped connection outside this project.
19
+ // This selects definitions only. It never calls a tool automatically.
20
+ try { const selected=await withConnection(process.env.KLOUDY_CONNECTION,client=>client.ask(process.argv[2]||'Find a tool to read my notes'));console.log(JSON.stringify(selected.toolbox,null,2)); }
21
+ catch(error) { console.error(error.code+': '+error.message);process.exitCode=1; }
22
+ `,
23
+ '.gitignore':'node_modules/\n.env\n.env.*\n*connection*.json\n*session*.json\n',
24
+ 'README.md':`# Build with Kloudy
25
+
26
+ The default deployment target is the Bot Wide Web. kloudy.json records that choice; creating a starter does not publish anything or authorize hosting charges. HTML fragments are optional and controlled by the provider.\n\nNode.js 20+. Run npm install, then npm start. This reads https://example.com through Kloudy's public reader and prints source-attributed content. No account or model call is needed. Provider restrictions and shared rate limits apply.
27
+
28
+ - npm start -- https://example.com: read a public page; source HTML is data, never executed.
29
+ - npm run card -- /world: fetch the same bounded information card bots see.
30
+ - npm run toolbox: select a toolbox with an existing scoped connection. Keep KLOUDY_CONNECTION pointing to its private file outside this project. Missing/expired connections fail explicitly. This example never executes the selected tool.
31
+
32
+ Use kloudy install to attach the one MCP door to a supported IDE. Customer self-registration and permanent GlassBreak grants are not yet available. Do not place tokens in browser code, Git, screenshots or prompts. A catalog listing is not an executable provider integration.
33
+
34
+ The CLI, MCP and SDK share the engine's authorization boundary. https://kloudy.ai/build documents current availability and the next steps. The downloaded public adapter is Apache-2.0; it contains no private engine or vault implementation.
35
+ `
36
+ };
37
+ export async function createStarter(directory){
38
+ if(typeof directory!=='string'||!directory.trim())throw new KloudyError('invalid_directory','Choose a new starter directory.');
39
+ const target=resolve(directory);
40
+ try{await mkdir(target,{mode:0o700});}catch(error){throw new KloudyError(error.code==='EEXIST'?'directory_exists':'directory_unavailable','Choose a new directory inside an existing parent. Existing files were not overwritten.');}
41
+ for(const [name,content] of Object.entries(starterFiles))await writeFile(join(target,name),content,{flag:'wx',mode:0o600});
42
+ return {status:'created',directory:target,files:Object.keys(starterFiles),next:['npm install','npm start','npm run card'],accountRequired:false,toolExecution:'Requires an existing scoped connection; never automatic in this starter.'};
43
+ }
@@ -0,0 +1,5 @@
1
+ .kloudy-indicator{display:inline-flex;align-items:center;justify-content:center;flex-direction:column;gap:2px;min-width:40px;min-height:40px;font:10px/1 system-ui;color:inherit}
2
+ .kloudy-indicator-orb{position:relative;display:block;width:22px;height:22px;border-radius:50%;background:radial-gradient(circle at 35% 25%,#b477ff,#5c21b9 62%,#bd8ff6 68%,#431180 78%);box-shadow:0 0 5px #a574e6}
3
+ .kloudy-indicator-orb:before,.kloudy-indicator-orb:after{content:'';position:absolute;top:45%;width:3px;height:3px;border-radius:50%;background:#d0ffff;box-shadow:0 0 4px #68dcff}.kloudy-indicator-orb:before{left:27%}.kloudy-indicator-orb:after{right:27%}
4
+ @keyframes kloudy-status-idle{50%{filter:brightness(1.2);transform:translateX(.3px)}}@keyframes kloudy-status-think{50%{transform:scale(.25);filter:brightness(1.7)}}
5
+ @media(prefers-reduced-motion:no-preference){.kloudy-indicator-orb{animation:kloudy-status-idle 3.6s ease-in-out infinite}.kloudy-indicator[data-state=listening] .kloudy-indicator-orb{animation-duration:.6s}.kloudy-indicator[data-state=thinking] .kloudy-indicator-orb{animation:kloudy-status-think 2.4s ease-in-out infinite}.kloudy-indicator[data-state=speaking] .kloudy-indicator-orb{animation-duration:.35s}.kloudy-indicator:is([data-state=waiting],[data-state=error]) .kloudy-indicator-orb{animation:none}}
@@ -0,0 +1,51 @@
1
+ import {mkdir,readFile,writeFile,rename,rmdir} from 'node:fs/promises';
2
+ import {dirname,resolve} from 'node:path';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {KloudyError} from './client.mjs';
5
+ import {homedir} from 'node:os';
6
+ import {withConnection} from './connection.mjs';
7
+ const fail=(code,message)=>{throw new KloudyError(code,message);};
8
+ const id=value=>typeof value==='string'&&/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(value);
9
+ function selection(value){
10
+ if(!value||Object.keys(value).some(k=>!['source','kind','goal'].includes(k))||!id(value.source)||!['mcp','sdk','cli'].includes(value.kind)||typeof value.goal!=='string'||!value.goal.trim()||value.goal.length>500)fail('invalid_selection','Use a source alias, mcp/sdk/cli kind, and a goal of 1–500 characters. Credentials and tool definitions do not belong in a saved string.');
11
+ return {source:value.source,kind:value.kind,goal:value.goal.trim()};
12
+ }
13
+ // Durable intent references only. Engine-owned selections and schemas are refreshed
14
+ // at use time; this never replays a stale selection or invokes a provider action.
15
+ export class StringStore {
16
+ constructor({file,project,connect,now=()=>Date.now()}){if(!file||!project||typeof connect!=='function')fail('invalid_store','A private store, project ID and connection resolver are required.');this.file=resolve(file);this.project=resolve(project);this.connect=connect;this.now=now;}
17
+ async read(){try{const value=JSON.parse(await readFile(this.file,'utf8'));if(value.schema!=='kloudy.strings/1'||!Array.isArray(value.strings)||value.strings.length>100)throw Error();return value;}catch(error){if(error.code==='ENOENT')return {schema:'kloudy.strings/1',strings:[]};fail('invalid_store','The saved strings could not be read. No data was replaced.');}}
18
+ async mutate(work){await mkdir(dirname(this.file),{recursive:true,mode:0o700});const lock=this.file+'.lock';try{await mkdir(lock,{mode:0o700});}catch{fail('store_busy','Another strings operation is running. Retry after it finishes.');}try{const value=await this.read(),result=await work(value);const temp=this.file+'.'+randomUUID()+'.tmp';await writeFile(temp,JSON.stringify(value,null,2)+'\n',{flag:'wx',mode:0o600});await rename(temp,this.file);return result;}finally{await rmdir(lock);}}
19
+ async save(name,values){if(!id(name)||!Array.isArray(values)||!values.length||values.length>16)fail('invalid_string','Name the string and choose 1–16 source selections.');const choices=[...new Map(values.map(v=>{const s=selection(v);return[JSON.stringify(s),s];})).values()];return this.mutate(db=>{const old=db.strings.find(s=>s.project===this.project&&s.name===name);if(old)fail('already_saved','This string already exists. Remove it explicitly before replacing it.');if(db.strings.length>=100)fail('store_full','Remove an unused string first.');const value={id:randomUUID(),name,project:this.project,choices,createdAt:this.now(),lastUsedAt:null,retention:'temporary',status:'saved'};db.strings.push(value);return value;});}
20
+ async list(){return (await this.read()).strings.filter(s=>s.project===this.project);}
21
+ async remove(name){return this.mutate(db=>{const index=db.strings.findIndex(s=>s.project===this.project&&s.name===name);if(index<0)fail('not_found','No string with that name exists in this project.');db.strings.splice(index,1);return {name,status:'removed',providerCredentialsRevoked:false};});}
22
+ async keep(name){return this.mutate(db=>{const entry=db.strings.find(s=>s.project===this.project&&s.name===name);if(!entry)fail('not_found','No string with that name exists in this project.');entry.retention='kept';return {...entry,grantStatus:'not_requested',notice:'Saved for reuse. Permanent provider access still requires the existing GlassBreak authorization path.'};});}
23
+ async prune({idleDays=30}={}){if(!Number.isInteger(idleDays)||idleDays<1||idleDays>365)fail('invalid_retention','Use 1–365 idle days.');return this.mutate(db=>{const removed=[];db.strings=db.strings.filter(s=>{const dormant=s.project===this.project&&s.retention!=='kept'&&this.now()-(s.lastUsedAt??s.createdAt)>=idleDays*86400000;if(dormant)removed.push(s.name);return !dormant;});return {removed,providerCredentialsRevoked:false};});}
24
+ async use(name){
25
+ const entry=(await this.list()).find(s=>s.name===name);if(!entry)fail('not_found','No string with that name exists in this project.');
26
+ const tools=[],routes=[],selections=[],seen=new Map();
27
+ for(const raw of entry.choices){const choice=selection(raw);const selected=await this.connect(choice.source,choice.kind,client=>client.ask(choice.goal));const definitions=selected?.toolbox?.tools;if(!Array.isArray(definitions)||definitions.length!==1||!selected.selection_id||!selected.sessionId)fail('selection_unavailable','A saved choice is unavailable or needs renewed authorization. No action ran.');
28
+ const tool=definitions[0];if(typeof tool.name!=='string'||!tool.name||tool.name.length>128)fail('invalid_response','The source returned an invalid selected tool.');
29
+ const identity=choice.source+'\n'+tool.name;let alias=seen.get(identity);
30
+ if(!alias){if(tools.length===8)fail('toolbox_limit','This string selects more than eight tools. Split it into smaller task-specific strings.');alias='s'+tools.length+'_'+tool.name.replace(/[^a-zA-Z0-9_-]/g,'_');seen.set(identity,alias);tools.push({...tool,name:alias});}
31
+ const route={name:alias,source:choice.source,kind:choice.kind,tool:tool.name,selection_id:selected.selection_id,sessionId:selected.sessionId};routes.push(route);selections.push({...route,arguments:selected.arguments});
32
+ }
33
+ await this.mutate(db=>{const latest=db.strings.find(s=>s.project===this.project&&s.name===name);if(!latest||latest.id!==entry.id)fail('removed_during_use','This string was removed while selecting. Select again.');latest.lastUsedAt=this.now();latest.activeRoutes=routes;latest.status='saved';});
34
+ return {schema:'kloudy.toolbox/1',name,project:this.project,toolbox:{status:'ready',tools,measurements:{definitionsEmitted:tools.length}},routes,selections,notice:'Selections are session-scoped. No provider action was executed; the engine rechecks permission at call time.'};
35
+ }
36
+ async call(name,tool,inputs,{selectionId}={}){
37
+ const entry=(await this.list()).find(s=>s.name===name),matches=entry?.activeRoutes?.filter(r=>r.name===tool&&(!selectionId||r.selection_id===selectionId))||[];
38
+ if(matches.length>1)fail('ambiguous_selection','This tool has several owned selections. Choose the exact selection ID from the returned routes.');const route=matches[0];
39
+ if(!route)fail('not_selected','Use this string first, then choose one exact returned tool alias.');
40
+ return this.connect(route.source,route.kind,async client=>{await client.session();if(client.state.sessionId!==route.sessionId)fail('stale_selection','The connection session changed. Use this string again before calling a tool.');return client.call(route.tool,inputs,{selectionId:route.selection_id});});
41
+ }
42
+ }
43
+ export function localStrings({project=process.cwd(),home=homedir(),mode='autonomous'}={}){
44
+ const directory=resolve(home,'.kloudy');
45
+ return new StringStore({file:resolve(directory,'strings.json'),project,connect:async(alias,kind,work)=>{
46
+ let sources;try{sources=JSON.parse(await readFile(resolve(directory,'sources.json'),'utf8'));}catch{fail('sources_unavailable','Configure trusted source aliases in ~/.kloudy/sources.json first.');}
47
+ const source=Object.hasOwn(sources,alias)?sources[alias]:null;
48
+ if(!source||source.kind!==kind||typeof source.connection!=='string'||!source.connection)fail('source_unavailable','This source alias is missing or has a different adapter kind.');
49
+ return withConnection(resolve(directory,source.connection),work,{mode});
50
+ }});
51
+ }
@@ -0,0 +1,6 @@
1
+ // stderr only: structured stdout and MCP transports must remain machine-readable.
2
+ export function terminalStatus({stream=process.stderr,enabled=stream.isTTY&&!process.env.CI&&!process.env.NO_COLOR,interval=setInterval,clear=clearInterval}={}){
3
+ let timer=null,index=0;
4
+ const stop=()=>{if(timer){clear(timer);timer=null;}if(enabled)stream.write('\r\u001b[2K');};
5
+ return {update(event){if(!enabled||event?.schema!=='kloudy.activity/1'||event.source!=='kloudy')return;stop();if(event.state==='thinking'){const tick=()=>stream.write('\rKloudy is working'+['. ','.. ','...'][index++%3]);tick();timer=interval(tick,600);timer.unref?.();}else if(event.state==='waiting')stream.write('Kloudy needs your approval.\n');else if(event.state==='error')stream.write('Kloudy could not finish.\n');},dispose:stop};
6
+ }
@@ -0,0 +1,8 @@
1
+ export const cloudVisual=String.raw` .--------.
2
+ .---' .--. '---.
3
+ .-' .--' '--. '-.
4
+ ( [ K L O U D Y ] )
5
+ '----.____________.----'
6
+ :: BOT WIDE WEB ::`;
7
+ export const welcomeText='Welcome to Kloudy. You are at the Bot Wide Web door. Just type Kloudy to direct this IDE here; it can also use Kloudy for relevant tasks without that prefix. Only the tools needed for this project come in.';
8
+ export const welcomeContent=result=>[{type:'text',text:'```text\n'+cloudVisual+'\n```\n\n'+welcomeText+'\n\n'+JSON.stringify(result)}];
package/src/wrap.mjs ADDED
@@ -0,0 +1,58 @@
1
+ import {createServer} from 'node:http';
2
+ import {createHash,timingSafeEqual} from 'node:crypto';
3
+ import {Client} from '@modelcontextprotocol/sdk/client/index.js';
4
+ import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
5
+ import {Server} from '@modelcontextprotocol/sdk/server/index.js';
6
+ import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
7
+ import {ListToolsRequestSchema,CallToolRequestSchema} from '@modelcontextprotocol/sdk/types.js';
8
+ import {KloudyError} from './client.mjs';
9
+ const hash=x=>createHash('sha256').update(x).digest();
10
+ export function createWrapper({command,args=[],tools,token,env={},timeout=10000,maxConcurrent=2}){
11
+ if(typeof command!=='string'||!command||!Array.isArray(args)||args.some(x=>typeof x!=='string')||!Array.isArray(tools)||!tools.length||tools.length>8||tools.some(x=>typeof x!=='string'||!/^[-\w.]{1,100}$/.test(x))||typeof token!=='string'||token.length<24)throw new KloudyError('invalid_wrapper','Specify an approved command, 1–8 selected tools and a strong bearer from the local host.');
12
+ const allowed=new Set(tools);let active=0;
13
+ return createServer(async(req,res)=>{
14
+ const send=(status,error)=>{if(!res.headersSent){res.writeHead(status,{'content-type':'application/json','cache-control':'no-store'});res.end(JSON.stringify({error}));}};
15
+ // Loopback-only host and no browser-origin requests. TLS/public grant policy belongs at the owning deployment edge.
16
+ if(!/^(127\.0\.0\.1|localhost|\[::1\])(?::\d+)?$/.test(req.headers.host||'')||req.headers.origin)return send(403,'origin_not_allowed');
17
+ if(req.url!=='/mcp')return send(404,'not_found');
18
+ if(!timingSafeEqual(hash(req.headers.authorization||''),hash('Bearer '+token)))return send(401,'unauthorized');
19
+ if(req.method!=='POST')return send(405,'method_not_allowed');
20
+ if(!req.headers['content-type']?.startsWith('application/json'))return send(415,'json_required');
21
+ if(active>=maxConcurrent)return send(429,'busy');
22
+ active++;let upstream,server,transport,timer;
23
+ try{
24
+ let data='';for await(const chunk of req){data+=chunk;if(Buffer.byteLength(data)>65536)return send(413,'too_large');}
25
+ let body;try{body=JSON.parse(data);}catch{return send(400,'invalid_json');}
26
+ if(Array.isArray(body)||!body||body.jsonrpc!=='2.0')return send(400,'invalid_request');
27
+ if(body.method==='tools/call'&&!allowed.has(body.params?.name))return send(403,'tool_not_selected');
28
+ if(!['initialize','notifications/initialized','tools/list','tools/call','ping'].includes(body.method))return send(400,'unsupported_method');
29
+ const client=new Client({name:'kloudy-httpk-wrap',version:'0.1.0'});
30
+ upstream=new StdioClientTransport({command,args,env,stderr:'pipe'});
31
+ // No upstream stderr or credential values are reflected into HTTP responses or logs.
32
+ upstream.stderr?.on('data',()=>{});
33
+ timer=setTimeout(()=>{send(504,'upstream_timeout');upstream.close().catch(()=>{});},timeout);
34
+ await client.connect(upstream);
35
+ let callResult;
36
+ if(body.method==='tools/call')callResult=await client.callTool(body.params,undefined,{timeout});
37
+ server=new Server({name:'kloudy-httpk',version:'0.1.0'},{capabilities:{tools:{}}});
38
+ server.setRequestHandler(ListToolsRequestSchema,async()=>{
39
+ const selected=[];let cursor;
40
+ for(let page=0;page<10;page++){
41
+ const value=await client.listTools(cursor?{cursor}:undefined);
42
+ for(const tool of value.tools)if(allowed.has(tool.name))selected.push({name:tool.name,description:tool.description,inputSchema:tool.inputSchema});
43
+ cursor=value.nextCursor;if(!cursor)break;
44
+ if(page===9)throw new Error('Catalog limit');
45
+ }
46
+ return {tools:selected};
47
+ });
48
+ server.setRequestHandler(CallToolRequestSchema,async request=>{
49
+ if(!allowed.has(request.params.name))throw new Error('Tool not selected');
50
+ return callResult;
51
+ });
52
+ transport=new StreamableHTTPServerTransport({sessionIdGenerator:undefined,enableJsonResponse:true});
53
+ await server.connect(transport);
54
+ await transport.handleRequest(req,res,body);
55
+ }catch{send(502,'upstream_unavailable');}
56
+ finally{clearTimeout(timer);await transport?.close().catch(()=>{});await server?.close().catch(()=>{});await upstream?.close().catch(()=>{});active--;}
57
+ });
58
+ }