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.
@@ -1,176 +1,176 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://kloudy.ai/schemas/sdf-webpage-v1.schema.json",
4
- "title": "Kloudy extractive SDF webpage profile v1",
5
- "allOf": [
6
- {
7
- "$ref": "https://sdfprotocol.org/schemas/sdf-document-0.2.schema.json"
8
- }
9
- ],
10
- "type": "object",
11
- "required": [
12
- "sections",
13
- "extensions",
14
- "metadata",
15
- "links",
16
- "entities"
17
- ],
18
- "properties": {
19
- "parent_type": {
20
- "const": "article"
21
- },
22
- "type": {
23
- "const": "article.x-kloudy-webpage"
24
- },
25
- "entities": {
26
- "maxItems": 0
27
- },
28
- "sections": {
29
- "type": "array",
30
- "minItems": 1,
31
- "maxItems": 40,
32
- "items": {
33
- "type": "object",
34
- "properties": {
35
- "title": {
36
- "type": "string",
37
- "maxLength": 160
38
- },
39
- "summary": {
40
- "type": "string",
41
- "maxLength": 1600
42
- }
43
- },
44
- "required": [
45
- "title",
46
- "summary"
47
- ]
48
- }
49
- },
50
- "links": {
51
- "maxItems": 50,
52
- "items": {
53
- "type": "object",
54
- "properties": {
55
- "href": {
56
- "type": "string",
57
- "maxLength": 2048
58
- },
59
- "title": {
60
- "type": "string",
61
- "maxLength": 160
62
- },
63
- "rel": {
64
- "const": "references"
65
- }
66
- },
67
- "required": [
68
- "href",
69
- "title",
70
- "rel"
71
- ]
72
- }
73
- },
74
- "extensions": {
75
- "type": "object",
76
- "required": [
77
- "x-kloudy"
78
- ],
79
- "properties": {
80
- "x-kloudy": {
81
- "type": "object",
82
- "required": [
83
- "profile",
84
- "interaction_mode",
85
- "capabilities",
86
- "ownership_verified",
87
- "published",
88
- "truncated",
89
- "resolution"
90
- ],
91
- "properties": {
92
- "profile": {
93
- "const": "kloudy.sdf.webpage/1"
94
- },
95
- "interaction_mode": {
96
- "const": "informational_only"
97
- },
98
- "capabilities": {
99
- "type": "array",
100
- "maxItems": 0
101
- },
102
- "ownership_verified": {
103
- "const": false
104
- },
105
- "published": {
106
- "const": false
107
- },
108
- "truncated": {
109
- "type": "boolean"
110
- },
111
- "resolution": {
112
- "enum": [
113
- "compact",
114
- "standard"
115
- ]
116
- }
117
- }
118
- }
119
- }
120
- },
121
- "source": {
122
- "type": "object",
123
- "properties": {
124
- "url": {
125
- "maxLength": 4096
126
- },
127
- "title": {
128
- "type": "string",
129
- "maxLength": 200
130
- }
131
- },
132
- "required": [
133
- "title"
134
- ]
135
- },
136
- "metadata": {
137
- "type": "object",
138
- "properties": {
139
- "title": {
140
- "type": "string",
141
- "maxLength": 200
142
- }
143
- },
144
- "required": [
145
- "title"
146
- ]
147
- },
148
- "summary": {
149
- "type": "object",
150
- "properties": {
151
- "brief": {
152
- "maxLength": 300
153
- },
154
- "key_points": {
155
- "maxItems": 4,
156
- "items": {
157
- "type": "string",
158
- "maxLength": 200
159
- }
160
- }
161
- }
162
- },
163
- "type_data": {
164
- "type": "object",
165
- "required": [
166
- "headline"
167
- ],
168
- "properties": {
169
- "headline": {
170
- "type": "string",
171
- "maxLength": 200
172
- }
173
- }
174
- }
175
- }
176
- }
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://kloudy.ai/schemas/sdf-webpage-v1.schema.json",
4
+ "title": "Kloudy extractive SDF webpage profile v1",
5
+ "allOf": [
6
+ {
7
+ "$ref": "https://sdfprotocol.org/schemas/sdf-document-0.2.schema.json"
8
+ }
9
+ ],
10
+ "type": "object",
11
+ "required": [
12
+ "sections",
13
+ "extensions",
14
+ "metadata",
15
+ "links",
16
+ "entities"
17
+ ],
18
+ "properties": {
19
+ "parent_type": {
20
+ "const": "article"
21
+ },
22
+ "type": {
23
+ "const": "article.x-kloudy-webpage"
24
+ },
25
+ "entities": {
26
+ "maxItems": 0
27
+ },
28
+ "sections": {
29
+ "type": "array",
30
+ "minItems": 1,
31
+ "maxItems": 40,
32
+ "items": {
33
+ "type": "object",
34
+ "properties": {
35
+ "title": {
36
+ "type": "string",
37
+ "maxLength": 160
38
+ },
39
+ "summary": {
40
+ "type": "string",
41
+ "maxLength": 1600
42
+ }
43
+ },
44
+ "required": [
45
+ "title",
46
+ "summary"
47
+ ]
48
+ }
49
+ },
50
+ "links": {
51
+ "maxItems": 50,
52
+ "items": {
53
+ "type": "object",
54
+ "properties": {
55
+ "href": {
56
+ "type": "string",
57
+ "maxLength": 2048
58
+ },
59
+ "title": {
60
+ "type": "string",
61
+ "maxLength": 160
62
+ },
63
+ "rel": {
64
+ "const": "references"
65
+ }
66
+ },
67
+ "required": [
68
+ "href",
69
+ "title",
70
+ "rel"
71
+ ]
72
+ }
73
+ },
74
+ "extensions": {
75
+ "type": "object",
76
+ "required": [
77
+ "x-kloudy"
78
+ ],
79
+ "properties": {
80
+ "x-kloudy": {
81
+ "type": "object",
82
+ "required": [
83
+ "profile",
84
+ "interaction_mode",
85
+ "capabilities",
86
+ "ownership_verified",
87
+ "published",
88
+ "truncated",
89
+ "resolution"
90
+ ],
91
+ "properties": {
92
+ "profile": {
93
+ "const": "kloudy.sdf.webpage/1"
94
+ },
95
+ "interaction_mode": {
96
+ "const": "informational_only"
97
+ },
98
+ "capabilities": {
99
+ "type": "array",
100
+ "maxItems": 0
101
+ },
102
+ "ownership_verified": {
103
+ "const": false
104
+ },
105
+ "published": {
106
+ "const": false
107
+ },
108
+ "truncated": {
109
+ "type": "boolean"
110
+ },
111
+ "resolution": {
112
+ "enum": [
113
+ "compact",
114
+ "standard"
115
+ ]
116
+ }
117
+ }
118
+ }
119
+ }
120
+ },
121
+ "source": {
122
+ "type": "object",
123
+ "properties": {
124
+ "url": {
125
+ "maxLength": 4096
126
+ },
127
+ "title": {
128
+ "type": "string",
129
+ "maxLength": 200
130
+ }
131
+ },
132
+ "required": [
133
+ "title"
134
+ ]
135
+ },
136
+ "metadata": {
137
+ "type": "object",
138
+ "properties": {
139
+ "title": {
140
+ "type": "string",
141
+ "maxLength": 200
142
+ }
143
+ },
144
+ "required": [
145
+ "title"
146
+ ]
147
+ },
148
+ "summary": {
149
+ "type": "object",
150
+ "properties": {
151
+ "brief": {
152
+ "maxLength": 300
153
+ },
154
+ "key_points": {
155
+ "maxItems": 4,
156
+ "items": {
157
+ "type": "string",
158
+ "maxLength": 200
159
+ }
160
+ }
161
+ }
162
+ },
163
+ "type_data": {
164
+ "type": "object",
165
+ "required": [
166
+ "headline"
167
+ ],
168
+ "properties": {
169
+ "headline": {
170
+ "type": "string",
171
+ "maxLength": 200
172
+ }
173
+ }
174
+ }
175
+ }
176
+ }
package/src/activity.mjs CHANGED
@@ -1,23 +1,23 @@
1
- // Public host adapter: operation state only. Never emits arguments, receipts or secrets.
2
- const METHODS=new Set(['conversation','negotiateHost','action','walletConnection','hosting','wallet','introduce','ask','query','assemble','call','status','confirm','decide']);
3
- export function observeClient(client,onActivity){
4
- if(!client||typeof onActivity!=='function')throw TypeError('Provide a Kloudy client and activity listener.');
5
- let active=0,sequence=0;
6
- const emit=(state,operation)=>{try{onActivity(Object.freeze({schema:'kloudy.activity/1',source:'kloudy',sequence:++sequence,state,operation}));}catch{/* A display listener cannot break or retry an engine action. */}};
7
- return new Proxy(client,{get(target,key){const value=Reflect.get(target,key);if(typeof value!=='function')return value;if(!METHODS.has(key))return value.bind(target);return async(...args)=>{active++;emit('thinking',key);try{const result=await value.apply(target,args);active--;const state=result?.state;emit(active?'thinking':state==='awaiting_approval'?'waiting':['queued','running'].includes(state)?'thinking':state==='failed'?'error':'idle',key);return result;}catch(error){active--;emit(active?'thinking':'error',key);throw error;}};}});
8
- }
9
-
10
- // A compatible IDE can mount this in its own status surface. This does not inject UI
11
- // into arbitrary IDEs or claim that MCP alone owns a host's status bar.
12
- export function mountStatusIndicator(root){
13
- const doc=root.ownerDocument,wrap=doc.createElement('span'),label=doc.createElement('span'),orb=doc.createElement('span');
14
- wrap.className='kloudy-indicator';wrap.setAttribute('role','status');wrap.setAttribute('aria-label','Kloudy ready');label.textContent='Kloudy';label.className='kloudy-indicator-label';orb.className='kloudy-indicator-orb';orb.setAttribute('aria-hidden','true');wrap.append(label,orb);root.append(wrap);wrap.dataset.state='idle';
15
- return {update(event){if(event?.schema!=='kloudy.activity/1'||event.source!=='kloudy'||!['idle','thinking','waiting','error','listening','speaking'].includes(event.state))return;wrap.dataset.state=event.state;wrap.setAttribute('aria-label','Kloudy '+({idle:'ready',thinking:'working',waiting:'needs approval',error:'could not finish',listening:'listening through your device',speaking:'speaking through your device'}[event.state]));},dispose(){wrap.remove();}};
16
- }
17
-
18
- // Covers every actual MCP tool call, including public reads and model discovery.
19
- export function instrumentToolHandler(handler,onActivity=()=>{}){
20
- let active=0,sequence=0;
21
- const emit=state=>{try{onActivity({schema:'kloudy.activity/1',source:'kloudy',sequence:++sequence,state,operation:'tool_call'});}catch{}};
22
- return async request=>{active++;emit('thinking');let state='error';try{const result=await handler(request);state=result?.isError?'error':result?._meta?.kloudyApproval?'waiting':['queued','running'].includes(result?.structuredContent?.state)?'thinking':'idle';return result;}finally{active--;emit(active?'thinking':state);}};
23
- }
1
+ // Public host adapter: operation state only. Never emits arguments, receipts or secrets.
2
+ const METHODS=new Set(['conversation','negotiateHost','action','walletConnection','hosting','wallet','introduce','ask','query','assemble','call','status','confirm','decide']);
3
+ export function observeClient(client,onActivity){
4
+ if(!client||typeof onActivity!=='function')throw TypeError('Provide a Kloudy client and activity listener.');
5
+ let active=0,sequence=0;
6
+ const emit=(state,operation)=>{try{onActivity(Object.freeze({schema:'kloudy.activity/1',source:'kloudy',sequence:++sequence,state,operation}));}catch{/* A display listener cannot break or retry an engine action. */}};
7
+ return new Proxy(client,{get(target,key){const value=Reflect.get(target,key);if(typeof value!=='function')return value;if(!METHODS.has(key))return value.bind(target);return async(...args)=>{active++;emit('thinking',key);try{const result=await value.apply(target,args);active--;const state=result?.state;emit(active?'thinking':state==='awaiting_approval'?'waiting':['queued','running'].includes(state)?'thinking':state==='failed'?'error':'idle',key);return result;}catch(error){active--;emit(active?'thinking':'error',key);throw error;}};}});
8
+ }
9
+
10
+ // A compatible IDE can mount this in its own status surface. This does not inject UI
11
+ // into arbitrary IDEs or claim that MCP alone owns a host's status bar.
12
+ export function mountStatusIndicator(root){
13
+ const doc=root.ownerDocument,wrap=doc.createElement('span'),label=doc.createElement('span'),orb=doc.createElement('span');
14
+ wrap.className='kloudy-indicator';wrap.setAttribute('role','status');wrap.setAttribute('aria-label','Kloudy ready');label.textContent='Kloudy';label.className='kloudy-indicator-label';orb.className='kloudy-indicator-orb';orb.setAttribute('aria-hidden','true');wrap.append(label,orb);root.append(wrap);wrap.dataset.state='idle';
15
+ return {update(event){if(event?.schema!=='kloudy.activity/1'||event.source!=='kloudy'||!['idle','thinking','waiting','error','listening','speaking'].includes(event.state))return;wrap.dataset.state=event.state;wrap.setAttribute('aria-label','Kloudy '+({idle:'ready',thinking:'working',waiting:'needs approval',error:'could not finish',listening:'listening through your device',speaking:'speaking through your device'}[event.state]));},dispose(){wrap.remove();}};
16
+ }
17
+
18
+ // Covers every actual MCP tool call, including public reads and model discovery.
19
+ export function instrumentToolHandler(handler,onActivity=()=>{}){
20
+ let active=0,sequence=0;
21
+ const emit=state=>{try{onActivity({schema:'kloudy.activity/1',source:'kloudy',sequence:++sequence,state,operation:'tool_call'});}catch{}};
22
+ return async request=>{active++;emit('thinking');let state='error';try{const result=await handler(request);state=result?.isError?'error':result?._meta?.kloudyApproval?'waiting':['queued','running'].includes(result?.structuredContent?.state)?'thinking':'idle';return result;}finally{active--;emit(active?'thinking':state);}};
23
+ }
package/src/approval.mjs CHANGED
@@ -1,25 +1,25 @@
1
- import {randomUUID} from 'node:crypto';
2
- export const approvalResource='ui://kloudy/approval.html';
3
- export const approvalTool={name:'kloudy_approval',description:'Submit the exact pending decision from the Kloudy approval view.',inputSchema:{type:'object',properties:{ticket:{type:'string'},decision:{type:'string',enum:['approve','cancel']}},required:['ticket','decision'],additionalProperties:false},_meta:{ui:{resourceUri:approvalResource,visibility:['app']}}};
4
- export class Approvals {
5
- constructor({now=()=>Date.now()}={}){this.now=now;this.pending=new Map();}
6
- present(result,selectionId,connection){
7
- if(result?.state!=='awaiting_approval')return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};
8
- const a=result.approval,expires=Number(a?.expires_at)*1000,risk=a?.risk_tier??a?.risk;
9
- // Do not fabricate a token or let incomplete backend state create an approval.
10
- if(!a||typeof a.token!=='string'||!a.token||a.token.length>512||typeof risk!=='string'||!risk.trim()||risk.length>64||!(typeof a.summary==='string'||a.summary&&typeof a.summary==='object')||!selectionId||!Number.isFinite(expires)||expires<=this.now())return {isError:true,content:[{type:'text',text:'The exact approval is missing or expired. Refresh the engine request.'}]};
11
- for(const [id,p] of this.pending)if(p.expires<=this.now())this.pending.delete(id);
12
- if(this.pending.size>=32)throw Error('Too many pending approvals');
13
- const ticket=randomUUID();this.pending.set(ticket,{selectionId,token:a.token,expires,connection});
14
- const summary={state:result.state,summary:a.summary,risk_tier:a.risk_tier??a.risk,expires_at:a.expires_at,notice:'Approve or cancel using the inline Kloudy view. Hosts without MCP Apps must provide their own consent UI; no approval was submitted.'};
15
- return {content:[{type:'text',text:JSON.stringify(summary)}],structuredContent:summary,_meta:{kloudyApproval:{...summary,ticket},ui:{resourceUri:approvalResource}}};
16
- }
17
- async decide(input,useClient){
18
- if(!input||Object.keys(input).some(k=>!['ticket','decision'].includes(k))||!['approve','cancel'].includes(input.decision))throw Error('Invalid approval decision');
19
- const pending=this.pending.get(input.ticket);if(!pending||pending.expires<=this.now()){this.pending.delete(input.ticket);throw Error('Approval missing or expired');}
20
- // Consume before dispatch, including an ambiguous network failure. Never retry
21
- // a mutation silently; the host must inspect the engine receipt afterwards.
22
- this.pending.delete(input.ticket);
23
- return (pending.connection||useClient)(client=>input.decision==='approve'?client.confirm({selectionId:pending.selectionId,token:pending.token,decision:'approve',source:'tap'}):client.rpc('kloudy/cancel',{selection_id:pending.selectionId}));
24
- }
25
- }
1
+ import {randomUUID} from 'node:crypto';
2
+ export const approvalResource='ui://kloudy/approval.html';
3
+ export const approvalTool={name:'kloudy_approval',description:'Submit the exact pending decision from the Kloudy approval view.',inputSchema:{type:'object',properties:{ticket:{type:'string'},decision:{type:'string',enum:['approve','cancel']}},required:['ticket','decision'],additionalProperties:false},_meta:{ui:{resourceUri:approvalResource,visibility:['app']}}};
4
+ export class Approvals {
5
+ constructor({now=()=>Date.now()}={}){this.now=now;this.pending=new Map();}
6
+ present(result,selectionId,connection){
7
+ if(result?.state!=='awaiting_approval')return {content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result};
8
+ const a=result.approval,expires=Number(a?.expires_at)*1000,risk=a?.risk_tier??a?.risk;
9
+ // Do not fabricate a token or let incomplete backend state create an approval.
10
+ if(!a||typeof a.token!=='string'||!a.token||a.token.length>512||typeof risk!=='string'||!risk.trim()||risk.length>64||!(typeof a.summary==='string'||a.summary&&typeof a.summary==='object')||!selectionId||!Number.isFinite(expires)||expires<=this.now())return {isError:true,content:[{type:'text',text:'The exact approval is missing or expired. Refresh the engine request.'}]};
11
+ for(const [id,p] of this.pending)if(p.expires<=this.now())this.pending.delete(id);
12
+ if(this.pending.size>=32)throw Error('Too many pending approvals');
13
+ const ticket=randomUUID();this.pending.set(ticket,{selectionId,token:a.token,expires,connection});
14
+ const summary={state:result.state,summary:a.summary,risk_tier:a.risk_tier??a.risk,expires_at:a.expires_at,notice:'Approve or cancel using the inline Kloudy view. Hosts without MCP Apps must provide their own consent UI; no approval was submitted.'};
15
+ return {content:[{type:'text',text:JSON.stringify(summary)}],structuredContent:summary,_meta:{kloudyApproval:{...summary,ticket},ui:{resourceUri:approvalResource}}};
16
+ }
17
+ async decide(input,useClient){
18
+ if(!input||Object.keys(input).some(k=>!['ticket','decision'].includes(k))||!['approve','cancel'].includes(input.decision))throw Error('Invalid approval decision');
19
+ const pending=this.pending.get(input.ticket);if(!pending||pending.expires<=this.now()){this.pending.delete(input.ticket);throw Error('Approval missing or expired');}
20
+ // Consume before dispatch, including an ambiguous network failure. Never retry
21
+ // a mutation silently; the host must inspect the engine receipt afterwards.
22
+ this.pending.delete(input.ticket);
23
+ return (pending.connection||useClient)(client=>input.decision==='approve'?client.confirm({selectionId:pending.selectionId,token:pending.token,decision:'approve',source:'tap'}):client.rpc('kloudy/cancel',{selection_id:pending.selectionId}));
24
+ }
25
+ }
@@ -0,0 +1,2 @@
1
+ // Invalid and unset rollout modes fail closed. Configuration never enables itself.
2
+ export const autoupdateEnabled=mode=>mode==='meta'||mode==='notify';