@apiosk/mcp 1.7.0 → 2.0.0

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 (100) hide show
  1. package/README.md +83 -612
  2. package/assets/brand/apiosk-a-20260921.png +0 -0
  3. package/assets/brand/apple-touch-icon.png +0 -0
  4. package/assets/brand/favicon-dark.ico +0 -0
  5. package/assets/brand/favicon-light.ico +0 -0
  6. package/assets/brand/favicon.ico +0 -0
  7. package/assets/brand/icon-192.png +0 -0
  8. package/assets/brand/icon-512.png +0 -0
  9. package/assets/brand/icon-maskable-512.png +0 -0
  10. package/assets/brand/inter-OFL.txt +93 -0
  11. package/assets/brand/inter-latin-400-normal.woff2 +0 -0
  12. package/assets/brand/inter-latin-500-normal.woff2 +0 -0
  13. package/assets/brand/inter-latin-600-normal.woff2 +0 -0
  14. package/assets/brand/mark-20260905-transparent.svg +1 -0
  15. package/assets/brand/mark-20260918.svg +1 -0
  16. package/assets/brand/mark-black-20260918.svg +1 -0
  17. package/assets/brand/mark-dark-20260905-transparent.png +0 -0
  18. package/assets/brand/mark-dark-20260918.png +0 -0
  19. package/assets/brand/mark-dark-96.png +0 -0
  20. package/assets/brand/mark-light-20260905-transparent.png +0 -0
  21. package/assets/brand/mark-light-20260918.png +0 -0
  22. package/assets/brand/mark-light-96.png +0 -0
  23. package/assets/brand/wordmark-black-320.png +0 -0
  24. package/assets/brand/wordmark-white-320.png +0 -0
  25. package/docs/branding.md +11 -0
  26. package/docs/marketplace-submission-2026-04-08.md +4 -0
  27. package/docs/marketplace-submission-2026-08-20.md +157 -0
  28. package/docs/openai-plugin-submission-2026-09-05.md +143 -0
  29. package/dxt.json +26 -22
  30. package/index.mjs +3 -1
  31. package/logo-optimized-light.png +0 -0
  32. package/package.json +14 -14
  33. package/plugin/apiosk/.codex-plugin/plugin.json +45 -0
  34. package/plugin/apiosk/.mcp.json +8 -0
  35. package/plugin/apiosk/assets/icon-dark.png +0 -0
  36. package/plugin/apiosk/assets/icon.png +0 -0
  37. package/plugin/apiosk/assets/icon.svg +1 -0
  38. package/plugin/apiosk/assets/logo.png +0 -0
  39. package/plugin/apiosk/skills/apiosk/SKILL.md +33 -0
  40. package/plugin/apiosk/skills/apiosk/agents/openai.yaml +12 -0
  41. package/server.json +58 -24
  42. package/server.mjs +136 -151
  43. package/src/approval-feedback.mjs +21 -0
  44. package/src/brand-routes.mjs +28 -0
  45. package/src/create-server.mjs +160 -46
  46. package/src/display-money.mjs +20 -0
  47. package/src/display-text.mjs +67 -0
  48. package/src/gateway-client.mjs +42 -0
  49. package/src/gateway-v2-ask.mjs +50 -0
  50. package/src/gateway-v2-card-account.mjs +23 -0
  51. package/src/gateway-v2-card-actions.mjs +67 -0
  52. package/src/gateway-v2-card-answer-text.mjs +130 -0
  53. package/src/gateway-v2-card-answer.mjs +68 -0
  54. package/src/gateway-v2-card-blocks.mjs +166 -0
  55. package/src/gateway-v2-card-body.mjs +259 -0
  56. package/src/gateway-v2-card-budget.mjs +21 -0
  57. package/src/gateway-v2-card-cbs.mjs +56 -0
  58. package/src/gateway-v2-card-choices.mjs +4 -0
  59. package/src/gateway-v2-card-clarification.mjs +25 -0
  60. package/src/gateway-v2-card-compact.mjs +34 -0
  61. package/src/gateway-v2-card-events.mjs +41 -0
  62. package/src/gateway-v2-card-presentation.mjs +125 -0
  63. package/src/gateway-v2-card-research.mjs +63 -0
  64. package/src/gateway-v2-card-result.mjs +32 -0
  65. package/src/gateway-v2-card-search.mjs +39 -0
  66. package/src/gateway-v2-card-sources.mjs +32 -0
  67. package/src/gateway-v2-card-style.mjs +76 -0
  68. package/src/gateway-v2-card-verdict.mjs +66 -0
  69. package/src/gateway-v2-card.mjs +91 -0
  70. package/src/gateway-v2-contracts.json +66 -0
  71. package/src/gateway-v2-instructions.md +113 -0
  72. package/src/gateway-v2-recovery.mjs +18 -0
  73. package/src/gateway-v2-report-links.mjs +14 -0
  74. package/src/gateway-v2-workflows.mjs +17 -0
  75. package/src/gateway-v2.mjs +179 -0
  76. package/src/oauth.mjs +402 -1949
  77. package/src/observability.mjs +19 -3
  78. package/src/result-presentation.mjs +9 -0
  79. package/src/runtime.mjs +24 -3670
  80. package/src/source-groups.mjs +91 -0
  81. package/src/source-value-format.mjs +25 -0
  82. package/src/tool-result.mjs +20 -0
  83. package/src/ui-bridge.mjs +185 -0
  84. package/src/well-known-routes.mjs +132 -0
  85. package/src/assets/wallet-accounts.mjs +0 -20513
  86. package/src/assets/walletconnect-provider.mjs +0 -6319
  87. package/src/discovery.mjs +0 -929
  88. package/src/external-fetch.mjs +0 -203
  89. package/src/funding-options.mjs +0 -255
  90. package/src/gateway-management.mjs +0 -107
  91. package/src/hosted-payment.mjs +0 -552
  92. package/src/hosted-wallets.mjs +0 -530
  93. package/src/listing-metadata.mjs +0 -287
  94. package/src/local-config.mjs +0 -256
  95. package/src/payment-guidance.mjs +0 -298
  96. package/src/publisher.mjs +0 -1288
  97. package/src/result-canvas.mjs +0 -16
  98. package/src/source-registry.mjs +0 -215
  99. package/src/wallet-store.mjs +0 -476
  100. package/src/x402-inspect.mjs +0 -361
@@ -0,0 +1,21 @@
1
+ // A budget refusal is not an expired login. Open the existing settings without
2
+ // revoking tokens, pretending to reconnect, or approving/replaying the task.
3
+ export const V2_CARD_BUDGET = `
4
+ function renderApprovalRecovery(response,data){
5
+ const codes=[response?.error_code,...(response?.errors||[]).map(error=>error.code)];
6
+ if(!codes.some(code=>['approval_per_request_limit','approval_daily_limit'].includes(code)))return;
7
+ const actions=byId('feedback-actions');
8
+ let url=null;try{url=trustedShortcut(new URL('/connections',dataApprovalOrigin()).href,'/connections')}catch{}
9
+ const button=el('button','primary','Set higher limits');button.type='button';
10
+ const help=el('p','meta','Choose your connection, change its per-request and daily limits, and save. Then return here and check status before approving.');
11
+ button.onclick=async()=>{
12
+ if(output!==data){showFeedback('This request was updated. Check status before continuing.','error');return}
13
+ if(!url||!await window.apiosk.openLink(url).catch(()=>false)){
14
+ help.textContent='Open Apiosk > Integrations, choose your connection, set higher spending limits and save. Then return here and check status before approving.';
15
+ window.apiosk.resize();
16
+ }
17
+ };
18
+ actions.append(button,help,el('p','meta','Or reconnect Apiosk in your chatbot and choose higher limits on the connection screen. After reconnecting, ask the question again using the new connection.'));
19
+ window.apiosk.resize();
20
+ }
21
+ `;
@@ -0,0 +1,56 @@
1
+ // Display only: join the returned CBS codebooks, never substitute missing values.
2
+ export function cbsAnnualView(data, results) {
3
+ const question = data.context_view?.conversation?.[0]?.question || '';
4
+ const years = [...new Set(question.match(/\b(?:19|20)\d{2}\b/g) || [])].sort();
5
+ if (!years.length || years.length > 2) return null;
6
+ const base = '/odata/v1/CBS/85809NED/';
7
+ const observations = results.find(r => r.source?.provider === 'cbs-statline' && r.source.path === base + 'Observations');
8
+ const entity = observations?.subject?.entity_ref;
9
+ if (!entity) return null;
10
+ const names = ['Observations', 'MeasureCodes', 'PeriodenCodes', 'BedrijfstakkenBranchesSBI2008Codes', 'BedrijfsgrootteCodes', 'Properties'];
11
+ const records = names.map(name => results.find(r => r.source?.provider === 'cbs-statline' && r.source.path === base + name && r.subject?.entity_ref === entity));
12
+ if (records.some(r => !r?.data || r.coverage === 'partial')) return null;
13
+ const [values, measures, periods, sectors, sizes, properties] = records.map(r => r.data);
14
+ if (![values, measures, periods, sectors, sizes].every(d => Array.isArray(d.value))) return null;
15
+ const selected = values.value.filter(r => years.some(year => r.Perioden === year + 'JJ00'));
16
+ if (!selected.length || years.some(year => !selected.some(r => r.Perioden === year + 'JJ00'))) return null;
17
+ const rows = [];
18
+ for (const row of selected) {
19
+ const measure = measures.value.find(v => v.Identifier === row.Measure);
20
+ const period = periods.value.find(v => v.Identifier === row.Perioden);
21
+ const sector = sectors.value.find(v => v.Identifier === row.BedrijfstakkenBranchesSBI2008);
22
+ const size = sizes.value.find(v => v.Identifier === row.Bedrijfsgrootte);
23
+ if (!measure?.Title || !measure.Unit || !period?.Title || !sector?.Title || !size?.Title) return null;
24
+ rows.push({period:period.Title.trim(), measure:measure.Title, unit:measure.Unit,
25
+ code:row.Measure, value:typeof row.Value === 'number' && Number.isFinite(row.Value) ? row.Value : 'Niet beschikbaar',
26
+ status:period.Status || 'Niet vermeld', sector:sector.Title, size:size.Title});
27
+ }
28
+ rows.sort((a,b) => a.period.localeCompare(b.period) || a.measure.localeCompare(b.measure));
29
+ const scopes = [...new Set(rows.map(r => r.sector + ' · ' + r.size))];
30
+ if (scopes.length !== 1) return null;
31
+ const url = properties.Distributions?.find(d => /^https:\/\/(?:dataportal|opendata)\.cbs\.nl\//.test(d.AccessUrl || ''))?.AccessUrl;
32
+ return {records, rows, subject:observations.subject.label, scope:scopes[0], title:properties.Title || observations.subject.label,
33
+ modified:properties.Modified || properties.ModificationDate, retrieved:observations.source.retrieved_at, url};
34
+ }
35
+
36
+ export const V2_CARD_CBS = `
37
+ ${cbsAnnualView.toString()}
38
+ function renderCbsAnnual(view){
39
+ const s=section(view.subject||view.title);s.classList.add('answer-section');
40
+ const number=value=>typeof value==='number'?new Intl.NumberFormat('nl-NL',{maximumFractionDigits:6}).format(value):value;
41
+ const growth=view.rows.filter(r=>r.code==='M004926'&&r.unit==='%'),latest=growth.at(-1);
42
+ if(latest){s.append(el('p','answer-number',(typeof latest.value==='number'&&latest.value>0?'+':'')+number(latest.value)+(typeof latest.value==='number'?'%':'')));s.append(el('p','answer-caption','Omzet in '+latest.period+' ten opzichte van '+(Number(latest.period)-1)));const previous=growth.at(-2);if(previous)s.append(el('p','meta','In '+previous.period+': '+number(previous.value)+(typeof previous.value==='number'?'%':'')+' ten opzichte van '+(Number(previous.period)-1)))}
43
+ const states=[...new Set(view.rows.map(r=>r.status))];s.append(el('p','answer-source','CBS StatLine'+(states.length===1?' · '+states[0]:'')));
44
+ const links=el('div','result-links');if(view.url){const link=el('button','text-action','Bron bekijken');link.onclick=()=>window.apiosk.openLink(view.url);links.append(link)}s.append(links);
45
+ const full=el('details','compact-details result-details');full.append(el('summary','','Details'));full.ontoggle=()=>window.apiosk.resize();
46
+ full.append(el('p','meta',view.scope));
47
+ const wrap=el('div','cbs-table-wrap'),table=el('table','cbs-table'),head=el('tr');for(const label of ['Jaar','Maatstaf','Waarde','Eenheid','Status'])head.append(el('th','',label));const thead=el('thead');thead.append(head);table.append(thead);const body=el('tbody');
48
+ for(const row of view.rows){const tr=el('tr');for(const value of [row.period,row.measure,number(row.value),row.unit,row.status])tr.append(el('td','',value));body.append(tr)}table.append(body);wrap.append(table);full.append(wrap);
49
+ if(view.modified)full.append(el('p','meta','CBS bijgewerkt: '+new Date(view.modified).toLocaleDateString('nl-NL')));
50
+ if(view.retrieved)full.append(el('p','meta','Opgehaald: '+new Date(view.retrieved).toLocaleString('nl-NL')));
51
+ const raw=el('details','full-result');raw.append(el('summary','','Brongegevens'),el('pre','',JSON.stringify(view.records,null,2)));full.append(raw);s.append(full);
52
+ if(!latest)full.open=true;
53
+ return {section:s,details:full,links};
54
+ }
55
+ `;
56
+
@@ -0,0 +1,4 @@
1
+ // Company selection changes only the visible name; entity references stay exact.
2
+ export const V2_CARD_CHOICES = `
3
+ function renderChoices(data){const select=(data.next_actions||[]).find(a=>a.kind==='select_entity'),candidates=data.context_view?.candidates;if(!select||!Array.isArray(candidates)||!candidates.length)return;const s=section('Which company do you mean?');s.classList.add('choice-section');s.setAttribute('role','region');s.setAttribute('aria-label','Choose the right company');s.append(el('p','meta','Choose once to continue within your approved amount.'));const list=el('div','choice-list');for(const c of candidates){const copy=el('span'),name=(c.facts||[]).find(f=>f.type==='company.name');copy.append(el('strong','',formatDisplayText(text(name?.value||c.label))));const identifiers=(c.facts||[]).filter(f=>/kvk|city|country|lei/i.test(f.type)).map(f=>(/kvk/i.test(f.type)?'KVK ':'')+text(f.value));copy.append(el('span','meta',identifiers.join(' · ')));const b=el('button');b.append(copy,el('span','','Select'));b.onclick=()=>callAction(select,{entity_ref:c.entity_ref});list.append(b)}s.append(list)}
4
+ `;
@@ -0,0 +1,25 @@
1
+ export const V2_CARD_CLARIFICATION = `
2
+ function renderInput(data){
3
+ const action=(data.next_actions||[]).find(a=>a.kind==='supply_input');
4
+ if(!action){renderClarification(data);return}
5
+ const schema=action.input_schema?.properties?.value||{},raw=text(action.label).replace(/^Provide\\s+/i,'').trim();
6
+ const names={'web.query':'Search query','web.source_search_query':'Search query','domain.name':'Website domain','company_registry.kvknummer':'KVK number','company.registration.lei':'LEI code','company.name':'Company name'};
7
+ const title=schema.title||names[raw]||(raw?pretty(raw):'');
8
+ if(!title){section('Request details unavailable').append(el('p','notice','The request did not identify the missing detail. Use Check status to reload the saved request.'));return}
9
+ const s=section(title),form=el('form','field actions'),field=el('input'),b=el('button','quiet','Continue');
10
+ const message=data.context_view?.clarification?.message||schema.description||(names[raw]==='Search query'?'What should the web search look for?':'Enter the '+title.toLowerCase()+' to continue.');
11
+ s.append(el('p','meta',message));field.id='requested-input';field.required=true;field.autocomplete='off';field.setAttribute('aria-label',title);
12
+ field.placeholder=Array.isArray(schema.examples)&&schema.examples.length?'For example: '+text(schema.examples[0]):title;
13
+ form.append(field,b);form.onsubmit=e=>{e.preventDefault();if(output!==data||busy||!field.value.trim())return;let value=field.value.trim();const type=schema.type;if(type==='integer'||type==='number')value=Number(value);else if(type==='boolean')value=value==='true';return callAction(action,{value})};s.append(form);
14
+ }
15
+ function renderClarification(data){
16
+ if(data.status!=='needs_input')return;
17
+ const turn=data.context_view?.conversation?.at(-1),message=data.context_view?.clarification?.message||turn?.reply;
18
+ const s=section('One detail is needed');s.append(el('p','notice',message||'Add the missing detail in your chat, or check the saved request status.'));
19
+ if(!message||!turn?.question||!data.state)return;
20
+ const form=el('form','field actions'),field=el('input'),b=el('button','quiet','Send answer');
21
+ field.required=true;field.autocomplete='off';field.placeholder='Your answer';field.setAttribute('aria-label','Your answer to the clarification');form.append(field,b);
22
+ form.onsubmit=async e=>{e.preventDefault();const answer=field.value.trim();if(!answer||busy||output!==data)return;busy=true;b.disabled=true;showFeedback('Updating your request…');try{const next=await window.apiosk.callTool('apiosk_discover',{question:turn.question+'\\n\\nUser clarification: '+answer,state:data.state});acceptResponse(next)}catch(error){showFeedback(error?.message||'Could not send your answer. Check status and try again.','error')}finally{busy=false;b.disabled=false}};
23
+ s.append(form);
24
+ }
25
+ `;
@@ -0,0 +1,34 @@
1
+ // The execution graph stays private; only consent, choices and results are visible.
2
+ export const V2_CARD_COMPACT = `
3
+ renderPlan=function(data){
4
+ const p=data.proposal;if(!p||['cancelled','succeeded','partial','failed','unsupported','needs_selection'].includes(data.status))return;
5
+ const details=p.step_details||[],subjects=[...new Set(details.map(d=>d.subject).filter(Boolean))];
6
+ const sources=[...new Map(details.filter(d=>d.source).map(d=>[d.source.directory_slug||d.source.slug||d.source.name,{...d.source,name:d.source.directory_name||d.source.name}])).values()];
7
+ const s=section(formatDisplayNarrative(subjects.join(' · ')||p.label||'Data request',data));s.classList.add('request-section');planSurface=s;
8
+ if(data.billing?.authorization_active||data.status==='running'){
9
+ s.append(el('p','compact-status','Getting your data…'));
10
+ s.append(el('p','meta','Approved up to '+money(p.max_total_atomic,p.currency,true)));return;
11
+ }
12
+ if(sources.length)s.append(el('p','meta',sources.length+(sources.length===1?' source':' sources')));
13
+ const price=el('div','request-price');price.append(el('span','price-label','Maximum total'),el('strong','',money(p.max_total_atomic,p.currency,true)));s.append(price);
14
+ s.append(el('p','meta','Approve once. The entire request stays within this amount.'));
15
+ const disclosure=el('details','compact-details'),list=el('div','compact-sources');disclosure.append(el('summary','','Details'));
16
+ for(const source of sources)list.append(sourceLine(source));disclosure.append(list);s.append(disclosure);
17
+ };
18
+ const renderExpandedCard=render;
19
+ render=function(data){
20
+ renderExpandedCard(data);if(!data||typeof data!=='object')return;
21
+ for(const section of sections.querySelectorAll(':scope > .section')){
22
+ const heading=section.querySelector('h3');
23
+ if(heading?.textContent==='Payment summary'){
24
+ const dutch=!!cbsAnnualView(data,data.context_view?.results||[]),charged=money(data.billing?.total_charged,data.billing?.currency),disclosure=el('details','compact-details');
25
+ disclosure.append(el('summary','',charged==null?(dutch?'Betalingsdetails':'Payment details'):(dutch?'Betaald ':'Charged ')+charged+(data.billing?.reserved&&data.billing.reserved!=='0'?' · Reserved '+money(data.billing.reserved,data.billing.currency):'')+(dutch?' · Betalingsdetails':' · Payment details')));
26
+ const grid=section.querySelector('.balances');if(grid)disclosure.append(grid);section.replaceChildren(disclosure);
27
+ }
28
+ }
29
+ for(const detail of data.proposal?.step_details||[]){if(detail.error?.message&&!(data.errors||[]).some(e=>e.message===detail.error.message))section('Needs attention').append(el('p','notice error',detail.error.message))}
30
+ if(planSurface){const disclosure=planSurface.querySelector('.compact-details');if(disclosure)planSurface.append(disclosure)}
31
+ if(typeof observeTask==='function')observeTask(data);
32
+ window.apiosk.resize();
33
+ };
34
+ `;
@@ -0,0 +1,41 @@
1
+ // Task-scoped SSE and recovery only read saved state; never authorize spending.
2
+ export const V2_CARD_EVENTS = `
3
+ let taskEvents=null,eventTask=null,eventUrl=null,eventRecovery=null,eventFailures=0;
4
+ function observeTask(data){
5
+ const ref=data.state?.state_ref,url=data.context_view?.events_url;
6
+ const idle=!data.context_view?.worker_active&&['needs_input','cancelled','succeeded','partial','failed','unsupported'].includes(data.status);
7
+ if(eventTask!==ref){eventFailures=0;if(eventRecovery){clearTimeout(eventRecovery);eventRecovery=null}}
8
+ if(eventTask===ref&&eventUrl===url&&taskEvents&&!idle)return;
9
+ if(taskEvents){taskEvents.close();taskEvents=null}
10
+ if(idle){if(eventRecovery){clearTimeout(eventRecovery);eventRecovery=null}return}
11
+ if(!ref||!url||typeof EventSource==='undefined'||eventRecovery||eventFailures>=3)return;
12
+ let parsed;try{parsed=new URL(url)}catch{return}
13
+ if(parsed.origin!=='__APIOSK_GATEWAY_ORIGIN__'||parsed.pathname!=='/v2/tasks/'+ref+'/events')return;
14
+ eventTask=ref;eventUrl=url;
15
+ const recover=()=>{
16
+ if(output?.state?.state_ref!==ref||taskEvents!==stream)return;
17
+ stream.close();taskEvents=null;eventFailures++;
18
+ showFeedback('Live updates are unavailable. Checking the saved request…');
19
+ eventRecovery=setTimeout(async()=>{
20
+ eventRecovery=null;if(output?.state?.state_ref!==ref)return;
21
+ const refreshed=await refreshTask(false);
22
+ if(output?.state?.state_ref!==ref)return;
23
+ if(!refreshed||eventFailures>=3)showFeedback('Use Check status to refresh the saved request.');
24
+ if(refreshed)observeTask(output);
25
+ },2000*eventFailures);
26
+ };
27
+ let stream;try{stream=new EventSource(url);taskEvents=stream}catch{showFeedback('Use Check status to refresh the saved request.');return}
28
+ stream.addEventListener('task',event=>{
29
+ if(taskEvents!==stream||output?.state?.state_ref!==ref)return;
30
+ try{
31
+ const next=JSON.parse(event.data);
32
+ if(next.state?.state_ref!==ref||Number(next.state.revision)<Number(output.state.revision))return;
33
+ if(next.context_view?.events_path)next.context_view.events_url=new URL(next.context_view.events_path,parsed.origin).href;
34
+ const documents=[next.context_view,next.result,...(next.context_view?.results||[]),...(next.context_view?.conversation||[]).flatMap(t=>[t.output,t.output?.result,...(t.output?.results||[])])];
35
+ for(const doc of documents){const path=doc?.report?.download_path;if(typeof path==='string'&&path.startsWith('/v2/tasks/'+ref+'/'))doc.report.url=new URL(path,parsed.origin).href;const evidence=doc?.report?.evidence_download_path;if(typeof evidence==='string'&&evidence.startsWith('/v2/tasks/'+ref+'/reports/')&&evidence.includes('/evidence.zip?'))doc.report.evidence_url=new URL(evidence,parsed.origin).href}
36
+ acceptResponse(next);
37
+ }catch{recover()}
38
+ });
39
+ stream.onerror=recover;
40
+ }
41
+ `;
@@ -0,0 +1,125 @@
1
+ import { sourceCurrency } from './source-value-format.mjs';
2
+ import { answerValue, answerValueContext } from './gateway-v2-card-answer-text.mjs';
3
+ // The App's answer tables for version-1 analyses: `analysis.presentation`
4
+ // (table, comparison, chart) and, without one, only the records and fields the
5
+ // saved answer cites. Never a dump of a provider's raw rows
6
+ // (app/src/lib/domain/answer-presentation.ts). Keep in sync with the App.
7
+ export function presentationTable(p, results = [], locale = 'en-US') {
8
+ const columns = Array.isArray(p.columns) ? p.columns : [], rows = Array.isArray(p.rows) ? p.rows : [];
9
+ const currencyColumn = columns.findIndex(c => /^(currency|valuta)$/i.test(String(c?.value)));
10
+ return {
11
+ title: p.title, columns: columns.map(c => String(c?.value)), rows: rows.map(r => (r || []).map(c => c?.value)), locale,
12
+ displayRows: rows.map(row => (row || []).map((cell, i) => {
13
+ if (!cell || !/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(String(cell.value))) return cell?.value;
14
+ const context = cell.result_ref && cell.pointer ? answerValueContext({ result_ref: cell.result_ref, pointer: cell.pointer }, results) : null;
15
+ return answerValue(cell.value, context?.field || String(columns[i]?.value), context?.currency ?? sourceCurrency(row[currencyColumn]?.value), locale);
16
+ })),
17
+ note: p.returned_count !== undefined ? rows.length + ' selected from ' + p.returned_count + ' returned records. This is not necessarily the complete search.' : 'Based on saved source data. Missing values are unknown.',
18
+ };
19
+ }
20
+ function presentationEvidence(row, field, value) {
21
+ if (!row.has(field)) { row.set(field, value);return; }
22
+ const previous = row.get(field);
23
+ if (previous !== value) row.set(field, [...new Set([...(Array.isArray(previous) ? previous : [previous]), value])]);
24
+ }
25
+ export function legacyAnswerTables(question, results, analysis) {
26
+ if (analysis?.presentation || !analysis?.observations?.length || !/\b(overzicht|tabel|table|overview|list|lijst|welke|which|vergelijk|compare|comparison)\b/i.test(question || '')) return [];
27
+ const record = v => v && typeof v === 'object' && !Array.isArray(v) ? v : {}, groups = new Map();
28
+ for (const evidence of analysis.observations.flatMap(o => o?.evidence || [])) {
29
+ const doc = (results || []).map(record).find(d => d.result_ref === evidence?.result_ref), pointer = String(evidence?.pointer || '');
30
+ if (!doc || !pointer.startsWith('/data/')) continue;
31
+ const parts = pointer.split('/').slice(1).map(p => p.replaceAll('~1', '/').replaceAll('~0', '~'));
32
+ let node = { ...doc, data: doc.data ?? doc.preview_data }, groupPath = '', rowPath = '', field = parts.slice(1).join(' / '), rowRecord = {};
33
+ for (let i = 0; i < parts.length; i++) {
34
+ if (Array.isArray(node) && /^(0|[1-9]\d*)$/.test(parts[i])) { groupPath = JSON.stringify(parts.slice(0, i));rowPath = JSON.stringify(parts.slice(0, i + 1));field = parts.slice(i + 1).join(' / ');rowRecord = record(node[Number(parts[i])]);break; }
35
+ node = record(node)[parts[i]];
36
+ }
37
+ const key = JSON.stringify([evidence.result_ref, groupPath]);
38
+ let group = groups.get(key);
39
+ if (!group) { const source = record(doc.source), subject = record(doc.subject);group = { name: String(subject.label ?? source.name ?? source.provider ?? 'Answer'), columns: [], rows: new Map() };groups.set(key, group); }
40
+ if (!rowPath) {
41
+ group.columns = ['Field', 'Source value'];
42
+ const row = group.rows.get(pointer) ?? new Map([['Field', field]]);
43
+ presentationEvidence(row, 'Source value', evidence.value);group.rows.set(pointer, row);continue;
44
+ }
45
+ let row = group.rows.get(rowPath);
46
+ if (!row) { row = new Map();group.rows.set(rowPath, row); }
47
+ if (!group.columns.includes(field)) group.columns.push(field);
48
+ presentationEvidence(row, field, evidence.value);
49
+ const link = rowRecord.url ?? record(rowRecord.link).href ?? record(record(rowRecord._links).self).href;
50
+ if (typeof link === 'string' && /^https?:\/\//i.test(link)) { if (!group.columns.includes('Source')) group.columns.push('Source');row.set('Source', link); }
51
+ }
52
+ const humanize = label => label.replace(/([a-z])([A-Z])/g, '$1 $2').replaceAll('_', ' ');
53
+ const tables = [...groups.values()].map(g => ({ title: g.name, columns: g.columns.map(humanize), rows: [...g.rows.values()].map(r => g.columns.map(c => r.get(c))), note: 'Only records and fields cited in this saved answer. Full source data is available in Details.' }));
54
+ if (/\b(vergelijk|compare|comparison)\b/i.test(question) && tables.length > 1 && tables.every(t => t.columns[0] === 'Field') && new Set(tables.map(t => t.title)).size === tables.length) {
55
+ const fields = [...new Set(tables.flatMap(t => t.rows.map(r => String(r[0]))))];
56
+ return [{ title: 'Comparison', columns: ['Feature', ...tables.map(t => t.title)], rows: fields.map(f => [humanize(f), ...tables.map(t => t.rows.find(r => r[0] === f)?.[1])]), note: 'Only facts cited in the saved answer. Missing values are unknown; retain differences in units and reporting periods.' }];
57
+ }
58
+ return tables;
59
+ }
60
+
61
+ // ---- DOM, run inside the card only --------------------------------------------
62
+ function renderAnswerPresentation(question, results, analysis) {
63
+ const p = analysis?.presentation, tables = p && typeof p === 'object' && 'title' in p ? [presentationTable(p, results, answerLocale(question, analysis))] : legacyAnswerTables(question, results, analysis);
64
+ if (!tables.length) return null;
65
+ const wrap = el('div', 'answer-blocks');wrap.setAttribute('aria-label', 'Answer data');
66
+ for (const table of tables) wrap.append(selectedTable(table, p?.kind === 'chart'));
67
+ return wrap;
68
+ }
69
+ function selectedTable(table, chart) {
70
+ const wrap = el('div', 'answer-table');let shown = 10;
71
+ const draw = () => {
72
+ const visible = { ...table, rows: table.rows.slice(0, shown) }, parts = [], figure = chart ? presentationChart(visible, table) : null;
73
+ if (figure) parts.push(figure);
74
+ parts.push(sourceTable(visible));
75
+ if (shown < table.rows.length) parts.push(blockMore('Show more selected rows (' + (table.rows.length - shown) + ' remaining)', () => { shown += 10;draw(); }));
76
+ if (shown > 10) parts.push(blockMore('Show fewer rows', () => { shown = 10;draw(); }));
77
+ wrap.replaceChildren(...parts);
78
+ };
79
+ draw();
80
+ return wrap;
81
+ }
82
+ function presentationChart(table, scale) {
83
+ const points = t => t.rows.filter(r => typeof r[0] === 'string' && typeof r[1] === 'number' && Number.isFinite(r[1]));
84
+ const shown = points(table);
85
+ if (!shown.length || shown.length !== table.rows.length) return null;
86
+ const all = points(scale).map(r => r[1]), min = Math.min(0, ...all), max = Math.max(0, ...all), range = max - min || 1, zero = (0 - min) / range * 100;
87
+ const figure = el('figure', 'answer-block bar-chart');figure.setAttribute('aria-label', table.title + ' chart');
88
+ figure.append(el('figcaption', '', table.title + ' · ' + table.columns[1]));
89
+ shown.forEach(([label, value], i) => {
90
+ const row = el('div', 'bar-row'), line = el('div', 'bar-line');
91
+ line.append(el('span', 'bar-label', label), el('span', 'fact-value', String(table.displayRows?.[i]?.[1] ?? new Intl.NumberFormat(table.locale ?? 'nl-NL', { maximumFractionDigits: 10 }).format(value))));
92
+ row.append(line, answerBar(zero, (value - min) / range * 100));figure.append(row);
93
+ });
94
+ return figure;
95
+ }
96
+ function presentationCell(value, key, locale = 'nl-NL') {
97
+ if (value == null || value === '') return el('span', '', '—');
98
+ if (Array.isArray(value)) { const list = el('ul', 'cell-list');for (const item of value) { const li = el('li');li.append(presentationCell(item, key, locale));list.append(li); }return list; }
99
+ if (typeof value === 'number' || typeof value === 'string' && /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) return el('span', '', answerValue(value, key, null, locale));
100
+ if (typeof value === 'boolean') return el('span', '', value ? 'Yes' : 'No');
101
+ if (typeof value !== 'string') return el('span', '', JSON.stringify(displayData(value, key)));
102
+ if (/^https?:\/\//i.test(value)) { try { const url = new URL(value);if (!url.username && !url.password) { const link = bodyLink(url.href, 'Open source ↗');link.title = url.href;link.setAttribute('aria-label', 'Open source: ' + url.hostname);return link; } } catch {} }
103
+ return el('span', '', formatDisplayText(value, key));
104
+ }
105
+ function sourceTable(table) {
106
+ const s = el('section', 'answer-block');s.setAttribute('aria-label', table.title);
107
+ s.append(el('h3', '', table.title), el('p', 'meta', table.rows.length + ' rows shown' + (table.total !== undefined ? ' · ' + new Intl.NumberFormat('nl-NL').format(table.total) + ' total matches' : '') + '. ' + table.note));
108
+ if (!table.rows.length) { s.append(el('p', '', 'No records returned.'));return s; }
109
+ const link = row => { for (const value of row.slice(1)) { if (typeof value !== 'string' || !/^https?:\/\//i.test(value)) continue;try { const url = new URL(value);if (!url.username && !url.password) return url.href; } catch {} }return null; };
110
+ const region = blockTable(table.title + ' table', table.columns, table.rows.map((row, i) => {
111
+ const tr = el('tr'), href = typeof row[0] === 'string' && !/^https?:\/\//i.test(row[0]) ? link(row) : null;
112
+ row.forEach((value, j) => {
113
+ const td = el('td'), shown = table.displayRows?.[i]?.[j] ?? value, cell = presentationCell(shown, j === 0 || table.columns[j] !== 'Source value' ? table.columns[j] : String(row[0]), table.locale);
114
+ td.append(j === 0 && href ? bodyLink(href, cell.textContent) : cell);tr.append(td);
115
+ });
116
+ return tr;
117
+ }));
118
+ if (table.columns.length > 3) region.querySelector('table')?.setAttribute('style', 'min-width:' + table.columns.length * 150 + 'px');
119
+ s.append(region);
120
+ return s;
121
+ }
122
+
123
+ export const V2_CARD_PRESENTATION = [
124
+ presentationTable, presentationEvidence, legacyAnswerTables, renderAnswerPresentation, selectedTable, presentationChart, presentationCell, sourceTable,
125
+ ].map(fn => fn.toString()).join('\n');
@@ -0,0 +1,63 @@
1
+ import { V2_CARD_CBS } from './gateway-v2-card-cbs.mjs';
2
+ import { V2_CARD_VERDICT } from './gateway-v2-card-verdict.mjs';
3
+ import { V2_CARD_ANSWER } from './gateway-v2-card-answer.mjs';
4
+ import { V2_CARD_ANSWER_TEXT } from './gateway-v2-card-answer-text.mjs';
5
+ import { V2_CARD_BLOCKS } from './gateway-v2-card-blocks.mjs';
6
+ import { V2_CARD_PRESENTATION } from './gateway-v2-card-presentation.mjs';
7
+ // All source values remain text nodes. This layer has no purchasing authority.
8
+ export const V2_CARD_RESEARCH = `
9
+ ${V2_CARD_CBS}
10
+ ${V2_CARD_VERDICT}
11
+ ${V2_CARD_ANSWER_TEXT}
12
+ ${V2_CARD_BLOCKS}
13
+ ${V2_CARD_PRESENTATION}
14
+ ${V2_CARD_ANSWER}
15
+ const renderSingleResult=renderResult;
16
+ const sourceDisclosureState=new Map();
17
+ function sourceGroupKey(result){const source=result?.source||{};return source.provider||source.name||source.url||'Source'}
18
+ function sourceMetric(group,names){for(const result of group){const body=displayData(result?.data!=null?result.data:result);if(!body||typeof body!=='object'||Array.isArray(body))continue;for(const name of names){const value=body[name];if(value!=null&&['string','number','boolean'].includes(typeof value))return value}}return null}
19
+ function sourceResultCount(group){const explicit=sourceMetric(group,['total_results','totalResults','total_count','totalCount','number_of_results','numberOfResults']);if(explicit!=null)return explicit;let count=0,found=false;for(const result of group){const body=displayData(result?.data!=null?result.data:result);for(const name of ['results','items','resultaten'])if(Array.isArray(body?.[name])){count+=body[name].length;found=true}if(body?.responses&&typeof body.responses==='object'){for(const response of Object.values(body.responses)){const total=response?.total?.value;if(typeof total==='number'){count+=total;found=true}else if(Array.isArray(response?.results)){count+=response.results.length;found=true}}}}return found?count:null}
20
+ function sourceSummaryCell(label,value){const cell=el('span','source-result-cell');cell.append(el('span','source-result-label',label),el('span','source-result-value',value==null||value===''?'—':value));return cell}
21
+ function renderSourceGroup(data,group){const source=group[0]?.source||{},details=el('details','source-result-group'),summary=el('summary','source-result-summary'),logo=sourceLogo(source);logo.classList.add('mini');summary.append(logo,el('span','source-result-name',source.name||source.provider||'Source'),sourceSummaryCell('Kind',sourceMetric(group,['kind'])),sourceSummaryCell('Results',sourceResultCount(group)),sourceSummaryCell('Items per page',sourceMetric(group,['items_per_page','itemsPerPage','page_size','pageSize'])),sourceSummaryCell('Page number',sourceMetric(group,['page_number','pageNumber','page'])),sourceSummaryCell('Start index',sourceMetric(group,['start_index','startIndex','offset'])));const body=el('div','source-result-details');for(const result of group){renderSingleResult({...data,result});const node=sections.lastElementChild;if(node)body.append(node)}details.append(summary,body);details.ontoggle=()=>window.apiosk.resize();return details}
22
+ function reportButton(label,url){const b=el('button','text-action',label);b.onclick=()=>window.apiosk.openLink(url);return b}
23
+ // The App's details drawer (GatewayV2OutputDetails): downloads, limitations and
24
+ // cited evidence above the source records. The answer text already carries the
25
+ // observations, so they are not repeated here.
26
+ function detailNotes(analysis,data,cbs){
27
+ const notes=el('div','detail-notes'),limitations=(analysis.limitations||[]).filter(t=>typeof t==='string'&&t.trim()),evidence=(analysis.observations||[]).flatMap(o=>o?.evidence||[]);
28
+ if(limitations.length){const list=el('ul');for(const limitation of limitations)list.append(el('li','',formatDisplayNarrative(limitation,data)));notes.append(el('h4','',cbs?'Toelichting':'Source limitations'),list)}
29
+ if(evidence.length){const list=el('ul','evidence-list');for(const item of evidence){const li=el('li');li.append(el('span','',(item.source?.name??formatDisplayNarrative(item.subject?.label||'Source',data))+' · '+(item.field??item.pointer)+': '+formatSourceValue(item.value,item.field||item.pointer,null,data)),el('span','fact-source',item.pointer));list.append(li)}notes.append(el('h4','',cbs?'Onderbouwing':'Source evidence'),list)}
30
+ return notes.children.length?notes:null}
31
+ renderResult=function(data){
32
+ const results=data.context_view?.results?.length?data.context_view.results:data.result==null?[]:[data.result],analysis=data.context_view?.analysis;
33
+ const verdict=supplierVerdictView(supplierVerdictAnalysis(data)),cbs=verdict?null:cbsAnnualView(data,results);const surface=verdict?renderSupplierVerdict(verdict):cbs?renderCbsAnnual(cbs):renderResearchAnswer(data,results);
34
+ // Without an analysis the source result is the answer, drawn as the App's
35
+ // purchase panel draws it: the primary result open, lookups under details.
36
+ let primary=null,primarySection=null;
37
+ if(!surface&&!analysis&&!data.context_view?.analyzing&&results.length){primary=data.result??results.at(-1);renderSingleResult({...data,result:primary});primarySection=sections.lastElementChild}
38
+ const rest=primary?results.filter(r=>r!==primary&&!(r?.result_ref&&r.result_ref===primary.result_ref)):results;
39
+ const report=data.context_view?.report,question=data.context_view?.conversation?.at(-1)?.question||'';
40
+ const pdf=report?.format==='pdf'&&typeof report.url==='string'&&report.url.startsWith('https://')?report.url:null,zip=typeof report?.evidence_url==='string'&&report.evidence_url.startsWith('https://')?report.evidence_url:null;
41
+ const notes=analysis&&!verdict?detailNotes(analysis,data,cbs):null;
42
+ if(rest.length||notes||pdf||zip){
43
+ const sourceCount=new Set(rest.map(result=>result.source?.provider||result.source?.name||'Source')).size;
44
+ const s=section('Sources and details');s.classList.add('source-results-section');
45
+ const disclosure=el('details','source-results-toggle');disclosure.append(el('summary','','Sources and details'+(rest.length?' · '+sourceCount+(sourceCount===1?' source':' sources')+' · '+rest.length+(rest.length===1?' result':' results'):'')));
46
+ const key=data.state?.state_ref;disclosure.open=key?sourceDisclosureState.get(key)===true:false;
47
+ disclosure.ontoggle=()=>{if(key){sourceDisclosureState.set(key,disclosure.open);if(sourceDisclosureState.size>20)sourceDisclosureState.delete(sourceDisclosureState.keys().next().value)}window.apiosk.resize()};
48
+ if(pdf||zip){const links=el('div','result-links');if(pdf)links.append(reportButton('Download PDF',pdf));if(zip)links.append(reportButton('Download evidence',zip));disclosure.append(links)}
49
+ if(notes)disclosure.append(notes);
50
+ const list=el('div','source-results-list'),groups=new Map();disclosure.append(list);s.append(disclosure);
51
+ for(const result of rest){if(cbs?.records.includes(result))continue;const key=sourceGroupKey(result);if(!groups.has(key))groups.set(key,[]);groups.get(key).push(result)}
52
+ for(const group of groups.values())list.append(renderSourceGroup(data,group));
53
+ }
54
+ if(surface?.title){byId('title').textContent=surface.title;byId('subtitle').textContent=surface.subtitle}
55
+ // A report is shown with the answer only when the question asked for a document.
56
+ if(pdf&&answerDocumentRequested(analysis,question)){const target=surface?.links||primarySection;if(target){const links=surface?.links||el('div','result-links');links.append(reportButton('Download PDF',pdf));if(!surface)target.append(links)}}
57
+ if(data.context_view?.analyzing)section('Analysis').append(el('p','notice','Analyzing the saved source results…'));
58
+ const cooldown=data.context_view?.cooldown;
59
+ if(cooldown){const kvk=/KVK/.test(cooldown.message||''),s=section(kvk?'Waiting for KVK':'Waiting for the source'),timer=el('p','notice');s.append(el('p','meta',cooldown.message),timer);const tick=()=>{const seconds=Math.max(0,Math.ceil((Date.parse(cooldown.until)-Date.now())/1000));timer.textContent=seconds?(kvk?'Next annual report in ':'Checking again in ')+Math.floor(seconds/60)+':'+String(seconds%60).padStart(2,'0'):'Continuing the approved request…'};tick();const interval=setInterval(()=>{if(!timer.isConnected){clearInterval(interval);return}tick()},1000);if(data.billing?.authorization_active&&data.billing.quote_ref===data.proposal?.quote_ref)watchUntil=Math.max(watchUntil,Date.parse(cooldown.until)+300000)}
60
+ };
61
+ const renderOriginalPlan=renderPlan;
62
+ renderPlan=function(data){renderOriginalPlan(data);if(!planSurface)return;const notices=data.context_view?.coverage_notices;if(Array.isArray(notices)&&notices.length){const coverage=el('div','plan-coverage');coverage.append(el('p','key','Scope of this check'));for(const notice of notices.slice(0,8)){if(typeof notice==='string')coverage.append(el('p','notice',notice))}planSurface.append(coverage)}const rows=planSurface.querySelectorAll('.step');for(const [index,row] of rows.entries()){const detail=data.proposal?.step_details?.[index];const copy=row.lastElementChild;if(detail?.subject)copy.prepend(el('p','step-title',formatDisplayNarrative(detail.subject,data)));if(detail?.error?.message)copy.append(el('p','notice error',detail.error.message))}};
63
+ `;
@@ -0,0 +1,32 @@
1
+ import { V2_SOURCE_VALUE_FORMAT } from './source-value-format.mjs';
2
+ import { V2_CARD_BODY } from './gateway-v2-card-body.mjs';
3
+ // One source result, as the App shows it: the readable answer body (purchase
4
+ // panel), filing fields and KVK company rows (GatewayV2Result), then the full
5
+ // source data collapsed. Source JSON stays unchanged; formatting is display-only.
6
+ export const V2_CARD_RESULT = `
7
+ ${V2_SOURCE_VALUE_FORMAT}
8
+ ${V2_CARD_BODY}
9
+ function filingRows(fields,depth=0,rows=[],currency=null){if(depth>12||!Array.isArray(fields))return rows;for(const field of fields.slice(0,200)){if(rows.length>=100)break;if(field&&typeof field.key==='string'&&['string','number','boolean'].includes(typeof field.value))rows.push([field.key.replace(/([a-z])([A-Z])/g,'$1 $2'),formatSourceValue(field.value,field.key,sourceCurrency(field)||currency)]);if(field?.opendataFields)filingRows(field.opendataFields,depth+1,rows,currency)}return rows}
10
+ function resultBody(result){return result&&typeof result==='object'?result.data!==undefined?result.data:result.preview_data!==undefined?result.preview_data:result:result}
11
+ function companyTable(companies,body){
12
+ const wrap=el('div','result-companies'),region=el('div','body-table-wrap'),table=el('table','body-table'),head=el('tr'),thead=el('thead'),tbody=el('tbody');
13
+ wrap.append(el('p','meta',companies.length+' results shown'+(typeof body.totaal==='number'?' of '+body.totaal:'')+(typeof body.pagina==='number'?' · Page '+body.pagina:'')));
14
+ for(const label of ['Company','KVK number','City'])head.append(el('th','',label));thead.append(head);
15
+ for(const row of companies){const tr=el('tr');for(const value of [row.naam,row.kvkNummer,row.adres?.binnenlandsAdres?.plaats??'—'])tr.append(el('td','',value));tbody.append(tr)}
16
+ table.append(thead,tbody);region.append(table);wrap.append(region);
17
+ if(body.volgende)wrap.append(el('p','meta','The source has more results. This response contains the current page.'));
18
+ return wrap}
19
+ function renderResult(data){if(data.result==null)return;byId('title').textContent=data.status==='partial'?'Partial source result':'Source result';byId('subtitle').textContent='Returned by the selected source.';
20
+ const result=data.result,source=result&&result.source||{},subject=result?.subject?.label,s=section(typeof subject==='string'&&subject?'Result · '+formatDisplayNarrative(subject,result):'Result'),wrap=el('div','result'),body=displayData(resultBody(result)),currency=sourceCurrency(body);
21
+ const companies=Array.isArray(body?.resultaten)&&body.resultaten.every(r=>typeof r?.naam==='string'&&typeof r?.kvkNummer==='string')?body.resultaten:null,filing=Array.isArray(body?.opendataFields);
22
+ const report=result?.report,links=el('div','result-links');wrap.append(links);
23
+ if(report?.format==='pdf'&&typeof report.url==='string'&&report.url.startsWith('https://')){const download=el('button','text-action','Download PDF');download.onclick=()=>window.apiosk.openLink(report.url);links.append(download)}
24
+ if(companies)wrap.append(companyTable(companies,body));
25
+ else if(filing){wrap.append(el('p','meta','Selected fields from the deposited annual accounts. '+(currency?'Currency: '+currency+'.':'No currency is specified in this response.')));const dl=el('dl','body-fields');for(const [key,value] of filingRows(body.opendataFields,0,[],currency)){const row=el('div','result-row');row.append(el('dt','key',key),el('dd','value',value));dl.append(row)}wrap.append(dl)}
26
+ else wrap.append(answerBody(resultBody(result)));
27
+ const origin=el('div','result-source');origin.append(sourceLine(source));
28
+ if(typeof source.url==='string'&&source.url.startsWith('https://')){const link=el('button','text-action','View source');link.onclick=()=>window.apiosk.openLink(source.url);origin.append(link)}
29
+ if(source.retrieved_at)origin.append(el('span','meta','Retrieved '+new Date(source.retrieved_at).toLocaleString()));
30
+ const full=el('details','full-result');full.append(el('summary','','Full source data'),el('pre','',JSON.stringify(result,null,2)));full.ontoggle=()=>window.apiosk.resize();
31
+ wrap.append(origin,full);s.append(wrap)}
32
+ `;
@@ -0,0 +1,39 @@
1
+ // Runs inside the card: the source search view (`view: "source_search"`), as
2
+ // the App's Ask v2 source table draws it. Read-only: nothing here quotes,
3
+ // approves or buys; a row only says what a source is, costs and needs.
4
+ export const V2_CARD_SEARCH = `
5
+ function searchPriceAtomic(candidate){
6
+ const exact=candidate?.endpoint?.price?.buyer_atomic;if(typeof exact==='string'&&/^\\d+$/.test(exact))return exact;
7
+ const match=String(candidate?.price??'').trim().match(/^(\\d+)(?:\\.(\\d{1,6}))?(?:\\s+USDC?)?$/i);
8
+ return match?String(BigInt(match[1])*1000000n+BigInt((match[2]||'').padEnd(6,'0'))):null}
9
+ function searchInputLabel(input){
10
+ const names={'company.name':'Company name','company_registry.kvknummer':'KVK number','company.registration.lei':'LEI code','domain.name':'Website domain','web.query':'Search query'};
11
+ return input?.schema?.title||names[input?.field]||pretty(input?.field||'')}
12
+ function searchRow(candidate){
13
+ const source=candidate.endpoint?.source||{},row=el('div','search-row'),copy=el('div','search-copy'),side=el('div','search-side');
14
+ const name=candidate.name||source.name||candidate.source_slug||'Source',supported=candidate.availability==='supported';
15
+ copy.append(el('p','search-name',name));
16
+ const about=candidate.description||candidate.endpoint?.description;if(about)copy.append(el('p','search-about',about));
17
+ const inputs=supported?(candidate.endpoint?.inputs||[]).filter(input=>input?.required):[];
18
+ if(inputs.length)copy.append(el('p','search-inputs','Needs '+inputs.map(searchInputLabel).filter(Boolean).join(', ')));
19
+ const atomic=searchPriceAtomic(candidate),price=atomic&&money(atomic,'USD');
20
+ side.append(el('span','search-price',price||'—'),el('span','search-state'+(supported?' supported':''),({supported:'Available',discovery_only:'Discovery only',unverified:'Unverified'})[candidate.availability]||pretty(candidate.availability||'Unverified')));
21
+ row.append(sourceLogo({name,logo_url:candidate.logo_url||source.logo_url}),copy,side);return row}
22
+ function renderSourceSearch(data){
23
+ const matches=Array.isArray(data.matches)?data.matches:[],capabilities=data.parsed_request?.capabilities||[],seen=new Set();let total=0;
24
+ for(const match of matches)for(const candidate of [...(match.apiosk||[]),...(match.coinbase||[])]){const key=candidate.endpoint_id||candidate.url||(candidate.source_slug||candidate.name)+':'+(candidate.capability||'');if(!seen.has(key)){seen.add(key);total++}}
25
+ byId('title').textContent=total===1?'1 matching source':total+' matching sources';
26
+ byId('subtitle').textContent=Number.isInteger(data.apiosk_sources_searched)?'Searched '+data.apiosk_sources_searched+' Apiosk sources. Nothing is bought from this list.':'Nothing is bought from this list.';
27
+ if(!matches.length)section('Sources').append(el('p','meta','No requested capability was recognised. Rephrase the question or browse all sources.'));
28
+ for(const match of matches){
29
+ const capability=capabilities.find(c=>c?.slug===match.slug),candidates=[...(match.apiosk||[]),...(match.coinbase||[])];
30
+ const s=section(capability?.name||pretty(match.slug||'Requested data'),candidates.length===1?'1 source':candidates.length+' sources');s.classList.add('search-section');
31
+ if(capability?.description)s.append(el('p','meta search-capability',capability.description));
32
+ const list=el('div','search-list');list.setAttribute('role','list');
33
+ for(const candidate of candidates){const row=searchRow(candidate);row.setAttribute('role','listitem');list.append(row)}
34
+ if(candidates.length)s.append(list);else s.append(el('p','meta','No matching source was found for this part of the question.'));
35
+ if(match.coinbase_status==='unavailable')s.append(el('p','meta','Coinbase Bazaar search is unavailable right now.'));
36
+ }
37
+ if(data.notice)section('About these results').append(el('p','meta',data.notice));
38
+ }
39
+ `;
@@ -0,0 +1,32 @@
1
+ // Runs inside the card: the source list, and what it is honest to print on it.
2
+ export const V2_CARD_SOURCES = `
3
+ /* Buyer-facing counts include only runtime-supported functions. Registered
4
+ endpoints belong in the integration audit, never in this overview. */
5
+ function coverage(source){const r=source&&source.readiness,c=(r&&r.contracts)||{};
6
+ if(!r||!r.status)return{label:'Availability unknown',unavailable:false};
7
+ if(r.status==='blocked')return{label:'Not purchasable',unavailable:true};
8
+ const usable=c.supported_endpoints||0;
9
+ if(!usable)return{label:'Not executable',unavailable:true};
10
+ return{label:text(usable)+(usable===1?' available function':' available functions'),unavailable:false}}
11
+ function renderSources(data){
12
+ const rows=Array.isArray(data.sources)?data.sources:[];
13
+ byId('title').textContent=data.total===1?'1 matching source':text(data.total||rows.length)+' matching sources';
14
+ byId('subtitle').textContent='Choose a source or narrow the search.';
15
+ const s=section('Sources',text(data.offset||0)+'–'+text((data.offset||0)+rows.length)+' of '+text(data.total||rows.length)),list=el('div','sources');
16
+ for(const source of rows){
17
+ const group=el('div'),row=el('div','source'),copy=el('div'),name=el('div','source-name',source.name||source.slug),meta=el('div','source-meta',[source.category,...(source.capabilities||[]).slice(0,2)].filter(Boolean).map(pretty).join(' · '));
18
+ copy.append(name,meta);const reach=coverage(source);
19
+ row.append(sourceLogo(source),copy,el('span','count'+(reach.unavailable?' unavailable':''),source.service_count?text(source.service_count)+' services':reach.label));
20
+ group.append(row);
21
+ if(Array.isArray(source.services)&&source.services.length){
22
+ const count=source.matching_service_count||source.services.length,detail=el('details','compact-details');detail.append(el('summary','','View '+text(count)+(count===1?' service':' services')));
23
+ for(const service of source.services){const item=el('div','source-service');item.append(el('strong','',service.name),el('p','meta',service.description));detail.append(item)}
24
+ detail.ontoggle=()=>window.apiosk.resize();group.append(detail);
25
+ }
26
+ list.append(group);
27
+ }
28
+ s.append(list);const actions=el('div','actions');
29
+ if(Number.isInteger(data.next_offset)){const b=el('button','quiet','Next page');b.onclick=async()=>{if(busy)return;busy=true;showFeedback('Loading the next sources…');try{const args={...input,offset:data.next_offset,limit:input.limit||20};render(await window.apiosk.callTool('apiosk_sources',args))}catch(e){showFeedback(e&&e.message||'Could not load the next page.','error')}finally{busy=false}};actions.append(b)}
30
+ if(actions.childNodes.length)s.append(actions)
31
+ }
32
+ `;