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
package/src/models.mjs CHANGED
@@ -1,55 +1,55 @@
1
- // User-owned compute adapter. No provider provisioning, routing or vault lives here.
2
- import {KloudyError} from './client.mjs';
3
- const fail=(code,message)=>{throw new KloudyError(code,message);};
4
- const modelName=value=>typeof value==='string'&&/^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]{0,199}$/.test(value);
5
- export class ModelClient {
6
- constructor({endpoint='http://127.0.0.1:11434/v1',resolveKey=async()=>null,fetcher=fetch,timeoutMs=90000}={}){
7
- let url;try{url=new URL(endpoint);}catch{}
8
- const local=url&&['127.0.0.1','[::1]','localhost'].includes(url.hostname);
9
- if(!url||url.username||url.password||url.search||url.hash||!(url.protocol==='https:'||url.protocol==='http:'&&local))fail('invalid_model_endpoint','Use your HTTPS model endpoint or local Ollama endpoint without credentials in the URL.');
10
- if(typeof resolveKey!=='function'||!Number.isInteger(timeoutMs)||timeoutMs<1||timeoutMs>120000)fail('invalid_model_config','Invalid model client configuration.');
11
- this.endpoint=url.href.replace(/\/$/,'');this.local=local;this.ollama=local&&url.port==='11434'&&url.pathname.replace(/\/$/,'')==='/v1';this.resolveKey=resolveKey;this.fetcher=fetcher;this.timeoutMs=timeoutMs;
12
- }
13
- async request(path,body){
14
- let key;try{key=await this.resolveKey();}catch{fail('model_key_unavailable','The host key resolver is unavailable.');}
15
- if(key!=null&&(typeof key!=='string'||!key||key.length>8192||/[\r\n]/.test(key)))fail('model_key_unavailable','The configured model credential is unavailable.');
16
- if(!this.local&&!key)fail('model_key_unavailable','Connect your own gateway credential through your host’s protected key resolver.');
17
- try{
18
- const target=path==='/api/tags'&&this.ollama?new URL('/api/tags',this.endpoint).href:this.endpoint+path;
19
- const response=await this.fetcher(target,{method:body?'POST':'GET',redirect:'error',signal:AbortSignal.timeout(this.timeoutMs),headers:{accept:'application/json',...(key?{authorization:'Bearer '+key}:{}),...(body?{'content-type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});
20
- if(!response.ok)fail(response.status===401||response.status===403?'model_authorization_required':'model_unavailable','Your model endpoint rejected the request. Nothing was retried.');
21
- if(!response.headers.get('content-type')?.includes('application/json'))fail('invalid_model_response','Expected a JSON model response.');
22
- const reader=response.body.getReader();let size=0,chunks=[];
23
- try{for(;;){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>1048576)fail('model_response_limit','Model response exceeds the limit.');chunks.push(value);}}finally{await reader.cancel().catch(()=>{});}
24
- const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}
25
- let result;try{result=JSON.parse(new TextDecoder().decode(bytes));}catch{fail('invalid_model_response','Expected a valid JSON model response.');}
26
- // An upstream error/body must not echo a gateway secret into a tool result.
27
- if(key&&JSON.stringify(result).includes(key))fail('invalid_model_response','Model response contained protected connection data.');
28
- return result;
29
- }catch(error){if(error instanceof KloudyError)throw error;fail('model_unavailable','Your model endpoint is unavailable. Nothing was retried.');}
30
- }
31
- async list(){
32
- const response=await this.request('/models');
33
- if(!Array.isArray(response?.data)||response.data.length>512||response.data.some(x=>!modelName(x?.id)))fail('invalid_model_catalog','The model endpoint returned an invalid catalog.');
34
- return {schema:'kloudy.models/1',state:'reachable',compute:this.local?'local_endpoint':'user_provider',provider_bill:'user',inference_verified:false,models:[...new Set(response.data.map(x=>x.id))].slice(0,128).map(id=>({id,status:'advertised'})),truncated:new Set(response.data.map(x=>x.id)).size>128};
35
- }
36
- async complete({model,prompt,maxTokens=128,allowProviderCharge=false}={}){
37
- if(!modelName(model)||typeof prompt!=='string'||!prompt.trim()||prompt.length>16000||!Number.isInteger(maxTokens)||maxTokens<1||maxTokens>4096||typeof allowProviderCharge!=='boolean')fail('invalid_model_request','Supply a model, bounded prompt and token limit.');
38
- if(!this.ollama&&!allowProviderCharge)fail('provider_charge_approval_required','Explicit authorization is required before using a provider through this endpoint.');
39
- const catalog=await this.list();if(!catalog.models.some(x=>x.id===model))fail('model_not_available','That model is not advertised by your endpoint.');
40
- if(this.ollama&&!allowProviderCharge){
41
- const tags=await this.request('/api/tags');
42
- const installed=tags?.models?.find(x=>x.name===model||x.model===model);
43
- if(!installed||!Number.isSafeInteger(installed.size)||installed.size<=0||installed.remote_host||installed.remote_model||/:cloud(?:$|[-:])/.test(model))fail('provider_charge_approval_required','This model is not verified as locally installed. Review its provider cost before running it.');
44
- }
45
- const result=await this.request('/chat/completions',{model,messages:[{role:'user',content:prompt}],max_tokens:maxTokens,stream:false});
46
- const text=result?.choices?.[0]?.message?.content;
47
- if(typeof text!=='string'||!text.trim()||text.length>32000)fail('invalid_model_response','The model did not return a bounded text answer.');
48
- return {schema:'kloudy.model-result/1',model,text,compute:catalog.compute,provider_bill:'user',usage:result.usage&&['prompt_tokens','completion_tokens','total_tokens'].every(k=>Number.isSafeInteger(result.usage[k])&&result.usage[k]>=0)?Object.fromEntries(['prompt_tokens','completion_tokens','total_tokens'].map(k=>[k,result.usage[k]])):null};
49
- }
50
- }
51
- export function configuredModels(env=process.env){
52
- const name=env.KLOUDY_MODEL_KEY_ENV;
53
- if(name&&!/^[A-Z][A-Z0-9_]{0,99}$/.test(name))fail('invalid_model_config','Use a credential environment reference, never the key itself.');
54
- return new ModelClient({endpoint:env.KLOUDY_MODEL_ENDPOINT||undefined,resolveKey:async()=>name?env[name]??null:null});
55
- }
1
+ // User-owned compute adapter. No provider provisioning, routing or vault lives here.
2
+ import {KloudyError} from './client.mjs';
3
+ const fail=(code,message)=>{throw new KloudyError(code,message);};
4
+ const modelName=value=>typeof value==='string'&&/^[a-zA-Z0-9][a-zA-Z0-9._:/@+-]{0,199}$/.test(value);
5
+ export class ModelClient {
6
+ constructor({endpoint='http://127.0.0.1:11434/v1',resolveKey=async()=>null,fetcher=fetch,timeoutMs=90000}={}){
7
+ let url;try{url=new URL(endpoint);}catch{}
8
+ const local=url&&['127.0.0.1','[::1]','localhost'].includes(url.hostname);
9
+ if(!url||url.username||url.password||url.search||url.hash||!(url.protocol==='https:'||url.protocol==='http:'&&local))fail('invalid_model_endpoint','Use your HTTPS model endpoint or local Ollama endpoint without credentials in the URL.');
10
+ if(typeof resolveKey!=='function'||!Number.isInteger(timeoutMs)||timeoutMs<1||timeoutMs>120000)fail('invalid_model_config','Invalid model client configuration.');
11
+ this.endpoint=url.href.replace(/\/$/,'');this.local=local;this.ollama=local&&url.port==='11434'&&url.pathname.replace(/\/$/,'')==='/v1';this.resolveKey=resolveKey;this.fetcher=fetcher;this.timeoutMs=timeoutMs;
12
+ }
13
+ async request(path,body){
14
+ let key;try{key=await this.resolveKey();}catch{fail('model_key_unavailable','The host key resolver is unavailable.');}
15
+ if(key!=null&&(typeof key!=='string'||!key||key.length>8192||/[\r\n]/.test(key)))fail('model_key_unavailable','The configured model credential is unavailable.');
16
+ if(!this.local&&!key)fail('model_key_unavailable','Connect your own gateway credential through your host’s protected key resolver.');
17
+ try{
18
+ const target=path==='/api/tags'&&this.ollama?new URL('/api/tags',this.endpoint).href:this.endpoint+path;
19
+ const response=await this.fetcher(target,{method:body?'POST':'GET',redirect:'error',signal:AbortSignal.timeout(this.timeoutMs),headers:{accept:'application/json',...(key?{authorization:'Bearer '+key}:{}),...(body?{'content-type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});
20
+ if(!response.ok)fail(response.status===401||response.status===403?'model_authorization_required':'model_unavailable','Your model endpoint rejected the request. Nothing was retried.');
21
+ if(!response.headers.get('content-type')?.includes('application/json'))fail('invalid_model_response','Expected a JSON model response.');
22
+ const reader=response.body.getReader();let size=0,chunks=[];
23
+ try{for(;;){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>1048576)fail('model_response_limit','Model response exceeds the limit.');chunks.push(value);}}finally{await reader.cancel().catch(()=>{});}
24
+ const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}
25
+ let result;try{result=JSON.parse(new TextDecoder().decode(bytes));}catch{fail('invalid_model_response','Expected a valid JSON model response.');}
26
+ // An upstream error/body must not echo a gateway secret into a tool result.
27
+ if(key&&JSON.stringify(result).includes(key))fail('invalid_model_response','Model response contained protected connection data.');
28
+ return result;
29
+ }catch(error){if(error instanceof KloudyError)throw error;fail('model_unavailable','Your model endpoint is unavailable. Nothing was retried.');}
30
+ }
31
+ async list(){
32
+ const response=await this.request('/models');
33
+ if(!Array.isArray(response?.data)||response.data.length>512||response.data.some(x=>!modelName(x?.id)))fail('invalid_model_catalog','The model endpoint returned an invalid catalog.');
34
+ return {schema:'kloudy.models/1',state:'reachable',compute:this.local?'local_endpoint':'user_provider',provider_bill:'user',inference_verified:false,models:[...new Set(response.data.map(x=>x.id))].slice(0,128).map(id=>({id,status:'advertised'})),truncated:new Set(response.data.map(x=>x.id)).size>128};
35
+ }
36
+ async complete({model,prompt,maxTokens=128,allowProviderCharge=false}={}){
37
+ if(!modelName(model)||typeof prompt!=='string'||!prompt.trim()||prompt.length>16000||!Number.isInteger(maxTokens)||maxTokens<1||maxTokens>4096||typeof allowProviderCharge!=='boolean')fail('invalid_model_request','Supply a model, bounded prompt and token limit.');
38
+ if(!this.ollama&&!allowProviderCharge)fail('provider_charge_approval_required','Explicit authorization is required before using a provider through this endpoint.');
39
+ const catalog=await this.list();if(!catalog.models.some(x=>x.id===model))fail('model_not_available','That model is not advertised by your endpoint.');
40
+ if(this.ollama&&!allowProviderCharge){
41
+ const tags=await this.request('/api/tags');
42
+ const installed=tags?.models?.find(x=>x.name===model||x.model===model);
43
+ if(!installed||!Number.isSafeInteger(installed.size)||installed.size<=0||installed.remote_host||installed.remote_model||/:cloud(?:$|[-:])/.test(model))fail('provider_charge_approval_required','This model is not verified as locally installed. Review its provider cost before running it.');
44
+ }
45
+ const result=await this.request('/chat/completions',{model,messages:[{role:'user',content:prompt}],max_tokens:maxTokens,stream:false});
46
+ const text=result?.choices?.[0]?.message?.content;
47
+ if(typeof text!=='string'||!text.trim()||text.length>32000)fail('invalid_model_response','The model did not return a bounded text answer.');
48
+ return {schema:'kloudy.model-result/1',model,text,compute:catalog.compute,provider_bill:'user',usage:result.usage&&['prompt_tokens','completion_tokens','total_tokens'].every(k=>Number.isSafeInteger(result.usage[k])&&result.usage[k]>=0)?Object.fromEntries(['prompt_tokens','completion_tokens','total_tokens'].map(k=>[k,result.usage[k]])):null};
49
+ }
50
+ }
51
+ export function configuredModels(env=process.env){
52
+ const name=env.KLOUDY_MODEL_KEY_ENV;
53
+ if(name&&!/^[A-Z][A-Z0-9_]{0,99}$/.test(name))fail('invalid_model_config','Use a credential environment reference, never the key itself.');
54
+ return new ModelClient({endpoint:env.KLOUDY_MODEL_ENDPOINT||undefined,resolveKey:async()=>name?env[name]??null:null});
55
+ }
@@ -1,21 +1,21 @@
1
- import {preferencesHTML} from './preferences-ui.mjs';
2
- export const SAMPLE_URI='ui://widget/kloudy-sample-v1.html';
3
- export const sampleTool={
4
- name:'show_tool_preferences_card',title:'Tool preferences card',
5
- description:'Show three short questions about preferred tool types, where projects run, and how tool updates appear. Use when the user wants to set or change these preferences. Displays choices only; does not install or run tools. Show once, not for every tool.',
6
- annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},
7
- inputSchema:{type:'object',properties:{
8
- kind:{type:'string',enum:['preferences'],description:'Show the three tool-preference questions.'},
9
- surface:{type:'string',enum:['ide','chatbot','site'],default:'chatbot',description:'Where the card appears: an editor, a chatbot, or the Kloudy website. Changes wording only.'}
10
- },required:['kind'],additionalProperties:false},
11
- _meta:{ui:{resourceUri:SAMPLE_URI},'openai/outputTemplate':SAMPLE_URI}
12
- };
13
- export const sampleResource={uri:SAMPLE_URI,name:'Tool preferences card',description:'Three questions about tools, project location and updates.',mimeType:'text/html;profile=mcp-app'};
14
- export function sampleArguments(a){return !!a&&typeof a==='object'&&!Array.isArray(a)&&Object.keys(a).every(k=>['kind','surface'].includes(k))&&a.kind==='preferences'&&(a.surface===undefined||['ide','chatbot','site'].includes(a.surface));}
15
- export function sampleMessage(method,params){return !!params&&typeof params==='object'&&!Array.isArray(params)&&(method==='resources/read'&&params.uri===SAMPLE_URI&&Object.keys(params).length===1||method==='tools/call'&&['show_tool_preferences_card','kloudy_sample_card'].includes(params.name)&&Object.keys(params).every(k=>['name','arguments'].includes(k))&&sampleArguments(params.arguments));}
16
- export function sampleResult(kind='preferences',surface='chatbot'){
17
- if(!sampleArguments({kind,surface}))throw Error('Choose tool preferences.');
18
- const url='https://kloudy.ai/samples?frontdoor='+surface;
19
- return {content:[{type:'text',text:`Tool preferences card\n\n[Choose your tools, project location and updates](${url})\n\nThese choices do not install or run tools.`}],structuredContent:{version:'kloudy.sample/1',kind,surface,url,executed:false,authorizesSpending:false}};
20
- }
21
- export function sampleContents(){return {contents:[{uri:SAMPLE_URI,mimeType:sampleResource.mimeType,text:preferencesHTML,_meta:{ui:{prefersBorder:true,domain:'https://kloudy.ai',csp:{connectDomains:[],resourceDomains:[]}},'openai/widgetDescription':'Three short questions about tool preferences.','openai/widgetPrefersBorder':true,'openai/widgetCSP':{connect_domains:[],resource_domains:[]},'openai/widgetDomain':'https://kloudy.ai'}}]};}
1
+ import {preferencesHTML} from './preferences-ui.mjs';
2
+ export const SAMPLE_URI='ui://widget/kloudy-sample-v1.html';
3
+ export const sampleTool={
4
+ name:'show_tool_preferences_card',title:'Tool preferences card',
5
+ description:'Show three short questions about preferred tool types, where projects run, and how tool updates appear. Use when the user wants to set or change these preferences. Displays choices only; does not install or run tools. Show once, not for every tool.',
6
+ annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},
7
+ inputSchema:{type:'object',properties:{
8
+ kind:{type:'string',enum:['preferences'],description:'Show the three tool-preference questions.'},
9
+ surface:{type:'string',enum:['ide','chatbot','site'],default:'chatbot',description:'Where the card appears: an editor, a chatbot, or the Kloudy website. Changes wording only.'}
10
+ },required:['kind'],additionalProperties:false},
11
+ _meta:{ui:{resourceUri:SAMPLE_URI},'openai/outputTemplate':SAMPLE_URI}
12
+ };
13
+ export const sampleResource={uri:SAMPLE_URI,name:'Tool preferences card',description:'Three questions about tools, project location and updates.',mimeType:'text/html;profile=mcp-app'};
14
+ export function sampleArguments(a){return !!a&&typeof a==='object'&&!Array.isArray(a)&&Object.keys(a).every(k=>['kind','surface'].includes(k))&&a.kind==='preferences'&&(a.surface===undefined||['ide','chatbot','site'].includes(a.surface));}
15
+ export function sampleMessage(method,params){return !!params&&typeof params==='object'&&!Array.isArray(params)&&(method==='resources/read'&&params.uri===SAMPLE_URI&&Object.keys(params).length===1||method==='tools/call'&&['show_tool_preferences_card','kloudy_sample_card'].includes(params.name)&&Object.keys(params).every(k=>['name','arguments'].includes(k))&&sampleArguments(params.arguments));}
16
+ export function sampleResult(kind='preferences',surface='chatbot'){
17
+ if(!sampleArguments({kind,surface}))throw Error('Choose tool preferences.');
18
+ const url='https://kloudy.ai/samples?frontdoor='+surface;
19
+ return {content:[{type:'text',text:`Tool preferences card\n\n[Choose your tools, project location and updates](${url})\n\nThese choices do not install or run tools.`}],structuredContent:{version:'kloudy.sample/1',kind,surface,url,executed:false,authorizesSpending:false}};
20
+ }
21
+ export function sampleContents(){return {contents:[{uri:SAMPLE_URI,mimeType:sampleResource.mimeType,text:preferencesHTML,_meta:{ui:{prefersBorder:true,domain:'https://kloudy.ai',csp:{connectDomains:[],resourceDomains:[]}},'openai/widgetDescription':'Three short questions about tool preferences.','openai/widgetPrefersBorder':true,'openai/widgetCSP':{connect_domains:[],resource_domains:[]},'openai/widgetDomain':'https://kloudy.ai'}}]};}