@vpxa/kb 0.1.32 → 0.1.34
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/package.json +1 -1
- package/packages/analyzers/dist/diagram-generator.js +2 -2
- package/packages/analyzers/dist/pattern-analyzer.js +1 -1
- package/packages/analyzers/dist/regex-call-graph.js +1 -1
- package/packages/cli/dist/commands/init/constants.d.ts +2 -0
- package/packages/cli/dist/commands/init/constants.js +1 -1
- package/packages/present/dist/index.html +321 -22
- package/packages/server/dist/server.js +1 -1
- package/packages/server/dist/tools/onboard.tool.js +2 -2
- package/packages/server/dist/tools/present/browser.d.ts +4 -0
- package/packages/server/dist/tools/present/browser.js +93 -0
- package/packages/server/dist/tools/present/helpers.d.ts +18 -0
- package/packages/server/dist/tools/present/helpers.js +1 -0
- package/packages/server/dist/tools/present/html.d.ts +18 -0
- package/packages/server/dist/tools/present/html.js +5 -0
- package/packages/server/dist/tools/present/index.d.ts +2 -0
- package/packages/server/dist/tools/present/index.js +1 -0
- package/packages/server/dist/tools/present/markdown.d.ts +17 -0
- package/packages/server/dist/tools/present/markdown.js +8 -0
- package/packages/server/dist/tools/present/templates.d.ts +14 -0
- package/packages/server/dist/tools/present/templates.js +472 -0
- package/packages/server/dist/tools/present/tool.d.ts +25 -0
- package/packages/server/dist/tools/present/tool.js +19 -0
- package/packages/server/dist/tools/present.tool.d.ts +1 -6
- package/packages/server/dist/tools/present.tool.js +1 -114
- package/packages/tools/dist/compact.js +1 -1
- package/packages/tools/dist/file-cache.js +3 -3
- package/packages/tools/dist/file-summary.js +1 -1
- package/packages/tools/dist/onboard.d.ts +7 -0
- package/packages/tools/dist/onboard.js +4 -4
- package/skills/present/SKILL.md +283 -12
|
@@ -1,114 +1 @@
|
|
|
1
|
-
import{
|
|
2
|
-
- "html" (default): Rich markdown in chat + embedded UIResource for MCP-UI hosts. Actions shown as numbered choices.
|
|
3
|
-
- "browser": Rich markdown in chat + opens beautiful themed dashboard in the browser. When actions are provided, the browser shows interactive buttons and the tool blocks until the user clicks, returning their selection.`),title:h.string().optional().describe(`Optional heading`),content:h.any().describe(`Content to present. Accepts: markdown string, array of objects (→ table), { nodes, edges } (→ mermaid graph), typed blocks [{ type, value }], or any JSON (→ tree).`),actions:h.array(O).optional().describe(`Interactive actions (buttons/selects). In html mode, shown as numbered choices. In browser mode, rendered as clickable buttons and the tool blocks until user clicks.`)};let A=!1;function j(t,n){let r=e(`present`);A||=(b(t,`KB Present App`,w,{description:`Rich interactive content viewer for KB tools`},async()=>({contents:[{uri:w,mimeType:y,text:D()}]})),!0),x(t,`present`,{title:r.title,description:`Present content to the user in the best format. Two modes:
|
|
4
|
-
- "html" (default): Rich markdown in chat + embedded UIResource. Actions shown as numbered choices for display.
|
|
5
|
-
- "browser": Rich markdown in chat + serves a themed dashboard on a local URL that auto-opens in your system browser. If openBrowserPage is available, also call it with the returned URL. When actions are provided, browser shows interactive buttons and the tool blocks until user clicks.
|
|
6
|
-
Use "html" for in-chat display. Use "browser" for visual dashboards and when you need interactive user input back.
|
|
7
|
-
After calling present with format "browser", extract the URL from the response and call openBrowserPage({ url }) to open it in VS Code.`,annotations:r.annotations,inputSchema:k,_meta:{ui:{resourceUri:w}}},async({format:e,title:t,content:r,actions:i})=>(e??`html`)===`browser`?await N(t,r,i,n):J(t,r,i))}let M=null;process.on(`exit`,()=>{if(M){try{M.close()}catch{}M=null}});async function N(e,t,n,r){let i=P(e,t),a=q(e,t,n),o,s,c=Array.isArray(n)?n:[],l=``;try{M&&=(M.close(),null),c.length>0&&(o=new Promise(e=>{s=e})),l=await new Promise((e,t)=>{let n=_((e,t)=>{if(e.method===`POST`&&e.url===`/callback`){let n=``;e.on(`data`,e=>{n+=e.toString()}),e.on(`end`,()=>{t.writeHead(200,{"Content-Type":`application/json`,"Access-Control-Allow-Origin":`*`}),t.end(`{"ok":true}`);try{let e=JSON.parse(n);s?.(e)}catch{}});return}if(e.method===`OPTIONS`){t.writeHead(204,{"Access-Control-Allow-Origin":`*`,"Access-Control-Allow-Methods":`POST`,"Access-Control-Allow-Headers":`Content-Type`}),t.end();return}t.writeHead(200,{"Content-Type":`text/html; charset=utf-8`}),t.end(a)});n.listen(0,`127.0.0.1`,()=>{let r=n.address();typeof r==`object`&&r?(M=n,e(`http://127.0.0.1:${r.port}`)):t(Error(`Failed to start present server`))}),setTimeout(()=>{n.close(),M===n&&(M=null)},300*1e3).unref()});try{g(process.platform===`win32`?`start "" "${l}"`:process.platform===`darwin`?`open "${l}"`:`xdg-open "${l}"`)}catch{}}catch{}let u=v({uri:`ui://kb/present-static.html`,content:{type:`rawHtml`,htmlString:a},encoding:`text`,adapters:{mcpApps:{enabled:!0}}}),d=l?`${i}\n\n---\n🌐 **Dashboard opened in browser:** ${l}`:i;if(c.length>0&&r?.available&&o)try{let e=await Promise.race([o,new Promise((e,t)=>setTimeout(()=>t(Error(`timeout`)),300*1e3))]);return{content:[{type:`text`,text:`${d}\n\n✅ **Selected:** ${e.actionId} = \`${e.value}\``},u]}}catch{return{content:[{type:`text`,text:`${d}\n\n⚠️ *No selection received (timed out).*`},u]}}return{content:[{type:`text`,text:d},u]}}function P(e,t){let n=[];if(e&&n.push(`# ${e}\n`),typeof t==`string`)n.push(t);else if(Array.isArray(t))if(t.length===0)n.push(`*(empty)*`);else if(Z(t[0]))for(let e of t)n.push(F(e));else if(typeof t[0]==`object`&&t[0]!==null)n.push(I(t));else for(let e of t)n.push(`- ${String(e)}`);else if(typeof t==`object`&&t){let e=t;Array.isArray(e.nodes)&&Array.isArray(e.edges)?n.push(R(e)):Array.isArray(e.metrics)?n.push(L(e.metrics)):n.push(z(e))}else n.push(String(t));return n.join(`
|
|
8
|
-
`)}function F(e){let t=[];switch(e.title&&t.push(`## ${e.title}\n`),e.type){case`markdown`:t.push(String(e.value??``));break;case`code`:t.push(`\`\`\`${e.language??``}\n${String(e.value??``)}\n\`\`\``);break;case`mermaid`:t.push(`\`\`\`mermaid\n${String(e.value??``)}\n\`\`\``);break;case`table`:Array.isArray(e.value)&&t.push(I(e.value));break;case`metrics`:Array.isArray(e.value)&&t.push(L(e.value));break;case`graph`:e.value&&typeof e.value==`object`&&t.push(R(e.value));break;case`cards`:if(Array.isArray(e.value))for(let n of e.value)t.push(`### ${n.title??`Card`}`),(n.body||n.description)&&t.push(String(n.body??n.description)),(n.badge||n.status)&&t.push(`> **${n.badge??n.status}**`),t.push(``);break;case`tree`:e.value&&typeof e.value==`object`&&t.push(z(e.value));break;case`chart`:{let n=e.value;n?.data&&Array.isArray(n.data)&&t.push(`*${String(n.chartType??`chart`)} chart — ${n.data.length} data points*`);break}case`timeline`:{let n=e.value;if(n?.items)for(let e of n.items){let n=e.status===`done`?`✅`:e.status===`active`?`🔄`:e.status===`error`?`❌`:`⬜`;t.push(`${n} **${e.title}**${e.description?` — ${e.description}`:``}`)}break}case`checklist`:{let n=e.value;if(n?.items)for(let e of n.items)t.push(`- [${e.checked?`x`:` `}] ${e.label}${e.note?` — ${e.note}`:``}`);break}case`comparison`:{let n=e.value;if(n?.columns&&n.columns.length>0){let e=Math.max(...n.columns.map(e=>e.items?.length??0)),r=n.columns.map(e=>e.title);t.push(`| ${r.join(` | `)} |`),t.push(`| ${r.map(()=>`---`).join(` | `)} |`);for(let r=0;r<e;r++)t.push(`| ${n.columns.map(e=>e.items?.[r]??``).join(` | `)} |`)}break}case`status-board`:{let n=e.value;if(n?.items)for(let e of n.items){let n=e.status===`success`?`🟢`:e.status===`warning`?`🟡`:e.status===`error`?`🔴`:e.status===`info`?`🔵`:`⚪`;t.push(`${n} **${e.label}**${e.detail?` — ${e.detail}`:``}`)}break}case`prompt`:{let n=e.value;n?.question&&(t.push(`> **${n.question}**`),n.context&&t.push(`> ${n.context}`));break}case`progress`:{let n=e.value;if(n?.items)for(let e of n.items){let n=e.max??100,r=n>0?Math.round(e.value/n*100):0,i=`█`.repeat(Math.round(r/5))+`░`.repeat(20-Math.round(r/5));t.push(`${e.label}: ${i} ${r}%`)}break}case`text`:t.push(String(e.value??``));break;case`heading`:t.push(`## ${String(e.value??``)}\n`);break;case`separator`:t.push(`---
|
|
9
|
-
`);break;default:t.push(JSON.stringify(e.value,null,2))}return t.push(``),t.join(`
|
|
10
|
-
`)}function I(e){if(e.length===0)return`*(empty table)*`;let t=Object.keys(e[0]),n=[];n.push(`| ${t.join(` | `)} |`),n.push(`| ${t.map(()=>`---`).join(` | `)} |`);for(let r of e)n.push(`| ${t.map(e=>String(r[e]??``)).join(` | `)} |`);return n.join(`
|
|
11
|
-
`)}function L(e){return e.map(e=>`- **${e.label}**: ${e.value}`).join(`
|
|
12
|
-
`)}function R(e){let t=["```mermaid",`graph LR`];for(let n of e.nodes){let e=X(String(n.id??n.name??``)),r=String(n.label??n.name??e);t.push(` ${e}["${r}"]`)}for(let n of e.edges){let e=X(String(n.source??n.from??``)),r=X(String(n.target??n.to??``)),i=n.label?`|${String(n.label)}|`:``;t.push(` ${e} -->${i} ${r}`)}return t.push("```"),t.join(`
|
|
13
|
-
`)}function z(e,t=0){let n=` `.repeat(t),r=[];for(let[i,a]of Object.entries(e))typeof a==`object`&&a&&!Array.isArray(a)?(r.push(`${n}- **${i}**:`),r.push(z(a,t+1))):r.push(`${n}- **${i}**: ${Y(a)}`);return r.join(`
|
|
14
|
-
`)}function B(e,n){let r=[];if(e&&r.push(`<h1>${t(e)}</h1>`),typeof n==`string`)r.push(`<div class="md-content">${S.parse(n)}</div>`);else if(Array.isArray(n))if(n.length===0)r.push(`<p><em>empty</em></p>`);else if(Z(n[0]))for(let e of n)r.push(V(e));else typeof n[0]==`object`&&n[0]!==null?r.push(H(n)):r.push(`<ul>${n.map(e=>`<li>${t(String(e))}</li>`).join(``)}</ul>`);else if(typeof n==`object`&&n){let e=n;Array.isArray(e.metrics)?r.push(U(e.metrics)):Array.isArray(e.nodes)&&Array.isArray(e.edges)?(r.push(`<pre class="mermaid">${t(K(e))}</pre>`),r.push(`<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"><\/script>`),r.push(`<script>mermaid.run();<\/script>`)):r.push(G(e))}else r.push(`<p>${t(String(n))}</p>`);return r.join(`
|
|
15
|
-
`)}function V(e){let l=[];switch(e.title&&l.push(`<h2>${t(e.title)}</h2>`),e.type){case`markdown`:l.push(`<div class="md-content">${S.parse(String(e.value??``))}</div>`);break;case`code`:l.push(`<pre><code>${t(String(e.value??``))}</code></pre>`);break;case`mermaid`:l.push(`<pre class="mermaid">${t(String(e.value??``))}</pre>`);break;case`table`:if(Array.isArray(e.value)){let t=e.value;if(t.length>0&&Array.isArray(t[0])){let e=t,n=e[0].map(String),r=e.slice(1).map(e=>Object.fromEntries(n.map((t,n)=>[t,e[n]])));l.push(H(r))}else l.push(H(t))}break;case`metrics`:{let t;Array.isArray(e.value)?t=e.value:e.value&&typeof e.value==`object`&&(t=Object.entries(e.value).map(([e,t])=>({label:e,value:String(t)}))),t&&l.push(U(t))}break;case`cards`:Array.isArray(e.value)&&l.push(W(e.value));break;case`tree`:e.value&&typeof e.value==`object`&&l.push(G(e.value));break;case`graph`:e.value&&typeof e.value==`object`&&l.push(`<pre class="mermaid">${t(K(e.value))}</pre>`);break;case`chart`:{let t=e.value;if(t&&!t.chartType&&t.type&&Array.isArray(t.labels)&&Array.isArray(t.datasets)){let n=t.labels,r=t.datasets,i=r.map((e,t)=>e.label??`series${t+1}`),a=n.map((e,t)=>{let n={_label:e};return r.forEach((e,r)=>{n[i[r]]=e.data[t]??0}),n}),o={type:`chart`,title:e.title,value:{chartType:String(t.type),data:a,xKey:`_label`,yKeys:i}};l.push(c(o))}else l.push(c(e));break}case`timeline`:{let t=e.value;Array.isArray(t)&&(t={items:t.map(e=>({title:String(e.event??e.title??``),phase:e.date==null?e.phase==null?void 0:String(e.phase):String(e.date),description:e.description==null?void 0:String(e.description),status:e.status??`done`}))}),t&&l.push(s(t));break}case`checklist`:e.value&&l.push(n(e.value));break;case`comparison`:e.value&&l.push(r(e.value));break;case`status-board`:e.value&&l.push(o(e.value));break;case`prompt`:e.value&&l.push(a(e.value));break;case`progress`:e.value&&l.push(i(e.value));break;case`text`:l.push(`<div class="md-content">${S.parse(String(e.value??``))}</div>`);break;case`heading`:l.push(`<h2>${t(String(e.value??``))}</h2>`);break;case`separator`:l.push(`<hr class="separator">`);break;default:l.push(`<pre>${t(JSON.stringify(e.value,null,2))}</pre>`)}return l.join(`
|
|
16
|
-
`)}function H(e){if(e.length===0)return`<p><em>empty table</em></p>`;let n=Object.keys(e[0]);return`<div class="table-wrap"><table><thead><tr>${n.map(e=>`<th>${t(e)}</th>`).join(``)}</tr></thead><tbody>${e.map(e=>`<tr>${n.map(n=>`<td>${t(String(e[n]??``))}</td>`).join(``)}</tr>`).join(`
|
|
17
|
-
`)}</tbody></table></div>`}function U(e){return`<div class="metric-grid">${e.map(e=>`<div class="metric"><div class="metric-value">${t(String(e.value))}</div><div class="metric-label">${t(e.label)}</div></div>`).join(``)}</div>`}function W(e){return`<div class="card-grid">${e.map(e=>{let n=[`<div class="card">`];return e.title&&n.push(`<div class="card-title">${t(String(e.title))}</div>`),(e.body||e.description)&&n.push(`<div class="card-body">${t(String(e.body??e.description))}</div>`),(e.badge||e.status)&&n.push(`<span class="badge">${t(String(e.badge??e.status))}</span>`),n.push(`</div>`),n.join(``)}).join(``)}</div>`}function G(e){let n=[];for(let[r,i]of Object.entries(e))typeof i==`object`&&i&&!Array.isArray(i)?n.push(`<div class="tree-node"><span class="tree-key">${t(r)}:</span><div class="tree-children">${G(i)}</div></div>`):n.push(`<div class="tree-node"><span class="tree-key">${t(r)}:</span> ${t(Y(i))}</div>`);return n.join(``)}function K(e){let t=[`graph LR`];for(let n of e.nodes){let e=X(String(n.id??n.name??``)),r=String(n.label??n.name??e);t.push(` ${e}["${r}"]`)}for(let n of e.edges){let e=X(String(n.source??n.from??``)),r=X(String(n.target??n.to??``)),i=n.label?`|${String(n.label)}|`:``;t.push(` ${e} -->${i} ${r}`)}return t.join(`
|
|
18
|
-
`)}function q(e,n,r){let i=B(e,n);return`<!DOCTYPE html>
|
|
19
|
-
<html lang="en">
|
|
20
|
-
<head>
|
|
21
|
-
<meta charset="UTF-8">
|
|
22
|
-
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
23
|
-
<title>${t(e??`KB Dashboard`)}</title>
|
|
24
|
-
${l}
|
|
25
|
-
<style>${u()}</style>
|
|
26
|
-
</head>
|
|
27
|
-
<body>
|
|
28
|
-
<div class="dashboard">
|
|
29
|
-
<div class="header">
|
|
30
|
-
<h1>${t(e??`KB Dashboard`)}</h1>
|
|
31
|
-
<div class="subtitle">Knowledge Base</div>
|
|
32
|
-
</div>
|
|
33
|
-
${i}
|
|
34
|
-
${(()=>{let e=Array.isArray(r)?r:[];return e.length===0?``:`
|
|
35
|
-
<div class="actions-bar">
|
|
36
|
-
<h2>Actions</h2>
|
|
37
|
-
<div class="actions-grid">${e.map(e=>{let n=String(e.id??``);if(e.type===`select`&&Array.isArray(e.options)){let r=e.options.map(e=>{let n=typeof e==`string`?e:e.label;return`<option value="${t(typeof e==`string`?e:e.value)}">${t(n)}</option>`}).join(``);return`<div class="action-group"><label>${t(String(e.label??``))}</label><select data-action-id="${t(n)}" onchange="sendCallback(${t(JSON.stringify(n))},this.value)">${r}</select></div>`}return`<button class="action-btn action-${String(e.variant??`default`)}" onclick="sendCallback(${t(JSON.stringify(n))},'clicked')">${t(String(e.label??``))}</button>`}).join(`
|
|
38
|
-
`)}</div>
|
|
39
|
-
<div id="action-feedback" class="action-feedback"></div>
|
|
40
|
-
</div>
|
|
41
|
-
<script>
|
|
42
|
-
let _cbSent=false;
|
|
43
|
-
function sendCallback(actionId,value){
|
|
44
|
-
if(_cbSent)return;
|
|
45
|
-
_cbSent=true;
|
|
46
|
-
document.querySelectorAll('.action-btn,.action-group select').forEach(el=>{el.disabled=true;el.style.opacity='0.5'});
|
|
47
|
-
const fb=document.getElementById('action-feedback');
|
|
48
|
-
fb.textContent='⏳ Sending selection: '+actionId+' = '+value;
|
|
49
|
-
fb.className='action-feedback sent';
|
|
50
|
-
fetch('/callback',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({actionId,value})})
|
|
51
|
-
.then(()=>{
|
|
52
|
-
fb.textContent='✓ Selection sent: '+actionId+' = '+value;
|
|
53
|
-
})
|
|
54
|
-
.catch(e=>{
|
|
55
|
-
fb.textContent='✗ Callback failed — '+e.message;
|
|
56
|
-
fb.style.color='var(--error)';
|
|
57
|
-
_cbSent=false;
|
|
58
|
-
document.querySelectorAll('.action-btn,.action-group select').forEach(el=>{el.disabled=false;el.style.opacity='1'});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
<\/script>`})()}
|
|
62
|
-
<div class="footer">KB MCP Server · Generated ${new Date().toLocaleString()}</div>
|
|
63
|
-
</div>
|
|
64
|
-
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"><\/script>
|
|
65
|
-
<script>
|
|
66
|
-
// Mermaid initialization
|
|
67
|
-
if(document.querySelector('.mermaid'))mermaid.initialize({theme:'dark',startOnLoad:true});
|
|
68
|
-
|
|
69
|
-
// Table sorting
|
|
70
|
-
document.querySelectorAll('table').forEach(table=>{
|
|
71
|
-
const headers=table.querySelectorAll('th');
|
|
72
|
-
headers.forEach((th,i)=>{
|
|
73
|
-
let asc=true;
|
|
74
|
-
th.addEventListener('click',()=>{
|
|
75
|
-
const tbody=table.querySelector('tbody')||table;
|
|
76
|
-
const rows=[...tbody.querySelectorAll('tr')].filter(r=>r.querySelector('td'));
|
|
77
|
-
rows.sort((a,b)=>{
|
|
78
|
-
const at=(a.cells[i]?.textContent||'').trim();
|
|
79
|
-
const bt=(b.cells[i]?.textContent||'').trim();
|
|
80
|
-
return asc?at.localeCompare(bt,undefined,{numeric:true}):bt.localeCompare(at,undefined,{numeric:true});
|
|
81
|
-
});
|
|
82
|
-
rows.forEach(r=>tbody.appendChild(r));
|
|
83
|
-
asc=!asc;
|
|
84
|
-
headers.forEach(h=>h.style.color='');
|
|
85
|
-
th.style.color='var(--primary)';
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Table search/filter
|
|
91
|
-
document.querySelectorAll('.table-wrap').forEach(wrap=>{
|
|
92
|
-
const table=wrap.querySelector('table');
|
|
93
|
-
if(!table||table.querySelectorAll('tr').length<5)return;
|
|
94
|
-
const bar=document.createElement('div');
|
|
95
|
-
bar.className='search-bar';
|
|
96
|
-
const input=document.createElement('input');
|
|
97
|
-
input.placeholder='Filter rows...';
|
|
98
|
-
const count=document.createElement('span');
|
|
99
|
-
count.className='count';
|
|
100
|
-
bar.appendChild(input);bar.appendChild(count);
|
|
101
|
-
wrap.parentNode.insertBefore(bar,wrap);
|
|
102
|
-
const rows=[...table.querySelectorAll('tbody tr, tr')].filter(r=>r.querySelector('td'));
|
|
103
|
-
const updateCount=()=>{const v=rows.filter(r=>r.style.display!=='none').length;count.textContent=v+'/'+rows.length};
|
|
104
|
-
updateCount();
|
|
105
|
-
input.addEventListener('input',()=>{
|
|
106
|
-
const q=input.value.toLowerCase();
|
|
107
|
-
rows.forEach(r=>{r.style.display=r.textContent.toLowerCase().includes(q)?'':'none'});
|
|
108
|
-
updateCount();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
<\/script>
|
|
112
|
-
</body>
|
|
113
|
-
</html>`}function J(e,t,n){let r=Array.isArray(n)?n:[],i=[];if(e&&i.push(`## ${e}\n`),Array.isArray(t)){for(let e of t)if(e&&typeof e==`object`&&typeof e.type==`string`)if(e.type===`metrics`&&e.value&&typeof e.value==`object`){let t=e.value;i.push(Object.entries(t).map(([e,t])=>`**${e}**: ${String(t)}`).join(` · `))}else e.type===`text`&&typeof e.value==`string`?i.push(e.value):i.push(`*${e.type}${e.title?`: ${String(e.title)}`:``}*`)}else typeof t==`string`&&i.push(t);if(r.length>0){i.push(``);for(let e=0;e<r.length;e++){let t=r[e],n=typeof t.label==`string`?t.label:`Action ${e+1}`;if(t.type===`select`&&Array.isArray(t.options)){let r=t.options.map(e=>typeof e==`string`?e:e.label).join(`, `);i.push(`${e+1}. **${n}** — choose: ${r}`)}else i.push(`${e+1}. **${n}**`)}}let a={title:e,content:t};return r.length>0&&(a.actions=r),{content:[{type:`text`,text:i.join(`
|
|
114
|
-
`)}],structuredContent:a}}function Y(e){return e==null?`null`:Array.isArray(e)?`[${e.map(Y).join(`, `)}]`:String(e)}function X(e){return e.replace(/[^a-zA-Z0-9_]/g,`_`)}function Z(e){return typeof e==`object`&&!!e&&`type`in e&&`value`in e}export{j as registerPresentTool};
|
|
1
|
+
import{registerPresentTool as e}from"./present/tool.js";import"./present/index.js";export{e as registerPresentTool};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{bookendReorder as e,cosineSimilarity as t,segment as n}from"./text-utils.js";import{readFile as r,stat as i}from"node:fs/promises";async function a(a,o){let{query:s,maxChars:c=3e3,minScore:l=.3,segmentation:u=`paragraph`}=o,d=o.tokenBudget?o.tokenBudget*4:c,f;if(o.text)f=o.text;else if(o.path){let e;try{e=await i(o.path)}catch(e){let t=e.code;throw t===`ENOENT`?Error(`File not found: ${o.path}. Check the path and try again.`):t===`EACCES`||t===`EPERM`?Error(`Permission denied reading ${o.path}. The file exists but is not accessible.`):e}if(e.size>1e7)throw Error(`File too large (${(e.size/1e6).toFixed(1)}MB). compact supports files up to 10MB. Consider splitting or using search instead.`);f=o.cache?(await o.cache.get(o.path)).content:await r(o.path,`utf-8`)}else throw Error(`Either "text" or "path" must be provided`);if(f.length<=d)return{text:f,originalChars:f.length,compressedChars:f.length,ratio:1,segmentsKept:1,segmentsTotal:1};let p=n(f,u);if(p.length===0)return{text:``,originalChars:f.length,compressedChars:0,ratio:0,segmentsKept:0,segmentsTotal:0};let m=await a.embed(s),h=[];for(let e=0;e<p.length;e++){let n=t(m,await a.embed(p[e]));h.push({text:p[e],score:n,index:e})}let g=h.filter(e=>e.score>=l).sort((e,t)=>t.score-e.score),_=[],v=0;for(let e of g){if(v+e.text.length>d){v===0&&(_.push({...e,text:e.text.slice(0,d)}),v=d);break}_.push(e),v+=e.text.length+2}let y=e(_.sort((e,t)=>t.score-e.score)).map(e=>e.text).join(`
|
|
1
|
+
import{bookendReorder as e,cosineSimilarity as t,segment as n}from"./text-utils.js";import{readFile as r,stat as i}from"node:fs/promises";async function a(a,o){let{query:s,maxChars:c=3e3,minScore:l=.3,segmentation:u=`paragraph`}=o,d=o.tokenBudget?o.tokenBudget*4:c,f;if(o.text)f=o.text;else if(o.path){let e;try{e=await i(o.path)}catch(e){let t=e.code;throw t===`ENOENT`?Error(`File not found: ${o.path}. Check the path and try again.`):t===`EACCES`||t===`EPERM`?Error(`Permission denied reading ${o.path}. The file exists but is not accessible.`):e}if(e.isDirectory())throw Error(`Path is a directory: ${o.path}. compact requires a file path, not a directory. Use analyze_structure or find to explore directories.`);if(e.size>1e7)throw Error(`File too large (${(e.size/1e6).toFixed(1)}MB). compact supports files up to 10MB. Consider splitting or using search instead.`);f=o.cache?(await o.cache.get(o.path)).content:await r(o.path,`utf-8`)}else throw Error(`Either "text" or "path" must be provided`);if(f.length<=d)return{text:f,originalChars:f.length,compressedChars:f.length,ratio:1,segmentsKept:1,segmentsTotal:1};let p=n(f,u);if(p.length===0)return{text:``,originalChars:f.length,compressedChars:0,ratio:0,segmentsKept:0,segmentsTotal:0};let m=await a.embed(s),h=[];for(let e=0;e<p.length;e++){let n=t(m,await a.embed(p[e]));h.push({text:p[e],score:n,index:e})}let g=h.filter(e=>e.score>=l).sort((e,t)=>t.score-e.score),_=[],v=0;for(let e of g){if(v+e.text.length>d){v===0&&(_.push({...e,text:e.text.slice(0,d)}),v=d);break}_.push(e),v+=e.text.length+2}let y=e(_.sort((e,t)=>t.score-e.score)).map(e=>e.text).join(`
|
|
2
2
|
|
|
3
3
|
`);return{text:y,originalChars:f.length,compressedChars:y.length,ratio:y.length/f.length,segmentsKept:_.length,segmentsTotal:p.length}}export{a as compact};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{estimateTokens as e}from"./text-utils.js";import{readFile as t,stat as n}from"node:fs/promises";import{resolve as r}from"node:path";import{createHash as i}from"node:crypto";var a=class i{cache=new Map;totalReads=0;cacheHits=0;static MAX_ENTRIES=500;async get(a){let s=r(a);this.totalReads++;let c=
|
|
2
|
-
`).length,a=e(n);return
|
|
3
|
-
`).length,
|
|
1
|
+
import{estimateTokens as e}from"./text-utils.js";import{readFile as t,stat as n}from"node:fs/promises";import{resolve as r}from"node:path";import{createHash as i}from"node:crypto";var a=class i{cache=new Map;totalReads=0;cacheHits=0;static MAX_ENTRIES=500;async get(a){let s=r(a);this.totalReads++;let c=await n(s);if(c.isDirectory())throw Error(`Path is a directory: ${a}. Expected a file path, not a directory. Use analyze_structure or find to explore directories.`);let l=c.mtimeMs,u=this.cache.get(s);if(u){if(u.mtimeMs===l)return this.cacheHits++,u.hitCount++,{content:u.content,hash:u.hash,lines:u.lines,estimatedTokens:u.estimatedTokens,hitCount:u.hitCount,changed:!1};let n=await t(s,`utf-8`),r=o(n);if(r===u.hash)return this.cacheHits++,u.hitCount++,u.mtimeMs=l,{content:u.content,hash:u.hash,lines:u.lines,estimatedTokens:u.estimatedTokens,hitCount:u.hitCount,changed:!1};let i=n.split(`
|
|
2
|
+
`).length,a=e(n);return u.content=n,u.hash=r,u.lines=i,u.estimatedTokens=a,u.hitCount++,u.mtimeMs=l,{content:n,hash:r,lines:i,estimatedTokens:a,hitCount:u.hitCount,changed:!0}}let d=await t(s,`utf-8`),f=o(d),p=d.split(`
|
|
3
|
+
`).length,m=e(d);if(this.cache.set(s,{content:d,hash:f,lines:p,estimatedTokens:m,hitCount:1,mtimeMs:l}),this.cache.size>i.MAX_ENTRIES){let e=this.cache.keys().next().value;e&&this.cache.delete(e)}return{content:d,hash:f,lines:p,estimatedTokens:m,hitCount:1,changed:!0}}invalidate(e){return this.cache.delete(r(e))}clear(){let e=this.cache.size;return this.cache.clear(),e}stats(){return{totalReads:this.totalReads,cacheHits:this.cacheHits,filesTracked:this.cache.size}}};function o(e){return i(`sha256`).update(e).digest(`hex`)}export{a as FileCache};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{readFile as e,stat as t}from"node:fs/promises";import{extname as n}from"node:path";import{SUPPORTED_EXTENSIONS as r,WasmRuntime as i,extractCalls as a,extractImports as o,extractSymbols as s}from"../../chunker/dist/index.js";async function c(a){let{path:o,previewLines:s=3}=a;if(!a.content){let e;try{e=await t(o)}catch(e){let t=e.code;throw t===`ENOENT`?Error(`File not found: ${o}. Check the path and try again.`):t===`EACCES`||t===`EPERM`?Error(`Permission denied reading ${o}. The file exists but is not accessible.`):e}if(e.size>1e7)throw Error(`File too large (${(e.size/1e6).toFixed(1)}MB). file_summary supports files up to 10MB. Use search or compact with a query instead.`)}let c=a.content??await e(o,`utf-8`),d=c.split(`
|
|
1
|
+
import{readFile as e,stat as t}from"node:fs/promises";import{extname as n}from"node:path";import{SUPPORTED_EXTENSIONS as r,WasmRuntime as i,extractCalls as a,extractImports as o,extractSymbols as s}from"../../chunker/dist/index.js";async function c(a){let{path:o,previewLines:s=3}=a;if(!a.content){let e;try{e=await t(o)}catch(e){let t=e.code;throw t===`ENOENT`?Error(`File not found: ${o}. Check the path and try again.`):t===`EACCES`||t===`EPERM`?Error(`Permission denied reading ${o}. The file exists but is not accessible.`):e}if(e.isDirectory())throw Error(`Path is a directory: ${o}. file_summary requires a file path, not a directory. Use analyze_structure or find to explore directories.`);if(e.size>1e7)throw Error(`File too large (${(e.size/1e6).toFixed(1)}MB). file_summary supports files up to 10MB. Use search or compact with a query instead.`)}let c=a.content??await e(o,`utf-8`),d=c.split(`
|
|
2
2
|
`),f=o.split(`.`).pop()??``,p=n(o);return i.get()&&r.has(p)?l(o,c,d,f,p):u(o,c,d,f)}async function l(e,t,n,r,i){let[c,l,u]=await Promise.all([s(t,i,e),o(t,i,e),a(t,i,e).catch(()=>[])]),f=l.map(e=>`import ${e.specifiers.length>0?`{ ${e.specifiers.join(`, `)} }`:`*`} from '${e.source}'`),p=[],m=[],h=[],g=[],_=[];for(let e of c)switch(e.exported&&p.push(e.name),e.kind){case`function`:case`method`:m.push({name:e.name,line:e.line,exported:e.exported,signature:e.signature});break;case`class`:h.push({name:e.name,line:e.line,exported:e.exported,signature:e.signature});break;case`interface`:g.push({name:e.name,line:e.line,exported:e.exported});break;case`type`:_.push({name:e.name,line:e.line,exported:e.exported});break}let v=l.map(e=>({source:e.source,specifiers:e.specifiers,isExternal:e.isExternal})),y=u.map(e=>({caller:e.callerName,callee:e.calleeName,line:e.line}));return{path:e,lines:n.length,language:d(r),imports:f,exports:p,functions:m,classes:h,interfaces:g,types:_,importDetails:v,callEdges:y.length>0?y:void 0,estimatedTokens:Math.ceil(t.length/4)}}function u(e,t,n,r){let i=[],a=[],o=[],s=[],c=[],l=[];for(let e=0;e<n.length;e+=1){let t=n[e],r=e+1;if(/^import\s+.+/.test(t)){i.push(t.trim());continue}let u=t.match(/^export\s+(?:async\s+)?function\s+(\w+)/);if(u){o.push({name:u[1],line:r,exported:!0}),a.push(u[1]);continue}let d=t.match(/^(?:async\s+)?function\s+(\w+)/);if(d){o.push({name:d[1],line:r,exported:!1});continue}let f=t.match(/^(export\s+)?const\s+(\w+)\s*=.*(?:=>|\bfunction\b)/);if(f){let e=!!f[1];o.push({name:f[2],line:r,exported:e}),e&&a.push(f[2]);continue}let p=t.match(/^export\s+const\s+(\w+)\s*=/);if(p){a.push(p[1]);continue}let m=t.match(/^(export\s+)?(?:abstract\s+)?class\s+(\w+)/);if(m){let e=!!m[1];s.push({name:m[2],line:r,exported:e}),e&&a.push(m[2]);continue}let h=t.match(/^(export\s+)?interface\s+(\w+)/);if(h){let e=!!h[1];c.push({name:h[2],line:r,exported:e}),e&&a.push(h[2]);continue}let g=t.match(/^(export\s+)?type\s+(\w+)/);if(g){let e=!!g[1];l.push({name:g[2],line:r,exported:e}),e&&a.push(g[2]);continue}let _=t.match(/^export\s+\{(.+)\}/);if(_){let e=_[1].split(`,`).map(e=>e.trim().split(/\s+as\s+/).pop()?.trim()??``).filter(Boolean);a.push(...e)}}return{path:e,lines:n.length,language:d(r),imports:i,exports:a,functions:o,classes:s,interfaces:c,types:l,estimatedTokens:Math.ceil(t.length/4)}}function d(e){return{ts:`typescript`,tsx:`typescript-jsx`,js:`javascript`,jsx:`javascript-jsx`,py:`python`,rs:`rust`,go:`go`,java:`java`,rb:`ruby`,md:`markdown`,json:`json`,yaml:`yaml`,yml:`yaml`,css:`css`,html:`html`,sh:`shell`,bash:`shell`}[e]??e}export{c as fileSummary};
|
|
@@ -34,6 +34,13 @@ interface OnboardResult {
|
|
|
34
34
|
outDir?: string;
|
|
35
35
|
/** Total duration in ms */
|
|
36
36
|
totalDurationMs: number;
|
|
37
|
+
/** Auto-generated knowledge entries for curated store persistence */
|
|
38
|
+
autoRemember?: Array<{
|
|
39
|
+
title: string;
|
|
40
|
+
content: string;
|
|
41
|
+
category: string;
|
|
42
|
+
tags: string[];
|
|
43
|
+
}>;
|
|
37
44
|
}
|
|
38
45
|
/**
|
|
39
46
|
* Run all onboarding analyses in parallel and return combined results.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{extractConfigValues as e}from"./config-extractor.js";import{buildDiagrams as t}from"./diagram-builder.js";import{buildCodeMap as n,buildSynthesisGuide as r}from"./synthesis-engine.js";import{DependencyAnalyzer as i,DiagramGenerator as a,EntryPointAnalyzer as o,PatternAnalyzer as
|
|
1
|
+
import{extractConfigValues as e}from"./config-extractor.js";import{buildDiagrams as t}from"./diagram-builder.js";import{buildCodeMap as n,buildSynthesisGuide as r}from"./synthesis-engine.js";import{DependencyAnalyzer as i,DiagramGenerator as a,EntryPointAnalyzer as o,PatternAnalyzer as ee,StructureAnalyzer as s,SymbolAnalyzer as c,extractRegexCallGraph as l,extractTsCallGraph as u}from"../../analyzers/dist/index.js";import{basename as d,join as f,resolve as p}from"node:path";import{existsSync as m,mkdirSync as h,readdirSync as g,rmSync as _,writeFileSync as v}from"node:fs";import{KB_PATHS as y}from"../../core/dist/index.js";const b={structure:`Project Structure`,dependencies:`Dependencies`,"entry-points":`Entry Points`,symbols:`Symbols`,patterns:`Patterns`,diagram:`C4 Container Diagram`,"code-map":`Code Map (Module Graph)`,"config-values":`Configuration Values`,"synthesis-guide":`Synthesis Guide`,"api-surface":`API Surface`,"type-inventory":`Type Inventory`};function x(e){let t=e.get(`symbols`);if(!t?.symbols?.length)return`# API Surface
|
|
2
2
|
|
|
3
3
|
*No symbol data available.*
|
|
4
4
|
`;let n=t.symbols.filter(e=>e.exported);if(n.length===0)return`# API Surface
|
|
@@ -6,7 +6,7 @@ import{extractConfigValues as e}from"./config-extractor.js";import{buildDiagrams
|
|
|
6
6
|
*No exported symbols found.*
|
|
7
7
|
`;let r=new Map;for(let e of n){let t=r.get(e.filePath)??[];t.push(e),r.set(e.filePath,t)}let i=[`# API Surface
|
|
8
8
|
`];for(let[e,t]of[...r.entries()].sort(([e],[t])=>e.localeCompare(t))){i.push(`## ${e}\n`);for(let e of t){e.decorators?.length&&i.push(e.decorators.join(` `));let t=e.signature??``,n=e.returnType?`: ${e.returnType}`:``;if(e.kind===`function`||e.kind===`method`)i.push(`### \`${e.name}${t}${n}\``);else if(e.kind===`class`)i.push(`### class \`${e.name}\`${t?` ${t}`:``}`);else if(e.kind===`interface`||e.kind===`type`){let t=e.typeBody?` ${e.typeBody}`:``;i.push(`### ${e.kind} \`${e.name}\`${t}`)}else i.push(`### ${e.kind} \`${e.name}\`${t?`: ${t}`:``}`);e.jsdoc&&i.push(`> ${e.jsdoc}`),i.push(``)}}let a=i.join(`
|
|
9
|
-
`);return a.length>1e5?`${a.slice(0,1e5)}\n\n*[truncated]*`:a}function
|
|
9
|
+
`);return a.length>1e5?`${a.slice(0,1e5)}\n\n*[truncated]*`:a}function S(e){let t=e.get(`symbols`);if(!t?.symbols?.length)return`# Type Inventory
|
|
10
10
|
|
|
11
11
|
*No symbol data available.*
|
|
12
12
|
`;let n=t.symbols.filter(e=>e.exported&&(e.kind===`interface`||e.kind===`type`||e.kind===`enum`));if(n.length===0)return`# Type Inventory
|
|
@@ -14,5 +14,5 @@ import{extractConfigValues as e}from"./config-extractor.js";import{buildDiagrams
|
|
|
14
14
|
*No exported types/interfaces found.*
|
|
15
15
|
`;let r=new Map;for(let e of n){let t=r.get(e.filePath)??[];t.push(e),r.set(e.filePath,t)}let i=[`# Type Inventory
|
|
16
16
|
`];for(let[e,t]of[...r.entries()].sort(([e],[t])=>e.localeCompare(t))){i.push(`## ${e}\n`);for(let e of t){let t=e.typeBody??`*body not available*`;e.jsdoc&&i.push(`> ${e.jsdoc}`),i.push(`### ${e.kind} \`${e.name}\``),i.push("```"),i.push(t),i.push("```\n")}}let a=i.join(`
|
|
17
|
-
`);return a.length>1e5?`${a.slice(0,1e5)}\n\n*[truncated]*`:a}async function
|
|
18
|
-
`),`utf-8`)}return{
|
|
17
|
+
`);return a.length>1e5?`${a.slice(0,1e5)}\n\n*[truncated]*`:a}async function C(C){let w=Date.now(),T=p(C.path),E=d(T),D=C.mode??`generate`,O=C.outDir??f(T,y.aiKb),k=new s,A=new i,j=new c,M=new ee,N=new o,P=new a,F=[{name:`structure`,fn:()=>k.analyze(T,{format:`markdown`,maxDepth:3,sourceOnly:!0})},{name:`dependencies`,fn:()=>A.analyze(T,{format:`markdown`})},{name:`entry-points`,fn:()=>N.analyze(T)},{name:`symbols`,fn:()=>j.analyze(T,{format:`markdown`})},{name:`patterns`,fn:()=>M.analyze(T)},{name:`diagram`,fn:()=>P.analyze(T,{diagramType:`architecture`})}],I=await Promise.allSettled(F.map(async e=>{let t=Date.now(),n=await e.fn();return{name:e.name,result:n,durationMs:Date.now()-t}})),L=[],R=new Map,z=new Map;for(let e of I)if(e.status===`fulfilled`){let{name:t,result:n,durationMs:r}=e.value,i=n;L.push({name:t,status:`success`,output:i.output,durationMs:r}),R.set(t,i.output),z.set(t,i.data)}else{let t=e.reason,n=F[I.indexOf(e)].name;L.push({name:n,status:`failed`,output:``,durationMs:0,error:t.message})}let B=Date.now(),V=null;try{let e=await u(T);if((!e||e.edges.length===0)&&(e=await l(T)),e&&e.edges.length>0){V=new Map;for(let t of e.edges){let e=V.get(t.from);e||(e=new Map,V.set(t.from,e));let n=e.get(t.to);if(n)for(let e of t.symbols)n.includes(e)||n.push(e);else e.set(t.to,[...t.symbols])}}}catch{}let H=Date.now()-B,U=Date.now(),W=n(z,E,V),G=Date.now()-U+H;if(L.push({name:`code-map`,status:`success`,output:W,durationMs:G}),R.set(`code-map`,W),V&&V.size>0){let e=t(V,z,E),n=L.find(e=>e.name===`diagram`);n&&(n.output=e,R.set(`diagram`,e))}let K=Date.now(),q=await e(T,E),J=Date.now()-K;L.push({name:`config-values`,status:`success`,output:q,durationMs:J}),R.set(`config-values`,q);let Y=r(L,D,E,z);L.push({name:`synthesis-guide`,status:`success`,output:Y,durationMs:0}),R.set(`synthesis-guide`,Y);let X=x(z);L.push({name:`api-surface`,status:`success`,output:X,durationMs:0}),R.set(`api-surface`,X);let Z=S(z);if(L.push({name:`type-inventory`,status:`success`,output:Z,durationMs:0}),R.set(`type-inventory`,Z),D===`generate`){if(m(O))for(let e of g(O))(e.endsWith(`.md`)||e.endsWith(`.json`))&&_(f(O,e),{force:!0});h(O,{recursive:!0});let e=new Date().toISOString();for(let[t,n]of R){let r=f(O,`${t}.md`),i=n.replaceAll(T,E);v(r,`<!-- Generated: ${e} -->\n<!-- Project: ${E} -->\n<!-- Source: ${T} -->\n\n`+i,`utf-8`)}let t=[`<!-- Generated: ${e} -->`,`<!-- Project: ${E} -->`,`<!-- Source: ${T} -->`,``,`# ${E} — Codebase Knowledge`,``,`## Contents`,``];for(let e of L){let n=`${e.name}.md`,r=b[e.name]??e.name,i=e.status===`success`?`✓`:`✗`,a=e.durationMs>0?` (${e.durationMs}ms)`:``;t.push(`- ${i} [${r}](./${n})${a}`)}t.push(``),v(f(O,`README.md`),t.join(`
|
|
18
|
+
`),`utf-8`)}let Q=[];Q.push({title:`Onboard: ${E} project overview`,content:Y.slice(0,2e3),category:`conventions`,tags:[`onboard`,`project-overview`,E]});let $=L.find(e=>e.name===`patterns`);return $?.status===`success`&&$.output&&Q.push({title:`Onboard: ${E} detected patterns`,content:$.output.slice(0,1500),category:`patterns`,tags:[`onboard`,`patterns`,E]}),q&&Q.push({title:`Onboard: ${E} config and commands`,content:q.slice(0,1500),category:`conventions`,tags:[`onboard`,`config`,`commands`,E]}),{path:T,mode:D,steps:L,outDir:D===`generate`?O:void 0,totalDurationMs:Date.now()-w,autoRemember:Q}}export{C as onboard};
|
package/skills/present/SKILL.md
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: present
|
|
3
|
-
description: "Use the KB `present` tool to display rich interactive dashboards, charts, timelines, status boards, and data visualizations in the browser or in-chat. Covers all block types, chart types, actions, and
|
|
4
|
-
argument-hint: "Content to present — data, analysis results, status report, comparison"
|
|
3
|
+
description: "Use the KB `present` tool to display rich interactive dashboards, charts, timelines, status boards, and data visualizations in the browser or in-chat. Covers all block types, chart types, actions, composition patterns, and MCP Apps templates (list-sort, data-table, picker, flame-graph, form, timeline, kanban, tree, diff-view, dashboard). Load this skill before calling the present tool to ensure professional output."
|
|
4
|
+
argument-hint: "Content to present — data, analysis results, status report, comparison, or interactive MCP App"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Present Tool — Rich Interactive Dashboards
|
|
8
8
|
|
|
9
9
|
The `present` tool renders structured content as a professional dark-themed dashboard. It supports two output modes:
|
|
10
10
|
|
|
11
|
-
- **`
|
|
12
|
-
- **`
|
|
11
|
+
- **`html`** — Renders an embedded UIResource for MCP-UI hosts (in-chat). Best for display-only content.
|
|
12
|
+
- **`browser`** — Serves a themed dashboard on a local URL. Best when you need **user interaction** (buttons, selections, form input). The tool blocks until the user clicks an action, then returns their selection.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Format Selection Rules (MUST FOLLOW)
|
|
15
|
+
|
|
16
|
+
| Situation | Format | Why |
|
|
17
|
+
|-----------|--------|-----|
|
|
18
|
+
| Display-only content (tables, charts, reports, status boards) | **`html`** | No interaction needed — render in-chat |
|
|
19
|
+
| Need user input back (confirmations, selections, form data) | **`browser`** | Browser supports blocking actions that return data |
|
|
20
|
+
| Rich visual dashboards without interaction | **`html`** | Prefer in-chat when no response is needed |
|
|
21
|
+
| User explicitly asks for browser | **`browser`** | Respect explicit preference |
|
|
22
|
+
|
|
23
|
+
**Rule: If no user interaction is needed, use `format: "html"`. If you need user interaction, use `format: "browser"`.**
|
|
15
24
|
|
|
16
25
|
---
|
|
17
26
|
|
|
18
|
-
## Browser Workflow (IMPORTANT)
|
|
27
|
+
## Browser Workflow (IMPORTANT — read carefully)
|
|
19
28
|
|
|
20
|
-
When using `format: "browser"`, the
|
|
29
|
+
When using `format: "browser"`, the tool starts a local HTTP server and returns a URL. **You MUST open the URL** so the user can see and interact with the dashboard.
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
### Steps:
|
|
32
|
+
1. Call `present` with `format: "browser"` and `actions` — it returns text containing `🌐 **Dashboard opened in browser:** http://127.0.0.1:{port}`
|
|
33
|
+
2. **Extract the URL from the response**
|
|
34
|
+
3. **Call `openBrowserPage({ url: "http://127.0.0.1:{port}" })` to open it in VS Code's Simple Browser**
|
|
25
35
|
|
|
26
36
|
```
|
|
27
37
|
// Step 1: Call present
|
|
28
|
-
result = present({ format: "browser", title: "...", content: [...] })
|
|
38
|
+
result = present({ format: "browser", title: "...", content: [...], actions: [...] })
|
|
29
39
|
|
|
30
|
-
// Step 2:
|
|
40
|
+
// Step 2: MUST open in VS Code Simple Browser
|
|
31
41
|
openBrowserPage({ url: "http://127.0.0.1:{port}" })
|
|
32
42
|
```
|
|
33
43
|
|
|
44
|
+
> **Fallback**: The server also auto-opens the system browser as a safety net. But you should ALWAYS call `openBrowserPage` yourself so the user sees it inside VS Code.
|
|
45
|
+
|
|
34
46
|
**Note:** The HTTP server auto-closes after 5 minutes. Open the page promptly after receiving the URL.
|
|
35
47
|
|
|
36
48
|
---
|
|
@@ -151,3 +163,262 @@ The dashboard uses a professional dark theme:
|
|
|
151
163
|
- Use `donut` for proportions/distributions
|
|
152
164
|
- Use `sparkline` inline with metrics for mini trends
|
|
153
165
|
7. **Table auto-features:** Tables with 5+ rows get automatic search/filter. Columns are sortable by click.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## MCP Apps Templates (VS Code Chat Widgets)
|
|
170
|
+
|
|
171
|
+
When `chat.mcp.apps.enabled` is true in VS Code settings, the `present` tool can render **interactive widgets directly in the chat panel** using the `template` parameter. These are distinct from the browser/html dashboard modes.
|
|
172
|
+
|
|
173
|
+
**Key difference:** Templates use `template` + object `content` (not array of blocks). No `format` parameter needed. The widget renders inline in VS Code chat. User interactions (reorder, select, submit) are returned to the LLM as structured data.
|
|
174
|
+
|
|
175
|
+
### When to use MCP Apps vs Browser Dashboard
|
|
176
|
+
|
|
177
|
+
| Use Case | Mode | Why |
|
|
178
|
+
|----------|------|-----|
|
|
179
|
+
| Quick user input (pick items, reorder, fill form) | **MCP App template** | Inline in chat, no context switch |
|
|
180
|
+
| Rich multi-section dashboard with charts | **browser** | Full page layout with all block types |
|
|
181
|
+
| Simple confirmation or selection | **Actions** (browser/html) | Buttons/selects in dashboard |
|
|
182
|
+
|
|
183
|
+
### Template: `list-sort`
|
|
184
|
+
|
|
185
|
+
Drag-and-drop reorderable list. Use for priority ordering, task sequencing, or ranked preferences.
|
|
186
|
+
|
|
187
|
+
```json
|
|
188
|
+
{
|
|
189
|
+
"template": "list-sort",
|
|
190
|
+
"title": "Priority Order",
|
|
191
|
+
"content": {
|
|
192
|
+
"items": [
|
|
193
|
+
{ "id": "task-1", "label": "Fix authentication bug" },
|
|
194
|
+
{ "id": "task-2", "label": "Add search feature" },
|
|
195
|
+
{ "id": "task-3", "label": "Update documentation" }
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Content schema:** `{ items: Array<{ id: string, label: string }> }`
|
|
202
|
+
**Returns:** Reordered array of items after user drags to reorder and submits.
|
|
203
|
+
|
|
204
|
+
### Template: `data-table`
|
|
205
|
+
|
|
206
|
+
Filterable, sortable table with optional summary stat cards. Use for structured data review, change summaries, or audit results.
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"template": "data-table",
|
|
211
|
+
"title": "File Changes",
|
|
212
|
+
"content": {
|
|
213
|
+
"columns": [
|
|
214
|
+
{ "key": "file", "label": "File" },
|
|
215
|
+
{ "key": "change", "label": "Change" },
|
|
216
|
+
{ "key": "lines", "label": "Lines" }
|
|
217
|
+
],
|
|
218
|
+
"rows": [
|
|
219
|
+
{ "file": "auth.ts", "change": "Added guard", "lines": 5 },
|
|
220
|
+
{ "file": "cache.ts", "change": "Fixed bug", "lines": 12 }
|
|
221
|
+
],
|
|
222
|
+
"stats": [
|
|
223
|
+
{ "label": "Files Changed", "value": "2" },
|
|
224
|
+
{ "label": "Total Lines", "value": "17" }
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Content schema:** `{ columns: Array<{ key: string, label: string }>, rows: Array<Record<string, unknown>>, stats?: Array<{ label: string, value: string }> }`
|
|
231
|
+
**Returns:** Selected row(s) data when user interacts and submits.
|
|
232
|
+
|
|
233
|
+
### Template: `picker`
|
|
234
|
+
|
|
235
|
+
Multi-select with categories and tag-based search. Use for component selection, feature toggles, or module picking.
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"template": "picker",
|
|
240
|
+
"title": "Select Modules",
|
|
241
|
+
"content": {
|
|
242
|
+
"categories": [
|
|
243
|
+
{ "id": "core", "label": "Core" },
|
|
244
|
+
{ "id": "plugins", "label": "Plugins" }
|
|
245
|
+
],
|
|
246
|
+
"items": [
|
|
247
|
+
{ "id": "auth", "label": "Authentication", "category": "core", "tags": ["security"] },
|
|
248
|
+
{ "id": "cache", "label": "Cache Layer", "category": "core", "tags": ["performance"] },
|
|
249
|
+
{ "id": "sentry", "label": "Sentry Plugin", "category": "plugins", "tags": ["monitoring"] }
|
|
250
|
+
]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Content schema:** `{ categories: Array<{ id: string, label: string }>, items: Array<{ id: string, label: string, category: string, tags?: string[] }> }`
|
|
256
|
+
**Returns:** Array of selected item IDs after user checks items and submits.
|
|
257
|
+
|
|
258
|
+
### Template: `flame-graph`
|
|
259
|
+
|
|
260
|
+
Hierarchical zoomable visualization. Use for codebase structure, performance profiles, dependency trees, or budget breakdowns.
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"template": "flame-graph",
|
|
265
|
+
"title": "Module Hierarchy",
|
|
266
|
+
"content": {
|
|
267
|
+
"profile": {
|
|
268
|
+
"name": "root",
|
|
269
|
+
"total": 100,
|
|
270
|
+
"children": [
|
|
271
|
+
{
|
|
272
|
+
"name": "packages/tools",
|
|
273
|
+
"total": 60,
|
|
274
|
+
"children": [
|
|
275
|
+
{ "name": "search.ts", "total": 35 },
|
|
276
|
+
{ "name": "compact.ts", "total": 25 }
|
|
277
|
+
]
|
|
278
|
+
},
|
|
279
|
+
{ "name": "packages/server", "total": 40 }
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Content schema:** `{ profile: { name: string, total: number, children?: Array<(recursive)> } }`
|
|
287
|
+
**Returns:** Clicked node information when user interacts.
|
|
288
|
+
|
|
289
|
+
### Template: `form`
|
|
290
|
+
|
|
291
|
+
Input fields with text inputs, selects, and checkboxes. Use for configuration, parameter collection, or setup wizards.
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"template": "form",
|
|
296
|
+
"title": "Project Configuration",
|
|
297
|
+
"content": {
|
|
298
|
+
"fields": [
|
|
299
|
+
{ "name": "projectName", "label": "Project Name", "type": "text", "value": "my-app" },
|
|
300
|
+
{ "name": "framework", "label": "Framework", "type": "select", "options": ["React", "Vue", "Svelte"], "value": "React" },
|
|
301
|
+
{ "name": "typescript", "label": "Use TypeScript", "type": "checkbox", "value": true },
|
|
302
|
+
{ "name": "description", "label": "Description", "type": "text", "value": "" }
|
|
303
|
+
]
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Content schema:** `{ fields: Array<{ name: string, label: string, type: "text" | "select" | "checkbox", value?: string | boolean, options?: string[] }> }`
|
|
309
|
+
**Returns:** Object with field name-value pairs after user fills out and submits.
|
|
310
|
+
|
|
311
|
+
### MCP Apps Best Practices
|
|
312
|
+
|
|
313
|
+
1. **Use meaningful `id` values** — they are returned in responses and used for programmatic handling.
|
|
314
|
+
2. **Keep list-sort items under 15** — longer lists become unwieldy for drag-and-drop.
|
|
315
|
+
3. **Always provide `value` defaults** in forms — pre-fill with sensible defaults to reduce user effort.
|
|
316
|
+
4. **Use `tags` in picker** — they power the search/filter functionality.
|
|
317
|
+
5. **Flame-graph `total` values** should be consistent — parent total should equal sum of children.
|
|
318
|
+
6. **Data-table `stats`** are optional — use them for KPI summary cards above the table.
|
|
319
|
+
7. **Combine with LLM logic** — use returned values to drive next steps (e.g., form → generate config, picker → scope analysis).
|
|
320
|
+
|
|
321
|
+
#### 6. `timeline` — Vertical Event Timeline (display only)
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"events": [
|
|
326
|
+
{"title": "v1.0 Release", "description": "Initial release with core features", "timestamp": "2025-01", "status": "complete"},
|
|
327
|
+
{"title": "v2.0 Beta", "description": "MCP Apps support", "timestamp": "2025-07", "status": "active"},
|
|
328
|
+
{"title": "v3.0 Planning", "status": "pending"}
|
|
329
|
+
]
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Fields: `title` (required), `description?`, `timestamp?`, `status?` (`'complete'|'active'|'pending'|'error'`), `icon?`, `category?`
|
|
334
|
+
|
|
335
|
+
#### 7. `kanban` — Drag-Drop Board (interactive — emits on card move)
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"columns": [
|
|
340
|
+
{"id": "todo", "label": "To Do"},
|
|
341
|
+
{"id": "in-progress", "label": "In Progress", "color": "#3b82f6"},
|
|
342
|
+
{"id": "done", "label": "Done", "color": "#22c55e"}
|
|
343
|
+
],
|
|
344
|
+
"cards": [
|
|
345
|
+
{"id": "c1", "title": "Fix auth bug", "column": "todo", "tags": ["bugfix"], "priority": "high"},
|
|
346
|
+
{"id": "c2", "title": "Add search", "column": "in-progress", "tags": ["feature"], "priority": "medium"}
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Returns `{cardId, fromColumn, toColumn}` when a card is dragged between columns.
|
|
352
|
+
|
|
353
|
+
#### 8. `tree` — Collapsible Tree View (display only)
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"root": {
|
|
358
|
+
"label": "packages/",
|
|
359
|
+
"children": [
|
|
360
|
+
{"label": "server/", "children": [{"label": "index.ts"}, {"label": "tools/", "children": [{"label": "present.ts"}]}]},
|
|
361
|
+
{"label": "present/", "children": [{"label": "present-app.ts"}]}
|
|
362
|
+
]
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Fields: `label` (required), `id?`, `icon?`, `children?: TreeNode[]`, `metadata?: Record<string,string>`. Accepts `root` as single node or array of nodes.
|
|
368
|
+
|
|
369
|
+
#### 9. `diff-view` — Side-by-Side Code Diff (display only)
|
|
370
|
+
|
|
371
|
+
```json
|
|
372
|
+
{
|
|
373
|
+
"files": [{
|
|
374
|
+
"path": "auth.ts",
|
|
375
|
+
"status": "modified",
|
|
376
|
+
"additions": 12,
|
|
377
|
+
"deletions": 3,
|
|
378
|
+
"hunks": [{
|
|
379
|
+
"header": "@@ -10,5 +10,14 @@",
|
|
380
|
+
"changes": [
|
|
381
|
+
{"type": "context", "content": "const auth = {};"},
|
|
382
|
+
{"type": "delete", "content": "// old validation"},
|
|
383
|
+
{"type": "add", "content": "// new JWT validation"},
|
|
384
|
+
{"type": "add", "content": "const token = verify(jwt);"}
|
|
385
|
+
]
|
|
386
|
+
}]
|
|
387
|
+
}],
|
|
388
|
+
"stats": {"additions": 12, "deletions": 3, "filesChanged": 1}
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Status values: `'added'|'modified'|'deleted'|'renamed'`. Change types: `'add'|'delete'|'context'`.
|
|
393
|
+
|
|
394
|
+
#### 10. `dashboard` — Metric Cards Grid (display only)
|
|
395
|
+
|
|
396
|
+
```json
|
|
397
|
+
{
|
|
398
|
+
"metrics": [
|
|
399
|
+
{"label": "Uptime", "value": "99.9%", "status": "success", "trend": {"value": "+0.1%", "direction": "up"}},
|
|
400
|
+
{"label": "Build Time", "value": "4.2s", "type": "progress", "progress": 42, "status": "warning"},
|
|
401
|
+
{"label": "Top Endpoints", "type": "list", "value": "3", "items": [
|
|
402
|
+
{"label": "/api/search", "value": "1.2k/day"},
|
|
403
|
+
{"label": "/api/present", "value": "890/day"}
|
|
404
|
+
]}
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Metric types: `'stat'` (default), `'progress'` (with bar), `'list'` (sub-items). Status: `'success'|'warning'|'error'|'info'`.
|
|
410
|
+
|
|
411
|
+
### Interaction Summary (All 10 Templates)
|
|
412
|
+
|
|
413
|
+
| Template | Trigger | Data Returned to LLM |
|
|
414
|
+
|----------|---------|---------------------|
|
|
415
|
+
| list-sort | Auto on drag-drop | Reordered ID array |
|
|
416
|
+
| data-table | Export button | Current filtered rows |
|
|
417
|
+
| picker | Apply Selection button | Selected ID array |
|
|
418
|
+
| flame-graph | Auto on node click | Clicked node object |
|
|
419
|
+
| form | Submit button | Field values object |
|
|
420
|
+
| kanban | Auto on card drag | `{cardId, fromColumn, toColumn}` |
|
|
421
|
+
| timeline | Display only | — |
|
|
422
|
+
| tree | Display only | — |
|
|
423
|
+
| diff-view | Display only | — |
|
|
424
|
+
| dashboard | Display only | — |
|