kloudy 1.0.0 → 1.0.2
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/AGENTS.md +83 -0
- package/README.md +88 -1
- package/bin/entry.mjs +4 -1
- package/bin/kloudy.mjs +10 -1
- package/package.json +3 -2
- package/schemas/kloudy.deployable-bot.schema.json +80 -80
- package/src/bots.mjs +144 -144
- package/src/feedback.mjs +159 -0
- package/src/modalities.mjs +28 -27
- package/src/recommend.mjs +756 -0
- package/src/scores.mjs +80 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
// Stack recommender. Kloudy never runs an LLM.
|
|
2
|
+
// `kloudy recommend <goal> [--json] [--limit N] [--offline]`
|
|
3
|
+
// `kloudy stack` is the same command.
|
|
4
|
+
// Ranking reads the bundled catalog. recommendCommand may apply cached score overrides.
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {readFileSync} from 'node:fs';
|
|
7
|
+
import {flags,loadLocal,localEndpoints} from './bots.mjs';
|
|
8
|
+
import {primarySortHint} from './modalities.mjs';
|
|
9
|
+
import {loadScoreOverrides,normalizeScores} from './scores.mjs';
|
|
10
|
+
|
|
11
|
+
export const RECOMMEND_SCHEMA='kloudy.recommend/1';
|
|
12
|
+
export const CATALOG_NAME='kloudy 1.0';
|
|
13
|
+
export const DEFAULT_LIMIT=5;
|
|
14
|
+
export const MIN_LIMIT=1;
|
|
15
|
+
export const MAX_LIMIT=20;
|
|
16
|
+
const PER_MODALITY=DEFAULT_LIMIT;
|
|
17
|
+
// Class order for recommendations is A, then B, then O.
|
|
18
|
+
// Score overrides apply after class: boost, then unchanged, then demote.
|
|
19
|
+
// Exclude drops a bot before the cap. Primary-modality hint, keyword hits,
|
|
20
|
+
// catalog score, and index tie-break follow the comparator in localSearch.
|
|
21
|
+
const CLASS_TIER={A:0,B:1,O:2};
|
|
22
|
+
const words=s=>String(s||'').toLowerCase().split(/[^\p{L}\p{N}_]+/u).filter(w=>w.length>1);
|
|
23
|
+
|
|
24
|
+
const RULE_LIST=[
|
|
25
|
+
['text to speech',['voice_generation']],
|
|
26
|
+
['text-to-speech',['voice_generation']],
|
|
27
|
+
['speech synthesis',['voice_generation']],
|
|
28
|
+
['voice generation',['voice_generation']],
|
|
29
|
+
['voice over',['voice_generation']],
|
|
30
|
+
['voiceover',['voice_generation']],
|
|
31
|
+
['text to image',['image_generation']],
|
|
32
|
+
['text-to-image',['image_generation']],
|
|
33
|
+
['image generation',['image_generation']],
|
|
34
|
+
['generate images',['image_generation']],
|
|
35
|
+
['generate image',['image_generation']],
|
|
36
|
+
['stable diffusion',['image_generation']],
|
|
37
|
+
['txt2img',['image_generation']],
|
|
38
|
+
['code execution',['code_execution']],
|
|
39
|
+
['execute code',['code_execution']],
|
|
40
|
+
['run code',['code_execution']],
|
|
41
|
+
['browser automation',['browser_automation']],
|
|
42
|
+
['web scraping',['browser_automation']],
|
|
43
|
+
['file system',['file_system']],
|
|
44
|
+
['command line',['cli']],
|
|
45
|
+
['command-line',['cli']],
|
|
46
|
+
['vector database',['embedding']],
|
|
47
|
+
['vector store',['embedding']],
|
|
48
|
+
['rest api',['api_calls']],
|
|
49
|
+
['api calls',['api_calls']],
|
|
50
|
+
['api call',['api_calls']],
|
|
51
|
+
['function calls',['agent_tools']],
|
|
52
|
+
['function call',['agent_tools']],
|
|
53
|
+
['function calling',['agent_tools']],
|
|
54
|
+
['tool calling',['agent_tools']],
|
|
55
|
+
['agent tools',['agent_tools']],
|
|
56
|
+
['agent tool',['agent_tools']],
|
|
57
|
+
['retrieval augmented',['rag']],
|
|
58
|
+
['microsoft teams',['chat_platforms']],
|
|
59
|
+
['chat window',['chat_ui']],
|
|
60
|
+
['chat ui',['chat_ui']],
|
|
61
|
+
['model server',['inference']],
|
|
62
|
+
['web search',['search']],
|
|
63
|
+
['transcribes',['audio']],
|
|
64
|
+
['transcribe',['audio']],
|
|
65
|
+
['transcribed',['audio']],
|
|
66
|
+
['transcribing',['audio']],
|
|
67
|
+
['transcriptions',['audio']],
|
|
68
|
+
['transcription',['audio']],
|
|
69
|
+
['summarizes',['text']],
|
|
70
|
+
['summarize',['text']],
|
|
71
|
+
['summarized',['text']],
|
|
72
|
+
['summarizing',['text']],
|
|
73
|
+
['summarization',['text']],
|
|
74
|
+
['summaries',['text']],
|
|
75
|
+
['summary',['text']],
|
|
76
|
+
['voice',['audio']],
|
|
77
|
+
['speech',['audio']],
|
|
78
|
+
['audio',['audio']],
|
|
79
|
+
['whisper',['audio']],
|
|
80
|
+
['microphone',['audio']],
|
|
81
|
+
['listen',['audio']],
|
|
82
|
+
['asr',['audio']],
|
|
83
|
+
['tts',['voice_generation']],
|
|
84
|
+
['speak',['voice_generation']],
|
|
85
|
+
['speaking',['voice_generation']],
|
|
86
|
+
['draw',['image_generation']],
|
|
87
|
+
['drawing',['image_generation']],
|
|
88
|
+
['images',['image']],
|
|
89
|
+
['image',['image']],
|
|
90
|
+
['pictures',['image']],
|
|
91
|
+
['picture',['image']],
|
|
92
|
+
['photos',['image']],
|
|
93
|
+
['photo',['image']],
|
|
94
|
+
['vision',['image']],
|
|
95
|
+
['videos',['video']],
|
|
96
|
+
['video',['video']],
|
|
97
|
+
['youtube',['video']],
|
|
98
|
+
['multimodal',['multimodal']],
|
|
99
|
+
['translates',['translation']],
|
|
100
|
+
['translate',['translation']],
|
|
101
|
+
['translation',['translation']],
|
|
102
|
+
['translator',['translation']],
|
|
103
|
+
['databases',['database_access']],
|
|
104
|
+
['database',['database_access']],
|
|
105
|
+
['postgresql',['database_access']],
|
|
106
|
+
['postgres',['database_access']],
|
|
107
|
+
['mongodb',['database_access']],
|
|
108
|
+
['sqlite',['database_access']],
|
|
109
|
+
['mysql',['database_access']],
|
|
110
|
+
['sql',['database_access']],
|
|
111
|
+
['storage',['database_access']],
|
|
112
|
+
['stored',['database_access']],
|
|
113
|
+
['stores',['database_access']],
|
|
114
|
+
['storing',['database_access']],
|
|
115
|
+
['store',['database_access']],
|
|
116
|
+
['filesystem',['file_system']],
|
|
117
|
+
['files',['file_system']],
|
|
118
|
+
['file',['file_system']],
|
|
119
|
+
['directory',['file_system']],
|
|
120
|
+
['folders',['file_system']],
|
|
121
|
+
['folder',['file_system']],
|
|
122
|
+
['disk',['file_system']],
|
|
123
|
+
['playwright',['browser_automation']],
|
|
124
|
+
['puppeteer',['browser_automation']],
|
|
125
|
+
['selenium',['browser_automation']],
|
|
126
|
+
['scraping',['browser_automation']],
|
|
127
|
+
['scrape',['browser_automation']],
|
|
128
|
+
['browser',['browser_automation']],
|
|
129
|
+
['webhooks',['api_calls']],
|
|
130
|
+
['webhook',['api_calls']],
|
|
131
|
+
['graphql',['api_calls']],
|
|
132
|
+
['apis',['api_calls']],
|
|
133
|
+
['api',['api_calls']],
|
|
134
|
+
['scheduling',['scheduling'],{family:'timer'}],
|
|
135
|
+
['schedules',['scheduling'],{family:'timer'}],
|
|
136
|
+
['schedule',['scheduling'],{family:'timer'}],
|
|
137
|
+
['calendar',['scheduling']],
|
|
138
|
+
['reminders',['scheduling']],
|
|
139
|
+
['reminder',['scheduling']],
|
|
140
|
+
['cron',['scheduling'],{family:'timer'}],
|
|
141
|
+
['notifications',['notifications']],
|
|
142
|
+
['notification',['notifications']],
|
|
143
|
+
['notify',['notifications']],
|
|
144
|
+
['alerts',['notifications']],
|
|
145
|
+
['alert',['notifications']],
|
|
146
|
+
['authentication',['auth_encryption']],
|
|
147
|
+
['encryption',['auth_encryption']],
|
|
148
|
+
['encrypt',['auth_encryption']],
|
|
149
|
+
['passwords',['auth_encryption']],
|
|
150
|
+
['password',['auth_encryption']],
|
|
151
|
+
['oauth',['auth_encryption']],
|
|
152
|
+
['login',['auth_encryption']],
|
|
153
|
+
['auth',['auth_encryption']],
|
|
154
|
+
['analytics',['data_analysis']],
|
|
155
|
+
['analysis',['data_analysis']],
|
|
156
|
+
['analyze',['data_analysis']],
|
|
157
|
+
['statistics',['data_analysis']],
|
|
158
|
+
['statistical',['data_analysis']],
|
|
159
|
+
['dataframe',['data_analysis']],
|
|
160
|
+
['documents',['document_processing']],
|
|
161
|
+
['document',['document_processing']],
|
|
162
|
+
['docx',['document_processing']],
|
|
163
|
+
['pdf',['document_processing']],
|
|
164
|
+
['ocr',['document_processing']],
|
|
165
|
+
['robotics',['robotics']],
|
|
166
|
+
['robots',['robotics']],
|
|
167
|
+
['robot',['robotics']],
|
|
168
|
+
['ros',['robotics']],
|
|
169
|
+
['embeddings',['embedding']],
|
|
170
|
+
['embedding',['embedding']],
|
|
171
|
+
['vectors',['embedding']],
|
|
172
|
+
['vector',['embedding']],
|
|
173
|
+
['searching',['search']],
|
|
174
|
+
['searches',['search']],
|
|
175
|
+
['search',['search']],
|
|
176
|
+
['lookup',['search']],
|
|
177
|
+
['websites',['web']],
|
|
178
|
+
['website',['web']],
|
|
179
|
+
['crawling',['web']],
|
|
180
|
+
['crawl',['web']],
|
|
181
|
+
['html',['web']],
|
|
182
|
+
['url',['web']],
|
|
183
|
+
['web',['web']],
|
|
184
|
+
['datasets',['data']],
|
|
185
|
+
['dataset',['data']],
|
|
186
|
+
['csv',['data']],
|
|
187
|
+
['etl',['data']],
|
|
188
|
+
['kubernetes',['cloud']],
|
|
189
|
+
['azure',['cloud']],
|
|
190
|
+
['cloud',['cloud']],
|
|
191
|
+
['aws',['cloud']],
|
|
192
|
+
['gcp',['cloud']],
|
|
193
|
+
['k8s',['cloud']],
|
|
194
|
+
['payments',['finance']],
|
|
195
|
+
['payment',['finance']],
|
|
196
|
+
['invoices',['finance']],
|
|
197
|
+
['invoice',['finance']],
|
|
198
|
+
['accounting',['finance']],
|
|
199
|
+
['trading',['finance']],
|
|
200
|
+
['stripe',['finance']],
|
|
201
|
+
['finance',['finance']],
|
|
202
|
+
['spreadsheets',['productivity']],
|
|
203
|
+
['spreadsheet',['productivity']],
|
|
204
|
+
['productivity',['productivity']],
|
|
205
|
+
['todos',['productivity']],
|
|
206
|
+
['todo',['productivity']],
|
|
207
|
+
['notes',['productivity']],
|
|
208
|
+
['messaging',['comms']],
|
|
209
|
+
['emails',['comms']],
|
|
210
|
+
['email',['comms']],
|
|
211
|
+
['sms',['comms']],
|
|
212
|
+
['vulnerabilities',['security']],
|
|
213
|
+
['vulnerability',['security']],
|
|
214
|
+
['security',['security']],
|
|
215
|
+
['pentest',['security']],
|
|
216
|
+
['geospatial',['maps']],
|
|
217
|
+
['locations',['maps']],
|
|
218
|
+
['location',['maps']],
|
|
219
|
+
['geocode',['maps']],
|
|
220
|
+
['maps',['maps']],
|
|
221
|
+
['map',['maps']],
|
|
222
|
+
['memories',['memory']],
|
|
223
|
+
['memory',['memory']],
|
|
224
|
+
['remember',['memory']],
|
|
225
|
+
['orchestration',['orchestration']],
|
|
226
|
+
['orchestrate',['orchestration']],
|
|
227
|
+
['workflows',['orchestration']],
|
|
228
|
+
['workflow',['orchestration']],
|
|
229
|
+
['pipelines',['orchestration']],
|
|
230
|
+
['pipeline',['orchestration']],
|
|
231
|
+
['cuda',['gpu_compute']],
|
|
232
|
+
['gpu',['gpu_compute']],
|
|
233
|
+
['mathematics',['math']],
|
|
234
|
+
['equations',['math']],
|
|
235
|
+
['equation',['math']],
|
|
236
|
+
['calculator',['math']],
|
|
237
|
+
['math',['math']],
|
|
238
|
+
['rag',['rag']],
|
|
239
|
+
['telegram',['chat_platforms']],
|
|
240
|
+
['discord',['chat_platforms']],
|
|
241
|
+
['slack',['chat_platforms']],
|
|
242
|
+
['chatbot',['chat_ui']],
|
|
243
|
+
['terminal',['cli']],
|
|
244
|
+
['shell',['cli']],
|
|
245
|
+
['cli',['cli']],
|
|
246
|
+
['benchmarks',['evaluation']],
|
|
247
|
+
['benchmark',['evaluation']],
|
|
248
|
+
['evaluation',['evaluation']],
|
|
249
|
+
['evaluate',['evaluation']],
|
|
250
|
+
['observability',['observability']],
|
|
251
|
+
['monitoring',['observability']],
|
|
252
|
+
['metrics',['observability']],
|
|
253
|
+
['tracing',['observability']],
|
|
254
|
+
['logs',['observability']],
|
|
255
|
+
['rabbitmq',['queues']],
|
|
256
|
+
['queues',['queues']],
|
|
257
|
+
['queue',['queues']],
|
|
258
|
+
['kafka',['queues']],
|
|
259
|
+
['inference',['inference']],
|
|
260
|
+
['vllm',['inference']],
|
|
261
|
+
['simulations',['simulation']],
|
|
262
|
+
['simulation',['simulation']],
|
|
263
|
+
['simulator',['simulation']],
|
|
264
|
+
['simulate',['simulation']],
|
|
265
|
+
['writing',['text']],
|
|
266
|
+
['prompt',['text']],
|
|
267
|
+
['chat',['text']],
|
|
268
|
+
['llm',['text']],
|
|
269
|
+
['text',['text']],
|
|
270
|
+
['coding',['code']],
|
|
271
|
+
['programming',['code']],
|
|
272
|
+
['developer',['code']],
|
|
273
|
+
['software',['code']],
|
|
274
|
+
['code',['code']],
|
|
275
|
+
['sandbox',['code_execution']],
|
|
276
|
+
['interpreter',['code_execution']],
|
|
277
|
+
['repl',['code_execution']],
|
|
278
|
+
// Phone and calls are audio only when an audio cue is in the same clause.
|
|
279
|
+
// Otherwise phone / Android / mobile are device sync, not voice tools.
|
|
280
|
+
['phone call',['audio']],
|
|
281
|
+
['phone calls',['audio']],
|
|
282
|
+
['call audio',['audio']],
|
|
283
|
+
['phone',['audio'],{when:'audio'}],
|
|
284
|
+
['calls',['audio'],{when:'audio'}],
|
|
285
|
+
['call',['audio'],{when:'audio'}],
|
|
286
|
+
['phones',['notifications','comms'],{family:'sync',when:'no-audio'}],
|
|
287
|
+
['phone',['notifications','comms'],{family:'sync',when:'no-audio'}],
|
|
288
|
+
['android',['notifications','comms'],{family:'sync'}],
|
|
289
|
+
['mobile',['notifications','comms'],{family:'sync'}],
|
|
290
|
+
['syncing',['notifications','comms'],{family:'sync'}],
|
|
291
|
+
['synced',['notifications','comms'],{family:'sync'}],
|
|
292
|
+
['syncs',['notifications','comms'],{family:'sync'}],
|
|
293
|
+
['sync',['notifications','comms'],{family:'sync'}],
|
|
294
|
+
['push notification',['notifications','comms'],{family:'sync'}],
|
|
295
|
+
['push notifications',['notifications','comms'],{family:'sync'}],
|
|
296
|
+
['galaxy watch',['data'],{family:'wearable'}],
|
|
297
|
+
['wear os',['data'],{family:'wearable'}],
|
|
298
|
+
['health connect',['data'],{family:'wearable'}],
|
|
299
|
+
['apple health',['data'],{family:'wearable'}],
|
|
300
|
+
['smartwatch',['data'],{family:'wearable'}],
|
|
301
|
+
['smart watch',['data'],{family:'wearable'}],
|
|
302
|
+
['pedometer',['data'],{family:'wearable'}],
|
|
303
|
+
['accelerometer',['data'],{family:'wearable'}],
|
|
304
|
+
['wearable',['data'],{family:'wearable'}],
|
|
305
|
+
['fitness',['data'],{family:'wearable'}],
|
|
306
|
+
['steps',['data'],{family:'wearable',when:'not-device'}],
|
|
307
|
+
['step',['data'],{family:'wearable',when:'not-device'}],
|
|
308
|
+
['watch',['data'],{family:'wearable'}],
|
|
309
|
+
['timers',['scheduling'],{family:'timer'}],
|
|
310
|
+
['timer',['scheduling'],{family:'timer'}],
|
|
311
|
+
['triggers',['scheduling'],{family:'timer',when:'timer'}],
|
|
312
|
+
['trigger',['scheduling'],{family:'timer',when:'timer'}],
|
|
313
|
+
['triggered',['scheduling'],{family:'timer',when:'timer'}],
|
|
314
|
+
['movement',['scheduling'],{family:'timer',when:'movement-timer'}],
|
|
315
|
+
['movement',['data'],{family:'wearable',when:'movement-sensor'}],
|
|
316
|
+
['calories',['text'],{family:'nutrition'}],
|
|
317
|
+
['calorie',['text'],{family:'nutrition'}],
|
|
318
|
+
['nutrition',['text'],{family:'nutrition'}],
|
|
319
|
+
['recipes',['text'],{family:'nutrition'}],
|
|
320
|
+
['recipe',['text'],{family:'nutrition'}],
|
|
321
|
+
['meals',['text'],{family:'nutrition'}],
|
|
322
|
+
['meal',['text'],{family:'nutrition'}],
|
|
323
|
+
['diets',['text'],{family:'nutrition'}],
|
|
324
|
+
['diet',['text'],{family:'nutrition'}],
|
|
325
|
+
['foods',['text'],{family:'nutrition'}],
|
|
326
|
+
['food',['text'],{family:'nutrition'}],
|
|
327
|
+
['facebook',['comms'],{family:'social'}],
|
|
328
|
+
['instagram',['comms'],{family:'social'}],
|
|
329
|
+
['twitter',['comms'],{family:'social'}],
|
|
330
|
+
['shares',['comms'],{family:'social',when:'social'}],
|
|
331
|
+
['shared',['comms'],{family:'social',when:'social'}],
|
|
332
|
+
['sharing',['comms'],{family:'social',when:'social'}],
|
|
333
|
+
['share',['comms'],{family:'social',when:'social'}],
|
|
334
|
+
['posting',['comms'],{family:'social',when:'social'}],
|
|
335
|
+
['posts',['comms'],{family:'social',when:'social'}],
|
|
336
|
+
['posted',['comms'],{family:'social',when:'social'}],
|
|
337
|
+
['post',['comms'],{family:'social',when:'social'}],
|
|
338
|
+
['x',['comms'],{family:'social',when:'social-x'}]
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
export const KEYWORD_RULES=RULE_LIST.map(([phrase,modalities,meta])=>({phrase,modalities:[...modalities],...(meta||{})}))
|
|
342
|
+
.sort((a,b)=>b.phrase.length-a.phrase.length||a.phrase.localeCompare(b.phrase)||(a.when||'').localeCompare(b.when||''));
|
|
343
|
+
|
|
344
|
+
const CLAUSE_SPLIT=/\s*(?:,|;|&)\s*|\s+\b(?:and|then)\b\s+/i;
|
|
345
|
+
const EDGE_STOP=new Set(['a','an','the','that','which','who','to','for','of','in','on','with','and','then','into','from','by','or','also']);
|
|
346
|
+
|
|
347
|
+
function bounded(text,start,end){
|
|
348
|
+
const before=start===0||!/[\p{L}\p{N}_]/u.test(text[start-1]);
|
|
349
|
+
const after=end===text.length||!/[\p{L}\p{N}_]/u.test(text[end]);
|
|
350
|
+
return before&&after;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const AUDIO_CUE=/\b(?:transcri(?:be|bes|bed|bing|ption|ptions)?|voices?|speech|audio|whisper|microphones?|asr|listen(?:s|ing|ed)?|phone calls?|call audio)\b/i;
|
|
354
|
+
const DEVICE_CUE=/\b(?:android|mobile|phones?|sync(?:ing|ed|s)?)\b/i;
|
|
355
|
+
const TIMER_CUE=/\b(?:timers?|schedul\w*|cron|triggers?|triggered)\b/i;
|
|
356
|
+
const DB_CUE=/\b(?:databases?|sql|postgres(?:ql)?|mysql|sqlite|mongodb)\b/i;
|
|
357
|
+
const SOCIAL_NET=/\b(?:facebook|instagram|twitter)\b/i;
|
|
358
|
+
const SOCIAL_X=/(?:\bx\.com\b|\b(?:post|share|tweet)(?:s|ed|ing)?\b[^.!?\n]{0,40}\bx\b|\bx\b[^.!?\n]{0,40}\b(?:post|share|tweet)(?:s|ed|ing)?\b)/i;
|
|
359
|
+
|
|
360
|
+
function ruleApplies(rule,text){
|
|
361
|
+
switch(rule.when){
|
|
362
|
+
case 'audio':return AUDIO_CUE.test(text);
|
|
363
|
+
case 'no-audio':return !AUDIO_CUE.test(text);
|
|
364
|
+
case 'social':return SOCIAL_NET.test(text)||SOCIAL_X.test(text);
|
|
365
|
+
case 'social-x':return SOCIAL_X.test(text);
|
|
366
|
+
case 'timer':return TIMER_CUE.test(text)&&!DB_CUE.test(text);
|
|
367
|
+
case 'movement-timer':return TIMER_CUE.test(text);
|
|
368
|
+
case 'movement-sensor':return !TIMER_CUE.test(text);
|
|
369
|
+
case 'not-device':return !DEVICE_CUE.test(text);
|
|
370
|
+
default:return true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function findKeywordHits(text){
|
|
375
|
+
const lower=String(text||'').toLowerCase();
|
|
376
|
+
const found=[];
|
|
377
|
+
for(const rule of KEYWORD_RULES){
|
|
378
|
+
if(!ruleApplies(rule,lower))continue;
|
|
379
|
+
let from=0;
|
|
380
|
+
while(from<=lower.length-rule.phrase.length){
|
|
381
|
+
const i=lower.indexOf(rule.phrase,from);
|
|
382
|
+
if(i<0)break;
|
|
383
|
+
const end=i+rule.phrase.length;
|
|
384
|
+
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,family:rule.family||null});
|
|
385
|
+
from=i+1;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
found.sort((a,b)=>a.start-b.start||a.end-b.end);
|
|
389
|
+
return found;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function cleanPart(text){
|
|
393
|
+
const bits=String(text||'').replace(/\s+/g,' ').trim().replace(/^[,;:.]+|[,;:.]+$/g,'').split(' ').filter(Boolean);
|
|
394
|
+
while(bits.length&&EDGE_STOP.has(bits[0].toLowerCase()))bits.shift();
|
|
395
|
+
while(bits.length&&EDGE_STOP.has(bits[bits.length-1].toLowerCase()))bits.pop();
|
|
396
|
+
return bits.join(' ');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function overlaps(a,b){
|
|
400
|
+
return a.some(m=>b.includes(m));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function clauseLabel(clause){
|
|
404
|
+
return cleanPart(clause)||String(clause||'').replace(/\s+/g,' ').trim();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function partsFromClause(clause){
|
|
408
|
+
const hits=findKeywordHits(clause);
|
|
409
|
+
if(!hits.length){
|
|
410
|
+
const text=clauseLabel(clause);
|
|
411
|
+
return text?[{text,modalities:[],keywords:[],family:null}]:[];
|
|
412
|
+
}
|
|
413
|
+
const groups=[];
|
|
414
|
+
for(const hit of hits){
|
|
415
|
+
const last=groups[groups.length-1];
|
|
416
|
+
if(last&&overlaps(last.modalities,hit.modalities)){
|
|
417
|
+
last.hits.push(hit);
|
|
418
|
+
for(const m of hit.modalities)if(!last.modalities.includes(m))last.modalities.push(m);
|
|
419
|
+
if(!last.keywords.includes(hit.phrase))last.keywords.push(hit.phrase);
|
|
420
|
+
if(hit.family&&!last.family)last.family=hit.family;
|
|
421
|
+
}else groups.push({modalities:[...hit.modalities],keywords:[hit.phrase],hits:[hit],family:hit.family||null});
|
|
422
|
+
}
|
|
423
|
+
if(groups.length===1){
|
|
424
|
+
const text=clauseLabel(clause)||groups[0].keywords.join(' ');
|
|
425
|
+
return [{text,modalities:groups[0].modalities,keywords:groups[0].keywords,family:groups[0].family}];
|
|
426
|
+
}
|
|
427
|
+
return groups.map((group,i)=>{
|
|
428
|
+
const start=i===0?0:splitPoint(clause,groups[i-1].hits.at(-1).end,group.hits[0].start);
|
|
429
|
+
const end=i===groups.length-1?clause.length:splitPoint(clause,group.hits.at(-1).end,groups[i+1].hits[0].start);
|
|
430
|
+
const text=clauseLabel(clause.slice(start,end))||group.keywords.join(' ');
|
|
431
|
+
return {text,modalities:group.modalities,keywords:group.keywords,family:group.family};
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function splitPoint(clause,leftEnd,rightStart){
|
|
436
|
+
const gap=clause.slice(leftEnd,rightStart);
|
|
437
|
+
const rel=gap.lastIndexOf(' ');
|
|
438
|
+
return rel>=0?leftEnd+rel+1:Math.floor((leftEnd+rightStart)/2);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export function decomposeGoal(goal){
|
|
442
|
+
const text=String(goal||'').replace(/\s+/g,' ').trim();
|
|
443
|
+
if(!text)return [];
|
|
444
|
+
const parts=[];
|
|
445
|
+
for(const clause of text.split(CLAUSE_SPLIT).map(s=>s.trim()).filter(Boolean))parts.push(...partsFromClause(clause));
|
|
446
|
+
return parts;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function packageVersion(){
|
|
450
|
+
return JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function installCommand(bot){
|
|
454
|
+
const eps=localEndpoints(bot);
|
|
455
|
+
for(const ep of eps)if(ep.command)return ep.command;
|
|
456
|
+
for(const ep of eps)if(ep.url)return ep.url;
|
|
457
|
+
return bot.endpoint||'';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function oneLine(bot){
|
|
461
|
+
const caps=(bot.caps||[]).map(c=>String(c).replace(/^cap\./,'').replace(/^mcp\./,'').replace(/[._]+/g,' ').trim()).filter(c=>c&&c.toLowerCase()!=='connect');
|
|
462
|
+
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';
|
|
463
|
+
const detail=caps.length?caps.slice(0,4).join(', '):(bot.modalities||[]).slice(0,3).join(', ');
|
|
464
|
+
const line=detail?`${bot.name} — ${kind}; ${detail}`:`${bot.name} — ${kind}`;
|
|
465
|
+
return line.replace(/\s+/g,' ').slice(0,180);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function whyLine(modality,keywords,tail='top-ranked in modality'){
|
|
469
|
+
const hint=(keywords||[]).filter(k=>KEYWORD_RULES.some(r=>r.phrase===k&&r.modalities.includes(modality))).slice(0,4).join('/');
|
|
470
|
+
return hint?`matched ${modality}/${hint}; ${tail}`:`matched ${modality}; ${tail}`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const KIND_CATALOG='catalog integration';
|
|
474
|
+
const KIND_SUGGEST='suggestion: build yourself / platform API';
|
|
475
|
+
const OPTIONAL_COVERAGE=new Set(['share','shares','shared','sharing','post','posts','posting','posted','send','sends','sending','sent','detect','detects','detecting','start','starts','starting','create','creates','use','uses','using','based','optionally','optional','results','data','experience','that','them']);
|
|
476
|
+
const SUGGEST={
|
|
477
|
+
wearable:'Wear OS sensor APIs and Health Connect are not in the kloudy 1.0 catalog. Build the sensing yourself or call the platform API. Unverified non-catalog guidance, not a catalog bot.',
|
|
478
|
+
sync:'Health Connect is not in the kloudy 1.0 catalog. It is a platform API for sending step data to an Android phone. Unverified non-catalog guidance, not a catalog bot.',
|
|
479
|
+
social:'The Facebook Graph API, Instagram API, and X API are not catalog bots. Unverified non-catalog guidance. Build the post yourself or call the platform API.'
|
|
480
|
+
};
|
|
481
|
+
const WEAK_REASON={
|
|
482
|
+
wearable:'Nearest catalog bots match a related fitness wearable (Garmin, Fitbit, or Apple Health), not Galaxy Watch step sensing.',
|
|
483
|
+
timer:'Catalog timers or schedulers share a timer or schedule keyword, but none start a timer from movement.',
|
|
484
|
+
sync:'Catalog Android tools share the Android keyword, but they do not send step data to a phone.',
|
|
485
|
+
nutrition:'Catalog nutrition tools only partly overlap this meal or calorie need.',
|
|
486
|
+
social:'Catalog social tools only partly overlap this post or share need.'
|
|
487
|
+
};
|
|
488
|
+
const CAP_NOISE=new Set(['connect','mcp','llm','language','agents','agent','scheduling','notifications','notification','api','calls','call','data','text','code','web','database','audio','video','image','memory','search','orchestration','skill','vision','browser','filesystem','security','cloud','comms']);
|
|
489
|
+
const SYNC_DENY=/\b(?:figma|frida|linkedin|phonepe|phonehome|phone-a-friend|phone-harness)\b/i;
|
|
490
|
+
const WATCH_DENY=/opencode|watchtower|framewatch|filewatch|firewatch|cloudwatch|swatch|cybersec|rewatch|langwatch|webwatch|vibewatch/i;
|
|
491
|
+
|
|
492
|
+
function contentKeywords(keywords){
|
|
493
|
+
const out=[];
|
|
494
|
+
for(const keyword of keywords||[]){
|
|
495
|
+
const text=String(keyword||'').toLowerCase();
|
|
496
|
+
if(!text||OPTIONAL_COVERAGE.has(text)||out.includes(text))continue;
|
|
497
|
+
out.push(text);
|
|
498
|
+
}
|
|
499
|
+
return out;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function botBlob(bot){
|
|
503
|
+
const caps=(bot.caps||[]).map(c=>String(c).replace(/^cap\./,'').replace(/^mcp\./,''));
|
|
504
|
+
return `${bot.name||''} ${bot.id||''} ${caps.join(' ')}`.toLowerCase();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function botTokens(bot){
|
|
508
|
+
return new Set(words(botBlob(bot)).filter(token=>!CAP_NOISE.has(token)));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function tokenCovers(tokens,raw,keyword){
|
|
512
|
+
const key=String(keyword||'').toLowerCase();
|
|
513
|
+
if(!key)return false;
|
|
514
|
+
if(key==='x')return tokens.has('twitter')||/(^|[^a-z0-9])x([^a-z0-9]|$)/.test(raw);
|
|
515
|
+
if(key.includes(' '))return raw.includes(key);
|
|
516
|
+
if(tokens.has(key))return true;
|
|
517
|
+
if(key.length>=6&&raw.includes(key))return true;
|
|
518
|
+
if(key.length>=4){
|
|
519
|
+
for(const token of tokens)if(token.startsWith(key)&&token.length<=key.length+3)return true;
|
|
520
|
+
}
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function timerToken(token){
|
|
525
|
+
if(token==='timer'||token==='timers')return true;
|
|
526
|
+
if(token.startsWith('schedule')||token.includes('scheduler'))return true;
|
|
527
|
+
if(token==='cron'||(token.startsWith('cron')&&!token.startsWith('cronomet')&&token.length<=10))return true;
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function familyHit(family,tokens,raw,keywords){
|
|
532
|
+
if(family==='wearable'){
|
|
533
|
+
const asked=(keywords||[]).join(' ').toLowerCase();
|
|
534
|
+
if(!/watch|step|fitness|pedometer|accelerometer|wear|health|garmin|fitbit|sensor/.test(asked))return null;
|
|
535
|
+
if(WATCH_DENY.test(raw))return null;
|
|
536
|
+
if([...tokens].some(token=>['garmin','fitbit','pedometer','accelerometer','smartwatch','wearable','fitness'].includes(token)))return 'related';
|
|
537
|
+
if(/apple-health|applehealth|health-connect|healthconnect|wear-os|wearos|galaxy-watch/.test(raw))return 'related';
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
if(family==='timer'){
|
|
541
|
+
const want=new Set((keywords||[]).map(keyword=>keyword.toLowerCase()));
|
|
542
|
+
if([...tokens].some(timerToken))return [...tokens].some(token=>token==='timer'||token==='timers')?'direct':'related';
|
|
543
|
+
if((want.has('trigger')||want.has('triggers')||want.has('triggered'))&&[...tokens].some(token=>token==='trigger'||token.startsWith('trigger')))return 'direct';
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
if(family==='sync'){
|
|
547
|
+
if(SYNC_DENY.test(raw))return null;
|
|
548
|
+
if([...tokens].some(token=>token==='android'||token==='adb'))return 'direct';
|
|
549
|
+
const want=new Set((keywords||[]).map(keyword=>keyword.toLowerCase()));
|
|
550
|
+
if(want.has('mobile')&&tokens.has('mobile'))return 'related';
|
|
551
|
+
if((want.has('phone')||want.has('phones'))&&(tokens.has('android')||tokens.has('adb')||tokens.has('sms')||[...tokens].some(token=>token.startsWith('push')||token.startsWith('notif'))))return 'related';
|
|
552
|
+
if((want.has('sync')||want.has('syncs')||want.has('synced')||want.has('syncing'))&&tokens.has('sync'))return 'related';
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
if(family==='nutrition'){
|
|
556
|
+
const want=new Set((keywords||[]).map(keyword=>keyword.toLowerCase()));
|
|
557
|
+
const allowed=new Set(['calorie','calories','nutrition','meal','meals','mealie','diet','diets','recipe','recipes']);
|
|
558
|
+
if(want.has('food')||want.has('foods')){allowed.add('food');allowed.add('foods');}
|
|
559
|
+
const hit=[...tokens].some(token=>allowed.has(token)||(allowed.has('meal')&&token.startsWith('meal')&&token.length<=7)||token.startsWith('calorie'));
|
|
560
|
+
return hit?'related':null;
|
|
561
|
+
}
|
|
562
|
+
if(family==='social'){
|
|
563
|
+
const want=new Set((keywords||[]).map(keyword=>keyword.toLowerCase()));
|
|
564
|
+
const nets=['facebook','instagram','twitter'].filter(net=>want.has(net)||(net==='twitter'&&want.has('x')));
|
|
565
|
+
const have=[...tokens].filter(token=>token==='facebook'||token==='instagram'||token==='twitter');
|
|
566
|
+
if(!have.length)return null;
|
|
567
|
+
if(nets.length&&!nets.some(net=>have.includes(net)))return null;
|
|
568
|
+
return 'direct';
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function overrideRank(scores,id){
|
|
574
|
+
const action=scores?.bots?.[id]?.action;
|
|
575
|
+
if(action==='boost')return -1;
|
|
576
|
+
if(action==='demote')return 1;
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function excluded(scores,id){
|
|
581
|
+
return scores?.bots?.[id]?.action==='exclude';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export function parseLimit(value){
|
|
585
|
+
if(value==null||value==='')return DEFAULT_LIMIT;
|
|
586
|
+
const text=String(value);
|
|
587
|
+
if(!/^\d+$/.test(text))throw new Error('Limit must be an integer from 1 to 20');
|
|
588
|
+
const n=Number(text);
|
|
589
|
+
if(!Number.isInteger(n)||n<MIN_LIMIT||n>MAX_LIMIT)throw new Error('Limit must be an integer from 1 to 20');
|
|
590
|
+
return n;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function stackIdFor(parts,unmatched=[]){
|
|
594
|
+
const lines=[];
|
|
595
|
+
for(const part of parts){
|
|
596
|
+
lines.push('part:'+part.part);
|
|
597
|
+
for(const bot of part.bots)lines.push('bot:'+bot.id);
|
|
598
|
+
}
|
|
599
|
+
for(const item of unmatched)lines.push('unmatched:'+item.part);
|
|
600
|
+
return createHash('sha256').update(lines.join('\n')).digest('hex');
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export function feedbackLine(stackId){
|
|
604
|
+
return `kloudy feedback --stack ${stackId} --bot <id> --result worked|failed|partial [--reason <short>]`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function selectFamily(bots,piece,limit,scores){
|
|
608
|
+
const want=piece.modalities||[];
|
|
609
|
+
const pool=[];
|
|
610
|
+
for(let i=0;i<bots.length;i++){
|
|
611
|
+
const bot=bots[i];
|
|
612
|
+
if(!installCommand(bot)||excluded(scores,bot.id))continue;
|
|
613
|
+
const raw=botBlob(bot);
|
|
614
|
+
const tokens=botTokens(bot);
|
|
615
|
+
const hit=familyHit(piece.family,tokens,raw,piece.keywords);
|
|
616
|
+
if(!hit)continue;
|
|
617
|
+
const need=contentKeywords(piece.keywords);
|
|
618
|
+
const hits=need.filter(keyword=>tokenCovers(tokens,raw,keyword)).length;
|
|
619
|
+
pool.push({b:bot,i,hits,direct:hit==='direct'||need.some(keyword=>tokenCovers(tokens,raw,keyword))?1:0});
|
|
620
|
+
}
|
|
621
|
+
const tier=cls=>CLASS_TIER[cls]??9;
|
|
622
|
+
pool.sort((x,y)=>y.direct-x.direct||tier(x.b.cls)-tier(y.b.cls)||overrideRank(scores,x.b.id)-overrideRank(scores,y.b.id)||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)));
|
|
623
|
+
const chosen=pool.slice(0,limit).map(x=>x.b);
|
|
624
|
+
const need=contentKeywords(piece.keywords);
|
|
625
|
+
const confidence=!chosen.length?'none':!need.length||need.every(keyword=>chosen.some(bot=>tokenCovers(botTokens(bot),botBlob(bot),keyword)))?'match':'weak';
|
|
626
|
+
return {bots:chosen,confidence,candidatesTotal:pool.length};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function botItem(bot,modality,keywords,tail){
|
|
630
|
+
return {
|
|
631
|
+
id:bot.id,
|
|
632
|
+
name:bot.name,
|
|
633
|
+
class:bot.cls,
|
|
634
|
+
command:installCommand(bot),
|
|
635
|
+
license:bot.license||'',
|
|
636
|
+
description:oneLine(bot),
|
|
637
|
+
why:whyLine(modality,keywords,tail),
|
|
638
|
+
modalities:[modality]
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function rankModality(bots,modality,keywords,scores){
|
|
643
|
+
const terms=[...new Set((keywords||[]).flatMap(words))];
|
|
644
|
+
const want=[modality];
|
|
645
|
+
const pool=[];
|
|
646
|
+
for(let i=0;i<bots.length;i++){
|
|
647
|
+
const b=bots[i];
|
|
648
|
+
if(!(b.modalities||[]).includes(modality)||excluded(scores,b.id))continue;
|
|
649
|
+
const blob=(b.name+' '+b.id+' '+(b.caps||[]).join(' ')).toLowerCase();
|
|
650
|
+
pool.push({b,i,hits:terms.filter(t=>blob.includes(t)).length});
|
|
651
|
+
}
|
|
652
|
+
const tier=cls=>CLASS_TIER[cls]??9;
|
|
653
|
+
pool.sort((x,y)=>tier(x.b.cls)-tier(y.b.cls)||overrideRank(scores,x.b.id)-overrideRank(scores,y.b.id)||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)));
|
|
654
|
+
return pool.map(x=>x.b);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function recordPiece(piece,bots,confidence,parts,unmatched,gaps,candidatesTotal=bots.length){
|
|
658
|
+
if(bots.length)parts.push({part:piece.text,modalities:piece.modalities,confidence,candidatesTotal,bots});
|
|
659
|
+
else unmatched.push({part:piece.text,reason:'no modality or catalog match'});
|
|
660
|
+
if(confidence==='match')return;
|
|
661
|
+
if(bots.length)gaps.push({part:piece.text,confidence,reason:WEAK_REASON[piece.family]||'Catalog hits only partly overlap this part.',kind:KIND_CATALOG});
|
|
662
|
+
const suggestKind=piece.family==='wearable'||piece.family==='sync'||piece.family==='social';
|
|
663
|
+
if(suggestKind&&SUGGEST[piece.family])gaps.push({part:piece.text,confidence:'none',reason:SUGGEST[piece.family],kind:KIND_SUGGEST});
|
|
664
|
+
else if(!bots.length)gaps.push({part:piece.text,confidence:'none',reason:'no modality or catalog match'});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function recommendGoal(goal,options={}){
|
|
668
|
+
const source=options.bots?{bots:options.bots}:loadLocal();
|
|
669
|
+
if(!source?.bots)throw new Error('No local catalog. Run `kloudy bots sync`.');
|
|
670
|
+
const limit=parseLimit(options.limit??options.perModality??PER_MODALITY);
|
|
671
|
+
const scores=normalizeScores(options.scores);
|
|
672
|
+
const pieces=decomposeGoal(goal);
|
|
673
|
+
const parts=[];
|
|
674
|
+
const unmatched=[];
|
|
675
|
+
const gaps=[];
|
|
676
|
+
const consider=pieces.length?pieces:[{text:String(goal||'').trim(),modalities:[],keywords:[],family:null}];
|
|
677
|
+
for(const piece of consider){
|
|
678
|
+
if(!piece.text)continue;
|
|
679
|
+
if(piece.family){
|
|
680
|
+
const picked=selectFamily(source.bots,piece,limit,scores);
|
|
681
|
+
const modality=piece.modalities[0];
|
|
682
|
+
const bots=picked.bots.map(bot=>botItem(bot,modality,piece.keywords,'catalog name search'));
|
|
683
|
+
for(const bot of bots)bot.modalities=[...piece.modalities];
|
|
684
|
+
recordPiece(piece,bots,picked.confidence,parts,unmatched,gaps,picked.candidatesTotal);
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if(!piece.modalities.length){
|
|
688
|
+
recordPiece(piece,[], 'none',parts,unmatched,gaps,0);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const seen=new Map();
|
|
692
|
+
const ranked=[];
|
|
693
|
+
for(const modality of piece.modalities){
|
|
694
|
+
for(const bot of rankModality(source.bots,modality,piece.keywords,scores)){
|
|
695
|
+
const prev=seen.get(bot.id);
|
|
696
|
+
if(prev){
|
|
697
|
+
if(!prev.modalities.includes(modality))prev.modalities.push(modality);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const item={bot,modalities:[modality]};
|
|
701
|
+
seen.set(bot.id,item);
|
|
702
|
+
ranked.push(item);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const bots=ranked.slice(0,limit).map(({bot,modalities})=>{
|
|
706
|
+
const item=botItem(bot,modalities[0],piece.keywords);
|
|
707
|
+
item.modalities=modalities;
|
|
708
|
+
return item;
|
|
709
|
+
});
|
|
710
|
+
recordPiece(piece,bots,bots.length?'match':'none',parts,unmatched,gaps,ranked.length);
|
|
711
|
+
}
|
|
712
|
+
const complete=parts.length>0&&parts.every(part=>part.confidence==='match')&&gaps.length===0&&unmatched.length===0;
|
|
713
|
+
const stackId=stackIdFor(parts,unmatched);
|
|
714
|
+
return {schema:RECOMMEND_SCHEMA,catalog:CATALOG_NAME,version:options.version||packageVersion(),goal:String(goal??''),limit,stackId,feedback:feedbackLine(stackId),complete,parts,unmatched,gaps};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function formatRecommend(result){
|
|
718
|
+
const lines=[`${result.catalog} · package ${result.version}`,`Goal: ${result.goal}`,`complete=${result.complete?'true':'false'}`,`limit=${result.limit}`,`stackId: ${result.stackId}`,`feedback: ${result.feedback}`];
|
|
719
|
+
result.parts.forEach((part,index)=>{
|
|
720
|
+
lines.push('',`${index+1}. ${part.part}`,`Modalities: ${part.modalities.join(', ')}`,`Confidence: ${part.confidence}`,`Candidates: ${part.candidatesTotal}`);
|
|
721
|
+
part.bots.forEach((bot,n)=>{
|
|
722
|
+
lines.push(` ${n+1}) ${bot.id} [${bot.class}] ${bot.license||'unknown license'}`,` ${bot.command}`,` ${bot.description}`,` why: ${bot.why}`);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
if(result.unmatched.length){
|
|
726
|
+
lines.push('','Unmatched');
|
|
727
|
+
for(const item of result.unmatched)lines.push(`- ${item.part}: ${item.reason}`);
|
|
728
|
+
}
|
|
729
|
+
lines.push('','Gaps');
|
|
730
|
+
if(!result.gaps.length)lines.push('(none)');
|
|
731
|
+
else for(const gap of result.gaps)lines.push(`- ${gap.part} [${gap.confidence}]${gap.kind?` ${gap.kind}:`:''} ${gap.reason}`);
|
|
732
|
+
return lines.join('\n');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export async function recommendCommand(args,opts={}){
|
|
736
|
+
const out=opts.out||(s=>console.log(s));
|
|
737
|
+
const o=flags(args);
|
|
738
|
+
const goal=o._.join(' ').trim();
|
|
739
|
+
if(!goal)throw new Error('Usage: kloudy recommend <goal> [--json] [--limit N]');
|
|
740
|
+
const limit=parseLimit(o.limit);
|
|
741
|
+
const scores=opts.scores!==undefined?opts.scores:await loadScoreOverrides({offline:!!o.offline,fetchImpl:opts.fetch,now:opts.now,dir:opts.dataDir,url:opts.scoresUrl});
|
|
742
|
+
const result=recommendGoal(goal,{limit,scores});
|
|
743
|
+
out(o.json?JSON.stringify(result,null,2):formatRecommend(result));
|
|
744
|
+
return result;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function agentsDocument(){
|
|
748
|
+
return readFileSync(new URL('../AGENTS.md',import.meta.url),'utf8');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
export function agentsCommand(opts={}){
|
|
752
|
+
const out=opts.out||(s=>console.log(s));
|
|
753
|
+
const text=agentsDocument().replace(/\s*$/,'');
|
|
754
|
+
out(text);
|
|
755
|
+
return text;
|
|
756
|
+
}
|