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.
- package/AGENTS.md +54 -0
- package/LICENSE +202 -202
- package/MCP-DEFAULT.md +12 -12
- package/README.md +375 -315
- package/bin/entry.mjs +25 -22
- package/bin/kloudy.mjs +80 -71
- package/package.json +4 -2
- package/schemas/SDF-LICENSE.txt +21 -21
- package/schemas/kloudy.deployable-bot.schema.json +80 -0
- package/schemas/sdf-document-0.2.schema.json +411 -411
- package/schemas/sdf-webpage-v1.schema.json +176 -176
- package/src/activity.mjs +23 -23
- package/src/approval-ui.mjs +2 -2
- package/src/approval.mjs +25 -25
- package/src/autoupdate.mjs +2 -2
- package/src/bots.mjs +144 -0
- package/src/chooser-link.mjs +17 -17
- package/src/client.mjs +96 -96
- package/src/connect-card.mjs +32 -32
- package/src/connect-ui.mjs +3 -3
- package/src/connection.mjs +25 -25
- package/src/conversation.mjs +53 -53
- package/src/deployable-bots.json.gz +0 -0
- package/src/editor-install.mjs +79 -79
- package/src/host-interface.mjs +10 -10
- package/src/hosted.mjs +42 -42
- package/src/httpk.mjs +66 -66
- package/src/install.mjs +35 -35
- package/src/introduce.mjs +33 -33
- package/src/mcp.mjs +74 -74
- package/src/modalities.mjs +28 -0
- package/src/models.mjs +55 -55
- package/src/preferences-card.mjs +21 -21
- package/src/preferences-ui.mjs +2 -2
- package/src/product-actions.mjs +22 -22
- package/src/public.mjs +41 -41
- package/src/query.mjs +28 -28
- package/src/recommend.mjs +478 -0
- package/src/replies.mjs +7 -7
- package/src/reply-templates.json +12 -12
- package/src/sample-actions.mjs +31 -31
- package/src/sample-card.mjs +11 -11
- package/src/sample-cards.mjs +47 -47
- package/src/sample-ui.mjs +2 -2
- package/src/sdf-budget.mjs +23 -23
- package/src/sdf.mjs +12 -12
- package/src/starter.mjs +43 -43
- package/src/status-indicator.css +5 -5
- package/src/strings.mjs +51 -51
- package/src/terminal-status.mjs +6 -6
- package/src/website-return.mjs +18 -18
- package/src/welcome.mjs +8 -8
- package/src/wrap.mjs +58 -58
package/src/product-actions.mjs
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
// Product-side validation only. The single HTTPK remains the execution authority.
|
|
2
|
-
const bounded=(value,max)=>typeof value==='string'&&value.length>0&&value.length<=max;
|
|
3
|
-
const identifier=value=>bounded(value,80)&&/^[a-zA-Z0-9_.:-]+$/.test(value);
|
|
4
|
-
export function actionInput(action,inputs){
|
|
5
|
-
if(!inputs||typeof inputs!=='object'||Array.isArray(inputs))throw Error('invalid_action_input');
|
|
6
|
-
const schemas={
|
|
7
|
-
'chain.head':{chain:v=>bounded(v,80)},
|
|
8
|
-
'wallet.prepare':{chain:v=>bounded(v,100),destination_chain:v=>bounded(v,100),sender:v=>bounded(v,100),recipient:v=>bounded(v,100),amount_atomic:v=>bounded(v,100)&&/^[1-9]\d*$/.test(v)},
|
|
9
|
-
'bww.fetch':{project:identifier,fragment_ids:v=>Array.isArray(v)&&v.length>=1&&v.length<=8&&new Set(v).size===v.length&&v.every(identifier),allow_html:v=>typeof v==='boolean'},
|
|
10
|
-
'bww.publish':{project:identifier,fragment_id:identifier,title:v=>bounded(v,120),text:v=>bounded(v,8000),html:v=>v===null||typeof v==='string'&&v.length<=12000,advertisement:v=>v===null||v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).length===2&&bounded(v.provider,80)&&bounded(v.text,500)}
|
|
11
|
-
};
|
|
12
|
-
const schema=schemas[action];
|
|
13
|
-
if(!schema||Object.keys(inputs).length!==Object.keys(schema).length||!Object.entries(schema).every(([key,validate])=>Object.hasOwn(inputs,key)&&validate(inputs[key])))throw Error('invalid_action_input');
|
|
14
|
-
return {type:'action',action,inputs};
|
|
15
|
-
}
|
|
16
|
-
export function hostingParams(budget){
|
|
17
|
-
if(budget===undefined)return {};
|
|
18
|
-
if(!Number.isSafeInteger(budget)||budget<=0||budget>1_000_000_000_000)throw Error('invalid_budget');
|
|
19
|
-
return {budget_usd_micros:budget};
|
|
20
|
-
}
|
|
21
|
-
export function connectionParams(chain){if(!bounded(chain,100))throw Error('invalid_chain');return {chain};}
|
|
22
|
-
export function stableIntentKey(key){if(!bounded(key,128)||!/^[a-zA-Z0-9_.:-]+$/.test(key))throw Error('invalid_idempotency_key');return key;}
|
|
1
|
+
// Product-side validation only. The single HTTPK remains the execution authority.
|
|
2
|
+
const bounded=(value,max)=>typeof value==='string'&&value.length>0&&value.length<=max;
|
|
3
|
+
const identifier=value=>bounded(value,80)&&/^[a-zA-Z0-9_.:-]+$/.test(value);
|
|
4
|
+
export function actionInput(action,inputs){
|
|
5
|
+
if(!inputs||typeof inputs!=='object'||Array.isArray(inputs))throw Error('invalid_action_input');
|
|
6
|
+
const schemas={
|
|
7
|
+
'chain.head':{chain:v=>bounded(v,80)},
|
|
8
|
+
'wallet.prepare':{chain:v=>bounded(v,100),destination_chain:v=>bounded(v,100),sender:v=>bounded(v,100),recipient:v=>bounded(v,100),amount_atomic:v=>bounded(v,100)&&/^[1-9]\d*$/.test(v)},
|
|
9
|
+
'bww.fetch':{project:identifier,fragment_ids:v=>Array.isArray(v)&&v.length>=1&&v.length<=8&&new Set(v).size===v.length&&v.every(identifier),allow_html:v=>typeof v==='boolean'},
|
|
10
|
+
'bww.publish':{project:identifier,fragment_id:identifier,title:v=>bounded(v,120),text:v=>bounded(v,8000),html:v=>v===null||typeof v==='string'&&v.length<=12000,advertisement:v=>v===null||v&&typeof v==='object'&&!Array.isArray(v)&&Object.keys(v).length===2&&bounded(v.provider,80)&&bounded(v.text,500)}
|
|
11
|
+
};
|
|
12
|
+
const schema=schemas[action];
|
|
13
|
+
if(!schema||Object.keys(inputs).length!==Object.keys(schema).length||!Object.entries(schema).every(([key,validate])=>Object.hasOwn(inputs,key)&&validate(inputs[key])))throw Error('invalid_action_input');
|
|
14
|
+
return {type:'action',action,inputs};
|
|
15
|
+
}
|
|
16
|
+
export function hostingParams(budget){
|
|
17
|
+
if(budget===undefined)return {};
|
|
18
|
+
if(!Number.isSafeInteger(budget)||budget<=0||budget>1_000_000_000_000)throw Error('invalid_budget');
|
|
19
|
+
return {budget_usd_micros:budget};
|
|
20
|
+
}
|
|
21
|
+
export function connectionParams(chain){if(!bounded(chain,100))throw Error('invalid_chain');return {chain};}
|
|
22
|
+
export function stableIntentKey(key){if(!bounded(key,128)||!/^[a-zA-Z0-9_.:-]+$/.test(key))throw Error('invalid_idempotency_key');return key;}
|
package/src/public.mjs
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import {validateSDF} from './sdf.mjs';
|
|
2
|
-
import {KloudyError} from './client.mjs';
|
|
3
|
-
const fail=(code,message)=>{throw new KloudyError(code,message);};
|
|
4
|
-
async function request(path,options,fetcher){
|
|
5
|
-
let response;try{response=await fetcher('https://kloudy.ai'+path,{...options,redirect:'error',signal:AbortSignal.timeout(25000)});}catch{fail('public_unavailable','Kloudy public reading is unavailable. No account action was submitted.');}
|
|
6
|
-
if(!response.ok)fail(response.status===429?'rate_limited':'public_unavailable','The public resource could not be read. Check the URL or try again later.');
|
|
7
|
-
const chunks=[];let size=0;for await(const chunk of response.body){size+=chunk.length;if(size>2097152)fail('invalid_response','Public response exceeded its size limit.');chunks.push(chunk);}
|
|
8
|
-
try{return JSON.parse(Buffer.concat(chunks).toString());}catch{fail('invalid_response','Expected a JSON public response.');}
|
|
9
|
-
}
|
|
10
|
-
export async function readPublicPage(address,{fetcher=fetch}={}){
|
|
11
|
-
let url;try{url=new URL(address);}catch{}
|
|
12
|
-
if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
|
|
13
|
-
const result=await request('/api/read',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href})},fetcher);
|
|
14
|
-
let source;try{source=new URL(result.url);}catch{}
|
|
15
|
-
if(!source||!['http:','https:'].includes(source.protocol)||source.username||source.password||typeof result.body!=='string'||typeof result.type!=='string')fail('invalid_response','Expected source-attributed public page content.');
|
|
16
|
-
return {url:source.href,type:result.type,body:result.body,bytes:result.bytes??Buffer.byteLength(result.body)};
|
|
17
|
-
}
|
|
18
|
-
export async function readCard(path='/world',{fetcher=fetch}={}){
|
|
19
|
-
if(typeof path!=='string'||!/^\/(?:[a-zA-Z0-9_-]|%20)*$/.test(path))fail('invalid_path','Use a Kloudy page path such as /world.');
|
|
20
|
-
const card=await request('/cards/'+(path==='/'?'home':path.slice(1))+'.json',{headers:{accept:'application/json'}},fetcher);
|
|
21
|
-
if(card?.interaction_mode!=='informational_only'||!Array.isArray(card.capabilities)||card.capabilities.length||typeof card.description!=='string'||card.description.length>5000||typeof card.name!=='string'||card.website!=='https://kloudy.ai'+path)fail('invalid_response','Expected an informational Kloudy card without execution permissions.');
|
|
22
|
-
return card;
|
|
23
|
-
}
|
|
24
|
-
export async function convertPage(address,{fetcher=fetch,resolution="compact"}={}){
|
|
25
|
-
let url;try{url=new URL(address);}catch{}
|
|
26
|
-
if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
|
|
27
|
-
if(!['compact','standard'].includes(resolution))fail('invalid_resolution','Choose compact or standard conversion.');
|
|
28
|
-
const card=await request('/api/convert/sdf',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href,resolution})},fetcher);
|
|
29
|
-
if(!validateSDF(card).valid)fail('invalid_response','Expected a valid bounded SDF webpage card.');
|
|
30
|
-
return card;
|
|
31
|
-
}
|
|
32
|
-
export async function convertPages(addresses,options={}){
|
|
33
|
-
if(!Array.isArray(addresses)||!addresses.length||addresses.length>8||addresses.some(a=>typeof a!=='string'||a.length>2048))fail('invalid_input','Convert 1–8 public addresses at a time.');
|
|
34
|
-
const cards=[],reports=[],seen=new Map();
|
|
35
|
-
for(const address of addresses){let canonical;try{const url=new URL(address);url.hash='';canonical=url.username||url.password?'invalid-address':url.href;}catch{canonical=address;}
|
|
36
|
-
if(seen.has(canonical)){reports.push({...seen.get(canonical),duplicate:true});continue;}
|
|
37
|
-
let report;try{const card=await convertPage(address,options);cards.push(card);report={source:canonical,status:'converted',card_id:card.id};}catch(error){report={source:canonical,status:'unreadable',reason:error.code||'conversion_failed',message:error.code?error.message:'Conversion could not finish.'};}
|
|
38
|
-
reports.push(report);seen.set(canonical,report);
|
|
39
|
-
}
|
|
40
|
-
return {schema:'kloudy.sdf.card-list/1',cards,reports,ranking:'none',model_calls:0};
|
|
41
|
-
}
|
|
1
|
+
import {validateSDF} from './sdf.mjs';
|
|
2
|
+
import {KloudyError} from './client.mjs';
|
|
3
|
+
const fail=(code,message)=>{throw new KloudyError(code,message);};
|
|
4
|
+
async function request(path,options,fetcher){
|
|
5
|
+
let response;try{response=await fetcher('https://kloudy.ai'+path,{...options,redirect:'error',signal:AbortSignal.timeout(25000)});}catch{fail('public_unavailable','Kloudy public reading is unavailable. No account action was submitted.');}
|
|
6
|
+
if(!response.ok)fail(response.status===429?'rate_limited':'public_unavailable','The public resource could not be read. Check the URL or try again later.');
|
|
7
|
+
const chunks=[];let size=0;for await(const chunk of response.body){size+=chunk.length;if(size>2097152)fail('invalid_response','Public response exceeded its size limit.');chunks.push(chunk);}
|
|
8
|
+
try{return JSON.parse(Buffer.concat(chunks).toString());}catch{fail('invalid_response','Expected a JSON public response.');}
|
|
9
|
+
}
|
|
10
|
+
export async function readPublicPage(address,{fetcher=fetch}={}){
|
|
11
|
+
let url;try{url=new URL(address);}catch{}
|
|
12
|
+
if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
|
|
13
|
+
const result=await request('/api/read',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href})},fetcher);
|
|
14
|
+
let source;try{source=new URL(result.url);}catch{}
|
|
15
|
+
if(!source||!['http:','https:'].includes(source.protocol)||source.username||source.password||typeof result.body!=='string'||typeof result.type!=='string')fail('invalid_response','Expected source-attributed public page content.');
|
|
16
|
+
return {url:source.href,type:result.type,body:result.body,bytes:result.bytes??Buffer.byteLength(result.body)};
|
|
17
|
+
}
|
|
18
|
+
export async function readCard(path='/world',{fetcher=fetch}={}){
|
|
19
|
+
if(typeof path!=='string'||!/^\/(?:[a-zA-Z0-9_-]|%20)*$/.test(path))fail('invalid_path','Use a Kloudy page path such as /world.');
|
|
20
|
+
const card=await request('/cards/'+(path==='/'?'home':path.slice(1))+'.json',{headers:{accept:'application/json'}},fetcher);
|
|
21
|
+
if(card?.interaction_mode!=='informational_only'||!Array.isArray(card.capabilities)||card.capabilities.length||typeof card.description!=='string'||card.description.length>5000||typeof card.name!=='string'||card.website!=='https://kloudy.ai'+path)fail('invalid_response','Expected an informational Kloudy card without execution permissions.');
|
|
22
|
+
return card;
|
|
23
|
+
}
|
|
24
|
+
export async function convertPage(address,{fetcher=fetch,resolution="compact"}={}){
|
|
25
|
+
let url;try{url=new URL(address);}catch{}
|
|
26
|
+
if(!url||!['https:','http:'].includes(url.protocol)||url.username||url.password||url.href.length>2048)fail('invalid_url','Use a public HTTP(S) URL without embedded credentials.');
|
|
27
|
+
if(!['compact','standard'].includes(resolution))fail('invalid_resolution','Choose compact or standard conversion.');
|
|
28
|
+
const card=await request('/api/convert/sdf',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({url:url.href,resolution})},fetcher);
|
|
29
|
+
if(!validateSDF(card).valid)fail('invalid_response','Expected a valid bounded SDF webpage card.');
|
|
30
|
+
return card;
|
|
31
|
+
}
|
|
32
|
+
export async function convertPages(addresses,options={}){
|
|
33
|
+
if(!Array.isArray(addresses)||!addresses.length||addresses.length>8||addresses.some(a=>typeof a!=='string'||a.length>2048))fail('invalid_input','Convert 1–8 public addresses at a time.');
|
|
34
|
+
const cards=[],reports=[],seen=new Map();
|
|
35
|
+
for(const address of addresses){let canonical;try{const url=new URL(address);url.hash='';canonical=url.username||url.password?'invalid-address':url.href;}catch{canonical=address;}
|
|
36
|
+
if(seen.has(canonical)){reports.push({...seen.get(canonical),duplicate:true});continue;}
|
|
37
|
+
let report;try{const card=await convertPage(address,options);cards.push(card);report={source:canonical,status:'converted',card_id:card.id};}catch(error){report={source:canonical,status:'unreadable',reason:error.code||'conversion_failed',message:error.code?error.message:'Conversion could not finish.'};}
|
|
38
|
+
reports.push(report);seen.set(canonical,report);
|
|
39
|
+
}
|
|
40
|
+
return {schema:'kloudy.sdf.card-list/1',cards,reports,ranking:'none',model_calls:0};
|
|
41
|
+
}
|
package/src/query.mjs
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import {KloudyError} from './client.mjs';
|
|
2
|
-
import {validateSDF} from './sdf.mjs';
|
|
3
|
-
const fail=message=>{throw new KloudyError('invalid_query',message);};
|
|
4
|
-
const engineBytes=value=>Buffer.byteLength(JSON.stringify(value).replace(/[^\x00-\x7f]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0')));
|
|
5
|
-
export function queryInput(need,{scope,limit=3}={}){
|
|
6
|
-
if(typeof need!=='string'||!need.trim()||need.length>1000||/[\x00-\x1f]/.test(need)||!Number.isInteger(limit)||limit<1||limit>5)fail('Use a need of 1–1000 characters and a result limit of 1–5.');
|
|
7
|
-
if(scope!==undefined){if(!scope||typeof scope!=='object'||Array.isArray(scope)||Object.keys(scope).some(k=>!['sources','discovered_via','service_area'].includes(k)))fail('Use the documented discovery scope.');
|
|
8
|
-
if(scope.sources!==undefined&&(!Array.isArray(scope.sources)||scope.sources.length>8||new Set(scope.sources).size!==scope.sources.length||scope.sources.some(s=>typeof s!=='string'||s.length>512||!safeURL(s))))fail('Scope sources must be up to eight distinct public URLs.');
|
|
9
|
-
if(scope.discovered_via!==undefined&&(!Array.isArray(scope.discovered_via)||!scope.discovered_via.length||scope.discovered_via.length>2||new Set(scope.discovered_via).size!==scope.discovered_via.length||scope.discovered_via.some(s=>!['seo','geo'].includes(s))))fail('Discovery scope is seo and/or geo.');
|
|
10
|
-
if(scope.service_area!==undefined&&(typeof scope.service_area!=='string'||!scope.service_area.trim()||scope.service_area.length>80||/[\x00-\x1f]/.test(scope.service_area)))fail('Use a service area of 1–80 characters.');
|
|
11
|
-
}
|
|
12
|
-
const input={version:'kldy.query/1',need,limit,...(scope?{scope}:{})};if(engineBytes(input)>2048)fail('The complete query must fit in 2 KiB.');return input;
|
|
13
|
-
}
|
|
14
|
-
function safeURL(value){try{const u=new URL(value);return ['https:','http:'].includes(u.protocol)&&!u.username&&!u.password;}catch{return false;}}
|
|
15
|
-
export function queryResult(result,limit){
|
|
16
|
-
if(!result||Object.keys(result).some(k=>!['version','query_sha256','state','matches','execution_started','permission_granted','next','measurements'].includes(k)))throw new KloudyError('invalid_response','Expected card references, not additional payloads.');
|
|
17
|
-
const allowed=['card_id','title','source_url','content_sha256','score','provenance','expires_at'];
|
|
18
|
-
if(result?.version!=='kldy.query/1'||!['matched','no_match'].includes(result.state)||!Array.isArray(result.matches)||result.matches.length>limit||result.execution_started!==false||result.permission_granted!==false||result.measurements?.definitions_emitted!==0||result.matches.some(m=>!m||Object.keys(m).some(k=>!allowed.includes(k))||typeof m.card_id!=='string'||typeof m.title!=='string'||!safeURL(m.source_url)||!/^[a-f0-9]{64}$/.test(m.content_sha256)||!Number.isFinite(m.expires_at)))throw new KloudyError('invalid_response','Expected bounded KLDY card references without tool definitions or permission.');
|
|
19
|
-
return result;
|
|
20
|
-
}
|
|
21
|
-
export function sdfIndex(cards,{discoveredVia,ttlSeconds=3600,now=Date.now()/1000}={}){
|
|
22
|
-
if(!Array.isArray(cards)||cards.length>64||!['seo','geo'].includes(discoveredVia)||!Number.isInteger(ttlSeconds)||ttlSeconds<1||ttlSeconds>86400)fail('Provide up to 64 validated cards, actual SEO/GEO provenance, and a lifetime of 1–86400 seconds.');
|
|
23
|
-
const seen=new Set(),entries=[];
|
|
24
|
-
for(const card of cards){if(!validateSDF(card).valid)fail('Every indexed card must pass SDF validation.');if(seen.has(card.id))continue;seen.add(card.id);const fetched=Date.parse(card.source.fetched_at)/1000,expires=Math.min(now+ttlSeconds,fetched+86400);if(expires<=now||fetched>now+300)fail('Refresh expired or future-dated source content before indexing.');
|
|
25
|
-
entries.push({id:card.id,source_url:card.source.url,discovered_via:discoveredVia,title:card.source.title.slice(0,120),summary:card.summary.brief,tags:[],capability_ids:[],service_areas:[],content_sha256:card.provenance.content_hash.slice(7),expires_at:expires});
|
|
26
|
-
}
|
|
27
|
-
const envelope={version:'bbe.sdf-index/1',cards:entries};if(engineBytes(envelope)>65536)fail('Split this index into smaller batches; the engine limit is 64 KiB.');return envelope;
|
|
28
|
-
}
|
|
1
|
+
import {KloudyError} from './client.mjs';
|
|
2
|
+
import {validateSDF} from './sdf.mjs';
|
|
3
|
+
const fail=message=>{throw new KloudyError('invalid_query',message);};
|
|
4
|
+
const engineBytes=value=>Buffer.byteLength(JSON.stringify(value).replace(/[^\x00-\x7f]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0')));
|
|
5
|
+
export function queryInput(need,{scope,limit=3}={}){
|
|
6
|
+
if(typeof need!=='string'||!need.trim()||need.length>1000||/[\x00-\x1f]/.test(need)||!Number.isInteger(limit)||limit<1||limit>5)fail('Use a need of 1–1000 characters and a result limit of 1–5.');
|
|
7
|
+
if(scope!==undefined){if(!scope||typeof scope!=='object'||Array.isArray(scope)||Object.keys(scope).some(k=>!['sources','discovered_via','service_area'].includes(k)))fail('Use the documented discovery scope.');
|
|
8
|
+
if(scope.sources!==undefined&&(!Array.isArray(scope.sources)||scope.sources.length>8||new Set(scope.sources).size!==scope.sources.length||scope.sources.some(s=>typeof s!=='string'||s.length>512||!safeURL(s))))fail('Scope sources must be up to eight distinct public URLs.');
|
|
9
|
+
if(scope.discovered_via!==undefined&&(!Array.isArray(scope.discovered_via)||!scope.discovered_via.length||scope.discovered_via.length>2||new Set(scope.discovered_via).size!==scope.discovered_via.length||scope.discovered_via.some(s=>!['seo','geo'].includes(s))))fail('Discovery scope is seo and/or geo.');
|
|
10
|
+
if(scope.service_area!==undefined&&(typeof scope.service_area!=='string'||!scope.service_area.trim()||scope.service_area.length>80||/[\x00-\x1f]/.test(scope.service_area)))fail('Use a service area of 1–80 characters.');
|
|
11
|
+
}
|
|
12
|
+
const input={version:'kldy.query/1',need,limit,...(scope?{scope}:{})};if(engineBytes(input)>2048)fail('The complete query must fit in 2 KiB.');return input;
|
|
13
|
+
}
|
|
14
|
+
function safeURL(value){try{const u=new URL(value);return ['https:','http:'].includes(u.protocol)&&!u.username&&!u.password;}catch{return false;}}
|
|
15
|
+
export function queryResult(result,limit){
|
|
16
|
+
if(!result||Object.keys(result).some(k=>!['version','query_sha256','state','matches','execution_started','permission_granted','next','measurements'].includes(k)))throw new KloudyError('invalid_response','Expected card references, not additional payloads.');
|
|
17
|
+
const allowed=['card_id','title','source_url','content_sha256','score','provenance','expires_at'];
|
|
18
|
+
if(result?.version!=='kldy.query/1'||!['matched','no_match'].includes(result.state)||!Array.isArray(result.matches)||result.matches.length>limit||result.execution_started!==false||result.permission_granted!==false||result.measurements?.definitions_emitted!==0||result.matches.some(m=>!m||Object.keys(m).some(k=>!allowed.includes(k))||typeof m.card_id!=='string'||typeof m.title!=='string'||!safeURL(m.source_url)||!/^[a-f0-9]{64}$/.test(m.content_sha256)||!Number.isFinite(m.expires_at)))throw new KloudyError('invalid_response','Expected bounded KLDY card references without tool definitions or permission.');
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
export function sdfIndex(cards,{discoveredVia,ttlSeconds=3600,now=Date.now()/1000}={}){
|
|
22
|
+
if(!Array.isArray(cards)||cards.length>64||!['seo','geo'].includes(discoveredVia)||!Number.isInteger(ttlSeconds)||ttlSeconds<1||ttlSeconds>86400)fail('Provide up to 64 validated cards, actual SEO/GEO provenance, and a lifetime of 1–86400 seconds.');
|
|
23
|
+
const seen=new Set(),entries=[];
|
|
24
|
+
for(const card of cards){if(!validateSDF(card).valid)fail('Every indexed card must pass SDF validation.');if(seen.has(card.id))continue;seen.add(card.id);const fetched=Date.parse(card.source.fetched_at)/1000,expires=Math.min(now+ttlSeconds,fetched+86400);if(expires<=now||fetched>now+300)fail('Refresh expired or future-dated source content before indexing.');
|
|
25
|
+
entries.push({id:card.id,source_url:card.source.url,discovered_via:discoveredVia,title:card.source.title.slice(0,120),summary:card.summary.brief,tags:[],capability_ids:[],service_areas:[],content_sha256:card.provenance.content_hash.slice(7),expires_at:expires});
|
|
26
|
+
}
|
|
27
|
+
const envelope={version:'bbe.sdf-index/1',cards:entries};if(engineBytes(envelope)>65536)fail('Split this index into smaller batches; the engine limit is 64 KiB.');return envelope;
|
|
28
|
+
}
|