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/src/scores.mjs ADDED
@@ -0,0 +1,80 @@
1
+ // Optional recommend score overrides. No model call.
2
+ // GET https://kloudy.ai/api/feedback/scores, cache about 24h, else bundled ranking.
3
+ import {existsSync,mkdirSync,readFileSync,writeFileSync} from 'node:fs';
4
+ import {homedir} from 'node:os';
5
+ import {join} from 'node:path';
6
+
7
+ export const SCORES_URL='https://kloudy.ai/api/feedback/scores';
8
+ export const SCORES_SCHEMA='kloudy.feedback-scores/1';
9
+ export const SCORE_TTL_MS=24*60*60*1000;
10
+ const ACTIONS=new Set(['boost','demote','exclude']);
11
+ const DISABLED=new Set(['','off','offline','0','false']);
12
+
13
+ export function dataDir(){
14
+ return process.env.KLOUDY_DATA_DIR||join(homedir(),'.kloudy');
15
+ }
16
+
17
+ export function scoresCachePath(dir=dataDir()){
18
+ return join(dir,'feedback-scores-cache.json');
19
+ }
20
+
21
+ function botsOf(body){
22
+ if(body.bots&&typeof body.bots==='object'&&!Array.isArray(body.bots))return body.bots;
23
+ const scores=body.scores;
24
+ if(!scores||typeof scores!=='object'||Array.isArray(scores))return null;
25
+ if(scores.bots&&typeof scores.bots==='object'&&!Array.isArray(scores.bots))return scores.bots;
26
+ return scores;
27
+ }
28
+
29
+ export function normalizeScores(body){
30
+ if(!body||typeof body!=='object'||Array.isArray(body))return null;
31
+ const source=botsOf(body);
32
+ if(!source)return null;
33
+ const bots={};
34
+ for(const [id,row] of Object.entries(source)){
35
+ if(typeof id!=='string'||!id)continue;
36
+ const action=typeof row==='string'?row:row&&row.action;
37
+ if(ACTIONS.has(action))bots[id]={action};
38
+ }
39
+ return {schema:typeof body.schema==='string'?body.schema:SCORES_SCHEMA,bots};
40
+ }
41
+
42
+ function scoresUrl(explicit){
43
+ const value=explicit??process.env.KLOUDY_SCORES_URL??SCORES_URL;
44
+ return DISABLED.has(String(value).trim())?null:value;
45
+ }
46
+
47
+ function readCache(file,now){
48
+ try{
49
+ if(!existsSync(file))return null;
50
+ const wrapped=JSON.parse(readFileSync(file,'utf8'));
51
+ const scores=normalizeScores(wrapped.scores??wrapped);
52
+ const fetchedAt=Number(wrapped.fetchedAt);
53
+ if(!scores||!Number.isFinite(fetchedAt))return null;
54
+ return {fetchedAt,scores,fresh:now-fetchedAt>=0&&now-fetchedAt<SCORE_TTL_MS};
55
+ }catch{
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function writeCache(file,fetchedAt,scores){
61
+ mkdirSync(join(file,'..'),{recursive:true});
62
+ writeFileSync(file,JSON.stringify({fetchedAt,scores})+'\n');
63
+ }
64
+
65
+ export async function loadScoreOverrides({fetchImpl=fetch,now=Date.now(),offline=false,dir=dataDir(),url}={}){
66
+ const cached=readCache(scoresCachePath(dir),now);
67
+ if(cached?.fresh)return cached.scores;
68
+ const endpoint=scoresUrl(url);
69
+ if(offline||!endpoint)return null;
70
+ try{
71
+ const response=await fetchImpl(endpoint,{method:'GET',redirect:'error',credentials:'omit',headers:{accept:'application/json'},signal:AbortSignal.timeout(4000)});
72
+ if(!response?.ok)return null;
73
+ const scores=normalizeScores(await response.json());
74
+ if(!scores)return null;
75
+ writeCache(scoresCachePath(dir),now,scores);
76
+ return scores;
77
+ }catch{
78
+ return null;
79
+ }
80
+ }