@zibby/skills 0.2.7 → 0.2.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
@@ -1,4 +1,4 @@
1
- import{SKILL_META as b}from"@zibby/skill-ids";import{existsSync as y,readFileSync as k}from"node:fs";import{homedir as O}from"node:os";import{join as g,dirname as _,resolve as v}from"node:path";import{fileURLToPath as S}from"node:url";function T(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let i=_(S(import.meta.url)),t=v(i,"..","bin","mcp-skill.mjs");return y(t)?t:null}function l(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let i=g(O(),".zibby","config.json");return y(i)&&JSON.parse(k(i,"utf-8")).sessionToken||null}catch{return null}}function p(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function N(){return(typeof process.env.WORKFLOW_TYPE=="string"?process.env.WORKFLOW_TYPE.trim():"")||"agent"}function h(i){return`${N()}:artifact:${i}`}async function u(i){let t=l();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). Artifacts are only available inside a Zibby run.");let e=await fetch(`${p()}/credits/artifacts`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify(i)});if(!e.ok){let r=await e.text().catch(()=>"");throw new Error(`artifact write failed (${e.status}): ${r.slice(0,300)}`)}return e.json()}async function P(i){let t=l();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). Artifacts are only available inside a Zibby run.");let e=await fetch(`${p()}/credits/artifacts/${encodeURIComponent(i)}`,{method:"GET",headers:{Authorization:`Bearer ${t}`}});if(!e.ok){let r=await e.text().catch(()=>"");throw new Error(`artifact get failed (${e.status}): ${r.slice(0,300)}`)}return e.json()}async function f(i,t){let e=l();if(!e)return;let r=await fetch(`${p()}/credits/review-memory`,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"},body:JSON.stringify({op:"store",scope:h(i),content:JSON.stringify(t)})});if(!r.ok){let n=await r.text().catch(()=>"");throw new Error(`artifact index write failed (${r.status}): ${n.slice(0,200)}`)}}async function R(i){let t=l();if(!t)return null;let e=await fetch(`${p()}/credits/review-memory`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify({op:"recall",scope:h(i)})});if(!e.ok)return null;let r=await e.json().catch(()=>null);if(!r?.found||!r?.memory?.content)return null;try{return JSON.parse(r.memory.content)}catch{return null}}function m(i){return typeof i?.html=="string"&&i.html.length>0?{format:"html",content:i.html}:typeof i?.markdown=="string"&&i.markdown.length>0?{format:"markdown",content:i.markdown}:null}var x={id:"artifact",meta:b.artifact,serverName:"artifact",allowedTools:["mcp__artifact__*"],description:"Artifacts \u2014 publish a self-contained, shareable HTML/Markdown page to Zibby and get back a URL; the index of what you published is your memory.",promptFragment:`## Artifacts (publish a shareable page, remember what you made)
1
+ import{SKILL_META as b}from"@zibby/skill-ids";import{existsSync as y,readFileSync as k}from"node:fs";import{homedir as O}from"node:os";import{join as g,dirname as _,resolve as v}from"node:path";import{fileURLToPath as S}from"node:url";function T(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let i=_(S(import.meta.url)),t=v(i,"..","bin","mcp-skill.mjs");return y(t)?t:null}function l(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let i=g(O(),".zibby","config.json");return y(i)&&JSON.parse(k(i,"utf-8")).sessionToken||null}catch{return null}}function p(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function N(){return(typeof process.env.WORKFLOW_TYPE=="string"?process.env.WORKFLOW_TYPE.trim():"")||"agent"}function h(i){return`${N()}:artifact:${i}`}async function u(i){let t=l();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). Artifacts are only available inside a Zibby run.");let e=await fetch(`${p()}/credits/artifacts`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify(i)});if(!e.ok){let r=await e.text().catch(()=>"");throw new Error(`artifact write failed (${e.status}): ${r.slice(0,300)}`)}return e.json()}async function P(i){let t=l();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). Artifacts are only available inside a Zibby run.");let e=await fetch(`${p()}/credits/artifacts/${encodeURIComponent(i)}`,{method:"GET",headers:{Authorization:`Bearer ${t}`}});if(!e.ok){let r=await e.text().catch(()=>"");throw new Error(`artifact get failed (${e.status}): ${r.slice(0,300)}`)}return e.json()}async function f(i,t){let e=l();if(!e)return;let r=await fetch(`${p()}/credits/review-memory`,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"},body:JSON.stringify({op:"store",scope:h(i),content:JSON.stringify(t)})});if(!r.ok){let n=await r.text().catch(()=>"");throw new Error(`artifact index write failed (${r.status}): ${n.slice(0,200)}`)}}async function R(i){let t=l();if(!t)return null;let e=await fetch(`${p()}/credits/review-memory`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify({op:"recall",scope:h(i)})});if(!e.ok)return null;let r=await e.json().catch(()=>null);if(!r?.found||!r?.memory?.content)return null;try{return JSON.parse(r.memory.content)}catch{return null}}function m(i){return typeof i?.html=="string"&&i.html.length>0?{format:"html",content:i.html}:typeof i?.markdown=="string"&&i.markdown.length>0?{format:"markdown",content:i.markdown}:null}var x={id:"artifact",callsBackend:!0,meta:b.artifact,serverName:"artifact",allowedTools:["mcp__artifact__*"],description:"Artifacts \u2014 publish a self-contained, shareable HTML/Markdown page to Zibby and get back a URL; the index of what you published is your memory.",promptFragment:`## Artifacts (publish a shareable page, remember what you made)
2
2
  You can PUBLISH a standalone page \u2014 a status report, a plan, a comparison table,
3
3
  a dashboard-y summary, a diagram, a "here's what I found" write-up \u2014 and get back
4
4
  a shareable URL. The page is a self-contained HTML (or Markdown) document; it is
@@ -0,0 +1,38 @@
1
+ /**
2
+ * backendSession — the ONE place the "backend-calling skill" contract lives.
3
+ *
4
+ * A skill whose CHILD-executed code calls Zibby's own backend
5
+ * (resolveIntegrationToken / getAccountApiUrl — the cloud auth path, and the
6
+ * self-host per-turn-JWT path) declares it ONCE with `callsBackend: true` on
7
+ * the skill object. This module then GUARANTEES the spawned MCP child's env
8
+ * carries the backend-session keys — authors no longer hand-maintain the same
9
+ * allowlist in both envKeys and resolve() (the two-places drift that bit
10
+ * github/gitlab 2026-07 and lark/lark-docs 2026-08; see
11
+ * src/__tests__/backend-session-env-contract.test.ts).
12
+ *
13
+ * The marker is ALSO consumed downstream: the self-host Copilot's turn-local
14
+ * delivery (backend/copilot-runtime chat-ops-bot/lib/turn-local-skills.js)
15
+ * reflects `callsBackend` off the registry to decide which children get the
16
+ * per-turn short-lived JWT instead of a stored PAT.
17
+ *
18
+ * Wiring: src/index.ts applies withBackendSessionEnv() to every skill at
19
+ * registration, so the REGISTERED skill (what the engine resolves) always
20
+ * honors the marker. Existing skills keep their explicit resolve() allowlists
21
+ * — this wrapper only ADDS missing session keys (a key the resolve() already
22
+ * set always wins), so behavior for the fixed skills is byte-identical.
23
+ */
24
+ /** The session keys a backend-calling MCP child needs (the contract-test set). */
25
+ export declare const BACKEND_SESSION_KEYS: readonly ["PROJECT_API_TOKEN", "ZIBBY_ACCOUNT_API_URL", "ZIBBY_ENV"];
26
+ /**
27
+ * Enforce the callsBackend contract on a skill object (idempotent):
28
+ * - envKeys gains any missing session key (the engine's command-style
29
+ * fallback copies ONLY envKeys — tool-resolver.ts);
30
+ * - resolve() is wrapped so a spawned child's env (a spec with `command`)
31
+ * gains any session key present in process.env that the original resolve()
32
+ * omitted. Keys the resolve() DID set always win — no override, no drop.
33
+ * Skills without callsBackend (or without resolve/command) pass through
34
+ * untouched. Mutates in place when possible; a frozen/sealed skill is wrapped
35
+ * as a shallow CLONE instead — always consume the RETURN value (index.ts
36
+ * registers the return, so the registry copy is the enforced one either way).
37
+ */
38
+ export declare function withBackendSessionEnv<T extends Record<string, any>>(skill: T): T;
@@ -0,0 +1 @@
1
+ var f=Object.freeze(["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"]),r=new WeakMap;function i(n){if(!n||n.callsBackend!==!0)return n;if(r.has(n))return r.get(n);let e=Object.isExtensible(n)&&!Object.isFrozen(n)?n:{...n};if(r.set(n,e),r.set(e,e),e.command||Array.isArray(e.envKeys)){let t=Array.isArray(e.envKeys)?[...e.envKeys]:[];for(let c of f)t.includes(c)||t.push(c);e.envKeys=t}if(typeof e.resolve=="function"){let t=e.resolve;e.resolve=function(...v){let s=t.apply(this,v);if(!s||!s.command)return s;let a={...s.env||{}};for(let o of f)a[o]===void 0&&process.env[o]&&(a[o]=process.env[o]);return{...s,env:a}}}return e}export{f as BACKEND_SESSION_KEYS,i as withBackendSessionEnv};
@@ -1,10 +1,10 @@
1
- import{existsSync as b}from"fs";import{fileURLToPath as S}from"url";import{dirname as v,resolve as N}from"path";import{resolveIntegrationToken as O}from"@zibby/core/backend-client.js";var y=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",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",connectPath:"/integrations?provider=lark"},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"},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"}});function I(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let r=v(S(import.meta.url)),e=N(r,"..","bin","mcp-slack.mjs");return b(e)?e:null}async function d(r,e={}){let{token:t}=await O("slack"),s=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(r),n=`https://slack.com/api/${r}`,a={Authorization:`Bearer ${t}`},u;if(s){let l=new URLSearchParams(e).toString();l&&(n+=`?${l}`)}else a["Content-Type"]="application/json; charset=utf-8",u=JSON.stringify(e);let i=await(await fetch(n,{method:s?"GET":"POST",headers:a,body:u})).json();if(!i.ok)throw new Error(`Slack API error: ${i.error}`);return i}var _={id:"slack",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 b}from"fs";import{fileURLToPath as S}from"url";import{dirname as v,resolve as N}from"path";import{resolveIntegrationToken as O}from"@zibby/core/backend-client.js";var y=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",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",connectPath:"/integrations?provider=lark"},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"},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"}});function I(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let r=v(S(import.meta.url)),e=N(r,"..","bin","mcp-slack.mjs");return b(e)?e:null}async function d(r,e={}){let{token:t}=await O("slack"),s=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(r),n=`https://slack.com/api/${r}`,a={Authorization:`Bearer ${t}`},u;if(s){let l=new URLSearchParams(e).toString();l&&(n+=`?${l}`)}else a["Content-Type"]="application/json; charset=utf-8",u=JSON.stringify(e);let i=await(await fetch(n,{method:s?"GET":"POST",headers:a,body:u})).json();if(!i.ok)throw new Error(`Slack API error: ${i.error}`);return i}var _={id:"slack",callsBackend:!0,serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:y.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack
2
2
  You have access to the user's Slack workspace. Use these tools:
3
3
  - slack_list_channels, slack_post_message, slack_reply_to_thread
4
4
  - slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
5
5
  - slack_get_users, slack_get_user_profile
6
6
  - slack_lookup_user_by_email (precise email\u2192user_id, prefer this over scanning slack_get_users)
7
- - slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let r=I();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{let t=await d("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(s=>({id:s.id,name:s.name,topic:s.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await d("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await d("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await d("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await d("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await d("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_users":{let t=await d("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(s=>!s.is_bot&&!s.deleted).map(s=>({id:s.id,name:s.real_name||s.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await d("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await d("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await d("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(s=>({id:s.id,handle:s.handle,name:s.name,description:s.description||"",user_count:Number(s.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await d("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let s=Math.max(1,Math.min(Number(e.limit)||5,25)),n=[],a,u=5;for(let i=0;i<u;i+=1){let l={limit:200};a&&(l.cursor=a);let c=await d("users.list",l);for(let o of c.members||[])o.deleted||o.is_bot||n.push(o);if(a=c.response_metadata?.next_cursor,!a)break}let m=[];for(let i of n){let l=(i.real_name||"").toLowerCase(),c=(i.profile?.display_name||"").toLowerCase(),o=(i.name||"").toLowerCase(),p=0;l.includes(t)&&(p+=100-Math.abs(l.length-t.length)),c.includes(t)&&(p+=60-Math.abs(c.length-t.length)),o.includes(t)&&(p+=30-Math.abs(o.length-t.length)),(l===t||c===t)&&(p+=200),p>0&&m.push({id:i.id,name:i.real_name||i.profile?.display_name||i.name,email:i.profile?.email||void 0,_score:p})}return m.sort((i,l)=>l._score-i._score),JSON.stringify({ok:!0,matches:m.slice(0,s).map(({_score:i,...l})=>l),scanned:n.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as T}from"fs";import{fileURLToPath as w}from"url";import{dirname as A,resolve as E}from"path";import{resolveIntegrationToken as L}from"@zibby/core/backend-client.js";function C(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let r=A(w(import.meta.url)),e=E(r,"..","bin","mcp-lark.mjs");return T(e)?e:null}var P=6e3*1e3,f=null;async function R(){let{appId:r,appSecret:e,host:t}=await L("lark");if(f&&f.appId===r&&f.expiresAt>Date.now())return{token:f.token,host:t};let n=await(await fetch(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return f={token:n.tenant_access_token,expiresAt:Date.now()+P,appId:r},{token:n.tenant_access_token,host:t}}async function g(r,e,t={}){let{token:s,host:n}=await R(),a=`${n}${e}`,u={method:r,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&(u.body=JSON.stringify(t));let i=await(await fetch(a,u)).json();if(i.code!==0)throw new Error(`Lark API ${e} error: ${i.msg||i.code}`);return i.data||{}}function k(r){return JSON.stringify({text:r})}function x(r){return!r||typeof r!="string"||r.startsWith("oc_")?"chat_id":r.startsWith("ou_")?"open_id":r.startsWith("on_")?"union_id":r.startsWith("cli_")?"app_id":r.includes("@")?"email":"chat_id"}var h={id:"lark",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=I();if(!r)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[r],env:e,alwaysLoad:!0}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{let t=await d("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(s=>({id:s.id,name:s.name,topic:s.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await d("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await d("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await d("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await d("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await d("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_users":{let t=await d("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(s=>!s.is_bot&&!s.deleted).map(s=>({id:s.id,name:s.real_name||s.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await d("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await d("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await d("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(s=>({id:s.id,handle:s.handle,name:s.name,description:s.description||"",user_count:Number(s.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await d("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let s=Math.max(1,Math.min(Number(e.limit)||5,25)),n=[],a,u=5;for(let i=0;i<u;i+=1){let l={limit:200};a&&(l.cursor=a);let c=await d("users.list",l);for(let o of c.members||[])o.deleted||o.is_bot||n.push(o);if(a=c.response_metadata?.next_cursor,!a)break}let m=[];for(let i of n){let l=(i.real_name||"").toLowerCase(),c=(i.profile?.display_name||"").toLowerCase(),o=(i.name||"").toLowerCase(),p=0;l.includes(t)&&(p+=100-Math.abs(l.length-t.length)),c.includes(t)&&(p+=60-Math.abs(c.length-t.length)),o.includes(t)&&(p+=30-Math.abs(o.length-t.length)),(l===t||c===t)&&(p+=200),p>0&&m.push({id:i.id,name:i.real_name||i.profile?.display_name||i.name,email:i.profile?.email||void 0,_score:p})}return m.sort((i,l)=>l._score-i._score),JSON.stringify({ok:!0,matches:m.slice(0,s).map(({_score:i,...l})=>l),scanned:n.length})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as T}from"fs";import{fileURLToPath as w}from"url";import{dirname as A,resolve as E}from"path";import{resolveIntegrationToken as L}from"@zibby/core/backend-client.js";function C(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let r=A(w(import.meta.url)),e=E(r,"..","bin","mcp-lark.mjs");return T(e)?e:null}var P=6e3*1e3,f=null;async function R(){let{appId:r,appSecret:e,host:t}=await L("lark");if(f&&f.appId===r&&f.expiresAt>Date.now())return{token:f.token,host:t};let n=await(await fetch(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:r,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return f={token:n.tenant_access_token,expiresAt:Date.now()+P,appId:r},{token:n.tenant_access_token,host:t}}async function g(r,e,t={}){let{token:s,host:n}=await R(),a=`${n}${e}`,u={method:r,headers:{Authorization:`Bearer ${s}`,"Content-Type":"application/json; charset=utf-8"}};r!=="GET"&&(u.body=JSON.stringify(t));let i=await(await fetch(a,u)).json();if(i.code!==0)throw new Error(`Lark API ${e} error: ${i.msg||i.code}`);return i.data||{}}function k(r){return JSON.stringify({text:r})}function x(r){return!r||typeof r!="string"||r.startsWith("oc_")?"chat_id":r.startsWith("ou_")?"open_id":r.startsWith("on_")?"union_id":r.startsWith("cli_")?"app_id":r.includes("@")?"email":"chat_id"}var h={id:"lark",callsBackend:!0,serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration: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)
@@ -1,10 +1,10 @@
1
- import{existsSync as x}from"fs";import{fileURLToPath as J}from"url";import{dirname as q,resolve as D}from"path";import{existsSync as S}from"fs";import{fileURLToPath as b}from"url";import{dirname as N,resolve as I}from"path";import{resolveIntegrationToken as O}from"@zibby/core/backend-client.js";var g=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),G=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",connectPath:"/integrations?provider=lark"},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"},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"}});function v(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let s=N(b(import.meta.url)),e=I(s,"..","bin","mcp-slack.mjs");return S(e)?e:null}async function _(s,e={}){let{token:t}=await O("slack"),r=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(s),n=`https://slack.com/api/${s}`,a={Authorization:`Bearer ${t}`},m;if(r){let l=new URLSearchParams(e).toString();l&&(n+=`?${l}`)}else a["Content-Type"]="application/json; charset=utf-8",m=JSON.stringify(e);let i=await(await fetch(n,{method:r?"GET":"POST",headers:a,body:m})).json();if(!i.ok)throw new Error(`Slack API error: ${i.error}`);return i}var f={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:g.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack
1
+ import{existsSync as x}from"fs";import{fileURLToPath as J}from"url";import{dirname as q,resolve as D}from"path";import{existsSync as S}from"fs";import{fileURLToPath as b}from"url";import{dirname as N,resolve as I}from"path";import{resolveIntegrationToken as O}from"@zibby/core/backend-client.js";var g=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),G=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",connectPath:"/integrations?provider=lark"},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"},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"}});function v(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let s=N(b(import.meta.url)),e=I(s,"..","bin","mcp-slack.mjs");return S(e)?e:null}async function _(s,e={}){let{token:t}=await O("slack"),r=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(s),n=`https://slack.com/api/${s}`,a={Authorization:`Bearer ${t}`},m;if(r){let l=new URLSearchParams(e).toString();l&&(n+=`?${l}`)}else a["Content-Type"]="application/json; charset=utf-8",m=JSON.stringify(e);let i=await(await fetch(n,{method:r?"GET":"POST",headers:a,body:m})).json();if(!i.ok)throw new Error(`Slack API error: ${i.error}`);return i}var f={id:"slack",callsBackend:!0,serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:g.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack
2
2
  You have access to the user's Slack workspace. Use these tools:
3
3
  - slack_list_channels, slack_post_message, slack_reply_to_thread
4
4
  - slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
5
5
  - slack_get_users, slack_get_user_profile
6
6
  - slack_lookup_user_by_email (precise email\u2192user_id, prefer this over scanning slack_get_users)
7
- - slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let s=v();if(!s)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[s],env:e,alwaysLoad:!0}},async handleToolCall(s,e){try{switch(s){case"slack_list_channels":{let t=await _("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(r=>({id:r.id,name:r.name,topic:r.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await _("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await _("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await _("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await _("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await _("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_users":{let t=await _("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(r=>!r.is_bot&&!r.deleted).map(r=>({id:r.id,name:r.real_name||r.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await _("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await _("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await _("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(r=>({id:r.id,handle:r.handle,name:r.name,description:r.description||"",user_count:Number(r.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await _("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let r=Math.max(1,Math.min(Number(e.limit)||5,25)),n=[],a,m=5;for(let i=0;i<m;i+=1){let l={limit:200};a&&(l.cursor=a);let c=await _("users.list",l);for(let o of c.members||[])o.deleted||o.is_bot||n.push(o);if(a=c.response_metadata?.next_cursor,!a)break}let d=[];for(let i of n){let l=(i.real_name||"").toLowerCase(),c=(i.profile?.display_name||"").toLowerCase(),o=(i.name||"").toLowerCase(),p=0;l.includes(t)&&(p+=100-Math.abs(l.length-t.length)),c.includes(t)&&(p+=60-Math.abs(c.length-t.length)),o.includes(t)&&(p+=30-Math.abs(o.length-t.length)),(l===t||c===t)&&(p+=200),p>0&&d.push({id:i.id,name:i.real_name||i.profile?.display_name||i.name,email:i.profile?.email||void 0,_score:p})}return d.sort((i,l)=>l._score-i._score),JSON.stringify({ok:!0,matches:d.slice(0,r).map(({_score:i,...l})=>l),scanned:n.length})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as P}from"fs";import{fileURLToPath as E}from"url";import{dirname as T,resolve as R}from"path";import{resolveIntegrationToken as A}from"@zibby/core/backend-client.js";function w(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let s=T(E(import.meta.url)),e=R(s,"..","bin","mcp-lark.mjs");return P(e)?e:null}var L=6e3*1e3,h=null;async function C(){let{appId:s,appSecret:e,host:t}=await A("lark");if(h&&h.appId===s&&h.expiresAt>Date.now())return{token:h.token,host:t};let n=await(await fetch(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:s,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return h={token:n.tenant_access_token,expiresAt:Date.now()+L,appId:s},{token:n.tenant_access_token,host:t}}async function u(s,e,t={}){let{token:r,host:n}=await C(),a=`${n}${e}`,m={method:s,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json; charset=utf-8"}};s!=="GET"&&(m.body=JSON.stringify(t));let i=await(await fetch(a,m)).json();if(i.code!==0)throw new Error(`Lark API ${e} error: ${i.msg||i.code}`);return i.data||{}}function k(s){return JSON.stringify({text:s})}function B(s){return!s||typeof s!="string"||s.startsWith("oc_")?"chat_id":s.startsWith("ou_")?"open_id":s.startsWith("on_")?"union_id":s.startsWith("cli_")?"app_id":s.includes("@")?"email":"chat_id"}var y={id:"lark",serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:g.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"],promptFragment:`## Lark
7
+ - slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let s=v();if(!s)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);for(let t of this.envKeys)process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[s],env:e,alwaysLoad:!0}},async handleToolCall(s,e){try{switch(s){case"slack_list_channels":{let t=await _("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(t.channels||[]).map(r=>({id:r.id,name:r.name,topic:r.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let t=await _("chat.postMessage",{channel:e.channel,text:e.text,...e.blocks?{blocks:e.blocks}:{}});return JSON.stringify({ok:!0,ts:t.ts,channel:t.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let t=await _("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:t.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await _("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let t=await _("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(t.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let t=await _("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(t.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_users":{let t=await _("users.list",{limit:100});return JSON.stringify({users:(t.members||[]).filter(r=>!r.is_bot&&!r.deleted).map(r=>({id:r.id,name:r.real_name||r.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let t=await _("users.profile.get",{user:e.user_id});return JSON.stringify({profile:t.profile})}case"slack_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});try{let t=await _("users.lookupByEmail",{email:e.email});return JSON.stringify({ok:!0,user:{id:t.user?.id,name:t.user?.real_name||t.user?.name,email:t.user?.profile?.email||e.email}})}catch(t){if(/users_not_found/.test(t.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw t}}case"slack_list_usergroups":{let t=await _("usergroups.list",{});return JSON.stringify({usergroups:(t.usergroups||[]).map(r=>({id:r.id,handle:r.handle,name:r.name,description:r.description||"",user_count:Number(r.user_count||0)}))})}case"slack_get_usergroup_members":{if(!e.usergroup)return JSON.stringify({error:"usergroup id is required"});let t=await _("usergroups.users.list",{usergroup:e.usergroup});return JSON.stringify({users:t.users||[]})}case"slack_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let r=Math.max(1,Math.min(Number(e.limit)||5,25)),n=[],a,m=5;for(let i=0;i<m;i+=1){let l={limit:200};a&&(l.cursor=a);let c=await _("users.list",l);for(let o of c.members||[])o.deleted||o.is_bot||n.push(o);if(a=c.response_metadata?.next_cursor,!a)break}let d=[];for(let i of n){let l=(i.real_name||"").toLowerCase(),c=(i.profile?.display_name||"").toLowerCase(),o=(i.name||"").toLowerCase(),p=0;l.includes(t)&&(p+=100-Math.abs(l.length-t.length)),c.includes(t)&&(p+=60-Math.abs(c.length-t.length)),o.includes(t)&&(p+=30-Math.abs(o.length-t.length)),(l===t||c===t)&&(p+=200),p>0&&d.push({id:i.id,name:i.real_name||i.profile?.display_name||i.name,email:i.profile?.email||void 0,_score:p})}return d.sort((i,l)=>l._score-i._score),JSON.stringify({ok:!0,matches:d.slice(0,r).map(({_score:i,...l})=>l),scanned:n.length})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM. Pass `blocks` (Block Kit) for a rich card; `text` is the required notification fallback.",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Notification/fallback text (required)"},blocks:{type:"array",description:"Block Kit blocks for rich formatting (optional). Each block is a Slack Block Kit object (header/section/divider/context). section blocks may carry a button accessory with a url."}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}},{name:"slack_lookup_user_by_email",description:"Find a Slack user by email. Returns { ok:true, user:{id,name,email} } on hit, { ok:false } when no user has that email. Prefer this over slack_get_users for email-based routing \u2014 single API call, exact match.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"slack_list_usergroups",description:"List workspace-defined user groups (e.g. @oncall, @platform). Each item has { id, handle, name, description, user_count }. Use the id with slack_get_usergroup_members to expand the membership.",input_schema:{type:"object",properties:{}}},{name:"slack_get_usergroup_members",description:"List user IDs that belong to a Slack usergroup. Pair with slack_post_message to DM each member, or use the group id directly in a channel message as <!subteam^ID> to @-mention.",input_schema:{type:"object",properties:{usergroup:{type:"string",description:"Usergroup id, e.g. S012ABC"}},required:["usergroup"]}},{name:"slack_search_users",description:'Fuzzy-search workspace users by display name or real name. Use when the user said something like "send to Sam" without an email. Returns up to `limit` ranked matches { id, name, email }. Slack has no native name-search API \u2014 this scans paginated users.list + does substring scoring (real_name > display_name > name). For large workspaces consider higher limit + ask the user to confirm if multiple hit.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}]};import{existsSync as P}from"fs";import{fileURLToPath as E}from"url";import{dirname as T,resolve as R}from"path";import{resolveIntegrationToken as A}from"@zibby/core/backend-client.js";function w(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let s=T(E(import.meta.url)),e=R(s,"..","bin","mcp-lark.mjs");return P(e)?e:null}var L=6e3*1e3,h=null;async function C(){let{appId:s,appSecret:e,host:t}=await A("lark");if(h&&h.appId===s&&h.expiresAt>Date.now())return{token:h.token,host:t};let n=await(await fetch(`${t}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:s,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return h={token:n.tenant_access_token,expiresAt:Date.now()+L,appId:s},{token:n.tenant_access_token,host:t}}async function u(s,e,t={}){let{token:r,host:n}=await C(),a=`${n}${e}`,m={method:s,headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json; charset=utf-8"}};s!=="GET"&&(m.body=JSON.stringify(t));let i=await(await fetch(a,m)).json();if(i.code!==0)throw new Error(`Lark API ${e} error: ${i.msg||i.code}`);return i.data||{}}function k(s){return JSON.stringify({text:s})}function B(s){return!s||typeof s!="string"||s.startsWith("oc_")?"chat_id":s.startsWith("ou_")?"open_id":s.startsWith("on_")?"union_id":s.startsWith("cli_")?"app_id":s.includes("@")?"email":"chat_id"}var y={id:"lark",callsBackend:!0,serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:g.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"],promptFragment:`## Lark
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)
@@ -1,4 +1,4 @@
1
- import{existsSync as f,readFileSync as b}from"node:fs";import{homedir as T}from"node:os";import{join as S,dirname as O,resolve as w}from"node:path";import{fileURLToPath as _}from"node:url";function I(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let s=O(_(import.meta.url)),t=w(s,"..","bin","mcp-skill.mjs");return f(t)?t:null}function m(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let s=S(T(),".zibby","config.json");return f(s)&&JSON.parse(b(s,"utf-8")).sessionToken||null}catch{return null}}function h(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function y(){return(typeof process.env.WORKFLOW_TYPE=="string"?process.env.WORKFLOW_TYPE.trim():"")||"agent"}function A(){let s={};for(let[t,e]of Object.entries(process.env)){let r=/^ZIBBY_STORE__(.+)$/.exec(t);if(!r)continue;let o=typeof e=="string"?e.trim():"";o&&(s[r[1]]=o)}return s}var u={};function C(){for(let s of Object.keys(u))delete u[s]}function d(s){let t={...A(),...u},e=Object.keys(t),r=typeof s=="string"?s.trim():"";return r?Object.prototype.hasOwnProperty.call(t,r)?{storeId:t[r],name:r}:{error:`unknown store '${r}'; available: ${e.join(", ")}`}:e.length===1?{storeId:t[e[0]],name:e[0]}:e.length===0?{error:"no stores bound to this agent"}:{error:`multiple stores are bound; pass \`store\` (one of: ${e.join(", ")})`}}async function N(s){let t=m();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). Stores are only available inside a Zibby run.");let e=await fetch(`${h()}/datasets/stores/ensure`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.ok){let r=await e.text().catch(()=>"");throw new Error(`ensure_store failed (${e.status}): ${r.slice(0,300)}`)}return e.json()}async function a(s,t,e){let r=m();if(!r)throw new Error("No backend credential (PROJECT_API_TOKEN). Dataset store is only available inside a Zibby run.");let o=`${h()}/datasets/stores/${encodeURIComponent(s)}/${t}`,n=await fetch(o,{method:"POST",headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){let i=await n.text().catch(()=>"");throw new Error(`Store ${t} failed (${n.status}): ${i.slice(0,300)}`)}return n.json()}var L=Math.floor(3.5*1024*1024);async function B(s,t,e,r){let o=await a(s,"put-url",{path:t,contentType:r});if(!o?.url)throw new Error("put-url did not return an upload URL");let n=await fetch(o.url,{method:o.method||"PUT",headers:o.headers||{"Content-Type":r},body:e});if(!n.ok){let i=await n.text().catch(()=>"");throw new Error(`direct upload failed (${n.status}): ${i.slice(0,200)}`)}return{ok:!0,path:o.path||t,size:e.length,contentType:o.contentType||r,via:"presign"}}async function R(s,t){let e=await a(s,"get-url",{path:t});if(!e?.url)throw new Error("get-url did not return a download URL");let r=await fetch(e.url,{method:e.method||"GET"});if(!r.ok){let n=await r.text().catch(()=>"");throw new Error(`direct download failed (${r.status}): ${n.slice(0,200)}`)}let o=await r.arrayBuffer();return{buf:Buffer.from(o),contentType:r.headers.get("content-type")||"application/octet-stream"}}var P={id:"dataset-store",serverName:"dataset_store",allowedTools:["mcp__dataset_store__*"],description:"Dataset store \u2014 a durable, queryable store for structured JSON records; append rows now, run SQL-style aggregations/reports later",promptFragment:`## Dataset Store (durable, queryable structured-record store)
1
+ import{existsSync as f,readFileSync as b}from"node:fs";import{homedir as T}from"node:os";import{join as S,dirname as O,resolve as w}from"node:path";import{fileURLToPath as _}from"node:url";function I(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let s=O(_(import.meta.url)),t=w(s,"..","bin","mcp-skill.mjs");return f(t)?t:null}function m(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let s=S(T(),".zibby","config.json");return f(s)&&JSON.parse(b(s,"utf-8")).sessionToken||null}catch{return null}}function h(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function y(){return(typeof process.env.WORKFLOW_TYPE=="string"?process.env.WORKFLOW_TYPE.trim():"")||"agent"}function A(){let s={};for(let[t,e]of Object.entries(process.env)){let r=/^ZIBBY_STORE__(.+)$/.exec(t);if(!r)continue;let o=typeof e=="string"?e.trim():"";o&&(s[r[1]]=o)}return s}var u={};function C(){for(let s of Object.keys(u))delete u[s]}function d(s){let t={...A(),...u},e=Object.keys(t),r=typeof s=="string"?s.trim():"";return r?Object.prototype.hasOwnProperty.call(t,r)?{storeId:t[r],name:r}:{error:`unknown store '${r}'; available: ${e.join(", ")}`}:e.length===1?{storeId:t[e[0]],name:e[0]}:e.length===0?{error:"no stores bound to this agent"}:{error:`multiple stores are bound; pass \`store\` (one of: ${e.join(", ")})`}}async function N(s){let t=m();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). Stores are only available inside a Zibby run.");let e=await fetch(`${h()}/datasets/stores/ensure`,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify(s)});if(!e.ok){let r=await e.text().catch(()=>"");throw new Error(`ensure_store failed (${e.status}): ${r.slice(0,300)}`)}return e.json()}async function a(s,t,e){let r=m();if(!r)throw new Error("No backend credential (PROJECT_API_TOKEN). Dataset store is only available inside a Zibby run.");let o=`${h()}/datasets/stores/${encodeURIComponent(s)}/${t}`,n=await fetch(o,{method:"POST",headers:{Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){let i=await n.text().catch(()=>"");throw new Error(`Store ${t} failed (${n.status}): ${i.slice(0,300)}`)}return n.json()}var L=Math.floor(3.5*1024*1024);async function B(s,t,e,r){let o=await a(s,"put-url",{path:t,contentType:r});if(!o?.url)throw new Error("put-url did not return an upload URL");let n=await fetch(o.url,{method:o.method||"PUT",headers:o.headers||{"Content-Type":r},body:e});if(!n.ok){let i=await n.text().catch(()=>"");throw new Error(`direct upload failed (${n.status}): ${i.slice(0,200)}`)}return{ok:!0,path:o.path||t,size:e.length,contentType:o.contentType||r,via:"presign"}}async function R(s,t){let e=await a(s,"get-url",{path:t});if(!e?.url)throw new Error("get-url did not return a download URL");let r=await fetch(e.url,{method:e.method||"GET"});if(!r.ok){let n=await r.text().catch(()=>"");throw new Error(`direct download failed (${r.status}): ${n.slice(0,200)}`)}let o=await r.arrayBuffer();return{buf:Buffer.from(o),contentType:r.headers.get("content-type")||"application/octet-stream"}}var P={id:"dataset-store",callsBackend:!0,serverName:"dataset_store",allowedTools:["mcp__dataset_store__*"],description:"Dataset store \u2014 a durable, queryable store for structured JSON records; append rows now, run SQL-style aggregations/reports later",promptFragment:`## Dataset Store (durable, queryable structured-record store)
2
2
  You have one or more durable stores for STRUCTURED records that survive across
3
3
  your stateless runs. Unlike key-value memory (for picking up where you left
4
4
  off), this is for accumulating DATA you want to QUERY and AGGREGATE later \u2014 e.g.
package/dist/discord.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import{existsSync as h}from"fs";import{fileURLToPath as g}from"url";import{dirname as u,resolve as m}from"path";import{resolveIntegrationToken as _}from"@zibby/core/backend-client.js";var d=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),S=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",connectPath:"/integrations?provider=lark"},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"},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"}});function I(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let i=u(g(import.meta.url)),e=m(i,"..","bin","mcp-skill.mjs");return h(e)?e:null}var f=process.env.DISCORD_API_URL||"https://discord.com/api/v10",D=2e3;function v(i,e=D){let n=String(i??"");if(!n.trim())return[];if(n.length<=e)return[n];let s=[],o=n;for(;o.length>e;){let t=o.slice(0,e).lastIndexOf(`
2
- `),a=t>e/2?t+1:e;s.push(o.slice(0,a)),o=o.slice(a)}return o&&s.push(o),s}async function p(){let i=(process.env.DISCORD_BOT_TOKEN||"").trim();if(i)return{token:i,guildId:(process.env.DISCORD_GUILD_ID||"").trim()};let e=await _(d.DISCORD),n=(e&&e.token?String(e.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:(e.guildId||"").trim()}}async function l(i,e,{token:n,body:s}={}){let o=n.startsWith("Bot ")?n:`Bot ${n}`,r=await fetch(`${f}${e}`,{method:i,headers:{Authorization:o,...s!==void 0?{"Content-Type":"application/json"}:{}},...s!==void 0?{body:JSON.stringify(s)}:{}}),t=await r.json().catch(()=>null);if(!r.ok){let a=t&&(t.message||t.error)?t.message||t.error:`HTTP ${r.status}`;throw new Error(`Discord API error (${r.status}): ${a}`)}return t}async function O({token:i,guildId:e},n){let s=String(n||"").trim()||e||(process.env.DISCORD_GUILD_ID||"").trim();if(s)return s;let o=await l("GET","/users/@me/guilds",{token:i}),r=Array.isArray(o)?o:[];if(r.length===1)return r[0].id;if(r.length===0)throw new Error("The bot is not in any Discord server \u2014 invite it to a server first.");let t=r.slice(0,10).map(a=>`${a.name} (${a.id})`).join(", ");throw new Error(`The bot is in ${r.length} servers \u2014 pass guildId explicitly. Servers: ${t}`)}var A={id:"discord",serverName:"discord",allowedTools:["mcp__discord__*"],requiresIntegration:d.DISCORD,envKeys:["DISCORD_BOT_TOKEN","DISCORD_GUILD_ID"],description:"Discord bot tools (send messages, list channels)",promptFragment:`## Discord
2
+ `),a=t>e/2?t+1:e;s.push(o.slice(0,a)),o=o.slice(a)}return o&&s.push(o),s}async function p(){let i=(process.env.DISCORD_BOT_TOKEN||"").trim();if(i)return{token:i,guildId:(process.env.DISCORD_GUILD_ID||"").trim()};let e=await _(d.DISCORD),n=(e&&e.token?String(e.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:(e.guildId||"").trim()}}async function l(i,e,{token:n,body:s}={}){let o=n.startsWith("Bot ")?n:`Bot ${n}`,r=await fetch(`${f}${e}`,{method:i,headers:{Authorization:o,...s!==void 0?{"Content-Type":"application/json"}:{}},...s!==void 0?{body:JSON.stringify(s)}:{}}),t=await r.json().catch(()=>null);if(!r.ok){let a=t&&(t.message||t.error)?t.message||t.error:`HTTP ${r.status}`;throw new Error(`Discord API error (${r.status}): ${a}`)}return t}async function O({token:i,guildId:e},n){let s=String(n||"").trim()||e||(process.env.DISCORD_GUILD_ID||"").trim();if(s)return s;let o=await l("GET","/users/@me/guilds",{token:i}),r=Array.isArray(o)?o:[];if(r.length===1)return r[0].id;if(r.length===0)throw new Error("The bot is not in any Discord server \u2014 invite it to a server first.");let t=r.slice(0,10).map(a=>`${a.name} (${a.id})`).join(", ");throw new Error(`The bot is in ${r.length} servers \u2014 pass guildId explicitly. Servers: ${t}`)}var A={id:"discord",callsBackend:!0,serverName:"discord",allowedTools:["mcp__discord__*"],requiresIntegration:d.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
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 i={};for(let n of this.envKeys)process.env[n]&&(i[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]&&(i[n]=process.env[n]);let e=I();return e?{type:"stdio",command:"node",args:[e,"../dist/discord.js","discordSkill"],env:i,description:this.description,alwaysLoad:!0}:{command:null,args:[],env:i,description:this.description}},async handleToolCall(i,e={}){try{switch(i){case"discord_send_message":{let n=String(e.channelId||"").trim(),s=String(e.text||"");if(!n||!s.trim())return JSON.stringify({ok:!1,error:"channelId and text are required"});let o=await p(),r=v(s),t=[];for(let a of r){let c=await l("POST",`/channels/${encodeURIComponent(n)}/messages`,{token:o.token,body:{content:a}});t.push(c&&c.id?c.id:"")}return JSON.stringify({ok:!0,channelId:n,messageIds:t,chunks:r.length})}case"discord_list_channels":{let n=await p(),s=await O(n,e.guildId),o=await l("GET",`/guilds/${encodeURIComponent(s)}/channels`,{token:n.token}),r=(Array.isArray(o)?o:[]).filter(t=>t&&(t.type===0||t.type===5)).map(t=>({id:t.id,name:t.name,type:t.type===5?"announcement":"text",...t.topic?{topic:t.topic}:{}}));return JSON.stringify({ok:!0,guildId:s,channels:r})}default:return JSON.stringify({ok:!1,error:`Unknown tool: ${i}`})}}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{D as DISCORD_MAX_CONTENT,v as chunkDiscordContent,A as discordSkill};
package/dist/figma.js CHANGED
@@ -1,4 +1,4 @@
1
- import{existsSync as b}from"fs";import{fileURLToPath as S}from"url";import{dirname as N,resolve as k}from"path";import{resolveIntegrationToken as O}from"@zibby/core/backend-client.js";var h=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),A=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",connectPath:"/integrations?provider=lark"},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"},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"}});function L(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let e=N(S(import.meta.url)),n=k(e,"..","bin","mcp-skill.mjs");return b(n)?n:null}async function f(e,n={}){let{token:i}=await O("figma"),r=e.startsWith("https://")?e:`https://api.figma.com${e}`,s={"X-Figma-Token":i,Accept:"application/json",...n.body?{"Content-Type":"application/json"}:{}},o=await fetch(r,{method:n.method||"GET",headers:s,body:n.body?JSON.stringify(n.body):void 0});if(!o.ok){let a=await o.text().catch(()=>"");throw new Error(`Figma API ${o.status}: ${a.slice(0,300)}`)}return o.json()}function g(e){let n=String(e||"").trim(),i=n.match(/figma\.com\/(?:file|design|board|proto)\/([A-Za-z0-9]+)/i);return i?i[1]:n}function u(e){return e==null?void 0:(Array.isArray(e)?e:String(e).split(",")).map(i=>String(i).trim().replace(/-/g,":")).filter(Boolean).join(",")}function I(e){let n=String(e||"").match(/[?&]node-id=([^&]+)/i);return n?decodeURIComponent(n[1]).replace(/-/g,":"):void 0}var p=e=>typeof e=="number"?Math.round(e*100)/100:e;function _(e){if(!e)return null;let n=r=>Math.round((r??0)*255).toString(16).padStart(2,"0"),i={hex:`#${n(e.r)}${n(e.g)}${n(e.b)}`.toUpperCase()};return e.a!=null&&e.a<1&&(i.opacity=p(e.a)),i}function R(e){return!e||e.visible===!1?null:e.type==="SOLID"?{type:"SOLID",..._(e.color),...e.opacity!=null?{opacity:e.opacity}:{}}:{type:e.type,...e.opacity!=null?{opacity:e.opacity}:{},...e.gradientStops?{stops:e.gradientStops.map(n=>({position:p(n.position),..._(n.color)}))}:{}}}function v(e,n){let i=e.absoluteBoundingBox,r={id:e.id,name:e.name,type:e.type};i&&(r.size={w:p(i.width),h:p(i.height)},r.position={x:p(i.x),y:p(i.y)}),e.opacity!=null&&e.opacity!==1&&(r.opacity=e.opacity),e.cornerRadius!=null&&(r.cornerRadius=e.cornerRadius),Array.isArray(e.rectangleCornerRadii)&&(r.cornerRadii=e.rectangleCornerRadii);let s=(e.fills||[]).map(R).filter(Boolean);s.length&&(r.fills=s);let o=(e.strokes||[]).map(R).filter(Boolean);o.length&&(r.strokes=o,e.strokeWeight!=null&&(r.strokeWeight=e.strokeWeight));let a=(e.effects||[]).filter(t=>t.visible!==!1).map(t=>({type:t.type,...t.radius!=null?{radius:t.radius}:{},...t.offset?{offset:{x:p(t.offset.x),y:p(t.offset.y)}}:{},...t.spread?{spread:t.spread}:{},...t.color?_(t.color):{}}));if(a.length&&(r.effects=a),e.layoutMode&&e.layoutMode!=="NONE"&&(r.autoLayout={direction:e.layoutMode,padding:{l:e.paddingLeft??0,r:e.paddingRight??0,t:e.paddingTop??0,b:e.paddingBottom??0},gap:e.itemSpacing??0,align:{primary:e.primaryAxisAlignItems,counter:e.counterAxisAlignItems}}),e.type==="TEXT"){let t=e.style||{};r.text=e.characters,r.textStyle={fontFamily:t.fontFamily,fontWeight:t.fontWeight,fontSize:t.fontSize,lineHeightPx:t.lineHeightPx!=null?p(t.lineHeightPx):void 0,letterSpacing:t.letterSpacing!=null?p(t.letterSpacing):void 0,align:t.textAlignHorizontal,case:t.textCase}}return n>0&&Array.isArray(e.children)&&e.children.length&&(r.children=e.children.map(t=>v(t,n-1))),r}var F={id:"figma",serverName:"figma",allowedTools:["mcp__figma__*"],requiresIntegration:h.FIGMA,envKeys:[],description:"Figma \u2014 read files, nodes and comments; extract dev-handoff specs (sizes/colors/fonts/auto-layout); list versions and image fills; render frames to PNG/JPG/SVG/PDF; browse team projects/files; and post comments",promptFragment:`## Figma
1
+ import{existsSync as b}from"fs";import{fileURLToPath as S}from"url";import{dirname as N,resolve as O}from"path";import{resolveIntegrationToken as k}from"@zibby/core/backend-client.js";var h=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),A=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",connectPath:"/integrations?provider=lark"},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"},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"}});function I(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let e=N(S(import.meta.url)),n=O(e,"..","bin","mcp-skill.mjs");return b(n)?n:null}async function f(e,n={}){let{token:i}=await k("figma"),o=e.startsWith("https://")?e:`https://api.figma.com${e}`,s={"X-Figma-Token":i,Accept:"application/json",...n.body?{"Content-Type":"application/json"}:{}},r=await fetch(o,{method:n.method||"GET",headers:s,body:n.body?JSON.stringify(n.body):void 0});if(!r.ok){let a=await r.text().catch(()=>"");throw new Error(`Figma API ${r.status}: ${a.slice(0,300)}`)}return r.json()}function g(e){let n=String(e||"").trim(),i=n.match(/figma\.com\/(?:file|design|board|proto)\/([A-Za-z0-9]+)/i);return i?i[1]:n}function u(e){return e==null?void 0:(Array.isArray(e)?e:String(e).split(",")).map(i=>String(i).trim().replace(/-/g,":")).filter(Boolean).join(",")}function L(e){let n=String(e||"").match(/[?&]node-id=([^&]+)/i);return n?decodeURIComponent(n[1]).replace(/-/g,":"):void 0}var p=e=>typeof e=="number"?Math.round(e*100)/100:e;function _(e){if(!e)return null;let n=o=>Math.round((o??0)*255).toString(16).padStart(2,"0"),i={hex:`#${n(e.r)}${n(e.g)}${n(e.b)}`.toUpperCase()};return e.a!=null&&e.a<1&&(i.opacity=p(e.a)),i}function R(e){return!e||e.visible===!1?null:e.type==="SOLID"?{type:"SOLID",..._(e.color),...e.opacity!=null?{opacity:e.opacity}:{}}:{type:e.type,...e.opacity!=null?{opacity:e.opacity}:{},...e.gradientStops?{stops:e.gradientStops.map(n=>({position:p(n.position),..._(n.color)}))}:{}}}function v(e,n){let i=e.absoluteBoundingBox,o={id:e.id,name:e.name,type:e.type};i&&(o.size={w:p(i.width),h:p(i.height)},o.position={x:p(i.x),y:p(i.y)}),e.opacity!=null&&e.opacity!==1&&(o.opacity=e.opacity),e.cornerRadius!=null&&(o.cornerRadius=e.cornerRadius),Array.isArray(e.rectangleCornerRadii)&&(o.cornerRadii=e.rectangleCornerRadii);let s=(e.fills||[]).map(R).filter(Boolean);s.length&&(o.fills=s);let r=(e.strokes||[]).map(R).filter(Boolean);r.length&&(o.strokes=r,e.strokeWeight!=null&&(o.strokeWeight=e.strokeWeight));let a=(e.effects||[]).filter(t=>t.visible!==!1).map(t=>({type:t.type,...t.radius!=null?{radius:t.radius}:{},...t.offset?{offset:{x:p(t.offset.x),y:p(t.offset.y)}}:{},...t.spread?{spread:t.spread}:{},...t.color?_(t.color):{}}));if(a.length&&(o.effects=a),e.layoutMode&&e.layoutMode!=="NONE"&&(o.autoLayout={direction:e.layoutMode,padding:{l:e.paddingLeft??0,r:e.paddingRight??0,t:e.paddingTop??0,b:e.paddingBottom??0},gap:e.itemSpacing??0,align:{primary:e.primaryAxisAlignItems,counter:e.counterAxisAlignItems}}),e.type==="TEXT"){let t=e.style||{};o.text=e.characters,o.textStyle={fontFamily:t.fontFamily,fontWeight:t.fontWeight,fontSize:t.fontSize,lineHeightPx:t.lineHeightPx!=null?p(t.lineHeightPx):void 0,letterSpacing:t.letterSpacing!=null?p(t.letterSpacing):void 0,align:t.textAlignHorizontal,case:t.textCase}}return n>0&&Array.isArray(e.children)&&e.children.length&&(o.children=e.children.map(t=>v(t,n-1))),o}var F={id:"figma",callsBackend:!0,serverName:"figma",allowedTools:["mcp__figma__*"],requiresIntegration:h.FIGMA,envKeys:["PROJECT_API_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV"],description:"Figma \u2014 read files, nodes and comments; extract dev-handoff specs (sizes/colors/fonts/auto-layout); list versions and image fills; render frames to PNG/JPG/SVG/PDF; browse team projects/files; and post comments",promptFragment:`## Figma
2
2
  You have access to the user's Figma files via the Figma REST API. Every tool that takes a file accepts EITHER the raw file key OR a full figma.com URL (figma.com/file|design/<key>/...). Node ids accept URL form \`1-23\` or API form \`1:23\` interchangeably. Tools:
3
3
 
4
4
  ### Identity
@@ -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=L();if(!e)return{command:null,args:[],env:{},description:this.description};let n={};for(let i of this.envKeys)process.env[i]&&(n[i]=process.env[i]);return{type:"stdio",command:"node",args:[e,"../dist/figma.js","figmaSkill"],env:n,description:this.description,alwaysLoad:!0}},async handleToolCall(e,n){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((n||{}).fileKey),{depth:r}=n||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let s=`/v1/files/${encodeURIComponent(i)}`;if(r!=null){let t=Number(r);!Number.isNaN(t)&&t>0&&(s+=`?depth=${t}`)}let o=await f(s),a=(o.document?.children||[]).map(t=>({id:t.id,name:t.name,type:t.type,childCount:Array.isArray(t.children)?t.children.length:0,children:(t.children||[]).slice(0,50).map(c=>({id:c.id,name:c.name,type:c.type}))}));return JSON.stringify({name:o.name,lastModified:o.lastModified,version:o.version,editorType:o.editorType,role:o.role,pages:a})}case"figma_get_nodes":{let i=g((n||{}).fileKey),{ids:r,depth:s}=n||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let o=u(r);if(!o)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",o),s!=null){let d=Number(s);!Number.isNaN(d)&&d>0&&a.set("depth",String(d))}let t=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${a.toString()}`),c={};for(let[d,m]of Object.entries(t.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:t.name,nodes:c})}case"figma_get_node_specs":{let i=g((n||{}).file),{ids:r,depth:s}=n||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let o=u(r);if(!o)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 t=new URLSearchParams;t.set("ids",o),t.set("depth",String(a));let c=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${t.toString()}`),d={};for(let[m,l]of Object.entries(c.nodes||{}))d[m]=l&&l.document?v(l.document,a):null;return JSON.stringify(d)}case"figma_render_png":{let i=g((n||{}).fileKey??(n||{}).file),{ids:r,scale:s,format:o,svg_include_id:a,version:t}=n||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let c=u(r);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(o||"").toLowerCase())?String(o).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)),t&&m.set("version",String(t));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((n||{}).file);if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let r=await f(`/v1/files/${encodeURIComponent(i)}/images`);return JSON.stringify({images:r.meta?.images??r.images??{}})}case"figma_get_file_versions":{let i=g((n||{}).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(o=>({id:o.id,label:o.label,description:o.description,createdAt:o.created_at,user:o.user?.handle}));return JSON.stringify({count:s.length,versions:s})}case"figma_get_comments":{let i=g((n||{}).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(o=>({id:o.id,message:o.message,user:o.user?.handle,createdAt:o.created_at,resolvedAt:o.resolved_at||null,parentId:o.parent_id||null}));return JSON.stringify({count:s.length,comments:s})}case"figma_post_comment":{let i=g((n||{}).file),{message:r,comment_id:s,node_id:o,x:a,y:t}=n||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});if(!r)return JSON.stringify({error:"message is required"});let c={message:r};s&&(c.comment_id=s),o?c.client_meta={node_id:u(o),node_offset:{x:a??0,y:t??0}}:(a!=null||t!=null)&&(c.client_meta={x:a??0,y:t??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}=n||{};if(!i)return JSON.stringify({error:"team_id is required"});let r=await f(`/v1/teams/${encodeURIComponent(i)}/projects`);return JSON.stringify({name:r.name,projects:r.projects||[]})}case"figma_get_project_files":{let{project_id:i,branch_data:r}=n||{};if(!i)return JSON.stringify({error:"project_id is required"});let s=new URLSearchParams;r&&s.set("branch_data","true");let o=s.toString(),a=await f(`/v1/projects/${encodeURIComponent(i)}/files${o?`?${o}`:""}`);return JSON.stringify({name:a.name,files:a.files||[]})}case"figma_parse_url":{let{url:i}=n||{};return JSON.stringify({file_key:g(i),node_id:I(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{F as figmaSkill,g as fileKeyFrom,I as nodeIdFromUrl,v as nodeSpec,u 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=I();if(!e)return{command:null,args:[],env:{},description:this.description};let n={};for(let i of this.envKeys)process.env[i]&&(n[i]=process.env[i]);return{type:"stdio",command:"node",args:[e,"../dist/figma.js","figmaSkill"],env:n,description:this.description,alwaysLoad:!0}},async handleToolCall(e,n){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((n||{}).fileKey),{depth:o}=n||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let s=`/v1/files/${encodeURIComponent(i)}`;if(o!=null){let t=Number(o);!Number.isNaN(t)&&t>0&&(s+=`?depth=${t}`)}let r=await f(s),a=(r.document?.children||[]).map(t=>({id:t.id,name:t.name,type:t.type,childCount:Array.isArray(t.children)?t.children.length:0,children:(t.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((n||{}).fileKey),{ids:o,depth:s}=n||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let r=u(o);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 t=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${a.toString()}`),c={};for(let[d,m]of Object.entries(t.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:t.name,nodes:c})}case"figma_get_node_specs":{let i=g((n||{}).file),{ids:o,depth:s}=n||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let r=u(o);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 t=new URLSearchParams;t.set("ids",r),t.set("depth",String(a));let c=await f(`/v1/files/${encodeURIComponent(i)}/nodes?${t.toString()}`),d={};for(let[m,l]of Object.entries(c.nodes||{}))d[m]=l&&l.document?v(l.document,a):null;return JSON.stringify(d)}case"figma_render_png":{let i=g((n||{}).fileKey??(n||{}).file),{ids:o,scale:s,format:r,svg_include_id:a,version:t}=n||{};if(!i)return JSON.stringify({error:"fileKey is required (a file key or a figma.com URL)"});let c=u(o);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)),t&&m.set("version",String(t));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((n||{}).file);if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});let o=await f(`/v1/files/${encodeURIComponent(i)}/images`);return JSON.stringify({images:o.meta?.images??o.images??{}})}case"figma_get_file_versions":{let i=g((n||{}).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((n||{}).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((n||{}).file),{message:o,comment_id:s,node_id:r,x:a,y:t}=n||{};if(!i)return JSON.stringify({error:"file is required (a file key or a figma.com URL)"});if(!o)return JSON.stringify({error:"message is required"});let c={message:o};s&&(c.comment_id=s),r?c.client_meta={node_id:u(r),node_offset:{x:a??0,y:t??0}}:(a!=null||t!=null)&&(c.client_meta={x:a??0,y:t??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}=n||{};if(!i)return JSON.stringify({error:"team_id is required"});let o=await f(`/v1/teams/${encodeURIComponent(i)}/projects`);return JSON.stringify({name:o.name,projects:o.projects||[]})}case"figma_get_project_files":{let{project_id:i,branch_data:o}=n||{};if(!i)return JSON.stringify({error:"project_id is required"});let s=new URLSearchParams;o&&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}=n||{};return JSON.stringify({file_key:g(i),node_id:L(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{F as figmaSkill,g as fileKeyFrom,L as nodeIdFromUrl,v as nodeSpec,u as normalizeNodeIds};
package/dist/gbrain.js CHANGED
@@ -1,4 +1,4 @@
1
- import{existsSync as f,readFileSync as b}from"node:fs";import{homedir as h}from"node:os";import{join as I,dirname as _,resolve as S}from"node:path";import{fileURLToPath as O}from"node:url";import{SKILL_META as E}from"@zibby/skill-ids";var T=3e4,p=1;function w(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let o=_(O(import.meta.url)),r=S(o,"..","bin","mcp-skill.mjs");return f(r)?r:null}function A(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let o=I(h(),".zibby","config.json");return f(o)&&JSON.parse(b(o,"utf-8")).sessionToken||null}catch{return null}}function k(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function v(){let o={};for(let[r,e]of Object.entries(process.env)){let t=/^ZIBBY_STORE__(.+)$/.exec(r);if(!t)continue;let n=typeof e=="string"?e.trim():"";n&&(o[t[1]]=n)}return o}function l(o){let r=v(),e=Object.keys(r),t=typeof o=="string"?o.trim():"";return t?Object.prototype.hasOwnProperty.call(r,t)?{storeId:r[t],name:t}:{error:`unknown store '${t}'; available: ${e.join(", ")}`}:e.length===1?{storeId:r[e[0]],name:e[0]}:e.length===0?{error:"no knowledge-base store is bound to this agent"}:{error:`multiple stores are bound; pass \`store\` (one of: ${e.join(", ")})`}}async function m(o,r,e){let t=A();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). The knowledge base is only available inside a Zibby run.");let n=`${k()}/datasets/stores/${encodeURIComponent(o)}/${r}`,s={Authorization:`Bearer ${t}`,"Content-Type":"application/json"},u=JSON.stringify(e),i;for(let c=0;c<=p;c++){let y=new AbortController,g=setTimeout(()=>y.abort(),T);try{let a=await fetch(n,{method:"POST",headers:s,body:u,signal:y.signal}),d=await a.text().catch(()=>"");if(a.status>=500&&c<p){i=new Error(`store ${a.status}`);continue}if(!a.ok)throw new Error(`gbrain ${r} failed (${a.status}): ${d.slice(0,300)}`);try{return JSON.parse(d)}catch{throw new Error(`store returned non-JSON: ${d.slice(0,200)}`)}}catch(a){if(i=a,!((a?.name==="AbortError"||a?.code==="ECONNREFUSED"||/fetch failed|network/i.test(String(a?.message)))&&c<p))break}finally{clearTimeout(g)}}throw i||new Error(`gbrain ${r} request failed`)}var N={id:"gbrain",serverName:"gbrain",allowedTools:["mcp__gbrain__*"],meta:E.gbrain,description:"Knowledge base (GBrain) \u2014 ingest source documents into, semantically query, and prune a per-tenant Postgres/pgvector brain (a `postgres`-type store, brokered by the control-plane)",promptFragment:`## Knowledge Base (GBrain \u2014 per-tenant document brain)
1
+ import{existsSync as f,readFileSync as b}from"node:fs";import{homedir as h}from"node:os";import{join as I,dirname as _,resolve as S}from"node:path";import{fileURLToPath as O}from"node:url";import{SKILL_META as E}from"@zibby/skill-ids";var T=3e4,l=1;function w(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let o=_(O(import.meta.url)),r=S(o,"..","bin","mcp-skill.mjs");return f(r)?r:null}function A(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let o=I(h(),".zibby","config.json");return f(o)&&JSON.parse(b(o,"utf-8")).sessionToken||null}catch{return null}}function k(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}function v(){let o={};for(let[r,e]of Object.entries(process.env)){let t=/^ZIBBY_STORE__(.+)$/.exec(r);if(!t)continue;let n=typeof e=="string"?e.trim():"";n&&(o[t[1]]=n)}return o}function p(o){let r=v(),e=Object.keys(r),t=typeof o=="string"?o.trim():"";return t?Object.prototype.hasOwnProperty.call(r,t)?{storeId:r[t],name:t}:{error:`unknown store '${t}'; available: ${e.join(", ")}`}:e.length===1?{storeId:r[e[0]],name:e[0]}:e.length===0?{error:"no knowledge-base store is bound to this agent"}:{error:`multiple stores are bound; pass \`store\` (one of: ${e.join(", ")})`}}async function m(o,r,e){let t=A();if(!t)throw new Error("No backend credential (PROJECT_API_TOKEN). The knowledge base is only available inside a Zibby run.");let n=`${k()}/datasets/stores/${encodeURIComponent(o)}/${r}`,s={Authorization:`Bearer ${t}`,"Content-Type":"application/json"},u=JSON.stringify(e),i;for(let c=0;c<=l;c++){let y=new AbortController,g=setTimeout(()=>y.abort(),T);try{let a=await fetch(n,{method:"POST",headers:s,body:u,signal:y.signal}),d=await a.text().catch(()=>"");if(a.status>=500&&c<l){i=new Error(`store ${a.status}`);continue}if(!a.ok)throw new Error(`gbrain ${r} failed (${a.status}): ${d.slice(0,300)}`);try{return JSON.parse(d)}catch{throw new Error(`store returned non-JSON: ${d.slice(0,200)}`)}}catch(a){if(i=a,!((a?.name==="AbortError"||a?.code==="ECONNREFUSED"||/fetch failed|network/i.test(String(a?.message)))&&c<l))break}finally{clearTimeout(g)}}throw i||new Error(`gbrain ${r} request failed`)}var N={id:"gbrain",callsBackend:!0,serverName:"gbrain",allowedTools:["mcp__gbrain__*"],meta:E.gbrain,description:"Knowledge base (GBrain) \u2014 ingest source documents into, semantically query, and prune a per-tenant Postgres/pgvector brain (a `postgres`-type store, brokered by the control-plane)",promptFragment:`## Knowledge Base (GBrain \u2014 per-tenant document brain)
2
2
  You have a per-tenant KNOWLEDGE BASE (a Postgres + pgvector "brain"), bound as a
3
3
  \`postgres\`-type store in the "AVAILABLE STORES" block below. Ingested documents
4
4
  are addressed by a STABLE \`sourceId\` so re-ingesting the same source UPSERTS
@@ -9,4 +9,4 @@ Tools:
9
9
  Pass deleted:true to remove a source that no longer exists upstream.
10
10
  - gbrain_query({ query, topK, store? }): semantic search; returns the topK most
11
11
  relevant document chunks with their sourceId and relevance score.
12
- - gbrain_delete({ sourceIds, store? }): remove documents by their stable sourceId(s).`,resolve(){let o=w();if(!o)return{command:null,args:[],env:{},description:this.description};let r={};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]&&(r[e]=process.env[e]);for(let e of Object.keys(process.env))/^ZIBBY_STORE__.+$/.test(e)&&process.env[e]&&(r[e]=process.env[e]);return{type:"stdio",command:"node",args:[o,"../dist/gbrain.js","gbrainSkill"],env:r,description:this.description,alwaysLoad:!0}},async handleToolCall(o,r){try{switch(o){case"gbrain_ingest":{let e=Array.isArray(r?.docs)?r.docs:null;if(!e)return JSON.stringify({error:"docs is required (array of { sourceId, markdown, deleted? })"});if(e.length===0)return JSON.stringify({ok:!0,upserted:0,deleted:0,chunks:0,note:"empty docs \u2014 nothing to ingest"});if(e.find(i=>!i||typeof i.sourceId!="string"||!i.sourceId.trim())!==void 0)return JSON.stringify({error:"every doc requires a non-empty string sourceId"});let n=e.find(i=>i.deleted!==!0&&(typeof i.markdown!="string"||!i.markdown.length));if(n!==void 0)return JSON.stringify({error:`doc "${n.sourceId}" has no markdown (required unless deleted:true)`});let s=l(r?.store);if(s.error)return JSON.stringify({error:s.error});let u=await m(s.storeId,"ingest",{docs:e});return JSON.stringify({...u,store:s.name,storeId:s.storeId})}case"gbrain_query":{let e=typeof r?.query=="string"?r.query.trim():"";if(!e)return JSON.stringify({error:"query is required"});let t=l(r?.store);if(t.error)return JSON.stringify({error:t.error});let n={query:e};Number.isInteger(r?.topK)&&r.topK>0&&(n.topK=Math.min(r.topK,50));let s=await m(t.storeId,"query",n);return JSON.stringify({...s,store:t.name,storeId:t.storeId})}case"gbrain_delete":{let e=Array.isArray(r?.sourceIds)?r.sourceIds.filter(s=>typeof s=="string"&&s.trim()):null;if(!e||e.length===0)return JSON.stringify({error:"sourceIds is required (non-empty array of strings)"});let t=l(r?.store);if(t.error)return JSON.stringify({error:t.error});let n=await m(t.storeId,"delete",{sourceIds:e});return JSON.stringify({...n,store:t.name,storeId:t.storeId})}default:return JSON.stringify({error:`Unknown tool: ${o}`})}}catch(e){return JSON.stringify({error:String(e?.message||e)})}},tools:[{name:"gbrain_ingest",description:'Upsert documents into the knowledge base. Each doc is { sourceId, markdown, deleted? }; sourceId is a STABLE id (e.g. "owner/repo#docs/x.md" or a Lark doc token) so re-ingesting the same source UPDATES in place instead of duplicating. Set deleted:true to remove a source.',input_schema:{type:"object",properties:{docs:{type:"array",description:"Documents to upsert.",items:{type:"object",properties:{sourceId:{type:"string",description:"Stable source id \u2014 the upsert/delete key."},markdown:{type:"string",description:"The normalized markdown content of the document."},deleted:{type:"boolean",description:"When true, remove this sourceId from the brain (markdown ignored)."}},required:["sourceId"]}},store:{type:"string",description:"The bound knowledge-base store NAME (from AVAILABLE STORES). Omit if exactly one store is bound."}},required:["docs"]}},{name:"gbrain_query",description:"Semantic-search the knowledge base and return the most relevant document chunks (each with its sourceId and relevance score).",input_schema:{type:"object",properties:{query:{type:"string",description:"Natural-language query."},topK:{type:"integer",description:"How many chunks to return (default 8, max 50)."},store:{type:"string",description:"The bound knowledge-base store NAME (from AVAILABLE STORES). Omit if exactly one store is bound."}},required:["query"]}},{name:"gbrain_delete",description:"Remove documents from the knowledge base by their stable sourceId(s).",input_schema:{type:"object",properties:{sourceIds:{type:"array",description:"Stable source ids to delete.",items:{type:"string"}},store:{type:"string",description:"The bound knowledge-base store NAME (from AVAILABLE STORES). Omit if exactly one store is bound."}},required:["sourceIds"]}}]},U=N;export{U as default,N as gbrainSkill};
12
+ - gbrain_delete({ sourceIds, store? }): remove documents by their stable sourceId(s).`,resolve(){let o=w();if(!o)return{command:null,args:[],env:{},description:this.description};let r={};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]&&(r[e]=process.env[e]);for(let e of Object.keys(process.env))/^ZIBBY_STORE__.+$/.test(e)&&process.env[e]&&(r[e]=process.env[e]);return{type:"stdio",command:"node",args:[o,"../dist/gbrain.js","gbrainSkill"],env:r,description:this.description,alwaysLoad:!0}},async handleToolCall(o,r){try{switch(o){case"gbrain_ingest":{let e=Array.isArray(r?.docs)?r.docs:null;if(!e)return JSON.stringify({error:"docs is required (array of { sourceId, markdown, deleted? })"});if(e.length===0)return JSON.stringify({ok:!0,upserted:0,deleted:0,chunks:0,note:"empty docs \u2014 nothing to ingest"});if(e.find(i=>!i||typeof i.sourceId!="string"||!i.sourceId.trim())!==void 0)return JSON.stringify({error:"every doc requires a non-empty string sourceId"});let n=e.find(i=>i.deleted!==!0&&(typeof i.markdown!="string"||!i.markdown.length));if(n!==void 0)return JSON.stringify({error:`doc "${n.sourceId}" has no markdown (required unless deleted:true)`});let s=p(r?.store);if(s.error)return JSON.stringify({error:s.error});let u=await m(s.storeId,"ingest",{docs:e});return JSON.stringify({...u,store:s.name,storeId:s.storeId})}case"gbrain_query":{let e=typeof r?.query=="string"?r.query.trim():"";if(!e)return JSON.stringify({error:"query is required"});let t=p(r?.store);if(t.error)return JSON.stringify({error:t.error});let n={query:e};Number.isInteger(r?.topK)&&r.topK>0&&(n.topK=Math.min(r.topK,50));let s=await m(t.storeId,"query",n);return JSON.stringify({...s,store:t.name,storeId:t.storeId})}case"gbrain_delete":{let e=Array.isArray(r?.sourceIds)?r.sourceIds.filter(s=>typeof s=="string"&&s.trim()):null;if(!e||e.length===0)return JSON.stringify({error:"sourceIds is required (non-empty array of strings)"});let t=p(r?.store);if(t.error)return JSON.stringify({error:t.error});let n=await m(t.storeId,"delete",{sourceIds:e});return JSON.stringify({...n,store:t.name,storeId:t.storeId})}default:return JSON.stringify({error:`Unknown tool: ${o}`})}}catch(e){return JSON.stringify({error:String(e?.message||e)})}},tools:[{name:"gbrain_ingest",description:'Upsert documents into the knowledge base. Each doc is { sourceId, markdown, deleted? }; sourceId is a STABLE id (e.g. "owner/repo#docs/x.md" or a Lark doc token) so re-ingesting the same source UPDATES in place instead of duplicating. Set deleted:true to remove a source.',input_schema:{type:"object",properties:{docs:{type:"array",description:"Documents to upsert.",items:{type:"object",properties:{sourceId:{type:"string",description:"Stable source id \u2014 the upsert/delete key."},markdown:{type:"string",description:"The normalized markdown content of the document."},deleted:{type:"boolean",description:"When true, remove this sourceId from the brain (markdown ignored)."}},required:["sourceId"]}},store:{type:"string",description:"The bound knowledge-base store NAME (from AVAILABLE STORES). Omit if exactly one store is bound."}},required:["docs"]}},{name:"gbrain_query",description:"Semantic-search the knowledge base and return the most relevant document chunks (each with its sourceId and relevance score).",input_schema:{type:"object",properties:{query:{type:"string",description:"Natural-language query."},topK:{type:"integer",description:"How many chunks to return (default 8, max 50)."},store:{type:"string",description:"The bound knowledge-base store NAME (from AVAILABLE STORES). Omit if exactly one store is bound."}},required:["query"]}},{name:"gbrain_delete",description:"Remove documents from the knowledge base by their stable sourceId(s).",input_schema:{type:"object",properties:{sourceIds:{type:"array",description:"Stable source ids to delete.",items:{type:"string"}},store:{type:"string",description:"The bound knowledge-base store NAME (from AVAILABLE STORES). Omit if exactly one store is bound."}},required:["sourceIds"]}}]},U=N;export{U as default,N as gbrainSkill};