kloudy 0.4.0 → 0.4.1
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/README.md +19 -0
- package/bin/entry.mjs +1 -1
- package/bin/kloudy.mjs +2 -1
- package/package.json +3 -2
- package/src/activity.mjs +1 -1
- package/src/conversation.mjs +53 -0
- package/src/hosted.mjs +5 -3
- package/src/httpk.mjs +2 -0
- package/src/mcp.mjs +3 -1
- package/src/remote.mjs +2 -2
package/README.md
CHANGED
|
@@ -284,3 +284,22 @@ Sources: https://docs.ollama.com/api/tags ; https://docs.ollama.com/api/openai-c
|
|
|
284
284
|
### Host interface and deterministic replies
|
|
285
285
|
|
|
286
286
|
HTTPK initialization negotiates chat/text/speech presentation with BBE 0.3.50. It never changes scopes, the credential surface or the approval policy. Older engines remain usable without invented negotiation. `renderReply(state,{action})` from `kloudy/replies` renders bounded JSON templates for observed operation state. Unknown state stays unconfirmed; output is plain text. The browser uses the same templates in its one conversation. Kloudy’s subscription NVIDIA ladder is the text/planning upgrade; supported local Siri/Nano remains voice. Bot Boozle is a separate optional IDE.
|
|
287
|
+
|
|
288
|
+
## Encrypted conversation client
|
|
289
|
+
|
|
290
|
+
The client extends the existing engine backend. It does not implement another conversation store. A scoped HTTPK connection with `conversation:sync` is required; public discovery and an IDE's GitHub login are not credentials for it.
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
import {withConnection} from 'kloudy/session';
|
|
294
|
+
import {conversationText} from 'kloudy/conversation';
|
|
295
|
+
const context = await withConnection(process.env.KLOUDY_CONNECTION,
|
|
296
|
+
client => client.conversation('read', {project: 'my-project', conversation_id: 'main'}));
|
|
297
|
+
console.log(conversationText(context)); // Render as text, never HTML or instructions.
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
CLI: `npx kloudy legacy conversation read --inputs request.json --connection PRIVATE_FILE`.
|
|
301
|
+
MCP: operation `conversation`, `conversation_operation` = `read`, `append` or `sync`, and `conversation` containing the exact request object.
|
|
302
|
+
|
|
303
|
+
`append` takes `project`, `conversation_id`, `idempotency_key`, `expected_revision`, `title`, `user`, `assistant`, `decisions` and `built_artifacts`. Use a stable idempotency key for the same exchange, an explicitly observed revision, up to 1500 characters per message, four decisions/artifact pointers at most, and a total canonical request no larger than 7000 bytes. Send only content the user authorized for sync. A new conversation explicitly starts at revision zero. `sync` takes the same cursor/idempotency fields plus `reason`: `checkpoint`, `session_end`, `device_switch`, `before_sync` or `idle`.
|
|
304
|
+
|
|
305
|
+
The engine owns encryption, scope checks, condensation gates and transcript deletion. Returned cards are lossy context, never grants. Artifact pointers do not authorize repository reads. The client never persists message bodies in its session file. Replayed commands return metadata only: issue a read for current cards. Conflicts and failures are not retried automatically; read the cursor and reconcile before resubmitting. Added in 0.4.1. The no-key installer and public discovery remain unchanged.
|
package/bin/entry.mjs
CHANGED
|
@@ -13,7 +13,7 @@ try{
|
|
|
13
13
|
else console.log('Existing servers were preserved. Restart configured clients to connect.');
|
|
14
14
|
if(results.some(r=>r.state==='unchanged_error'))process.exitCode=1;
|
|
15
15
|
}else if(args[0]==='mcp'&&args.length===1){await serveRemote();}
|
|
16
|
-
else if(args[0]==='--version'){console.log('0.4.
|
|
16
|
+
else if(args[0]==='--version'){console.log('0.4.1');}
|
|
17
17
|
else if(['--help','help'].includes(args[0]))console.log('npx kloudy Add to detected editors (asks before each write)\nnpx kloudy <question> Ask the public Kloudy MCP endpoint\nnpx kloudy mcp Remote MCP over local stdio\nnpx kloudy legacy <args> Existing SDK/engine CLI commands\n\nFree discovery needs no key. For authorized account actions set KLOUDY_MCP_TOKEN privately; never put a key in command arguments. Node 20.19+ required.');
|
|
18
18
|
else if(args[0]==='legacy'){process.argv.splice(2,1);await import('./kloudy.mjs');}
|
|
19
19
|
else{if(args[0]?.startsWith('--'))throw Error('Unknown option');const result=await askRemote(args.join(' '));for(const c of result.content||[])if(c.type==='text')process.stdout.write((process.env.KLOUDY_MCP_TOKEN?c.text.replaceAll(process.env.KLOUDY_MCP_TOKEN,'[redacted]'):c.text)+'\n');if(result.isError)process.exitCode=1;}
|
package/bin/kloudy.mjs
CHANGED
|
@@ -19,7 +19,7 @@ try{
|
|
|
19
19
|
const connection=option('connection')||process.env.KLOUDY_CONNECTION;
|
|
20
20
|
const project=option('project')||process.cwd(),decision=option('decision');
|
|
21
21
|
if(command==='help'||command==='--help'){
|
|
22
|
-
console.log('Ask for Kloudy. One CLI, one MCP, tools appear when needed.\n\nkloudy models list [--endpoint URL] [--key-env NAME]\nkloudy models run NAME --prompt-file FILE [--max-tokens 128] [--allow-provider-charge]\nkloudy init my-app\nkloudy read https://example.com\nkloudy card /world\nkloudy convert https://example.com\nkloudy query "need" --connection FILE\nkloudy ask "goal" --connection FILE [--candidates FILE]\nkloudy assemble --selections FILE --connection FILE\nkloudy call NAME --inputs FILE [--selection ID] --connection FILE\nkloudy action chain.head --inputs FILE --connection FILE [--idempotency-key KEY]\nkloudy wallet-connect ethereum --connection FILE\nkloudy hosting [--budget-micros 100000000] --connection FILE\nkloudy wallet balance --connection FILE\nkloudy wallet top_up --amount 10.00 --connection FILE\nkloudy host --connection FILE\nkloudy status --connection FILE\nkloudy cancel --connection FILE\nkloudy mcp --connection FILE\nkloudy introduce [--project DIR] [--decision yes|no]\nkloudy install [--ide cursor|claude|vscode|codex] [--url HTTPS_URL] [--token-env NAME] [--connection FILE]\nkloudy wrap --config FILE [--port 8796]\nkloudy strings save NAME --selections FILE\nkloudy strings use NAME\nkloudy strings call NAME --tool ALIAS --inputs FILE\nkloudy strings list|prune\nkloudy strings keep|remove NAME\nkloudy login\n\nRegister with email + short 2FA through the Core account service. Existing engine connections remain usable. Hosted registration is not supplied by this client package.');
|
|
22
|
+
console.log('Ask for Kloudy. One CLI, one MCP, tools appear when needed.\n\nkloudy models list [--endpoint URL] [--key-env NAME]\nkloudy models run NAME --prompt-file FILE [--max-tokens 128] [--allow-provider-charge]\nkloudy init my-app\nkloudy read https://example.com\nkloudy card /world\nkloudy convert https://example.com\nkloudy query "need" --connection FILE\nkloudy ask "goal" --connection FILE [--candidates FILE]\nkloudy assemble --selections FILE --connection FILE\nkloudy call NAME --inputs FILE [--selection ID] --connection FILE\nkloudy action chain.head --inputs FILE --connection FILE [--idempotency-key KEY]\nkloudy wallet-connect ethereum --connection FILE\nkloudy hosting [--budget-micros 100000000] --connection FILE\nkloudy wallet balance --connection FILE\nkloudy wallet top_up --amount 10.00 --connection FILE\nkloudy conversation read|append|sync --inputs FILE --connection FILE\nkloudy host --connection FILE\nkloudy status --connection FILE\nkloudy cancel --connection FILE\nkloudy mcp --connection FILE\nkloudy introduce [--project DIR] [--decision yes|no]\nkloudy install [--ide cursor|claude|vscode|codex] [--url HTTPS_URL] [--token-env NAME] [--connection FILE]\nkloudy wrap --config FILE [--port 8796]\nkloudy strings save NAME --selections FILE\nkloudy strings use NAME\nkloudy strings call NAME --tool ALIAS --inputs FILE\nkloudy strings list|prune\nkloudy strings keep|remove NAME\nkloudy login\n\nRegister with email + short 2FA through the Core account service. Existing engine connections remain usable. Hosted registration is not supplied by this client package.');
|
|
23
23
|
}else if(command==='models'){
|
|
24
24
|
const action=args.shift(),endpoint=option('endpoint'),keyEnv=option('key-env'),file=option('prompt-file'),max=option('max-tokens');
|
|
25
25
|
const allow=args.includes('--allow-provider-charge');if(allow)args.splice(args.indexOf('--allow-provider-charge'),1);
|
|
@@ -53,6 +53,7 @@ try{
|
|
|
53
53
|
const candidatesFile=option('candidates'),inputsFile=option('inputs'),selectionsFile=option('selections'),selectionId=option('selection'),lane=option('lane')||'non_medical';
|
|
54
54
|
if(args.some(x=>x.startsWith('--')))throw new Error('Unknown option');
|
|
55
55
|
const result=await withConnection(connection,async client=>{
|
|
56
|
+
if(command==='conversation'){if(!client.conversation||args.length!==1||!inputsFile)throw Error('Use conversation read|append|sync --inputs FILE with a scoped connection');return client.conversation(args[0],JSON.parse(await readFile(inputsFile,'utf8')));}
|
|
56
57
|
if(command==='action'){if(!client.action||args.length!==1||!inputsFile)throw Error('Action name and inputs file required');return client.action(args[0],JSON.parse(await readFile(inputsFile,'utf8')),{idempotencyKey:idempotencyKey??undefined});}
|
|
57
58
|
if(command==='wallet-connect'){if(!client.walletConnection||args.length!==1)throw Error('Chain required');return client.walletConnection(args[0]);}
|
|
58
59
|
if(command==='hosting'){if(!client.hosting||args.length||budgetMicros!==null&&!/^[1-9]\d*$/.test(budgetMicros))throw Error('Use an integer USD-micros budget');return client.hosting({budgetUsdMicros:budgetMicros===null?undefined:Number(budgetMicros)});}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kloudy",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Ask for Kloudy: CLI, MCP and SDK clients for the same scoped engine session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"./status-indicator.css": "./src/status-indicator.css",
|
|
26
26
|
"./models": "./src/models.mjs",
|
|
27
27
|
"./host-interface": "./src/host-interface.mjs",
|
|
28
|
-
"./replies": "./src/replies.mjs"
|
|
28
|
+
"./replies": "./src/replies.mjs",
|
|
29
|
+
"./conversation": "./src/conversation.mjs"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"bin",
|
package/src/activity.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Public host adapter: operation state only. Never emits arguments, receipts or secrets.
|
|
2
|
-
const METHODS=new Set(['negotiateHost','action','walletConnection','hosting','wallet','introduce','ask','query','assemble','call','status','confirm','decide']);
|
|
2
|
+
const METHODS=new Set(['conversation','negotiateHost','action','walletConnection','hosting','wallet','introduce','ask','query','assemble','call','status','confirm','decide']);
|
|
3
3
|
export function observeClient(client,onActivity){
|
|
4
4
|
if(!client||typeof onActivity!=='function')throw TypeError('Provide a Kloudy client and activity listener.');
|
|
5
5
|
let active=0,sequence=0;
|
|
@@ -0,0 +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
|
+
}
|
package/src/hosted.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import {conversationParams,conversationResult} from './conversation.mjs';
|
|
1
2
|
import {actionInput,hostingParams,connectionParams,stableIntentKey} from './product-actions.mjs';
|
|
2
3
|
import {welcomeContent} from './welcome.mjs';
|
|
3
4
|
// Stateless MCP adapter for the engine's existing HTTPK methods.
|
|
4
5
|
// No grants, model decisions, retries, provider credentials or execution live here.
|
|
5
6
|
import {randomUUID} from 'node:crypto';
|
|
6
|
-
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:['action','wallet_connection','hosting','wallet','introduce','ask','query','assemble','call','confirm','status','cancel']},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}};
|
|
7
|
+
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}};
|
|
7
8
|
const toolResult=result=>({content:[{type:'text',text:JSON.stringify(result)}],structuredContent:result});
|
|
8
9
|
export function adaptMcp(message){
|
|
9
10
|
let upstream=message,project=x=>x;
|
|
@@ -12,7 +13,8 @@ export function adaptMcp(message){
|
|
|
12
13
|
if(message.method==='tools/call'&&message.params?.name==='kloudy'){
|
|
13
14
|
const a=message.params.arguments||{},operation=a.operation||'introduce';
|
|
14
15
|
let method,params;
|
|
15
|
-
if(operation==='
|
|
16
|
+
if(operation==='conversation'){method='kloudy/conversation/'+a.conversation_operation;params=conversationParams(a.conversation_operation,a.conversation);}
|
|
17
|
+
else if(operation==='action'){method='kloudy/intent';params={idempotency_key:stableIntentKey(a.idempotency_key||randomUUID()),input:actionInput(a.action,a.inputs)};}
|
|
16
18
|
else if(operation==='wallet_connection'){method='kloudy/wallet/connection';params=connectionParams(a.chain);}
|
|
17
19
|
else if(operation==='hosting'){method='kloudy/hosting';params=hostingParams(a.budget_usd_micros);}
|
|
18
20
|
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}:{})};}
|
|
@@ -22,7 +24,7 @@ export function adaptMcp(message){
|
|
|
22
24
|
else if(operation==='confirm'){method='kloudy/confirm';params=confirmationParams(a);}
|
|
23
25
|
else if(operation==='status'||operation==='cancel'){method=operation==='status'?'kloudy/receipt':'kloudy/cancel';params={selection_id:a.selection_id};}
|
|
24
26
|
else throw Error('unsupported_operation');
|
|
25
|
-
upstream={...message,method,params};project=operation==='call'?x=>x:operation==='introduce'?x=>({...toolResult(x),content:welcomeContent(x)}):toolResult;
|
|
27
|
+
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;
|
|
26
28
|
}
|
|
27
29
|
return {upstream,project};
|
|
28
30
|
}
|
package/src/httpk.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {conversationParams,conversationResult} from './conversation.mjs';
|
|
1
2
|
import {hostCapabilities,readHostInterface} from './host-interface.mjs';
|
|
2
3
|
import {confirmationParams} from './hosted.mjs';
|
|
3
4
|
import {queryInput,queryResult} from './query.mjs';
|
|
@@ -30,6 +31,7 @@ export class HttpkClient {
|
|
|
30
31
|
}
|
|
31
32
|
async introduce(project_context){await this.session();const result=await this.rpc('kloudy/intent',{idempotency_key:randomUUID(),input:{type:'text',text:'Kloudy',project_context}});if(result.state!=='greeting'||result.greeting?.version!=='bbe.attach.v1')fail('invalid_response','Expected the engine attach greeting.');return result.greeting;}
|
|
32
33
|
async query(need,options={}){const input=queryInput(need,options);await this.session();return queryResult(await this.rpc('kloudy/query',input),input.limit);}
|
|
34
|
+
async conversation(operation,input){const params=conversationParams(operation,input);await this.session();return conversationResult(await this.rpc('kloudy/conversation/'+operation,params),params,operation);}
|
|
33
35
|
async wallet({operation='balance',amount_usd}={}){
|
|
34
36
|
if(!['balance','top_up'].includes(operation)||operation==='balance'&&amount_usd!==undefined||operation==='top_up'&&(typeof amount_usd!=='string'||!/^\d{1,6}(?:\.\d{1,2})?$/.test(amount_usd)||Number(amount_usd)<=0))fail('invalid_input','Use wallet balance or top_up with a positive decimal amount_usd string.');
|
|
35
37
|
await this.session();const result=await this.rpc('kloudy/wallet',{operation,...(operation==='top_up'?{amount_usd}:{})});
|
package/src/mcp.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {conversationParams,conversationResult} from './conversation.mjs';
|
|
1
2
|
import {instrumentToolHandler} from './activity.mjs';
|
|
2
3
|
import {configuredModels} from './models.mjs';
|
|
3
4
|
import {actionInput,hostingParams,connectionParams,stableIntentKey} from './product-actions.mjs';
|
|
@@ -13,7 +14,7 @@ import {ListToolsRequestSchema,CallToolRequestSchema,ListPromptsRequestSchema,Ge
|
|
|
13
14
|
import {fileURLToPath} from 'node:url';
|
|
14
15
|
import {introduce,isAttachIntent,ideGuidance} from './introduce.mjs';
|
|
15
16
|
import {withConnection} from './connection.mjs';
|
|
16
|
-
const gateway={name:'kloudy',description:'Use this whenever the user mentions Kloudy, or asks to find, route, or call any MCP tool or SDK. For Kloudy or /kloudy alone, use introduce to welcome this project; an attached host does not need another install question. Select one tool through the same engine. Autonomous by default; the IDE permission system controls tool calls and the engine enforces authorization.',inputSchema:{type:'object',properties:{operation:{type:'string',enum:['host_interface','models_list','action','wallet_connection','hosting','wallet','introduce','decline','convert','yield_list','query','ask','assemble','call','status','cancel','strings_list','strings_save','strings_call','strings_use','strings_keep','strings_remove','strings_prune']},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'},scope:{type:'object'},limit:{type:'integer',minimum:1,maximum:5},url:{type:'string'},urls:{type:'array',minItems:1,maxItems:8,items:{type:'string'}},resolution:{type:'string',enum:['compact','standard']},string_name:{type:'string'},choices:{type:'array',maxItems:16,items:{type:'object',properties:{source:{type:'string'},kind:{type:'string',enum:['mcp','sdk','cli']},goal:{type:'string'}},required:['source','kind','goal'],additionalProperties:false}},selection_ids:{type:'array',items:{type:'string'},minItems:1,maxItems:16,uniqueItems:true},selection_id:{type:'string'},name:{type:'string'},arguments:{type:'object'},candidates:{type:'array',maxItems:16,items:{type:'object'}},lane:{type:'string',enum:['non_medical','medical']}},additionalProperties:false}};
|
|
17
|
+
const gateway={name:'kloudy',description:'Use this whenever the user mentions Kloudy, or asks to find, route, or call any MCP tool or SDK. For Kloudy or /kloudy alone, use introduce to welcome this project; an attached host does not need another install question. Select one tool through the same engine. Autonomous by default; the IDE permission system controls tool calls and the engine enforces authorization.',inputSchema:{type:'object',properties:{operation:{type:'string',enum:['conversation','host_interface','models_list','action','wallet_connection','hosting','wallet','introduce','decline','convert','yield_list','query','ask','assemble','call','status','cancel','strings_list','strings_save','strings_call','strings_use','strings_keep','strings_remove','strings_prune']},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'},scope:{type:'object'},limit:{type:'integer',minimum:1,maximum:5},url:{type:'string'},urls:{type:'array',minItems:1,maxItems:8,items:{type:'string'}},resolution:{type:'string',enum:['compact','standard']},string_name:{type:'string'},choices:{type:'array',maxItems:16,items:{type:'object',properties:{source:{type:'string'},kind:{type:'string',enum:['mcp','sdk','cli']},goal:{type:'string'}},required:['source','kind','goal'],additionalProperties:false}},selection_ids:{type:'array',items:{type:'string'},minItems:1,maxItems:16,uniqueItems:true},selection_id:{type:'string'},name:{type:'string'},arguments:{type:'object'},candidates:{type:'array',maxItems:16,items:{type:'object'}},lane:{type:'string',enum:['non_medical','medical']}},additionalProperties:false}};
|
|
17
18
|
export function createMcpServer({useClient,greet=introduce,strings=localStrings,models=configuredModels,onActivity}){
|
|
18
19
|
const server=new Server({name:'kloudy',version:'0.3.15'},{capabilities:{tools:{listChanged:true},prompts:{},resources:{}},instructions:'When the user says Kloudy or /kloudy, use operation introduce. Return the current project greeting. Do not repeatedly ask to install an already attached tool. Respect a no answer and the host permission ceiling. '+Object.values(ideGuidance).join(' ')});
|
|
19
20
|
const approvals=new Approvals(),hasUI=()=>getUiCapability(server.getClientCapabilities())?.mimeTypes?.includes(RESOURCE_MIME_TYPE);
|
|
@@ -48,6 +49,7 @@ export function createMcpServer({useClient,greet=introduce,strings=localStrings,
|
|
|
48
49
|
const input=request.params.arguments||{};
|
|
49
50
|
if(request.params.name.startsWith('selected_'))return client.call(request.params.name.slice(9),input);
|
|
50
51
|
if(request.params.name!=='kloudy')throw new Error('Unknown tool');
|
|
52
|
+
if(input.operation==='conversation'){if(!client.conversation)throw Error('Conversation sync requires a scoped HTTPK connection');return client.conversation(input.conversation_operation,input.conversation);}
|
|
51
53
|
if(input.operation==='host_interface')return client.negotiateHost();
|
|
52
54
|
if(input.operation==='action')return client.action(input.action,input.inputs,{idempotencyKey:input.idempotency_key});
|
|
53
55
|
if(input.operation==='wallet_connection')return client.walletConnection(input.chain);
|
package/src/remote.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {ListToolsRequestSchema,CallToolRequestSchema} from '@modelcontextprotoco
|
|
|
6
6
|
import {REMOTE} from './editor-install.mjs';
|
|
7
7
|
export async function connectRemote({key=process.env.KLOUDY_MCP_TOKEN,fetcher=fetch}={}){
|
|
8
8
|
if(key!==undefined&&!/^\S{20,512}$/.test(key))throw Error('Invalid key');
|
|
9
|
-
const client=new Client({name:'kloudy',version:'0.4.
|
|
9
|
+
const client=new Client({name:'kloudy',version:'0.4.1'},{capabilities:{}});
|
|
10
10
|
const transport=new StreamableHTTPClientTransport(new URL(REMOTE),{requestInit:{headers:key?{Authorization:'Bearer '+key}:{},redirect:'error'},fetch:(url,options)=>fetcher(url,{...options,redirect:'error'}),reconnectionOptions:{maxRetries:0}});
|
|
11
11
|
try{await client.connect(transport,{timeout:25000});return client;}catch(e){await client.close().catch(()=>{});throw e;}
|
|
12
12
|
}
|
|
@@ -17,7 +17,7 @@ export async function askRemote(question,options){
|
|
|
17
17
|
}
|
|
18
18
|
export async function serveRemote(options){
|
|
19
19
|
const remote=await connectRemote(options);
|
|
20
|
-
const server=new Server({name:'kloudy',version:'0.4.
|
|
20
|
+
const server=new Server({name:'kloudy',version:'0.4.1'},{capabilities:{tools:{}},instructions:'Use Kloudy when mentioned, or to discover MCP tools and SDKs. Public discovery is free; account actions need an existing authorized key.'});
|
|
21
21
|
server.setRequestHandler(ListToolsRequestSchema,req=>remote.listTools(req.params));
|
|
22
22
|
server.setRequestHandler(CallToolRequestSchema,req=>remote.callTool(req.params,undefined,{timeout:25000}));
|
|
23
23
|
server.onclose=()=>{void remote.close();};
|