kloudy 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MCP-DEFAULT.md +12 -12
- package/README.md +307 -307
- package/bin/entry.mjs +22 -22
- package/bin/kloudy.mjs +70 -70
- package/package.json +68 -68
- package/schemas/SDF-LICENSE.txt +21 -21
- package/schemas/sdf-document-0.2.schema.json +411 -411
- package/schemas/sdf-webpage-v1.schema.json +176 -176
- package/src/activity.mjs +23 -23
- package/src/approval.mjs +25 -25
- package/src/autoupdate.mjs +2 -0
- package/src/client.mjs +96 -96
- package/src/connect-card.mjs +7 -11
- package/src/connect-ui.mjs +2 -2
- package/src/connection.mjs +14 -14
- package/src/editor-install.mjs +22 -4
- package/src/hosted.mjs +40 -35
- package/src/httpk.mjs +64 -64
- package/src/install.mjs +35 -35
- package/src/introduce.mjs +33 -33
- package/src/mcp.mjs +72 -72
- package/src/models.mjs +55 -55
- package/src/preferences-ui.mjs +1 -1
- package/src/public.mjs +41 -41
- package/src/remote.mjs +59 -27
- package/src/sample-cards.mjs +1 -1
- package/src/sample-ui.mjs +1 -1
- package/src/starter.mjs +43 -43
- package/src/strings.mjs +51 -51
- package/src/wrap.mjs +58 -58
package/src/client.mjs
CHANGED
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import {randomUUID} from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
export class KloudyError extends Error {
|
|
4
|
-
constructor(code,message){super(message);this.code=code;}
|
|
5
|
-
}
|
|
6
|
-
const fail=(code,message)=>{throw new KloudyError(code,message);};
|
|
7
|
-
const object=x=>x&&typeof x==='object'&&!Array.isArray(x);
|
|
8
|
-
const id=x=>{if(typeof x!=='string'||!/^[-\w]{1,128}$/.test(x))fail('invalid_id','Invalid engine identifier.');return encodeURIComponent(x);};
|
|
9
|
-
const terminal=x=>['completed','failed','cancelled'].includes(x);
|
|
10
|
-
export function httpTransport({origin,token,fetcher=fetch}){
|
|
11
|
-
const url=new URL(origin);
|
|
12
|
-
if(url.username||url.password||url.pathname!=='/'||url.search||url.hash||!(url.protocol==='https:'||(url.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(url.hostname))))fail('invalid_origin','Use an HTTPS or loopback engine origin.');
|
|
13
|
-
if(typeof token!=='string'||!token||/[\r\n]/.test(token))fail('registration_required','Ask for Kloudy and register before searching for tools.');
|
|
14
|
-
return async(method,path,body)=>{
|
|
15
|
-
let response;
|
|
16
|
-
try{response=await fetcher(url.origin+'/v1/integrations'+path,{method,redirect:'error',signal:AbortSignal.timeout(15000),headers:{authorization:'Bearer '+token,...(body?{'content-type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});}
|
|
17
|
-
catch{fail('engine_unavailable','The engine is unavailable. No operation was retried.');}
|
|
18
|
-
if(!response.ok)fail([401,403].includes(response.status)?'registration_required':response.status===409?'stale_state':response.status===429?'rate_limited':'engine_unavailable','The engine did not accept this request. Reconnect, refresh or finish pending work before retrying.');
|
|
19
|
-
let size=0;const chunks=[];
|
|
20
|
-
for await(const chunk of response.body){size+=chunk.length;if(size>524288)fail('invalid_response','The engine response exceeded its limit.');chunks.push(chunk);}
|
|
21
|
-
try{return JSON.parse(Buffer.concat(chunks).toString());}catch{fail('invalid_response','Invalid engine response.');}
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
export function projectToolbox(receipt){
|
|
25
|
-
const n=receipt?.narrowing;
|
|
26
|
-
if(!object(n)||!Array.isArray(n.shortlist)||n.shortlist.length>3)fail('invalid_response','Expected a bounded engine shortlist.');
|
|
27
|
-
const box=n.toolbox,fn=box?.tool?.function;
|
|
28
|
-
let tools=[];
|
|
29
|
-
if(box?.status==='ready'){
|
|
30
|
-
if(!fn||typeof fn.name!=='string'||!/^[-\w.]{1,100}$/.test(fn.name)||typeof fn.description!=='string'||fn.description.length>2000||fn.parameters?.type!=='object'||!object(fn.parameters.properties)||!n.shortlist.some(x=>x.index===box.candidate_index)||n.measurements?.definitions_emitted!==1)fail('invalid_response','Expected exactly one authorized tool definition.');
|
|
31
|
-
const properties={};
|
|
32
|
-
for(const [name,field] of Object.entries(fn.parameters.properties)){
|
|
33
|
-
if(!/^\w{1,80}$/.test(name)||['__proto__','constructor','prototype'].includes(name)||field?.type!=='string')fail('unsupported_schema','This client supports the current engine string-input tools.');
|
|
34
|
-
properties[name]={type:'string'};
|
|
35
|
-
for(const key of ['minLength','maxLength'])if(field[key]!==undefined){if(!Number.isSafeInteger(field[key])||field[key]<0||field[key]>10000)fail('invalid_response','Invalid tool input bound.');properties[name][key]=field[key];}
|
|
36
|
-
}
|
|
37
|
-
if(Object.keys(properties).length>16)fail('invalid_response','Too many tool inputs.');
|
|
38
|
-
const required=fn.parameters.required??[];
|
|
39
|
-
if(!Array.isArray(required)||required.some(x=>!Object.hasOwn(properties,x)))fail('invalid_response','Invalid required inputs.');
|
|
40
|
-
tools=[{name:fn.name,description:fn.description,inputSchema:{type:'object',properties,required,additionalProperties:false}}];
|
|
41
|
-
}
|
|
42
|
-
return {status:tools.length?'ready':'empty',tools,reason:tools.length?'One engine-selected tool.':String(box?.reason||'no_match').slice(0,100),measurements:{candidatesRead:n.measurements?.candidates_read??null,definitionsEmitted:tools.length}};
|
|
43
|
-
}
|
|
44
|
-
export class KloudyClient {
|
|
45
|
-
constructor({transport,state={},save=async()=>{},mode='autonomous'}){if(typeof transport!=='function')fail('registration_required','Registration and a scoped engine connection are required.');if(!['autonomous','confirm'].includes(mode))fail('invalid_mode','Choose autonomous or confirm.');this.mode=mode;this.transport=transport;this.state=state;this.save=save;this.busy=false;}
|
|
46
|
-
async exclusive(work){if(this.busy)fail('busy','Wait for the current operation.');this.busy=true;try{return await work();}finally{this.busy=false;}}
|
|
47
|
-
async request(method,path,body,limit=4096){if(body&&Buffer.byteLength(JSON.stringify(body))>limit)fail('too_large','Request exceeds the engine limit.');return this.transport(method,path,body);}
|
|
48
|
-
async session(){
|
|
49
|
-
if(!this.state.sessionId){const opened=await this.request('POST','/sessions',{idempotency_key:randomUUID(),front_door:'kloudy'});id(opened.session_id);this.state.sessionId=opened.session_id;await this.save(this.state);}
|
|
50
|
-
let current=await this.request('GET','/sessions/'+id(this.state.sessionId));
|
|
51
|
-
if(current.state==='closed')fail('session_closed','This session is closed. Register or reconnect a fresh session.');
|
|
52
|
-
if(!Number.isSafeInteger(current.revision)||current.revision<0)fail('invalid_response','Invalid engine session revision.');
|
|
53
|
-
if(current.interaction_mode!==this.mode){
|
|
54
|
-
await this.request('POST',`/sessions/${id(this.state.sessionId)}/mode`,{idempotency_key:randomUUID(),expected_revision:current.revision,mode:this.mode,source:'typed'});
|
|
55
|
-
current=await this.request('GET','/sessions/'+id(this.state.sessionId));
|
|
56
|
-
if(current.interaction_mode!==this.mode||!Number.isSafeInteger(current.revision)||current.revision<0)fail('mode_not_supported','The engine has not acknowledged this surface’s approval mode. No tool was invoked.');
|
|
57
|
-
}
|
|
58
|
-
return current;
|
|
59
|
-
}
|
|
60
|
-
async ask(goal,{candidates=[],lane='non_medical'}={}){return this.exclusive(async()=>{
|
|
61
|
-
if(typeof goal!=='string'||!goal.trim()||[...goal].length>500||!['medical','non_medical'].includes(lane)||!Array.isArray(candidates)||candidates.length>16)fail('invalid_input','Use a goal of 1–500 characters and at most 16 public metadata candidates.');
|
|
62
|
-
const current=await this.session();
|
|
63
|
-
if(current.latest_request&&!terminal(current.latest_request.state))fail('pending_request','Finish or cancel the current action first.');
|
|
64
|
-
this.state.selection=null;await this.save(this.state);
|
|
65
|
-
if(!candidates.length){
|
|
66
|
-
const receipt=await this.request('POST',`/sessions/${id(this.state.sessionId)}/route`,{idempotency_key:randomUUID(),expected_revision:current.revision,query:goal,lane,compare:false,profile:null,url:null,language:'en'});
|
|
67
|
-
// An empty Toolbox never widens into a whole-server catalog.
|
|
68
|
-
const result={schema:'kloudy.toolbox/1',sessionId:this.state.sessionId,toolbox:{status:'empty',tools:[],reason:'discovery_feed_required'},fallback:{kind:'source_card',available:false}};
|
|
69
|
-
const destinations=receipt.routing?.view?.destinations;
|
|
70
|
-
if(Array.isArray(destinations)&&destinations[0]){const d=destinations[0];try{const url=new URL(d.url);if(url.protocol==='https:'&&!url.username&&!url.password)result.fallback={kind:'source_card',available:true,title:String(d.title||'Source').slice(0,200),url:url.href};}catch{}}
|
|
71
|
-
return result;
|
|
72
|
-
}
|
|
73
|
-
for(const c of candidates){if(!object(c)||Object.keys(c).some(k=>!['source_url','discovered_via','resource'].includes(k))||!['seo','geo'].includes(c.discovered_via)||!object(c.resource))fail('invalid_input','Use public KLDY/FHIR discovery metadata.');const u=new URL(c.source_url);if(u.protocol!=='https:'||u.username||u.password||u.search||u.hash)fail('invalid_input','Metadata sources must be credential-free HTTPS identifiers.');}
|
|
74
|
-
const key=randomUUID();
|
|
75
|
-
const receipt=await this.request('POST',`/sessions/${id(this.state.sessionId)}/narrow`,{idempotency_key:key,expected_revision:current.revision,goal,lane,candidates},65536);
|
|
76
|
-
const toolbox=projectToolbox(receipt);
|
|
77
|
-
this.state.selection=toolbox.tools.length?{key,tool:toolbox.tools[0]}:null;await this.save(this.state);
|
|
78
|
-
return {schema:'kloudy.toolbox/1',sessionId:this.state.sessionId,toolbox};
|
|
79
|
-
});}
|
|
80
|
-
async call(name,inputs){return this.exclusive(async()=>{
|
|
81
|
-
const selected=this.state.selection;if(!selected||selected.tool.name!==name)fail('not_selected','Ask for Kloudy to select this tool first.');
|
|
82
|
-
const schema=selected.tool.inputSchema;if(!object(inputs)||Object.keys(inputs).some(k=>!Object.hasOwn(schema.properties,k))||schema.required.some(k=>!Object.hasOwn(inputs,k)))fail('invalid_input','Use only the selected tool inputs.');
|
|
83
|
-
for(const [k,v] of Object.entries(inputs)){const f=schema.properties[k];if(typeof v!=='string'||[...v].length<(f.minLength||0)||[...v].length>(f.maxLength??10000))fail('invalid_input','Invalid tool input.');}
|
|
84
|
-
const current=await this.session();if(current.latest_request&&!terminal(current.latest_request.state))fail('pending_request','Finish the pending action first.');
|
|
85
|
-
const receipt=await this.request('POST',`/sessions/${id(this.state.sessionId)}/escalate`,{idempotency_key:randomUUID(),expected_revision:current.revision,narrowing_key:selected.key,inputs});
|
|
86
|
-
id(receipt.request_id);this.state.requestId=receipt.request_id;this.state.selection=null;await this.save(this.state);return this.status();
|
|
87
|
-
});}
|
|
88
|
-
async status(){if(!this.state.requestId)return {state:'idle',mode:this.mode,sessionId:this.state.sessionId||null};const receipt=await this.request('GET','/requests/'+id(this.state.requestId));return {...receipt,...(this.mode==='autonomous'&&receipt.state==='awaiting_approval'?{mode:'autonomous',blocked:{code:'engine_autonomy_not_supported',message:'The engine still requires a separate approval for this action. No approval was fabricated. Cancel or check the engine deployment.'}}:{})};}
|
|
89
|
-
async decide(decision,revision){return this.exclusive(async()=>{
|
|
90
|
-
if(!this.state.requestId)fail('no_request','No action is pending in this session.');
|
|
91
|
-
if(decision==='approve'){if(this.mode!=='confirm')fail('autonomous_only','This client does not offer a second approval gate.');if(typeof revision!=='string'||!revision||revision.length>256)fail('approval_required','Read the exact action and supply its approval revision.');await this.request('POST',`/requests/${id(this.state.requestId)}/approve`,{revision,decision:'approve'});}
|
|
92
|
-
else if(decision==='cancel')await this.request('POST',`/requests/${id(this.state.requestId)}/cancel`,{});
|
|
93
|
-
else fail('invalid_input','Choose approve or cancel.');
|
|
94
|
-
return this.status();
|
|
95
|
-
});}
|
|
96
|
-
}
|
|
1
|
+
import {randomUUID} from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export class KloudyError extends Error {
|
|
4
|
+
constructor(code,message){super(message);this.code=code;}
|
|
5
|
+
}
|
|
6
|
+
const fail=(code,message)=>{throw new KloudyError(code,message);};
|
|
7
|
+
const object=x=>x&&typeof x==='object'&&!Array.isArray(x);
|
|
8
|
+
const id=x=>{if(typeof x!=='string'||!/^[-\w]{1,128}$/.test(x))fail('invalid_id','Invalid engine identifier.');return encodeURIComponent(x);};
|
|
9
|
+
const terminal=x=>['completed','failed','cancelled'].includes(x);
|
|
10
|
+
export function httpTransport({origin,token,fetcher=fetch}){
|
|
11
|
+
const url=new URL(origin);
|
|
12
|
+
if(url.username||url.password||url.pathname!=='/'||url.search||url.hash||!(url.protocol==='https:'||(url.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(url.hostname))))fail('invalid_origin','Use an HTTPS or loopback engine origin.');
|
|
13
|
+
if(typeof token!=='string'||!token||/[\r\n]/.test(token))fail('registration_required','Ask for Kloudy and register before searching for tools.');
|
|
14
|
+
return async(method,path,body)=>{
|
|
15
|
+
let response;
|
|
16
|
+
try{response=await fetcher(url.origin+'/v1/integrations'+path,{method,redirect:'error',signal:AbortSignal.timeout(15000),headers:{authorization:'Bearer '+token,...(body?{'content-type':'application/json'}:{})},...(body?{body:JSON.stringify(body)}:{})});}
|
|
17
|
+
catch{fail('engine_unavailable','The engine is unavailable. No operation was retried.');}
|
|
18
|
+
if(!response.ok)fail([401,403].includes(response.status)?'registration_required':response.status===409?'stale_state':response.status===429?'rate_limited':'engine_unavailable','The engine did not accept this request. Reconnect, refresh or finish pending work before retrying.');
|
|
19
|
+
let size=0;const chunks=[];
|
|
20
|
+
for await(const chunk of response.body){size+=chunk.length;if(size>524288)fail('invalid_response','The engine response exceeded its limit.');chunks.push(chunk);}
|
|
21
|
+
try{return JSON.parse(Buffer.concat(chunks).toString());}catch{fail('invalid_response','Invalid engine response.');}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function projectToolbox(receipt){
|
|
25
|
+
const n=receipt?.narrowing;
|
|
26
|
+
if(!object(n)||!Array.isArray(n.shortlist)||n.shortlist.length>3)fail('invalid_response','Expected a bounded engine shortlist.');
|
|
27
|
+
const box=n.toolbox,fn=box?.tool?.function;
|
|
28
|
+
let tools=[];
|
|
29
|
+
if(box?.status==='ready'){
|
|
30
|
+
if(!fn||typeof fn.name!=='string'||!/^[-\w.]{1,100}$/.test(fn.name)||typeof fn.description!=='string'||fn.description.length>2000||fn.parameters?.type!=='object'||!object(fn.parameters.properties)||!n.shortlist.some(x=>x.index===box.candidate_index)||n.measurements?.definitions_emitted!==1)fail('invalid_response','Expected exactly one authorized tool definition.');
|
|
31
|
+
const properties={};
|
|
32
|
+
for(const [name,field] of Object.entries(fn.parameters.properties)){
|
|
33
|
+
if(!/^\w{1,80}$/.test(name)||['__proto__','constructor','prototype'].includes(name)||field?.type!=='string')fail('unsupported_schema','This client supports the current engine string-input tools.');
|
|
34
|
+
properties[name]={type:'string'};
|
|
35
|
+
for(const key of ['minLength','maxLength'])if(field[key]!==undefined){if(!Number.isSafeInteger(field[key])||field[key]<0||field[key]>10000)fail('invalid_response','Invalid tool input bound.');properties[name][key]=field[key];}
|
|
36
|
+
}
|
|
37
|
+
if(Object.keys(properties).length>16)fail('invalid_response','Too many tool inputs.');
|
|
38
|
+
const required=fn.parameters.required??[];
|
|
39
|
+
if(!Array.isArray(required)||required.some(x=>!Object.hasOwn(properties,x)))fail('invalid_response','Invalid required inputs.');
|
|
40
|
+
tools=[{name:fn.name,description:fn.description,inputSchema:{type:'object',properties,required,additionalProperties:false}}];
|
|
41
|
+
}
|
|
42
|
+
return {status:tools.length?'ready':'empty',tools,reason:tools.length?'One engine-selected tool.':String(box?.reason||'no_match').slice(0,100),measurements:{candidatesRead:n.measurements?.candidates_read??null,definitionsEmitted:tools.length}};
|
|
43
|
+
}
|
|
44
|
+
export class KloudyClient {
|
|
45
|
+
constructor({transport,state={},save=async()=>{},mode='autonomous'}){if(typeof transport!=='function')fail('registration_required','Registration and a scoped engine connection are required.');if(!['autonomous','confirm'].includes(mode))fail('invalid_mode','Choose autonomous or confirm.');this.mode=mode;this.transport=transport;this.state=state;this.save=save;this.busy=false;}
|
|
46
|
+
async exclusive(work){if(this.busy)fail('busy','Wait for the current operation.');this.busy=true;try{return await work();}finally{this.busy=false;}}
|
|
47
|
+
async request(method,path,body,limit=4096){if(body&&Buffer.byteLength(JSON.stringify(body))>limit)fail('too_large','Request exceeds the engine limit.');return this.transport(method,path,body);}
|
|
48
|
+
async session(){
|
|
49
|
+
if(!this.state.sessionId){const opened=await this.request('POST','/sessions',{idempotency_key:randomUUID(),front_door:'kloudy'});id(opened.session_id);this.state.sessionId=opened.session_id;await this.save(this.state);}
|
|
50
|
+
let current=await this.request('GET','/sessions/'+id(this.state.sessionId));
|
|
51
|
+
if(current.state==='closed')fail('session_closed','This session is closed. Register or reconnect a fresh session.');
|
|
52
|
+
if(!Number.isSafeInteger(current.revision)||current.revision<0)fail('invalid_response','Invalid engine session revision.');
|
|
53
|
+
if(current.interaction_mode!==this.mode){
|
|
54
|
+
await this.request('POST',`/sessions/${id(this.state.sessionId)}/mode`,{idempotency_key:randomUUID(),expected_revision:current.revision,mode:this.mode,source:'typed'});
|
|
55
|
+
current=await this.request('GET','/sessions/'+id(this.state.sessionId));
|
|
56
|
+
if(current.interaction_mode!==this.mode||!Number.isSafeInteger(current.revision)||current.revision<0)fail('mode_not_supported','The engine has not acknowledged this surface’s approval mode. No tool was invoked.');
|
|
57
|
+
}
|
|
58
|
+
return current;
|
|
59
|
+
}
|
|
60
|
+
async ask(goal,{candidates=[],lane='non_medical'}={}){return this.exclusive(async()=>{
|
|
61
|
+
if(typeof goal!=='string'||!goal.trim()||[...goal].length>500||!['medical','non_medical'].includes(lane)||!Array.isArray(candidates)||candidates.length>16)fail('invalid_input','Use a goal of 1–500 characters and at most 16 public metadata candidates.');
|
|
62
|
+
const current=await this.session();
|
|
63
|
+
if(current.latest_request&&!terminal(current.latest_request.state))fail('pending_request','Finish or cancel the current action first.');
|
|
64
|
+
this.state.selection=null;await this.save(this.state);
|
|
65
|
+
if(!candidates.length){
|
|
66
|
+
const receipt=await this.request('POST',`/sessions/${id(this.state.sessionId)}/route`,{idempotency_key:randomUUID(),expected_revision:current.revision,query:goal,lane,compare:false,profile:null,url:null,language:'en'});
|
|
67
|
+
// An empty Toolbox never widens into a whole-server catalog.
|
|
68
|
+
const result={schema:'kloudy.toolbox/1',sessionId:this.state.sessionId,toolbox:{status:'empty',tools:[],reason:'discovery_feed_required'},fallback:{kind:'source_card',available:false}};
|
|
69
|
+
const destinations=receipt.routing?.view?.destinations;
|
|
70
|
+
if(Array.isArray(destinations)&&destinations[0]){const d=destinations[0];try{const url=new URL(d.url);if(url.protocol==='https:'&&!url.username&&!url.password)result.fallback={kind:'source_card',available:true,title:String(d.title||'Source').slice(0,200),url:url.href};}catch{}}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
for(const c of candidates){if(!object(c)||Object.keys(c).some(k=>!['source_url','discovered_via','resource'].includes(k))||!['seo','geo'].includes(c.discovered_via)||!object(c.resource))fail('invalid_input','Use public KLDY/FHIR discovery metadata.');const u=new URL(c.source_url);if(u.protocol!=='https:'||u.username||u.password||u.search||u.hash)fail('invalid_input','Metadata sources must be credential-free HTTPS identifiers.');}
|
|
74
|
+
const key=randomUUID();
|
|
75
|
+
const receipt=await this.request('POST',`/sessions/${id(this.state.sessionId)}/narrow`,{idempotency_key:key,expected_revision:current.revision,goal,lane,candidates},65536);
|
|
76
|
+
const toolbox=projectToolbox(receipt);
|
|
77
|
+
this.state.selection=toolbox.tools.length?{key,tool:toolbox.tools[0]}:null;await this.save(this.state);
|
|
78
|
+
return {schema:'kloudy.toolbox/1',sessionId:this.state.sessionId,toolbox};
|
|
79
|
+
});}
|
|
80
|
+
async call(name,inputs){return this.exclusive(async()=>{
|
|
81
|
+
const selected=this.state.selection;if(!selected||selected.tool.name!==name)fail('not_selected','Ask for Kloudy to select this tool first.');
|
|
82
|
+
const schema=selected.tool.inputSchema;if(!object(inputs)||Object.keys(inputs).some(k=>!Object.hasOwn(schema.properties,k))||schema.required.some(k=>!Object.hasOwn(inputs,k)))fail('invalid_input','Use only the selected tool inputs.');
|
|
83
|
+
for(const [k,v] of Object.entries(inputs)){const f=schema.properties[k];if(typeof v!=='string'||[...v].length<(f.minLength||0)||[...v].length>(f.maxLength??10000))fail('invalid_input','Invalid tool input.');}
|
|
84
|
+
const current=await this.session();if(current.latest_request&&!terminal(current.latest_request.state))fail('pending_request','Finish the pending action first.');
|
|
85
|
+
const receipt=await this.request('POST',`/sessions/${id(this.state.sessionId)}/escalate`,{idempotency_key:randomUUID(),expected_revision:current.revision,narrowing_key:selected.key,inputs});
|
|
86
|
+
id(receipt.request_id);this.state.requestId=receipt.request_id;this.state.selection=null;await this.save(this.state);return this.status();
|
|
87
|
+
});}
|
|
88
|
+
async status(){if(!this.state.requestId)return {state:'idle',mode:this.mode,sessionId:this.state.sessionId||null};const receipt=await this.request('GET','/requests/'+id(this.state.requestId));return {...receipt,...(this.mode==='autonomous'&&receipt.state==='awaiting_approval'?{mode:'autonomous',blocked:{code:'engine_autonomy_not_supported',message:'The engine still requires a separate approval for this action. No approval was fabricated. Cancel or check the engine deployment.'}}:{})};}
|
|
89
|
+
async decide(decision,revision){return this.exclusive(async()=>{
|
|
90
|
+
if(!this.state.requestId)fail('no_request','No action is pending in this session.');
|
|
91
|
+
if(decision==='approve'){if(this.mode!=='confirm')fail('autonomous_only','This client does not offer a second approval gate.');if(typeof revision!=='string'||!revision||revision.length>256)fail('approval_required','Read the exact action and supply its approval revision.');await this.request('POST',`/requests/${id(this.state.requestId)}/approve`,{revision,decision:'approve'});}
|
|
92
|
+
else if(decision==='cancel')await this.request('POST',`/requests/${id(this.state.requestId)}/cancel`,{});
|
|
93
|
+
else fail('invalid_input','Choose approve or cancel.');
|
|
94
|
+
return this.status();
|
|
95
|
+
});}
|
|
96
|
+
}
|
package/src/connect-card.mjs
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
import {connectHTML,publicConnectHTML} from './connect-ui.mjs';
|
|
2
2
|
|
|
3
3
|
export const CONNECT_URI='ui://widget/kloudy-connect-v1.html';
|
|
4
|
-
export const connectResource={uri:CONNECT_URI,name:'Kloudy setup',description:'
|
|
4
|
+
export const connectResource={uri:CONNECT_URI,name:'Kloudy setup',description:'Show the Kloudy install step or sign-in availability.',mimeType:'text/html;profile=mcp-app'};
|
|
5
5
|
export const connectTool={
|
|
6
6
|
name:'kloudy_connect_card',
|
|
7
7
|
title:'Kloudy sign-in and install card',
|
|
8
|
-
description:'Show sign-in or install
|
|
8
|
+
description:'Show sign-in availability or the one-step install card. First call find_mcp_tools with operation ask and a short setup request. If it returns kind account or connect, call this card once with the returned mode. Copying the command does not install anything or authorize private tools. Text-only hosts show the same instruction.',
|
|
9
9
|
annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},
|
|
10
10
|
inputSchema:{type:'object',properties:{mode:{type:'string',enum:['signin','install'],description:'Use the mode returned by find_mcp_tools: signin for account sign-in, or install for editor setup.'}},required:['mode'],additionalProperties:false},
|
|
11
|
-
_meta:{ui:{resourceUri:CONNECT_URI},'openai/outputTemplate':CONNECT_URI,'openai/toolInvocation/invoking':'Opening Kloudy setup…','openai/toolInvocation/invoked':'Kloudy setup
|
|
11
|
+
_meta:{ui:{resourceUri:CONNECT_URI},'openai/outputTemplate':CONNECT_URI,'openai/toolInvocation/invoking':'Opening Kloudy setup…','openai/toolInvocation/invoked':'Kloudy setup'}
|
|
12
12
|
};
|
|
13
13
|
export function connectArguments(value){return !!value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).length===1&&['signin','install'].includes(value.mode);}
|
|
14
14
|
export function connectResult(mode,{publicOnly=false}={}){
|
|
15
15
|
const install=mode==='install';
|
|
16
|
-
const message=install?'Choose
|
|
17
|
-
const actions=install?
|
|
18
|
-
|
|
19
|
-
...(!publicOnly?[{label:'Connect to Muse',url:'https://kloudy.ai/install#muse'}]:[]),
|
|
20
|
-
{label:'Sign in',url:'https://kloudy.ai/install'}
|
|
21
|
-
]:[{label:'Sign in to Kloudy',url:'https://kloudy.ai/install'},{label:'Add to an IDE',url:'https://kloudy.ai/install#clients'}];
|
|
22
|
-
return {content:[{type:'text',text:message+'\n\n'+actions.map(a=>`[${a.label}](${a.url})`).join('\n')}],structuredContent:{tier:'public',kind:install?'connect':'account',mode,items:[],message,actions,model_calls:0,executed:false}};
|
|
16
|
+
const message=install?'Choose Connect, then pick your app.':'GitHub sign-in coming soon. Find public tools today.';
|
|
17
|
+
const actions=[install?{label:'Connect',url:'https://kloudy.ai/install'}:{label:'Find tools',url:'https://kloudy.ai/install#discover'}];
|
|
18
|
+
return {content:[{type:'text',text:message+'\n\n'+actions.map(a=>`[${a.label}](${a.url})`).join('\n')}],structuredContent:{tier:'public',kind:install?'connect':'account',mode,items:[],message,...(install?{copyText:'npx kloudy'}:{}),actions,model_calls:0,executed:false}};
|
|
23
19
|
}
|
|
24
20
|
export function connectContents({publicOnly=false}={}){return {contents:[{uri:CONNECT_URI,mimeType:connectResource.mimeType,text:publicOnly?publicConnectHTML:connectHTML,_meta:{
|
|
25
21
|
ui:{prefersBorder:true,domain:'https://kloudy.ai',csp:{connectDomains:[],resourceDomains:[]}},
|
|
26
22
|
'openai/widgetDescription':'Kloudy sign-in and setup choices. No account is created and no tool is authorized by opening a link.',
|
|
27
23
|
'openai/widgetPrefersBorder':true,'openai/widgetDomain':'https://kloudy.ai',
|
|
28
|
-
'openai/widgetCSP':{connect_domains:[],resource_domains:[],redirect_domains:['https://kloudy.ai','https://cursor.com','https://
|
|
24
|
+
'openai/widgetCSP':{connect_domains:[],resource_domains:[],redirect_domains:['https://kloudy.ai','https://cursor.com','https://code.visualstudio.com','https://windsurf.com','https://devin.ai','https://claude.com','https://claude.ai','https://chatgpt.com']}
|
|
29
25
|
}}]};}
|
|
30
26
|
// Public UI metadata still requires credential validation when called with a credential.
|
|
31
27
|
export function connectMessage(method,params={}){
|