@zibby/skills 0.1.28 → 0.1.30

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/lark.js CHANGED
@@ -1,8 +1,8 @@
1
- import{existsSync as y}from"fs";import{fileURLToPath as f}from"url";import{dirname as k,resolve as b}from"path";import{resolveIntegrationToken as v}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"}),I=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"}});function S(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let i=k(f(import.meta.url)),e=b(i,"..","bin","mcp-lark.mjs");return y(e)?e:null}var T=6e3*1e3,p=null;async function O(){let{appId:i,appSecret:e,host:t}=await v("lark");if(p&&p.appId===i&&p.expiresAt>Date.now())return{token:p.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:i,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return p={token:n.tenant_access_token,expiresAt:Date.now()+T,appId:i},{token:n.tenant_access_token,host:t}}async function _(i,e,t={}){let{token:a,host:n}=await O(),r=`${n}${e}`,m={method:i,headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json; charset=utf-8"}};i!=="GET"&&(m.body=JSON.stringify(t));let c=await(await fetch(r,m)).json();if(c.code!==0)throw new Error(`Lark API ${e} error: ${c.msg||c.code}`);return c.data||{}}function g(i){return JSON.stringify({text:i})}function N(i){return!i||typeof i!="string"||i.startsWith("oc_")?"chat_id":i.startsWith("ou_")?"open_id":i.startsWith("on_")?"union_id":i.startsWith("cli_")?"app_id":i.includes("@")?"email":"chat_id"}var E={id:"lark",serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:h.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:[],promptFragment:`## Lark (connected)
1
+ import{existsSync as y}from"fs";import{fileURLToPath as f}from"url";import{dirname as k,resolve as b}from"path";import{resolveIntegrationToken as v}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"}),N=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"}});function T(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let i=k(f(import.meta.url)),e=b(i,"..","bin","mcp-lark.mjs");return y(e)?e:null}var S=6e3*1e3,p=null;async function I(){let{appId:i,appSecret:e,host:t}=await v("lark");if(p&&p.appId===i&&p.expiresAt>Date.now())return{token:p.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:i,app_secret:e})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return p={token:n.tenant_access_token,expiresAt:Date.now()+S,appId:i},{token:n.tenant_access_token,host:t}}async function d(i,e,t={}){let{token:a,host:n}=await I(),r=`${n}${e}`,m={method:i,headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json; charset=utf-8"}};i!=="GET"&&(m.body=JSON.stringify(t));let c=await(await fetch(r,m)).json();if(c.code!==0)throw new Error(`Lark API ${e} error: ${c.msg||c.code}`);return c.data||{}}function g(i){return JSON.stringify({text:i})}function O(i){return!i||typeof i!="string"||i.startsWith("oc_")?"chat_id":i.startsWith("ou_")?"open_id":i.startsWith("on_")?"union_id":i.startsWith("cli_")?"app_id":i.includes("@")?"email":"chat_id"}var C={id:"lark",serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:h.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:[],promptFragment:`## Lark (connected)
2
2
  You can send messages and replies on Lark. Use:
3
3
  - lark_send_message: post a message to a chat, user, or DM
4
4
  - lark_reply: reply to an existing message (threaded)
5
5
  - lark_list_chats: list chats the bot is a member of
6
6
  - lark_get_chat_history: fetch recent messages in a chat
7
7
  - lark_lookup_user_by_email: resolve an email \u2192 open_id for direct DM (prefer this over emailing through lark_send_message when the agent has a user_id already)
8
- When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let i=S();if(!i)return null;let e={};for(let t of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[i],env:e,alwaysLoad:!0}},tools:[{name:"lark_send_message",description:"Send a text message to a Lark chat, user, or DM. receive_id can be a chat_id (oc_*), open_id (ou_*), union_id (on_*), or email.",input_schema:{type:"object",properties:{receive_id:{type:"string",description:"Target id: chat_id (oc_*), open_id (ou_*), union_id (on_*), or email"},text:{type:"string",description:"Message text"}},required:["receive_id","text"]}},{name:"lark_reply",description:"Reply to an existing Lark message (creates a thread). Use the message_id from the inbound event.",input_schema:{type:"object",properties:{message_id:{type:"string",description:"Lark message id (om_*) to reply to"},text:{type:"string",description:"Reply text"}},required:["message_id","text"]}},{name:"lark_list_chats",description:"List chats (groups + DMs) the bot is a member of.",input_schema:{type:"object",properties:{page_size:{type:"number",description:"Max results (default 50)"}}}},{name:"lark_get_chat_history",description:"Fetch recent messages in a chat.",input_schema:{type:"object",properties:{chat_id:{type:"string",description:"Chat id (oc_*)"},page_size:{type:"number",description:"Max messages (default 20)"}},required:["chat_id"]}},{name:"lark_lookup_user_by_email",description:"Resolve an email address to a Lark user id (open_id). Returns { ok:true, user:{open_id,email,name} } on hit, { ok:false } if no Lark user has that email. Use the open_id as `receive_id` in lark_send_message to DM.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"lark_search_users",description:'Fuzzy-search users by name across chats the bot is a member of. Lark has no public org-wide user search API for bots \u2014 this walks the bot\'s chat memberships and matches names client-side. Best for "send to Sam" style routing where you have a name but no email. Returns up to `limit` ranked matches { open_id, name }.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against user names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}],async handleToolCall(i,e){try{switch(i){case"lark_send_message":{if(!e.receive_id||!e.text)return JSON.stringify({error:"receive_id and text are required"});let t=N(e.receive_id),a=await _("POST",`/open-apis/im/v1/messages?receive_id_type=${t}`,{receive_id:e.receive_id,msg_type:"text",content:g(e.text)});return JSON.stringify({ok:!0,message_id:a.message_id})}case"lark_reply":{if(!e.message_id||!e.text)return JSON.stringify({error:"message_id and text are required"});let t=await _("POST",`/open-apis/im/v1/messages/${encodeURIComponent(e.message_id)}/reply`,{msg_type:"text",content:g(e.text)});return JSON.stringify({ok:!0,message_id:t.message_id})}case"lark_list_chats":{let t=e.page_size||50,n=((await _("GET",`/open-apis/im/v1/chats?page_size=${t}`)).items||[]).map(r=>({chat_id:r.chat_id,name:r.name,description:r.description,owner_id:r.owner_id,chat_mode:r.chat_mode}));return JSON.stringify({chats:n})}case"lark_get_chat_history":{if(!e.chat_id)return JSON.stringify({error:"chat_id is required"});let t=e.page_size||20,n=((await _("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(e.chat_id)}&page_size=${t}&sort_type=ByCreateTimeDesc`)).items||[]).map(r=>({message_id:r.message_id,sender_id:r.sender?.id,sender_type:r.sender?.sender_type,msg_type:r.msg_type,content:r.body?.content,create_time:r.create_time}));return JSON.stringify({messages:n})}case"lark_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});let a=((await _("POST","/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id",{emails:[e.email]})).user_list||[]).find(n=>n.email===e.email&&n.user_id);return JSON.stringify(a?{ok:!0,user:{open_id:a.user_id,email:a.email,name:a.name||void 0}}:{ok:!1,reason:"no_lark_user_for_email"})}case"lark_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let a=Math.max(1,Math.min(Number(e.limit)||5,25)),n=200,m=((await _("GET","/open-apis/im/v1/chats?page_size=100")).items||[]).map(o=>o.chat_id),l=new Set,c=[];for(let o of m){if(c.length>=n)break;try{let s=await _("GET",`/open-apis/im/v1/chats/${encodeURIComponent(o)}/members?member_id_type=open_id&page_size=100`);for(let d of s.items||[])if(!(!d.member_id||l.has(d.member_id))&&(l.add(d.member_id),c.push({open_id:d.member_id,name:d.name||""}),c.length>=n))break}catch(s){console.warn(`[lark] member scan failed for ${o}: ${s.message}`)}}let u=[];for(let o of c){let s=(o.name||"").toLowerCase();if(!s)continue;let d=0;s.includes(t)&&(d+=100-Math.abs(s.length-t.length)),s===t&&(d+=200),d>0&&u.push({open_id:o.open_id,name:o.name,_score:d})}return u.sort((o,s)=>s._score-o._score),JSON.stringify({ok:!0,matches:u.slice(0,a).map(({_score:o,...s})=>s),scanned:c.length})}default:return JSON.stringify({error:`Unknown tool: ${i}`})}}catch(t){return JSON.stringify({error:t.message})}}};function C(){p=null}export{C as _resetLarkTokenCache,E as larkSkill};
8
+ When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let i=T();if(!i)return null;let e={};for(let t of["PROJECT_API_TOKEN","ZIBBY_USER_TOKEN","ZIBBY_ACCOUNT_API_URL","ZIBBY_ENV","ZIBBY_PROD_ACCOUNT_API_URL","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(e[t]=process.env[t]);return{type:"stdio",command:"node",args:[i],env:e,alwaysLoad:!0}},tools:[{name:"lark_send_message",description:"Send a text message to a Lark chat, user, or DM. receive_id can be a chat_id (oc_*), open_id (ou_*), union_id (on_*), or email.",input_schema:{type:"object",properties:{receive_id:{type:"string",description:"Target id: chat_id (oc_*), open_id (ou_*), union_id (on_*), or email"},text:{type:"string",description:"Message text"}},required:["receive_id","text"]}},{name:"lark_reply",description:"Reply to an existing Lark message (creates a thread). Use the message_id from the inbound event.",input_schema:{type:"object",properties:{message_id:{type:"string",description:"Lark message id (om_*) to reply to"},text:{type:"string",description:"Reply text"}},required:["message_id","text"]}},{name:"lark_list_chats",description:"List chats (groups + DMs) the bot is a member of.",input_schema:{type:"object",properties:{page_size:{type:"number",description:"Max results (default 50)"}}}},{name:"lark_get_chat_history",description:"Fetch recent messages in a chat.",input_schema:{type:"object",properties:{chat_id:{type:"string",description:"Chat id (oc_*)"},page_size:{type:"number",description:"Max messages (default 20)"}},required:["chat_id"]}},{name:"lark_lookup_user_by_email",description:"Resolve an email address to a Lark user id (open_id). Returns { ok:true, user:{open_id,email,name} } on hit, { ok:false } if no Lark user has that email. Use the open_id as `receive_id` in lark_send_message to DM.",input_schema:{type:"object",properties:{email:{type:"string",description:"Email address to look up"}},required:["email"]}},{name:"lark_search_users",description:'Fuzzy-search users by name across chats the bot is a member of. Lark has no public org-wide user search API for bots \u2014 this walks the bot\'s chat memberships and matches names client-side. Best for "send to Sam" style routing where you have a name but no email. Returns up to `limit` ranked matches { open_id, name }.',input_schema:{type:"object",properties:{query:{type:"string",description:"Substring to match against user names (case-insensitive)"},limit:{type:"number",description:"Max matches to return (default 5, max 25)"}},required:["query"]}}],async handleToolCall(i,e){try{switch(i){case"lark_send_message":{if(!e.receive_id||!e.text)return JSON.stringify({error:"receive_id and text are required"});let t=O(e.receive_id),a=await d("POST",`/open-apis/im/v1/messages?receive_id_type=${t}`,{receive_id:e.receive_id,msg_type:"text",content:g(e.text)});return JSON.stringify({ok:!0,message_id:a.message_id})}case"lark_reply":{if(!e.message_id||!e.text)return JSON.stringify({error:"message_id and text are required"});let t=await d("POST",`/open-apis/im/v1/messages/${encodeURIComponent(e.message_id)}/reply`,{msg_type:"text",content:g(e.text)});return JSON.stringify({ok:!0,message_id:t.message_id})}case"lark_list_chats":{let t=e.page_size||50,n=((await d("GET",`/open-apis/im/v1/chats?page_size=${t}`)).items||[]).map(r=>({chat_id:r.chat_id,name:r.name,description:r.description,owner_id:r.owner_id,chat_mode:r.chat_mode}));return JSON.stringify({chats:n})}case"lark_get_chat_history":{if(!e.chat_id)return JSON.stringify({error:"chat_id is required"});let t=e.page_size||20,n=((await d("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(e.chat_id)}&page_size=${t}&sort_type=ByCreateTimeDesc`)).items||[]).map(r=>({message_id:r.message_id,sender_id:r.sender?.id,sender_type:r.sender?.sender_type,msg_type:r.msg_type,content:r.body?.content,create_time:r.create_time}));return JSON.stringify({messages:n})}case"lark_lookup_user_by_email":{if(!e.email)return JSON.stringify({error:"email is required"});let a=((await d("POST","/open-apis/contact/v3/users/batch_get_id?user_id_type=open_id",{emails:[e.email]})).user_list||[]).find(n=>n.email===e.email&&n.user_id);return JSON.stringify(a?{ok:!0,user:{open_id:a.user_id,email:a.email,name:a.name||void 0}}:{ok:!1,reason:"no_lark_user_for_email"})}case"lark_search_users":{if(!e.query||typeof e.query!="string")return JSON.stringify({error:"query is required"});let t=e.query.trim().toLowerCase();if(!t)return JSON.stringify({ok:!0,matches:[]});let a=Math.max(1,Math.min(Number(e.limit)||5,25)),n=200,m=((await d("GET","/open-apis/im/v1/chats?page_size=100")).items||[]).map(o=>o.chat_id),l=new Set,c=[];for(let o of m){if(c.length>=n)break;try{let s=await d("GET",`/open-apis/im/v1/chats/${encodeURIComponent(o)}/members?member_id_type=open_id&page_size=100`);for(let _ of s.items||[])if(!(!_.member_id||l.has(_.member_id))&&(l.add(_.member_id),c.push({open_id:_.member_id,name:_.name||""}),c.length>=n))break}catch(s){console.warn(`[lark] member scan failed for ${o}: ${s.message}`)}}let u=[];for(let o of c){let s=(o.name||"").toLowerCase();if(!s)continue;let _=0;s.includes(t)&&(_+=100-Math.abs(s.length-t.length)),s===t&&(_+=200),_>0&&u.push({open_id:o.open_id,name:o.name,_score:_})}return u.sort((o,s)=>s._score-o._score),JSON.stringify({ok:!0,matches:u.slice(0,a).map(({_score:o,...s})=>s),scanned:c.length})}default:return JSON.stringify({error:`Unknown tool: ${i}`})}}catch(t){return JSON.stringify({error:t.message})}}};function E(){p=null}export{E as _resetLarkTokenCache,C as larkSkill};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/skills",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Built-in skill definitions for Zibby test automation framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/dist/sentry.js CHANGED
@@ -1,5 +1,5 @@
1
- import{existsSync as u}from"fs";import{fileURLToPath as d}from"url";import{dirname as y,resolve as m}from"path";import{resolveIntegrationToken as c}from"@zibby/core/backend-client.js";var o=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"}),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"}});function _(){if(process.env.MCP_SENTRY_PATH)return process.env.MCP_SENTRY_PATH;let n=y(d(import.meta.url)),r=m(n,"..","bin","mcp-sentry.mjs");return u(r)?r:null}async function p(n,r={}){let{token:t,organizationSlug:e}=await c("sentry"),s=`https://sentry.io/api/0/organizations/${e}${n}`,i=await fetch(s,{method:r.method||"GET",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}});if(!i.ok){let l=await i.text().catch(()=>"");throw new Error(`Sentry API ${i.status}: ${l.slice(0,300)}`)}return i.json()}async function g(){return p("/projects/?per_page=50")}async function h({query:n="is:unresolved",sort:r="date",project:t,limit:e=25}={}){let s=`/issues/?query=${encodeURIComponent(n)}&sort=${r}&per_page=${e}`;return t&&(s+=`&project=${encodeURIComponent(t)}`),p(s)}async function f(n){if(!n)throw new Error("sentryGetIssue: issueId is required");let{token:r}=await c("sentry"),t=await fetch(`https://sentry.io/api/0/issues/${n}/`,{headers:{Authorization:`Bearer ${r}`}});if(!t.ok){let e=await t.text().catch(()=>"");throw new Error(`Sentry API ${t.status}: ${e.slice(0,300)}`)}return t.json()}var a={id:"sentry",serverName:"sentry",allowedTools:["mcp__sentry__*"],requiresIntegration:o.SENTRY,description:"Sentry error tracking \u2014 projects, issues, events",envKeys:[],tools:[],promptFragment:`## Sentry (connected)
1
+ import{existsSync as u}from"fs";import{fileURLToPath as d}from"url";import{dirname as y,resolve as _}from"path";import{resolveIntegrationToken as c}from"@zibby/core/backend-client.js";var o=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"}),I=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"}});function m(){if(process.env.MCP_SENTRY_PATH)return process.env.MCP_SENTRY_PATH;let n=y(d(import.meta.url)),r=_(n,"..","bin","mcp-sentry.mjs");return u(r)?r:null}async function p(n,r={}){let{token:t,organizationSlug:e}=await c("sentry"),s=`https://sentry.io/api/0/organizations/${e}${n}`,i=await fetch(s,{method:r.method||"GET",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}});if(!i.ok){let l=await i.text().catch(()=>"");throw new Error(`Sentry API ${i.status}: ${l.slice(0,300)}`)}return i.json()}async function g(){return p("/projects/?per_page=50")}async function h({query:n="is:unresolved",sort:r="date",project:t,limit:e=25}={}){let s=`/issues/?query=${encodeURIComponent(n)}&sort=${r}&per_page=${e}`;return t&&(s+=`&project=${encodeURIComponent(t)}`),p(s)}async function f(n){if(!n)throw new Error("sentryGetIssue: issueId is required");let{token:r}=await c("sentry"),t=await fetch(`https://sentry.io/api/0/issues/${n}/`,{headers:{Authorization:`Bearer ${r}`}});if(!t.ok){let e=await t.text().catch(()=>"");throw new Error(`Sentry API ${t.status}: ${e.slice(0,300)}`)}return t.json()}var a={id:"sentry",serverName:"sentry",allowedTools:["mcp__sentry__*"],requiresIntegration:o.SENTRY,description:"Sentry error tracking \u2014 projects, issues, events",envKeys:[],tools:[],promptFragment:`## Sentry (connected)
2
2
  You have access to the user's Sentry. Use these tools:
3
3
  - sentry_list_projects: List projects in the organization
4
4
  - sentry_list_issues: List errors/issues (supports Sentry search query, project filter, sort)
5
- - sentry_get_issue: Get detailed info about a specific issue (requires issueId)`,resolve(){let n=_();if(!n)return null;let r={};for(let t of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(r[t]=process.env[t]);return{type:"stdio",command:"node",args:[n],env:r,alwaysLoad:!0}},async handleToolCall(n,r={}){try{switch(n){case"sentry_list_projects":{let t=await g();return JSON.stringify({projects:t.map(e=>({slug:e.slug,name:e.name,platform:e.platform}))})}case"sentry_list_issues":{let t=await h({query:r.query,sort:r.sort,project:r.project,limit:r.limit});return JSON.stringify({issues:t.map(e=>({id:e.id,title:e.title,culprit:e.culprit,count:e.count,firstSeen:e.firstSeen,lastSeen:e.lastSeen,level:e.level,status:e.status}))})}case"sentry_get_issue":{let t=await f(r.issueId);return JSON.stringify({id:t.id,title:t.title,culprit:t.culprit,metadata:t.metadata,count:t.count,userCount:t.userCount,firstSeen:t.firstSeen,lastSeen:t.lastSeen,level:t.level,status:t.status,project:{slug:t.project?.slug,name:t.project?.name}})}default:return JSON.stringify({error:`Unknown tool: ${n}`})}}catch(t){return JSON.stringify({error:t.message})}},toolsForAssistant:[{name:"sentry_list_projects",description:"List Sentry projects",input_schema:{type:"object",properties:{}}},{name:"sentry_list_issues",description:"List Sentry issues (errors)",input_schema:{type:"object",properties:{project:{type:"string",description:"Project slug (optional)"},query:{type:"string",description:"Sentry search query (default: is:unresolved)"},sort:{type:"string",description:"Sort order: date, new, priority, freq, user (default: date)"},limit:{type:"number",description:"Max issues to return (default 25)"}}}},{name:"sentry_get_issue",description:"Get details of a specific Sentry issue",input_schema:{type:"object",properties:{issueId:{type:"string",description:"Sentry issue ID"}},required:["issueId"]}}]};a.tools=a.toolsForAssistant;export{p as sentryFetch,f as sentryGetIssue,h as sentryListIssues,g as sentryListProjects,a as sentrySkill};
5
+ - sentry_get_issue: Get detailed info about a specific issue (requires issueId)`,resolve(){let n=m();if(!n)return null;let r={};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]&&(r[t]=process.env[t]);return{type:"stdio",command:"node",args:[n],env:r,alwaysLoad:!0}},async handleToolCall(n,r={}){try{switch(n){case"sentry_list_projects":{let t=await g();return JSON.stringify({projects:t.map(e=>({slug:e.slug,name:e.name,platform:e.platform}))})}case"sentry_list_issues":{let t=await h({query:r.query,sort:r.sort,project:r.project,limit:r.limit});return JSON.stringify({issues:t.map(e=>({id:e.id,title:e.title,culprit:e.culprit,count:e.count,firstSeen:e.firstSeen,lastSeen:e.lastSeen,level:e.level,status:e.status}))})}case"sentry_get_issue":{let t=await f(r.issueId);return JSON.stringify({id:t.id,title:t.title,culprit:t.culprit,metadata:t.metadata,count:t.count,userCount:t.userCount,firstSeen:t.firstSeen,lastSeen:t.lastSeen,level:t.level,status:t.status,project:{slug:t.project?.slug,name:t.project?.name}})}default:return JSON.stringify({error:`Unknown tool: ${n}`})}}catch(t){return JSON.stringify({error:t.message})}},toolsForAssistant:[{name:"sentry_list_projects",description:"List Sentry projects",input_schema:{type:"object",properties:{}}},{name:"sentry_list_issues",description:"List Sentry issues (errors)",input_schema:{type:"object",properties:{project:{type:"string",description:"Project slug (optional)"},query:{type:"string",description:"Sentry search query (default: is:unresolved)"},sort:{type:"string",description:"Sort order: date, new, priority, freq, user (default: date)"},limit:{type:"number",description:"Max issues to return (default 25)"}}}},{name:"sentry_get_issue",description:"Get details of a specific Sentry issue",input_schema:{type:"object",properties:{issueId:{type:"string",description:"Sentry issue ID"}},required:["issueId"]}}]};a.tools=a.toolsForAssistant;export{p as sentryFetch,f as sentryGetIssue,h as sentryListIssues,g as sentryListProjects,a as sentrySkill};
package/dist/slack.js CHANGED
@@ -1,7 +1,7 @@
1
- import{existsSync as h}from"fs";import{fileURLToPath as g}from"url";import{dirname as f,resolve as y}from"path";import{resolveIntegrationToken as k}from"@zibby/core/backend-client.js";var _=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"}),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"}});function b(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let a=f(g(import.meta.url)),t=y(a,"..","bin","mcp-slack.mjs");return h(t)?t:null}async function n(a,t={}){let{token:e}=await k("slack"),r=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(a),c=`https://slack.com/api/${a}`,o={Authorization:`Bearer ${e}`},m;if(r){let i=new URLSearchParams(t).toString();i&&(c+=`?${i}`)}else o["Content-Type"]="application/json; charset=utf-8",m=JSON.stringify(t);let s=await(await fetch(c,{method:r?"GET":"POST",headers:o,body:m})).json();if(!s.ok)throw new Error(`Slack API error: ${s.error}`);return s}var P={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:_.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack (connected)
1
+ import{existsSync as h}from"fs";import{fileURLToPath as g}from"url";import{dirname as f,resolve as k}from"path";import{resolveIntegrationToken as y}from"@zibby/core/backend-client.js";var _=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"}),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"}});function b(){if(process.env.MCP_SLACK_PATH)return process.env.MCP_SLACK_PATH;let a=f(g(import.meta.url)),t=k(a,"..","bin","mcp-slack.mjs");return h(t)?t:null}async function n(a,t={}){let{token:e}=await y("slack"),r=["conversations.list","users.list","users.profile.get","users.lookupByEmail","usergroups.list","usergroups.users.list","conversations.history","conversations.replies"].includes(a),c=`https://slack.com/api/${a}`,o={Authorization:`Bearer ${e}`},m;if(r){let i=new URLSearchParams(t).toString();i&&(c+=`?${i}`)}else o["Content-Type"]="application/json; charset=utf-8",m=JSON.stringify(t);let s=await(await fetch(c,{method:r?"GET":"POST",headers:o,body:m})).json();if(!s.ok)throw new Error(`Slack API error: ${s.error}`);return s}var P={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:_.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack (connected)
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 a=b();if(!a)return null;let t={};for(let e of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[e]&&(t[e]=process.env[e]);for(let e of this.envKeys)process.env[e]&&(t[e]=process.env[e]);return{type:"stdio",command:"node",args:[a],env:t,alwaysLoad:!0}},async handleToolCall(a,t){try{switch(a){case"slack_list_channels":{let e=await n("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(e.channels||[]).map(r=>({id:r.id,name:r.name,topic:r.topic?.value}))})}case"slack_post_message":{if(!t.channel||!t.text)return JSON.stringify({error:"channel and text are required"});let e=await n("chat.postMessage",{channel:t.channel,text:t.text});return JSON.stringify({ok:!0,ts:e.ts,channel:e.channel})}case"slack_reply_to_thread":{if(!t.channel||!t.thread_ts||!t.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let e=await n("chat.postMessage",{channel:t.channel,thread_ts:t.thread_ts,text:t.text});return JSON.stringify({ok:!0,ts:e.ts})}case"slack_add_reaction":return!t.channel||!t.timestamp||!t.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await n("reactions.add",{channel:t.channel,timestamp:t.timestamp,name:t.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!t.channel)return JSON.stringify({error:"channel is required"});let e=await n("conversations.history",{channel:t.channel,limit:t.limit||20});return JSON.stringify({messages:(e.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_thread_replies":{if(!t.channel||!t.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let e=await n("conversations.replies",{channel:t.channel,ts:t.thread_ts});return JSON.stringify({messages:(e.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_users":{let e=await n("users.list",{limit:100});return JSON.stringify({users:(e.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(!t.user_id)return JSON.stringify({error:"user_id is required"});let e=await n("users.profile.get",{user:t.user_id});return JSON.stringify({profile:e.profile})}case"slack_lookup_user_by_email":{if(!t.email)return JSON.stringify({error:"email is required"});try{let e=await n("users.lookupByEmail",{email:t.email});return JSON.stringify({ok:!0,user:{id:e.user?.id,name:e.user?.real_name||e.user?.name,email:e.user?.profile?.email||t.email}})}catch(e){if(/users_not_found/.test(e.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw e}}case"slack_list_usergroups":{let e=await n("usergroups.list",{});return JSON.stringify({usergroups:(e.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(!t.usergroup)return JSON.stringify({error:"usergroup id is required"});let e=await n("usergroups.users.list",{usergroup:t.usergroup});return JSON.stringify({users:e.users||[]})}case"slack_search_users":{if(!t.query||typeof t.query!="string")return JSON.stringify({error:"query is required"});let e=t.query.trim().toLowerCase();if(!e)return JSON.stringify({ok:!0,matches:[]});let r=Math.max(1,Math.min(Number(t.limit)||5,25)),c=[],o,m=5;for(let s=0;s<m;s+=1){let i={limit:200};o&&(i.cursor=o);let l=await n("users.list",i);for(let p of l.members||[])p.deleted||p.is_bot||c.push(p);if(o=l.response_metadata?.next_cursor,!o)break}let d=[];for(let s of c){let i=(s.real_name||"").toLowerCase(),l=(s.profile?.display_name||"").toLowerCase(),p=(s.name||"").toLowerCase(),u=0;i.includes(e)&&(u+=100-Math.abs(i.length-e.length)),l.includes(e)&&(u+=60-Math.abs(l.length-e.length)),p.includes(e)&&(u+=30-Math.abs(p.length-e.length)),(i===e||l===e)&&(u+=200),u>0&&d.push({id:s.id,name:s.real_name||s.profile?.display_name||s.name,email:s.profile?.email||void 0,_score:u})}return d.sort((s,i)=>i._score-s._score),JSON.stringify({ok:!0,matches:d.slice(0,r).map(({_score:s,...i})=>i),scanned:c.length})}default:return JSON.stringify({error:`Unknown tool: ${a}`})}}catch(e){return JSON.stringify({error:e.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",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Message text"}},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"]}}]};export{P as slackSkill};
7
+ - slack_list_usergroups, slack_get_usergroup_members (workspace-defined teams like @oncall, @platform)`,resolve(){let a=b();if(!a)return null;let t={};for(let e 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[e]&&(t[e]=process.env[e]);for(let e of this.envKeys)process.env[e]&&(t[e]=process.env[e]);return{type:"stdio",command:"node",args:[a],env:t,alwaysLoad:!0}},async handleToolCall(a,t){try{switch(a){case"slack_list_channels":{let e=await n("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(e.channels||[]).map(r=>({id:r.id,name:r.name,topic:r.topic?.value}))})}case"slack_post_message":{if(!t.channel||!t.text)return JSON.stringify({error:"channel and text are required"});let e=await n("chat.postMessage",{channel:t.channel,text:t.text,...t.blocks?{blocks:t.blocks}:{}});return JSON.stringify({ok:!0,ts:e.ts,channel:e.channel})}case"slack_reply_to_thread":{if(!t.channel||!t.thread_ts||!t.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let e=await n("chat.postMessage",{channel:t.channel,thread_ts:t.thread_ts,text:t.text});return JSON.stringify({ok:!0,ts:e.ts})}case"slack_add_reaction":return!t.channel||!t.timestamp||!t.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await n("reactions.add",{channel:t.channel,timestamp:t.timestamp,name:t.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!t.channel)return JSON.stringify({error:"channel is required"});let e=await n("conversations.history",{channel:t.channel,limit:t.limit||20});return JSON.stringify({messages:(e.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_thread_replies":{if(!t.channel||!t.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let e=await n("conversations.replies",{channel:t.channel,ts:t.thread_ts});return JSON.stringify({messages:(e.messages||[]).map(r=>({user:r.user,text:r.text,ts:r.ts}))})}case"slack_get_users":{let e=await n("users.list",{limit:100});return JSON.stringify({users:(e.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(!t.user_id)return JSON.stringify({error:"user_id is required"});let e=await n("users.profile.get",{user:t.user_id});return JSON.stringify({profile:e.profile})}case"slack_lookup_user_by_email":{if(!t.email)return JSON.stringify({error:"email is required"});try{let e=await n("users.lookupByEmail",{email:t.email});return JSON.stringify({ok:!0,user:{id:e.user?.id,name:e.user?.real_name||e.user?.name,email:e.user?.profile?.email||t.email}})}catch(e){if(/users_not_found/.test(e.message))return JSON.stringify({ok:!1,reason:"users_not_found"});throw e}}case"slack_list_usergroups":{let e=await n("usergroups.list",{});return JSON.stringify({usergroups:(e.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(!t.usergroup)return JSON.stringify({error:"usergroup id is required"});let e=await n("usergroups.users.list",{usergroup:t.usergroup});return JSON.stringify({users:e.users||[]})}case"slack_search_users":{if(!t.query||typeof t.query!="string")return JSON.stringify({error:"query is required"});let e=t.query.trim().toLowerCase();if(!e)return JSON.stringify({ok:!0,matches:[]});let r=Math.max(1,Math.min(Number(t.limit)||5,25)),c=[],o,m=5;for(let s=0;s<m;s+=1){let i={limit:200};o&&(i.cursor=o);let l=await n("users.list",i);for(let p of l.members||[])p.deleted||p.is_bot||c.push(p);if(o=l.response_metadata?.next_cursor,!o)break}let d=[];for(let s of c){let i=(s.real_name||"").toLowerCase(),l=(s.profile?.display_name||"").toLowerCase(),p=(s.name||"").toLowerCase(),u=0;i.includes(e)&&(u+=100-Math.abs(i.length-e.length)),l.includes(e)&&(u+=60-Math.abs(l.length-e.length)),p.includes(e)&&(u+=30-Math.abs(p.length-e.length)),(i===e||l===e)&&(u+=200),u>0&&d.push({id:s.id,name:s.real_name||s.profile?.display_name||s.name,email:s.profile?.email||void 0,_score:u})}return d.sort((s,i)=>i._score-s._score),JSON.stringify({ok:!0,matches:d.slice(0,r).map(({_score:s,...i})=>i),scanned:c.length})}default:return JSON.stringify({error:`Unknown tool: ${a}`})}}catch(e){return JSON.stringify({error:e.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"]}}]};export{P as slackSkill};
@@ -1,5 +1,5 @@
1
1
  ---
2
- sidebar_position: 4
2
+ sidebar_position: 5
3
3
  title: Agent operator
4
4
  ---
5
5
 
@@ -111,4 +111,20 @@ When you `zibby app upgrade <id>` manually, agent-ops watches the rollout and ro
111
111
 
112
112
  The activity log shows the full attempted upgrade timeline so you can see why a rollback happened.
113
113
 
114
- Done with apps. See [Workflows](../get-started/your-first-workflow) for the agent-pipeline counterpart, or [CLI Reference](../cli-reference#app-commands) for the full `zibby app` command surface.
114
+ ## How goal-mode deploys are supervised
115
+
116
+ When you `zibby app deploy --goal "..."` (see [Goal-mode deploys](./goal-mode)), agent-ops switches into a different mode at task start: `AGENT_OPS_BOOTSTRAP_MODE=agent_script`. Instead of "run a known image and health-check it", it does:
117
+
118
+ 1. **Plan** — Claude (Write+Read tools only, no Bash) writes a complete `/tmp/install.sh` from the customer's goal text + the house rules. ~2 turns, ~$0.05 in tokens.
119
+ 2. **Supervise** — agent-ops execs the script in a process group, then every 30s sends Claude (text-only) a snapshot of stdout/stderr tail + proc status. Claude returns one JSON line: `continue`, `done`, or `intervene`. On `intervene`, agent-ops SIGTERMs the process group, gives it 5s grace, SIGKILLs, and replans with the previous failure context.
120
+ 3. **Auto-short-circuit** — if the script exits with code 0 AND the verify port returns 2xx-499, agent-ops declares `done` without consulting the supervisor. This catches the false-positive `intervene` that fires when the planner backgrounds the app with nohup and Claude can't see "startup logs" in the snapshot.
121
+
122
+ Hard caps: 5 iterations, 30 min wall-clock, $1.00 token budget — whichever hits first wins. Every iteration's script, supervisor turns, and final status are persisted under `/var/lib/agent-ops/agent_script-state/` on the per-instance EFS volume — `zibby app logs <id>` surfaces the timeline.
123
+
124
+ ### House rules
125
+
126
+ agent-ops reads `AGENT_OPS_BOOTSTRAP_SYSTEM_RULES` at task start and prepends it to the planner's system prompt. We curate ~6 rules in the backend covering things like: "Write COMPLETE bash scripts per turn, never run individual commands", "Never background installs; only the final long-running app process", "End with a curl health-check loop on the target port", "If install > 15 min, switch to a pre-built distribution". The rules are env-driven so we can iterate wording without rebuilding agent-ops — and it's why goal-mode deploys tend to converge reliably rather than oscillate between "almost done" and "container exited".
127
+
128
+ You don't set these yourself; they're operator-curated. But they're the reason the planner consistently emits an install script that ends in a working `curl localhost:<port>` health check, not a half-finished session of individual commands.
129
+
130
+ → Done with apps. See [Goal-mode deploys](./goal-mode), [Auth proxy](./auth), or [CLI Reference](../cli-reference#app-commands) for the full `zibby app` command surface.
@@ -0,0 +1,158 @@
1
+ ---
2
+ sidebar_position: 4
3
+ title: Auth proxy
4
+ ---
5
+
6
+ # Auth proxy
7
+
8
+ Most self-hosted apps either ship with weak default credentials (`admin/admin` Grafana) or no app-level auth at all (n8n's free tier, raw webhook receivers). The auth proxy is a per-instance opt-in **Caddy sidecar** that fronts the ALB and validates a credential before letting the request reach the app.
9
+
10
+ It's not a replacement for the app's own login system — it's a pre-filter. The app keeps its own session model; the proxy just makes sure the request never gets there without a valid credential.
11
+
12
+ ## Architecture
13
+
14
+ ```
15
+ without auth:
16
+ client → ALB → task:<appPort>
17
+
18
+ with --auth-type basic:
19
+ client → ALB → task:8888 (caddy)
20
+ │ basic-auth check (bcrypt hash)
21
+
22
+ localhost:<appPort> (app)
23
+
24
+ with --auth-type token:
25
+ client → ALB → task:8888 (caddy)
26
+ │ Authorization: Bearer <token>
27
+
28
+ localhost:<appPort> (app)
29
+ ```
30
+
31
+ The Caddy image is `caddy:2-alpine` direct (no custom ECR build). The entrypoint is an inline `sh -c` script that templates the Caddyfile from env at boot. The app keeps listening on its own internal port (unchanged); ALB just routes to Caddy instead.
32
+
33
+ Cost: zero. Same Fargate task, +1 small container (~30 MB RAM, ~0 CPU).
34
+
35
+ ## Storage
36
+
37
+ - **Basic auth.** Password is bcrypt-hashed at the backend; the hash lives in the encrypted env bag. Plaintext is never persisted server-side.
38
+ - **Token auth.** Token is KMS-encrypted at rest. Backend keeps the last 4 characters for audit (so `zibby app status` can show `Auth: token (…a3f9)` without leaking the secret). If the customer doesn't supply a token, the backend generates a 32-char URL-safe random one and returns it ONCE in the deploy response.
39
+
40
+ You can rotate either at any time without redeploying the app (see `set-auth` below) — only the Caddy container restarts.
41
+
42
+ ## Deploying with auth
43
+
44
+ ### Basic auth
45
+
46
+ ```bash
47
+ zibby app deploy grafana \
48
+ --project <project-id> \
49
+ --name metrics \
50
+ --auth-type basic \
51
+ --auth-user admin \
52
+ --auth-password 'S0me-long-passphrase!'
53
+ ```
54
+
55
+ `--auth-user` is printable ASCII (no spaces), 1-64 chars. `--auth-password` is 8-256 chars and can come from `ZIBBY_APP_AUTH_PASSWORD` env to keep it out of shell history.
56
+
57
+ Verify:
58
+
59
+ ```bash
60
+ curl -I https://a1b2c3d4.apps.zibby.dev
61
+ # HTTP/2 401 ← Caddy bounces unauthenticated request
62
+
63
+ curl -I -u 'admin:S0me-long-passphrase!' https://a1b2c3d4.apps.zibby.dev
64
+ # HTTP/2 302 ← Grafana redirects to /login (the app's own auth, behind the proxy)
65
+ ```
66
+
67
+ ### Token auth (auto-generated)
68
+
69
+ ```bash
70
+ zibby app deploy gotify --project <id> --name notify --auth-type token
71
+ ```
72
+
73
+ Response includes:
74
+
75
+ ```
76
+ ✔ Deployed (instanceId: f1e2d3c4)
77
+ → Public URL: https://f1e2d3c4.apps.zibby.dev
78
+ → Auth token: 7Kf3uL9pXmQ2vR8sT4nW6yE1bH5dG0aC
79
+ ^^^ shown ONCE — save it now, you can't retrieve it
80
+ ```
81
+
82
+ Verify:
83
+
84
+ ```bash
85
+ curl -I https://f1e2d3c4.apps.zibby.dev
86
+ # HTTP/2 401
87
+
88
+ curl -I -H 'Authorization: Bearer 7Kf3uL9pXmQ2vR8sT4nW6yE1bH5dG0aC' \
89
+ https://f1e2d3c4.apps.zibby.dev
90
+ # HTTP/2 200
91
+ ```
92
+
93
+ ### Token auth (caller-supplied)
94
+
95
+ If you already have a token (e.g. minted by your own auth system), pass it explicitly:
96
+
97
+ ```bash
98
+ zibby app deploy gotify \
99
+ --project <id> --name notify \
100
+ --auth-type token \
101
+ --auth-token "$(cat ~/.secrets/gotify-bearer.txt)"
102
+ ```
103
+
104
+ Also accepts `ZIBBY_APP_AUTH_TOKEN` env.
105
+
106
+ ### No auth (the default)
107
+
108
+ ```bash
109
+ zibby app deploy grafana --project <id> --name metrics
110
+ # No Caddy container; ALB routes straight to the app's port.
111
+ ```
112
+
113
+ ## Changing auth after deploy
114
+
115
+ `zibby app set-auth <instanceId>` is the rotate/replace endpoint. It has the same auth flags as `deploy`, with PATCH semantics — omitted flags preserve current state.
116
+
117
+ ### Rotate just the password
118
+
119
+ ```bash
120
+ zibby app set-auth a1b2c3d4 --auth-password 'N3w-passphrase-2026!'
121
+ ```
122
+
123
+ Only the password changes — auth type and username are preserved. The ECS task rolls (~60-90s) to pick up the new bcrypt hash; the app container keeps its data.
124
+
125
+ ### Switch from basic to token
126
+
127
+ ```bash
128
+ zibby app set-auth a1b2c3d4 --auth-type token --auth-token 'mY-shared-bearer-token'
129
+ ```
130
+
131
+ ### Switch from no-auth to basic
132
+
133
+ ```bash
134
+ zibby app set-auth a1b2c3d4 \
135
+ --auth-type basic \
136
+ --auth-user admin \
137
+ --auth-password 'S0me-long-passphrase!'
138
+ ```
139
+
140
+ This is the path most operators take: deploy quickly without auth, verify the app works, then put Caddy in front before exposing it to the world.
141
+
142
+ ### Remove auth entirely
143
+
144
+ ```bash
145
+ zibby app set-auth a1b2c3d4 --off
146
+ ```
147
+
148
+ The Caddy container is stripped from the next task definition revision; the ALB target group flips back to the app's port. ~60-90s rolling replace.
149
+
150
+ ## When to use which
151
+
152
+ - **Basic auth** — humans hitting a dashboard (Grafana, Uptime Kuma, draw.io, OpenObserve). One shared credential the team knows.
153
+ - **Token auth** — machines hitting an endpoint (webhook receivers, internal APIs, anything you're going to put in another tool's "header to send" box). Long random string, rotated on schedule.
154
+ - **No auth** — only when the app has its own SSO already wired up (Authentik, Zitadel) and you actively want the open URL.
155
+
156
+ The proxy doesn't replace per-user accounts inside the app — for that you still need the app's own auth (e.g. Grafana's user database, n8n's user table). Treat the proxy as a perimeter, not an identity system.
157
+
158
+ → Next: [Goal-mode deploys](./goal-mode) or [Agent operator](./agent-ops)
@@ -5,7 +5,7 @@ title: Deploy your first app
5
5
 
6
6
  # Deploy your first app
7
7
 
8
- A complete walk-through — from `zibby app templates` to a running n8n instance behind a stable URL — in 30 seconds.
8
+ A complete walk-through — from `zibby app templates` to a running instance behind a stable URL — in under two minutes.
9
9
 
10
10
  ## Prerequisites
11
11
 
@@ -25,24 +25,27 @@ zibby app templates
25
25
  ```
26
26
 
27
27
  ```
28
- ID Display name Tier Rate Description
29
- n8n n8n Light $0.05/hr Workflow automation. 200+ integrations.
30
- grafana Grafana Light $0.05/hr Dashboards for metrics, logs, traces.
31
- gastown Gas Town Light $0.05/hr Multi-agent workspace. Coordinate Claude, Codex, Cursor, Gemini.
32
- drawio draw.io Light $0.05/hr Client-side diagram editor. Flowcharts, UML, ER, network.
33
- open-webui Open WebUI Heavy $0.25/hr ChatGPT-style UI for Ollama / OpenAI-compatible endpoints.
28
+ ID Display name Tier Rate Description
29
+ grafana Grafana Light $0.05/hr Dashboards for metrics, logs, traces.
30
+ uptime-kuma Uptime Kuma Light $0.05/hr Self-hosted Pingdom-alt.
31
+ drawio draw.io Light $0.05/hr Client-side diagram editor.
32
+ gastown Gas Town Light $0.05/hr Multi-agent workspace.
33
+ open-webui Open WebUI Heavy $0.25/hr ChatGPT-style UI for Ollama / OpenAI.
34
+ openhands OpenHands Heavy $0.25/hr AI software-engineer agent (V1).
35
+ docmost Docmost Heavy $0.25/hr Wiki + collaboration (multi-service).
36
+ … (20 entries total — see `zibby app templates` for the full list)
34
37
  ```
35
38
 
36
- ## Deploy
39
+ ## Deploy from the catalog
37
40
 
38
41
  ```bash
39
- zibby app deploy n8n --project <project-id> --name automations
42
+ zibby app deploy grafana --project <project-id> --name metrics
40
43
  ```
41
44
 
42
45
  On success:
43
46
 
44
47
  ```
45
- ↑ Provisioning n8n on Fargate…
48
+ ↑ Provisioning grafana on Fargate…
46
49
  ECS service + EFS volume + ALB target group
47
50
  agent-ops sidecar starting…
48
51
  ✔ Deployed (instanceId: a1b2c3d4)
@@ -56,12 +59,12 @@ The provisioning steps:
56
59
 
57
60
  1. **Allocate an instance ID** — short hex token used as the subdomain
58
61
  2. **Create EFS access point** — per-instance volume, encrypted at rest, AZ-pinned
59
- 3. **Register task definition** — pinned to the catalog entry's image + your resource tier
60
- 4. **Spin up ECS service** — desired count 1, agent-ops sidecar bundled alongside the app container
62
+ 3. **Register task definition** — pinned to the catalog entry's image(s) + your resource tier; one container per `services[]` entry for multi-service apps
63
+ 4. **Spin up ECS service** — desired count 1, agent-ops sidecar bundled alongside the app container(s)
61
64
  5. **Wire the ALB** — listener rule routes `<id>.apps.zibby.dev` to the new target group
62
65
  6. **Health-check loop** — the first agent-ops tick fires once the container is up
63
66
 
64
- Wall-clock: ~45-90 seconds. The CLI streams progress and prints the public URL the moment the ALB is responsive.
67
+ Wall-clock: ~45-90 seconds for catalog deploys. The CLI streams progress and prints the public URL the moment the ALB is responsive.
65
68
 
66
69
  ## Verify
67
70
 
@@ -70,7 +73,7 @@ zibby app status a1b2c3d4
70
73
  ```
71
74
 
72
75
  ```
73
- automations (n8n v1.97.1)
76
+ metrics (grafana v10.4.2)
74
77
  ┌ status running (1/1) ✓
75
78
  ├ resources 0.5 vCPU · 1 GB RAM ✓
76
79
  └ hourly $0.05/hr ✓
@@ -80,7 +83,84 @@ Public URL: https://a1b2c3d4.apps.zibby.dev
80
83
  Last agent-ops run: 14:00:01 hourly_health_check ok (1.2s)
81
84
  ```
82
85
 
83
- Open the URL in a browser — n8n's setup screen renders, you create the admin account, and the instance is private to you. The data sits on the EFS volume, encrypted and isolated; no other Zibby customer can reach it.
86
+ Open the URL in a browser — Grafana's login screen renders, you sign in with the default `admin / admin` (and immediately rotate it — or better, follow the next section and put basic auth in front of the whole thing). The data sits on the EFS volume, encrypted and isolated; no other Zibby customer can reach it.
87
+
88
+ ## Deploying with auth from day one
89
+
90
+ Most self-hosted dashboards (Grafana, Uptime Kuma, n8n on the free tier) either have weak default credentials or no app-level auth at all. The fix is the **auth proxy** — opt in at deploy time and a Caddy sidecar fronts the ALB:
91
+
92
+ ```bash
93
+ zibby app deploy grafana \
94
+ --project <project-id> \
95
+ --name metrics \
96
+ --auth-type basic \
97
+ --auth-user admin \
98
+ --auth-password 'S0me-long-passphrase!'
99
+ ```
100
+
101
+ Verify it sticks:
102
+
103
+ ```bash
104
+ curl -I https://a1b2c3d4.apps.zibby.dev
105
+ # HTTP/2 401 ← unauthenticated request is bounced by Caddy
106
+
107
+ curl -I -u 'admin:S0me-long-passphrase!' https://a1b2c3d4.apps.zibby.dev
108
+ # HTTP/2 302 ← Grafana sees the request and redirects to /login
109
+ ```
110
+
111
+ Token-based auth is also supported (for webhook receivers, machine-to-machine access):
112
+
113
+ ```bash
114
+ zibby app deploy gotify --project <id> --name notify --auth-type token
115
+ # Backend auto-generates a 32-char URL-safe token and prints it ONCE.
116
+ ```
117
+
118
+ You can also pass `--auth-token <yours>` to use your own value. Full details + the password-rotation flow are in [Auth proxy](./auth).
119
+
120
+ ## Deploying anything else (goal-mode)
121
+
122
+ If the app you want isn't in the catalog — or its license forbids us shipping a one-click bundle — use **goal-mode**. You describe the install in plain English, Claude writes a bash script, agent-ops runs and supervises it inside the container until the app is responding on a port.
123
+
124
+ ```bash
125
+ zibby app deploy --goal "Install n8n on port 5678 with sqlite persistence" \
126
+ --project <project-id> \
127
+ --name automations \
128
+ --max-turns 40 \
129
+ --timeout-min 30
130
+ ```
131
+
132
+ Output streams the planner's progress + the supervisor's verdicts every ~30s:
133
+
134
+ ```
135
+ ↑ Goal-mode deploy: "Install n8n on port 5678 with sqlite persistence"
136
+ phase 1: planning install script (Claude write+read tools)…
137
+ phase 2: executing /tmp/install.sh under supervision…
138
+ [30s ] supervisor: continue — npm install in progress
139
+ [60s ] supervisor: continue — npm install complete, starting n8n
140
+ [90s ] auto-short-circuit: process exit 0, port 5678 returns 200
141
+ ✔ Deployed (instanceId: f1e2d3c4)
142
+ → Public URL: https://f1e2d3c4.apps.zibby.dev
143
+ ```
144
+
145
+ License terms of whatever you install are yours, not Zibby's — same model as deploying on your own EC2. Full architecture + cost expectations: [Goal-mode deploys](./goal-mode).
146
+
147
+ ## Choosing your model
148
+
149
+ Goal-mode deploys (and the cheatsheet-mode catalog entries that have a planner step) call Claude inside the container. You can pick which model:
150
+
151
+ ```bash
152
+ zibby app deploy --goal "..." --model claude-sonnet-4-6
153
+ zibby app deploy --goal "..." --model claude-opus-4-8 # heavier installs
154
+ zibby app deploy --goal "..." --model claude-haiku-4-5-20251001 # cheaper, faster
155
+ ```
156
+
157
+ Rule of thumb:
158
+
159
+ - **Sonnet** (default) — most installs fit. Good speed/cost balance.
160
+ - **Opus** — bump up for installs that hit lots of intervene loops (heavy native compilation, weird init systems, anything where the first plan tends to be wrong).
161
+ - **Haiku** — fine for installs you've already run once and know are reliable.
162
+
163
+ Token spend per deploy is typically $0.05 - $0.30 on Sonnet. Opus can go to ~$1.00 on a deploy that takes 5 intervene iterations.
84
164
 
85
165
  ## Watch logs while it warms up
86
166
 
@@ -93,11 +173,18 @@ zibby app logs a1b2c3d4 -t
93
173
  Logs cover both the app container **and** the agent-ops sidecar. Container logs are color-coded by source:
94
174
 
95
175
  ```
96
- 14:00:00.122 [n8n] Listening on port 5678
176
+ 14:00:00.122 [grafana] Listening on port 3000
97
177
  14:00:01.044 [agent-ops] hourly_health_check: HTTP 200 in 1.2s
98
178
  14:00:01.061 [agent-ops] ✓ instance healthy — next tick in 60m
99
179
  ```
100
180
 
181
+ For multi-service apps (e.g. docmost), scope to one container with `--service`:
182
+
183
+ ```bash
184
+ zibby app logs <id> -t --service db # just the postgres container
185
+ zibby app logs <id> -t --service web # just the docmost web container
186
+ ```
187
+
101
188
  `Ctrl+C` exits tail mode; logs persist in CloudWatch with 30-day retention.
102
189
 
103
190
  ## What's actually private vs shared
@@ -106,11 +193,11 @@ Mental model that lines up with what the bill shows:
106
193
 
107
194
  | Resource | Per-instance? |
108
195
  |---|---|
109
- | Subdomain (`<id>.apps.zibby.dev`) | Yours |
110
- | EFS volume | Yours, encrypted |
111
- | ALB target group | Yours |
112
- | ECS task definition | Yours (revisions tracked) |
113
- | Fargate task | Yours |
196
+ | Subdomain (`<id>.apps.zibby.dev`) | Yours |
197
+ | EFS volume | Yours, encrypted |
198
+ | ALB target group | Yours |
199
+ | ECS task definition | Yours (revisions tracked) |
200
+ | Fargate task | Yours |
114
201
  | ALB itself | Shared — pooled across all tenants |
115
202
  | ECS cluster | Shared |
116
203
  | EFS file system | Shared, but per-instance access points enforce isolation |