@zibby/skills 2.0.7 → 2.0.9

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 CHANGED
@@ -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 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 &lt;escaped&gt; 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};
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}},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 &lt;escaped&gt; 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};
@@ -13,4 +13,4 @@ Pick the destination with \`output\`:
13
13
  compute the geometry from the data so the bars cannot come out wrong.
14
14
  - **Attaching a chart to the run \u2192 \`output:'png'\` (default), \`'svg'\` or
15
15
  \`'both'\`.** Writes files to the run's output folder (auto-uploaded as run
16
- artifacts) and returns the paths. Default 800\xD7600.`,resolve({sessionPath:e,nodeName:t}={}){let r=H();if(!r)return{command:null,args:[],env:{},description:this.description};let n={},a=e&&t?u(e,t):null;a&&(n.ZIBBY_NODE_SESSION_PATH=a),e&&(n.ZIBBY_SESSION_PATH=e);for(let s of["ZIBBY_NODE_SESSION_PATH","ZIBBY_SESSION_PATH"])!n[s]&&process.env[s]&&(n[s]=process.env[s]);return{type:"stdio",command:"node",args:[r,"../dist/chartRender.js","chartRenderSkill"],env:n,description:this.description,alwaysLoad:!0}},async handleToolCall(e,t){if(e!=="chart_render")return JSON.stringify({error:`Unknown tool: ${e}`});try{let r=t?.spec;if(!c(r))return JSON.stringify({error:"spec must be a plain ECharts option OBJECT (e.g. { xAxis: {...}, yAxis: {...}, series: [...] }) \u2014 got "+(r===null?"null":Array.isArray(r)?"an array":typeof r)+". Pass the option object itself, not a string or an array."});let n=x(t?.width,M),a=x(t?.height,R),s=["svg","png","both","svg-inline"].includes(t?.output)?t.output:"png",y=G(t?.filename)||`chart-${Date.now()}`,o;try{o=Y(J(r),n,a)}catch(i){return JSON.stringify({error:`Chart render failed: ${i.message}. The spec must be a valid Apache ECharts option (check series[].type, and that xAxis/yAxis/radar match the series type). Fix the spec and retry.`})}if(typeof o!="string"||!o.includes("<svg"))return JSON.stringify({error:"Chart render produced no SVG \u2014 the spec likely describes an empty chart (no series?). Add at least one series and retry."});if(s==="svg-inline"){let i;try{i=Z(o)}catch(p){return JSON.stringify({error:`Chart cannot be inlined: ${p.message}`})}let l=Buffer.byteLength(i,"utf-8");return l>A?JSON.stringify({error:`Rendered SVG is ${Math.round(l/1024)}KB, over the ${Math.round(A/1024)}KB inline limit. Inline SVG is returned through the conversation and pasted into the page, so it must stay small. Aggregate or sample the data down (a readable chart rarely needs >1000 points), or use output:'svg' to write a file instead.`}):JSON.stringify({ok:!0,width:n,height:a,svg:i,bytes:l,files:[]})}let v=$(),h=[];if(s==="svg"||s==="both"){let i=u(v,`${y}.svg`);S(i,o,"utf-8"),h.push({path:i,format:"svg",bytes:Buffer.byteLength(o,"utf-8")})}if(s==="png"||s==="both"){let i;try{i=K(o)}catch(p){return JSON.stringify({error:`PNG rasterization failed: ${p.message}. Retry with output:'svg' if you only need the vector.`})}let l=u(v,`${y}.png`);S(l,i),h.push({path:l,format:"png",bytes:i.length})}return JSON.stringify({ok:!0,width:n,height:a,files:h})}catch(r){return JSON.stringify({error:`chart_render failed: ${r.message}`})}},tools:[{name:"chart_render",description:`Render a chart LOCALLY (no external service) from a standard Apache ECharts option. Pass the raw ECharts option as \`spec\` \u2014 every ECharts chart type works (bar, line, pie, radar, scatter, heatmap, \u2026). USE output:'svg-inline' WHEN BUILDING AN HTML PAGE, ARTIFACT OR REPORT: it returns the chart as an \`svg\` string (self-contained, no external refs) to paste directly into your HTML \u2014 a published page blocks external hosts, so a file path will not render there, and hand-written bar markup is never needed because ECharts computes the geometry. Otherwise output 'png' (default) / 'svg' / 'both' writes files to the run output folder and returns their paths. animation is forced off; background defaults to white. Bar example: {"xAxis":{"type":"category","data":["Q1","Q2"]},"yAxis":{},"series":[{"type":"bar","data":[12,30]}]}. Radar example: {"legend":{"data":["A","B"]},"radar":{"indicator":[{"name":"speed","max":10},{"name":"cost","max":10},{"name":"quality","max":10}]},"series":[{"type":"radar","data":[{"name":"A","value":[7,4,9]},{"name":"B","value":[5,8,6]}]}]}.`,input_schema:{type:"object",properties:{spec:{type:"object",description:"The Apache ECharts option object, passed through as-is (series, xAxis/yAxis, radar, legend, title, \u2026)."},width:{type:"number",description:"Image width in px (default 800, max 4096)."},height:{type:"number",description:"Image height in px (default 600, max 4096)."},output:{type:"string",enum:["svg","png","both","svg-inline"],description:"Destination. 'svg-inline' writes NO file and returns the SVG markup in the result for pasting into an HTML page/artifact \u2014 use this whenever you are building HTML. 'png' (default) / 'svg' / 'both' write files to the run output folder and return their paths."},filename:{type:"string",description:"Optional file basename (no extension); defaults to chart-<timestamp>."}},required:["spec"]}}]};export{te as chartRenderSkill};
16
+ artifacts) and returns the paths. Default 800\xD7600.`,resolve({sessionPath:e,nodeName:t}={}){let r=H();if(!r)return{command:null,args:[],env:{},description:this.description};let n={},a=e&&t?u(e,t):null;a&&(n.ZIBBY_NODE_SESSION_PATH=a),e&&(n.ZIBBY_SESSION_PATH=e);for(let s of["ZIBBY_NODE_SESSION_PATH","ZIBBY_SESSION_PATH"])!n[s]&&process.env[s]&&(n[s]=process.env[s]);return{type:"stdio",command:"node",args:[r,"../dist/chartRender.js","chartRenderSkill"],env:n,description:this.description}},async handleToolCall(e,t){if(e!=="chart_render")return JSON.stringify({error:`Unknown tool: ${e}`});try{let r=t?.spec;if(!c(r))return JSON.stringify({error:"spec must be a plain ECharts option OBJECT (e.g. { xAxis: {...}, yAxis: {...}, series: [...] }) \u2014 got "+(r===null?"null":Array.isArray(r)?"an array":typeof r)+". Pass the option object itself, not a string or an array."});let n=x(t?.width,M),a=x(t?.height,R),s=["svg","png","both","svg-inline"].includes(t?.output)?t.output:"png",y=G(t?.filename)||`chart-${Date.now()}`,o;try{o=Y(J(r),n,a)}catch(i){return JSON.stringify({error:`Chart render failed: ${i.message}. The spec must be a valid Apache ECharts option (check series[].type, and that xAxis/yAxis/radar match the series type). Fix the spec and retry.`})}if(typeof o!="string"||!o.includes("<svg"))return JSON.stringify({error:"Chart render produced no SVG \u2014 the spec likely describes an empty chart (no series?). Add at least one series and retry."});if(s==="svg-inline"){let i;try{i=Z(o)}catch(p){return JSON.stringify({error:`Chart cannot be inlined: ${p.message}`})}let l=Buffer.byteLength(i,"utf-8");return l>A?JSON.stringify({error:`Rendered SVG is ${Math.round(l/1024)}KB, over the ${Math.round(A/1024)}KB inline limit. Inline SVG is returned through the conversation and pasted into the page, so it must stay small. Aggregate or sample the data down (a readable chart rarely needs >1000 points), or use output:'svg' to write a file instead.`}):JSON.stringify({ok:!0,width:n,height:a,svg:i,bytes:l,files:[]})}let v=$(),h=[];if(s==="svg"||s==="both"){let i=u(v,`${y}.svg`);S(i,o,"utf-8"),h.push({path:i,format:"svg",bytes:Buffer.byteLength(o,"utf-8")})}if(s==="png"||s==="both"){let i;try{i=K(o)}catch(p){return JSON.stringify({error:`PNG rasterization failed: ${p.message}. Retry with output:'svg' if you only need the vector.`})}let l=u(v,`${y}.png`);S(l,i),h.push({path:l,format:"png",bytes:i.length})}return JSON.stringify({ok:!0,width:n,height:a,files:h})}catch(r){return JSON.stringify({error:`chart_render failed: ${r.message}`})}},tools:[{name:"chart_render",description:`Render a chart LOCALLY (no external service) from a standard Apache ECharts option. Pass the raw ECharts option as \`spec\` \u2014 every ECharts chart type works (bar, line, pie, radar, scatter, heatmap, \u2026). USE output:'svg-inline' WHEN BUILDING AN HTML PAGE, ARTIFACT OR REPORT: it returns the chart as an \`svg\` string (self-contained, no external refs) to paste directly into your HTML \u2014 a published page blocks external hosts, so a file path will not render there, and hand-written bar markup is never needed because ECharts computes the geometry. Otherwise output 'png' (default) / 'svg' / 'both' writes files to the run output folder and returns their paths. animation is forced off; background defaults to white. Bar example: {"xAxis":{"type":"category","data":["Q1","Q2"]},"yAxis":{},"series":[{"type":"bar","data":[12,30]}]}. Radar example: {"legend":{"data":["A","B"]},"radar":{"indicator":[{"name":"speed","max":10},{"name":"cost","max":10},{"name":"quality","max":10}]},"series":[{"type":"radar","data":[{"name":"A","value":[7,4,9]},{"name":"B","value":[5,8,6]}]}]}.`,input_schema:{type:"object",properties:{spec:{type:"object",description:"The Apache ECharts option object, passed through as-is (series, xAxis/yAxis, radar, legend, title, \u2026)."},width:{type:"number",description:"Image width in px (default 800, max 4096)."},height:{type:"number",description:"Image height in px (default 600, max 4096)."},output:{type:"string",enum:["svg","png","both","svg-inline"],description:"Destination. 'svg-inline' writes NO file and returns the SVG markup in the result for pasting into an HTML page/artifact \u2014 use this whenever you are building HTML. 'png' (default) / 'svg' / 'both' write files to the run output folder and return their paths."},filename:{type:"string",description:"Optional file basename (no extension); defaults to chart-<timestamp>."}},required:["spec"]}}]};export{te as chartRenderSkill};
@@ -1,17 +1,17 @@
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
1
+ import{existsSync as E}from"fs";import{fileURLToPath as L}from"url";import{dirname as A,resolve as w}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=A(L(import.meta.url)),e=w(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=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
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}},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=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)
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}},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.
@@ -4,14 +4,14 @@ You have access to the user's Slack workspace. Use these tools:
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=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
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}},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 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)
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}},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 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};
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}},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};
package/dist/code-scan.js CHANGED
@@ -6,4 +6,4 @@ Java/Python/Go/Ruby/PHP\u2192semgrep) and runs the matching tool. Pass \`files\`
6
6
  a review) or \`dir\` (a directory to scan). Findings are GROUND-TRUTH CANDIDATES:
7
7
  triage them for THIS change, verify each in context (false positives exist \u2014
8
8
  trace before asserting), fold noise, and turn the real ones into inline
9
- suggestions. Don't hand-lint what the tool already covers, and don't re-run it.`,resolve(){let e=D();return e?{type:"stdio",command:"node",args:[e,"../dist/code-scan.js","codeScanSkill"],env:{},description:this.description,alwaysLoad:!0}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(e,n){if(e!=="scan_code")return JSON.stringify({error:`Unknown tool: ${e}`});try{let s=Array.isArray(n?.files)?n.files.filter(a=>typeof a=="string"&&a.trim()):null,t;if(n?.dir&&typeof n.dir=="string"?t=d(n.dir):s&&s.length?t=ie(s.map(a=>d(a))):t=process.cwd(),!u(t)||!w(t).isDirectory())return JSON.stringify({error:`dir does not exist or is not a directory: ${t}`});let i=s?s.map(a=>d(t,a)):null,r=[],o=0;for(let a of ne){let c=!1;try{c=!!a.detect(t)}catch{c=!1}if(!c)continue;let k=new Set(a.langs.map(y=>y.toLowerCase())),A=i?i.filter(y=>k.has(m(y).toLowerCase())):re(t,a.langs,G),g=se(a,t,A);Array.isArray(g.findings)&&(o+=g.findings.length),r.push(g)}return r.length?JSON.stringify({ok:!0,baseDir:t,totalFindings:o,scanners:r}):JSON.stringify({ok:!0,baseDir:t,scanners:[],totalFindings:0,note:"No known stack detected (no package.json / pyproject / go.mod \u2026). Review by hand."})}catch(s){return JSON.stringify({error:`scan_code failed: ${s.message}`})}},tools:[{name:"scan_code",description:"Run the right deterministic linter for a checked-out repo and return structured findings. Auto-detects the stack (JS/TS \u2192 oxlint; Java/Python/Go/Ruby/PHP \u2192 semgrep OSS) and runs each matching tool, scoped to files in its languages. Pass `files` (e.g. the changed files of the PR \u2014 recommended for a review) OR `dir` (a directory to scan). Returns { scanners: [ { scanner, findings: [ { file, line, severity, rule, message } ] } ] }. Findings are CANDIDATES \u2014 verify each in context before asserting. Best-effort: a stack whose linter is not installed is skipped with a note.",input_schema:{type:"object",properties:{dir:{type:"string",description:"Absolute path to the checked-out repo (or subdirectory) to scan. Defaults to the current working directory."},files:{type:"array",items:{type:"string"},description:"Explicit list of files to scan (paths relative to `dir`, or absolute). Best for a code review \u2014 pass the PR's changed files. When omitted, the whole `dir` is walked (bounded)."}}}}]};function ie(e){if(!e.length)return process.cwd();if(e.length===1)return h(e[0]);let n=e.map(r=>r.split("/")),s=n[0],t=[];for(let r=0;r<s.length;r++){let o=s[r];if(n.every(a=>a[r]===o))t.push(o);else break}let i=t.join("/");return i&&u(i)&&w(i).isDirectory()?i:h(e[0])}export{ne as SCANNERS,W as buildSemgrepTargets,me as codeScanSkill,Z as parseOxlint,Y as parseSemgrep};
9
+ suggestions. Don't hand-lint what the tool already covers, and don't re-run it.`,resolve(){let e=D();return e?{type:"stdio",command:"node",args:[e,"../dist/code-scan.js","codeScanSkill"],env:{},description:this.description}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(e,n){if(e!=="scan_code")return JSON.stringify({error:`Unknown tool: ${e}`});try{let s=Array.isArray(n?.files)?n.files.filter(a=>typeof a=="string"&&a.trim()):null,t;if(n?.dir&&typeof n.dir=="string"?t=d(n.dir):s&&s.length?t=ie(s.map(a=>d(a))):t=process.cwd(),!u(t)||!w(t).isDirectory())return JSON.stringify({error:`dir does not exist or is not a directory: ${t}`});let i=s?s.map(a=>d(t,a)):null,r=[],o=0;for(let a of ne){let c=!1;try{c=!!a.detect(t)}catch{c=!1}if(!c)continue;let k=new Set(a.langs.map(y=>y.toLowerCase())),A=i?i.filter(y=>k.has(m(y).toLowerCase())):re(t,a.langs,G),g=se(a,t,A);Array.isArray(g.findings)&&(o+=g.findings.length),r.push(g)}return r.length?JSON.stringify({ok:!0,baseDir:t,totalFindings:o,scanners:r}):JSON.stringify({ok:!0,baseDir:t,scanners:[],totalFindings:0,note:"No known stack detected (no package.json / pyproject / go.mod \u2026). Review by hand."})}catch(s){return JSON.stringify({error:`scan_code failed: ${s.message}`})}},tools:[{name:"scan_code",description:"Run the right deterministic linter for a checked-out repo and return structured findings. Auto-detects the stack (JS/TS \u2192 oxlint; Java/Python/Go/Ruby/PHP \u2192 semgrep OSS) and runs each matching tool, scoped to files in its languages. Pass `files` (e.g. the changed files of the PR \u2014 recommended for a review) OR `dir` (a directory to scan). Returns { scanners: [ { scanner, findings: [ { file, line, severity, rule, message } ] } ] }. Findings are CANDIDATES \u2014 verify each in context before asserting. Best-effort: a stack whose linter is not installed is skipped with a note.",input_schema:{type:"object",properties:{dir:{type:"string",description:"Absolute path to the checked-out repo (or subdirectory) to scan. Defaults to the current working directory."},files:{type:"array",items:{type:"string"},description:"Explicit list of files to scan (paths relative to `dir`, or absolute). Best for a code review \u2014 pass the PR's changed files. When omitted, the whole `dir` is walked (bounded)."}}}}]};function ie(e){if(!e.length)return process.cwd();if(e.length===1)return h(e[0]);let n=e.map(r=>r.split("/")),s=n[0],t=[];for(let r=0;r<s.length;r++){let o=s[r];if(n.every(a=>a[r]===o))t.push(o);else break}let i=t.join("/");return i&&u(i)&&w(i).isDirectory()?i:h(e[0])}export{ne as SCANNERS,W as buildSemgrepTargets,me as codeScanSkill,Z as parseOxlint,Y as parseSemgrep};
package/dist/codeStats.js CHANGED
@@ -1,4 +1,4 @@
1
- import{existsSync as _}from"fs";import{fileURLToPath as v}from"url";import{dirname as x,resolve as k}from"path";function w(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let s=x(v(import.meta.url)),e=k(s,"..","bin","mcp-skill.mjs");return _(e)?e:null}var S=[{name:"trivial",max:5},{name:"small",max:30},{name:"medium",max:150},{name:"large",max:600},{name:"huge",max:1/0}];function N(s,e){let t=(Number(s)||0)+(Number(e)||0);for(let o of S)if(t<=o.max)return o.name;return"huge"}function T(s){let t=String(s||"").toLowerCase().split("/").pop()||"",o=t.match(/\.(test|spec)\.[a-z0-9]+$/);if(o)return`.${o[1]}`;let i=t.lastIndexOf(".");return i<=0?/^(dockerfile|makefile|jenkinsfile|procfile|gemfile|rakefile)$/i.test(t)?t.toLowerCase():"":t.slice(i)}var C={code:[".js",".jsx",".ts",".tsx",".mjs",".cjs",".py",".go",".rs",".java",".kt",".rb",".php",".c",".cc",".cpp",".h",".hpp",".cs",".swift",".scala",".m",".dart",".ex",".exs",".clj",".sh",".bash"],test:[".test",".spec"],docs:[".md",".mdx",".rst",".txt",".adoc"],config:[".json",".yml",".yaml",".toml",".ini",".env",".cfg",".conf",".lock",".xml",".properties","dockerfile","makefile","jenkinsfile","procfile",".tf",".gradle"],style:[".css",".scss",".sass",".less",".styl"],data:[".csv",".tsv",".sql",".parquet",".proto",".graphql",".gql"]},P=(()=>{let s={};for(let[e,t]of Object.entries(C))for(let o of t)s[o]=e;return s})();function j(s){return P[String(s||"").toLowerCase()]||"other"}function E(s,e={}){let t=Array.isArray(s)?s:[],o=[],i={},n={code:0,test:0,docs:0,config:0,style:0,data:0,other:0},a=0,m=0,c={path:"",changed:-1};for(let r of t){if(!r||typeof r!="object")continue;let l=String(r.path||r.newPath||r.filename||r.new_path||"").trim();if(!l)continue;o.push(l);let g=Number(r.additions??r.add??0)||0,y=Number(r.deletions??r.del??0)||0;a+=g,m+=y;let d=T(l);i[d||"(none)"]=(i[d||"(none)"]||0)+1,n[j(d)]+=1;let b=g+y;b>c.changed&&(c={path:l,changed:b})}let p=Number(e.additions),u=Number(e.deletions),h=Number.isFinite(p)?p:a,f=Number.isFinite(u)?u:m;return{filesChanged:o.length,filePaths:o,extCounts:i,kindCounts:n,additions:h,deletions:f,sizeBucket:N(h,f),largestFile:c.path||null,dirs:[...new Set(o.map(r=>r.includes("/")?r.split("/")[0]:"(root)"))].sort()}}function O(s){let e="",t=String(s||"");for(let o=0;o<t.length;o++){let i=t[o];i==="*"?t[o+1]==="*"?(e+=".*",o++,t[o+1]==="/"&&o++):e+="[^/]*":i==="?"?e+="[^/]":"\\^$.|+()[]{}".includes(i)?e+=`\\${i}`:e+=i}return new RegExp(`^${e}$`)}function A(s,e){let t=Array.isArray(s)?s.map(String):[],o=(Array.isArray(e)?e:[]).filter(Boolean).map(O);if(!o.length)return{total:t.length,hits:0,ratio:0,matched:[]};let i=t.filter(n=>o.some(a=>a.test(n)));return{total:t.length,hits:i.length,ratio:t.length?Number((i.length/t.length).toFixed(4)):0,matched:i}}function L(s,e){let t=(Array.isArray(s)?s:[]).map(Number).filter(n=>Number.isFinite(n)),o=Number(e);if(!t.length||!Number.isFinite(o))return null;let i=t.filter(n=>n<=o).length;return Number((i/t.length*100).toFixed(1))}var R={id:"code-stats",serverName:"code_stats",allowedTools:["mcp__code_stats__*"],description:"Deterministic code statistics \u2014 pure, reproducible facts from changed-file lists + numbers (extension/work-kind mix, size bucket, core-path glob coverage, percentile rank). Zero LLM, zero network.",promptFragment:`## Code Stats (DETERMINISTIC facts \u2014 pure, reproducible, zero-LLM)
1
+ import{existsSync as _}from"fs";import{fileURLToPath as v}from"url";import{dirname as x,resolve as k}from"path";function w(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let s=x(v(import.meta.url)),e=k(s,"..","bin","mcp-skill.mjs");return _(e)?e:null}var S=[{name:"trivial",max:5},{name:"small",max:30},{name:"medium",max:150},{name:"large",max:600},{name:"huge",max:1/0}];function N(s,e){let t=(Number(s)||0)+(Number(e)||0);for(let o of S)if(t<=o.max)return o.name;return"huge"}function T(s){let t=String(s||"").toLowerCase().split("/").pop()||"",o=t.match(/\.(test|spec)\.[a-z0-9]+$/);if(o)return`.${o[1]}`;let i=t.lastIndexOf(".");return i<=0?/^(dockerfile|makefile|jenkinsfile|procfile|gemfile|rakefile)$/i.test(t)?t.toLowerCase():"":t.slice(i)}var C={code:[".js",".jsx",".ts",".tsx",".mjs",".cjs",".py",".go",".rs",".java",".kt",".rb",".php",".c",".cc",".cpp",".h",".hpp",".cs",".swift",".scala",".m",".dart",".ex",".exs",".clj",".sh",".bash"],test:[".test",".spec"],docs:[".md",".mdx",".rst",".txt",".adoc"],config:[".json",".yml",".yaml",".toml",".ini",".env",".cfg",".conf",".lock",".xml",".properties","dockerfile","makefile","jenkinsfile","procfile",".tf",".gradle"],style:[".css",".scss",".sass",".less",".styl"],data:[".csv",".tsv",".sql",".parquet",".proto",".graphql",".gql"]},P=(()=>{let s={};for(let[e,t]of Object.entries(C))for(let o of t)s[o]=e;return s})();function j(s){return P[String(s||"").toLowerCase()]||"other"}function E(s,e={}){let t=Array.isArray(s)?s:[],o=[],i={},n={code:0,test:0,docs:0,config:0,style:0,data:0,other:0},a=0,m=0,l={path:"",changed:-1};for(let r of t){if(!r||typeof r!="object")continue;let c=String(r.path||r.newPath||r.filename||r.new_path||"").trim();if(!c)continue;o.push(c);let g=Number(r.additions??r.add??0)||0,y=Number(r.deletions??r.del??0)||0;a+=g,m+=y;let d=T(c);i[d||"(none)"]=(i[d||"(none)"]||0)+1,n[j(d)]+=1;let b=g+y;b>l.changed&&(l={path:c,changed:b})}let p=Number(e.additions),u=Number(e.deletions),h=Number.isFinite(p)?p:a,f=Number.isFinite(u)?u:m;return{filesChanged:o.length,filePaths:o,extCounts:i,kindCounts:n,additions:h,deletions:f,sizeBucket:N(h,f),largestFile:l.path||null,dirs:[...new Set(o.map(r=>r.includes("/")?r.split("/")[0]:"(root)"))].sort()}}function O(s){let e="",t=String(s||"");for(let o=0;o<t.length;o++){let i=t[o];i==="*"?t[o+1]==="*"?(e+=".*",o++,t[o+1]==="/"&&o++):e+="[^/]*":i==="?"?e+="[^/]":"\\^$.|+()[]{}".includes(i)?e+=`\\${i}`:e+=i}return new RegExp(`^${e}$`)}function A(s,e){let t=Array.isArray(s)?s.map(String):[],o=(Array.isArray(e)?e:[]).filter(Boolean).map(O);if(!o.length)return{total:t.length,hits:0,ratio:0,matched:[]};let i=t.filter(n=>o.some(a=>a.test(n)));return{total:t.length,hits:i.length,ratio:t.length?Number((i.length/t.length).toFixed(4)):0,matched:i}}function R(s,e){let t=(Array.isArray(s)?s:[]).map(Number).filter(n=>Number.isFinite(n)),o=Number(e);if(!t.length||!Number.isFinite(o))return null;let i=t.filter(n=>n<=o).length;return Number((i/t.length*100).toFixed(1))}var F={id:"code-stats",serverName:"code_stats",allowedTools:["mcp__code_stats__*"],description:"Deterministic code statistics \u2014 pure, reproducible facts from changed-file lists + numbers (extension/work-kind mix, size bucket, core-path glob coverage, percentile rank). Zero LLM, zero network.",promptFragment:`## Code Stats (DETERMINISTIC facts \u2014 pure, reproducible, zero-LLM)
2
2
  These tools COMPUTE reproducible facts from data you already have. Use them so
3
3
  your numbers are auditable and survive challenge (same input \u2192 same output).
4
4
 
@@ -16,4 +16,4 @@ Tools:
16
16
  reproducible "works in the core vs the edges" number.
17
17
  - code_stats_percentile: Given \`values\` (a cohort's numbers) + a \`value\`,
18
18
  returns the 0-100 percentile rank. Use to express every metric as a rank
19
- within the team (reproducible + comparable across teams), not a raw number.`,resolve(){let s=w();return s?{type:"stdio",command:"node",args:[s,"../dist/codeStats.js","codeStatsSkill"],env:{},description:this.description,alwaysLoad:!0}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(s,e){try{switch(s){case"code_stats_commit_facts":return JSON.stringify(E(e?.files,{additions:e?.additions,deletions:e?.deletions}));case"code_stats_glob_hit":return JSON.stringify(A(e?.paths,e?.globs));case"code_stats_percentile":return JSON.stringify({percentile:L(e?.values,e?.value)});default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"code_stats_commit_facts",description:"Deterministic per-commit facts from its changed-file list. Pass files: [{path, additions, deletions}] (gitlab_get_commit / github_get_commit shape). Returns filePaths, extCounts, kindCounts (code/test/docs/config/style/data/other), additions, deletions, sizeBucket (trivial/small/medium/large/huge), largestFile, dirs. Pure \u2014 same input always same output.",input_schema:{type:"object",properties:{files:{type:"array",description:"Changed files. Each { path (or newPath/filename), additions?, deletions? }.",items:{type:"object",properties:{path:{type:"string"},additions:{type:"number"},deletions:{type:"number"}}}},additions:{type:"number",description:"Optional commit-level total additions override (else summed from files)."},deletions:{type:"number",description:"Optional commit-level total deletions override."}},required:["files"]}},{name:"code_stats_glob_hit",description:"How many of paths match ANY of globs (supports **, *, ?). Returns { total, hits, ratio, matched }. Use for reproducible core-path coverage against the caller's own core globs.",input_schema:{type:"object",properties:{paths:{type:"array",items:{type:"string"},description:"File paths to test."},globs:{type:"array",items:{type:"string"},description:'Glob patterns, e.g. ["src/payment/**", "auth/**"].'}},required:["paths","globs"]}},{name:"code_stats_percentile",description:'The 0-100 percentile rank of value within values (cumulative "% at or below"). Reproducible + cross-team comparable. Empty cohort \u2192 null.',input_schema:{type:"object",properties:{values:{type:"array",items:{type:"number"},description:"The cohort's numbers."},value:{type:"number",description:"The value to rank."}},required:["values","value"]}}]},B=R;export{R as codeStatsSkill,E as commitFacts,B as default,j as extKind,T as fileExtension,A as globHit,O as globToRegExp,L as percentileRank,N as sizeBucket};
19
+ within the team (reproducible + comparable across teams), not a raw number.`,resolve(){let s=w();return s?{type:"stdio",command:"node",args:[s,"../dist/codeStats.js","codeStatsSkill"],env:{},description:this.description}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(s,e){try{switch(s){case"code_stats_commit_facts":return JSON.stringify(E(e?.files,{additions:e?.additions,deletions:e?.deletions}));case"code_stats_glob_hit":return JSON.stringify(A(e?.paths,e?.globs));case"code_stats_percentile":return JSON.stringify({percentile:R(e?.values,e?.value)});default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"code_stats_commit_facts",description:"Deterministic per-commit facts from its changed-file list. Pass files: [{path, additions, deletions}] (gitlab_get_commit / github_get_commit shape). Returns filePaths, extCounts, kindCounts (code/test/docs/config/style/data/other), additions, deletions, sizeBucket (trivial/small/medium/large/huge), largestFile, dirs. Pure \u2014 same input always same output.",input_schema:{type:"object",properties:{files:{type:"array",description:"Changed files. Each { path (or newPath/filename), additions?, deletions? }.",items:{type:"object",properties:{path:{type:"string"},additions:{type:"number"},deletions:{type:"number"}}}},additions:{type:"number",description:"Optional commit-level total additions override (else summed from files)."},deletions:{type:"number",description:"Optional commit-level total deletions override."}},required:["files"]}},{name:"code_stats_glob_hit",description:"How many of paths match ANY of globs (supports **, *, ?). Returns { total, hits, ratio, matched }. Use for reproducible core-path coverage against the caller's own core globs.",input_schema:{type:"object",properties:{paths:{type:"array",items:{type:"string"},description:"File paths to test."},globs:{type:"array",items:{type:"string"},description:'Glob patterns, e.g. ["src/payment/**", "auth/**"].'}},required:["paths","globs"]}},{name:"code_stats_percentile",description:'The 0-100 percentile rank of value within values (cumulative "% at or below"). Reproducible + cross-team comparable. Empty cohort \u2192 null.',input_schema:{type:"object",properties:{values:{type:"array",items:{type:"number"},description:"The cohort's numbers."},value:{type:"number",description:"The value to rank."}},required:["values","value"]}}]},B=F;export{F as codeStatsSkill,E as commitFacts,B as default,j as extKind,T as fileExtension,A as globHit,O as globToRegExp,R as percentileRank,N as sizeBucket};
@@ -10,5 +10,5 @@ relationships, or "where does X live / what depends on Y":
10
10
  - get_code_snippet / search_code: pull the exact code for a node / text match.
11
11
  - index_status / list_projects: confirm the index is present before querying.
12
12
  The repo is indexed for you at the start of the run; if a query comes back
13
- empty, call index_status, and only re-index (index_repository) if needed.`,resolve(){let e=a();try{c(e,{recursive:!0})}catch{}let r={CBM_CACHE_DIR:e};return process.env.WORKSPACE&&(r.WORKSPACE=process.env.WORKSPACE),{type:"stdio",command:i(),args:[],env:r,description:this.description,alwaysLoad:!0}},invokeAgentOptions(){try{let e=l();if(!e)return{};let r=a();try{c(r,{recursive:!0})}catch{}let t=n(r,`.cbm-indexed-${_(e)}`);if(s(t))return{};let o=i();if(!s(o)&&!process.env.CBM_BIN)return{};let p=d(o,["cli","index_repository",JSON.stringify({repo_path:e})],{env:{...process.env,CBM_CACHE_DIR:r},encoding:"utf-8",timeout:300*1e3,maxBuffer:32*1024*1024});try{m(t,`${new Date().toISOString()} status=${p.status}
13
+ empty, call index_status, and only re-index (index_repository) if needed.`,resolve(){let e=a();try{c(e,{recursive:!0})}catch{}let r={CBM_CACHE_DIR:e};return process.env.WORKSPACE&&(r.WORKSPACE=process.env.WORKSPACE),{type:"stdio",command:i(),args:[],env:r,description:this.description}},invokeAgentOptions(){try{let e=l();if(!e)return{};let r=a();try{c(r,{recursive:!0})}catch{}let t=n(r,`.cbm-indexed-${_(e)}`);if(s(t))return{};let o=i();if(!s(o)&&!process.env.CBM_BIN)return{};let p=d(o,["cli","index_repository",JSON.stringify({repo_path:e})],{env:{...process.env,CBM_CACHE_DIR:r},encoding:"utf-8",timeout:300*1e3,maxBuffer:32*1024*1024});try{m(t,`${new Date().toISOString()} status=${p.status}
14
14
  `)}catch{}}catch{}return{}}};export{x as codebaseMemorySkill};
package/dist/discord.js CHANGED
@@ -2,4 +2,4 @@ import{existsSync as T}from"fs";import{fileURLToPath as S}from"url";import{dirna
2
2
  `),a=o>t/2?o+1:t;i.push(r.slice(0,a)),r=r.slice(a)}return r&&i.push(r),i}async function h(){let e=(process.env.DISCORD_BOT_TOKEN||"").trim();if(e)return{token:e,guildId:(process.env.DISCORD_GUILD_ID||"").trim()};let t=await D(l.DISCORD),n=(t&&t.token?String(t.token):"").trim();if(!n)throw new Error("Discord is not connected: connect Discord in the Zibby dashboard or set DISCORD_BOT_TOKEN.");return{token:n,guildId:(t.guildId||"").trim()}}async function p(e,t,{token:n,body:i}={}){let r=n.startsWith("Bot ")?n:`Bot ${n}`,s=await _(`${E}${t}`,{method:e,headers:{Authorization:r,...i!==void 0?{"Content-Type":"application/json"}:{}},...i!==void 0?{body:JSON.stringify(i)}:{}},{kind:"api",what:`Discord ${e} ${t}`}),o=await s.json().catch(()=>null);if(!s.ok){let a=o&&(o.message||o.error)?o.message||o.error:`HTTP ${s.status}`;throw new Error(`Discord API error (${s.status}): ${a}`)}return o}async function A({token:e,guildId:t},n){let i=String(n||"").trim()||t||(process.env.DISCORD_GUILD_ID||"").trim();if(i)return i;let r=await p("GET","/users/@me/guilds",{token:e}),s=Array.isArray(r)?r:[];if(s.length===1)return s[0].id;if(s.length===0)throw new Error("The bot is not in any Discord server \u2014 invite it to a server first.");let o=s.slice(0,10).map(a=>`${a.name} (${a.id})`).join(", ");throw new Error(`The bot is in ${s.length} servers \u2014 pass guildId explicitly. Servers: ${o}`)}var B={id:"discord",callsBackend:!0,serverName:"discord",allowedTools:["mcp__discord__*"],requiresIntegration:l.DISCORD,envKeys:["DISCORD_BOT_TOKEN","DISCORD_GUILD_ID"],description:"Discord bot tools (send messages, list channels)",promptFragment:`## Discord
3
3
  You can post to the user's Discord server as their bot. Tools:
4
4
  - discord_send_message(channelId, text) \u2014 post a message to a channel (long text is auto-chunked to Discord's 2000-char limit)
5
- - discord_list_channels(guildId?) \u2014 list the server's text channels (id + name) to find where to post; guildId is optional when the bot is in one server`,resolve(){let e={};for(let n of this.envKeys)process.env[n]&&(e[n]=process.env[n]);for(let n of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","ZIBBY_SELF_HOST","DISCORD_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[n]&&(e[n]=process.env[n]);let t=v();return t?{type:"stdio",command:"node",args:[t,"../dist/discord.js","discordSkill"],env:e,description:this.description,alwaysLoad:!0}:{command:null,args:[],env:e,description:this.description}},async handleToolCall(e,t={}){try{switch(e){case"discord_send_message":{let n=String(t.channelId||"").trim(),i=String(t.text||"");if(!n||!i.trim())return JSON.stringify({ok:!1,error:"channelId and text are required"});let r=await h(),s=y(i),o=[];for(let a of s){let c=await p("POST",`/channels/${encodeURIComponent(n)}/messages`,{token:r.token,body:{content:a}});o.push(c&&c.id?c.id:"")}return JSON.stringify({ok:!0,channelId:n,messageIds:o,chunks:s.length})}case"discord_list_channels":{let n=await h(),i=await A(n,t.guildId),r=await p("GET",`/guilds/${encodeURIComponent(i)}/channels`,{token:n.token}),s=(Array.isArray(r)?r:[]).filter(o=>o&&(o.type===0||o.type===5)).map(o=>({id:o.id,name:o.name,type:o.type===5?"announcement":"text",...o.topic?{topic:o.topic}:{}}));return JSON.stringify({ok:!0,guildId:i,channels:s})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${e}`})}}catch(n){return JSON.stringify({ok:!1,error:n.message})}},tools:[{name:"discord_send_message",description:"Post a message to a Discord channel as the connected bot. Text over 2000 chars is automatically split into sequential messages.",input_schema:{type:"object",properties:{channelId:{type:"string",description:"Discord channel id (snowflake). Use discord_list_channels to find it."},text:{type:"string",description:"Message text (Discord markdown supported)"}},required:["channelId","text"]}},{name:"discord_list_channels",description:"List the text channels of the connected Discord server (id + name). Pass guildId only when the bot is in multiple servers.",input_schema:{type:"object",properties:{guildId:{type:"string",description:"Discord server (guild) id \u2014 optional when the bot is in exactly one server or one was captured at connect time"}}}}]};export{L as DISCORD_MAX_CONTENT,y as chunkDiscordContent,B as discordSkill};
5
+ - discord_list_channels(guildId?) \u2014 list the server's text channels (id + name) to find where to post; guildId is optional when the bot is in one server`,resolve(){let e={};for(let n of this.envKeys)process.env[n]&&(e[n]=process.env[n]);for(let n of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","ZIBBY_SELF_HOST","DISCORD_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[n]&&(e[n]=process.env[n]);let t=v();return t?{type:"stdio",command:"node",args:[t,"../dist/discord.js","discordSkill"],env:e,description:this.description}:{command:null,args:[],env:e,description:this.description}},async handleToolCall(e,t={}){try{switch(e){case"discord_send_message":{let n=String(t.channelId||"").trim(),i=String(t.text||"");if(!n||!i.trim())return JSON.stringify({ok:!1,error:"channelId and text are required"});let r=await h(),s=y(i),o=[];for(let a of s){let c=await p("POST",`/channels/${encodeURIComponent(n)}/messages`,{token:r.token,body:{content:a}});o.push(c&&c.id?c.id:"")}return JSON.stringify({ok:!0,channelId:n,messageIds:o,chunks:s.length})}case"discord_list_channels":{let n=await h(),i=await A(n,t.guildId),r=await p("GET",`/guilds/${encodeURIComponent(i)}/channels`,{token:n.token}),s=(Array.isArray(r)?r:[]).filter(o=>o&&(o.type===0||o.type===5)).map(o=>({id:o.id,name:o.name,type:o.type===5?"announcement":"text",...o.topic?{topic:o.topic}:{}}));return JSON.stringify({ok:!0,guildId:i,channels:s})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${e}`})}}catch(n){return JSON.stringify({ok:!1,error:n.message})}},tools:[{name:"discord_send_message",description:"Post a message to a Discord channel as the connected bot. Text over 2000 chars is automatically split into sequential messages.",input_schema:{type:"object",properties:{channelId:{type:"string",description:"Discord channel id (snowflake). Use discord_list_channels to find it."},text:{type:"string",description:"Message text (Discord markdown supported)"}},required:["channelId","text"]}},{name:"discord_list_channels",description:"List the text channels of the connected Discord server (id + name). Pass guildId only when the bot is in multiple servers.",input_schema:{type:"object",properties:{guildId:{type:"string",description:"Discord server (guild) id \u2014 optional when the bot is in exactly one server or one was captured at connect time"}}}}]};export{L as DISCORD_MAX_CONTENT,y as chunkDiscordContent,B as discordSkill};
package/dist/figma.js CHANGED
@@ -29,4 +29,4 @@ You have access to the user's Figma files via the Figma REST API. Every tool tha
29
29
 
30
30
  ### Notes
31
31
  - The file key is NOT the file name \u2014 it's the opaque id segment in the URL (or just paste the URL).
32
- - Node ids look like "1:23" and come from figma_get_file / figma_get_nodes / figma_parse_url output.`,resolve(){let e=w();if(!e)return{command:null,args:[],env:{},description:this.description};let t={};for(let i of this.envKeys)process.env[i]&&(t[i]=process.env[i]);return{type:"stdio",command:"node",args:[e,"../dist/figma.js","figmaSkill"],env:t,description:this.description,alwaysLoad:!0}},async handleToolCall(e,t){try{switch(e){case"figma_get_me":{let i=await f("/v1/me");return JSON.stringify({id:i.id,handle:i.handle,email:i.email,imgUrl:i.img_url})}case"figma_get_file":{let i=g((t||{}).fileKey),{depth:n}=t||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let s=`/v1/files/${encodeURIComponent(i)}`;if(n!=null){let o=Number(n);!Number.isNaN(o)&&o>0&&(s+=`?depth=${o}`)}let r=await f(s),a=(r.document?.children||[]).map(o=>({id:o.id,name:o.name,type:o.type,childCount:Array.isArray(o.children)?o.children.length:0,children:(o.children||[]).slice(0,50).map(c=>({id:c.id,name:c.name,type:c.type}))}));return JSON.stringify({name:r.name,lastModified:r.lastModified,version:r.version,editorType:r.editorType,role:r.role,pages:a})}case"figma_get_nodes":{let i=g((t||{}).fileKey),{ids:n,depth:s}=t||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let r=_(n);if(!r)return JSON.stringify({error:"ids is required (comma-separated or array of node ids; `1-23` or `1:23`)"});let a=new URLSearchParams;if(a.set("ids",r),s!=null){let d=Number(s);!Number.isNaN(d)&&d>0&&a.set("depth",String(d))}let o=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${a.toString()}`),c={};for(let[d,m]of Object.entries(o.nodes||{}))c[d]=m?.document?{id:m.document.id,name:m.document.name,type:m.document.type,document:m.document}:m;return JSON.stringify({name:o.name,nodes:c})}case"figma_get_node_specs":{let i=g((t||{}).file),{ids:n,depth:s}=t||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let r=_(n);if(!r)return JSON.stringify({error:"ids is required (comma-separated or array of node ids; `1-23` or `1:23`)"});let a=Number(s);Number.isNaN(a)&&(a=2),a=Math.min(6,Math.max(0,Math.trunc(a)));let o=new URLSearchParams;o.set("ids",r),o.set("depth",String(a));let c=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${o.toString()}`),d={};for(let[m,l]of Object.entries(c.nodes||{}))d[m]=l&&l.document?R(l.document,a):null;return JSON.stringify(d)}case"figma_render_png":{let i=g((t||{}).fileKey??(t||{}).file),{ids:n,scale:s,format:r,svg_include_id:a,version:o}=t||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let c=_(n);if(!c)return JSON.stringify({error:"ids is required (comma-separated or array of node ids; `1-23` or `1:23`)"});let d=["png","jpg","svg","pdf"].includes(String(r||"").toLowerCase())?String(r).toLowerCase():"png",m=new URLSearchParams;m.set("ids",c),m.set("format",d);let l;(d==="png"||d==="jpg")&&(l=Number(s),(Number.isNaN(l)||l<=0)&&(l=1),l=Math.min(4,Math.max(.01,l)),m.set("scale",String(l))),d==="svg"&&a!=null&&m.set("svg_include_id",String(!!a)),o&&m.set("version",String(o));let y=await f(`/v1/images/${encodeURIComponent(i)}?${m.toString()}`);return y.err?JSON.stringify({error:`Figma render error: ${y.err}`}):JSON.stringify({format:d,...l!=null?{scale:l}:{},images:y.images||{}})}case"figma_get_image_fills":{let i=g((t||{}).file);if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let n=await f(`/v1/files/${encodeURIComponent(i)}/images`);return JSON.stringify({images:n.meta?.images??n.images??{}})}case"figma_get_file_versions":{let i=g((t||{}).file);if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let s=((await f(`/v1/files/${encodeURIComponent(i)}/versions`)).versions||[]).map(r=>({id:r.id,label:r.label,description:r.description,createdAt:r.created_at,user:r.user?.handle}));return JSON.stringify({count:s.length,versions:s})}case"figma_get_comments":{let i=g((t||{}).fileKey);if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let s=((await f(`/v1/files/${encodeURIComponent(i)}/comments`)).comments||[]).map(r=>({id:r.id,message:r.message,user:r.user?.handle,createdAt:r.created_at,resolvedAt:r.resolved_at||null,parentId:r.parent_id||null}));return JSON.stringify({count:s.length,comments:s})}case"figma_post_comment":{let i=g((t||{}).file),{message:n,comment_id:s,node_id:r,x:a,y:o}=t||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});if(!n)return JSON.stringify({error:"message is required"});let c={message:n};s&&(c.comment_id=s),r?c.client_meta={node_id:_(r),node_offset:{x:a??0,y:o??0}}:(a!=null||o!=null)&&(c.client_meta={x:a??0,y:o??0});let d=await f(`/v1/files/${encodeURIComponent(i)}/comments`,{method:"POST",body:c});return JSON.stringify({id:d.id,message:d.message,fileKey:i,createdAt:d.created_at,parentId:d.parent_id||null})}case"figma_get_team_projects":{let{team_id:i}=t||{};if(!i)return JSON.stringify({error:"team_id is required"});let n=await f(`/v1/teams/${encodeURIComponent(i)}/projects`);return JSON.stringify({name:n.name,projects:n.projects||[]})}case"figma_get_project_files":{let{project_id:i,branch_data:n}=t||{};if(!i)return JSON.stringify({error:"project_id is required"});let s=new URLSearchParams;n&&s.set("branch_data","true");let r=s.toString(),a=await f(`/v1/projects/${encodeURIComponent(i)}/files${r?`?${r}`:""}`);return JSON.stringify({name:a.name,files:a.files||[]})}case"figma_parse_url":{let{url:i}=t||{};return JSON.stringify({file_key:g(i),node_id:M(i)??null})}default:return JSON.stringify({error:`Unknown tool: ${e}`})}}catch(i){return JSON.stringify({error:i.message})}},tools:[{name:"figma_get_me",description:"Get the authenticated Figma user profile (handle, email, id)",input_schema:{type:"object",properties:{}}},{name:"figma_get_file",description:"Get a Figma file's document tree. Accepts a file key OR a figma.com URL (figma.com/file|design/<key>/... \u2014 NOT the file name). Returns a summarized map of pages and their top-level frames/nodes. Use figma_get_nodes or figma_get_node_specs to drill into a specific node.",input_schema:{type:"object",properties:{fileKey:{type:"string",description:'A file key OR a figma.com URL (e.g. "aBcD1234" or figma.com/design/aBcD1234/My-File)'},depth:{type:"number",description:"Optional: limit how deep the node tree is traversed (1-2 is usually enough to list pages/frames). Omit for the full tree."}},required:["fileKey"]}},{name:"figma_get_nodes",description:"Get specific nodes from a Figma file by their node ids. Use after figma_get_file to inspect a particular frame/component without re-fetching the whole file.",input_schema:{type:"object",properties:{fileKey:{type:"string",description:"A file key OR a figma.com URL"},ids:{type:"array",items:{type:"string"},description:'Node ids to fetch. Accepts URL form ["1-23"] or API form ["1:23"]. A comma-separated string is also accepted.'},depth:{type:"number",description:"Optional: limit traversal depth within each node."}},required:["fileKey","ids"]}},{name:"figma_get_node_specs",description:"Dev-Mode-style inspection computed from the REST tree: for the given nodes and their children returns size, position, colors (hex), fonts/text-style, spacing/auto-layout, corner radius, strokes and effects \u2014 a compact spec for reproducing a design in code. Prefer this over figma_get_nodes when your job is to build UI from a Figma file.",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"},ids:{type:"array",items:{type:"string"},description:'Node ids to inspect (URL form "1-23" or API form "1:23"). A comma-separated string is also accepted.'},depth:{type:"number",description:"How many levels of children to walk (0-6, default 2)."}},required:["file","ids"]}},{name:"figma_render_png",description:"Render one or more Figma nodes to an image and return the URLs (a map of nodeId \u2192 URL). Despite the name it renders PNG/JPG/SVG/PDF (default png). Use to show or download a visual of a frame/component.",input_schema:{type:"object",properties:{fileKey:{type:"string",description:"A file key OR a figma.com URL"},ids:{type:"array",items:{type:"string"},description:'Node ids to render (URL form "1-23" or API form "1:23"). A comma-separated string is also accepted.'},format:{type:"string",enum:["png","jpg","svg","pdf"],description:"Output format (default png)."},scale:{type:"number",description:"Render scale, 0.01\u20134 (raster formats only; default 1). 2 for retina/hi-dpi."},svg_include_id:{type:"boolean",description:"For SVG: include element ids as attributes."},version:{type:"string",description:"Render a specific file version id (from figma_get_file_versions)."}},required:["fileKey","ids"]}},{name:"figma_get_image_fills",description:"Get a map of imageRef \u2192 download URL for every image fill used in a Figma file.",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"}},required:["file"]}},{name:"figma_get_file_versions",description:"List a Figma file's saved version history (version id, label, created_at, user). A version id can be passed to figma_render_png to render a past version.",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"}},required:["file"]}},{name:"figma_get_comments",description:"Read the comments on a Figma file",input_schema:{type:"object",properties:{fileKey:{type:"string",description:"A file key OR a figma.com URL"}},required:["fileKey"]}},{name:"figma_post_comment",description:"Post a comment on a Figma file (WRITE \u2014 needs a PAT with comment-write scope). Optionally reply to a comment (comment_id), pin it to a node (node_id, with optional x/y offset), or pin to a canvas point (x/y).",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"},message:{type:"string",description:"The comment text."},comment_id:{type:"string",description:"Optional: reply to this parent comment id."},node_id:{type:"string",description:'Optional: pin the comment to this node ("1-23" or "1:23").'},x:{type:"number",description:"Optional: x position (node offset if node_id given, else canvas point)."},y:{type:"number",description:"Optional: y position (node offset if node_id given, else canvas point)."}},required:["file","message"]}},{name:"figma_get_team_projects",description:"List the projects in a Figma team. Get team_id from a Figma team URL (\u2026/files/team/<TEAM_ID>/\u2026).",input_schema:{type:"object",properties:{team_id:{type:"string",description:"The Figma team id."}},required:["team_id"]}},{name:"figma_get_project_files",description:"List the files in a Figma project (from figma_get_team_projects). Returns file key, name, thumbnail, last_modified.",input_schema:{type:"object",properties:{project_id:{type:"string",description:"The Figma project id."},branch_data:{type:"boolean",description:"Optional: include branch metadata for each file."}},required:["project_id"]}},{name:"figma_parse_url",description:"Local helper (no API call): extract the file key and node id (colon form) from a figma.com file/design URL. Returns { file_key, node_id }.",input_schema:{type:"object",properties:{url:{type:"string",description:"A full figma.com file/design URL."}},required:["url"]}}]};export{q as figmaSkill,g as fileKeyFrom,M as nodeIdFromUrl,R as nodeSpec,_ as normalizeNodeIds};
32
+ - Node ids look like "1:23" and come from figma_get_file / figma_get_nodes / figma_parse_url output.`,resolve(){let e=w();if(!e)return{command:null,args:[],env:{},description:this.description};let t={};for(let i of this.envKeys)process.env[i]&&(t[i]=process.env[i]);return{type:"stdio",command:"node",args:[e,"../dist/figma.js","figmaSkill"],env:t,description:this.description}},async handleToolCall(e,t){try{switch(e){case"figma_get_me":{let i=await f("/v1/me");return JSON.stringify({id:i.id,handle:i.handle,email:i.email,imgUrl:i.img_url})}case"figma_get_file":{let i=g((t||{}).fileKey),{depth:n}=t||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let s=`/v1/files/${encodeURIComponent(i)}`;if(n!=null){let o=Number(n);!Number.isNaN(o)&&o>0&&(s+=`?depth=${o}`)}let r=await f(s),a=(r.document?.children||[]).map(o=>({id:o.id,name:o.name,type:o.type,childCount:Array.isArray(o.children)?o.children.length:0,children:(o.children||[]).slice(0,50).map(c=>({id:c.id,name:c.name,type:c.type}))}));return JSON.stringify({name:r.name,lastModified:r.lastModified,version:r.version,editorType:r.editorType,role:r.role,pages:a})}case"figma_get_nodes":{let i=g((t||{}).fileKey),{ids:n,depth:s}=t||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let r=_(n);if(!r)return JSON.stringify({error:"ids is required (comma-separated or array of node ids; `1-23` or `1:23`)"});let a=new URLSearchParams;if(a.set("ids",r),s!=null){let d=Number(s);!Number.isNaN(d)&&d>0&&a.set("depth",String(d))}let o=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${a.toString()}`),c={};for(let[d,m]of Object.entries(o.nodes||{}))c[d]=m?.document?{id:m.document.id,name:m.document.name,type:m.document.type,document:m.document}:m;return JSON.stringify({name:o.name,nodes:c})}case"figma_get_node_specs":{let i=g((t||{}).file),{ids:n,depth:s}=t||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let r=_(n);if(!r)return JSON.stringify({error:"ids is required (comma-separated or array of node ids; `1-23` or `1:23`)"});let a=Number(s);Number.isNaN(a)&&(a=2),a=Math.min(6,Math.max(0,Math.trunc(a)));let o=new URLSearchParams;o.set("ids",r),o.set("depth",String(a));let c=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${o.toString()}`),d={};for(let[m,l]of Object.entries(c.nodes||{}))d[m]=l&&l.document?R(l.document,a):null;return JSON.stringify(d)}case"figma_render_png":{let i=g((t||{}).fileKey??(t||{}).file),{ids:n,scale:s,format:r,svg_include_id:a,version:o}=t||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let c=_(n);if(!c)return JSON.stringify({error:"ids is required (comma-separated or array of node ids; `1-23` or `1:23`)"});let d=["png","jpg","svg","pdf"].includes(String(r||"").toLowerCase())?String(r).toLowerCase():"png",m=new URLSearchParams;m.set("ids",c),m.set("format",d);let l;(d==="png"||d==="jpg")&&(l=Number(s),(Number.isNaN(l)||l<=0)&&(l=1),l=Math.min(4,Math.max(.01,l)),m.set("scale",String(l))),d==="svg"&&a!=null&&m.set("svg_include_id",String(!!a)),o&&m.set("version",String(o));let y=await f(`/v1/images/${encodeURIComponent(i)}?${m.toString()}`);return y.err?JSON.stringify({error:`Figma render error: ${y.err}`}):JSON.stringify({format:d,...l!=null?{scale:l}:{},images:y.images||{}})}case"figma_get_image_fills":{let i=g((t||{}).file);if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let n=await f(`/v1/files/${encodeURIComponent(i)}/images`);return JSON.stringify({images:n.meta?.images??n.images??{}})}case"figma_get_file_versions":{let i=g((t||{}).file);if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let s=((await f(`/v1/files/${encodeURIComponent(i)}/versions`)).versions||[]).map(r=>({id:r.id,label:r.label,description:r.description,createdAt:r.created_at,user:r.user?.handle}));return JSON.stringify({count:s.length,versions:s})}case"figma_get_comments":{let i=g((t||{}).fileKey);if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let s=((await f(`/v1/files/${encodeURIComponent(i)}/comments`)).comments||[]).map(r=>({id:r.id,message:r.message,user:r.user?.handle,createdAt:r.created_at,resolvedAt:r.resolved_at||null,parentId:r.parent_id||null}));return JSON.stringify({count:s.length,comments:s})}case"figma_post_comment":{let i=g((t||{}).file),{message:n,comment_id:s,node_id:r,x:a,y:o}=t||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});if(!n)return JSON.stringify({error:"message is required"});let c={message:n};s&&(c.comment_id=s),r?c.client_meta={node_id:_(r),node_offset:{x:a??0,y:o??0}}:(a!=null||o!=null)&&(c.client_meta={x:a??0,y:o??0});let d=await f(`/v1/files/${encodeURIComponent(i)}/comments`,{method:"POST",body:c});return JSON.stringify({id:d.id,message:d.message,fileKey:i,createdAt:d.created_at,parentId:d.parent_id||null})}case"figma_get_team_projects":{let{team_id:i}=t||{};if(!i)return JSON.stringify({error:"team_id is required"});let n=await f(`/v1/teams/${encodeURIComponent(i)}/projects`);return JSON.stringify({name:n.name,projects:n.projects||[]})}case"figma_get_project_files":{let{project_id:i,branch_data:n}=t||{};if(!i)return JSON.stringify({error:"project_id is required"});let s=new URLSearchParams;n&&s.set("branch_data","true");let r=s.toString(),a=await f(`/v1/projects/${encodeURIComponent(i)}/files${r?`?${r}`:""}`);return JSON.stringify({name:a.name,files:a.files||[]})}case"figma_parse_url":{let{url:i}=t||{};return JSON.stringify({file_key:g(i),node_id:M(i)??null})}default:return JSON.stringify({error:`Unknown tool: ${e}`})}}catch(i){return JSON.stringify({error:i.message})}},tools:[{name:"figma_get_me",description:"Get the authenticated Figma user profile (handle, email, id)",input_schema:{type:"object",properties:{}}},{name:"figma_get_file",description:"Get a Figma file's document tree. Accepts a file key OR a figma.com URL (figma.com/file|design/<key>/... \u2014 NOT the file name). Returns a summarized map of pages and their top-level frames/nodes. Use figma_get_nodes or figma_get_node_specs to drill into a specific node.",input_schema:{type:"object",properties:{fileKey:{type:"string",description:'A file key OR a figma.com URL (e.g. "aBcD1234" or figma.com/design/aBcD1234/My-File)'},depth:{type:"number",description:"Optional: limit how deep the node tree is traversed (1-2 is usually enough to list pages/frames). Omit for the full tree."}},required:["fileKey"]}},{name:"figma_get_nodes",description:"Get specific nodes from a Figma file by their node ids. Use after figma_get_file to inspect a particular frame/component without re-fetching the whole file.",input_schema:{type:"object",properties:{fileKey:{type:"string",description:"A file key OR a figma.com URL"},ids:{type:"array",items:{type:"string"},description:'Node ids to fetch. Accepts URL form ["1-23"] or API form ["1:23"]. A comma-separated string is also accepted.'},depth:{type:"number",description:"Optional: limit traversal depth within each node."}},required:["fileKey","ids"]}},{name:"figma_get_node_specs",description:"Dev-Mode-style inspection computed from the REST tree: for the given nodes and their children returns size, position, colors (hex), fonts/text-style, spacing/auto-layout, corner radius, strokes and effects \u2014 a compact spec for reproducing a design in code. Prefer this over figma_get_nodes when your job is to build UI from a Figma file.",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"},ids:{type:"array",items:{type:"string"},description:'Node ids to inspect (URL form "1-23" or API form "1:23"). A comma-separated string is also accepted.'},depth:{type:"number",description:"How many levels of children to walk (0-6, default 2)."}},required:["file","ids"]}},{name:"figma_render_png",description:"Render one or more Figma nodes to an image and return the URLs (a map of nodeId \u2192 URL). Despite the name it renders PNG/JPG/SVG/PDF (default png). Use to show or download a visual of a frame/component.",input_schema:{type:"object",properties:{fileKey:{type:"string",description:"A file key OR a figma.com URL"},ids:{type:"array",items:{type:"string"},description:'Node ids to render (URL form "1-23" or API form "1:23"). A comma-separated string is also accepted.'},format:{type:"string",enum:["png","jpg","svg","pdf"],description:"Output format (default png)."},scale:{type:"number",description:"Render scale, 0.01\u20134 (raster formats only; default 1). 2 for retina/hi-dpi."},svg_include_id:{type:"boolean",description:"For SVG: include element ids as attributes."},version:{type:"string",description:"Render a specific file version id (from figma_get_file_versions)."}},required:["fileKey","ids"]}},{name:"figma_get_image_fills",description:"Get a map of imageRef \u2192 download URL for every image fill used in a Figma file.",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"}},required:["file"]}},{name:"figma_get_file_versions",description:"List a Figma file's saved version history (version id, label, created_at, user). A version id can be passed to figma_render_png to render a past version.",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"}},required:["file"]}},{name:"figma_get_comments",description:"Read the comments on a Figma file",input_schema:{type:"object",properties:{fileKey:{type:"string",description:"A file key OR a figma.com URL"}},required:["fileKey"]}},{name:"figma_post_comment",description:"Post a comment on a Figma file (WRITE \u2014 needs a PAT with comment-write scope). Optionally reply to a comment (comment_id), pin it to a node (node_id, with optional x/y offset), or pin to a canvas point (x/y).",input_schema:{type:"object",properties:{file:{type:"string",description:"A file key OR a figma.com URL"},message:{type:"string",description:"The comment text."},comment_id:{type:"string",description:"Optional: reply to this parent comment id."},node_id:{type:"string",description:'Optional: pin the comment to this node ("1-23" or "1:23").'},x:{type:"number",description:"Optional: x position (node offset if node_id given, else canvas point)."},y:{type:"number",description:"Optional: y position (node offset if node_id given, else canvas point)."}},required:["file","message"]}},{name:"figma_get_team_projects",description:"List the projects in a Figma team. Get team_id from a Figma team URL (\u2026/files/team/<TEAM_ID>/\u2026).",input_schema:{type:"object",properties:{team_id:{type:"string",description:"The Figma team id."}},required:["team_id"]}},{name:"figma_get_project_files",description:"List the files in a Figma project (from figma_get_team_projects). Returns file key, name, thumbnail, last_modified.",input_schema:{type:"object",properties:{project_id:{type:"string",description:"The Figma project id."},branch_data:{type:"boolean",description:"Optional: include branch metadata for each file."}},required:["project_id"]}},{name:"figma_parse_url",description:"Local helper (no API call): extract the file key and node id (colon form) from a figma.com file/design URL. Returns { file_key, node_id }.",input_schema:{type:"object",properties:{url:{type:"string",description:"A full figma.com file/design URL."}},required:["url"]}}]};export{q as figmaSkill,g as fileKeyFrom,M as nodeIdFromUrl,R as nodeSpec,_ as normalizeNodeIds};