kloudy 0.4.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +202 -202
  2. package/MCP-DEFAULT.md +12 -12
  3. package/README.md +317 -313
  4. package/bin/entry.mjs +23 -22
  5. package/bin/kloudy.mjs +74 -71
  6. package/package.json +3 -2
  7. package/schemas/SDF-LICENSE.txt +21 -21
  8. package/schemas/kloudy.deployable-bot.schema.json +80 -0
  9. package/schemas/sdf-document-0.2.schema.json +411 -411
  10. package/schemas/sdf-webpage-v1.schema.json +176 -176
  11. package/src/activity.mjs +23 -23
  12. package/src/approval-ui.mjs +2 -2
  13. package/src/approval.mjs +25 -25
  14. package/src/autoupdate.mjs +2 -2
  15. package/src/bots.mjs +144 -0
  16. package/src/chooser-link.mjs +17 -17
  17. package/src/client.mjs +96 -96
  18. package/src/connect-card.mjs +32 -32
  19. package/src/connect-ui.mjs +3 -3
  20. package/src/connection.mjs +25 -25
  21. package/src/conversation.mjs +53 -53
  22. package/src/deployable-bots.json.gz +0 -0
  23. package/src/editor-install.mjs +79 -79
  24. package/src/host-interface.mjs +10 -10
  25. package/src/hosted.mjs +42 -42
  26. package/src/httpk.mjs +66 -66
  27. package/src/install.mjs +35 -35
  28. package/src/introduce.mjs +33 -33
  29. package/src/mcp.mjs +74 -74
  30. package/src/modalities.mjs +27 -0
  31. package/src/models.mjs +55 -55
  32. package/src/preferences-card.mjs +21 -21
  33. package/src/preferences-ui.mjs +2 -2
  34. package/src/product-actions.mjs +22 -22
  35. package/src/public.mjs +41 -41
  36. package/src/query.mjs +28 -28
  37. package/src/remote.mjs +9 -2
  38. package/src/replies.mjs +7 -7
  39. package/src/reply-templates.json +12 -12
  40. package/src/sample-actions.mjs +31 -31
  41. package/src/sample-card.mjs +11 -11
  42. package/src/sample-cards.mjs +47 -47
  43. package/src/sample-ui.mjs +2 -2
  44. package/src/sdf-budget.mjs +23 -23
  45. package/src/sdf.mjs +12 -12
  46. package/src/starter.mjs +43 -43
  47. package/src/status-indicator.css +5 -5
  48. package/src/strings.mjs +51 -51
  49. package/src/terminal-status.mjs +6 -6
  50. package/src/website-return.mjs +18 -0
  51. package/src/welcome.mjs +8 -8
  52. package/src/wrap.mjs +58 -58
@@ -1,25 +1,25 @@
1
- import {HttpkClient} from './httpk.mjs';
2
- import {observeClient} from './activity.mjs';
3
- import {readFile,writeFile,rename,mkdir,rmdir} from 'node:fs/promises';
4
- import {dirname,resolve} from 'node:path';
5
- import {createHash,randomUUID} from 'node:crypto';
6
- import {homedir} from 'node:os';
7
- import {KloudyClient,KloudyError,httpTransport} from './client.mjs';
8
-
9
- export async function withConnection(file,work,{mode='autonomous',onActivity}={}){
10
- file||=resolve(homedir(),'.kloudy','connection.json');
11
- let c;try{c=JSON.parse(await readFile(file,'utf8'));}catch{throw new KloudyError('connection_unavailable','The scoped connection file is unavailable.');}
12
- const transport=c.endpoint?null:httpTransport({origin:c.origin,token:c.access_token});
13
- const owner=createHash('sha256').update((c.endpoint||c.origin)+'\n'+(c.client_id||c.access_token)).digest('hex');
14
- const stateFile=resolve(dirname(file),'kloudy-open-session-'+owner.slice(0,24)+'.json'),lock=stateFile+'.lock';
15
- try{await mkdir(lock,{mode:0o700});}catch{throw new KloudyError('connection_busy','Another Kloudy client owns this session. Retry after it exits; stale lock recovery is documented.');}
16
- try{
17
- let state={owner};try{const saved=JSON.parse(await readFile(stateFile,'utf8'));if(saved.owner===owner)state=saved;}catch(error){if(error.code!=='ENOENT')throw new KloudyError('invalid_state','Saved session could not be read.');
18
- // Reuse only the matching legacy checkpoint; never overwrite another source.
19
- try{const legacy=JSON.parse(await readFile(resolve(dirname(file),'kloudy-open-session.json'),'utf8'));if(legacy.owner===owner)state=legacy;}catch(legacyError){if(legacyError.code!=='ENOENT')throw new KloudyError('invalid_state','Legacy session could not be read.');}
20
- }
21
- const save=async value=>{const temp=stateFile+'.'+randomUUID()+'.tmp';await writeFile(temp,JSON.stringify(value),{mode:0o600,flag:'wx'});await rename(temp,stateFile);};
22
- const client=c.endpoint?new HttpkClient({endpoint:c.endpoint,token:c.access_token,state,save,mode}):new KloudyClient({transport,state,save,mode:mode==='auto'?'autonomous':mode});
23
- return await work(onActivity?observeClient(client,onActivity):client);
24
- }finally{await rmdir(lock);}
25
- }
1
+ import {HttpkClient} from './httpk.mjs';
2
+ import {observeClient} from './activity.mjs';
3
+ import {readFile,writeFile,rename,mkdir,rmdir} from 'node:fs/promises';
4
+ import {dirname,resolve} from 'node:path';
5
+ import {createHash,randomUUID} from 'node:crypto';
6
+ import {homedir} from 'node:os';
7
+ import {KloudyClient,KloudyError,httpTransport} from './client.mjs';
8
+
9
+ export async function withConnection(file,work,{mode='autonomous',onActivity}={}){
10
+ file||=resolve(homedir(),'.kloudy','connection.json');
11
+ let c;try{c=JSON.parse(await readFile(file,'utf8'));}catch{throw new KloudyError('connection_unavailable','The scoped connection file is unavailable.');}
12
+ const transport=c.endpoint?null:httpTransport({origin:c.origin,token:c.access_token});
13
+ const owner=createHash('sha256').update((c.endpoint||c.origin)+'\n'+(c.client_id||c.access_token)).digest('hex');
14
+ const stateFile=resolve(dirname(file),'kloudy-open-session-'+owner.slice(0,24)+'.json'),lock=stateFile+'.lock';
15
+ try{await mkdir(lock,{mode:0o700});}catch{throw new KloudyError('connection_busy','Another Kloudy client owns this session. Retry after it exits; stale lock recovery is documented.');}
16
+ try{
17
+ let state={owner};try{const saved=JSON.parse(await readFile(stateFile,'utf8'));if(saved.owner===owner)state=saved;}catch(error){if(error.code!=='ENOENT')throw new KloudyError('invalid_state','Saved session could not be read.');
18
+ // Reuse only the matching legacy checkpoint; never overwrite another source.
19
+ try{const legacy=JSON.parse(await readFile(resolve(dirname(file),'kloudy-open-session.json'),'utf8'));if(legacy.owner===owner)state=legacy;}catch(legacyError){if(legacyError.code!=='ENOENT')throw new KloudyError('invalid_state','Legacy session could not be read.');}
20
+ }
21
+ const save=async value=>{const temp=stateFile+'.'+randomUUID()+'.tmp';await writeFile(temp,JSON.stringify(value),{mode:0o600,flag:'wx'});await rename(temp,stateFile);};
22
+ const client=c.endpoint?new HttpkClient({endpoint:c.endpoint,token:c.access_token,state,save,mode}):new KloudyClient({transport,state,save,mode:mode==='auto'?'autonomous':mode});
23
+ return await work(onActivity?observeClient(client,onActivity):client);
24
+ }finally{await rmdir(lock);}
25
+ }
@@ -1,53 +1,53 @@
1
- // Client contract for the existing encrypted engine store. No local transcript store.
2
- const bad=()=>{throw Error('Invalid conversation contract. No sync or permission was inferred.');};
3
- const plain=v=>v&&typeof v==='object'&&!Array.isArray(v);
4
- const exact=(v,keys)=>{if(!plain(v)||Object.keys(v).length!==keys.length||keys.some(k=>!Object.hasOwn(v,k)))bad();};
5
- const bounded=(v,max)=>typeof v==='string'&&v.trim().length>0&&[...v].length<=max;
6
- const identifier=(v,max=80)=>typeof v==='string'&&new RegExp(`^[a-zA-Z0-9_.:-]{1,${max}}$`).test(v);
7
- const integer=v=>Number.isSafeInteger(v)&&v>=0;
8
- export const conversationOperations=['read','append','sync'];
9
- export function conversationParams(operation,input){
10
- if(!conversationOperations.includes(operation))bad();
11
- const common=['project','conversation_id'];
12
- const keys=operation==='read'?common:[...common,'idempotency_key','expected_revision',...(operation==='sync'?['reason']:['title','user','assistant','decisions','built_artifacts'])];
13
- exact(input,keys);
14
- if(!identifier(input.project)||!identifier(input.conversation_id))bad();
15
- if(operation!=='read'&&(!identifier(input.idempotency_key,128)||!integer(input.expected_revision)))bad();
16
- if(operation==='sync'&&!['checkpoint','session_end','device_switch','before_sync','idle'].includes(input.reason))bad();
17
- if(operation==='append'){
18
- if(!bounded(input.title,120)||!bounded(input.user,1500)||!bounded(input.assistant,1500))bad();
19
- for(const key of ['decisions','built_artifacts'])if(!Array.isArray(input[key])||input[key].length>4||input[key].some(x=>!bounded(x,240)))bad();
20
- }
21
- // Engine canonical JSON escapes Unicode. Match its 7000-byte bound before sending.
22
- const encoded=JSON.stringify(input).replace(/[\u007f-\uffff]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0'));
23
- if(new TextEncoder().encode(encoded).length>7000)bad();
24
- return structuredClone(input);
25
- }
26
- export function conversationResult(value,input,operation='read'){
27
- if(!plain(value)||value.project!==input.project||value.conversation_id!==input.conversation_id)bad();
28
- for(const key of ['revision','condensed_revision','pending_exchanges'])if(!integer(value[key]))bad();
29
- if(value.condensed_revision>value.revision||value.pending_exchanges>value.revision)bad();
30
- const cursor={conversation_id:value.conversation_id,project:value.project,revision:value.revision,condensed_revision:value.condensed_revision,pending_exchanges:value.pending_exchanges};
31
- // Duplicate command receipts intentionally omit cards. Read explicitly for fresh context.
32
- if(value.replayed===true){if(operation==='read')bad();return {...cursor,replayed:true,context_available:false};}
33
- if(value.raw_transcript_returned!==false||value.storage!=='engine_encrypted'||value.compute!=='engine_host'||typeof value.minimum_gate_met!=='boolean'||!plain(value.display)||value.display.render_as!=='plain_text'||!bounded(value.display.title,120)||!bounded(value.display.text,1000))bad();
34
- let deck=null;
35
- if(value.deck!==null){
36
- const d=value.deck;
37
- if(!plain(d)||d.version!=='sdf.conversation/1'||d.lossy!==true||d.method!=='bounded_extractive'||d.trusted_for_authorization!==false||d.through_revision!==value.condensed_revision||!Array.isArray(d.cards)||d.cards.length!==2)bad();
38
- const [subject,context]=d.cards;
39
- if(!plain(subject)||subject.type!=='subject'||subject.project!==input.project||!bounded(subject.title,120)||!plain(context)||context.type!=='context')bad();
40
- const points={};for(const key of ['user_main_points','assistant_main_points','decisions','built_artifacts']){
41
- if(!Array.isArray(context[key])||context[key].length>12||context[key].some(x=>!bounded(x,key==='built_artifacts'?240:360)))bad();
42
- points[key]=[...context[key]];
43
- }
44
- deck={version:d.version,lossy:true,method:d.method,trusted_for_authorization:false,through_revision:d.through_revision,cards:[{type:'subject',title:subject.title,project:subject.project},{type:'context',...points}]};
45
- }
46
- return {...cursor,deck,raw_transcript_returned:false,minimum_gate_met:value.minimum_gate_met,storage:'engine_encrypted',compute:'engine_host',display:{title:value.display.title,text:value.display.text,render_as:'plain_text'},...(operation==='read'?{}:{replayed:false})};
47
- }
48
- // Text only. Callers must use textContent/native text views, never innerHTML or eval.
49
- export function conversationText(view){
50
- if(!view.deck)return view.context_available===false?'Command already recorded. Read the current conversation context.':view.display.text;
51
- const [subject,context]=view.deck.cards;
52
- return [subject.title,'Saved context (lossy summary; never authorization).',...context.user_main_points,...context.assistant_main_points,...context.decisions,...context.built_artifacts].join('\n');
53
- }
1
+ // Client contract for the existing encrypted engine store. No local transcript store.
2
+ const bad=()=>{throw Error('Invalid conversation contract. No sync or permission was inferred.');};
3
+ const plain=v=>v&&typeof v==='object'&&!Array.isArray(v);
4
+ const exact=(v,keys)=>{if(!plain(v)||Object.keys(v).length!==keys.length||keys.some(k=>!Object.hasOwn(v,k)))bad();};
5
+ const bounded=(v,max)=>typeof v==='string'&&v.trim().length>0&&[...v].length<=max;
6
+ const identifier=(v,max=80)=>typeof v==='string'&&new RegExp(`^[a-zA-Z0-9_.:-]{1,${max}}$`).test(v);
7
+ const integer=v=>Number.isSafeInteger(v)&&v>=0;
8
+ export const conversationOperations=['read','append','sync'];
9
+ export function conversationParams(operation,input){
10
+ if(!conversationOperations.includes(operation))bad();
11
+ const common=['project','conversation_id'];
12
+ const keys=operation==='read'?common:[...common,'idempotency_key','expected_revision',...(operation==='sync'?['reason']:['title','user','assistant','decisions','built_artifacts'])];
13
+ exact(input,keys);
14
+ if(!identifier(input.project)||!identifier(input.conversation_id))bad();
15
+ if(operation!=='read'&&(!identifier(input.idempotency_key,128)||!integer(input.expected_revision)))bad();
16
+ if(operation==='sync'&&!['checkpoint','session_end','device_switch','before_sync','idle'].includes(input.reason))bad();
17
+ if(operation==='append'){
18
+ if(!bounded(input.title,120)||!bounded(input.user,1500)||!bounded(input.assistant,1500))bad();
19
+ for(const key of ['decisions','built_artifacts'])if(!Array.isArray(input[key])||input[key].length>4||input[key].some(x=>!bounded(x,240)))bad();
20
+ }
21
+ // Engine canonical JSON escapes Unicode. Match its 7000-byte bound before sending.
22
+ const encoded=JSON.stringify(input).replace(/[\u007f-\uffff]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0'));
23
+ if(new TextEncoder().encode(encoded).length>7000)bad();
24
+ return structuredClone(input);
25
+ }
26
+ export function conversationResult(value,input,operation='read'){
27
+ if(!plain(value)||value.project!==input.project||value.conversation_id!==input.conversation_id)bad();
28
+ for(const key of ['revision','condensed_revision','pending_exchanges'])if(!integer(value[key]))bad();
29
+ if(value.condensed_revision>value.revision||value.pending_exchanges>value.revision)bad();
30
+ const cursor={conversation_id:value.conversation_id,project:value.project,revision:value.revision,condensed_revision:value.condensed_revision,pending_exchanges:value.pending_exchanges};
31
+ // Duplicate command receipts intentionally omit cards. Read explicitly for fresh context.
32
+ if(value.replayed===true){if(operation==='read')bad();return {...cursor,replayed:true,context_available:false};}
33
+ if(value.raw_transcript_returned!==false||value.storage!=='engine_encrypted'||value.compute!=='engine_host'||typeof value.minimum_gate_met!=='boolean'||!plain(value.display)||value.display.render_as!=='plain_text'||!bounded(value.display.title,120)||!bounded(value.display.text,1000))bad();
34
+ let deck=null;
35
+ if(value.deck!==null){
36
+ const d=value.deck;
37
+ if(!plain(d)||d.version!=='sdf.conversation/1'||d.lossy!==true||d.method!=='bounded_extractive'||d.trusted_for_authorization!==false||d.through_revision!==value.condensed_revision||!Array.isArray(d.cards)||d.cards.length!==2)bad();
38
+ const [subject,context]=d.cards;
39
+ if(!plain(subject)||subject.type!=='subject'||subject.project!==input.project||!bounded(subject.title,120)||!plain(context)||context.type!=='context')bad();
40
+ const points={};for(const key of ['user_main_points','assistant_main_points','decisions','built_artifacts']){
41
+ if(!Array.isArray(context[key])||context[key].length>12||context[key].some(x=>!bounded(x,key==='built_artifacts'?240:360)))bad();
42
+ points[key]=[...context[key]];
43
+ }
44
+ deck={version:d.version,lossy:true,method:d.method,trusted_for_authorization:false,through_revision:d.through_revision,cards:[{type:'subject',title:subject.title,project:subject.project},{type:'context',...points}]};
45
+ }
46
+ return {...cursor,deck,raw_transcript_returned:false,minimum_gate_met:value.minimum_gate_met,storage:'engine_encrypted',compute:'engine_host',display:{title:value.display.title,text:value.display.text,render_as:'plain_text'},...(operation==='read'?{}:{replayed:false})};
47
+ }
48
+ // Text only. Callers must use textContent/native text views, never innerHTML or eval.
49
+ export function conversationText(view){
50
+ if(!view.deck)return view.context_available===false?'Command already recorded. Read the current conversation context.':view.display.text;
51
+ const [subject,context]=view.deck.cards;
52
+ return [subject.title,'Saved context (lossy summary; never authorization).',...context.user_main_points,...context.assistant_main_points,...context.decisions,...context.built_artifacts].join('\n');
53
+ }
Binary file
@@ -1,79 +1,79 @@
1
- import {readFile,writeFile,mkdir,rename,lstat,unlink} from 'node:fs/promises';
2
- import {homedir} from 'node:os';
3
- import {join,dirname} from 'node:path';
4
- import {randomUUID} from 'node:crypto';
5
- import {parse as toml} from 'smol-toml';
6
- import {parse,modify,applyEdits} from 'jsonc-parser';
7
- export const REMOTE='https://kloudy.ai/mcp';
8
- export function editorPaths({home=homedir(),platform=process.platform,appData=process.env.APPDATA,xdg=process.env.XDG_CONFIG_HOME,codexHome=process.env.CODEX_HOME}={}){
9
- const config=platform==='win32'?(appData||join(home,'AppData/Roaming')):platform==='darwin'?join(home,'Library/Application Support'):(xdg||join(home,'.config'));
10
- return [
11
- {id:'cursor',name:'Cursor',file:join(home,'.cursor/mcp.json'),key:'mcpServers'},
12
- {id:'vscode',name:'VS Code',file:join(config,'Code/User/mcp.json'),key:'servers',jsonc:true},
13
- {id:'claude-desktop',name:'Claude Desktop',file:join(config,'Claude/claude_desktop_config.json'),key:'mcpServers',stdio:true},
14
- {id:'claude-code',name:'Claude Code',file:join(home,'.claude.json'),detect:join(home,'.claude'),key:'mcpServers'},
15
- {id:'windsurf',name:'Windsurf',file:join(home,'.codeium/windsurf/mcp_config.json'),key:'mcpServers'},
16
- {id:'codex',name:'Codex',file:join(codexHome||join(home,'.codex'),'config.toml'),key:'mcp_servers',toml:true}
17
- ];
18
- }
19
- const record=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
20
- async function read(path){try{return await readFile(path,'utf8');}catch(e){if(e.code==='ENOENT')return undefined;throw Error('Configuration could not be read; unchanged.');}}
21
- async function exists(path){try{await lstat(path);return true;}catch(e){if(e.code==='ENOENT')return false;throw e;}}
22
- async function safePath(path){for(let p=path;;p=dirname(p)){try{const s=await lstat(p);if(s.isSymbolicLink())throw Error('Symbolic link configuration needs manual installation.');if(p===path&&(!s.isFile()||s.nlink>1))throw Error('Configuration is not a regular private file.');}catch(e){if(e.code!=='ENOENT')throw e;}if(dirname(p)===p)break;}}
23
- export function editorEntry(host,key){
24
- if(key!==undefined&&(typeof key!=='string'||!/^\S{20,512}$/.test(key)))throw Error('Invalid Kloudy key; not written.');
25
- if(host.stdio)return {command:'npx',args:['-y','kloudy@latest','mcp'],...(key?{env:{KLOUDY_MCP_TOKEN:key}}:{})};
26
- const headers=key?{Authorization:'Bearer '+key}:undefined;
27
- if(host.toml)return {url:REMOTE,...(headers?{http_headers:headers}:{})};
28
- return {...(['vscode','claude-code'].includes(host.id)?{type:'http'}:{}),[host.id==='windsurf'?'serverUrl':'url']:REMOTE,...(headers?{headers}:{})};
29
- }
30
- // Recognize only installer-owned command shapes; additional user options stay untouched.
31
- export function healable(entry){
32
- if(!record(entry)||Object.keys(entry).some(k=>!['command','args','type'].includes(k))||!Array.isArray(entry.args)||entry.type!==undefined&&entry.type!=='stdio')return false;
33
- const args=entry.args;
34
- const node=typeof entry.command==='string'&&/(?:^|[\\/])node(?:\.exe)?$/i.test(entry.command);
35
- const cached=node&&args.length===2&&args[1]==='mcp'&&typeof args[0]==='string'&&/[\\/]_npx[\\/].*[\\/]kloudy[\\/]bin[\\/]kloudy\.mjs$/.test(args[0]);
36
- const npx=typeof entry.command==='string'&&/(?:^|[\\/])npx(?:\.cmd|\.exe)?$/i.test(entry.command);
37
- const pinned=npx&&(args.length===2||args.length===3&&args[0]==='-y')&&args.at(-1)==='mcp'&&/^kloudy@\d+\.\d+\.\d+$/.test(args.at(-2));
38
- return cached||pinned;
39
- }
40
- function replaceTomlEntry(old,entry){
41
- // Replace only a simple table whose parsed value was recognized above. Quoted or
42
- // dotted table styles are preserved for manual handling instead of reformatting.
43
- const pattern=/^\[mcp_servers\.kloudy\][^\r\n]*\r?\n[^]*?(?=^\[|(?![^]))/m;
44
- if(!pattern.test(old))throw Error('Nonstandard Kloudy table; unchanged.');
45
- return old.replace(pattern,'[mcp_servers.kloudy]\n'+Object.entries(entry).map(([k,v])=>k+' = '+(k==='http_headers'?'{ Authorization = '+JSON.stringify(v.Authorization)+' }':JSON.stringify(v))).join('\n')+'\n');
46
- }
47
- export async function installEditors({confirm=async()=>false,hosts=editorPaths(),key,onResult=()=>{}}={}){
48
- const results=[];
49
- for(const host of hosts){
50
- let state;
51
- try{
52
- if(!await exists(host.file)&&!await exists(host.detect||dirname(host.file)))continue;
53
- await safePath(host.file);const old=await read(host.file);let value,errors=[];
54
- try{value=old===undefined?{}:host.toml?toml(old):parse(old,errors,{allowTrailingComma:!!host.jsonc,disallowComments:!host.jsonc});}catch{throw Error('Configuration cannot be parsed; unchanged.');}
55
- if(errors.length||!record(value)||value[host.key]!==undefined&&!record(value[host.key]))throw Error('Configuration cannot be parsed; unchanged.');
56
- const existing=value[host.key]?.kloudy,healing=healable(existing);
57
- if(Object.hasOwn(value[host.key]||{},'kloudy')&&!healing)state='existing_kloudy_preserved';
58
- else if(!await confirm({client:host.name,path:host.file,authenticated:!!key}))state='declined';
59
- else{
60
- const entry=editorEntry(host,key);let next;
61
- if(host.toml){next=healing?replaceTomlEntry(old,entry):(old||'')+'\n[mcp_servers.kloudy]\nurl = '+JSON.stringify(REMOTE)+'\n'+(key?'http_headers = { Authorization = '+JSON.stringify('Bearer '+key)+' }\n':'');toml(next);}
62
- else next=applyEdits(old||'{}',modify(old||'{}',[host.key,'kloudy'],entry,{formattingOptions:{insertSpaces:true,tabSize:2,eol:old?.includes('\r\n')?'\r\n':'\n'}}))+'\n';
63
- await safePath(host.file);if(await read(host.file)!==old)throw Error('Configuration changed during approval; unchanged. Run again.');
64
- await mkdir(dirname(host.file),{recursive:true});
65
- const lock=host.file+'.kloudy-lock';await writeFile(lock,'',{flag:'wx',mode:0o600});
66
- let temp;
67
- try{
68
- if(await read(host.file)!==old)throw Error('Configuration changed; unchanged.');
69
- if(old!==undefined)await writeFile(host.file+'.kloudy-backup-'+randomUUID(),old,{flag:'wx',mode:0o600});
70
- temp=host.file+'.kloudy-'+randomUUID()+'.tmp';await writeFile(temp,next,{flag:'wx',mode:0o600});
71
- if(await read(host.file)!==old)throw Error('Configuration changed; unchanged.');
72
- await rename(temp,host.file);temp=null;state=healing?'healed':'configured';
73
- }finally{if(temp)await unlink(temp).catch(()=>{});await unlink(lock).catch(()=>{});}
74
- }
75
- }catch{state='unchanged_error';}
76
- const result={client:host.name,path:host.file,state};results.push(result);onResult(result);
77
- }
78
- return results;
79
- }
1
+ import {readFile,writeFile,mkdir,rename,lstat,unlink} from 'node:fs/promises';
2
+ import {homedir} from 'node:os';
3
+ import {join,dirname} from 'node:path';
4
+ import {randomUUID} from 'node:crypto';
5
+ import {parse as toml} from 'smol-toml';
6
+ import {parse,modify,applyEdits} from 'jsonc-parser';
7
+ export const REMOTE='https://kloudy.ai/mcp';
8
+ export function editorPaths({home=homedir(),platform=process.platform,appData=process.env.APPDATA,xdg=process.env.XDG_CONFIG_HOME,codexHome=process.env.CODEX_HOME}={}){
9
+ const config=platform==='win32'?(appData||join(home,'AppData/Roaming')):platform==='darwin'?join(home,'Library/Application Support'):(xdg||join(home,'.config'));
10
+ return [
11
+ {id:'cursor',name:'Cursor',file:join(home,'.cursor/mcp.json'),key:'mcpServers'},
12
+ {id:'vscode',name:'VS Code',file:join(config,'Code/User/mcp.json'),key:'servers',jsonc:true},
13
+ {id:'claude-desktop',name:'Claude Desktop',file:join(config,'Claude/claude_desktop_config.json'),key:'mcpServers',stdio:true},
14
+ {id:'claude-code',name:'Claude Code',file:join(home,'.claude.json'),detect:join(home,'.claude'),key:'mcpServers'},
15
+ {id:'windsurf',name:'Windsurf',file:join(home,'.codeium/windsurf/mcp_config.json'),key:'mcpServers'},
16
+ {id:'codex',name:'Codex',file:join(codexHome||join(home,'.codex'),'config.toml'),key:'mcp_servers',toml:true}
17
+ ];
18
+ }
19
+ const record=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
20
+ async function read(path){try{return await readFile(path,'utf8');}catch(e){if(e.code==='ENOENT')return undefined;throw Error('Configuration could not be read; unchanged.');}}
21
+ async function exists(path){try{await lstat(path);return true;}catch(e){if(e.code==='ENOENT')return false;throw e;}}
22
+ async function safePath(path){for(let p=path;;p=dirname(p)){try{const s=await lstat(p);if(s.isSymbolicLink())throw Error('Symbolic link configuration needs manual installation.');if(p===path&&(!s.isFile()||s.nlink>1))throw Error('Configuration is not a regular private file.');}catch(e){if(e.code!=='ENOENT')throw e;}if(dirname(p)===p)break;}}
23
+ export function editorEntry(host,key){
24
+ if(key!==undefined&&(typeof key!=='string'||!/^\S{20,512}$/.test(key)))throw Error('Invalid Kloudy key; not written.');
25
+ if(host.stdio)return {command:'npx',args:['-y','kloudy@latest','mcp'],...(key?{env:{KLOUDY_MCP_TOKEN:key}}:{})};
26
+ const headers=key?{Authorization:'Bearer '+key}:undefined;
27
+ if(host.toml)return {url:REMOTE,...(headers?{http_headers:headers}:{})};
28
+ return {...(['vscode','claude-code'].includes(host.id)?{type:'http'}:{}),[host.id==='windsurf'?'serverUrl':'url']:REMOTE,...(headers?{headers}:{})};
29
+ }
30
+ // Recognize only installer-owned command shapes; additional user options stay untouched.
31
+ export function healable(entry){
32
+ if(!record(entry)||Object.keys(entry).some(k=>!['command','args','type'].includes(k))||!Array.isArray(entry.args)||entry.type!==undefined&&entry.type!=='stdio')return false;
33
+ const args=entry.args;
34
+ const node=typeof entry.command==='string'&&/(?:^|[\\/])node(?:\.exe)?$/i.test(entry.command);
35
+ const cached=node&&args.length===2&&args[1]==='mcp'&&typeof args[0]==='string'&&/[\\/]_npx[\\/].*[\\/]kloudy[\\/]bin[\\/]kloudy\.mjs$/.test(args[0]);
36
+ const npx=typeof entry.command==='string'&&/(?:^|[\\/])npx(?:\.cmd|\.exe)?$/i.test(entry.command);
37
+ const pinned=npx&&(args.length===2||args.length===3&&args[0]==='-y')&&args.at(-1)==='mcp'&&/^kloudy@\d+\.\d+\.\d+$/.test(args.at(-2));
38
+ return cached||pinned;
39
+ }
40
+ function replaceTomlEntry(old,entry){
41
+ // Replace only a simple table whose parsed value was recognized above. Quoted or
42
+ // dotted table styles are preserved for manual handling instead of reformatting.
43
+ const pattern=/^\[mcp_servers\.kloudy\][^\r\n]*\r?\n[^]*?(?=^\[|(?![^]))/m;
44
+ if(!pattern.test(old))throw Error('Nonstandard Kloudy table; unchanged.');
45
+ return old.replace(pattern,'[mcp_servers.kloudy]\n'+Object.entries(entry).map(([k,v])=>k+' = '+(k==='http_headers'?'{ Authorization = '+JSON.stringify(v.Authorization)+' }':JSON.stringify(v))).join('\n')+'\n');
46
+ }
47
+ export async function installEditors({confirm=async()=>false,hosts=editorPaths(),key,onResult=()=>{}}={}){
48
+ const results=[];
49
+ for(const host of hosts){
50
+ let state;
51
+ try{
52
+ if(!await exists(host.file)&&!await exists(host.detect||dirname(host.file)))continue;
53
+ await safePath(host.file);const old=await read(host.file);let value,errors=[];
54
+ try{value=old===undefined?{}:host.toml?toml(old):parse(old,errors,{allowTrailingComma:!!host.jsonc,disallowComments:!host.jsonc});}catch{throw Error('Configuration cannot be parsed; unchanged.');}
55
+ if(errors.length||!record(value)||value[host.key]!==undefined&&!record(value[host.key]))throw Error('Configuration cannot be parsed; unchanged.');
56
+ const existing=value[host.key]?.kloudy,healing=healable(existing);
57
+ if(Object.hasOwn(value[host.key]||{},'kloudy')&&!healing)state='existing_kloudy_preserved';
58
+ else if(!await confirm({client:host.name,path:host.file,authenticated:!!key}))state='declined';
59
+ else{
60
+ const entry=editorEntry(host,key);let next;
61
+ if(host.toml){next=healing?replaceTomlEntry(old,entry):(old||'')+'\n[mcp_servers.kloudy]\nurl = '+JSON.stringify(REMOTE)+'\n'+(key?'http_headers = { Authorization = '+JSON.stringify('Bearer '+key)+' }\n':'');toml(next);}
62
+ else next=applyEdits(old||'{}',modify(old||'{}',[host.key,'kloudy'],entry,{formattingOptions:{insertSpaces:true,tabSize:2,eol:old?.includes('\r\n')?'\r\n':'\n'}}))+'\n';
63
+ await safePath(host.file);if(await read(host.file)!==old)throw Error('Configuration changed during approval; unchanged. Run again.');
64
+ await mkdir(dirname(host.file),{recursive:true});
65
+ const lock=host.file+'.kloudy-lock';await writeFile(lock,'',{flag:'wx',mode:0o600});
66
+ let temp;
67
+ try{
68
+ if(await read(host.file)!==old)throw Error('Configuration changed; unchanged.');
69
+ if(old!==undefined)await writeFile(host.file+'.kloudy-backup-'+randomUUID(),old,{flag:'wx',mode:0o600});
70
+ temp=host.file+'.kloudy-'+randomUUID()+'.tmp';await writeFile(temp,next,{flag:'wx',mode:0o600});
71
+ if(await read(host.file)!==old)throw Error('Configuration changed; unchanged.');
72
+ await rename(temp,host.file);temp=null;state=healing?'healed':'configured';
73
+ }finally{if(temp)await unlink(temp).catch(()=>{});await unlink(lock).catch(()=>{});}
74
+ }
75
+ }catch{state='unchanged_error';}
76
+ const result={client:host.name,path:host.file,state};results.push(result);onResult(result);
77
+ }
78
+ return results;
79
+ }
@@ -1,10 +1,10 @@
1
- // Presentation hints cannot grant scopes or change approval mode.
2
- export function hostCapabilities(hints={chat:true,text_input:true,speech_input:false}){
3
- if(!hints||Array.isArray(hints)||typeof hints!=='object'||Object.keys(hints).some(k=>!['chat','text_input','speech_input'].includes(k))||Object.values(hints).some(v=>typeof v!=='boolean'))throw Error('invalid_host_capabilities');
4
- return {experimental:{kloudy:{...hints}}};
5
- }
6
- export function readHostInterface(value){
7
- if(value===undefined)return null; // Older engines remain usable, without invented negotiation.
8
- if(value?.version!=='kloudy.host-interface/1'||!['host','kloudy'].includes(value.conversation_renderer)||!['host','kloudy'].includes(value.text_input)||value.authorization_changed!==false||value.duplicate_chat_required!==false||value.duplicate_text_input_required!==false||value.voice?.engine_audio_capture!==false||value.voice?.engine_speech_synthesis!==false||value.subscription_text_planning?.requires_entitlement!==true||value.subscription_text_planning?.provider!=='nvidia'||value.bot_boozle?.required!==false)throw Error('invalid_host_interface');
9
- return structuredClone(value);
10
- }
1
+ // Presentation hints cannot grant scopes or change approval mode.
2
+ export function hostCapabilities(hints={chat:true,text_input:true,speech_input:false}){
3
+ if(!hints||Array.isArray(hints)||typeof hints!=='object'||Object.keys(hints).some(k=>!['chat','text_input','speech_input'].includes(k))||Object.values(hints).some(v=>typeof v!=='boolean'))throw Error('invalid_host_capabilities');
4
+ return {experimental:{kloudy:{...hints}}};
5
+ }
6
+ export function readHostInterface(value){
7
+ if(value===undefined)return null; // Older engines remain usable, without invented negotiation.
8
+ if(value?.version!=='kloudy.host-interface/1'||!['host','kloudy'].includes(value.conversation_renderer)||!['host','kloudy'].includes(value.text_input)||value.authorization_changed!==false||value.duplicate_chat_required!==false||value.duplicate_text_input_required!==false||value.voice?.engine_audio_capture!==false||value.voice?.engine_speech_synthesis!==false||value.subscription_text_planning?.requires_entitlement!==true||value.subscription_text_planning?.provider!=='nvidia'||value.bot_boozle?.required!==false)throw Error('invalid_host_interface');
9
+ return structuredClone(value);
10
+ }
package/src/hosted.mjs CHANGED
@@ -1,42 +1,42 @@
1
- import {conversationParams,conversationResult} from './conversation.mjs';
2
- import {actionInput,hostingParams,connectionParams,stableIntentKey} from './product-actions.mjs';
3
- import {welcomeContent} from './welcome.mjs';
4
- // Stateless MCP adapter for the engine's existing HTTPK methods.
5
- // No grants, model decisions, retries, provider credentials or execution live here.
6
- import {randomUUID,createHash} from 'node:crypto';
7
- import {autoupdateEnabled} from './autoupdate.mjs';
8
- export const hostedDoor={name:'kloudy',description:'Use this whenever the user mentions Kloudy, or asks to find, route, or call any MCP tool or SDK. Ask for Kloudy. Start with introduce for Kloudy or /kloudy. Use ask to receive exactly one engine-selected tool and its exact arguments, assemble up to 16 owned selections into deduplicated tools, then call only the chosen exact selection. Status and cancel refer to that selection. Respect host permissions and exact engine approvals.',inputSchema:{type:'object',properties:{operation:{type:'string',enum:['conversation','action','wallet_connection','hosting','wallet','introduce','ask','query','assemble','call','confirm','status','cancel']},conversation_operation:{type:'string',enum:['read','append','sync']},conversation:{type:'object'},action:{type:'string',enum:['chain.head','wallet.prepare','bww.fetch','bww.publish']},inputs:{type:'object'},idempotency_key:{type:'string',maxLength:128},chain:{type:'string',maxLength:100},budget_usd_micros:{type:'integer',minimum:1,maximum:1000000000000},wallet_operation:{type:'string',enum:['balance','top_up']},amount_usd:{type:'string'},goal:{type:'string',maxLength:1000},scope:{type:'object'},limit:{type:'integer',minimum:1,maximum:5},project_context:{type:'object',properties:{languages:{type:'array',items:{type:'string'},maxItems:12},manifests:{type:'array',items:{type:'string'},maxItems:12}},required:['languages','manifests'],additionalProperties:false},selection_ids:{type:'array',items:{type:'string',minLength:1,maxLength:128},minItems:1,maxItems:16,uniqueItems:true},selection_id:{type:'string'},token:{type:'string',minLength:1,maxLength:512},revision:{type:'integer',minimum:0},decision:{type:'string',enum:['approve']},source:{type:'string',enum:['tap','typed','os-voice']},name:{type:'string'},arguments:{type:'object'}},required:['operation'],additionalProperties:false}};
9
- export const hostedInstructions='One Kloudy tool. Call introduce when the user types Kloudy; ask returns one selected tool; call executes only its exact selection. The engine enforces grants and approval. Welcome the user once. Kloudy or /kloudy addresses this system explicitly; use Kloudy for relevant tasks without requiring that prefix. Bring in only tools relevant to the current project. An account connection or retained tool does not activate it in unrelated projects. Natural keep/cleanup requests need the authoritative lifecycle service; do not invent retention, deletion or grants. Use host authorization when needed; never collect provider secrets in chat.';
10
- const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(k=>[k,canonical(value[k])])):value;
11
- export const hostedCatalog={version:createHash('sha256').update(JSON.stringify(canonical({tools:[hostedDoor],instructions:hostedInstructions}))).digest('hex').slice(0,12),issued:new Date().toISOString(),minClient:null,deprecations:[]};
12
- const toolResult=result=>({content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result});
13
- export function adaptMcp(message,{autoupdate=process.env.KLOUDY_MCP_AUTOUPDATE}={}){
14
- const enabled=autoupdateEnabled(autoupdate);
15
- let upstream=message,project=x=>x;
16
- if(message.method==='initialize')project=x=>({...x,instructions:hostedInstructions,capabilities:{tools:{listChanged:enabled?(x.capabilities?.tools?.listChanged??true):false}},...(enabled?{_meta:{...x._meta,'ai.kloudy/catalog':hostedCatalog}}:{})});
17
- if(message.method==='tools/list'&&!message.params?._meta?.selection_ids)project=()=>({tools:[hostedDoor],...(enabled?{_meta:{'ai.kloudy/catalog':hostedCatalog}}:{})});
18
- if(message.method==='tools/call'&&message.params?.name==='kloudy'){
19
- const a=message.params.arguments||{},operation=a.operation||'introduce';
20
- let method,params;
21
- if(operation==='conversation'){method='kloudy/conversation/'+a.conversation_operation;params=conversationParams(a.conversation_operation,a.conversation);}
22
- else if(operation==='action'){method='kloudy/intent';params={idempotency_key:stableIntentKey(a.idempotency_key||randomUUID()),input:actionInput(a.action,a.inputs)};}
23
- else if(operation==='wallet_connection'){method='kloudy/wallet/connection';params=connectionParams(a.chain);}
24
- else if(operation==='hosting'){method='kloudy/hosting';params=hostingParams(a.budget_usd_micros);}
25
- else if(operation==='wallet'){const operation=a.wallet_operation||'balance';if(!['balance','top_up'].includes(operation)||operation==='balance'&&a.amount_usd!==undefined||operation==='top_up'&&(typeof a.amount_usd!=='string'||!/^\d{1,6}(?:\.\d{1,2})?$/.test(a.amount_usd)||Number(a.amount_usd)<=0))throw Error('invalid_wallet_request');method='kloudy/wallet';params={operation,...(operation==='top_up'?{amount_usd:a.amount_usd}:{})};}
26
- else if(operation==='introduce'||operation==='ask'){
27
- method='kloudy/intent';params={idempotency_key:randomUUID(),input:{type:'text',text:operation==='introduce'?'Kloudy':a.goal,...(operation==='introduce'?{project_context:a.project_context||{languages:[],manifests:[]}}:{})}};
28
- }else if(operation==='query'){method='kloudy/query';params={version:'kldy.query/1',need:a.goal,limit:a.limit??3,...(a.scope?{scope:a.scope}:{})};}else if(operation==='assemble'){if(!Array.isArray(a.selection_ids)||!a.selection_ids.length||a.selection_ids.length>16||new Set(a.selection_ids).size!==a.selection_ids.length||a.selection_ids.some(id=>typeof id!=='string'||!id||id.length>128))throw Error('invalid_selection_ids');method='tools/list';params={_meta:{selection_ids:a.selection_ids}};}else if(operation==='call'){method='tools/call';params={name:a.name,arguments:a.arguments,_meta:{selection_id:a.selection_id}};}
29
- else if(operation==='confirm'){method='kloudy/confirm';params=confirmationParams(a);}
30
- else if(operation==='status'||operation==='cancel'){method=operation==='status'?'kloudy/receipt':'kloudy/cancel';params={selection_id:a.selection_id};}
31
- else throw Error('unsupported_operation');
32
- upstream={...message,method,params};project=operation==='conversation'?x=>toolResult(conversationResult(x,params,a.conversation_operation)):operation==='call'?x=>x:operation==='introduce'?x=>({...toolResult(x),content:welcomeContent(x)}):toolResult;
33
- }
34
- return {upstream,project};
35
- }
36
-
37
- export function confirmationParams(a){
38
- if(typeof a.selection_id!=='string'||!a.selection_id||a.selection_id.length>128||a.decision!=='approve'||!['tap','typed','os-voice'].includes(a.source))throw Error('invalid_confirmation');
39
- const token=a.token!==undefined,revision=a.revision!==undefined;
40
- if(token===revision||token&&(typeof a.token!=='string'||!a.token||a.token.length>512)||revision&&(!Number.isSafeInteger(a.revision)||a.revision<0))throw Error('invalid_confirmation_binding');
41
- return {selection_id:a.selection_id,decision:a.decision,source:a.source,...(token?{token:a.token}:{revision:a.revision})};
42
- }
1
+ import {conversationParams,conversationResult} from './conversation.mjs';
2
+ import {actionInput,hostingParams,connectionParams,stableIntentKey} from './product-actions.mjs';
3
+ import {welcomeContent} from './welcome.mjs';
4
+ // Stateless MCP adapter for the engine's existing HTTPK methods.
5
+ // No grants, model decisions, retries, provider credentials or execution live here.
6
+ import {randomUUID,createHash} from 'node:crypto';
7
+ import {autoupdateEnabled} from './autoupdate.mjs';
8
+ export const hostedDoor={name:'kloudy',description:'Use this whenever the user mentions Kloudy, or asks to find, route, or call any MCP tool or SDK. Ask for Kloudy. Start with introduce for Kloudy or /kloudy. Use ask to receive exactly one engine-selected tool and its exact arguments, assemble up to 16 owned selections into deduplicated tools, then call only the chosen exact selection. Status and cancel refer to that selection. Respect host permissions and exact engine approvals.',inputSchema:{type:'object',properties:{operation:{type:'string',enum:['conversation','action','wallet_connection','hosting','wallet','introduce','ask','query','assemble','call','confirm','status','cancel']},conversation_operation:{type:'string',enum:['read','append','sync']},conversation:{type:'object'},action:{type:'string',enum:['chain.head','wallet.prepare','bww.fetch','bww.publish']},inputs:{type:'object'},idempotency_key:{type:'string',maxLength:128},chain:{type:'string',maxLength:100},budget_usd_micros:{type:'integer',minimum:1,maximum:1000000000000},wallet_operation:{type:'string',enum:['balance','top_up']},amount_usd:{type:'string'},goal:{type:'string',maxLength:1000},scope:{type:'object'},limit:{type:'integer',minimum:1,maximum:5},project_context:{type:'object',properties:{languages:{type:'array',items:{type:'string'},maxItems:12},manifests:{type:'array',items:{type:'string'},maxItems:12}},required:['languages','manifests'],additionalProperties:false},selection_ids:{type:'array',items:{type:'string',minLength:1,maxLength:128},minItems:1,maxItems:16,uniqueItems:true},selection_id:{type:'string'},token:{type:'string',minLength:1,maxLength:512},revision:{type:'integer',minimum:0},decision:{type:'string',enum:['approve']},source:{type:'string',enum:['tap','typed','os-voice']},name:{type:'string'},arguments:{type:'object'}},required:['operation'],additionalProperties:false}};
9
+ export const hostedInstructions='One Kloudy tool. Call introduce when the user types Kloudy; ask returns one selected tool; call executes only its exact selection. The engine enforces grants and approval. Welcome the user once. Kloudy or /kloudy addresses this system explicitly; use Kloudy for relevant tasks without requiring that prefix. Bring in only tools relevant to the current project. An account connection or retained tool does not activate it in unrelated projects. Natural keep/cleanup requests need the authoritative lifecycle service; do not invent retention, deletion or grants. Use host authorization when needed; never collect provider secrets in chat.';
10
+ const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(k=>[k,canonical(value[k])])):value;
11
+ export const hostedCatalog={version:createHash('sha256').update(JSON.stringify(canonical({tools:[hostedDoor],instructions:hostedInstructions}))).digest('hex').slice(0,12),issued:new Date().toISOString(),minClient:null,deprecations:[]};
12
+ const toolResult=result=>({content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result});
13
+ export function adaptMcp(message,{autoupdate=process.env.KLOUDY_MCP_AUTOUPDATE}={}){
14
+ const enabled=autoupdateEnabled(autoupdate);
15
+ let upstream=message,project=x=>x;
16
+ if(message.method==='initialize')project=x=>({...x,instructions:hostedInstructions,capabilities:{tools:{listChanged:enabled?(x.capabilities?.tools?.listChanged??true):false}},...(enabled?{_meta:{...x._meta,'ai.kloudy/catalog':hostedCatalog}}:{})});
17
+ if(message.method==='tools/list'&&!message.params?._meta?.selection_ids)project=()=>({tools:[hostedDoor],...(enabled?{_meta:{'ai.kloudy/catalog':hostedCatalog}}:{})});
18
+ if(message.method==='tools/call'&&message.params?.name==='kloudy'){
19
+ const a=message.params.arguments||{},operation=a.operation||'introduce';
20
+ let method,params;
21
+ if(operation==='conversation'){method='kloudy/conversation/'+a.conversation_operation;params=conversationParams(a.conversation_operation,a.conversation);}
22
+ else if(operation==='action'){method='kloudy/intent';params={idempotency_key:stableIntentKey(a.idempotency_key||randomUUID()),input:actionInput(a.action,a.inputs)};}
23
+ else if(operation==='wallet_connection'){method='kloudy/wallet/connection';params=connectionParams(a.chain);}
24
+ else if(operation==='hosting'){method='kloudy/hosting';params=hostingParams(a.budget_usd_micros);}
25
+ else if(operation==='wallet'){const operation=a.wallet_operation||'balance';if(!['balance','top_up'].includes(operation)||operation==='balance'&&a.amount_usd!==undefined||operation==='top_up'&&(typeof a.amount_usd!=='string'||!/^\d{1,6}(?:\.\d{1,2})?$/.test(a.amount_usd)||Number(a.amount_usd)<=0))throw Error('invalid_wallet_request');method='kloudy/wallet';params={operation,...(operation==='top_up'?{amount_usd:a.amount_usd}:{})};}
26
+ else if(operation==='introduce'||operation==='ask'){
27
+ method='kloudy/intent';params={idempotency_key:randomUUID(),input:{type:'text',text:operation==='introduce'?'Kloudy':a.goal,...(operation==='introduce'?{project_context:a.project_context||{languages:[],manifests:[]}}:{})}};
28
+ }else if(operation==='query'){method='kloudy/query';params={version:'kldy.query/1',need:a.goal,limit:a.limit??3,...(a.scope?{scope:a.scope}:{})};}else if(operation==='assemble'){if(!Array.isArray(a.selection_ids)||!a.selection_ids.length||a.selection_ids.length>16||new Set(a.selection_ids).size!==a.selection_ids.length||a.selection_ids.some(id=>typeof id!=='string'||!id||id.length>128))throw Error('invalid_selection_ids');method='tools/list';params={_meta:{selection_ids:a.selection_ids}};}else if(operation==='call'){method='tools/call';params={name:a.name,arguments:a.arguments,_meta:{selection_id:a.selection_id}};}
29
+ else if(operation==='confirm'){method='kloudy/confirm';params=confirmationParams(a);}
30
+ else if(operation==='status'||operation==='cancel'){method=operation==='status'?'kloudy/receipt':'kloudy/cancel';params={selection_id:a.selection_id};}
31
+ else throw Error('unsupported_operation');
32
+ upstream={...message,method,params};project=operation==='conversation'?x=>toolResult(conversationResult(x,params,a.conversation_operation)):operation==='call'?x=>x:operation==='introduce'?x=>({...toolResult(x),content:welcomeContent(x)}):toolResult;
33
+ }
34
+ return {upstream,project};
35
+ }
36
+
37
+ export function confirmationParams(a){
38
+ if(typeof a.selection_id!=='string'||!a.selection_id||a.selection_id.length>128||a.decision!=='approve'||!['tap','typed','os-voice'].includes(a.source))throw Error('invalid_confirmation');
39
+ const token=a.token!==undefined,revision=a.revision!==undefined;
40
+ if(token===revision||token&&(typeof a.token!=='string'||!a.token||a.token.length>512)||revision&&(!Number.isSafeInteger(a.revision)||a.revision<0))throw Error('invalid_confirmation_binding');
41
+ return {selection_id:a.selection_id,decision:a.decision,source:a.source,...(token?{token:a.token}:{revision:a.revision})};
42
+ }