kloudy 0.4.8 → 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 (51) hide show
  1. package/LICENSE +202 -202
  2. package/MCP-DEFAULT.md +12 -12
  3. package/README.md +317 -315
  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/replies.mjs +7 -7
  38. package/src/reply-templates.json +12 -12
  39. package/src/sample-actions.mjs +31 -31
  40. package/src/sample-card.mjs +11 -11
  41. package/src/sample-cards.mjs +47 -47
  42. package/src/sample-ui.mjs +2 -2
  43. package/src/sdf-budget.mjs +23 -23
  44. package/src/sdf.mjs +12 -12
  45. package/src/starter.mjs +43 -43
  46. package/src/status-indicator.css +5 -5
  47. package/src/strings.mjs +51 -51
  48. package/src/terminal-status.mjs +6 -6
  49. package/src/website-return.mjs +18 -18
  50. package/src/welcome.mjs +8 -8
  51. package/src/wrap.mjs +58 -58
@@ -1,23 +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*$/,'')+'…';}
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 CHANGED
@@ -1,12 +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
- }
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
+ }
package/src/starter.mjs CHANGED
@@ -1,43 +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':'latest'}},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 npx -y kloudy@latest 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
- }
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':'latest'}},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 npx -y kloudy@latest 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
+ }
@@ -1,5 +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}}
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}}
package/src/strings.mjs CHANGED
@@ -1,51 +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
- }
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
+ }
@@ -1,6 +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
- }
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
+ }
@@ -1,18 +1,18 @@
1
- // Delivery keys protect inert presentation data. They never authorize engine calls.
2
- import {generateKeyPairSync,createPublicKey,diffieHellman,hkdfSync,createDecipheriv,createHash} from 'node:crypto';
3
- const version='kloudy.website-return/1',hash=s=>createHash('sha256').update(s).digest('hex'),origin='https://kloudy.ai',root=origin+'/api/customer/visual-chooser/return/';
4
- export const websiteReturnTool={name:'receive_website_selection',description:'Wait for the exact inert selection returned by the signed-in website to this MCP connection. No IDE extension, website cookie, account token, model or grant is needed. Use after showing the chooser link. A waiting result is safe to check again; do not repeat mutations after uncertain outcomes.',inputSchema:{type:'object',additionalProperties:false,properties:{waitSeconds:{type:'integer',minimum:0,maximum:45}}},annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}};
5
- export class WebsiteSelectionReturn{
6
- constructor({fetcher=fetch,clock=()=>Date.now(),sleep=ms=>new Promise(r=>setTimeout(r,ms))}={}){Object.assign(this,{fetcher,clock,sleep});this.keys=generateKeyPairSync('x25519');this.publicKey=this.keys.publicKey.export({format:'jwk'}).x;this.sequence=0;this.receipt=null;}
7
- async request(action,input){const response=await this.fetcher(root+action,{method:'POST',headers:{origin,'content-type':'application/json'},body:JSON.stringify(input),redirect:'error',signal:AbortSignal.timeout(10000)});const text=await response.text();if(text.length>300000)throw Error('Return response exceeds the bounded manifest');const value=JSON.parse(text);if(!response.ok)throw Error(typeof value.error==='string'?value.error:'Website return unavailable');return value;}
8
- async open(goal){if(this.link)return this.link;if(typeof goal!=='string'||!goal.trim()||goal.length>1000)throw Error('Use a presentation goal up to 1000 characters');const result=await this.request('open',{publicKey:this.publicKey,goal});if(result.version!==version||!/^[a-f0-9]{32}$/.test(result.id)||result.linkIsAuthorization!==false||result.chooserUrl!==origin+'/api/customer/visual-chooser/start#return='+result.id||result.pairCode!==hash(result.id+':'+this.publicKey).slice(0,8).toUpperCase())throw Error('Unexpected website return attachment');this.link=result;return result;}
9
- decrypt(envelope){
10
- if(envelope?.version!==version||envelope.id!==this.link?.id||envelope.sequence!==this.sequence+1||!/^[A-Za-z0-9_-]{43}$/.test(envelope.serverPublicKey)||!/^[A-Za-z0-9_-]{16}$/.test(envelope.iv)||!/^[A-Za-z0-9_-]{22}$/.test(envelope.tag)||typeof envelope.ciphertext!=='string'||envelope.ciphertext.length>270000)throw Error('Unexpected return envelope');
11
- const aad=JSON.stringify([version,this.link.id,hash(this.publicKey),envelope.sequence]),peer=createPublicKey({key:{kty:'OKP',crv:'X25519',x:envelope.serverPublicKey},format:'jwk'}),secret=diffieHellman({privateKey:this.keys.privateKey,publicKey:peer}),key=Buffer.from(hkdfSync('sha256',secret,Buffer.alloc(0),Buffer.from(aad),32)),decipher=createDecipheriv('aes-256-gcm',key,Buffer.from(envelope.iv,'base64url'));decipher.setAAD(Buffer.from(aad));decipher.setAuthTag(Buffer.from(envelope.tag,'base64url'));const payload=JSON.parse(Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext,'base64url')),decipher.final()]).toString()),s=payload.result?.structuredContent,m=s?.selectedManifest;
12
- if(payload.version!==version||payload.accountProjectVerified!==true||payload.executionAllowed!==false||typeof payload.conversationId!=='string'||typeof payload.receiptId!=='string'||payload.projectId!==m?.projectId||payload.projectId!==s?.projectState?.projectId||payload.revision!==s.revision||m.revision!==s.revision||s.executed!==false||m.executionAllowed!==false||m.authorizesSpending!==false||m.authorizesInstallation!==false)throw Error('Website selection is not an exact inert project manifest');
13
- if(this.receipt&&(payload.projectId!==this.receipt.projectId||payload.conversationId!==this.receipt.conversationId||s.projectState.originalGoal!==this.receipt.result.structuredContent.projectState.originalGoal))throw Error('Account project or original conversation changed');
14
- if(typeof payload.ackNonce!=='string'||!/^[A-Za-z0-9_-]{43}$/.test(payload.ackNonce))throw Error('Missing exact delivery acknowledgment');this.pendingAck={id:this.link.id,sequence:envelope.sequence,nonce:payload.ackNonce};delete payload.ackNonce;this.sequence=envelope.sequence;this.receipt=payload;return payload;
15
- }
16
- async receive({waitSeconds=45}={}){if(!Number.isInteger(waitSeconds)||waitSeconds<0||waitSeconds>45)throw Error('Wait up to 45 seconds');if(!this.link)return {state:'not-open',instructions:'Show the visual chooser with your goal first.'};if(this.pendingAck){await this.request('ack',this.pendingAck);this.pendingAck=null;return {state:'received',...this.receipt};}const end=this.clock()+waitSeconds*1000;do{const value=await this.request('poll',{id:this.link.id,afterSequence:this.sequence});if(value.version!==version||!['waiting','ready'].includes(value.state))throw Error('Unknown website return state');if(value.state==='ready'){const receipt=this.decrypt(value.envelope);try{await this.request('ack',this.pendingAck);this.pendingAck=null;return {state:'received',...receipt};}catch{return {state:'received-acknowledgment-unconfirmed',...receipt};}}if(this.clock()>=end)break;await this.sleep(Math.min(2000,end-this.clock()));}while(this.clock()<=end);return {state:'waiting',chooserUrl:this.link.chooserUrl,pairCode:this.link.pairCode,conversationId:this.receipt?.conversationId??null,lastReceipt:this.receipt??null};}
17
- close(){this.keys=null;this.link=null;this.receipt=null;}
18
- }
1
+ // Delivery keys protect inert presentation data. They never authorize engine calls.
2
+ import {generateKeyPairSync,createPublicKey,diffieHellman,hkdfSync,createDecipheriv,createHash} from 'node:crypto';
3
+ const version='kloudy.website-return/1',hash=s=>createHash('sha256').update(s).digest('hex'),origin='https://kloudy.ai',root=origin+'/api/customer/visual-chooser/return/';
4
+ export const websiteReturnTool={name:'receive_website_selection',description:'Wait for the exact inert selection returned by the signed-in website to this MCP connection. No IDE extension, website cookie, account token, model or grant is needed. Use after showing the chooser link. A waiting result is safe to check again; do not repeat mutations after uncertain outcomes.',inputSchema:{type:'object',additionalProperties:false,properties:{waitSeconds:{type:'integer',minimum:0,maximum:45}}},annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}};
5
+ export class WebsiteSelectionReturn{
6
+ constructor({fetcher=fetch,clock=()=>Date.now(),sleep=ms=>new Promise(r=>setTimeout(r,ms))}={}){Object.assign(this,{fetcher,clock,sleep});this.keys=generateKeyPairSync('x25519');this.publicKey=this.keys.publicKey.export({format:'jwk'}).x;this.sequence=0;this.receipt=null;}
7
+ async request(action,input){const response=await this.fetcher(root+action,{method:'POST',headers:{origin,'content-type':'application/json'},body:JSON.stringify(input),redirect:'error',signal:AbortSignal.timeout(10000)});const text=await response.text();if(text.length>300000)throw Error('Return response exceeds the bounded manifest');const value=JSON.parse(text);if(!response.ok)throw Error(typeof value.error==='string'?value.error:'Website return unavailable');return value;}
8
+ async open(goal){if(this.link)return this.link;if(typeof goal!=='string'||!goal.trim()||goal.length>1000)throw Error('Use a presentation goal up to 1000 characters');const result=await this.request('open',{publicKey:this.publicKey,goal});if(result.version!==version||!/^[a-f0-9]{32}$/.test(result.id)||result.linkIsAuthorization!==false||result.chooserUrl!==origin+'/api/customer/visual-chooser/start#return='+result.id||result.pairCode!==hash(result.id+':'+this.publicKey).slice(0,8).toUpperCase())throw Error('Unexpected website return attachment');this.link=result;return result;}
9
+ decrypt(envelope){
10
+ if(envelope?.version!==version||envelope.id!==this.link?.id||envelope.sequence!==this.sequence+1||!/^[A-Za-z0-9_-]{43}$/.test(envelope.serverPublicKey)||!/^[A-Za-z0-9_-]{16}$/.test(envelope.iv)||!/^[A-Za-z0-9_-]{22}$/.test(envelope.tag)||typeof envelope.ciphertext!=='string'||envelope.ciphertext.length>270000)throw Error('Unexpected return envelope');
11
+ const aad=JSON.stringify([version,this.link.id,hash(this.publicKey),envelope.sequence]),peer=createPublicKey({key:{kty:'OKP',crv:'X25519',x:envelope.serverPublicKey},format:'jwk'}),secret=diffieHellman({privateKey:this.keys.privateKey,publicKey:peer}),key=Buffer.from(hkdfSync('sha256',secret,Buffer.alloc(0),Buffer.from(aad),32)),decipher=createDecipheriv('aes-256-gcm',key,Buffer.from(envelope.iv,'base64url'));decipher.setAAD(Buffer.from(aad));decipher.setAuthTag(Buffer.from(envelope.tag,'base64url'));const payload=JSON.parse(Buffer.concat([decipher.update(Buffer.from(envelope.ciphertext,'base64url')),decipher.final()]).toString()),s=payload.result?.structuredContent,m=s?.selectedManifest;
12
+ if(payload.version!==version||payload.accountProjectVerified!==true||payload.executionAllowed!==false||typeof payload.conversationId!=='string'||typeof payload.receiptId!=='string'||payload.projectId!==m?.projectId||payload.projectId!==s?.projectState?.projectId||payload.revision!==s.revision||m.revision!==s.revision||s.executed!==false||m.executionAllowed!==false||m.authorizesSpending!==false||m.authorizesInstallation!==false)throw Error('Website selection is not an exact inert project manifest');
13
+ if(this.receipt&&(payload.projectId!==this.receipt.projectId||payload.conversationId!==this.receipt.conversationId||s.projectState.originalGoal!==this.receipt.result.structuredContent.projectState.originalGoal))throw Error('Account project or original conversation changed');
14
+ if(typeof payload.ackNonce!=='string'||!/^[A-Za-z0-9_-]{43}$/.test(payload.ackNonce))throw Error('Missing exact delivery acknowledgment');this.pendingAck={id:this.link.id,sequence:envelope.sequence,nonce:payload.ackNonce};delete payload.ackNonce;this.sequence=envelope.sequence;this.receipt=payload;return payload;
15
+ }
16
+ async receive({waitSeconds=45}={}){if(!Number.isInteger(waitSeconds)||waitSeconds<0||waitSeconds>45)throw Error('Wait up to 45 seconds');if(!this.link)return {state:'not-open',instructions:'Show the visual chooser with your goal first.'};if(this.pendingAck){await this.request('ack',this.pendingAck);this.pendingAck=null;return {state:'received',...this.receipt};}const end=this.clock()+waitSeconds*1000;do{const value=await this.request('poll',{id:this.link.id,afterSequence:this.sequence});if(value.version!==version||!['waiting','ready'].includes(value.state))throw Error('Unknown website return state');if(value.state==='ready'){const receipt=this.decrypt(value.envelope);try{await this.request('ack',this.pendingAck);this.pendingAck=null;return {state:'received',...receipt};}catch{return {state:'received-acknowledgment-unconfirmed',...receipt};}}if(this.clock()>=end)break;await this.sleep(Math.min(2000,end-this.clock()));}while(this.clock()<=end);return {state:'waiting',chooserUrl:this.link.chooserUrl,pairCode:this.link.pairCode,conversationId:this.receipt?.conversationId??null,lastReceipt:this.receipt??null};}
17
+ close(){this.keys=null;this.link=null;this.receipt=null;}
18
+ }
package/src/welcome.mjs CHANGED
@@ -1,8 +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)}];
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 CHANGED
@@ -1,58 +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
- }
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
+ }