kloudy 0.4.8 → 1.0.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.
Files changed (53) hide show
  1. package/AGENTS.md +54 -0
  2. package/LICENSE +202 -202
  3. package/MCP-DEFAULT.md +12 -12
  4. package/README.md +375 -315
  5. package/bin/entry.mjs +25 -22
  6. package/bin/kloudy.mjs +80 -71
  7. package/package.json +4 -2
  8. package/schemas/SDF-LICENSE.txt +21 -21
  9. package/schemas/kloudy.deployable-bot.schema.json +80 -0
  10. package/schemas/sdf-document-0.2.schema.json +411 -411
  11. package/schemas/sdf-webpage-v1.schema.json +176 -176
  12. package/src/activity.mjs +23 -23
  13. package/src/approval-ui.mjs +2 -2
  14. package/src/approval.mjs +25 -25
  15. package/src/autoupdate.mjs +2 -2
  16. package/src/bots.mjs +144 -0
  17. package/src/chooser-link.mjs +17 -17
  18. package/src/client.mjs +96 -96
  19. package/src/connect-card.mjs +32 -32
  20. package/src/connect-ui.mjs +3 -3
  21. package/src/connection.mjs +25 -25
  22. package/src/conversation.mjs +53 -53
  23. package/src/deployable-bots.json.gz +0 -0
  24. package/src/editor-install.mjs +79 -79
  25. package/src/host-interface.mjs +10 -10
  26. package/src/hosted.mjs +42 -42
  27. package/src/httpk.mjs +66 -66
  28. package/src/install.mjs +35 -35
  29. package/src/introduce.mjs +33 -33
  30. package/src/mcp.mjs +74 -74
  31. package/src/modalities.mjs +28 -0
  32. package/src/models.mjs +55 -55
  33. package/src/preferences-card.mjs +21 -21
  34. package/src/preferences-ui.mjs +2 -2
  35. package/src/product-actions.mjs +22 -22
  36. package/src/public.mjs +41 -41
  37. package/src/query.mjs +28 -28
  38. package/src/recommend.mjs +478 -0
  39. package/src/replies.mjs +7 -7
  40. package/src/reply-templates.json +12 -12
  41. package/src/sample-actions.mjs +31 -31
  42. package/src/sample-card.mjs +11 -11
  43. package/src/sample-cards.mjs +47 -47
  44. package/src/sample-ui.mjs +2 -2
  45. package/src/sdf-budget.mjs +23 -23
  46. package/src/sdf.mjs +12 -12
  47. package/src/starter.mjs +43 -43
  48. package/src/status-indicator.css +5 -5
  49. package/src/strings.mjs +51 -51
  50. package/src/terminal-status.mjs +6 -6
  51. package/src/website-return.mjs +18 -18
  52. package/src/welcome.mjs +8 -8
  53. package/src/wrap.mjs +58 -58
@@ -0,0 +1,478 @@
1
+ // Offline stack recommender. No network and no model call.
2
+ // `kloudy recommend <goal> [--json]` and `kloudy stack <goal> [--json]`.
3
+ import {readFileSync} from 'node:fs';
4
+ import {flags,loadLocal,localEndpoints} from './bots.mjs';
5
+ import {primarySortHint} from './modalities.mjs';
6
+
7
+ export const RECOMMEND_SCHEMA='kloudy.recommend/1';
8
+ export const CATALOG_NAME='kloudy 1.0';
9
+ const PER_MODALITY=5;
10
+ // Class order for recommendations is A, then B, then O.
11
+ // Primary-modality hint, keyword hits, catalog score, and index tie-break
12
+ // follow the comparator in localSearch.
13
+ const CLASS_TIER={A:0,B:1,O:2};
14
+ const words=s=>String(s||'').toLowerCase().split(/[^\p{L}\p{N}_]+/u).filter(w=>w.length>1);
15
+
16
+ const RULE_LIST=[
17
+ ['text to speech',['voice_generation']],
18
+ ['text-to-speech',['voice_generation']],
19
+ ['speech synthesis',['voice_generation']],
20
+ ['voice generation',['voice_generation']],
21
+ ['voice over',['voice_generation']],
22
+ ['voiceover',['voice_generation']],
23
+ ['text to image',['image_generation']],
24
+ ['text-to-image',['image_generation']],
25
+ ['image generation',['image_generation']],
26
+ ['generate images',['image_generation']],
27
+ ['generate image',['image_generation']],
28
+ ['stable diffusion',['image_generation']],
29
+ ['txt2img',['image_generation']],
30
+ ['code execution',['code_execution']],
31
+ ['execute code',['code_execution']],
32
+ ['run code',['code_execution']],
33
+ ['browser automation',['browser_automation']],
34
+ ['web scraping',['browser_automation']],
35
+ ['file system',['file_system']],
36
+ ['command line',['cli']],
37
+ ['command-line',['cli']],
38
+ ['vector database',['embedding']],
39
+ ['vector store',['embedding']],
40
+ ['rest api',['api_calls']],
41
+ ['api calls',['api_calls']],
42
+ ['api call',['api_calls']],
43
+ ['function calls',['agent_tools']],
44
+ ['function call',['agent_tools']],
45
+ ['function calling',['agent_tools']],
46
+ ['tool calling',['agent_tools']],
47
+ ['agent tools',['agent_tools']],
48
+ ['agent tool',['agent_tools']],
49
+ ['retrieval augmented',['rag']],
50
+ ['microsoft teams',['chat_platforms']],
51
+ ['chat window',['chat_ui']],
52
+ ['chat ui',['chat_ui']],
53
+ ['model server',['inference']],
54
+ ['web search',['search']],
55
+ ['transcribes',['audio']],
56
+ ['transcribe',['audio']],
57
+ ['transcribed',['audio']],
58
+ ['transcribing',['audio']],
59
+ ['transcriptions',['audio']],
60
+ ['transcription',['audio']],
61
+ ['summarizes',['text']],
62
+ ['summarize',['text']],
63
+ ['summarized',['text']],
64
+ ['summarizing',['text']],
65
+ ['summarization',['text']],
66
+ ['summaries',['text']],
67
+ ['summary',['text']],
68
+ ['voice',['audio']],
69
+ ['speech',['audio']],
70
+ ['audio',['audio']],
71
+ ['whisper',['audio']],
72
+ ['microphone',['audio']],
73
+ ['phone',['audio']],
74
+ ['calls',['audio']],
75
+ ['listen',['audio']],
76
+ ['asr',['audio']],
77
+ ['tts',['voice_generation']],
78
+ ['speak',['voice_generation']],
79
+ ['speaking',['voice_generation']],
80
+ ['draw',['image_generation']],
81
+ ['drawing',['image_generation']],
82
+ ['images',['image']],
83
+ ['image',['image']],
84
+ ['pictures',['image']],
85
+ ['picture',['image']],
86
+ ['photos',['image']],
87
+ ['photo',['image']],
88
+ ['vision',['image']],
89
+ ['videos',['video']],
90
+ ['video',['video']],
91
+ ['youtube',['video']],
92
+ ['multimodal',['multimodal']],
93
+ ['translates',['translation']],
94
+ ['translate',['translation']],
95
+ ['translation',['translation']],
96
+ ['translator',['translation']],
97
+ ['databases',['database_access']],
98
+ ['database',['database_access']],
99
+ ['postgresql',['database_access']],
100
+ ['postgres',['database_access']],
101
+ ['mongodb',['database_access']],
102
+ ['sqlite',['database_access']],
103
+ ['mysql',['database_access']],
104
+ ['sql',['database_access']],
105
+ ['storage',['database_access']],
106
+ ['stored',['database_access']],
107
+ ['stores',['database_access']],
108
+ ['storing',['database_access']],
109
+ ['store',['database_access']],
110
+ ['filesystem',['file_system']],
111
+ ['files',['file_system']],
112
+ ['file',['file_system']],
113
+ ['directory',['file_system']],
114
+ ['folders',['file_system']],
115
+ ['folder',['file_system']],
116
+ ['disk',['file_system']],
117
+ ['playwright',['browser_automation']],
118
+ ['puppeteer',['browser_automation']],
119
+ ['selenium',['browser_automation']],
120
+ ['scraping',['browser_automation']],
121
+ ['scrape',['browser_automation']],
122
+ ['browser',['browser_automation']],
123
+ ['webhooks',['api_calls']],
124
+ ['webhook',['api_calls']],
125
+ ['graphql',['api_calls']],
126
+ ['apis',['api_calls']],
127
+ ['api',['api_calls']],
128
+ ['scheduling',['scheduling']],
129
+ ['schedules',['scheduling']],
130
+ ['schedule',['scheduling']],
131
+ ['calendar',['scheduling']],
132
+ ['reminders',['scheduling']],
133
+ ['reminder',['scheduling']],
134
+ ['cron',['scheduling']],
135
+ ['notifications',['notifications']],
136
+ ['notification',['notifications']],
137
+ ['notify',['notifications']],
138
+ ['alerts',['notifications']],
139
+ ['alert',['notifications']],
140
+ ['authentication',['auth_encryption']],
141
+ ['encryption',['auth_encryption']],
142
+ ['encrypt',['auth_encryption']],
143
+ ['passwords',['auth_encryption']],
144
+ ['password',['auth_encryption']],
145
+ ['oauth',['auth_encryption']],
146
+ ['login',['auth_encryption']],
147
+ ['auth',['auth_encryption']],
148
+ ['analytics',['data_analysis']],
149
+ ['analysis',['data_analysis']],
150
+ ['analyze',['data_analysis']],
151
+ ['statistics',['data_analysis']],
152
+ ['statistical',['data_analysis']],
153
+ ['dataframe',['data_analysis']],
154
+ ['documents',['document_processing']],
155
+ ['document',['document_processing']],
156
+ ['docx',['document_processing']],
157
+ ['pdf',['document_processing']],
158
+ ['ocr',['document_processing']],
159
+ ['robotics',['robotics']],
160
+ ['robots',['robotics']],
161
+ ['robot',['robotics']],
162
+ ['ros',['robotics']],
163
+ ['embeddings',['embedding']],
164
+ ['embedding',['embedding']],
165
+ ['vectors',['embedding']],
166
+ ['vector',['embedding']],
167
+ ['searching',['search']],
168
+ ['searches',['search']],
169
+ ['search',['search']],
170
+ ['lookup',['search']],
171
+ ['websites',['web']],
172
+ ['website',['web']],
173
+ ['crawling',['web']],
174
+ ['crawl',['web']],
175
+ ['html',['web']],
176
+ ['url',['web']],
177
+ ['web',['web']],
178
+ ['datasets',['data']],
179
+ ['dataset',['data']],
180
+ ['csv',['data']],
181
+ ['etl',['data']],
182
+ ['kubernetes',['cloud']],
183
+ ['azure',['cloud']],
184
+ ['cloud',['cloud']],
185
+ ['aws',['cloud']],
186
+ ['gcp',['cloud']],
187
+ ['k8s',['cloud']],
188
+ ['payments',['finance']],
189
+ ['payment',['finance']],
190
+ ['invoices',['finance']],
191
+ ['invoice',['finance']],
192
+ ['accounting',['finance']],
193
+ ['trading',['finance']],
194
+ ['stripe',['finance']],
195
+ ['finance',['finance']],
196
+ ['spreadsheets',['productivity']],
197
+ ['spreadsheet',['productivity']],
198
+ ['productivity',['productivity']],
199
+ ['todos',['productivity']],
200
+ ['todo',['productivity']],
201
+ ['notes',['productivity']],
202
+ ['messaging',['comms']],
203
+ ['emails',['comms']],
204
+ ['email',['comms']],
205
+ ['sms',['comms']],
206
+ ['vulnerabilities',['security']],
207
+ ['vulnerability',['security']],
208
+ ['security',['security']],
209
+ ['pentest',['security']],
210
+ ['geospatial',['maps']],
211
+ ['locations',['maps']],
212
+ ['location',['maps']],
213
+ ['geocode',['maps']],
214
+ ['maps',['maps']],
215
+ ['map',['maps']],
216
+ ['memories',['memory']],
217
+ ['memory',['memory']],
218
+ ['remember',['memory']],
219
+ ['orchestration',['orchestration']],
220
+ ['orchestrate',['orchestration']],
221
+ ['workflows',['orchestration']],
222
+ ['workflow',['orchestration']],
223
+ ['pipelines',['orchestration']],
224
+ ['pipeline',['orchestration']],
225
+ ['cuda',['gpu_compute']],
226
+ ['gpu',['gpu_compute']],
227
+ ['mathematics',['math']],
228
+ ['equations',['math']],
229
+ ['equation',['math']],
230
+ ['calculator',['math']],
231
+ ['math',['math']],
232
+ ['rag',['rag']],
233
+ ['telegram',['chat_platforms']],
234
+ ['discord',['chat_platforms']],
235
+ ['slack',['chat_platforms']],
236
+ ['chatbot',['chat_ui']],
237
+ ['terminal',['cli']],
238
+ ['shell',['cli']],
239
+ ['cli',['cli']],
240
+ ['benchmarks',['evaluation']],
241
+ ['benchmark',['evaluation']],
242
+ ['evaluation',['evaluation']],
243
+ ['evaluate',['evaluation']],
244
+ ['observability',['observability']],
245
+ ['monitoring',['observability']],
246
+ ['metrics',['observability']],
247
+ ['tracing',['observability']],
248
+ ['logs',['observability']],
249
+ ['rabbitmq',['queues']],
250
+ ['queues',['queues']],
251
+ ['queue',['queues']],
252
+ ['kafka',['queues']],
253
+ ['inference',['inference']],
254
+ ['vllm',['inference']],
255
+ ['simulations',['simulation']],
256
+ ['simulation',['simulation']],
257
+ ['simulator',['simulation']],
258
+ ['simulate',['simulation']],
259
+ ['writing',['text']],
260
+ ['prompt',['text']],
261
+ ['chat',['text']],
262
+ ['llm',['text']],
263
+ ['text',['text']],
264
+ ['coding',['code']],
265
+ ['programming',['code']],
266
+ ['developer',['code']],
267
+ ['software',['code']],
268
+ ['code',['code']],
269
+ ['sandbox',['code_execution']],
270
+ ['interpreter',['code_execution']],
271
+ ['repl',['code_execution']]
272
+ ];
273
+
274
+ export const KEYWORD_RULES=RULE_LIST.map(([phrase,modalities])=>({phrase,modalities:[...modalities]}))
275
+ .sort((a,b)=>b.phrase.length-a.phrase.length||a.phrase.localeCompare(b.phrase));
276
+
277
+ const CLAUSE_SPLIT=/\s*(?:,|;|&)\s*|\s+\b(?:and|then)\b\s+/i;
278
+ const EDGE_STOP=new Set(['a','an','the','that','which','who','to','for','of','in','on','with','and','then','into','from','by','or','also']);
279
+
280
+ function bounded(text,start,end){
281
+ const before=start===0||!/[\p{L}\p{N}_]/u.test(text[start-1]);
282
+ const after=end===text.length||!/[\p{L}\p{N}_]/u.test(text[end]);
283
+ return before&&after;
284
+ }
285
+
286
+ export function findKeywordHits(text){
287
+ const lower=String(text||'').toLowerCase();
288
+ const found=[];
289
+ for(const rule of KEYWORD_RULES){
290
+ let from=0;
291
+ while(from<=lower.length-rule.phrase.length){
292
+ const i=lower.indexOf(rule.phrase,from);
293
+ if(i<0)break;
294
+ const end=i+rule.phrase.length;
295
+ if(bounded(lower,i,end)&&!found.some(h=>i<h.end&&end>h.start))found.push({start:i,end,phrase:rule.phrase,modalities:rule.modalities});
296
+ from=i+1;
297
+ }
298
+ }
299
+ found.sort((a,b)=>a.start-b.start||a.end-b.end);
300
+ return found;
301
+ }
302
+
303
+ function cleanPart(text){
304
+ const bits=String(text||'').replace(/\s+/g,' ').trim().replace(/^[,;:.]+|[,;:.]+$/g,'').split(' ').filter(Boolean);
305
+ while(bits.length&&EDGE_STOP.has(bits[0].toLowerCase()))bits.shift();
306
+ while(bits.length&&EDGE_STOP.has(bits[bits.length-1].toLowerCase()))bits.pop();
307
+ return bits.join(' ');
308
+ }
309
+
310
+ function overlaps(a,b){
311
+ return a.some(m=>b.includes(m));
312
+ }
313
+
314
+ function clauseLabel(clause){
315
+ return cleanPart(clause)||String(clause||'').replace(/\s+/g,' ').trim();
316
+ }
317
+
318
+ function partsFromClause(clause){
319
+ const hits=findKeywordHits(clause);
320
+ if(!hits.length){
321
+ const text=clauseLabel(clause);
322
+ return text?[{text,modalities:[],keywords:[]}]:[];
323
+ }
324
+ const groups=[];
325
+ for(const hit of hits){
326
+ const last=groups[groups.length-1];
327
+ if(last&&overlaps(last.modalities,hit.modalities)){
328
+ last.hits.push(hit);
329
+ for(const m of hit.modalities)if(!last.modalities.includes(m))last.modalities.push(m);
330
+ if(!last.keywords.includes(hit.phrase))last.keywords.push(hit.phrase);
331
+ }else groups.push({modalities:[...hit.modalities],keywords:[hit.phrase],hits:[hit]});
332
+ }
333
+ if(groups.length===1){
334
+ const text=clauseLabel(clause)||groups[0].keywords.join(' ');
335
+ return [{text,modalities:groups[0].modalities,keywords:groups[0].keywords}];
336
+ }
337
+ return groups.map((group,i)=>{
338
+ const start=i===0?0:splitPoint(clause,groups[i-1].hits.at(-1).end,group.hits[0].start);
339
+ const end=i===groups.length-1?clause.length:splitPoint(clause,group.hits.at(-1).end,groups[i+1].hits[0].start);
340
+ const text=clauseLabel(clause.slice(start,end))||group.keywords.join(' ');
341
+ return {text,modalities:group.modalities,keywords:group.keywords};
342
+ });
343
+ }
344
+
345
+ function splitPoint(clause,leftEnd,rightStart){
346
+ const gap=clause.slice(leftEnd,rightStart);
347
+ const rel=gap.lastIndexOf(' ');
348
+ return rel>=0?leftEnd+rel+1:Math.floor((leftEnd+rightStart)/2);
349
+ }
350
+
351
+ export function decomposeGoal(goal){
352
+ const text=String(goal||'').replace(/\s+/g,' ').trim();
353
+ if(!text)return [];
354
+ const parts=[];
355
+ for(const clause of text.split(CLAUSE_SPLIT).map(s=>s.trim()).filter(Boolean))parts.push(...partsFromClause(clause));
356
+ return parts;
357
+ }
358
+
359
+ export function packageVersion(){
360
+ return JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
361
+ }
362
+
363
+ export function installCommand(bot){
364
+ const eps=localEndpoints(bot);
365
+ for(const ep of eps)if(ep.command)return ep.command;
366
+ for(const ep of eps)if(ep.url)return ep.url;
367
+ return bot.endpoint||'';
368
+ }
369
+
370
+ function oneLine(bot){
371
+ const caps=(bot.caps||[]).map(c=>String(c).replace(/^cap\./,'').replace(/^mcp\./,'').replace(/[._]+/g,' ').trim()).filter(c=>c&&c.toLowerCase()!=='connect');
372
+ const kind={http:'live HTTP endpoint',npx:'npx package',uvx:'uvx package',hf:'Hugging Face model',botboozle:'open-source package'}[bot.kind]||bot.kind||'bot';
373
+ const detail=caps.length?caps.slice(0,4).join(', '):(bot.modalities||[]).slice(0,3).join(', ');
374
+ const line=detail?`${bot.name} — ${kind}; ${detail}`:`${bot.name} — ${kind}`;
375
+ return line.replace(/\s+/g,' ').slice(0,180);
376
+ }
377
+
378
+ function whyLine(modality,keywords){
379
+ const hint=(keywords||[]).filter(k=>KEYWORD_RULES.some(r=>r.phrase===k&&r.modalities.includes(modality))).slice(0,4).join('/');
380
+ return hint?`matched ${modality}/${hint}; top-ranked in modality`:`matched ${modality}; top-ranked in modality`;
381
+ }
382
+
383
+ function rankModality(bots,modality,keywords,limit){
384
+ const terms=[...new Set((keywords||[]).flatMap(words))];
385
+ const want=[modality];
386
+ const pool=[];
387
+ for(let i=0;i<bots.length;i++){
388
+ const b=bots[i];
389
+ if(!(b.modalities||[]).includes(modality))continue;
390
+ const blob=(b.name+' '+b.id+' '+(b.caps||[]).join(' ')).toLowerCase();
391
+ pool.push({b,i,hits:terms.filter(t=>blob.includes(t)).length});
392
+ }
393
+ const tier=cls=>CLASS_TIER[cls]??9;
394
+ pool.sort((x,y)=>tier(x.b.cls)-tier(y.b.cls)||primarySortHint(y.b.modality_primary,want)-primarySortHint(x.b.modality_primary,want)||y.hits-x.hits||(y.b.score||0)-(x.b.score||0)||x.i-y.i||String(x.b.id).localeCompare(String(y.b.id)));
395
+ return pool.slice(0,limit).map(x=>x.b);
396
+ }
397
+
398
+ export function recommendGoal(goal,options={}){
399
+ const source=options.bots?{bots:options.bots}:loadLocal();
400
+ if(!source?.bots)throw new Error('No local catalog. Run `kloudy bots sync`.');
401
+ const limit=options.perModality??PER_MODALITY;
402
+ const pieces=decomposeGoal(goal);
403
+ const parts=[];
404
+ const unmatched=[];
405
+ const consider=pieces.length?pieces:[{text:String(goal||'').trim(),modalities:[],keywords:[]}];
406
+ for(const piece of consider){
407
+ if(!piece.text)continue;
408
+ if(!piece.modalities.length){
409
+ unmatched.push({part:piece.text,reason:'no modality or catalog match'});
410
+ continue;
411
+ }
412
+ const seen=new Map();
413
+ const bots=[];
414
+ for(const modality of piece.modalities){
415
+ for(const bot of rankModality(source.bots,modality,piece.keywords,limit)){
416
+ const prev=seen.get(bot.id);
417
+ if(prev){
418
+ if(!prev.modalities.includes(modality))prev.modalities.push(modality);
419
+ continue;
420
+ }
421
+ const item={
422
+ id:bot.id,
423
+ name:bot.name,
424
+ class:bot.cls,
425
+ command:installCommand(bot),
426
+ license:bot.license||'',
427
+ description:oneLine(bot),
428
+ why:whyLine(modality,piece.keywords),
429
+ modalities:[modality]
430
+ };
431
+ seen.set(bot.id,item);
432
+ bots.push(item);
433
+ }
434
+ }
435
+ if(!bots.length){
436
+ unmatched.push({part:piece.text,reason:'no modality or catalog match'});
437
+ continue;
438
+ }
439
+ parts.push({part:piece.text,modalities:piece.modalities,bots});
440
+ }
441
+ return {schema:RECOMMEND_SCHEMA,catalog:CATALOG_NAME,version:options.version||packageVersion(),goal:String(goal??''),parts,unmatched};
442
+ }
443
+
444
+ export function formatRecommend(result){
445
+ const lines=[`${result.catalog} · package ${result.version}`,`Goal: ${result.goal}`];
446
+ result.parts.forEach((part,index)=>{
447
+ lines.push('',`${index+1}. ${part.part}`,`Modalities: ${part.modalities.join(', ')}`);
448
+ part.bots.forEach((bot,n)=>{
449
+ lines.push(` ${n+1}) ${bot.id} [${bot.class}] ${bot.license||'unknown license'}`,` ${bot.command}`,` ${bot.description}`,` why: ${bot.why}`);
450
+ });
451
+ });
452
+ if(result.unmatched.length){
453
+ lines.push('','Unmatched');
454
+ for(const item of result.unmatched)lines.push(`- ${item.part}: ${item.reason}`);
455
+ }
456
+ return lines.join('\n');
457
+ }
458
+
459
+ export function recommendCommand(args,opts={}){
460
+ const out=opts.out||(s=>console.log(s));
461
+ const o=flags(args);
462
+ const goal=o._.join(' ').trim();
463
+ if(!goal)throw new Error('Usage: kloudy recommend <goal> [--json]');
464
+ const result=recommendGoal(goal);
465
+ out(o.json?JSON.stringify(result,null,2):formatRecommend(result));
466
+ return result;
467
+ }
468
+
469
+ export function agentsDocument(){
470
+ return readFileSync(new URL('../AGENTS.md',import.meta.url),'utf8');
471
+ }
472
+
473
+ export function agentsCommand(opts={}){
474
+ const out=opts.out||(s=>console.log(s));
475
+ const text=agentsDocument().replace(/\s*$/,'');
476
+ out(text);
477
+ return text;
478
+ }
package/src/replies.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import catalog from './reply-templates.json' with {type:'json'};
2
- // Plain text output only. Hosts must render as text, never trusted HTML.
3
- export function renderReply(state,slots){
4
- const template=catalog.templates[Object.hasOwn(catalog.templates,state)?state:'unknown'];
5
- if(!slots||Array.isArray(slots)||Object.keys(slots).length!==1||typeof slots.action!=='string'||!slots.action.trim()||slots.action.length>160||/[\u0000-\u001f\u007f]/.test(slots.action))throw Error('invalid_reply_slots');
6
- return {version:catalog.version,status:template.status,text:template.text.replace('{action}',()=>slots.action)};
7
- }
1
+ import catalog from './reply-templates.json' with {type:'json'};
2
+ // Plain text output only. Hosts must render as text, never trusted HTML.
3
+ export function renderReply(state,slots){
4
+ const template=catalog.templates[Object.hasOwn(catalog.templates,state)?state:'unknown'];
5
+ if(!slots||Array.isArray(slots)||Object.keys(slots).length!==1||typeof slots.action!=='string'||!slots.action.trim()||slots.action.length>160||/[\u0000-\u001f\u007f]/.test(slots.action))throw Error('invalid_reply_slots');
6
+ return {version:catalog.version,status:template.status,text:template.text.replace('{action}',()=>slots.action)};
7
+ }
@@ -1,12 +1,12 @@
1
- {
2
- "version": "kloudy.replies/1",
3
- "templates": {
4
- "awaiting_approval": {"status":"approval", "text":"{action} — Needs your approval."},
5
- "queued": {"status":"pending", "text":"{action} — Queued."},
6
- "running": {"status":"pending", "text":"{action} — Working."},
7
- "completed": {"status":"completed", "text":"{action} — Completed."},
8
- "failed": {"status":"failed", "text":"{action} — Failed."},
9
- "cancelled": {"status":"cancelled", "text":"{action} — Cancelled."},
10
- "unknown": {"status":"pending", "text":"{action} — Status not confirmed."}
11
- }
12
- }
1
+ {
2
+ "version": "kloudy.replies/1",
3
+ "templates": {
4
+ "awaiting_approval": {"status":"approval", "text":"{action} — Needs your approval."},
5
+ "queued": {"status":"pending", "text":"{action} — Queued."},
6
+ "running": {"status":"pending", "text":"{action} — Working."},
7
+ "completed": {"status":"completed", "text":"{action} — Completed."},
8
+ "failed": {"status":"failed", "text":"{action} — Failed."},
9
+ "cancelled": {"status":"cancelled", "text":"{action} — Cancelled."},
10
+ "unknown": {"status":"pending", "text":"{action} — Status not confirmed."}
11
+ }
12
+ }
@@ -1,31 +1,31 @@
1
- // These controllers require a trusted account/engine adapter. Never feed model output as an invoice.
2
- const id=v=>typeof v==='string'&&/^[\w:.-]{1,128}$/.test(v);
3
- const minor=v=>Number.isSafeInteger(v)&&v>=0&&v<=100000000;
4
- const same=(a,b)=>a?.accountRef===b?.accountRef&&a?.projectId===b?.projectId&&a?.grantId===b?.grantId;
5
- export function validateBill(b,binding,now=Date.now()){
6
- if(!binding||!['accountRef','projectId','grantId'].every(k=>id(binding[k])))throw Error('Verify your project before paying.');
7
- if(!b||!same(b,binding)||!['due','paid','paused'].includes(b.state)||!id(b.billId)||!id(b.revision)||b.currency!=='usd'||!minor(b.totalMinor)||!Number.isFinite(b.expiresAt)||b.expiresAt<=now||b.expiresAt>now+900000||!Array.isArray(b.lines)||b.lines.length>20||!b.lines.every(l=>typeof l.label==='string'&&l.label.length<=120&&minor(l.amountMinor))||b.lines.reduce((n,l)=>n+l.amountMinor,0)!==b.totalMinor)throw Error('Refresh this bill before paying.');
8
- return structuredClone(b);
9
- }
10
- export class BillApproval{
11
- constructor({adapter,binding,now=Date.now,key=()=>globalThis.crypto.randomUUID()}){this.adapter=adapter;this.binding=structuredClone(binding);this.now=now;this.key=key;this.generation=0;this.bill=null;this.pending=null;this.approvalKey=null;}
12
- reset(){this.generation++;this.bill=null;this.approvalKey=null;}
13
- async read(billId){if(!id(billId))throw Error('Invalid bill');this.reset();const generation=this.generation;const b=validateBill(await this.adapter.readBill(billId),this.binding,this.now());if(generation!==this.generation||b.billId!==billId)throw Error('Bill changed.');this.bill=b;this.approvalKey=this.key();return b;}
14
- async pay({approvedBillId,approvedRevision}={}){
15
- if(this.pending)return this.pending;
16
- const b=validateBill(this.bill,this.binding,this.now());
17
- if(b.state!=='due'&&b.state!=='paused'||approvedBillId!==b.billId||approvedRevision!==b.revision)throw Error('Approve the current bill before paying.');
18
- const generation=this.generation,key=this.approvalKey;
19
- this.pending=(async()=>{const current=await this.adapter.binding();if(!same(current,this.binding)||current.connected!==true)throw Error('Project authorization changed.');if(generation!==this.generation)throw Error('Bill changed.');validateBill(b,this.binding,this.now());
20
- const result=await this.adapter.payBill({billId:b.billId,revision:b.revision,totalMinor:b.totalMinor,currency:b.currency,idempotencyKey:key});
21
- if(generation!==this.generation)throw Error('Payment status needs a fresh receipt.');
22
- if(result?.state!=='paid'||!same(result,this.binding)||result.billId!==b.billId||result.revision!==b.revision||result.totalMinor!==b.totalMinor||result.currency!==b.currency||!id(result.receiptId))throw Error('Payment is not confirmed. Check the receipt before retrying.');
23
- this.bill={...b,state:'paid'};return result;
24
- })().finally(()=>{this.pending=null;});return this.pending;
25
- }
26
- }
27
- const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
28
- const amount=n=>new Intl.NumberFormat('en-US',{style:'currency',currency:'USD'}).format(n/100);
29
- export function billCardMarkup(b,binding,{now=Date.now(),busy=false}={}){b=validateBill(b,binding,now);return `<section class="sample-card"><header>Kloudy Sample card · Wallet</header><h1>${b.state==='paid'?'Payment received':'Your bill is ready'}</h1><p class="bill-amount">${amount(b.totalMinor)}</p>${b.state==='paid'?'':`<button class="sample-primary" data-bill-pay ${busy?'disabled':''}>Pay ${amount(b.totalMinor)}</button><p class="sample-note">Only this bill. Your balance is never charged without your approval.</p>`}<details><summary>Cost breakdown</summary><dl>${b.lines.map(l=>`<dt>${e(l.label)}</dt><dd>${amount(l.amountMinor)}</dd>`).join('')}</dl></details>${b.state==='paid'?'':'<a href="https://kloudy.ai/?view=wallet">Add money to wallet</a><p class="sample-note">Adding money does not pay this bill. Return here to approve it.</p>'}</section>`;}
30
- export function attachmentCardMarkup(name){return `<section class="sample-card"><header>Kloudy Sample card</header><h1>Where should this tool stay?</h1><p>${e(String(name).slice(0,160))}</p><div class="sample-options"><button data-attachment="project">This project</button><button data-attachment="account">All my projects</button></div><p class="sample-note">The engine must confirm the link. Permissions remain scoped and revocable; no permanent grant or payment is created.</p></section>`;}
31
- export function clarificationCardMarkup(question,choices){if(typeof question!=='string'||question.length>160||!Array.isArray(choices)||choices.length<2||choices.length>4||choices.some(c=>!id(c.id)||typeof c.label!=='string'||c.label.length>60)||new Set(choices.map(c=>c.id)).size!==choices.length)throw Error('Invalid clarification');return `<section class="sample-card"><header>Kloudy Sample card</header><h1>${e(question)}</h1><div class="sample-options">${choices.map(c=>`<button data-clarify="${e(c.id)}">${e(c.label)}</button>`).join('')}</div></section>`;}
1
+ // These controllers require a trusted account/engine adapter. Never feed model output as an invoice.
2
+ const id=v=>typeof v==='string'&&/^[\w:.-]{1,128}$/.test(v);
3
+ const minor=v=>Number.isSafeInteger(v)&&v>=0&&v<=100000000;
4
+ const same=(a,b)=>a?.accountRef===b?.accountRef&&a?.projectId===b?.projectId&&a?.grantId===b?.grantId;
5
+ export function validateBill(b,binding,now=Date.now()){
6
+ if(!binding||!['accountRef','projectId','grantId'].every(k=>id(binding[k])))throw Error('Verify your project before paying.');
7
+ if(!b||!same(b,binding)||!['due','paid','paused'].includes(b.state)||!id(b.billId)||!id(b.revision)||b.currency!=='usd'||!minor(b.totalMinor)||!Number.isFinite(b.expiresAt)||b.expiresAt<=now||b.expiresAt>now+900000||!Array.isArray(b.lines)||b.lines.length>20||!b.lines.every(l=>typeof l.label==='string'&&l.label.length<=120&&minor(l.amountMinor))||b.lines.reduce((n,l)=>n+l.amountMinor,0)!==b.totalMinor)throw Error('Refresh this bill before paying.');
8
+ return structuredClone(b);
9
+ }
10
+ export class BillApproval{
11
+ constructor({adapter,binding,now=Date.now,key=()=>globalThis.crypto.randomUUID()}){this.adapter=adapter;this.binding=structuredClone(binding);this.now=now;this.key=key;this.generation=0;this.bill=null;this.pending=null;this.approvalKey=null;}
12
+ reset(){this.generation++;this.bill=null;this.approvalKey=null;}
13
+ async read(billId){if(!id(billId))throw Error('Invalid bill');this.reset();const generation=this.generation;const b=validateBill(await this.adapter.readBill(billId),this.binding,this.now());if(generation!==this.generation||b.billId!==billId)throw Error('Bill changed.');this.bill=b;this.approvalKey=this.key();return b;}
14
+ async pay({approvedBillId,approvedRevision}={}){
15
+ if(this.pending)return this.pending;
16
+ const b=validateBill(this.bill,this.binding,this.now());
17
+ if(b.state!=='due'&&b.state!=='paused'||approvedBillId!==b.billId||approvedRevision!==b.revision)throw Error('Approve the current bill before paying.');
18
+ const generation=this.generation,key=this.approvalKey;
19
+ this.pending=(async()=>{const current=await this.adapter.binding();if(!same(current,this.binding)||current.connected!==true)throw Error('Project authorization changed.');if(generation!==this.generation)throw Error('Bill changed.');validateBill(b,this.binding,this.now());
20
+ const result=await this.adapter.payBill({billId:b.billId,revision:b.revision,totalMinor:b.totalMinor,currency:b.currency,idempotencyKey:key});
21
+ if(generation!==this.generation)throw Error('Payment status needs a fresh receipt.');
22
+ if(result?.state!=='paid'||!same(result,this.binding)||result.billId!==b.billId||result.revision!==b.revision||result.totalMinor!==b.totalMinor||result.currency!==b.currency||!id(result.receiptId))throw Error('Payment is not confirmed. Check the receipt before retrying.');
23
+ this.bill={...b,state:'paid'};return result;
24
+ })().finally(()=>{this.pending=null;});return this.pending;
25
+ }
26
+ }
27
+ const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
28
+ const amount=n=>new Intl.NumberFormat('en-US',{style:'currency',currency:'USD'}).format(n/100);
29
+ export function billCardMarkup(b,binding,{now=Date.now(),busy=false}={}){b=validateBill(b,binding,now);return `<section class="sample-card"><header>Kloudy Sample card · Wallet</header><h1>${b.state==='paid'?'Payment received':'Your bill is ready'}</h1><p class="bill-amount">${amount(b.totalMinor)}</p>${b.state==='paid'?'':`<button class="sample-primary" data-bill-pay ${busy?'disabled':''}>Pay ${amount(b.totalMinor)}</button><p class="sample-note">Only this bill. Your balance is never charged without your approval.</p>`}<details><summary>Cost breakdown</summary><dl>${b.lines.map(l=>`<dt>${e(l.label)}</dt><dd>${amount(l.amountMinor)}</dd>`).join('')}</dl></details>${b.state==='paid'?'':'<a href="https://kloudy.ai/?view=wallet">Add money to wallet</a><p class="sample-note">Adding money does not pay this bill. Return here to approve it.</p>'}</section>`;}
30
+ export function attachmentCardMarkup(name){return `<section class="sample-card"><header>Kloudy Sample card</header><h1>Where should this tool stay?</h1><p>${e(String(name).slice(0,160))}</p><div class="sample-options"><button data-attachment="project">This project</button><button data-attachment="account">All my projects</button></div><p class="sample-note">The engine must confirm the link. Permissions remain scoped and revocable; no permanent grant or payment is created.</p></section>`;}
31
+ export function clarificationCardMarkup(question,choices){if(typeof question!=='string'||question.length>160||!Array.isArray(choices)||choices.length<2||choices.length>4||choices.some(c=>!id(c.id)||typeof c.label!=='string'||c.label.length>60)||new Set(choices.map(c=>c.id)).size!==choices.length)throw Error('Invalid clarification');return `<section class="sample-card"><header>Kloudy Sample card</header><h1>${e(question)}</h1><div class="sample-options">${choices.map(c=>`<button data-clarify="${e(c.id)}">${e(c.label)}</button>`).join('')}</div></section>`;}
@@ -1,11 +1,11 @@
1
- import {sampleHTML} from './sample-ui.mjs';
2
- export const SAMPLE_URI='ui://widget/kloudy-sample-v1.html';
3
- export const sampleTool={name:'kloudy_sample_card',title:'Kloudy Sample card',description:'Show a compact Kloudy preference card when the user asks to choose tool types, project location or tool-update frequency. Also opens account-owned wallet/activity views or explains BWW conversion. Navigation and preference collection only: never grants access, installs tools or authorizes payment. Use once at first relevant setup, or when preferences are unclear; do not repeat for every tool.',annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:'object',properties:{kind:{type:'string',enum:['preferences','conversion','analytics','wallet']},surface:{type:'string',enum:['ide','chatbot','site'],default:'ide'}},required:['kind'],additionalProperties:false},_meta:{ui:{resourceUri:SAMPLE_URI},'openai/outputTemplate':SAMPLE_URI}};
4
- export const sampleResource={uri:SAMPLE_URI,name:'Kloudy Sample card',description:'Compact preferences and account navigation. No execution or billing authority.',mimeType:'text/html;profile=mcp-app'};
5
- export function sampleArguments(a){return a&&typeof a==='object'&&!Array.isArray(a)&&Object.keys(a).every(k=>['kind','surface'].includes(k))&&['preferences','conversion','analytics','wallet'].includes(a.kind)&&(a.surface===undefined||['ide','chatbot','site'].includes(a.surface));}
6
- export function sampleMessage(method,params){return method==='resources/read'&&params?.uri===SAMPLE_URI&&Object.keys(params).length===1||method==='tools/call'&&params?.name==='kloudy_sample_card'&&Object.keys(params).every(k=>['name','arguments'].includes(k))&&sampleArguments(params.arguments);}
7
- export function sampleResult(kind='preferences',surface='ide'){
8
- const url={preferences:'https://kloudy.ai/samples?frontdoor='+surface,conversion:'https://kloudy.ai/samples?card=conversion',analytics:'https://kloudy.ai/bots',wallet:'https://kloudy.ai/?view=wallet'}[kind];
9
- return {content:[{type:'text',text:`Kloudy Sample card\n\n[${kind==='preferences'?'Choose your tool preferences':'Open '+kind}](${url})\n\nSilent delivery is the default. New costs require approval. Opening a card does not connect a tool or make a payment.`}],structuredContent:{version:'kloudy.sample/1',kind,surface,url,executed:false,authorizesSpending:false}};
10
- }
11
- export function sampleContents(){return {contents:[{uri:SAMPLE_URI,mimeType:sampleResource.mimeType,text:sampleHTML,_meta:{ui:{prefersBorder:true,domain:'https://kloudy.ai',csp:{connectDomains:[],resourceDomains:[]}},'openai/widgetDescription':'Kloudy Sample card. Three short preference questions; no tool execution or payment.','openai/widgetPrefersBorder':true,'openai/widgetCSP':{connect_domains:[],resource_domains:[]},'openai/widgetDomain':'https://kloudy.ai'}}]};}
1
+ import {sampleHTML} from './sample-ui.mjs';
2
+ export const SAMPLE_URI='ui://widget/kloudy-sample-v1.html';
3
+ export const sampleTool={name:'kloudy_sample_card',title:'Kloudy Sample card',description:'Show a compact Kloudy preference card when the user asks to choose tool types, project location or tool-update frequency. Also opens account-owned wallet/activity views or explains BWW conversion. Navigation and preference collection only: never grants access, installs tools or authorizes payment. Use once at first relevant setup, or when preferences are unclear; do not repeat for every tool.',annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false},inputSchema:{type:'object',properties:{kind:{type:'string',enum:['preferences','conversion','analytics','wallet']},surface:{type:'string',enum:['ide','chatbot','site'],default:'ide'}},required:['kind'],additionalProperties:false},_meta:{ui:{resourceUri:SAMPLE_URI},'openai/outputTemplate':SAMPLE_URI}};
4
+ export const sampleResource={uri:SAMPLE_URI,name:'Kloudy Sample card',description:'Compact preferences and account navigation. No execution or billing authority.',mimeType:'text/html;profile=mcp-app'};
5
+ export function sampleArguments(a){return a&&typeof a==='object'&&!Array.isArray(a)&&Object.keys(a).every(k=>['kind','surface'].includes(k))&&['preferences','conversion','analytics','wallet'].includes(a.kind)&&(a.surface===undefined||['ide','chatbot','site'].includes(a.surface));}
6
+ export function sampleMessage(method,params){return method==='resources/read'&&params?.uri===SAMPLE_URI&&Object.keys(params).length===1||method==='tools/call'&&params?.name==='kloudy_sample_card'&&Object.keys(params).every(k=>['name','arguments'].includes(k))&&sampleArguments(params.arguments);}
7
+ export function sampleResult(kind='preferences',surface='ide'){
8
+ const url={preferences:'https://kloudy.ai/samples?frontdoor='+surface,conversion:'https://kloudy.ai/samples?card=conversion',analytics:'https://kloudy.ai/bots',wallet:'https://kloudy.ai/?view=wallet'}[kind];
9
+ return {content:[{type:'text',text:`Kloudy Sample card\n\n[${kind==='preferences'?'Choose your tool preferences':'Open '+kind}](${url})\n\nSilent delivery is the default. New costs require approval. Opening a card does not connect a tool or make a payment.`}],structuredContent:{version:'kloudy.sample/1',kind,surface,url,executed:false,authorizesSpending:false}};
10
+ }
11
+ export function sampleContents(){return {contents:[{uri:SAMPLE_URI,mimeType:sampleResource.mimeType,text:sampleHTML,_meta:{ui:{prefersBorder:true,domain:'https://kloudy.ai',csp:{connectDomains:[],resourceDomains:[]}},'openai/widgetDescription':'Kloudy Sample card. Three short preference questions; no tool execution or payment.','openai/widgetPrefersBorder':true,'openai/widgetCSP':{connect_domains:[],resource_domains:[]},'openai/widgetDomain':'https://kloudy.ai'}}]};}