@zibby/skills 0.1.26 → 0.1.28

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,7 +1,8 @@
1
- import{existsSync as l}from"fs";import{fileURLToPath as m}from"url";import{dirname as g,resolve as h}from"path";import{resolveIntegrationToken as u}from"@zibby/core/backend-client.js";var p=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"}),b=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 y(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let e=g(m(import.meta.url)),t=h(e,"..","bin","mcp-lark.mjs");return l(t)?t:null}var f=6e3*1e3,s=null;async function k(){let{appId:e,appSecret:t,host:i}=await u("lark");if(s&&s.appId===e&&s.expiresAt>Date.now())return{token:s.token,host:i};let r=await(await fetch(`${i}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:e,app_secret:t})})).json();if(r.code!==0)throw new Error(`Lark tenant_access_token failed: ${r.msg||r.code}`);return s={token:r.tenant_access_token,expiresAt:Date.now()+f,appId:e},{token:r.tenant_access_token,host:i}}async function c(e,t,i={}){let{token:a,host:r}=await k(),n=`${r}${t}`,d={method:e,headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json; charset=utf-8"}};e!=="GET"&&(d.body=JSON.stringify(i));let o=await(await fetch(n,d)).json();if(o.code!==0)throw new Error(`Lark API ${t} error: ${o.msg||o.code}`);return o.data||{}}function _(e){return JSON.stringify({text:e})}function T(e){return!e||typeof e!="string"||e.startsWith("oc_")?"chat_id":e.startsWith("ou_")?"open_id":e.startsWith("on_")?"union_id":e.startsWith("cli_")?"app_id":e.includes("@")?"email":"chat_id"}var L={id:"lark",serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:p.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"}),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)
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
- When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let e=y();if(!e)return null;let t={};for(let i of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[i]&&(t[i]=process.env[i]);return{type:"stdio",command:"node",args:[e],env:t,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"]}}],async handleToolCall(e,t){try{switch(e){case"lark_send_message":{if(!t.receive_id||!t.text)return JSON.stringify({error:"receive_id and text are required"});let i=T(t.receive_id),a=await c("POST",`/open-apis/im/v1/messages?receive_id_type=${i}`,{receive_id:t.receive_id,msg_type:"text",content:_(t.text)});return JSON.stringify({ok:!0,message_id:a.message_id})}case"lark_reply":{if(!t.message_id||!t.text)return JSON.stringify({error:"message_id and text are required"});let i=await c("POST",`/open-apis/im/v1/messages/${encodeURIComponent(t.message_id)}/reply`,{msg_type:"text",content:_(t.text)});return JSON.stringify({ok:!0,message_id:i.message_id})}case"lark_list_chats":{let i=t.page_size||50,r=((await c("GET",`/open-apis/im/v1/chats?page_size=${i}`)).items||[]).map(n=>({chat_id:n.chat_id,name:n.name,description:n.description,owner_id:n.owner_id,chat_mode:n.chat_mode}));return JSON.stringify({chats:r})}case"lark_get_chat_history":{if(!t.chat_id)return JSON.stringify({error:"chat_id is required"});let i=t.page_size||20,r=((await c("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(t.chat_id)}&page_size=${i}&sort_type=ByCreateTimeDesc`)).items||[]).map(n=>({message_id:n.message_id,sender_id:n.sender?.id,sender_type:n.sender?.sender_type,msg_type:n.msg_type,content:n.body?.content,create_time:n.create_time}));return JSON.stringify({messages:r})}default:return JSON.stringify({error:`Unknown tool: ${e}`})}}catch(i){return JSON.stringify({error:i.message})}}};function P(){s=null}export{P as _resetLarkTokenCache,L as larkSkill};
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};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/skills",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "Built-in skill definitions for Zibby test automation framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,6 +8,7 @@
8
8
  ".": "./dist/index.js",
9
9
  "./bin/mcp-sentry.mjs": "./bin/mcp-sentry.mjs",
10
10
  "./bin/mcp-lark.mjs": "./bin/mcp-lark.mjs",
11
+ "./bin/mcp-slack.mjs": "./bin/mcp-slack.mjs",
11
12
  "./browser": "./dist/browser.js",
12
13
  "./jira": "./dist/jira.js",
13
14
  "./github": "./dist/github.js",
package/dist/slack.js CHANGED
@@ -1,5 +1,7 @@
1
- import{resolveIntegrationToken as d}from"@zibby/core/backend-client.js";var p=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"}),_=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"}});async function r(i,e={}){let{token:n}=await d("slack"),t=["conversations.list","users.list","users.profile.get","conversations.history","conversations.replies"].includes(i),a=`https://slack.com/api/${i}`,o={Authorization:`Bearer ${n}`},c;if(t){let l=new URLSearchParams(e).toString();l&&(a+=`?${l}`)}else o["Content-Type"]="application/json; charset=utf-8",c=JSON.stringify(e);let s=await(await fetch(a,{method:t?"GET":"POST",headers:o,body:c})).json();if(!s.ok)throw new Error(`Slack API error: ${s.error}`);return s}var y={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:p.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 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)
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
- - slack_get_users, slack_get_user_profile`,resolve(){let i={};for(let e of this.envKeys)process.env[e]&&(i[e]=process.env[e]);return{command:"npx",args:["-y","@modelcontextprotocol/server-slack@latest"],env:i}},async handleToolCall(i,e){try{switch(i){case"slack_list_channels":{let n=await r("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(n.channels||[]).map(t=>({id:t.id,name:t.name,topic:t.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let n=await r("chat.postMessage",{channel:e.channel,text:e.text});return JSON.stringify({ok:!0,ts:n.ts,channel:n.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 n=await r("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:n.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await r("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 n=await r("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(n.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let n=await r("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(n.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_users":{let n=await r("users.list",{limit:100});return JSON.stringify({users:(n.members||[]).filter(t=>!t.is_bot&&!t.deleted).map(t=>({id:t.id,name:t.real_name||t.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let n=await r("users.profile.get",{user:e.user_id});return JSON.stringify({profile:n.profile})}default:return JSON.stringify({error:`Unknown tool: ${i}`})}}catch(n){return JSON.stringify({error:n.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"]}}]};export{y as slackSkill};
5
+ - slack_get_users, slack_get_user_profile
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};
@@ -0,0 +1,114 @@
1
+ ---
2
+ sidebar_position: 4
3
+ title: Agent operator
4
+ ---
5
+
6
+ # The agent-ops sidecar
7
+
8
+ Every Zibby Managed App ships with **agent-ops**, an autonomous daemon sidecar that runs alongside the app container. It does what a human operator would do — checks health, restarts on crash, prunes disk, rolls upgrades — only it does it every hour, never sleeps, and never forgets to file a runbook.
9
+
10
+ This is the structural difference between "deploy button on a VM" and **Zibby**.
11
+
12
+ ## What it does
13
+
14
+ | Task | Cadence | What happens |
15
+ |---|---|---|
16
+ | **Hourly health check** | every 60 min | HTTP probe + container state + EFS usage. Recorded as a structured run record. |
17
+ | **Self-heal on OOM** | event-driven | Container exits with OOMKilled → agent-ops triggers an ECS restart, records the recovery. |
18
+ | **Disk-pressure prune** | when EFS > 90% | Removes safe-to-delete caches (e.g. n8n execution history older than 30 days). Configurable. |
19
+ | **Upgrade orchestration** | on schedule | When a new app version lands in the catalog, agent-ops can run the in-place upgrade on a cron you set. |
20
+ | **Activity log** | every action | One row in the app's "Agent activity" tab, with structured fields you can grep / chart. |
21
+
22
+ Every action lands in DynamoDB as an `app-runs` record — queryable by anything from a workflow node to a Grafana dashboard.
23
+
24
+ ## See it in action
25
+
26
+ ```bash
27
+ zibby app activity a1b2c3d4
28
+ ```
29
+
30
+ ```
31
+ Time Action Status Duration Notes
32
+ 14:00:01 hourly_health_check ok 1.2s
33
+ 13:00:01 hourly_health_check ok 1.1s
34
+ 12:04:38 oom_recovery ok 4.8s restarted container after OOMKilled
35
+ 12:00:01 hourly_health_check warn 2.1s container restarting
36
+ 11:00:01 hourly_health_check ok 1.3s
37
+ 10:00:01 hourly_health_check ok 1.0s
38
+ ```
39
+
40
+ The dashboard's "Agent activity" tab shows the same records with extra context (HTTP status codes, container logs at failure time, recovery diff).
41
+
42
+ ## Run records ≠ logs
43
+
44
+ A run record is **structured metadata** about something agent-ops did:
45
+
46
+ ```json
47
+ {
48
+ "instanceId": "a1b2c3d4",
49
+ "runId": "01J9KZQF...",
50
+ "action": "hourly_health_check",
51
+ "status": "ok",
52
+ "startedAt": "2026-05-30T14:00:01Z",
53
+ "duration_ms": 1234,
54
+ "httpStatus": 200,
55
+ "containerState": "RUNNING"
56
+ }
57
+ ```
58
+
59
+ Logs are unstructured text. Run records are queryable, chartable, and aggregatable — that's why the Agent activity tab can show a 30-day uptime % without you running grep.
60
+
61
+ The CLI exposes them:
62
+
63
+ ```bash
64
+ zibby app activity a1b2c3d4 --since 7d
65
+ zibby app activity a1b2c3d4 --action oom_recovery
66
+ zibby app activity a1b2c3d4 --json | jq '.[] | select(.status == "warn")'
67
+ ```
68
+
69
+ ## Why a sidecar, not a centralized controller?
70
+
71
+ Three properties only this shape gives you:
72
+
73
+ 1. **No noisy-neighbor failure mode** — your instance's agent-ops can't be blocked by another instance's slow health check
74
+ 2. **Per-instance customization without a central feature flag** — env vars (`AGENT_OPS_CHECK_INTERVAL_MIN=15`, `AGENT_OPS_PRUNE_THRESHOLD=80`) live on your instance and just work
75
+ 3. **Egress identity matches the app** — outbound calls from agent-ops use the same ENI / NAT path as the app itself, so when the app has a dedicated egress IP, agent-ops's webhook callbacks come from the same IP
76
+
77
+ ## Customize via env
78
+
79
+ Per-instance agent-ops behavior is tunable via env vars (set on the app instance — Apps → ENV tab — or via `zibby app env set`):
80
+
81
+ | Env var | Default | What it controls |
82
+ |---|---|---|
83
+ | `AGENT_OPS_CHECK_INTERVAL_MIN` | 60 | Minutes between hourly health checks |
84
+ | `AGENT_OPS_PRUNE_THRESHOLD` | 90 | EFS usage % that triggers disk-pressure prune |
85
+ | `AGENT_OPS_AUTO_UPGRADE` | `false` | If `true`, upgrade automatically when catalog publishes a new version |
86
+ | `AGENT_OPS_NOTIFY_WEBHOOK` | — | URL to POST run records to (any HTTPS endpoint — your own backend, n8n, etc.) |
87
+
88
+ `AGENT_OPS_NOTIFY_WEBHOOK` is how you wire agent-ops into your existing observability stack — fire every run record into your team's #ops Slack via a workflow trigger, into Datadog via their webhook receiver, or into your own database.
89
+
90
+ ## Hooking agent-ops into a workflow
91
+
92
+ The most powerful pattern: a Zibby workflow that runs **on agent-ops events**.
93
+
94
+ Example: when an `oom_recovery` fires, run a workflow that pulls the container's last-100-lines, classifies the crash, and pages whoever owns this app:
95
+
96
+ ```bash
97
+ zibby app env set a1b2c3d4 \
98
+ AGENT_OPS_NOTIFY_WEBHOOK=https://api-prod.zibby.app/v1/workflows/<wf-uuid>/trigger
99
+ ```
100
+
101
+ The workflow receives the run record as `input`, can call back to `zibby app logs` / `zibby app status`, and decides what to do. Agent-ops + workflows compose into a self-operating fleet — humans only get pinged for genuinely novel failure modes.
102
+
103
+ ## Upgrade orchestration
104
+
105
+ When you `zibby app upgrade <id>` manually, agent-ops watches the rollout and rolls back if the new task fails health checks twice in a row. With `AGENT_OPS_AUTO_UPGRADE=true` set, the upgrade fires on a cron (default: weekly, Sunday 04:00 UTC) — agent-ops runs the same flow:
106
+
107
+ 1. Register new task definition revision (catalog's latest)
108
+ 2. Update service, watch the rollout
109
+ 3. If 2 consecutive health checks pass on the new revision → keep it
110
+ 4. If 2 fail → roll back to the previous revision, log a `failed_upgrade` run record
111
+
112
+ The activity log shows the full attempted upgrade timeline so you can see why a rollback happened.
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.
@@ -0,0 +1,120 @@
1
+ ---
2
+ sidebar_position: 2
3
+ title: Deploy your first app
4
+ ---
5
+
6
+ # Deploy your first app
7
+
8
+ A complete walk-through — from `zibby app templates` to a running n8n instance behind a stable URL — in 30 seconds.
9
+
10
+ ## Prerequisites
11
+
12
+ You'll need the CLI installed and authenticated:
13
+
14
+ ```bash
15
+ npm install -g @zibby/cli
16
+ zibby login # OAuth in browser, saves session to ~/.zibby/config.json
17
+ ```
18
+
19
+ You also need a project. If you don't have one yet, deploy a workflow first or create one in the [Zibby dashboard](https://studio.zibby.dev) — apps are scoped to projects so per-instance EFS volumes can be isolated per team.
20
+
21
+ ## Browse the catalog
22
+
23
+ ```bash
24
+ zibby app templates
25
+ ```
26
+
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.
34
+ ```
35
+
36
+ ## Deploy
37
+
38
+ ```bash
39
+ zibby app deploy n8n --project <project-id> --name automations
40
+ ```
41
+
42
+ On success:
43
+
44
+ ```
45
+ ↑ Provisioning n8n on Fargate…
46
+ ECS service + EFS volume + ALB target group
47
+ agent-ops sidecar starting…
48
+ ✔ Deployed (instanceId: a1b2c3d4)
49
+ → Public URL: https://a1b2c3d4.apps.zibby.dev
50
+ ```
51
+
52
+ `--project` is interactive-prompted if omitted (CLI walks you through your project list).
53
+ `--name` controls the **display name** — what shows in `zibby app list` and the dashboard. The subdomain is a separate opaque identifier (the instance ID), stable for the life of the instance.
54
+
55
+ The provisioning steps:
56
+
57
+ 1. **Allocate an instance ID** — short hex token used as the subdomain
58
+ 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
61
+ 5. **Wire the ALB** — listener rule routes `<id>.apps.zibby.dev` to the new target group
62
+ 6. **Health-check loop** — the first agent-ops tick fires once the container is up
63
+
64
+ Wall-clock: ~45-90 seconds. The CLI streams progress and prints the public URL the moment the ALB is responsive.
65
+
66
+ ## Verify
67
+
68
+ ```bash
69
+ zibby app status a1b2c3d4
70
+ ```
71
+
72
+ ```
73
+ ● automations (n8n v1.97.1)
74
+ ┌ status running (1/1) ✓
75
+ ├ resources 0.5 vCPU · 1 GB RAM ✓
76
+ └ hourly $0.05/hr ✓
77
+
78
+ Public URL: https://a1b2c3d4.apps.zibby.dev
79
+
80
+ Last agent-ops run: 14:00:01 hourly_health_check ok (1.2s)
81
+ ```
82
+
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.
84
+
85
+ ## Watch logs while it warms up
86
+
87
+ If the app behaves oddly on first launch, tail logs:
88
+
89
+ ```bash
90
+ zibby app logs a1b2c3d4 -t
91
+ ```
92
+
93
+ Logs cover both the app container **and** the agent-ops sidecar. Container logs are color-coded by source:
94
+
95
+ ```
96
+ 14:00:00.122 [n8n] Listening on port 5678
97
+ 14:00:01.044 [agent-ops] hourly_health_check: HTTP 200 in 1.2s
98
+ 14:00:01.061 [agent-ops] ✓ instance healthy — next tick in 60m
99
+ ```
100
+
101
+ `Ctrl+C` exits tail mode; logs persist in CloudWatch with 30-day retention.
102
+
103
+ ## What's actually private vs shared
104
+
105
+ Mental model that lines up with what the bill shows:
106
+
107
+ | Resource | Per-instance? |
108
+ |---|---|
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 |
114
+ | ALB itself | Shared — pooled across all tenants |
115
+ | ECS cluster | Shared |
116
+ | EFS file system | Shared, but per-instance access points enforce isolation |
117
+
118
+ The shared bits are why per-minute pricing can be $0.05/hr instead of $30/mo — economies of scale on the platform side.
119
+
120
+ → Next: [Manage instances](./managing)
@@ -0,0 +1,74 @@
1
+ ---
2
+ sidebar_position: 1
3
+ title: Apps overview
4
+ ---
5
+
6
+ # Managed Apps
7
+
8
+ One-click hosted instances of open-source tools (n8n, Grafana, Open WebUI, draw.io, Gas Town, …), each private to your project — with an **autonomous agent-ops sidecar** that handles health checks, self-healing, and upgrades on its own.
9
+
10
+ ```bash
11
+ zibby app templates # browse the catalog
12
+ zibby app deploy n8n # one-click — ECS service + EFS volume + ALB target group
13
+ zibby app logs <id> -t # tail logs, SSE auto-reconnect
14
+ zibby app status <id> # uptime, cost, version, agent-ops activity
15
+ ```
16
+
17
+ ## Why apps (not workflows)
18
+
19
+ Both are pillars of Zibby Cloud. Pick by **how long the thing needs to run**:
20
+
21
+ | | **Workflow** | **App** |
22
+ |---|---|---|
23
+ | Lifetime | Per-trigger (seconds to minutes) | Long-lived (24/7 or paused) |
24
+ | Surface | A graph of agent CLI calls | A whole open-source application |
25
+ | Billing | Per execution | Per minute, while running |
26
+ | Persistence | Session JSONL + S3 artifacts | Encrypted-at-rest EFS volume |
27
+ | Best for | "When ticket lands, classify it" | "Host n8n for the team" |
28
+
29
+ If you find yourself wanting to **run an open-source web app behind a stable URL**, that's an App. If you want **agent-driven business logic that fires on events**, that's a Workflow.
30
+
31
+ ## What you get with every app
32
+
33
+ - **Private subdomain** — `<instance-id>.apps.zibby.dev`, TLS by default
34
+ - **Dedicated EFS volume** — encrypted-at-rest, persists across container restarts and upgrades
35
+ - **Per-instance ALB target group** — your traffic doesn't share a load balancer with other tenants
36
+ - **Per-minute Fargate billing** — including the agent-ops sidecar, pause-to-stop billing
37
+ - **agent-ops sidecar** (see [Agent operator](./agent-ops)) — hourly health checks, self-healing, upgrades
38
+ - **SSE log streaming** — `zibby app logs -t` tails any container from anywhere
39
+ - **Dedicated egress IP addon** — pin outbound HTTPS through one whitelistable IP for self-hosted GitLab / Salesforce / Oracle Cloud
40
+
41
+ ## The catalog
42
+
43
+ Each marketplace entry is a curated bundle: container image, EFS volume layout, ALB wiring, secrets pattern, resource defaults. Today's catalog:
44
+
45
+ | App | Category | Tier | Rate |
46
+ |---|---|---|---|
47
+ | **n8n** | Workflow automation | Light | $0.05/hr |
48
+ | **Grafana** | Metrics + dashboards | Light | $0.05/hr |
49
+ | **Gas Town** | Multi-agent workspace | Light | $0.05/hr |
50
+ | **draw.io** | Diagrams + flowcharts | Light | $0.05/hr |
51
+ | **Open WebUI** | ChatGPT-style UI for Ollama | Heavy | $0.25/hr |
52
+
53
+ `zibby app templates` is the canonical, always-up-to-date list — the table above is a snapshot.
54
+
55
+ ## How tiers work
56
+
57
+ The catalog groups apps into three resource tiers:
58
+
59
+ | Tier | CPU | RAM | Rate |
60
+ |---|---|---|---|
61
+ | **Light** | 0.5 vCPU | 1 GB | $0.05/hr |
62
+ | **Standard** | 1 vCPU | 2 GB | $0.12/hr |
63
+ | **Heavy** | 2 vCPU | 4 GB | $0.25/hr |
64
+
65
+ Per-instance resource overrides are supported when you need to bump CPU / memory for one specific deployment without forking the catalog entry. See [Managing instances → resource overrides](./managing#resource-overrides).
66
+
67
+ ## Pricing model
68
+
69
+ - **Per-minute Fargate billing** while the instance is running, scoped to the tier above
70
+ - **No flat platform fee** for apps — you pay only for what's running
71
+ - **Pause to stop the meter** — `zibby app destroy` immediately stops billing; redeploy when you need it back (data is gone after destroy; pause-without-destroy is on the roadmap)
72
+ - **Free tier**: $10 in credits on signup, enough to run a Light app for ~8 days
73
+
74
+ → Next: [Deploy your first app](./deploy)
@@ -0,0 +1,121 @@
1
+ ---
2
+ sidebar_position: 3
3
+ title: Manage instances
4
+ ---
5
+
6
+ # Operating instances
7
+
8
+ Every lifecycle action — restart, scale, upgrade, rotate credentials, tear down — is one CLI call. All operations are scoped by **instance ID** (`a1b2c3d4`-style); `zibby app list` shows the ID alongside the display name.
9
+
10
+ ## Inventory
11
+
12
+ ```bash
13
+ zibby app list # all instances under your account
14
+ zibby app list --project <project-id> # scope to one project
15
+ ```
16
+
17
+ ```
18
+ ID Name App Tier Status Hourly Uptime
19
+ a1b2c3d4 automations n8n@1.97.1 Light running $0.05/hr 7d 14h
20
+ a8f7e6d5 metrics grafana Light running $0.05/hr 21d 3h
21
+ b2c3d4e5 webui open-webui Heavy paused — —
22
+ ```
23
+
24
+ `paused` instances are not billed; `running` are. `status` is updated every 60s by the agent-ops sidecar.
25
+
26
+ ## Single-instance status
27
+
28
+ ```bash
29
+ zibby app status a1b2c3d4
30
+ ```
31
+
32
+ A one-screen summary: status, resources, hourly rate, public URL, last agent-ops run.
33
+
34
+ ## Logs
35
+
36
+ ```bash
37
+ zibby app logs a1b2c3d4 # last 200 lines, both containers
38
+ zibby app logs a1b2c3d4 -t # tail mode, polls every 3s
39
+ zibby app logs a1b2c3d4 --lines 1000 # bigger window
40
+ zibby app logs a1b2c3d4 --json # raw JSON lines
41
+ zibby app logs a1b2c3d4 --verbose # full body, no parsing
42
+ ```
43
+
44
+ Logs include both the **app** container and the **agent-ops** sidecar, prefixed by source. Tail mode reconnects automatically on network blips.
45
+
46
+ ## Upgrade (zero-downtime)
47
+
48
+ ```bash
49
+ zibby app upgrade a1b2c3d4
50
+ zibby app upgrade a1b2c3d4 --version 0.1.16 # pin a specific agent-ops version
51
+ ```
52
+
53
+ Behind the scenes:
54
+
55
+ 1. Register a new task definition revision (same image, same volume, same env)
56
+ 2. Update the ECS service with the new revision
57
+ 3. ALB drains old tasks while new ones come up; the listener serves the new tasks once they pass health checks
58
+ 4. Old tasks shut down
59
+
60
+ A load-bearing n8n stays serving traffic the whole time. `--yes` skips the confirmation prompt for automation.
61
+
62
+ ## Restart
63
+
64
+ ```bash
65
+ zibby app restart a1b2c3d4
66
+ ```
67
+
68
+ Forces the ECS service to roll the current tasks — useful when an app gets wedged on a stuck connection and you don't want a full upgrade.
69
+
70
+ ## Rotate credentials
71
+
72
+ For BYOK apps (e.g. open-webui pointing at Anthropic via your own key):
73
+
74
+ ```bash
75
+ zibby app update-credential a1b2c3d4
76
+ ```
77
+
78
+ This picks up whatever's currently in your workspace credentials (set via [Settings → Workspace credentials](https://studio.zibby.dev/settings/workspace) or `zibby creds set`) and rolls the task with the new secret env. EFS data is preserved; the task restarts in ~30s.
79
+
80
+ ## ENV vars
81
+
82
+ Every app instance has a per-instance encrypted env-var bag, same shape as workflow env. Use it for per-instance config (e.g. `N8N_ENCRYPTION_KEY`, `DATABASE_URL` pointing at an external RDS).
83
+
84
+ Set via the dashboard (Apps → instance → ENV tab) or via CLI:
85
+
86
+ ```bash
87
+ zibby app env list a1b2c3d4
88
+ zibby app env set a1b2c3d4 N8N_HOST=automations.acme.com
89
+ zibby app env unset a1b2c3d4 OLD_FLAG
90
+ ```
91
+
92
+ Changes apply on the next task restart. Use `zibby app restart` to roll immediately.
93
+
94
+ ## Resource overrides
95
+
96
+ Default resources come from the catalog entry's tier. To bump CPU / memory for one instance:
97
+
98
+ ```bash
99
+ zibby app deploy n8n --project <id> --cpu 1024 --memory 2048 # 1 vCPU / 2 GB
100
+ ```
101
+
102
+ Per-instance overrides survive upgrades; the upgrade flow re-registers the task definition with the same override values unless `--reset-resources` is passed.
103
+
104
+ ## Destroy
105
+
106
+ ```bash
107
+ zibby app destroy a1b2c3d4
108
+ zibby app destroy a1b2c3d4 --yes # skip confirmation
109
+ ```
110
+
111
+ This:
112
+
113
+ 1. Drains the ECS service (in-flight requests finish)
114
+ 2. Deletes the service + task definition revision
115
+ 3. Removes the ALB listener rule + target group
116
+ 4. Releases the EFS access point — **destroys the volume data permanently**
117
+ 5. Stops the billing meter immediately
118
+
119
+ There's no soft-delete. If you might want the data later, snapshot it externally first (or wait for the pause-without-destroy feature on the roadmap).
120
+
121
+ → Next: [Agent operator](./agent-ops)