@zibby/skills 2.0.4 → 2.0.6
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/dist/artifact.js +7 -7
- package/dist/chat-notify.js +4 -4
- package/dist/chatProgress.js +4 -4
- package/dist/datasetStore.js +2 -2
- package/dist/discord.js +3 -3
- package/dist/figma.js +2 -2
- package/dist/git-write.js +9 -9
- package/dist/github.js +5 -5
- package/dist/gitlab.js +5 -5
- package/dist/googleDocs.js +9 -9
- package/dist/hubspot.js +1 -1
- package/dist/index.js +179 -181
- package/dist/jira.d.ts +36 -1
- package/dist/jira.js +22 -20
- package/dist/kvMemory.js +2 -2
- package/dist/lark.js +2 -2
- package/dist/larkAttendance.js +1 -1
- package/dist/larkDocs.js +3 -3
- package/dist/lib/http-deadline.d.ts +117 -0
- package/dist/lib/http-deadline.js +1 -0
- package/dist/lib/markup.d.ts +158 -0
- package/dist/lib/markup.js +22 -0
- package/dist/linear.js +15 -15
- package/dist/linkedin.js +2 -2
- package/dist/llm-billing.js +1 -1
- package/dist/notion.js +7 -7
- package/dist/opendesign.js +2 -2
- package/dist/package.json +6 -5
- package/dist/report.d.ts +42 -42
- package/dist/review.js +4 -4
- package/dist/reviewMemoryIo.js +1 -1
- package/dist/sentry.js +2 -2
- package/dist/skill-installer.js +3 -3
- package/dist/slack.js +2 -2
- package/dist/trackers/github-adapter.js +5 -5
- package/dist/trackers/index.js +46 -44
- package/dist/trackers/jira-adapter.js +24 -22
- package/dist/trackers/linear-adapter.js +19 -19
- package/dist/trackers/plane-adapter.js +1 -1
- package/dist/triggerAgent.js +1 -1
- package/dist/vikunja.d.ts +8 -1
- package/dist/vikunja.js +22 -7
- package/docs/templates/catalog-metadata.md +101 -0
- package/docs/templates/composition.md +97 -0
- package/docs/templates/deploy-time-config.md +228 -0
- package/docs/templates/index.md +138 -0
- package/docs/templates/models.md +157 -0
- package/docs/templates/node-declarations.md +149 -0
- package/docs/templates/remote-mcp.md +105 -0
- package/docs/templates/requires.md +183 -0
- package/docs/templates/sidecars.md +163 -0
- package/docs/templates/stores.md +113 -0
- package/docs/templates/surfaces.md +184 -0
- package/docs/templates/update-behavior.md +108 -0
- package/package.json +6 -5
package/dist/artifact.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import{SKILL_META as Ze}from"@zibby/skill-ids";import{existsSync as se,readFileSync as Ye}from"node:fs";import{homedir as Ve}from"node:os";import{join as Ge,dirname as Ke,resolve as ze}from"node:path";import{fileURLToPath as qe}from"node:url";import{existsSync as st}from"node:fs";import{dirname as ct,resolve as dt}from"node:path";import{fileURLToPath as ut}from"node:url";var w={ZERO_WIDTH:"zero-width-bar",ROW_COUNT:"data-row-count",ROW_MISSING:"data-row-missing",BAR_COUNT:"data-bar-count",BAR_PROPORTION:"bar-proportion",VALUE_MISMATCH:"data-value-mismatch",PLACEHOLDER_TEXT:"placeholder-text",OVERFLOW:"overflow-clipped"},ue=5,pe=20,fe=.04,ge=3,me=.5;function k(a,n){if(n==null||n<0)return null;let t=1;for(let e=0;e<n&&e<a.length;e+=1)a.charCodeAt(e)===10&&(t+=1);return t}function v(a,n,t){let e=n[t.code]||0;e>=ue||a.length>=pe||(n[t.code]=e+1,a.push(t))}var ye={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" ",ndash:"\u2013",mdash:"\u2014",hellip:"\u2026"};function Z(a){return String(a).replace(/&(#x?[0-9a-f]+|[a-z]+);/gi,(n,t)=>{if(t[0]==="#"){let r=t[1]==="x"||t[1]==="X"?parseInt(t.slice(2),16):parseInt(t.slice(1),10);return Number.isFinite(r)?String.fromCodePoint(r):n}let e=ye[String(t).toLowerCase()];return e??n})}function O(a){return Z(String(a??"")).replace(/\s+/g," ").trim().toLowerCase()}function F(a){return Math.round(a*10)/10}var be=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),M=new Set(["script","style","textarea"]),we=new Set(["span","a","em","strong","b","i","u","s","small","code","label","abbr","cite","q","sub","sup","mark","time","var","kbd","samp","font","tt"]),ke=/^<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9:_.-]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>/;function ve(a){let n={},t=/([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))|([a-zA-Z_:][-a-zA-Z0-9_:.]*)/g,e;for(;(e=t.exec(a))!==null;)e[1]?n[e[1].toLowerCase()]=e[3]!=null?e[3]:e[4]!=null?e[4]:e[5]==null?"":e[5]:e[6]&&(n[e[6].toLowerCase()]="");return n}function Te(a){let n={tag:"#root",attrs:{},children:[],parent:null,start:0},t=n,e=[n],r=0,i=(o,s)=>{if(s<=o)return;let c=a.slice(o,s);c.trim()&&t.children.push({tag:"#text",attrs:{},children:[],parent:t,start:o,value:c})};for(;r<a.length;){let o=a.indexOf("<",r);if(o<0){i(r,a.length);break}if(i(r,o),a.startsWith("<!--",o)){let d=a.indexOf("-->",o);r=d<0?a.length:d+3;continue}if(a.startsWith("<!",o)||a.startsWith("<?",o)){let d=a.indexOf(">",o);r=d<0?a.length:d+1;continue}let s=ke.exec(a.slice(o));if(!s){r=o+1;continue}let c=s[0],l=s[2].toLowerCase();if(s[1]){for(let d=e.length-1;d>0;d-=1)if(e[d].tag===l){e.length=d,t=e[d-1];break}r=o+c.length;continue}let u={tag:l,attrs:ve(s[3]||""),children:[],parent:t,start:o};if(t.children.push(u),r=o+c.length,!(s[4]||be.has(l))){if(M.has(l)){let d=a.slice(r),h=new RegExp(`</\\s*${l}\\s*>`,"i").exec(d);u.raw=h?d.slice(0,h.index):d,r+=h?h.index+h[0].length:d.length;continue}e.push(u),t=u}}return n}function S(a,n){for(let t of a.children)t.tag!=="#text"&&(n(t),S(t,n))}function $(a){let n=[],t=e=>{for(let r of e.children)r.tag==="#text"?n.push(r.value||""):(n.push(" "),t(r),n.push(" "))};return t(a),O(n.join(""))}function Se(a){let n=[],t=e=>{for(let r of e.children)r.tag==="#text"?n.push({value:r.value||"",start:r.start,parent:e}):r.tag!=="pre"&&r.tag!=="code"&&t(r)};return t(a),n}function A(a){let n={};for(let t of String(a||"").split(";")){let e=t.indexOf(":");e<0||(n[t.slice(0,e).trim().toLowerCase()]=t.slice(e+1).trim().replace(/\s*!important$/i,""))}return n}function Ne(a){let n=[];return S(a,t=>{if(t.tag!=="style"||!t.raw)return;let e=t.raw.replace(/@[a-z-]+[^{]*\{/gi," "),r=/([^{}]+)\{([^{}]*)\}/g,i;for(;(i=r.exec(e))!==null;){let o=A(i[2]);for(let s of i[1].split(",")){let c=s.trim();if(!c||/[\s>+~:[\]*]/.test(c))continue;let l=[],u=null,d=null,h=!0,p=/([.#]?)([A-Za-z_][-\w]*)/g,f;for(;(f=p.exec(c))!==null;)if(f[1]===".")l.push(f[2].toLowerCase());else if(f[1]==="#")u=f[2];else if(f.index===0)d=f[2].toLowerCase();else{h=!1;break}h&&(d||u||l.length)&&n.push({classes:l,id:u,tag:d,decls:o})}}}),n}function Y(a){return String(a.attrs.class||"").split(/\s+/).filter(Boolean).map(n=>n.toLowerCase())}function Oe(a,n,t){if(a.tag&&a.tag!==n.tag||a.id&&a.id!==n.attrs.id)return!1;for(let e of a.classes)if(t.indexOf(e)<0)return!1;return!0}function T(a,n,t){let e=A(a.attrs.style)[n];if(e!=null&&e!=="")return e.toLowerCase();let r=Y(a),i=null;for(let o of t){if(!Oe(o,a,r))continue;let s=o.decls[n];s!=null&&s!==""&&(i=s.toLowerCase())}return i}function W(a,n){let t=T(a,"display",n);return t||(we.has(a.tag)?"inline":"block")}function U(a){if(!a)return null;let n=/^(-?\d+(?:\.\d+)?)px$/.exec(String(a).trim());return n?Number(n[1]):null}function V(a){if(!a)return null;let n=/^(-?\d+(?:\.\d+)?)%$/.exec(String(a).trim());return n?Number(n[1]):null}function P(a){return a!=null&&typeof a=="object"&&!Array.isArray(a)}function xe(a){if(!a.length||!P(a[0]))return a.map(e=>e==null?"":String(e));let n=null,t=0;for(let e of Object.keys(a[0])){let r=a.map(o=>P(o)&&o[e]!=null?String(o[e]):"");if(r.some(o=>o.length<2))continue;let i=new Set(r.map(O)).size;if(i>t&&(t=i,n=r),i===a.length)break}return n||a.map((e,r)=>String(P(e)?Object.values(e)[0]==null?r:Object.values(e)[0]:e))}function Re(a){let n=[],t=[],e=new Set,r=(o,s)=>{if(!(s.length<2)){if(s.every(P)){n.push({path:o,records:s,identities:xe(s)});let c=Object.keys(s[0]),l=c.filter(d=>s.every(h=>typeof h[d]=="number"&&Number.isFinite(h[d]))),u=c.filter(d=>s.every(h=>typeof h[d]=="string"));l.length===1&&u.length>=1&&t.push({path:o,labels:s.map(d=>String(d[u[0]])),values:s.map(d=>Number(d[l[0]]))});return}s.every(c=>typeof c=="number"&&Number.isFinite(c))&&t.push({path:o,labels:s.map((c,l)=>`#${l}`),values:s.slice()})}},i=(o,s,c)=>{if(!(c>6||o==null||typeof o!="object"||e.has(o))){if(e.add(o),Array.isArray(o)){r(s,o);for(let l=0;l<o.length&&l<200;l+=1)i(o[l],`${s}[${l}]`,c+1);return}for(let l of Object.keys(o))i(o[l],s?`${s}.${l}`:l,c+1)}};return i(a,"",0),{collections:n,series:t}}function Ee(a){let n=String(a||"").match(/[MmLlHhVvZz]|-?\d*\.?\d+(?:e-?\d+)?/gi);if(!n)return null;let t=0,e=0,r="",i=1/0,o=-1/0,s=1/0,c=-1/0,l=()=>{!Number.isFinite(t)||!Number.isFinite(e)||(t<i&&(i=t),t>o&&(o=t),e<s&&(s=e),e>c&&(c=e))},u=0,d=0;for(;u<n.length&&d++<2e4;){let h=n[u];if(/[A-Za-z]/.test(h)&&(r=h,u+=1,r==="Z"||r==="z"))continue;if(u>=n.length)break;let p=()=>{let f=Number(n[u]);return u+=1,f};switch(r){case"M":t=p(),e=p(),l(),r="L";break;case"m":t+=p(),e+=p(),l(),r="l";break;case"L":t=p(),e=p(),l();break;case"l":t+=p(),e+=p(),l();break;case"H":t=p(),l();break;case"h":t+=p(),l();break;case"V":e=p(),l();break;case"v":e+=p(),l();break;default:u+=1;break}}return!Number.isFinite(i)||!Number.isFinite(s)?null:{w:o-i,h:c-s}}var Ae=/(^|[-_])(bar|gauge|meter|fill|progress|track)([-_]|$)/i;function _e(a){let n=a.parent;for(let t=0;n&&t<3;t+=1,n=n.parent){let e=$(n);if(e)return e}return""}function G(a,n){if(Y(a).some(r=>Ae.test(r)))return!0;let t=A(a.attrs.style);return!!(t.background||t["background-color"]||T(a,"background",n)||T(a,"background-color",n))&&$(a)===""}function Ie(a){let n=new Map;for(let e of a)for(let r=e.node.parent;r;r=r.parent)n.set(r,(n.get(r)||0)+1);let t=new Map;for(let e of a){let r=null;for(let i=e.node.parent;i;i=i.parent)if((n.get(i)||0)>=2){r=i;break}r&&(t.has(r)||t.set(r,[]),t.get(r).push(e))}return[...t.values()].filter(e=>e.length>=2)}function Ce(a,n){let t=new Map,e=[];S(a,i=>{if(i.tag==="path"||i.tag==="rect"){let u=i.attrs.ecmeta_series_index;if(u==null)return;let d=null;if(i.tag==="rect"){let f=Number(i.attrs.width),b=Number(i.attrs.height);Number.isFinite(f)&&Number.isFinite(b)&&(d={w:f,h:b})}else d=Ee(i.attrs.d||"");if(!d)return;let h=i.attrs.ecmeta_data_index,p=String(u);t.has(p)||t.set(p,[]),t.get(p).push({bar:{node:i,extent:NaN,unit:"user",dataIndex:h==null||h===""?null:Number(h),context:"",start:i.start},box:d});return}if(M.has(i.tag)||i.tag==="svg")return;let o=A(i.attrs.style),s=o.width==null?o["inline-size"]:o.width;if(s==null||s==="")return;let c=V(s),l=U(s);c==null&&l==null||G(i,n)&&e.push({node:i,extent:c??l,unit:c==null?"px":"%",dataIndex:null,context:_e(i),start:i.start})});let r=[];for(let i of t.values()){if(i.length<2)continue;let o=l=>Math.max(...l)-Math.min(...l),s=o(i.map(l=>l.box.h))>=o(i.map(l=>l.box.w));for(let l of i)l.bar.extent=s?l.box.h:l.box.w;let c=i.map(l=>l.bar).sort((l,u)=>l.dataIndex!=null&&u.dataIndex!=null?l.dataIndex-u.dataIndex:l.start-u.start);r.push(c)}for(let i of Ie(e)){let o=i.filter(l=>l.unit==="%"),s=i.filter(l=>l.unit==="px"),c=o.length>=s.length?o:s;c.length>=2&&r.push(c)}return r}function Le(a){let n=[];return S(a,t=>{if(t.tag!=="table")return;let e=[];S(t,r=>{if(r.tag!=="tr")return;let i=r.children.filter(o=>o.tag==="td"||o.tag==="th");i.length&&i.every(o=>o.tag==="th")||e.push(r)}),n.push({node:t,rows:e,rowTexts:e.map($),start:t.start})}),n}function Pe(a,n,t,e,r,i,o){let s=$(n);for(let c of e){let l=c.records.length;if(l<3)continue;let u=[],d=0;for(let g of c.identities){let y=O(g);!y||y.length<2||(d+=1,s.includes(y)||u.push(String(g)))}if(d<3)continue;let h=d-u.length,p=d?h/d:1,f=null,b=0;for(let g of t){if(!g.rows.length)continue;let y=0,R=new Set;for(let L of g.rowTexts){let E=!1;c.identities.forEach((de,he)=>{let J=O(de);J.length>=2&&L.indexOf(J)>=0&&(E=!0,R.add(he))}),E&&(y+=1)}y<Math.max(2,Math.ceil(g.rows.length*.5))||R.size<Math.ceil(y*.8)||y>b&&(b=y,f=g)}let m=f!=null;o.push({path:c.path||"(root)",records:l,identitiesSearched:d,presentInPage:h,renderedFraction:Number(p.toFixed(3)),boundTableRows:m?f.rows.length:null}),!(p<me)&&(u.length&&v(r,i,{code:w.ROW_MISSING,line:m?k(a,f.start):1,message:`The data at \`${c.path||"(root)"}\` has ${l} records but ${u.length} of them appear NOWHERE on the page: ${u.slice(0,6).map(g=>JSON.stringify(String(g).slice(0,40))).join(", ")}${u.length>6?`, +${u.length-6} more`:""}.`,hint:"Render every record you were given, or say in the page that it is a partial view and how many were left out. A silently truncated table reads as the complete answer."}),u.length===0&&m&&f.rows.length!==l&&v(r,i,{code:w.ROW_COUNT,line:k(a,f.start),message:`The table renders ${f.rows.length} body row(s) but the data at \`${c.path||"(root)"}\` has ${l} records \u2014 ${Math.abs(l-f.rows.length)} ${l>f.rows.length?"missing":"extra"}.`,hint:'Emit exactly one <tr> per record. If you meant to show only some of them, state the cut ("top 10 of 23") in the page itself.'}))}}function $e(a,n){let t=new Array(a.length).fill(-1);if(a.every(i=>i.dataIndex!=null)){let i=0;if(a.forEach((o,s)=>{let c=o.dataIndex;c>=0&&c<n.values.length&&(t[s]=c,i+=1)}),i===a.length)return t}t.fill(-1);let e=new Set,r=0;if(a.forEach((i,o)=>{for(let s=0;s<n.labels.length;s+=1){if(e.has(s))continue;let c=O(n.labels[s]);if(c.length>=2&&i.context.indexOf(c)>=0){t[o]=s,e.add(s),r+=1;break}}}),r===a.length)return t;if(a.length!==n.values.length)return null;for(let i=0;i<a.length;i+=1)t[i]=i;return t}function He(a,n){let t=null,e=0;for(let i of n){let o=i.labels.filter(s=>{let c=O(s);return c.length>=2&&a.some(l=>l.context.indexOf(c)>=0)}).length;o>e&&(e=o,t=i)}if(e>=2)return t;let r=n.filter(i=>i.values.length===a.length);return r.length===1?r[0]:null}function Be(a,n,t,e,r,i){i.barGroups=n.map(o=>o.length),i.boundBars=0;for(let o of n){let s=He(o,t);if(!s)continue;if(s.values.length!==o.length){v(e,r,{code:w.BAR_COUNT,line:k(a,o[0].start),message:`The chart draws ${o.length} bar(s) but the series at \`${s.path||"(root)"}\` has ${s.values.length} value(s).`,hint:"Draw one bar per value. A dropped bar silently removes a whole category from the reader's view of the data."});continue}let c=$e(o,s);if(!c||c.some(h=>h<0))continue;i.boundBars+=o.length;let l=[];for(let h=0;h<o.length;h+=1)o[h].extent>0&&s.values[c[h]]>0&&l.push(h);if(l.length>=ge){let h=l.map(g=>o[g].extent/s.values[c[g]]).sort((g,y)=>g-y),p=Math.floor(h.length/2),f=h.length%2?h[p]:(h[p-1]+h[p])/2,b=Math.max(...s.values.map(g=>Math.abs(g))),m=f*b;if(f>0&&m>0)for(let g of l){let y=s.values[c[g]],R=f*y,L=Math.abs(o[g].extent-R)/m;if(L<=fe)continue;let E=o[g].unit==="%"?"%":"";v(e,r,{code:w.BAR_PROPORTION,line:k(a,o[g].start),message:`The bar for ${JSON.stringify(s.labels[c[g]])} (value ${y}) is drawn at ${F(o[g].extent)}${E}, but every other bar in this chart puts it at ${F(R)}${E} \u2014 off by ${Math.round(L*100)}% of the full-scale bar. The chart misrepresents the data.`,hint:"Every bar in one chart must use the SAME value\u2192length scale. Compute each length from its datum with one formula (value / max * 100), never by writing the numbers out by hand."})}}let u=s.values.reduce((h,p)=>h+p,0),d=Math.max(...s.values.map(h=>Math.abs(h)));for(let h=0;h<o.length;h+=1){let p=s.values[c[h]],f=o[h].context;if(!f)continue;let b=(f.match(/-?\d[\d,]*(?:\.\d+)?/g)||[]).map(m=>Number(m.replace(/,/g,""))).filter(m=>Number.isFinite(m));b.length&&(b.some(m=>Math.abs(m-p)<1e-9)||u>0&&b.some(m=>Math.abs(m-p/u*100)<.6)||d>0&&b.some(m=>Math.abs(m-p/d*100)<.6)||v(e,r,{code:w.VALUE_MISMATCH,line:k(a,o[h].start),message:`The row for ${JSON.stringify(s.labels[c[h]])} prints ${b.slice(0,3).join(", ")} but its datum is ${p}.`,hint:"Print the figure your tools returned, unchanged, and use that same figure everywhere it appears on the page."}))}}}var Ue=/^(flex|inline-flex|grid|inline-grid)$/;function Me(a,n,t,e,r,i){let o=0;S(n,s=>{if(M.has(s.tag))return;let c=A(s.attrs.style).width;if(!c)return;let l=V(c);if(l==null||l<=0||!G(s,t))return;o+=1;let u=W(s,t);if(u==="none"){v(e,r,{code:w.ZERO_WIDTH,line:k(a,s.start),message:`<${s.tag}> is given style="width:${c}" but its effective display is \`none\` \u2014 it is not rendered at all.`,hint:"Remove the display:none rule that matches this element, or remove the element. A width on a display:none box is dead code."});return}if(u!=="inline")return;let d=s.parent;if(d&&d.tag!=="#root"&&Ue.test(W(d,t)))return;let h=T(s,"position",t);if(h==="absolute"||h==="fixed")return;let p=T(s,"float",t);p==="left"||p==="right"||v(e,r,{code:w.ZERO_WIDTH,line:k(a,s.start),message:`<${s.tag}${s.attrs.class?` class="${s.attrs.class}"`:""}> is given style="width:${c}" but its effective display is \`inline\` \u2014 CSS IGNORES width on a non-replaced inline box, so this bar renders with no length at all. The source looks right; the page will not.`,hint:"Give the bar display:block or display:inline-block (or make its container display:flex). A <span> is inline by DEFAULT \u2014 that alone is enough to swallow the width."})}),i.percentWidthBars=o}function De(a,n,t,e,r){S(n,i=>{let o=U(T(i,"width",t));if(!(o==null||o<=0))for(let s=i.parent;s&&s.tag!=="#root";s=s.parent){let c=U(T(s,"width",t));if(c==null||c<=0)continue;if(o<=c)return;let l=T(s,"overflow",t)||T(s,"overflow-x",t);if(l!=="hidden"&&l!=="clip")return;v(e,r,{code:w.OVERFLOW,line:k(a,i.start),message:`<${i.tag}> is ${o}px wide inside a ${c}px ancestor with overflow:${l} \u2014 ${Math.round(o-c)}px of it is clipped and can never be seen.`,hint:"Size the child from its container (a percentage, or max-width:100%), or widen the container. Content the reader cannot see is content you did not publish."});return}})}var je=[{re:/\[object (?:Object|Array|Promise|Map|Set|Null|Undefined)\]/g,what:"an object stringified instead of a field being picked off it"},{re:/\$\{[^}\n]{0,80}\}/g,what:"an un-substituted JavaScript template placeholder"},{re:/\{\{[^}\n]{0,80}\}\}/g,what:"an un-substituted moustache placeholder"},{re:/(?:^|[^\w.])(NaN)(?![\w])/g,what:"NaN \u2014 an arithmetic result that was never a number"},{re:/(?:^|[^\w.])(-?Infinity)(?![\w])/g,what:"Infinity \u2014 a division by zero reached the page"}],Je=/^(undefined|null)$/i,Fe=new Set(["td","th","li","span","div","strong","em","b"]);function We(a,n,t,e,r){let i=0;for(let o of Se(n)){let s=o.value;i+=s.length;for(let l of je){l.re.lastIndex=0;let u;for(;(u=l.re.exec(s))!==null;){let d=u[1]==null?u[0]:u[1];v(t,e,{code:w.PLACEHOLDER_TEXT,line:k(a,o.start+u.index+u[0].indexOf(d)),message:`The page prints ${JSON.stringify(d.slice(0,60))} as VISIBLE TEXT \u2014 ${l.what}. A reader sees this literally.`,hint:"Format the value before it reaches the page: pick the field you meant off the object, and guard the arithmetic so a missing input renders as an em dash rather than NaN."}),u.index===l.re.lastIndex&&(l.re.lastIndex+=1)}}let c=Z(s).trim();Je.test(c)&&Fe.has(o.parent.tag)&&v(t,e,{code:w.PLACEHOLDER_TEXT,line:k(a,o.start),message:`A <${o.parent.tag}> contains only the literal text "${c}" \u2014 the value it was meant to hold was ${c}.`,hint:'Render an em dash (\u2014) or "n/a" for a missing value; never let the JavaScript literal reach the reader.'})}r.textCharsScanned=i}function K({html:a,data:n}){let t=typeof a=="string"?a:"",e=[],r={};if(!t.trim())return{ok:!1,defects:[{code:"empty-content",line:1,message:"No html was passed to the check.",hint:"Pass the exact page source you are about to publish."}],checked:{tables:0,bodyRows:0,bars:0,barGroups:[],boundBars:0,seriesInData:0,collections:[]}};let i=Te(t),o=Ne(i),s=Le(i),c=Ce(i,o),l=Re(n),u={tables:s.length,bodyRows:s.reduce((d,h)=>d+h.rows.length,0),bars:c.reduce((d,h)=>d+h.length,0),barGroups:[],boundBars:0,seriesInData:l.series.length,collections:[]};return Pe(t,i,s,l.collections,e,r,u.collections),Be(t,c,l.series,e,r,u),Me(t,i,o,e,r,u),De(t,i,o,e,r),We(t,i,e,r,u),e.sort((d,h)=>(d.line||0)-(h.line||0)),{ok:e.length===0,defects:e,checked:u}}function Xe(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let a=Ke(qe(import.meta.url)),n=ze(a,"..","bin","mcp-skill.mjs");return se(n)?n:null}function I(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let a=Ge(Ve(),".zibby","config.json");return se(a)&&JSON.parse(Ye(a,"utf-8")).sessionToken||null}catch{return null}}function C(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function Qe(){return(typeof process.env.WORKFLOW_TYPE=="string"?process.env.WORKFLOW_TYPE.trim():"")||"agent"}function le(a){return`${Qe()}:artifact:${a}`}var x="headings, paragraphs, **bold**, *italic*, `inline code`, fenced code blocks, bullet + numbered lists, blockquotes, horizontal rules, links, and GitHub-style pipe tables (including |---:| alignment)",B="raw HTML (tags are escaped and print as literal text), images, nested lists, footnotes and strikethrough",z='It is read in a list next to everything else you and your teammates have ever published, so it must say WHAT THIS ONE IS \u2014 the subject, not the genre. "Vikunja label sort toggle \u2014 3 of 5 checks failed" survives that list; "Exploration Report" does not, and three of those in a row are indistinguishable. Name the specific thing: the ticket, the feature, the file, the finding.',q='A self-explanatory Markdown STORY shown UNDER the artifact on its page \u2014 a recording or a screenshot cannot explain itself: without this the whole page says `video/webm \xB7 3.8 MB \xB7 Download`. Structure it as: (1) ONE opening sentence of context + goal \u2014 what task this artifact is about and what it is meant to show or prove, written for a reader who knows nothing; (2) the STORY \u2014 what was done and what happened, in plain prose or short bullets; (3) ONE closing sentence \u2014 what the reader should conclude. Complete sentences, Markdown formatting (short paragraphs, bullets) welcome. NEVER compressed arrow-chains ("A \u2192 B \u2192 C"), bare procedure logs, or unexplained jargon \u2014 a step list with no goal and no conclusion explains nothing.',_=2,N=0;function kt(){N=0}function j(a,n){return JSON.stringify({published:!1,stop:!0,attemptsUsed:_,defects:n||[],error:a,instruction:`STOP. You have used both publish attempts and the page is still rejected. Do NOT call artifact_publish or artifact_update again in this turn. Tell the user plainly that you could not produce a clean page, say in one line what was wrong, and give them the content directly in the chat instead. A broken link is worse than an honest "I couldn't".`})}function et(a,n,t){return JSON.stringify({published:!1,stop:!1,attemptsRemaining:_-N,defects:t||[],error:n,instruction:"The page was NOT published and no URL exists. Fix EXACTLY the defects listed above and call once more \u2014 you get ONE more attempt, then this tool will stop you."+(a==="html"?` You are on the \`html\` path, where you author the entire document and every mistake ships. Unless this page genuinely needs a custom visual layout, the fastest and most reliable fix is to re-send the SAME content as \`markdown\` \u2014 Zibby then generates the document skeleton, CSS and tables for you and these defects become impossible. Markdown supports ${x}.`:` Markdown supports ${x}; it does NOT support ${B}.`)})}var X="The page WAS published and the url is valid. This only means the data\u2194render pre-flight could not reach a verdict \u2014 nothing is known to be wrong. Open the page yourself if the figures matter.";function tt(a){let n=new Set,t=(a||[]).map((r,i)=>{let o=r.line?` line ${r.line}:`:"",s="";return r.hint&&!n.has(r.code)&&(n.add(r.code),s=` \u2014 ${r.hint}`),`${i+1}. [${r.code}]${o} ${r.message}${s}`}),e=t.length;return`Artifact NOT published and NOTHING was sent \u2014 the page was checked against the \`data\` you passed and disagrees with it in ${e} place${e===1?"":"s"}:
|
|
2
|
-
${
|
|
3
|
-
`)}`}function
|
|
1
|
+
import{SKILL_META as Xe}from"@zibby/skill-ids";import{existsSync as ce,readFileSync as Qe}from"node:fs";import{homedir as et}from"node:os";import{join as tt,dirname as nt,resolve as rt}from"node:path";import{fileURLToPath as at}from"node:url";import{existsSync as ft}from"node:fs";import{dirname as mt,resolve as yt}from"node:path";import{fileURLToPath as wt}from"node:url";var w={ZERO_WIDTH:"zero-width-bar",ROW_COUNT:"data-row-count",ROW_MISSING:"data-row-missing",BAR_COUNT:"data-bar-count",BAR_PROPORTION:"bar-proportion",VALUE_MISMATCH:"data-value-mismatch",PLACEHOLDER_TEXT:"placeholder-text",OVERFLOW:"overflow-clipped"},fe=5,ge=20,me=.04,ye=3,be=.5;function k(r,n){if(n==null||n<0)return null;let e=1;for(let t=0;t<n&&t<r.length;t+=1)r.charCodeAt(t)===10&&(e+=1);return e}function T(r,n,e){let t=n[e.code]||0;t>=fe||r.length>=ge||(n[e.code]=t+1,r.push(e))}var we={amp:"&",lt:"<",gt:">",quot:'"',apos:"'",nbsp:" ",ndash:"\u2013",mdash:"\u2014",hellip:"\u2026"};function Y(r){return String(r).replace(/&(#x?[0-9a-f]+|[a-z]+);/gi,(n,e)=>{if(e[0]==="#"){let a=e[1]==="x"||e[1]==="X"?parseInt(e.slice(2),16):parseInt(e.slice(1),10);return Number.isFinite(a)?String.fromCodePoint(a):n}let t=we[String(e).toLowerCase()];return t??n})}function N(r){return Y(String(r??"")).replace(/\s+/g," ").trim().toLowerCase()}function K(r){return Math.round(r*10)/10}var ke=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]),j=new Set(["script","style","textarea"]),Te=new Set(["span","a","em","strong","b","i","u","s","small","code","label","abbr","cite","q","sub","sup","mark","time","var","kbd","samp","font","tt"]),Se=/^<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9:_.-]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>/;function ve(r){let n={},e=/([a-zA-Z_:][-a-zA-Z0-9_:.]*)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'=<>`]+))|([a-zA-Z_:][-a-zA-Z0-9_:.]*)/g,t;for(;(t=e.exec(r))!==null;)t[1]?n[t[1].toLowerCase()]=t[3]!=null?t[3]:t[4]!=null?t[4]:t[5]==null?"":t[5]:t[6]&&(n[t[6].toLowerCase()]="");return n}function Oe(r){let n={tag:"#root",attrs:{},children:[],parent:null,start:0},e=n,t=[n],a=0,i=(o,s)=>{if(s<=o)return;let c=r.slice(o,s);c.trim()&&e.children.push({tag:"#text",attrs:{},children:[],parent:e,start:o,value:c})};for(;a<r.length;){let o=r.indexOf("<",a);if(o<0){i(a,r.length);break}if(i(a,o),r.startsWith("<!--",o)){let d=r.indexOf("-->",o);a=d<0?r.length:d+3;continue}if(r.startsWith("<!",o)||r.startsWith("<?",o)){let d=r.indexOf(">",o);a=d<0?r.length:d+1;continue}let s=Se.exec(r.slice(o));if(!s){a=o+1;continue}let c=s[0],l=s[2].toLowerCase();if(s[1]){for(let d=t.length-1;d>0;d-=1)if(t[d].tag===l){t.length=d,e=t[d-1];break}a=o+c.length;continue}let u={tag:l,attrs:ve(s[3]||""),children:[],parent:e,start:o};if(e.children.push(u),a=o+c.length,!(s[4]||ke.has(l))){if(j.has(l)){let d=r.slice(a),h=new RegExp(`</\\s*${l}\\s*>`,"i").exec(d);u.raw=h?d.slice(0,h.index):d,a+=h?h.index+h[0].length:d.length;continue}t.push(u),e=u}}return n}function v(r,n){for(let e of r.children)e.tag!=="#text"&&(n(e),v(e,n))}function P(r){let n=[],e=t=>{for(let a of t.children)a.tag==="#text"?n.push(a.value||""):(n.push(" "),e(a),n.push(" "))};return e(r),N(n.join(""))}function Ne(r){let n=[],e=t=>{for(let a of t.children)a.tag==="#text"?n.push({value:a.value||"",start:a.start,parent:t}):a.tag!=="pre"&&a.tag!=="code"&&e(a)};return e(r),n}function A(r){let n={};for(let e of String(r||"").split(";")){let t=e.indexOf(":");t<0||(n[e.slice(0,t).trim().toLowerCase()]=e.slice(t+1).trim().replace(/\s*!important$/i,""))}return n}function xe(r){let n=[];return v(r,e=>{if(e.tag!=="style"||!e.raw)return;let t=e.raw.replace(/@[a-z-]+[^{]*\{/gi," "),a=/([^{}]+)\{([^{}]*)\}/g,i;for(;(i=a.exec(t))!==null;){let o=A(i[2]);for(let s of i[1].split(",")){let c=s.trim();if(!c||/[\s>+~:[\]*]/.test(c))continue;let l=[],u=null,d=null,h=!0,p=/([.#]?)([A-Za-z_][-\w]*)/g,f;for(;(f=p.exec(c))!==null;)if(f[1]===".")l.push(f[2].toLowerCase());else if(f[1]==="#")u=f[2];else if(f.index===0)d=f[2].toLowerCase();else{h=!1;break}h&&(d||u||l.length)&&n.push({classes:l,id:u,tag:d,decls:o})}}}),n}function G(r){return String(r.attrs.class||"").split(/\s+/).filter(Boolean).map(n=>n.toLowerCase())}function Ee(r,n,e){if(r.tag&&r.tag!==n.tag||r.id&&r.id!==n.attrs.id)return!1;for(let t of r.classes)if(e.indexOf(t)<0)return!1;return!0}function S(r,n,e){let t=A(r.attrs.style)[n];if(t!=null&&t!=="")return t.toLowerCase();let a=G(r),i=null;for(let o of e){if(!Ee(o,r,a))continue;let s=o.decls[n];s!=null&&s!==""&&(i=s.toLowerCase())}return i}function Z(r,n){let e=S(r,"display",n);return e||(Te.has(r.tag)?"inline":"block")}function D(r){if(!r)return null;let n=/^(-?\d+(?:\.\d+)?)px$/.exec(String(r).trim());return n?Number(n[1]):null}function V(r){if(!r)return null;let n=/^(-?\d+(?:\.\d+)?)%$/.exec(String(r).trim());return n?Number(n[1]):null}function $(r){return r!=null&&typeof r=="object"&&!Array.isArray(r)}function Re(r){if(!r.length||!$(r[0]))return r.map(t=>t==null?"":String(t));let n=null,e=0;for(let t of Object.keys(r[0])){let a=r.map(o=>$(o)&&o[t]!=null?String(o[t]):"");if(a.some(o=>o.length<2))continue;let i=new Set(a.map(N)).size;if(i>e&&(e=i,n=a),i===r.length)break}return n||r.map((t,a)=>String($(t)?Object.values(t)[0]==null?a:Object.values(t)[0]:t))}function _e(r){let n=[],e=[],t=new Set,a=(o,s)=>{if(!(s.length<2)){if(s.every($)){n.push({path:o,records:s,identities:Re(s)});let c=Object.keys(s[0]),l=c.filter(d=>s.every(h=>typeof h[d]=="number"&&Number.isFinite(h[d]))),u=c.filter(d=>s.every(h=>typeof h[d]=="string"));l.length===1&&u.length>=1&&e.push({path:o,labels:s.map(d=>String(d[u[0]])),values:s.map(d=>Number(d[l[0]]))});return}s.every(c=>typeof c=="number"&&Number.isFinite(c))&&e.push({path:o,labels:s.map((c,l)=>`#${l}`),values:s.slice()})}},i=(o,s,c)=>{if(!(c>6||o==null||typeof o!="object"||t.has(o))){if(t.add(o),Array.isArray(o)){a(s,o);for(let l=0;l<o.length&&l<200;l+=1)i(o[l],`${s}[${l}]`,c+1);return}for(let l of Object.keys(o))i(o[l],s?`${s}.${l}`:l,c+1)}};return i(r,"",0),{collections:n,series:e}}function Ae(r){let n=String(r||"").match(/[MmLlHhVvZz]|-?\d*\.?\d+(?:e-?\d+)?/gi);if(!n)return null;let e=0,t=0,a="",i=1/0,o=-1/0,s=1/0,c=-1/0,l=()=>{!Number.isFinite(e)||!Number.isFinite(t)||(e<i&&(i=e),e>o&&(o=e),t<s&&(s=t),t>c&&(c=t))},u=0,d=0;for(;u<n.length&&d++<2e4;){let h=n[u];if(/[A-Za-z]/.test(h)&&(a=h,u+=1,a==="Z"||a==="z"))continue;if(u>=n.length)break;let p=()=>{let f=Number(n[u]);return u+=1,f};switch(a){case"M":e=p(),t=p(),l(),a="L";break;case"m":e+=p(),t+=p(),l(),a="l";break;case"L":e=p(),t=p(),l();break;case"l":e+=p(),t+=p(),l();break;case"H":e=p(),l();break;case"h":e+=p(),l();break;case"V":t=p(),l();break;case"v":t+=p(),l();break;default:u+=1;break}}return!Number.isFinite(i)||!Number.isFinite(s)?null:{w:o-i,h:c-s}}var Ie=/(^|[-_])(bar|gauge|meter|fill|progress|track)([-_]|$)/i;function Le(r){let n=r.parent;for(let e=0;n&&e<3;e+=1,n=n.parent){let t=P(n);if(t)return t}return""}function q(r,n){if(G(r).some(a=>Ie.test(a)))return!0;let e=A(r.attrs.style);return!!(e.background||e["background-color"]||S(r,"background",n)||S(r,"background-color",n))&&P(r)===""}function Me(r){let n=new Map;for(let t of r)for(let a=t.node.parent;a;a=a.parent)n.set(a,(n.get(a)||0)+1);let e=new Map;for(let t of r){let a=null;for(let i=t.node.parent;i;i=i.parent)if((n.get(i)||0)>=2){a=i;break}a&&(e.has(a)||e.set(a,[]),e.get(a).push(t))}return[...e.values()].filter(t=>t.length>=2)}function Ce(r,n){let e=new Map,t=[];v(r,i=>{if(i.tag==="path"||i.tag==="rect"){let u=i.attrs.ecmeta_series_index;if(u==null)return;let d=null;if(i.tag==="rect"){let f=Number(i.attrs.width),b=Number(i.attrs.height);Number.isFinite(f)&&Number.isFinite(b)&&(d={w:f,h:b})}else d=Ae(i.attrs.d||"");if(!d)return;let h=i.attrs.ecmeta_data_index,p=String(u);e.has(p)||e.set(p,[]),e.get(p).push({bar:{node:i,extent:NaN,unit:"user",dataIndex:h==null||h===""?null:Number(h),context:"",start:i.start},box:d});return}if(j.has(i.tag)||i.tag==="svg")return;let o=A(i.attrs.style),s=o.width==null?o["inline-size"]:o.width;if(s==null||s==="")return;let c=V(s),l=D(s);c==null&&l==null||q(i,n)&&t.push({node:i,extent:c??l,unit:c==null?"px":"%",dataIndex:null,context:Le(i),start:i.start})});let a=[];for(let i of e.values()){if(i.length<2)continue;let o=l=>Math.max(...l)-Math.min(...l),s=o(i.map(l=>l.box.h))>=o(i.map(l=>l.box.w));for(let l of i)l.bar.extent=s?l.box.h:l.box.w;let c=i.map(l=>l.bar).sort((l,u)=>l.dataIndex!=null&&u.dataIndex!=null?l.dataIndex-u.dataIndex:l.start-u.start);a.push(c)}for(let i of Me(t)){let o=i.filter(l=>l.unit==="%"),s=i.filter(l=>l.unit==="px"),c=o.length>=s.length?o:s;c.length>=2&&a.push(c)}return a}function $e(r){let n=[];return v(r,e=>{if(e.tag!=="table")return;let t=[];v(e,a=>{if(a.tag!=="tr")return;let i=a.children.filter(o=>o.tag==="td"||o.tag==="th");i.length&&i.every(o=>o.tag==="th")||t.push(a)}),n.push({node:e,rows:t,rowTexts:t.map(P),start:e.start})}),n}function Pe(r,n,e,t,a,i,o){let s=P(n);for(let c of t){let l=c.records.length;if(l<3)continue;let u=[],d=0;for(let g of c.identities){let y=N(g);!y||y.length<2||(d+=1,s.includes(y)||u.push(String(g)))}if(d<3)continue;let h=d-u.length,p=d?h/d:1,f=null,b=0;for(let g of e){if(!g.rows.length)continue;let y=0,R=new Set;for(let C of g.rowTexts){let _=!1;c.identities.forEach((ue,pe)=>{let W=N(ue);W.length>=2&&C.indexOf(W)>=0&&(_=!0,R.add(pe))}),_&&(y+=1)}y<Math.max(2,Math.ceil(g.rows.length*.5))||R.size<Math.ceil(y*.8)||y>b&&(b=y,f=g)}let m=f!=null;o.push({path:c.path||"(root)",records:l,identitiesSearched:d,presentInPage:h,renderedFraction:Number(p.toFixed(3)),boundTableRows:m?f.rows.length:null}),!(p<be)&&(u.length&&T(a,i,{code:w.ROW_MISSING,line:m?k(r,f.start):1,message:`The data at \`${c.path||"(root)"}\` has ${l} records but ${u.length} of them appear NOWHERE on the page: ${u.slice(0,6).map(g=>JSON.stringify(String(g).slice(0,40))).join(", ")}${u.length>6?`, +${u.length-6} more`:""}.`,hint:"Render every record you were given, or say in the page that it is a partial view and how many were left out. A silently truncated table reads as the complete answer."}),u.length===0&&m&&f.rows.length!==l&&T(a,i,{code:w.ROW_COUNT,line:k(r,f.start),message:`The table renders ${f.rows.length} body row(s) but the data at \`${c.path||"(root)"}\` has ${l} records \u2014 ${Math.abs(l-f.rows.length)} ${l>f.rows.length?"missing":"extra"}.`,hint:'Emit exactly one <tr> per record. If you meant to show only some of them, state the cut ("top 10 of 23") in the page itself.'}))}}function Ue(r,n){let e=new Array(r.length).fill(-1);if(r.every(i=>i.dataIndex!=null)){let i=0;if(r.forEach((o,s)=>{let c=o.dataIndex;c>=0&&c<n.values.length&&(e[s]=c,i+=1)}),i===r.length)return e}e.fill(-1);let t=new Set,a=0;if(r.forEach((i,o)=>{for(let s=0;s<n.labels.length;s+=1){if(t.has(s))continue;let c=N(n.labels[s]);if(c.length>=2&&i.context.indexOf(c)>=0){e[o]=s,t.add(s),a+=1;break}}}),a===r.length)return e;if(r.length!==n.values.length)return null;for(let i=0;i<r.length;i+=1)e[i]=i;return e}function He(r,n){let e=null,t=0;for(let i of n){let o=i.labels.filter(s=>{let c=N(s);return c.length>=2&&r.some(l=>l.context.indexOf(c)>=0)}).length;o>t&&(t=o,e=i)}if(t>=2)return e;let a=n.filter(i=>i.values.length===r.length);return a.length===1?a[0]:null}function Be(r,n,e,t,a,i){i.barGroups=n.map(o=>o.length),i.boundBars=0;for(let o of n){let s=He(o,e);if(!s)continue;if(s.values.length!==o.length){T(t,a,{code:w.BAR_COUNT,line:k(r,o[0].start),message:`The chart draws ${o.length} bar(s) but the series at \`${s.path||"(root)"}\` has ${s.values.length} value(s).`,hint:"Draw one bar per value. A dropped bar silently removes a whole category from the reader's view of the data."});continue}let c=Ue(o,s);if(!c||c.some(h=>h<0))continue;i.boundBars+=o.length;let l=[];for(let h=0;h<o.length;h+=1)o[h].extent>0&&s.values[c[h]]>0&&l.push(h);if(l.length>=ye){let h=l.map(g=>o[g].extent/s.values[c[g]]).sort((g,y)=>g-y),p=Math.floor(h.length/2),f=h.length%2?h[p]:(h[p-1]+h[p])/2,b=Math.max(...s.values.map(g=>Math.abs(g))),m=f*b;if(f>0&&m>0)for(let g of l){let y=s.values[c[g]],R=f*y,C=Math.abs(o[g].extent-R)/m;if(C<=me)continue;let _=o[g].unit==="%"?"%":"";T(t,a,{code:w.BAR_PROPORTION,line:k(r,o[g].start),message:`The bar for ${JSON.stringify(s.labels[c[g]])} (value ${y}) is drawn at ${K(o[g].extent)}${_}, but every other bar in this chart puts it at ${K(R)}${_} \u2014 off by ${Math.round(C*100)}% of the full-scale bar. The chart misrepresents the data.`,hint:"Every bar in one chart must use the SAME value\u2192length scale. Compute each length from its datum with one formula (value / max * 100), never by writing the numbers out by hand."})}}let u=s.values.reduce((h,p)=>h+p,0),d=Math.max(...s.values.map(h=>Math.abs(h)));for(let h=0;h<o.length;h+=1){let p=s.values[c[h]],f=o[h].context;if(!f)continue;let b=(f.match(/-?\d[\d,]*(?:\.\d+)?/g)||[]).map(m=>Number(m.replace(/,/g,""))).filter(m=>Number.isFinite(m));b.length&&(b.some(m=>Math.abs(m-p)<1e-9)||u>0&&b.some(m=>Math.abs(m-p/u*100)<.6)||d>0&&b.some(m=>Math.abs(m-p/d*100)<.6)||T(t,a,{code:w.VALUE_MISMATCH,line:k(r,o[h].start),message:`The row for ${JSON.stringify(s.labels[c[h]])} prints ${b.slice(0,3).join(", ")} but its datum is ${p}.`,hint:"Print the figure your tools returned, unchanged, and use that same figure everywhere it appears on the page."}))}}}var De=/^(flex|inline-flex|grid|inline-grid)$/;function je(r,n,e,t,a,i){let o=0;v(n,s=>{if(j.has(s.tag))return;let c=A(s.attrs.style).width;if(!c)return;let l=V(c);if(l==null||l<=0||!q(s,e))return;o+=1;let u=Z(s,e);if(u==="none"){T(t,a,{code:w.ZERO_WIDTH,line:k(r,s.start),message:`<${s.tag}> is given style="width:${c}" but its effective display is \`none\` \u2014 it is not rendered at all.`,hint:"Remove the display:none rule that matches this element, or remove the element. A width on a display:none box is dead code."});return}if(u!=="inline")return;let d=s.parent;if(d&&d.tag!=="#root"&&De.test(Z(d,e)))return;let h=S(s,"position",e);if(h==="absolute"||h==="fixed")return;let p=S(s,"float",e);p==="left"||p==="right"||T(t,a,{code:w.ZERO_WIDTH,line:k(r,s.start),message:`<${s.tag}${s.attrs.class?` class="${s.attrs.class}"`:""}> is given style="width:${c}" but its effective display is \`inline\` \u2014 CSS IGNORES width on a non-replaced inline box, so this bar renders with no length at all. The source looks right; the page will not.`,hint:"Give the bar display:block or display:inline-block (or make its container display:flex). A <span> is inline by DEFAULT \u2014 that alone is enough to swallow the width."})}),i.percentWidthBars=o}function Je(r,n,e,t,a){v(n,i=>{let o=D(S(i,"width",e));if(!(o==null||o<=0))for(let s=i.parent;s&&s.tag!=="#root";s=s.parent){let c=D(S(s,"width",e));if(c==null||c<=0)continue;if(o<=c)return;let l=S(s,"overflow",e)||S(s,"overflow-x",e);if(l!=="hidden"&&l!=="clip")return;T(t,a,{code:w.OVERFLOW,line:k(r,i.start),message:`<${i.tag}> is ${o}px wide inside a ${c}px ancestor with overflow:${l} \u2014 ${Math.round(o-c)}px of it is clipped and can never be seen.`,hint:"Size the child from its container (a percentage, or max-width:100%), or widen the container. Content the reader cannot see is content you did not publish."});return}})}var Fe=[{re:/\[object (?:Object|Array|Promise|Map|Set|Null|Undefined)\]/g,what:"an object stringified instead of a field being picked off it"},{re:/\$\{[^}\n]{0,80}\}/g,what:"an un-substituted JavaScript template placeholder"},{re:/\{\{[^}\n]{0,80}\}\}/g,what:"an un-substituted moustache placeholder"},{re:/(?:^|[^\w.])(NaN)(?![\w])/g,what:"NaN \u2014 an arithmetic result that was never a number"},{re:/(?:^|[^\w.])(-?Infinity)(?![\w])/g,what:"Infinity \u2014 a division by zero reached the page"}],We=/^(undefined|null)$/i,Ke=new Set(["td","th","li","span","div","strong","em","b"]);function Ze(r,n,e,t,a){let i=0;for(let o of Ne(n)){let s=o.value;i+=s.length;for(let l of Fe){l.re.lastIndex=0;let u;for(;(u=l.re.exec(s))!==null;){let d=u[1]==null?u[0]:u[1];T(e,t,{code:w.PLACEHOLDER_TEXT,line:k(r,o.start+u.index+u[0].indexOf(d)),message:`The page prints ${JSON.stringify(d.slice(0,60))} as VISIBLE TEXT \u2014 ${l.what}. A reader sees this literally.`,hint:"Format the value before it reaches the page: pick the field you meant off the object, and guard the arithmetic so a missing input renders as an em dash rather than NaN."}),u.index===l.re.lastIndex&&(l.re.lastIndex+=1)}}let c=Y(s).trim();We.test(c)&&Ke.has(o.parent.tag)&&T(e,t,{code:w.PLACEHOLDER_TEXT,line:k(r,o.start),message:`A <${o.parent.tag}> contains only the literal text "${c}" \u2014 the value it was meant to hold was ${c}.`,hint:'Render an em dash (\u2014) or "n/a" for a missing value; never let the JavaScript literal reach the reader.'})}a.textCharsScanned=i}function z({html:r,data:n}){let e=typeof r=="string"?r:"",t=[],a={};if(!e.trim())return{ok:!1,defects:[{code:"empty-content",line:1,message:"No html was passed to the check.",hint:"Pass the exact page source you are about to publish."}],checked:{tables:0,bodyRows:0,bars:0,barGroups:[],boundBars:0,seriesInData:0,collections:[]}};let i=Oe(e),o=xe(i),s=$e(i),c=Ce(i,o),l=_e(n),u={tables:s.length,bodyRows:s.reduce((d,h)=>d+h.rows.length,0),bars:c.reduce((d,h)=>d+h.length,0),barGroups:[],boundBars:0,seriesInData:l.series.length,collections:[]};return Pe(e,i,s,l.collections,t,a,u.collections),Be(e,c,l.series,t,a,u),je(e,i,o,t,a,u),Je(e,i,o,t,a),Ze(e,i,t,a,u),t.sort((d,h)=>(d.line||0)-(h.line||0)),{ok:t.length===0,defects:t,checked:u}}var U={api:{knob:"SKILL_API_TIMEOUT_MS",fallback:3e4},transfer:{knob:"SKILL_TRANSFER_TIMEOUT_MS",fallback:12e4},job:{knob:"SKILL_JOB_TIMEOUT_MS",fallback:3e5}};function Ye(r,n,e=process.env){let t=Number(e?.[r]);return Number.isFinite(t)&&t>0?Math.min(6e5,Math.max(1e3,Math.floor(t))):n}function Ge(r="api",n=process.env){let e=U[r]||U.api;return Ye(e.knob,e.fallback,n)}function Ve(r){return r?.name==="TimeoutError"||r?.name==="AbortError"}function qe(r){try{return new URL(String(r?.url??r)).host||"unknown host"}catch{return"unknown host"}}function ze(r,n){if(!r)return n;let e=new AbortController,t=o=>{e.signal.aborted||e.abort(o)},a=()=>t(r.reason),i=()=>t(n.reason);return e.signal.addEventListener("abort",()=>{r.removeEventListener("abort",a),n.removeEventListener("abort",i)},{once:!0}),r.aborted?t(r.reason):n.aborted?t(n.reason):(r.addEventListener("abort",a,{once:!0}),n.addEventListener("abort",i,{once:!0})),e.signal}async function x(r,n={},e={}){let t=e.kind||"api",a=(U[t]||U.api).knob,i=e.timeoutMs?Math.min(6e5,Math.max(1e3,Math.floor(e.timeoutMs))):Ge(t),o=n?.signal;if(o?.aborted)throw o.reason??new DOMException("This operation was aborted","AbortError");let s=AbortSignal.timeout(i),c=ze(o,s);try{return await fetch(r,{...n,signal:c})}catch(l){throw s.aborted&&Ve(l)?new Error(`${e.what||"request"} TIMED OUT after ${i}ms against ${qe(r)} (${a})`):l}}function ot(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let r=nt(at(import.meta.url)),n=rt(r,"..","bin","mcp-skill.mjs");return ce(n)?n:null}function L(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let r=tt(et(),".zibby","config.json");return ce(r)&&JSON.parse(Qe(r,"utf-8")).sessionToken||null}catch{return null}}function M(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function it(){return(typeof process.env.WORKFLOW_TYPE=="string"?process.env.WORKFLOW_TYPE.trim():"")||"agent"}function de(r){return`${it()}:artifact:${r}`}var E="headings, paragraphs, **bold**, *italic*, `inline code`, fenced code blocks, bullet + numbered lists, blockquotes, horizontal rules, links, and GitHub-style pipe tables (including |---:| alignment)",B="raw HTML (tags are escaped and print as literal text), images, nested lists, footnotes and strikethrough",X='It is read in a list next to everything else you and your teammates have ever published, so it must say WHAT THIS ONE IS \u2014 the subject, not the genre. "Vikunja label sort toggle \u2014 3 of 5 checks failed" survives that list; "Exploration Report" does not, and three of those in a row are indistinguishable. Name the specific thing: the ticket, the feature, the file, the finding.',Q='A self-explanatory Markdown STORY shown UNDER the artifact on its page \u2014 a recording or a screenshot cannot explain itself: without this the whole page says `video/webm \xB7 3.8 MB \xB7 Download`. Structure it as: (1) ONE opening sentence of context + goal \u2014 what task this artifact is about and what it is meant to show or prove, written for a reader who knows nothing; (2) the STORY \u2014 what was done and what happened, in plain prose or short bullets; (3) ONE closing sentence \u2014 what the reader should conclude. Complete sentences, Markdown formatting (short paragraphs, bullets) welcome. NEVER compressed arrow-chains ("A \u2192 B \u2192 C"), bare procedure logs, or unexplained jargon \u2014 a step list with no goal and no conclusion explains nothing.',I=2,O=0;function _t(){O=0}function F(r,n){return JSON.stringify({published:!1,stop:!0,attemptsUsed:I,defects:n||[],error:r,instruction:`STOP. You have used both publish attempts and the page is still rejected. Do NOT call artifact_publish or artifact_update again in this turn. Tell the user plainly that you could not produce a clean page, say in one line what was wrong, and give them the content directly in the chat instead. A broken link is worse than an honest "I couldn't".`})}function st(r,n,e){return JSON.stringify({published:!1,stop:!1,attemptsRemaining:I-O,defects:e||[],error:n,instruction:"The page was NOT published and no URL exists. Fix EXACTLY the defects listed above and call once more \u2014 you get ONE more attempt, then this tool will stop you."+(r==="html"?` You are on the \`html\` path, where you author the entire document and every mistake ships. Unless this page genuinely needs a custom visual layout, the fastest and most reliable fix is to re-send the SAME content as \`markdown\` \u2014 Zibby then generates the document skeleton, CSS and tables for you and these defects become impossible. Markdown supports ${E}.`:` Markdown supports ${E}; it does NOT support ${B}.`)})}var ee="The page WAS published and the url is valid. This only means the data\u2194render pre-flight could not reach a verdict \u2014 nothing is known to be wrong. Open the page yourself if the figures matter.";function lt(r){let n=new Set,e=(r||[]).map((a,i)=>{let o=a.line?` line ${a.line}:`:"",s="";return a.hint&&!n.has(a.code)&&(n.add(a.code),s=` \u2014 ${a.hint}`),`${i+1}. [${a.code}]${o} ${a.message}${s}`}),t=e.length;return`Artifact NOT published and NOTHING was sent \u2014 the page was checked against the \`data\` you passed and disagrees with it in ${t} place${t===1?"":"s"}:
|
|
2
|
+
${e.join(`
|
|
3
|
+
`)}`}function ct(r,n){let e;try{e=z({html:r,data:n})}catch(i){return{unverified:{status:"unverified",reason:`the data\u2194render pre-flight threw and was skipped: ${String(i?.message||i).slice(0,200)}`,note:ee}}}if(!e?.ok)return{rejected:{message:lt(e?.defects),defects:e?.defects||[]}};let t=e.checked||{};return(Number(t.tables)||0)+(Number(t.bars)||0)===0?{unverified:{status:"unverified",reason:"the pre-flight found no tables and no bars in this page, so it had nothing to compare against your data \u2014 a clean result here proves nothing",checked:t,note:ee}}:null}function te(r,n,e){return O+=1,{__rejected:!0,payload:O>=I?F(n,e):st(r,n,e)}}async function dt(r){let n=L();if(!n)throw new Error("No backend credential (PROJECT_API_TOKEN). Artifacts are only available inside a Zibby run.");let e=await x(`${M()}/credits/artifacts`,{method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"},body:JSON.stringify(r)},{kind:"api",what:"artifact publish"});if(!e.ok){let t=await e.text().catch(()=>""),a=null;try{a=JSON.parse(t)}catch{}let i=new Error(e.status===422&&a?.error?a.error:`artifact write failed (${e.status}): ${t.slice(0,300)}`);throw i.status=e.status,Array.isArray(a?.defects)&&(i.defects=a.defects),i}return e.json()}async function ne(r,n,e){let t=e?ct(e.html,e.data):null;if(t?.rejected)return te(n,t.rejected.message,t.rejected.defects);try{let a=await dt(r);return O=0,t?.unverified&&(a.__preflightNote=t.unverified),a}catch(a){if(a?.status!==422)throw a;return te(n,a.message,a.defects)}}async function he(r){let n=L();if(!n)throw new Error("No backend credential (PROJECT_API_TOKEN). Artifacts are only available inside a Zibby run.");let e=await x(`${M()}/credits/artifacts/${encodeURIComponent(r)}`,{method:"GET",headers:{Authorization:`Bearer ${n}`}},{kind:"api",what:`artifact read ${r}`});if(!e.ok){let t=await e.text().catch(()=>"");throw new Error(`artifact get failed (${e.status}): ${t.slice(0,300)}`)}return e.json()}async function re(r,n,e={}){let t=L();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). Artifacts are only available inside a Zibby run.");let a=await x(`${M()}/credits/artifacts/${encodeURIComponent(r)}/${n}`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify(e)},{kind:"api",what:`artifact ${n} ${r}`});if(!a.ok){let i=await a.text().catch(()=>""),o=null;try{o=JSON.parse(i)}catch{}throw new Error(o?.error||`artifact ${n} failed (${a.status}): ${i.slice(0,300)}`)}return a.json()}var J=60;function ae(r,n){let e=Math.max(0,n-Math.floor(J/2)),a=r.slice(e,e+J).replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t");return(e>0?"\u2026":"")+a+(e+J<r.length?"\u2026":"")}function ht(r,n){if(r===n)return null;let e=Math.min(r.length,n.length),t=0;for(;t<e&&r[t]===n[t];)t+=1;return{kind:t===e?n.length<r.length?"truncated":"longer":"diverged",sentChars:r.length,storedChars:n.length,delta:n.length-r.length,firstDiffAt:t,sentExcerpt:ae(r,t),storedExcerpt:ae(n,t)}}var H="Read-back compares the STORED SOURCE with what was sent (transport/storage integrity only). It does NOT check that the page renders correctly.";async function oe(r,n){let e;try{e=await he(r)}catch(i){return{status:"unverified",scope:H,reason:`read-back request failed: ${String(i?.message||i).slice(0,200)}`,note:"The page WAS published and the url is valid \u2014 this only means the extra integrity check could not run. No action needed."}}let t=e?.content;if(typeof t!="string")return{status:"unverified",scope:H,reason:"read-back returned no content field",note:"The page WAS published and the url is valid \u2014 this only means the extra integrity check could not run. No action needed."};if(t===""&&n!=="")return{status:"unverified",scope:H,reason:"read-back returned empty content, which the backend also returns when its object-store read errors \u2014 indistinguishable, so this is inconclusive rather than a mismatch",note:"The page WAS published and the url is valid. If the page looks blank when opened, re-send the same content with artifact_update."};let a=ht(n,t);return a?{status:"mismatch",scope:H,...a,note:`The stored page source is NOT what was sent (${a.kind}: sent ${a.sentChars} chars, stored ${a.storedChars}, first difference at character ${a.firstDiffAt}). The url exists and the page is live, so do NOT re-publish a new one. You may call artifact_update ONCE with the identical content to rewrite the blob; if it differs again, stop and tell the user the published page may be incomplete rather than retrying.`}:null}async function ie(r,n){let e=L();if(!e)return;let t=await x(`${M()}/credits/review-memory`,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"},body:JSON.stringify({op:"store",scope:de(r),content:JSON.stringify(n)})},{kind:"api",what:"artifact index store"});if(!t.ok){let a=await t.text().catch(()=>"");throw new Error(`artifact index write failed (${t.status}): ${a.slice(0,200)}`)}}async function ut(r){let n=L();if(!n)return null;let e=await x(`${M()}/credits/review-memory`,{method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json"},body:JSON.stringify({op:"recall",scope:de(r)})},{kind:"api",what:"artifact index recall"});if(!e.ok)return null;let t=await e.json().catch(()=>null);if(!t?.found||!t?.memory?.content)return null;try{return JSON.parse(t.memory.content)}catch{return null}}function se(r){let n=typeof r?.html=="string"&&r.html.length>0,e=typeof r?.markdown=="string"&&r.markdown.length>0,t=typeof r?.text=="string"&&r.text.length>0;return t&&(n||e)?{error:"Pass exactly ONE of markdown, html or text \u2014 text is raw source stored as-is (code/logs/JSON/CSV); markdown and html are rendered documents."}:n&&e?{error:"Pass EITHER markdown OR html, not both \u2014 they are different rendering paths. Use markdown unless the page needs a custom visual layout."}:e?{format:"markdown",content:r.markdown}:n?{format:"html",content:r.html}:t?{format:"text",content:r.text}:null}function le(r,n){let e=r?.data;if(e==null)return null;if(!n||n.format!=="html")return{error:"`data` is only meaningful together with `html`. The pre-flight reads a rendered HTML document (tables, bars, CSS), so on the markdown path it would check nothing while reporting a clean page \u2014 that is worse than not checking. Re-send WITHOUT `data`. Do NOT switch to `html` just to get the check: on the markdown path Zibby generates the document, so the collapsed-bar and broken-geometry defects it looks for cannot happen there in the first place."};let t=e;if(typeof t=="string")try{t=JSON.parse(t)}catch{return{error:'`data` was a string but not valid JSON \u2014 pass the tool results as a JSON object (wrap a bare array as {"rows": [...]}).'}}return{preflight:{html:n.content,data:t}}}var At={id:"artifact",callsBackend:!0,meta:Xe.artifact,serverName:"artifact",allowedTools:["mcp__artifact__*"],description:"Artifacts \u2014 publish a self-contained, shareable HTML/Markdown page to Zibby and get back a URL; the index of what you published is your memory.",promptFragment:`## Artifacts (publish a shareable page, remember what you made)
|
|
4
4
|
You can PUBLISH a standalone page \u2014 a status report, a plan, a comparison table,
|
|
5
5
|
a dashboard-y summary, a diagram, a "here's what I found" write-up \u2014 and get back
|
|
6
6
|
a shareable URL.
|
|
@@ -8,7 +8,7 @@ a shareable URL.
|
|
|
8
8
|
### WRITE IT AS MARKDOWN. That is the default, and it is not a stylistic hint.
|
|
9
9
|
Pass \`markdown\` and Zibby generates the whole document for you \u2014 doctype, head,
|
|
10
10
|
CSS, typography, dark mode, table styling. You supply only the CONTENT. Markdown
|
|
11
|
-
supports ${
|
|
11
|
+
supports ${E}. It does NOT support ${B}.
|
|
12
12
|
|
|
13
13
|
Pass \`html\` ONLY when the page genuinely needs a visual layout Markdown cannot
|
|
14
14
|
express \u2014 a bar chart, a gauge, a custom grid, an SVG diagram. On the html path
|
|
@@ -22,9 +22,9 @@ credentials), so on the html path keep ALL CSS/JS/images INLINE (inline
|
|
|
22
22
|
load. NEVER escape the document: send real tags, not \`<div>\`.
|
|
23
23
|
|
|
24
24
|
### NAME IT FOR SOMEONE SCROLLING A LIST OF FIFTY, and CAPTION IT.
|
|
25
|
-
The \`title\`: ${
|
|
25
|
+
The \`title\`: ${X}
|
|
26
26
|
|
|
27
|
-
Then pass \`description\`: ${
|
|
27
|
+
Then pass \`description\`: ${Q} Headings, bullets and
|
|
28
28
|
tables are all fine. Write one for every artifact a human will open.
|
|
29
29
|
|
|
30
30
|
The SAME two rules govern an artifact a TOOL creates for you \u2014 a browser
|
|
@@ -114,4 +114,4 @@ sharing it silently.
|
|
|
114
114
|
To recall WHAT YOU HAVE ALREADY PUBLISHED, use your kv-memory tool
|
|
115
115
|
kv_recall_prefix with keyPrefix "artifact:" \u2014 each entry is the index record
|
|
116
116
|
{ id, title, url, kind, createdAt, summary } for a page you made. (Publishing
|
|
117
|
-
records this automatically; you don't store it yourself.)`,resolve(){let a=Xe();if(!a)return{command:null,args:[],env:{},description:this.description};let n={};for(let t of["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","ZIBBY_USER_TOKEN","WORKFLOW_TYPE"])process.env[t]&&(n[t]=process.env[t]);return{type:"stdio",command:"node",args:[a,"../dist/artifact.js","artifactSkill"],env:n,description:this.description,alwaysLoad:!0}},async handleToolCall(a,n){try{switch(a){case"artifact_publish":{if(N>=_)return j("Publish budget already spent this turn.",[]);let t=typeof n?.title=="string"?n.title.trim():"";if(!t)return JSON.stringify({error:"title is required"});let e=oe(n);if(!e)return JSON.stringify({error:"provide exactly one of markdown (preferred) or html or text (non-empty string)"});if(e.error)return JSON.stringify({error:e.error});let r=ie(n,e);if(r?.error)return JSON.stringify({error:r.error});if(e.format==="text"&&!(typeof n?.contentType=="string"?n.contentType.trim():""))return JSON.stringify({error:"contentType is required with text \u2014 text/plain for code/logs, application/json for JSON data, text/csv for CSV"});let i={title:t,[e.format]:e.content};e.format==="text"&&(i.contentType=n.contentType.trim()),typeof n?.kind=="string"&&n.kind.trim()&&(i.kind=n.kind.trim()),typeof n?.favicon=="string"&&n.favicon.trim()&&(i.favicon=n.favicon.trim()),typeof n?.description=="string"&&n.description.trim()&&(i.description=n.description),n?.share===!0&&(i.share=!0);let o=await ee(i,e.format,r?.preflight);if(o?.__rejected)return o.payload;let s=o?.id,c=o?.url;if(!s||!c)return JSON.stringify({error:"artifact write returned no id/url",response:o});let l={id:s,url:c};typeof o.rn=="string"&&o.rn&&(l.rn=o.rn),o.shared&&(l.shared=!0,l.shareUrl=o.shareUrl,l.password=o.password,l.instruction="Send BOTH the shareUrl and the password in your reply. Copy the password EXACTLY \u2014 it is case-sensitive, shown once, and cannot be looked up again (artifact_share issues a new one, which kills this one)."),o.__preflightNote&&(l.preflight=o.__preflightNote);let u=await ae(s,e.content);u&&(l.readBack=u);let d={id:s,title:t,url:c,kind:i.kind||null,createdAt:o.createdAt||new Date().toISOString(),summary:typeof n?.summary=="string"&&n.summary.trim()?n.summary.trim():t};e.format==="text"?(d.contentType=typeof o.contentType=="string"?o.contentType:i.contentType,d.bytes=typeof o.bytes=="number"?o.bytes:Buffer.byteLength(e.content,"utf8")):(d.format=e.format,d.bytes=Buffer.byteLength(e.content,"utf8"));try{await re(s,d)}catch(h){l.indexWarning=h.message}return JSON.stringify(l)}case"artifact_update":{if(N>=_)return j("Publish budget already spent this turn.",[]);let t=typeof n?.id=="string"?n.id.trim():"";if(!t)return JSON.stringify({error:"id is required"});let e=oe(n);if(e?.error)return JSON.stringify({error:e.error});let r=typeof n?.title=="string"?n.title.trim():"",i=typeof n?.description=="string"&&n.description.trim();if(!e&&!r&&!i)return JSON.stringify({error:"nothing to update \u2014 pass a new title, description, and/or markdown|html"});let o=ie(n,e);if(o?.error)return JSON.stringify({error:o.error});let s={id:t};r&&(s.title=r),e&&(s[e.format]=e.content),typeof n?.kind=="string"&&n.kind.trim()&&(s.kind=n.kind.trim()),typeof n?.favicon=="string"&&n.favicon.trim()&&(s.favicon=n.favicon.trim()),i&&(s.description=n.description);let c=await ee(s,e?.format,o?.preflight);if(c?.__rejected)return c.payload;let l=c?.url;if(!l)return JSON.stringify({error:"artifact update returned no url",response:c});let u={id:t,url:l};if(typeof c.rn=="string"&&c.rn&&(u.rn=c.rn),c.__preflightNote&&(u.preflight=c.__preflightNote),e){let p=await ae(t,e.content);p&&(u.readBack=p)}let d=await ot(t)||{},h={...d,id:t,url:l,title:r||d.title||"Untitled",kind:s.kind||d.kind||null,createdAt:d.createdAt||c.createdAt||new Date().toISOString(),updatedAt:c.updatedAt||new Date().toISOString()};e&&(e.format==="text"?(h.contentType=typeof c.contentType=="string"?c.contentType:d.contentType||null,h.bytes=typeof c.bytes=="number"?c.bytes:Buffer.byteLength(e.content,"utf8")):(h.format=e.format,h.bytes=Buffer.byteLength(e.content,"utf8"))),typeof n?.summary=="string"&&n.summary.trim()?h.summary=n.summary.trim():h.summary||(h.summary=h.title);try{await re(t,h)}catch(p){u.indexWarning=p.message}return JSON.stringify(u)}case"artifact_get":{let t=typeof n?.id=="string"?n.id.trim():"";if(!t)return JSON.stringify({error:"id is required"});let e=await ce(t);return JSON.stringify(e)}case"artifact_share":{let t=typeof n?.id=="string"?n.id.trim():"";if(!t)return JSON.stringify({error:"id is required"});let e=await te(t,"protect");return JSON.stringify({id:e.id,url:e.url,password:e.password,shared:!0,instruction:"Send BOTH the url and the password to the person who asked. Copy the password EXACTLY \u2014 it is case-sensitive and cannot be recovered later (call artifact_share again to issue a new one)."})}case"artifact_unshare":{let t=typeof n?.id=="string"?n.id.trim():"";if(!t)return JSON.stringify({error:"id is required"});let e=await te(t,"unprotect");return JSON.stringify({id:e.id,shared:!1,wasShared:e.wasShared})}default:return JSON.stringify({error:`Unknown tool: ${a}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"artifact_publish",description:`Publish a NEW self-contained, shareable page (report/plan/table/dashboard/diagram/write-up) and get back a shareable URL. USE \`markdown\` \u2014 it is the default path and Zibby generates the entire document for you (doctype, head, CSS, dark mode, table styling); you supply only the content, so the page cannot come out malformed. Markdown supports ${x}; it does NOT support ${B}. Use \`html\` ONLY when the page needs a visual layout Markdown cannot express (a bar chart, a gauge, a custom grid, an SVG diagram) \u2014 on that path you author the whole document and every mistake ships verbatim. Never pass both. Use \`text\` (+ required \`contentType\`) to publish RAW SOURCE as-is \u2014 code, a script, logs, JSON data, CSV. It is stored verbatim, never rendered as a page, and shown in a read-only code viewer with a download link. contentType: text/plain for code/logs/scripts, application/json for JSON data, text/csv for CSV. For code, put the FILENAME in \`title\` (e.g. "deploy.sh") \u2014 viewers derive the language label from the extension \u2014 and set kind: "code". Exactly one of markdown|html|text per call. Every figure on the page must come from a tool result or this conversation \u2014 do not do arithmetic in prose and print the result, and use the same number everywhere it appears. The content MUST come from the current conversation or data you fetched this turn; if you are not sure what the page should be about, ASK instead of calling this tool, and never publish a demo/sample/placeholder or an account-overview page as a stand-in. On the html path keep all CSS/JS/images INLINE (inline <style>/<script>, data: URIs) and send real tags (never <escaped> markup) \u2014 the page is sandboxed on view with no network at all, so every external URL is dead. PASS \`data\` WHENEVER THE HTML PAGE RENDERS A DATASET \u2014 a table of records, a bar chart, a row of meters. Give it the raw tool results the page was built from, unchanged; the page is then checked AGAINST that data before anything is sent, catching a table short of rows, a bar drawn at the wrong length, and leftover \${\u2026} or [object Object]. It costs you nothing when the page is right. Skip it for prose pages, and never pass it with \`markdown\` (that is an error, not a no-op). The content is VALIDATED before any URL exists: if it is broken \u2014 by that data check or by the server \u2014 you get the exact defects and line numbers back, you get ONE retry, and then the tool stops you. Both kinds of rejection share the SAME two attempts. Returns { id, url }. PASS \`share: true\` WHEN THE READERS HAVE NO ZIBBY ACCOUNT \u2014 you get back { shareUrl, password } in this same call, instead of publishing a link they cannot open and then having to call artifact_share. The password is GENERATED; you never invent one. Without \`share\` the page is member-only, which is the default and is correct for anything internal.`,input_schema:{type:"object",properties:{title:{type:"string",description:`The page title (also the browser tab title). ${z}`},markdown:{type:"string",description:`PREFERRED. The page content as Markdown \u2014 Zibby renders it into a styled, self-contained document, so you never author a skeleton. Supports ${x}. Does NOT support ${B}. Provide this OR html, never both.`},html:{type:"string",description:"The EXCEPTION path \u2014 only for a layout Markdown cannot express (bar chart, gauge, custom grid, SVG diagram). A self-contained HTML document or fragment, served VERBATIM: you own the doctype, head, CSS and every tag. All assets must be inline (<style>/<script>, data: URIs). Provide this OR markdown, never both."},text:{type:"string",description:'RAW SOURCE published as-is \u2014 code, a script, logs, JSON data, CSV. Stored verbatim (never rendered as an HTML page) and shown in a read-only code viewer with a download link. Requires `contentType`. For code, make `title` the filename (e.g. "deploy.sh" \u2014 the viewer derives the language from the extension) and set kind: "code". Provide this OR markdown OR html.'},contentType:{type:"string",description:"REQUIRED with `text`, meaningless otherwise. One of: text/plain (code, scripts, logs), application/json (JSON data), text/csv (CSV). Nothing else is accepted \u2014 HTML and Markdown are published through their own fields, never as text."},data:{type:"object",description:'Optional, and worth passing WHENEVER the html page renders a dataset (a table of records, a bar chart, meters): the raw tool results the page was built from, as JSON, UNCHANGED \u2014 do not summarise it and do not write your own expectations, the check derives what must be true by itself (wrap a bare array as {"rows": [...]}). The page is then verified against it BEFORE publishing: row counts, bar lengths vs their values, bars whose width the CSS throws away, and placeholder residue. If they disagree nothing is sent and you get the defects back, using one of your two attempts. Only valid with `html` \u2014 passing it with `markdown` is an error.'},kind:{type:"string",description:'Optional label for what this is, e.g. "report", "plan", "dashboard", "diagram". Stored in your index.'},favicon:{type:"string",description:'Optional emoji used as the browser-tab icon, e.g. "\u{1F4CA}".'},summary:{type:"string",description:"Optional one-line summary for your own index (defaults to the title). Helps you recall later what this page was."},description:{type:"string",description:`Optional. ${q} This is the artifact's caption, NOT its content: the page/file itself is the content, and a reader who opens it should be able to understand what they are looking at without asking you. Write it as a short report with headings, bullets and tables (same Markdown subset as the \`markdown\` field, 16 KB max).`},share:{type:"boolean",description:"Set true to publish the page WITH a password link in one step \u2014 the response then also carries { shareUrl, password }, which you send together. Use it whenever the people who asked are in a chat channel and have no Zibby account. Omit it (the default) for anything internal: the page is then readable only by logged-in members of this project, and no password exists."}},required:["title"]}},{name:"artifact_update",description:"Revise an EXISTING artifact by id \u2014 the shareable URL stays the same, the content is replaced (new version). Pass the fields to change (title and/or markdown|html|text). `text` only replaces an artifact that was PUBLISHED as text, and it keeps its original contentType \u2014 the server refuses a type change and any doc\u2194text conversion. Prefer `markdown` for the same reason artifact_publish does: Zibby generates the document, so it cannot come out malformed. An update is validated exactly like a publish \u2014 including the optional `data` check when you send new `html` that renders a dataset \u2014 and if the new content is broken it is REFUSED, the page keeps its last good version, and the rejection shares the same two attempts as publish. Returns { id, url }.",input_schema:{type:"object",properties:{id:{type:"string",description:'The id of the artifact to update (from a prior artifact_publish, or your kv-memory "artifact:" index).'},title:{type:"string",description:"New title (optional)."},markdown:{type:"string",description:`PREFERRED. New Markdown content (optional) \u2014 Zibby renders the document skeleton. Supports ${x}. Provide this OR html.`},html:{type:"string",description:"New HTML content (optional), served verbatim \u2014 only for layouts Markdown cannot express. Provide this OR markdown."},text:{type:"string",description:"New raw text content (optional) \u2014 only for an artifact that was PUBLISHED as text. It keeps its original contentType; the server refuses a type change or a doc\u2194text conversion. Provide at most one of markdown|html|text."},data:{type:"object",description:"Optional \u2014 same as on artifact_publish: when the new `html` renders a dataset, pass the raw tool results it was built from and the page is checked against them before anything is sent. Only valid alongside new `html`."},kind:{type:"string",description:"Optional updated kind label."},favicon:{type:"string",description:"Optional updated emoji favicon."},summary:{type:"string",description:"Optional updated one-line index summary."},description:{type:"string",description:"Optional updated Markdown note shown under the artifact on its detail page. You may pass this ALONE to add or rewrite the explanation without touching the artifact itself \u2014 the content keeps its current version."}},required:["id"]}},{name:"artifact_get",description:'Fetch ONE artifact you published, by id \u2192 { metadata, content }. Use to reuse / edit / re-publish a page. To LIST what you have published, use your kv-memory tool kv_recall_prefix with keyPrefix "artifact:".',input_schema:{type:"object",properties:{id:{type:"string",description:"The artifact id."}},required:["id"]}},{name:"artifact_share",description:"Make an artifact viewable WITHOUT a Zibby login, by password. Returns { url, password } \u2014 a link anyone can open plus the password that opens it. USE THIS when the people who need to read the page do not have Zibby accounts \u2014 the usual case is a Lark or Slack channel where you were asked for something and the channel members are not Zibby users. The normal artifact url only opens for logged-in members of this project, so for those recipients it is a dead link; this is what makes it readable. You MUST then send BOTH the url and the password in your reply, and copy the password EXACTLY (it is case-sensitive). It is shown ONCE and cannot be looked up afterwards \u2014 if it is lost, call this again to issue a new one, which also stops the old one working. Works on any artifact you published, old or new. Calling it again on an already-shared artifact replaces the password. Use artifact_unshare to revoke.",input_schema:{type:"object",properties:{id:{type:"string",description:'The id of the artifact to share (from artifact_publish, or your kv-memory "artifact:" index).'}},required:["id"]}},{name:"artifact_unshare",description:`REMOVE an artifact's password \u2014 this is the tool for "take the password off", "remove the password", "stop sharing" and "revoke the link", which are all the same act. The page goes back to the DEFAULT it was published with: viewable only by logged-in members of this project, with no password anywhere. Immediate and total: anyone who already entered the password loses access too, not just new visitors. Safe to call on an artifact that was never shared. Note what this does NOT do: there is no way to leave a page open to anyone with the link and no password. Removing the password closes the anonymous door; it does not prop it open. Say that plainly if you are asked for a passwordless public link.`,input_schema:{type:"object",properties:{id:{type:"string",description:"The artifact id to stop sharing."}},required:["id"]}}]};export{q as ARTIFACT_DESCRIPTION_RULE,z as ARTIFACT_TITLE_RULE,rt as __compareStoredSource,kt as __resetPublishBudget,vt as artifactSkill};
|
|
117
|
+
records this automatically; you don't store it yourself.)`,resolve(){let r=ot();if(!r)return{command:null,args:[],env:{},description:this.description};let n={};for(let e of["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","ZIBBY_USER_TOKEN","WORKFLOW_TYPE"])process.env[e]&&(n[e]=process.env[e]);return{type:"stdio",command:"node",args:[r,"../dist/artifact.js","artifactSkill"],env:n,description:this.description,alwaysLoad:!0}},async handleToolCall(r,n){try{switch(r){case"artifact_publish":{if(O>=I)return F("Publish budget already spent this turn.",[]);let e=typeof n?.title=="string"?n.title.trim():"";if(!e)return JSON.stringify({error:"title is required"});let t=se(n);if(!t)return JSON.stringify({error:"provide exactly one of markdown (preferred) or html or text (non-empty string)"});if(t.error)return JSON.stringify({error:t.error});let a=le(n,t);if(a?.error)return JSON.stringify({error:a.error});if(t.format==="text"&&!(typeof n?.contentType=="string"?n.contentType.trim():""))return JSON.stringify({error:"contentType is required with text \u2014 text/plain for code/logs, application/json for JSON data, text/csv for CSV"});let i={title:e,[t.format]:t.content};t.format==="text"&&(i.contentType=n.contentType.trim()),typeof n?.kind=="string"&&n.kind.trim()&&(i.kind=n.kind.trim()),typeof n?.favicon=="string"&&n.favicon.trim()&&(i.favicon=n.favicon.trim()),typeof n?.description=="string"&&n.description.trim()&&(i.description=n.description),n?.share===!0&&(i.share=!0);let o=await ne(i,t.format,a?.preflight);if(o?.__rejected)return o.payload;let s=o?.id,c=o?.url;if(!s||!c)return JSON.stringify({error:"artifact write returned no id/url",response:o});let l={id:s,url:c};typeof o.rn=="string"&&o.rn&&(l.rn=o.rn),o.shared&&(l.shared=!0,l.shareUrl=o.shareUrl,l.password=o.password,l.instruction="Send BOTH the shareUrl and the password in your reply. Copy the password EXACTLY \u2014 it is case-sensitive, shown once, and cannot be looked up again (artifact_share issues a new one, which kills this one)."),o.__preflightNote&&(l.preflight=o.__preflightNote);let u=await oe(s,t.content);u&&(l.readBack=u);let d={id:s,title:e,url:c,kind:i.kind||null,createdAt:o.createdAt||new Date().toISOString(),summary:typeof n?.summary=="string"&&n.summary.trim()?n.summary.trim():e};t.format==="text"?(d.contentType=typeof o.contentType=="string"?o.contentType:i.contentType,d.bytes=typeof o.bytes=="number"?o.bytes:Buffer.byteLength(t.content,"utf8")):(d.format=t.format,d.bytes=Buffer.byteLength(t.content,"utf8"));try{await ie(s,d)}catch(h){l.indexWarning=h.message}return JSON.stringify(l)}case"artifact_update":{if(O>=I)return F("Publish budget already spent this turn.",[]);let e=typeof n?.id=="string"?n.id.trim():"";if(!e)return JSON.stringify({error:"id is required"});let t=se(n);if(t?.error)return JSON.stringify({error:t.error});let a=typeof n?.title=="string"?n.title.trim():"",i=typeof n?.description=="string"&&n.description.trim();if(!t&&!a&&!i)return JSON.stringify({error:"nothing to update \u2014 pass a new title, description, and/or markdown|html"});let o=le(n,t);if(o?.error)return JSON.stringify({error:o.error});let s={id:e};a&&(s.title=a),t&&(s[t.format]=t.content),typeof n?.kind=="string"&&n.kind.trim()&&(s.kind=n.kind.trim()),typeof n?.favicon=="string"&&n.favicon.trim()&&(s.favicon=n.favicon.trim()),i&&(s.description=n.description);let c=await ne(s,t?.format,o?.preflight);if(c?.__rejected)return c.payload;let l=c?.url;if(!l)return JSON.stringify({error:"artifact update returned no url",response:c});let u={id:e,url:l};if(typeof c.rn=="string"&&c.rn&&(u.rn=c.rn),c.__preflightNote&&(u.preflight=c.__preflightNote),t){let p=await oe(e,t.content);p&&(u.readBack=p)}let d=await ut(e)||{},h={...d,id:e,url:l,title:a||d.title||"Untitled",kind:s.kind||d.kind||null,createdAt:d.createdAt||c.createdAt||new Date().toISOString(),updatedAt:c.updatedAt||new Date().toISOString()};t&&(t.format==="text"?(h.contentType=typeof c.contentType=="string"?c.contentType:d.contentType||null,h.bytes=typeof c.bytes=="number"?c.bytes:Buffer.byteLength(t.content,"utf8")):(h.format=t.format,h.bytes=Buffer.byteLength(t.content,"utf8"))),typeof n?.summary=="string"&&n.summary.trim()?h.summary=n.summary.trim():h.summary||(h.summary=h.title);try{await ie(e,h)}catch(p){u.indexWarning=p.message}return JSON.stringify(u)}case"artifact_get":{let e=typeof n?.id=="string"?n.id.trim():"";if(!e)return JSON.stringify({error:"id is required"});let t=await he(e);return JSON.stringify(t)}case"artifact_share":{let e=typeof n?.id=="string"?n.id.trim():"";if(!e)return JSON.stringify({error:"id is required"});let t=await re(e,"protect");return JSON.stringify({id:t.id,url:t.url,password:t.password,shared:!0,instruction:"Send BOTH the url and the password to the person who asked. Copy the password EXACTLY \u2014 it is case-sensitive and cannot be recovered later (call artifact_share again to issue a new one)."})}case"artifact_unshare":{let e=typeof n?.id=="string"?n.id.trim():"";if(!e)return JSON.stringify({error:"id is required"});let t=await re(e,"unprotect");return JSON.stringify({id:t.id,shared:!1,wasShared:t.wasShared})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(e){return JSON.stringify({error:e.message})}},tools:[{name:"artifact_publish",description:`Publish a NEW self-contained, shareable page (report/plan/table/dashboard/diagram/write-up) and get back a shareable URL. USE \`markdown\` \u2014 it is the default path and Zibby generates the entire document for you (doctype, head, CSS, dark mode, table styling); you supply only the content, so the page cannot come out malformed. Markdown supports ${E}; it does NOT support ${B}. Use \`html\` ONLY when the page needs a visual layout Markdown cannot express (a bar chart, a gauge, a custom grid, an SVG diagram) \u2014 on that path you author the whole document and every mistake ships verbatim. Never pass both. Use \`text\` (+ required \`contentType\`) to publish RAW SOURCE as-is \u2014 code, a script, logs, JSON data, CSV. It is stored verbatim, never rendered as a page, and shown in a read-only code viewer with a download link. contentType: text/plain for code/logs/scripts, application/json for JSON data, text/csv for CSV. For code, put the FILENAME in \`title\` (e.g. "deploy.sh") \u2014 viewers derive the language label from the extension \u2014 and set kind: "code". Exactly one of markdown|html|text per call. Every figure on the page must come from a tool result or this conversation \u2014 do not do arithmetic in prose and print the result, and use the same number everywhere it appears. The content MUST come from the current conversation or data you fetched this turn; if you are not sure what the page should be about, ASK instead of calling this tool, and never publish a demo/sample/placeholder or an account-overview page as a stand-in. On the html path keep all CSS/JS/images INLINE (inline <style>/<script>, data: URIs) and send real tags (never <escaped> markup) \u2014 the page is sandboxed on view with no network at all, so every external URL is dead. PASS \`data\` WHENEVER THE HTML PAGE RENDERS A DATASET \u2014 a table of records, a bar chart, a row of meters. Give it the raw tool results the page was built from, unchanged; the page is then checked AGAINST that data before anything is sent, catching a table short of rows, a bar drawn at the wrong length, and leftover \${\u2026} or [object Object]. It costs you nothing when the page is right. Skip it for prose pages, and never pass it with \`markdown\` (that is an error, not a no-op). The content is VALIDATED before any URL exists: if it is broken \u2014 by that data check or by the server \u2014 you get the exact defects and line numbers back, you get ONE retry, and then the tool stops you. Both kinds of rejection share the SAME two attempts. Returns { id, url }. PASS \`share: true\` WHEN THE READERS HAVE NO ZIBBY ACCOUNT \u2014 you get back { shareUrl, password } in this same call, instead of publishing a link they cannot open and then having to call artifact_share. The password is GENERATED; you never invent one. Without \`share\` the page is member-only, which is the default and is correct for anything internal.`,input_schema:{type:"object",properties:{title:{type:"string",description:`The page title (also the browser tab title). ${X}`},markdown:{type:"string",description:`PREFERRED. The page content as Markdown \u2014 Zibby renders it into a styled, self-contained document, so you never author a skeleton. Supports ${E}. Does NOT support ${B}. Provide this OR html, never both.`},html:{type:"string",description:"The EXCEPTION path \u2014 only for a layout Markdown cannot express (bar chart, gauge, custom grid, SVG diagram). A self-contained HTML document or fragment, served VERBATIM: you own the doctype, head, CSS and every tag. All assets must be inline (<style>/<script>, data: URIs). Provide this OR markdown, never both."},text:{type:"string",description:'RAW SOURCE published as-is \u2014 code, a script, logs, JSON data, CSV. Stored verbatim (never rendered as an HTML page) and shown in a read-only code viewer with a download link. Requires `contentType`. For code, make `title` the filename (e.g. "deploy.sh" \u2014 the viewer derives the language from the extension) and set kind: "code". Provide this OR markdown OR html.'},contentType:{type:"string",description:"REQUIRED with `text`, meaningless otherwise. One of: text/plain (code, scripts, logs), application/json (JSON data), text/csv (CSV). Nothing else is accepted \u2014 HTML and Markdown are published through their own fields, never as text."},data:{type:"object",description:'Optional, and worth passing WHENEVER the html page renders a dataset (a table of records, a bar chart, meters): the raw tool results the page was built from, as JSON, UNCHANGED \u2014 do not summarise it and do not write your own expectations, the check derives what must be true by itself (wrap a bare array as {"rows": [...]}). The page is then verified against it BEFORE publishing: row counts, bar lengths vs their values, bars whose width the CSS throws away, and placeholder residue. If they disagree nothing is sent and you get the defects back, using one of your two attempts. Only valid with `html` \u2014 passing it with `markdown` is an error.'},kind:{type:"string",description:'Optional label for what this is, e.g. "report", "plan", "dashboard", "diagram". Stored in your index.'},favicon:{type:"string",description:'Optional emoji used as the browser-tab icon, e.g. "\u{1F4CA}".'},summary:{type:"string",description:"Optional one-line summary for your own index (defaults to the title). Helps you recall later what this page was."},description:{type:"string",description:`Optional. ${Q} This is the artifact's caption, NOT its content: the page/file itself is the content, and a reader who opens it should be able to understand what they are looking at without asking you. Write it as a short report with headings, bullets and tables (same Markdown subset as the \`markdown\` field, 16 KB max).`},share:{type:"boolean",description:"Set true to publish the page WITH a password link in one step \u2014 the response then also carries { shareUrl, password }, which you send together. Use it whenever the people who asked are in a chat channel and have no Zibby account. Omit it (the default) for anything internal: the page is then readable only by logged-in members of this project, and no password exists."}},required:["title"]}},{name:"artifact_update",description:"Revise an EXISTING artifact by id \u2014 the shareable URL stays the same, the content is replaced (new version). Pass the fields to change (title and/or markdown|html|text). `text` only replaces an artifact that was PUBLISHED as text, and it keeps its original contentType \u2014 the server refuses a type change and any doc\u2194text conversion. Prefer `markdown` for the same reason artifact_publish does: Zibby generates the document, so it cannot come out malformed. An update is validated exactly like a publish \u2014 including the optional `data` check when you send new `html` that renders a dataset \u2014 and if the new content is broken it is REFUSED, the page keeps its last good version, and the rejection shares the same two attempts as publish. Returns { id, url }.",input_schema:{type:"object",properties:{id:{type:"string",description:'The id of the artifact to update (from a prior artifact_publish, or your kv-memory "artifact:" index).'},title:{type:"string",description:"New title (optional)."},markdown:{type:"string",description:`PREFERRED. New Markdown content (optional) \u2014 Zibby renders the document skeleton. Supports ${E}. Provide this OR html.`},html:{type:"string",description:"New HTML content (optional), served verbatim \u2014 only for layouts Markdown cannot express. Provide this OR markdown."},text:{type:"string",description:"New raw text content (optional) \u2014 only for an artifact that was PUBLISHED as text. It keeps its original contentType; the server refuses a type change or a doc\u2194text conversion. Provide at most one of markdown|html|text."},data:{type:"object",description:"Optional \u2014 same as on artifact_publish: when the new `html` renders a dataset, pass the raw tool results it was built from and the page is checked against them before anything is sent. Only valid alongside new `html`."},kind:{type:"string",description:"Optional updated kind label."},favicon:{type:"string",description:"Optional updated emoji favicon."},summary:{type:"string",description:"Optional updated one-line index summary."},description:{type:"string",description:"Optional updated Markdown note shown under the artifact on its detail page. You may pass this ALONE to add or rewrite the explanation without touching the artifact itself \u2014 the content keeps its current version."}},required:["id"]}},{name:"artifact_get",description:'Fetch ONE artifact you published, by id \u2192 { metadata, content }. Use to reuse / edit / re-publish a page. To LIST what you have published, use your kv-memory tool kv_recall_prefix with keyPrefix "artifact:".',input_schema:{type:"object",properties:{id:{type:"string",description:"The artifact id."}},required:["id"]}},{name:"artifact_share",description:"Make an artifact viewable WITHOUT a Zibby login, by password. Returns { url, password } \u2014 a link anyone can open plus the password that opens it. USE THIS when the people who need to read the page do not have Zibby accounts \u2014 the usual case is a Lark or Slack channel where you were asked for something and the channel members are not Zibby users. The normal artifact url only opens for logged-in members of this project, so for those recipients it is a dead link; this is what makes it readable. You MUST then send BOTH the url and the password in your reply, and copy the password EXACTLY (it is case-sensitive). It is shown ONCE and cannot be looked up afterwards \u2014 if it is lost, call this again to issue a new one, which also stops the old one working. Works on any artifact you published, old or new. Calling it again on an already-shared artifact replaces the password. Use artifact_unshare to revoke.",input_schema:{type:"object",properties:{id:{type:"string",description:'The id of the artifact to share (from artifact_publish, or your kv-memory "artifact:" index).'}},required:["id"]}},{name:"artifact_unshare",description:`REMOVE an artifact's password \u2014 this is the tool for "take the password off", "remove the password", "stop sharing" and "revoke the link", which are all the same act. The page goes back to the DEFAULT it was published with: viewable only by logged-in members of this project, with no password anywhere. Immediate and total: anyone who already entered the password loses access too, not just new visitors. Safe to call on an artifact that was never shared. Note what this does NOT do: there is no way to leave a page open to anyone with the link and no password. Removing the password closes the anonymous door; it does not prop it open. Say that plainly if you are asked for a passwordless public link.`,input_schema:{type:"object",properties:{id:{type:"string",description:"The artifact id to stop sharing."}},required:["id"]}}]};export{Q as ARTIFACT_DESCRIPTION_RULE,X as ARTIFACT_TITLE_RULE,ht as __compareStoredSource,_t as __resetPublishBudget,At as artifactSkill};
|
package/dist/chat-notify.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import{existsSync as
|
|
1
|
+
import{existsSync as E}from"fs";import{fileURLToPath as L}from"url";import{dirname as w,resolve as A}from"path";import{resolveIntegrationToken as C}from"@zibby/core/backend-client.js";var y=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",LARK_DOCS:"lark_docs",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",VIKUNJA:"vikunja",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),j=Object.freeze({sentry:{id:"sentry",name:"Sentry",connectPath:"/integrations?provider=sentry"},jira:{id:"jira",name:"Jira",connectPath:"/integrations?provider=jira"},github:{id:"github",name:"GitHub",connectPath:"/integrations?provider=github"},gitlab:{id:"gitlab",name:"GitLab",connectPath:"/integrations?provider=gitlab"},slack:{id:"slack",name:"Slack",connectPath:"/integrations?provider=slack"},lark:{id:"lark",name:"Lark Chat",connectPath:"/integrations?provider=lark"},lark_docs:{id:"lark_docs",name:"Lark App",connectPath:"/integrations?provider=lark_docs"},openai_billing:{id:"openai_billing",name:"OpenAI Admin",connectPath:"/integrations?provider=openai_billing"},anthropic_billing:{id:"anthropic_billing",name:"Anthropic Admin",connectPath:"/integrations?provider=anthropic_billing"},cursor_admin:{id:"cursor_admin",name:"Cursor Admin",connectPath:"/integrations?provider=cursor_admin"},notion:{id:"notion",name:"Notion",connectPath:"/integrations?provider=notion"},google:{id:"google",name:"Google Docs",connectPath:"/integrations?provider=google"},plane:{id:"plane",name:"Plane",connectPath:"/integrations?provider=plane"},linear:{id:"linear",name:"Linear",connectPath:"/integrations?provider=linear"},vikunja:{id:"vikunja",name:"Vikunja",connectPath:"/integrations?provider=vikunja"},figma:{id:"figma",name:"Figma",connectPath:"/integrations?provider=figma"},hubspot:{id:"hubspot",name:"HubSpot",connectPath:"/integrations?provider=hubspot"},open_design:{id:"open_design",name:"OpenDesign",connectPath:"/integrations?provider=open_design"},linkedin_personal:{id:"linkedin_personal",name:"LinkedIn (Personal)",connectPath:"/integrations?provider=linkedin_personal"},linkedin_business:{id:"linkedin_business",name:"LinkedIn (Business)",connectPath:"/integrations?provider=linkedin_business"},discord:{id:"discord",name:"Discord",connectPath:"/integrations?provider=discord"}});var b={api:{knob:"SKILL_API_TIMEOUT_MS",fallback:3e4},transfer:{knob:"SKILL_TRANSFER_TIMEOUT_MS",fallback:12e4},job:{knob:"SKILL_JOB_TIMEOUT_MS",fallback:3e5}};function O(r,e,t=process.env){let n=Number(t?.[r]);return Number.isFinite(n)&&n>0?Math.min(6e5,Math.max(1e3,Math.floor(n))):e}function v(r="api",e=process.env){let t=b[r]||b.api;return O(t.knob,t.fallback,e)}function I(r){return r?.name==="TimeoutError"||r?.name==="AbortError"}function T(r){try{return new URL(String(r?.url??r)).host||"unknown host"}catch{return"unknown host"}}function N(r,e){if(!r)return e;let t=new AbortController,n=d=>{t.signal.aborted||t.abort(d)},i=()=>n(r.reason),a=()=>n(e.reason);return t.signal.addEventListener("abort",()=>{r.removeEventListener("abort",i),e.removeEventListener("abort",a)},{once:!0}),r.aborted?n(r.reason):e.aborted?n(e.reason):(r.addEventListener("abort",i,{once:!0}),e.addEventListener("abort",a,{once:!0})),t.signal}async function g(r,e={},t={}){let n=t.kind||"api",i=(b[n]||b.api).knob,a=t.timeoutMs?Math.min(6e5,Math.max(1e3,Math.floor(t.timeoutMs))):v(n),d=e?.signal;if(d?.aborted)throw d.reason??new DOMException("This operation was aborted","AbortError");let m=AbortSignal.timeout(a),s=N(d,m);try{return await fetch(r,{...e,signal:s})}catch(o){throw m.aborted&&I(o)?new Error(`${t.what||"request"} TIMED OUT after ${a}ms against ${T(r)} (${i})`):o}}function R(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let r=w(L(import.meta.url)),e=A(r,"..","bin","mcp-slack.mjs");return E(e)?e:null}async function _(r,e={}){let{token:t}=await C("slack"),n=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(r),i=`https://slack.com/api/${r}`,a={Authorization:`Bearer ${t}`},d;if(n){let o=new URLSearchParams(e).toString();o&&(i+=`?${o}`)}else a["Content-Type"]="application/json; charset=utf-8",d=JSON.stringify(e);let s=await(await g(i,{method:n?"GET":"POST",headers:a,body:d},{kind:"api",what:`Slack ${r}`})).json();if(!s.ok)throw new Error(`Slack API error: ${s.error}`);return s}var u={id:"slack",callsBackend:!0,serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:y.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack
|
|
2
2
|
You have access to the user's Slack workspace. Use these tools:
|
|
3
3
|
- slack_list_channels, slack_post_message, slack_reply_to_thread
|
|
4
4
|
- slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
|
|
5
5
|
- slack_get_users, slack_get_user_profile
|
|
6
6
|
- slack_lookup_user_by_email (precise email\u2192user_id, prefer this over scanning slack_get_users)
|
|
7
|
-
- slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let r=I();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{let t=await d("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(s=>({id:s.id,name:s.name,topic:s.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await d("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await d("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await d("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await d("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await d("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_users":{let t=await d("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(s=>!s.is_bot&&!s.deleted).map(s=>({id:s.id,name:s.real_name||s.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await d("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await d("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await d("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(s=>({id:s.id,handle:s.handle,name:s.name,description:s.description||"",user_count:Number(s.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await d("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let s=Math.max(1,Math.min(Number(e.limit)||5,25)),n=[],a,u=5;for(let i=0;i<u;i+=1){let l={limit:200};a&&(l.cursor=a);let c=await d("users.list",l);for(let o of c.members||[])o.deleted||o.is_bot||n.push(o);if(a=c.response_metadata?.next_cursor,!a)break}let m=[];for(let i of n){let l=(i.real_name||"").toLowerCase(),c=(i.profile?.display_name||"").toLowerCase(),o=(i.name||"").toLowerCase(),p=0;l.includes(t)&&(p+=100-Math.abs(l.length-t.length)),c.includes(t)&&(p+=60-Math.abs(c.length-t.length)),o.includes(t)&&(p+=30-Math.abs(o.length-t.length)),(l===t||c===t)&&(p+=200),p>0&&m.push({id:i.id,name:i.real_name||i.profile?.display_name||i.name,email:i.profile?.email||void 0,_score:p})}return m.sort((i,l)=>l._score-i._score),JSON.stringify({ok:!0,matches:m.slice(0,s).map(({_score:i,...l})=>l),scanned:n.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as T}from"fs";import{fileURLToPath as A}from"url";import{dirname as w,resolve as L}from"path";import{resolveIntegrationToken as E}from"@zibby/core/backend-client.js";function C(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let r=w(A(import.meta.url)),e=L(r,"..","bin","mcp-lark.mjs");return T(e)?e:null}var P=6e3*1e3,f=null;async function R(){let{appId:r,appSecret:e,host:t}=await E("lark");if(f&&f.appId===r&&f.expiresAt>Date.now())return{token:f.token,host:t};let n=await(await fetch(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return f={token:n.tenant_access_token,expiresAt:Date.now()+P,appId:r},{token:n.tenant_access_token,host:t}}async function g(r,e,t={}){let{token:s,host:n}=await R(),a=`${n}${e}`,u={method:r,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&(u.body=JSON.stringify(t));let i=await(await fetch(a,u)).json();if(i.code!==0)throw new Error(`Lark API ${e} error: ${i.msg||i.code}`);return i.data||{}}function y(r){return JSON.stringify({text:r})}function x(r){return!r||typeof r!="string"||r.startsWith("oc_")?"chat_id":r.startsWith("ou_")?"open_id":r.startsWith("on_")?"union_id":r.startsWith("cli_")?"app_id":r.includes("@")?"email":"chat_id"}var h={id:"lark",callsBackend:!0,serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:k.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"],promptFragment:`## Lark
|
|
7
|
+
- slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let r=R();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{let t=await _("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(n=>({id:n.id,name:n.name,topic:n.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await _("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await _("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await _("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await _("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(n=>({user:n.user,text:n.text,ts:n.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await _("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(n=>({user:n.user,text:n.text,ts:n.ts}))})}case"slack_get_users":{let t=await _("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(n=>!n.is_bot&&!n.deleted).map(n=>({id:n.id,name:n.real_name||n.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await _("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await _("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await _("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(n=>({id:n.id,handle:n.handle,name:n.name,description:n.description||"",user_count:Number(n.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await _("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let n=Math.max(1,Math.min(Number(e.limit)||5,25)),i=[],a,d=5;for(let s=0;s<d;s+=1){let o={limit:200};a&&(o.cursor=a);let l=await _("users.list",o);for(let c of l.members||[])c.deleted||c.is_bot||i.push(c);if(a=l.response_metadata?.next_cursor,!a)break}let m=[];for(let s of i){let o=(s.real_name||"").toLowerCase(),l=(s.profile?.display_name||"").toLowerCase(),c=(s.name||"").toLowerCase(),p=0;o.includes(t)&&(p+=100-Math.abs(o.length-t.length)),l.includes(t)&&(p+=60-Math.abs(l.length-t.length)),c.includes(t)&&(p+=30-Math.abs(c.length-t.length)),(o===t||l===t)&&(p+=200),p>0&&m.push({id:s.id,name:s.real_name||s.profile?.display_name||s.name,email:s.profile?.email||void 0,_score:p})}return m.sort((s,o)=>o._score-s._score),JSON.stringify({ok:!0,matches:m.slice(0,n).map(({_score:s,...o})=>o),scanned:i.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as P}from"fs";import{fileURLToPath as M}from"url";import{dirname as x,resolve as K}from"path";import{resolveIntegrationToken as J}from"@zibby/core/backend-client.js";function U(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let r=x(M(import.meta.url)),e=K(r,"..","bin","mcp-lark.mjs");return P(e)?e:null}var D=6e3*1e3,k=null;async function q(){let{appId:r,appSecret:e,host:t}=await J("lark");if(k&&k.appId===r&&k.expiresAt>Date.now())return{token:k.token,host:t};let i=await(await g(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})},{kind:"api",what:"Lark tenant_access_token"})).json();if(i.code!==0)throw new Error(`Lark tenant_access_token failed: ${i.msg||i.code}`);return k={token:i.tenant_access_token,expiresAt:Date.now()+D,appId:r},{token:i.tenant_access_token,host:t}}async function f(r,e,t={}){let{token:n,host:i}=await q(),a=`${i}${e}`,d={method:r,headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&(d.body=JSON.stringify(t));let s=await(await g(a,d,{kind:"api",what:`Lark ${r} ${e}`})).json();if(s.code!==0)throw new Error(`Lark API ${e} error: ${s.msg||s.code}`);return s.data||{}}function S(r){return JSON.stringify({text:r})}function B(r){return!r||typeof r!="string"||r.startsWith("oc_")?"chat_id":r.startsWith("ou_")?"open_id":r.startsWith("on_")?"union_id":r.startsWith("cli_")?"app_id":r.includes("@")?"email":"chat_id"}var h={id:"lark",callsBackend:!0,serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:y.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"],promptFragment:`## Lark
|
|
8
8
|
You can send messages and replies on Lark. Use:
|
|
9
9
|
- lark_send_message: post a message to a chat, user, or DM
|
|
10
10
|
- lark_reply: reply to an existing message (threaded)
|
|
11
11
|
- lark_list_chats: list chats the bot is a member of
|
|
12
12
|
- lark_get_chat_history: fetch recent messages in a chat
|
|
13
13
|
- lark_lookup_user_by_email: resolve an email \u2192 open_id for direct DM (prefer this over emailing through lark_send_message when the agent has a user_id already)
|
|
14
|
-
When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let r=
|
|
14
|
+
When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let r=U();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},tools:[{name:"lark_send_message",description:"Send a text message to a Lark chat, user, or DM. receive_id can be a chat_id (oc_*), open_id (ou_*), union_id (on_*), or email.",input_schema:{type:"object",properties:{receive_id:{type:"string",description:"Target id: chat_id (oc_*), open_id (ou_*), union_id (on_*), or email"},text:{type:"string",description:"Message text"}},required:["receive_id","text"]}},{name:"lark_reply",description:"Reply to an existing Lark message (creates a thread). Use the message_id from the inbound event.",input_schema:{type:"object",properties:{message_id:{type:"string",description:"Lark message id (om_*) to reply to"},text:{type:"string",description:"Reply text"}},required:["message_id","text"]}},{name:"lark_list_chats",description:"List chats (groups + DMs) the bot is a member of.",input_schema:{type:"object",properties:{page_size:{type:"number",description:"Max results (default 50)"}}}},{name:"lark_get_chat_history",description:"Fetch recent messages in a chat.",input_schema:{type:"object",properties:{chat_id:{type:"string",description:"Chat id (oc_*)"},page_size:{type:"number",description:"Max messages (default 20)"}},required:["chat_id"]}},{name:"lark_lookup_user_by_email",description:"Resolve an email address to a Lark user id (open_id). Returns { ok:true, user:{open_id,email,name} } on hit, { ok:false } if no Lark user has that email. Use the open_id as `receive_id` in lark_send_message to DM.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"lark_search_users",description:'Fuzzy-search users by name across chats the bot is a member of. Lark has no public org-wide user search API for bots \u2014 this walks the bot\'s chat memberships and matches names client-side. Best for "send to Sam" style routing where you have a name but no email. Returns up to `limit` ranked matches { open_id, name }.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against user names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}],async handleToolCall(r,e){try{switch(r){case"lark_send_message":{if(!e.receive_id||!e.text)return JSON.stringify({error:"receive_id and text are required"});let t=B(e.receive_id),n=await f("POST",`/open-apis/im/v1/messages?receive_id_type=${t}`,{receive_id:e.receive_id,msg_type:"text",content:S(e.text)});return JSON.stringify({ok:!0,message_id:n.message_id})}case"lark_reply":{if(!e.message_id||!e.text)return JSON.stringify({error:"message_id and text are required"});let t=await f("POST",`/open-apis/im/v1/messages/${encodeURIComponent(e.message_id)}/reply`,{msg_type:"text",content:S(e.text)});return JSON.stringify({ok:!0,message_id:t.message_id})}case"lark_list_chats":{let t=e.page_size||50,i=((await f("GET",`/open-apis/im/v1/chats?page_size=${t}`)).items||[]).map(a=>({chat_id:a.chat_id,name:a.name,description:a.description,owner_id:a.owner_id,chat_mode:a.chat_mode}));return JSON.stringify({chats:i})}case"lark_get_chat_history":{if(!e.chat_id)return JSON.stringify({error:"chat_id is required"});let t=e.page_size||20,i=((await f("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(e.chat_id)}&page_size=${t}&sort_type=ByCreateTimeDesc`)).items||[]).map(a=>({message_id:a.message_id,sender_id:a.sender?.id,sender_type:a.sender?.sender_type,msg_type:a.msg_type,content:a.body?.content,create_time:a.create_time}));return JSON.stringify({messages:i})}case"lark_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});let n=((await f("POST","/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id",{emails:[e.email]})).user_list||[]).find(i=>i.email===e.email&&i.user_id);return JSON.stringify(n?{ok:!0,user:{open_id:n.user_id,email:n.email,name:n.name||void 0}}:{ok:!1,reason:"no_lark_user_for_email"})}case"lark_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let n=Math.max(1,Math.min(Number(e.limit)||5,25)),i=200,d=((await f("GET","/open-apis/im/v1/chats?page_size=100")).items||[]).map(l=>l.chat_id),m=new Set,s=[];for(let l of d){if(s.length>=i)break;try{let c=await f("GET",`/open-apis/im/v1/chats/${encodeURIComponent(l)}/members?member_id_type=open_id&page_size=100`);for(let p of c.items||[])if(!(!p.member_id||m.has(p.member_id))&&(m.add(p.member_id),s.push({open_id:p.member_id,name:p.name||""}),s.length>=i))break}catch(c){console.warn(`[lark] member scan failed for ${l}: ${c.message}`)}}let o=[];for(let l of s){let c=(l.name||"").toLowerCase();if(!c)continue;let p=0;c.includes(t)&&(p+=100-Math.abs(c.length-t.length)),c===t&&(p+=200),p>0&&o.push({open_id:l.open_id,name:l.name,_score:p})}return o.sort((l,c)=>c._score-l._score),JSON.stringify({ok:!0,matches:o.slice(0,n).map(({_score:l,...c})=>c),scanned:s.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}}};var oe={id:"chat_notify",description:"Chat notification meta-skill \u2014 routes to whichever messaging integration (Slack OR Lark) the user has configured for this project.",envKeys:[...u.envKeys||[],...h.envKeys||[]],get serverName(){if(process.env.SLACK_CHANNEL)return u.serverName;if(process.env.LARK_RECEIVE_ID)return h.serverName;if(process.env.SLACK_BOT_TOKEN)return u.serverName},get allowedTools(){return process.env.SLACK_CHANNEL?u.allowedTools||[]:process.env.LARK_RECEIVE_ID?h.allowedTools||[]:process.env.SLACK_BOT_TOKEN?u.allowedTools||[]:[]},promptFragment:`## Chat notifications (Slack OR Lark \u2014 at least one connected)
|
|
15
15
|
You can post chat messages via:
|
|
16
16
|
- slack_post_message (channel, text[, blocks]) \u2014 Slack. The \`channel\` is REQUIRED on every call.
|
|
17
17
|
- lark_send_message (receive_id, text) \u2014 Lark.
|
|
18
18
|
Where to post:
|
|
19
19
|
- If SLACK_CHANNEL / LARK_RECEIVE_ID is set, that is the default destination \u2014 use it.
|
|
20
20
|
- If NO destination env var is set but your instructions name a channel (e.g. "post to #bla"), post to THAT channel \u2014 pass it as the \`channel\` arg (a "#name" or "C0123" id both work).
|
|
21
|
-
- If neither an env var NOR an instruction gives you a channel, do NOT post \u2014 there is nowhere to send it.`,resolve(r){return process.env.SLACK_CHANNEL&&typeof
|
|
21
|
+
- If neither an env var NOR an instruction gives you a channel, do NOT post \u2014 there is nowhere to send it.`,resolve(r){return process.env.SLACK_CHANNEL&&typeof u.resolve=="function"?u.resolve(r):process.env.LARK_RECEIVE_ID&&typeof h.resolve=="function"?h.resolve(r):process.env.SLACK_BOT_TOKEN&&typeof u.resolve=="function"?u.resolve(r):null},async handleToolCall(r,e,t){return typeof r=="string"&&r.startsWith("slack_")?u.handleToolCall(r,e,t):typeof r=="string"&&r.startsWith("lark_")?h.handleToolCall(r,e,t):JSON.stringify({error:`chat_notify: unknown tool "${r}". Expected slack_* or lark_*.`})},get tools(){return[...u.tools||[],...h.tools||[]]}};export{oe as chatNotifySkill};
|
package/dist/chatProgress.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import{existsSync as
|
|
1
|
+
import{existsSync as q}from"fs";import{fileURLToPath as G}from"url";import{dirname as $,resolve as Y}from"path";import{existsSync as E}from"fs";import{fileURLToPath as L}from"url";import{dirname as A,resolve as P}from"path";import{resolveIntegrationToken as R}from"@zibby/core/backend-client.js";var f=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",LARK_DOCS:"lark_docs",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",VIKUNJA:"vikunja",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),H=Object.freeze({sentry:{id:"sentry",name:"Sentry",connectPath:"/integrations?provider=sentry"},jira:{id:"jira",name:"Jira",connectPath:"/integrations?provider=jira"},github:{id:"github",name:"GitHub",connectPath:"/integrations?provider=github"},gitlab:{id:"gitlab",name:"GitLab",connectPath:"/integrations?provider=gitlab"},slack:{id:"slack",name:"Slack",connectPath:"/integrations?provider=slack"},lark:{id:"lark",name:"Lark Chat",connectPath:"/integrations?provider=lark"},lark_docs:{id:"lark_docs",name:"Lark App",connectPath:"/integrations?provider=lark_docs"},openai_billing:{id:"openai_billing",name:"OpenAI Admin",connectPath:"/integrations?provider=openai_billing"},anthropic_billing:{id:"anthropic_billing",name:"Anthropic Admin",connectPath:"/integrations?provider=anthropic_billing"},cursor_admin:{id:"cursor_admin",name:"Cursor Admin",connectPath:"/integrations?provider=cursor_admin"},notion:{id:"notion",name:"Notion",connectPath:"/integrations?provider=notion"},google:{id:"google",name:"Google Docs",connectPath:"/integrations?provider=google"},plane:{id:"plane",name:"Plane",connectPath:"/integrations?provider=plane"},linear:{id:"linear",name:"Linear",connectPath:"/integrations?provider=linear"},vikunja:{id:"vikunja",name:"Vikunja",connectPath:"/integrations?provider=vikunja"},figma:{id:"figma",name:"Figma",connectPath:"/integrations?provider=figma"},hubspot:{id:"hubspot",name:"HubSpot",connectPath:"/integrations?provider=hubspot"},open_design:{id:"open_design",name:"OpenDesign",connectPath:"/integrations?provider=open_design"},linkedin_personal:{id:"linkedin_personal",name:"LinkedIn (Personal)",connectPath:"/integrations?provider=linkedin_personal"},linkedin_business:{id:"linkedin_business",name:"LinkedIn (Business)",connectPath:"/integrations?provider=linkedin_business"},discord:{id:"discord",name:"Discord",connectPath:"/integrations?provider=discord"}});var k={api:{knob:"SKILL_API_TIMEOUT_MS",fallback:3e4},transfer:{knob:"SKILL_TRANSFER_TIMEOUT_MS",fallback:12e4},job:{knob:"SKILL_JOB_TIMEOUT_MS",fallback:3e5}};function I(r,e,t=process.env){let n=Number(t?.[r]);return Number.isFinite(n)&&n>0?Math.min(6e5,Math.max(1e3,Math.floor(n))):e}function O(r="api",e=process.env){let t=k[r]||k.api;return I(t.knob,t.fallback,e)}function N(r){return r?.name==="TimeoutError"||r?.name==="AbortError"}function T(r){try{return new URL(String(r?.url??r)).host||"unknown host"}catch{return"unknown host"}}function v(r,e){if(!r)return e;let t=new AbortController,n=p=>{t.signal.aborted||t.abort(p)},i=()=>n(r.reason),a=()=>n(e.reason);return t.signal.addEventListener("abort",()=>{r.removeEventListener("abort",i),e.removeEventListener("abort",a)},{once:!0}),r.aborted?n(r.reason):e.aborted?n(e.reason):(r.addEventListener("abort",i,{once:!0}),e.addEventListener("abort",a,{once:!0})),t.signal}async function h(r,e={},t={}){let n=t.kind||"api",i=(k[n]||k.api).knob,a=t.timeoutMs?Math.min(6e5,Math.max(1e3,Math.floor(t.timeoutMs))):O(n),p=e?.signal;if(p?.aborted)throw p.reason??new DOMException("This operation was aborted","AbortError");let _=AbortSignal.timeout(a),s=v(p,_);try{return await fetch(r,{...e,signal:s})}catch(o){throw _.aborted&&N(o)?new Error(`${t.what||"request"} TIMED OUT after ${a}ms against ${T(r)} (${i})`):o}}function w(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let r=A(L(import.meta.url)),e=P(r,"..","bin","mcp-slack.mjs");return E(e)?e:null}async function m(r,e={}){let{token:t}=await R("slack"),n=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(r),i=`https://slack.com/api/${r}`,a={Authorization:`Bearer ${t}`},p;if(n){let o=new URLSearchParams(e).toString();o&&(i+=`?${o}`)}else a["Content-Type"]="application/json; charset=utf-8",p=JSON.stringify(e);let s=await(await h(i,{method:n?"GET":"POST",headers:a,body:p},{kind:"api",what:`Slack ${r}`})).json();if(!s.ok)throw new Error(`Slack API error: ${s.error}`);return s}var y={id:"slack",callsBackend:!0,serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:f.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack
|
|
2
2
|
You have access to the user's Slack workspace. Use these tools:
|
|
3
3
|
- slack_list_channels, slack_post_message, slack_reply_to_thread
|
|
4
4
|
- slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
|
|
5
5
|
- slack_get_users, slack_get_user_profile
|
|
6
6
|
- slack_lookup_user_by_email (precise email\u2192user_id, prefer this over scanning slack_get_users)
|
|
7
|
-
- slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let s=O();if(!s)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[s],env:e,alwaysLoad:!0}},async handleToolCall(s,e){try{switch(s){case"slack_list_channels":{let t=await _("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(r=>({id:r.id,name:r.name,topic:r.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await _("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await _("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await _("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await _("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await _("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_users":{let t=await _("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(r=>!r.is_bot&&!r.deleted).map(r=>({id:r.id,name:r.real_name||r.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await _("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await _("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await _("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(r=>({id:r.id,handle:r.handle,name:r.name,description:r.description||"",user_count:Number(r.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await _("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let r=Math.max(1,Math.min(Number(e.limit)||5,25)),n=[],a,m=5;for(let i=0;i<m;i+=1){let l={limit:200};a&&(l.cursor=a);let c=await _("users.list",l);for(let o of c.members||[])o.deleted||o.is_bot||n.push(o);if(a=c.response_metadata?.next_cursor,!a)break}let d=[];for(let i of n){let l=(i.real_name||"").toLowerCase(),c=(i.profile?.display_name||"").toLowerCase(),o=(i.name||"").toLowerCase(),p=0;l.includes(t)&&(p+=100-Math.abs(l.length-t.length)),c.includes(t)&&(p+=60-Math.abs(c.length-t.length)),o.includes(t)&&(p+=30-Math.abs(o.length-t.length)),(l===t||c===t)&&(p+=200),p>0&&d.push({id:i.id,name:i.real_name||i.profile?.display_name||i.name,email:i.profile?.email||void 0,_score:p})}return d.sort((i,l)=>l._score-i._score),JSON.stringify({ok:!0,matches:d.slice(0,r).map(({_score:i,...l})=>l),scanned:n.length})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as P}from"fs";import{fileURLToPath as E}from"url";import{dirname as T,resolve as A}from"path";import{resolveIntegrationToken as R}from"@zibby/core/backend-client.js";function L(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let s=T(E(import.meta.url)),e=A(s,"..","bin","mcp-lark.mjs");return P(e)?e:null}var w=6e3*1e3,h=null;async function C(){let{appId:s,appSecret:e,host:t}=await R("lark");if(h&&h.appId===s&&h.expiresAt>Date.now())return{token:h.token,host:t};let n=await(await fetch(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:s,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return h={token:n.tenant_access_token,expiresAt:Date.now()+w,appId:s},{token:n.tenant_access_token,host:t}}async function u(s,e,t={}){let{token:r,host:n}=await C(),a=`${n}${e}`,m={method:s,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json; charset=utf-8"}};s!=="GET"&&(m.body=JSON.stringify(t));let i=await(await fetch(a,m)).json();if(i.code!==0)throw new Error(`Lark API ${e} error: ${i.msg||i.code}`);return i.data||{}}function k(s){return JSON.stringify({text:s})}function B(s){return!s||typeof s!="string"||s.startsWith("oc_")?"chat_id":s.startsWith("ou_")?"open_id":s.startsWith("on_")?"union_id":s.startsWith("cli_")?"app_id":s.includes("@")?"email":"chat_id"}var y={id:"lark",callsBackend:!0,serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:g.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"],promptFragment:`## Lark
|
|
7
|
+
- slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let r=w();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{let t=await m("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(n=>({id:n.id,name:n.name,topic:n.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await m("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await m("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await m("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await m("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(n=>({user:n.user,text:n.text,ts:n.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await m("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(n=>({user:n.user,text:n.text,ts:n.ts}))})}case"slack_get_users":{let t=await m("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(n=>!n.is_bot&&!n.deleted).map(n=>({id:n.id,name:n.real_name||n.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await m("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await m("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await m("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(n=>({id:n.id,handle:n.handle,name:n.name,description:n.description||"",user_count:Number(n.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await m("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let n=Math.max(1,Math.min(Number(e.limit)||5,25)),i=[],a,p=5;for(let s=0;s<p;s+=1){let o={limit:200};a&&(o.cursor=a);let l=await m("users.list",o);for(let c of l.members||[])c.deleted||c.is_bot||i.push(c);if(a=l.response_metadata?.next_cursor,!a)break}let _=[];for(let s of i){let o=(s.real_name||"").toLowerCase(),l=(s.profile?.display_name||"").toLowerCase(),c=(s.name||"").toLowerCase(),d=0;o.includes(t)&&(d+=100-Math.abs(o.length-t.length)),l.includes(t)&&(d+=60-Math.abs(l.length-t.length)),c.includes(t)&&(d+=30-Math.abs(c.length-t.length)),(o===t||l===t)&&(d+=200),d>0&&_.push({id:s.id,name:s.real_name||s.profile?.display_name||s.name,email:s.profile?.email||void 0,_score:d})}return _.sort((s,o)=>o._score-s._score),JSON.stringify({ok:!0,matches:_.slice(0,n).map(({_score:s,...o})=>o),scanned:i.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as C}from"fs";import{fileURLToPath as M}from"url";import{dirname as x,resolve as B}from"path";import{resolveIntegrationToken as D}from"@zibby/core/backend-client.js";function K(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let r=x(M(import.meta.url)),e=B(r,"..","bin","mcp-lark.mjs");return C(e)?e:null}var J=6e3*1e3,g=null;async function U(){let{appId:r,appSecret:e,host:t}=await D("lark");if(g&&g.appId===r&&g.expiresAt>Date.now())return{token:g.token,host:t};let i=await(await h(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})},{kind:"api",what:"Lark tenant_access_token"})).json();if(i.code!==0)throw new Error(`Lark tenant_access_token failed: ${i.msg||i.code}`);return g={token:i.tenant_access_token,expiresAt:Date.now()+J,appId:r},{token:i.tenant_access_token,host:t}}async function u(r,e,t={}){let{token:n,host:i}=await U(),a=`${i}${e}`,p={method:r,headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&(p.body=JSON.stringify(t));let s=await(await h(a,p,{kind:"api",what:`Lark ${r} ${e}`})).json();if(s.code!==0)throw new Error(`Lark API ${e} error: ${s.msg||s.code}`);return s.data||{}}function b(r){return JSON.stringify({text:r})}function j(r){return!r||typeof r!="string"||r.startsWith("oc_")?"chat_id":r.startsWith("ou_")?"open_id":r.startsWith("on_")?"union_id":r.startsWith("cli_")?"app_id":r.includes("@")?"email":"chat_id"}var S={id:"lark",callsBackend:!0,serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:f.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"],promptFragment:`## Lark
|
|
8
8
|
You can send messages and replies on Lark. Use:
|
|
9
9
|
- lark_send_message: post a message to a chat, user, or DM
|
|
10
10
|
- lark_reply: reply to an existing message (threaded)
|
|
11
11
|
- lark_list_chats: list chats the bot is a member of
|
|
12
12
|
- lark_get_chat_history: fetch recent messages in a chat
|
|
13
13
|
- lark_lookup_user_by_email: resolve an email \u2192 open_id for direct DM (prefer this over emailing through lark_send_message when the agent has a user_id already)
|
|
14
|
-
When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let
|
|
14
|
+
When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let r=K();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},tools:[{name:"lark_send_message",description:"Send a text message to a Lark chat, user, or DM. receive_id can be a chat_id (oc_*), open_id (ou_*), union_id (on_*), or email.",input_schema:{type:"object",properties:{receive_id:{type:"string",description:"Target id: chat_id (oc_*), open_id (ou_*), union_id (on_*), or email"},text:{type:"string",description:"Message text"}},required:["receive_id","text"]}},{name:"lark_reply",description:"Reply to an existing Lark message (creates a thread). Use the message_id from the inbound event.",input_schema:{type:"object",properties:{message_id:{type:"string",description:"Lark message id (om_*) to reply to"},text:{type:"string",description:"Reply text"}},required:["message_id","text"]}},{name:"lark_list_chats",description:"List chats (groups + DMs) the bot is a member of.",input_schema:{type:"object",properties:{page_size:{type:"number",description:"Max results (default 50)"}}}},{name:"lark_get_chat_history",description:"Fetch recent messages in a chat.",input_schema:{type:"object",properties:{chat_id:{type:"string",description:"Chat id (oc_*)"},page_size:{type:"number",description:"Max messages (default 20)"}},required:["chat_id"]}},{name:"lark_lookup_user_by_email",description:"Resolve an email address to a Lark user id (open_id). Returns { ok:true, user:{open_id,email,name} } on hit, { ok:false } if no Lark user has that email. Use the open_id as `receive_id` in lark_send_message to DM.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"lark_search_users",description:'Fuzzy-search users by name across chats the bot is a member of. Lark has no public org-wide user search API for bots \u2014 this walks the bot\'s chat memberships and matches names client-side. Best for "send to Sam" style routing where you have a name but no email. Returns up to `limit` ranked matches { open_id, name }.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against user names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}],async handleToolCall(r,e){try{switch(r){case"lark_send_message":{if(!e.receive_id||!e.text)return JSON.stringify({error:"receive_id and text are required"});let t=j(e.receive_id),n=await u("POST",`/open-apis/im/v1/messages?receive_id_type=${t}`,{receive_id:e.receive_id,msg_type:"text",content:b(e.text)});return JSON.stringify({ok:!0,message_id:n.message_id})}case"lark_reply":{if(!e.message_id||!e.text)return JSON.stringify({error:"message_id and text are required"});let t=await u("POST",`/open-apis/im/v1/messages/${encodeURIComponent(e.message_id)}/reply`,{msg_type:"text",content:b(e.text)});return JSON.stringify({ok:!0,message_id:t.message_id})}case"lark_list_chats":{let t=e.page_size||50,i=((await u("GET",`/open-apis/im/v1/chats?page_size=${t}`)).items||[]).map(a=>({chat_id:a.chat_id,name:a.name,description:a.description,owner_id:a.owner_id,chat_mode:a.chat_mode}));return JSON.stringify({chats:i})}case"lark_get_chat_history":{if(!e.chat_id)return JSON.stringify({error:"chat_id is required"});let t=e.page_size||20,i=((await u("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(e.chat_id)}&page_size=${t}&sort_type=ByCreateTimeDesc`)).items||[]).map(a=>({message_id:a.message_id,sender_id:a.sender?.id,sender_type:a.sender?.sender_type,msg_type:a.msg_type,content:a.body?.content,create_time:a.create_time}));return JSON.stringify({messages:i})}case"lark_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});let n=((await u("POST","/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id",{emails:[e.email]})).user_list||[]).find(i=>i.email===e.email&&i.user_id);return JSON.stringify(n?{ok:!0,user:{open_id:n.user_id,email:n.email,name:n.name||void 0}}:{ok:!1,reason:"no_lark_user_for_email"})}case"lark_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let n=Math.max(1,Math.min(Number(e.limit)||5,25)),i=200,p=((await u("GET","/open-apis/im/v1/chats?page_size=100")).items||[]).map(l=>l.chat_id),_=new Set,s=[];for(let l of p){if(s.length>=i)break;try{let c=await u("GET",`/open-apis/im/v1/chats/${encodeURIComponent(l)}/members?member_id_type=open_id&page_size=100`);for(let d of c.items||[])if(!(!d.member_id||_.has(d.member_id))&&(_.add(d.member_id),s.push({open_id:d.member_id,name:d.name||""}),s.length>=i))break}catch(c){console.warn(`[lark] member scan failed for ${l}: ${c.message}`)}}let o=[];for(let l of s){let c=(l.name||"").toLowerCase();if(!c)continue;let d=0;c.includes(t)&&(d+=100-Math.abs(c.length-t.length)),c===t&&(d+=200),d>0&&o.push({open_id:l.open_id,name:l.name,_score:d})}return o.sort((l,c)=>c._score-l._score),JSON.stringify({ok:!0,matches:o.slice(0,n).map(({_score:l,...c})=>c),scanned:s.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}}};function Z(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let r=$(G(import.meta.url)),e=Y(r,"..","bin","mcp-skill.mjs");return q(e)?e:null}function F(r={}){let e=String(r.provider||process.env.ZIBBY_PROGRESS_PROVIDER||(process.env.LARK_RECEIVE_ID?"lark":process.env.SLACK_CHANNEL||process.env.SLACK_BOT_TOKEN?"slack":"")).toLowerCase(),t=String(r.chatId||r.channel||process.env.ZIBBY_PROGRESS_CHAT_ID||(e==="lark"?process.env.LARK_RECEIVE_ID:process.env.SLACK_CHANNEL)||"").trim(),n=String(r.mention||process.env.ZIBBY_PROGRESS_MENTION||"").trim();return{provider:e,chatId:t,mention:n}}var z={id:"chat-progress",serverName:"chat_progress",allowedTools:["mcp__chat_progress__*"],envKeys:[...y.envKeys||[],...S.envKeys||[],"ZIBBY_PROGRESS_PROVIDER","ZIBBY_PROGRESS_CHAT_ID","ZIBBY_PROGRESS_MENTION","SLACK_CHANNEL","LARK_RECEIVE_ID"],description:"Report progress back to the triggering chat during a long-running job \u2014 a one-line status the human sees while the work runs. General + reusable; fire-and-forget.",promptFragment:`## Chat Progress (tell the human you're still working \u2014 long jobs only)
|
|
15
15
|
When a run is LONG (many API pages, a shard-capped backfill, dozens of scored
|
|
16
16
|
items), the human who triggered it can't see the middle \u2014 only the final
|
|
17
17
|
notify. Post brief milestones so they know it's alive:
|
|
@@ -21,4 +21,4 @@ notify. Post brief milestones so they know it's alive:
|
|
|
21
21
|
commits, continuing\u2026"). Pass the chat target from your \`notify\` input when
|
|
22
22
|
you have it (provider + chatId); otherwise it resolves from the runtime.
|
|
23
23
|
Fire-and-forget \u2014 if it can't post, just keep working; never treat a failed
|
|
24
|
-
progress ping as an error.`,resolve(){let
|
|
24
|
+
progress ping as an error.`,resolve(){let r=Z();if(!r)return{command:null,args:[],env:{},description:this.description};let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","ZIBBY_USER_TOKEN","SLACK_BOT_TOKEN","SLACK_TEAM_ID","ZIBBY_PROGRESS_PROVIDER","ZIBBY_PROGRESS_CHAT_ID","ZIBBY_PROGRESS_MENTION","SLACK_CHANNEL","LARK_RECEIVE_ID"])process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r,"../dist/chatProgress.js","chatProgressSkill"],env:e,description:this.description,alwaysLoad:!0}},async handleToolCall(r,e){if(r!=="report_progress")return JSON.stringify({error:`Unknown tool: ${r}`});try{let t=String(e?.message||"").trim().slice(0,2e3);if(!t)return JSON.stringify({ok:!1,skipped:"empty message"});let{provider:n,chatId:i,mention:a}=F(e);if(!n||!i)return JSON.stringify({ok:!1,skipped:"no chat target"});let p=a&&n==="slack"?`<@${a}> ${t}`:t,_;n==="lark"?_=await S.handleToolCall("lark_send_message",{receive_id:i,text:t}):_=await y.handleToolCall("slack_post_message",{channel:i,text:p});let s=null;try{s=JSON.parse(_)}catch{}return s&&s.error?JSON.stringify({ok:!1,skipped:`post failed: ${s.error}`}):JSON.stringify({ok:!0,provider:n,posted:!0})}catch(t){return JSON.stringify({ok:!1,skipped:`error: ${t.message}`})}},tools:[{name:"report_progress",description:"Post a ONE-LINE progress status to the chat that triggered this run (so the human sees the long job is alive). Fire-and-forget \u2014 never fails the run. Target resolves from your notify input (provider + chatId) or the runtime; you usually just pass the message.",input_schema:{type:"object",properties:{message:{type:"string",description:'A short human status line, e.g. "Scored 60/188 commits, continuing\u2026".'},provider:{type:"string",enum:["lark","slack"],description:"Optional \u2014 the chat provider (from your notify input). Defaults from the runtime."},chatId:{type:"string",description:"Optional \u2014 the target chat/channel id (from your notify input). Defaults from the runtime."}},required:["message"]}}]},fe=z;export{z as chatProgressSkill,fe as default};
|