@zibby/skills 0.1.32 → 0.1.34

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.
Files changed (71) hide show
  1. package/dist/chat-memory.js +29 -27
  2. package/dist/chat-notify.js +3 -3
  3. package/dist/github.js +4 -3
  4. package/dist/gitlab.js +19 -0
  5. package/dist/index.js +141 -120
  6. package/dist/integrations.js +1 -1
  7. package/dist/jira.js +2 -2
  8. package/dist/lark.js +1 -1
  9. package/dist/linear.js +14 -14
  10. package/dist/llm-billing.js +1 -1
  11. package/dist/package.json +3 -1
  12. package/dist/plane.js +1 -1
  13. package/dist/sentry.js +2 -2
  14. package/dist/slack.js +1 -1
  15. package/dist/trackers/github-adapter.js +4 -3
  16. package/dist/trackers/index.js +13 -12
  17. package/dist/trackers/jira-adapter.js +1 -1
  18. package/dist/trackers/linear-adapter.js +16 -16
  19. package/docs/apps/agent-ops.md +6 -6
  20. package/docs/apps/deploy.md +1 -1
  21. package/docs/apps/index.md +45 -42
  22. package/docs/apps/managing.md +1 -1
  23. package/docs/cli-reference.md +67 -67
  24. package/docs/cloning-repositories.md +9 -9
  25. package/docs/cloud/bundles.md +8 -8
  26. package/docs/cloud/dedicated-egress.md +6 -6
  27. package/docs/cloud/env-vars.md +29 -29
  28. package/docs/cloud/limits.md +11 -11
  29. package/docs/cloud/triggering.md +16 -16
  30. package/docs/concepts/agents.md +4 -4
  31. package/docs/concepts/sessions.md +7 -7
  32. package/docs/concepts/state.md +1 -1
  33. package/docs/concepts/sub-graphs.md +9 -9
  34. package/docs/get-started/deploy.md +14 -14
  35. package/docs/get-started/install.md +5 -3
  36. package/docs/get-started/run-locally.md +12 -12
  37. package/docs/get-started/trigger-and-logs.md +14 -14
  38. package/docs/get-started/use-from-agents.md +17 -17
  39. package/docs/get-started/your-first-workflow.md +10 -7
  40. package/docs/integrations/gitlab.md +43 -0
  41. package/docs/integrations/lark.md +41 -0
  42. package/docs/integrations/linear.md +43 -0
  43. package/docs/integrations/notion.md +33 -0
  44. package/docs/integrations/plane.md +46 -0
  45. package/docs/integrations/sentry.md +42 -0
  46. package/docs/integrations/slack.md +33 -0
  47. package/docs/intro.md +16 -12
  48. package/docs/legacy/test-automation.md +2 -2
  49. package/docs/packages/cli.md +11 -11
  50. package/docs/packages/core.md +2 -2
  51. package/docs/packages/mcp-cli.md +18 -18
  52. package/docs/packages/skills.md +2 -2
  53. package/docs/packages/ui-memory.md +2 -2
  54. package/docs/recipes/bug-autofix.md +85 -0
  55. package/docs/recipes/github-ai-scout.md +61 -0
  56. package/docs/recipes/index.md +39 -34
  57. package/docs/recipes/pipeline-supervisor.md +57 -0
  58. package/docs/recipes/sentry-triage.md +7 -7
  59. package/docs/recipes/test.md +6 -6
  60. package/docs/skills/browser.md +2 -2
  61. package/docs/skills/chat-memory.md +40 -11
  62. package/docs/skills/core-tools.md +1 -1
  63. package/docs/skills/function-skill.md +1 -1
  64. package/docs/skills/github.md +2 -2
  65. package/docs/skills/index.md +4 -0
  66. package/docs/skills/jira.md +1 -1
  67. package/docs/skills/lark.md +1 -1
  68. package/docs/skills/memory.md +2 -2
  69. package/docs/skills/sentry.md +1 -1
  70. package/docs/skills/slack.md +2 -2
  71. package/package.json +3 -1
@@ -1,4 +1,4 @@
1
- var S=process.env.LINEAR_API_URL||"https://api.linear.app/graphql";function L(){if(process.env.LINEAR_OAUTH_TOKEN)return`Bearer ${process.env.LINEAR_OAUTH_TOKEN}`;let s=process.env.LINEAR_API_KEY;if(!s)throw new Error("Linear is not connected: set LINEAR_API_KEY (personal API key) or LINEAR_OAUTH_TOKEN.");return s}async function f(s,i={}){let t=await fetch(S,{method:"POST",headers:{Authorization:L(),"Content-Type":"application/json"},body:JSON.stringify({query:s,variables:i})});if(!t.ok){let a=await t.text().catch(()=>"");throw new Error(`Linear API ${t.status}: ${a.slice(0,300)}`)}let e=await t.json().catch(()=>null);if(!e)throw new Error("Linear API returned a non-JSON body");if(Array.isArray(e.errors)&&e.errors.length){let a=e.errors.map(n=>n?.message||String(n)).join("; ");throw new Error(`Linear GraphQL error: ${a.slice(0,300)}`)}return e.data}function h(s){return String(s||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function E(s,i){let t=h(s),e=h(i);if(!t||!e)return 0;if(t===e)return 1;if(t.length===1||e.length===1)return t===e?1:0;let a=d=>{let c=new Map;for(let y=0;y<d.length-1;y++){let g=d.slice(y,y+2);c.set(g,(c.get(g)||0)+1)}return c},n=a(t),r=a(e),o=0,l=0,m=0;for(let d of n.values())l+=d;for(let d of r.values())m+=d;for(let[d,c]of n.entries())o+=Math.min(c,r.get(d)||0);return 2*o/Math.max(1,l+m)}function v(s,i){let t=Array.isArray(s)?s:[];if(!t.length)return{state:null,strategy:"no-states"};let e=h(i);if(!e)return{state:null,strategy:"no-target"};let a=t.find(o=>h(o.name)===e);if(a)return{state:a,strategy:"exact"};let n={backlog:["backlog"],unstarted:["todo","unstarted","open"],started:["inprogress","started","doing","wip"],completed:["done","completed","closed","resolved","fixed"],canceled:["canceled","cancelled","wontfix","won'tfix"],triage:["triage"]};for(let[o,l]of Object.entries(n)){if(!l.some(d=>h(d)===e))continue;let m=t.find(d=>d.type===o);if(m)return{state:m,strategy:"type-alias"}}let r=t.map(o=>({s:o,score:E(i,o.name)})).sort((o,l)=>l.score-o.score);return r[0]&&r[0].score>=.5?{state:r[0].s,strategy:"fuzzy"}:{state:null,strategy:"no-match"}}var O=`
1
+ var k=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",PLANE:"plane",LINEAR:"linear"}),C=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"},plane:{id:"plane",name:"Plane",connectPath:"/integrations?provider=plane"},linear:{id:"linear",name:"Linear",connectPath:"/integrations?provider=linear"}});var S=process.env.LINEAR_API_URL||"https://api.linear.app/graphql";function E(){if(process.env.LINEAR_OAUTH_TOKEN)return`Bearer ${process.env.LINEAR_OAUTH_TOKEN}`;let n=process.env.LINEAR_API_KEY;if(!n)throw new Error("Linear is not connected: set LINEAR_API_KEY (personal API key) or LINEAR_OAUTH_TOKEN.");return n}async function f(n,i={}){let t=await fetch(S,{method:"POST",headers:{Authorization:E(),"Content-Type":"application/json"},body:JSON.stringify({query:n,variables:i})});if(!t.ok){let s=await t.text().catch(()=>"");throw new Error(`Linear API ${t.status}: ${s.slice(0,300)}`)}let e=await t.json().catch(()=>null);if(!e)throw new Error("Linear API returned a non-JSON body");if(Array.isArray(e.errors)&&e.errors.length){let s=e.errors.map(a=>a?.message||String(a)).join("; ");throw new Error(`Linear GraphQL error: ${s.slice(0,300)}`)}return e.data}function h(n){return String(n||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function v(n,i){let t=h(n),e=h(i);if(!t||!e)return 0;if(t===e)return 1;if(t.length===1||e.length===1)return t===e?1:0;let s=d=>{let c=new Map;for(let y=0;y<d.length-1;y++){let g=d.slice(y,y+2);c.set(g,(c.get(g)||0)+1)}return c},a=s(t),r=s(e),o=0,l=0,m=0;for(let d of a.values())l+=d;for(let d of r.values())m+=d;for(let[d,c]of a.entries())o+=Math.min(c,r.get(d)||0);return 2*o/Math.max(1,l+m)}function T(n,i){let t=Array.isArray(n)?n:[];if(!t.length)return{state:null,strategy:"no-states"};let e=h(i);if(!e)return{state:null,strategy:"no-target"};let s=t.find(o=>h(o.name)===e);if(s)return{state:s,strategy:"exact"};let a={backlog:["backlog"],unstarted:["todo","unstarted","open"],started:["inprogress","started","doing","wip"],completed:["done","completed","closed","resolved","fixed"],canceled:["canceled","cancelled","wontfix","won'tfix"],triage:["triage"]};for(let[o,l]of Object.entries(a)){if(!l.some(d=>h(d)===e))continue;let m=t.find(d=>d.type===o);if(m)return{state:m,strategy:"type-alias"}}let r=t.map(o=>({s:o,score:v(i,o.name)})).sort((o,l)=>l.score-o.score);return r[0]&&r[0].score>=.5?{state:r[0].s,strategy:"fuzzy"}:{state:null,strategy:"no-match"}}var O=`
2
2
  id
3
3
  identifier
4
4
  number
@@ -12,7 +12,7 @@ var S=process.env.LINEAR_API_URL||"https://api.linear.app/graphql";function L(){
12
12
  assignee { id name displayName email }
13
13
  labels { nodes { id name color } }
14
14
  team { id key name }
15
- `,_={id:"linear",serverName:"linear",allowedTools:["mcp__linear__*"],envKeys:["LINEAR_API_KEY","LINEAR_OAUTH_TOKEN"],description:"Linear \u2014 issues, comments, workflow states (GraphQL API key)",promptFragment:`## Linear (connected)
15
+ `,I={id:"linear",serverName:"linear",allowedTools:["mcp__linear__*"],requiresIntegration:k.LINEAR,envKeys:["LINEAR_API_KEY","LINEAR_OAUTH_TOKEN"],description:"Linear \u2014 issues, comments, workflow states (GraphQL API key)",promptFragment:`## Linear (connected)
16
16
  You have direct access to the user's Linear workspace (GraphQL API). Tools:
17
17
 
18
18
  ### Discovery
@@ -30,32 +30,32 @@ You have direct access to the user's Linear workspace (GraphQL API). Tools:
30
30
 
31
31
  ### Notes
32
32
  - Always resolve a team first when you need states or want to create/move issues by state name \u2014 states only make sense within their team.
33
- - Issue identifier (ENG-123) and internal id (uuid) are both accepted by get/update tools.`,resolve(){let s={};for(let i of this.envKeys)process.env[i]&&(s[i]=process.env[i]);return process.env.LINEAR_API_URL&&(s.LINEAR_API_URL=process.env.LINEAR_API_URL),{command:null,args:[],env:s,description:this.description}},async handleToolCall(s,i){try{switch(s){case"linear_list_teams":{let e=(await f(`
33
+ - Issue identifier (ENG-123) and internal id (uuid) are both accepted by get/update tools.`,resolve(){let n={};for(let i of this.envKeys)process.env[i]&&(n[i]=process.env[i]);return process.env.LINEAR_API_URL&&(n.LINEAR_API_URL=process.env.LINEAR_API_URL),{command:null,args:[],env:n,description:this.description}},async handleToolCall(n,i){try{switch(n){case"linear_list_teams":{let e=(await f(`
34
34
  query Teams($first: Int) {
35
35
  teams(first: $first) {
36
36
  nodes { id key name description }
37
37
  }
38
38
  }
39
- `,{first:i?.limit||50}))?.teams?.nodes||[];return JSON.stringify({count:e.length,teams:e})}case"linear_list_states":{let{teamId:t,teamKey:e}=i||{},a=t;if(!a&&e&&(a=await k(e)),a){let l=(await f(`
39
+ `,{first:i?.limit||50}))?.teams?.nodes||[];return JSON.stringify({count:e.length,teams:e})}case"linear_list_states":{let{teamId:t,teamKey:e}=i||{},s=t;if(!s&&e&&(s=await N(e)),s){let l=(await f(`
40
40
  query States($teamId: String!) {
41
41
  team(id: $teamId) {
42
42
  id key name
43
43
  states { nodes { id name type color position } }
44
44
  }
45
45
  }
46
- `,{teamId:a}))?.team,m=(l?.states?.nodes||[]).slice().sort((d,c)=>(d.position||0)-(c.position||0));return JSON.stringify({team:l?{id:l.id,key:l.key,name:l.name}:null,count:m.length,states:m})}let r=(await f(`
46
+ `,{teamId:s}))?.team,m=(l?.states?.nodes||[]).slice().sort((d,c)=>(d.position||0)-(c.position||0));return JSON.stringify({team:l?{id:l.id,key:l.key,name:l.name}:null,count:m.length,states:m})}let r=(await f(`
47
47
  query AllStates($first: Int) {
48
48
  workflowStates(first: $first) {
49
49
  nodes { id name type color team { id key name } }
50
50
  }
51
51
  }
52
- `,{first:i?.limit||200}))?.workflowStates?.nodes||[];return JSON.stringify({scope:"workspace",count:r.length,states:r})}case"linear_list_labels":{let{teamId:t}=i||{},a=(await f(`
52
+ `,{first:i?.limit||200}))?.workflowStates?.nodes||[];return JSON.stringify({scope:"workspace",count:r.length,states:r})}case"linear_list_labels":{let{teamId:t}=i||{},s=(await f(`
53
53
  query Labels($first: Int, $filter: IssueLabelFilter) {
54
54
  issueLabels(first: $first, filter: $filter) {
55
55
  nodes { id name color team { id key } }
56
56
  }
57
57
  }
58
- `,{first:i?.limit||100,filter:t?{team:{id:{eq:t}}}:void 0}))?.issueLabels?.nodes||[];return JSON.stringify({count:a.length,labels:a})}case"linear_list_issues":{let{teamId:t,teamKey:e,stateId:a,stateName:n,label:r,assigneeId:o,updatedAfter:l,limit:m}=i||{},d={},c=t;!c&&e&&(c=await k(e)),c&&(d.team={id:{eq:c}}),a?d.state={id:{eq:a}}:n&&(d.state={name:{eqIgnoreCase:n}}),r&&(d.labels={name:{eqIgnoreCase:r}}),o&&(d.assignee={id:{eq:o}}),l&&(d.updatedAt={gt:l});let g=((await f(`
58
+ `,{first:i?.limit||100,filter:t?{team:{id:{eq:t}}}:void 0}))?.issueLabels?.nodes||[];return JSON.stringify({count:s.length,labels:s})}case"linear_list_issues":{let{teamId:t,teamKey:e,stateId:s,stateName:a,label:r,assigneeId:o,updatedAfter:l,limit:m}=i||{},d={},c=t;!c&&e&&(c=await N(e)),c&&(d.team={id:{eq:c}}),s?d.state={id:{eq:s}}:a&&(d.state={name:{eqIgnoreCase:a}}),r&&(d.labels={name:{eqIgnoreCase:r}}),o&&(d.assignee={id:{eq:o}}),l&&(d.updatedAt={gt:l});let g=((await f(`
59
59
  query Issues($first: Int, $filter: IssueFilter, $orderBy: PaginationOrderBy) {
60
60
  issues(first: $first, filter: $filter, orderBy: $orderBy) {
61
61
  nodes {
@@ -67,48 +67,48 @@ You have direct access to the user's Linear workspace (GraphQL API). Tools:
67
67
  }
68
68
  }
69
69
  }
70
- `,{first:m||30,filter:Object.keys(d).length?d:void 0,orderBy:"updatedAt"}))?.issues?.nodes||[]).map(u=>({id:u.id,identifier:u.identifier,number:u.number,title:u.title,url:u.url,priority:u.priority,state:u.state?.name,stateType:u.state?.type,assignee:u.assignee?.displayName||null,labels:(u.labels?.nodes||[]).map(p=>p.name),team:u.team?.key,createdAt:u.createdAt,updatedAt:u.updatedAt}));return JSON.stringify({count:g.length,issues:g})}case"linear_get_issue":{let t=i?.issueId||i?.identifier||i?.issueKey;if(!t)return JSON.stringify({error:"issueId or identifier is required"});let e=await I(t);return JSON.stringify(e?{id:e.id,identifier:e.identifier,number:e.number,title:e.title,description:e.description||"",url:e.url,priority:e.priority,state:e.state?.name,stateId:e.state?.id,stateType:e.state?.type,assignee:e.assignee?.displayName||e.assignee?.name||null,assigneeId:e.assignee?.id||null,labels:(e.labels?.nodes||[]).map(a=>a.name),team:e.team?{id:e.team.id,key:e.team.key,name:e.team.name}:null,createdAt:e.createdAt,updatedAt:e.updatedAt}:{error:`Issue not found: ${t}`})}case"linear_get_comments":{let t=i?.issueId||i?.identifier||i?.issueKey;if(!t)return JSON.stringify({error:"issueId or identifier is required"});let e=await I(t,`
70
+ `,{first:m||30,filter:Object.keys(d).length?d:void 0,orderBy:"updatedAt"}))?.issues?.nodes||[]).map(u=>({id:u.id,identifier:u.identifier,number:u.number,title:u.title,url:u.url,priority:u.priority,state:u.state?.name,stateType:u.state?.type,assignee:u.assignee?.displayName||null,labels:(u.labels?.nodes||[]).map(p=>p.name),team:u.team?.key,createdAt:u.createdAt,updatedAt:u.updatedAt}));return JSON.stringify({count:g.length,issues:g})}case"linear_get_issue":{let t=i?.issueId||i?.identifier||i?.issueKey;if(!t)return JSON.stringify({error:"issueId or identifier is required"});let e=await _(t);return JSON.stringify(e?{id:e.id,identifier:e.identifier,number:e.number,title:e.title,description:e.description||"",url:e.url,priority:e.priority,state:e.state?.name,stateId:e.state?.id,stateType:e.state?.type,assignee:e.assignee?.displayName||e.assignee?.name||null,assigneeId:e.assignee?.id||null,labels:(e.labels?.nodes||[]).map(s=>s.name),team:e.team?{id:e.team.id,key:e.team.key,name:e.team.name}:null,createdAt:e.createdAt,updatedAt:e.updatedAt}:{error:`Issue not found: ${t}`})}case"linear_get_comments":{let t=i?.issueId||i?.identifier||i?.issueKey;if(!t)return JSON.stringify({error:"issueId or identifier is required"});let e=await _(t,`
71
71
  id identifier
72
72
  comments(first: ${Number(i?.limit)||50}) {
73
73
  nodes { id body createdAt updatedAt user { id name displayName } }
74
74
  }
75
- `);if(!e)return JSON.stringify({error:`Issue not found: ${t}`});let a=(e.comments?.nodes||[]).map(n=>({id:n.id,author:n.user?.displayName||n.user?.name||"Unknown",body:n.body||"",createdAt:n.createdAt,updatedAt:n.updatedAt})).sort((n,r)=>String(r.createdAt).localeCompare(String(n.createdAt)));return JSON.stringify({count:a.length,issue:e.identifier,comments:a})}case"linear_add_comment":{let t=i?.issueId||i?.identifier||i?.issueKey,e=i?.body;if(!t||!e)return JSON.stringify({error:"issueId/identifier and body are required"});let a=await I(t,"id identifier");if(!a)return JSON.stringify({error:`Issue not found: ${t}`});let r=(await f(`
75
+ `);if(!e)return JSON.stringify({error:`Issue not found: ${t}`});let s=(e.comments?.nodes||[]).map(a=>({id:a.id,author:a.user?.displayName||a.user?.name||"Unknown",body:a.body||"",createdAt:a.createdAt,updatedAt:a.updatedAt})).sort((a,r)=>String(r.createdAt).localeCompare(String(a.createdAt)));return JSON.stringify({count:s.length,issue:e.identifier,comments:s})}case"linear_add_comment":{let t=i?.issueId||i?.identifier||i?.issueKey,e=i?.body;if(!t||!e)return JSON.stringify({error:"issueId/identifier and body are required"});let s=await _(t,"id identifier");if(!s)return JSON.stringify({error:`Issue not found: ${t}`});let r=(await f(`
76
76
  mutation AddComment($input: CommentCreateInput!) {
77
77
  commentCreate(input: $input) {
78
78
  success
79
79
  comment { id url createdAt }
80
80
  }
81
81
  }
82
- `,{input:{issueId:a.id,body:e}}))?.commentCreate;return JSON.stringify({ok:!!r?.success,commentId:r?.comment?.id,url:r?.comment?.url})}case"linear_update_state":{let t=i?.issueId||i?.identifier||i?.issueKey,{stateId:e,stateName:a,toStatus:n,status:r}=i||{};if(!t)return JSON.stringify({error:"issueId or identifier is required"});let o=await I(t,`
82
+ `,{input:{issueId:s.id,body:e}}))?.commentCreate;return JSON.stringify({ok:!!r?.success,commentId:r?.comment?.id,url:r?.comment?.url})}case"linear_update_state":{let t=i?.issueId||i?.identifier||i?.issueKey,{stateId:e,stateName:s,toStatus:a,status:r}=i||{};if(!t)return JSON.stringify({error:"issueId or identifier is required"});let o=await _(t,`
83
83
  id identifier
84
84
  state { id name type }
85
85
  team { id key states { nodes { id name type position } } }
86
- `);if(!o)return JSON.stringify({error:`Issue not found: ${t}`});let l=e,m=e?{strategy:"explicit-id"}:null;if(!l){let y=String(a||n||r||"").trim(),g=(o.team?.states?.nodes||[]).slice().sort((p,N)=>(p.position||0)-(N.position||0));if(!y)return JSON.stringify({ok:!1,error:"stateId or stateName/toStatus is required",issue:o.identifier,availableStates:g.map(p=>({id:p.id,name:p.name,type:p.type}))});let u=v(g,y);if(!u.state)return JSON.stringify({ok:!1,error:`No workflow state matches "${y}" in team ${o.team?.key}`,issue:o.identifier,availableStates:g.map(p=>({id:p.id,name:p.name,type:p.type}))});l=u.state.id,m={strategy:u.strategy,matchedName:u.state.name}}let c=(await f(`
86
+ `);if(!o)return JSON.stringify({error:`Issue not found: ${t}`});let l=e,m=e?{strategy:"explicit-id"}:null;if(!l){let y=String(s||a||r||"").trim(),g=(o.team?.states?.nodes||[]).slice().sort((p,L)=>(p.position||0)-(L.position||0));if(!y)return JSON.stringify({ok:!1,error:"stateId or stateName/toStatus is required",issue:o.identifier,availableStates:g.map(p=>({id:p.id,name:p.name,type:p.type}))});let u=T(g,y);if(!u.state)return JSON.stringify({ok:!1,error:`No workflow state matches "${y}" in team ${o.team?.key}`,issue:o.identifier,availableStates:g.map(p=>({id:p.id,name:p.name,type:p.type}))});l=u.state.id,m={strategy:u.strategy,matchedName:u.state.name}}let c=(await f(`
87
87
  mutation MoveIssue($id: String!, $input: IssueUpdateInput!) {
88
88
  issueUpdate(id: $id, input: $input) {
89
89
  success
90
90
  issue { id identifier state { id name type } }
91
91
  }
92
92
  }
93
- `,{id:o.id,input:{stateId:l}}))?.issueUpdate;return JSON.stringify({ok:!!c?.success,issue:c?.issue?.identifier||o.identifier,stateAfter:c?.issue?.state?.name||null,stateTypeAfter:c?.issue?.state?.type||null,resolution:m})}case"linear_link_attachment":{let t=i?.issueId||i?.identifier||i?.issueKey,{url:e,title:a,subtitle:n}=i||{};if(!t||!e)return JSON.stringify({error:"issueId/identifier and url are required"});let r=await I(t,"id identifier");if(!r)return JSON.stringify({error:`Issue not found: ${t}`});let l=(await f(`
93
+ `,{id:o.id,input:{stateId:l}}))?.issueUpdate;return JSON.stringify({ok:!!c?.success,issue:c?.issue?.identifier||o.identifier,stateAfter:c?.issue?.state?.name||null,stateTypeAfter:c?.issue?.state?.type||null,resolution:m})}case"linear_link_attachment":{let t=i?.issueId||i?.identifier||i?.issueKey,{url:e,title:s,subtitle:a}=i||{};if(!t||!e)return JSON.stringify({error:"issueId/identifier and url are required"});let r=await _(t,"id identifier");if(!r)return JSON.stringify({error:`Issue not found: ${t}`});let l=(await f(`
94
94
  mutation LinkAttachment($input: AttachmentCreateInput!) {
95
95
  attachmentCreate(input: $input) {
96
96
  success
97
97
  attachment { id url title }
98
98
  }
99
99
  }
100
- `,{input:{issueId:r.id,url:e,title:a||e,subtitle:n||void 0}}))?.attachmentCreate;return JSON.stringify({ok:!!l?.success,attachmentId:l?.attachment?.id,url:l?.attachment?.url})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"linear_list_teams",description:"List Linear teams (id, key, name). Needed to scope workflow states and issue queries.",input_schema:{type:"object",properties:{limit:{type:"number",description:"Max teams (default: 50)"}}}},{name:"linear_list_states",description:"List a team's workflow states (id, name, type: backlog|unstarted|started|completed|canceled|triage). Linear states are PER-TEAM. Omit team to list all states across the workspace.",input_schema:{type:"object",properties:{teamId:{type:"string",description:"Team uuid"},teamKey:{type:"string",description:"Team key (e.g. ENG); resolved to an id if teamId omitted"},limit:{type:"number",description:"Max states when listing workspace-wide (default: 200)"}}}},{name:"linear_list_labels",description:"List issue labels, optionally scoped to a team.",input_schema:{type:"object",properties:{teamId:{type:"string",description:"Optional team uuid to scope labels"},limit:{type:"number",description:"Max labels (default: 100)"}}}},{name:"linear_list_issues",description:"List/poll Linear issues filtered by team, state, label, assignee, and an updatedAfter cursor. Returns newest-updated first.",input_schema:{type:"object",properties:{teamId:{type:"string",description:"Team uuid"},teamKey:{type:"string",description:"Team key (e.g. ENG); resolved if teamId omitted"},stateId:{type:"string",description:"Filter by workflow state uuid"},stateName:{type:"string",description:"Filter by state name (case-insensitive)"},label:{type:"string",description:"Filter by label name (case-insensitive)"},assigneeId:{type:"string",description:"Filter by assignee uuid"},updatedAfter:{type:"string",description:"ISO-8601 timestamp; only issues updated after this (polling cursor)"},limit:{type:"number",description:"Max issues (default: 30)"}}}},{name:"linear_get_issue",description:"Get a single Linear issue by identifier (e.g. ENG-123) or internal uuid \u2014 title, description, state, labels, assignee, url.",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"}}}},{name:"linear_get_comments",description:"Get comments on a Linear issue (newest first).",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},limit:{type:"number",description:"Max comments (default: 50)"}}}},{name:"linear_add_comment",description:"Add a comment to a Linear issue (markdown supported).",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},body:{type:"string",description:"Comment body (markdown)"}},required:["body"]}},{name:"linear_update_state",description:"Move a Linear issue to a different workflow state. Pass a state NAME (toStatus/stateName) and the tool resolves it to the issue's team's matching state id (exact -> type-alias -> fuzzy), or pass stateId directly. Linear has no transitions \u2014 this sets the state.",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},stateId:{type:"string",description:"Target workflow state uuid (skips name resolution)"},stateName:{type:"string",description:'Target state name (e.g. "In Progress", "Done")'},toStatus:{type:"string",description:"Alias for stateName"}}}},{name:"linear_link_attachment",description:"Attach a URL (e.g. a GitHub PR) to a Linear issue via native attachments. Use this for PR links; fall back to linear_add_comment if it fails.",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},url:{type:"string",description:"The URL to attach (e.g. a PR link)"},title:{type:"string",description:"Attachment title (defaults to the URL)"},subtitle:{type:"string",description:"Optional attachment subtitle"}},required:["url"]}}]};async function k(s){return(await f(`
100
+ `,{input:{issueId:r.id,url:e,title:s||e,subtitle:a||void 0}}))?.attachmentCreate;return JSON.stringify({ok:!!l?.success,attachmentId:l?.attachment?.id,url:l?.attachment?.url})}default:return JSON.stringify({error:`Unknown tool: ${n}`})}}catch(t){return JSON.stringify({error:t.message})}},tools:[{name:"linear_list_teams",description:"List Linear teams (id, key, name). Needed to scope workflow states and issue queries.",input_schema:{type:"object",properties:{limit:{type:"number",description:"Max teams (default: 50)"}}}},{name:"linear_list_states",description:"List a team's workflow states (id, name, type: backlog|unstarted|started|completed|canceled|triage). Linear states are PER-TEAM. Omit team to list all states across the workspace.",input_schema:{type:"object",properties:{teamId:{type:"string",description:"Team uuid"},teamKey:{type:"string",description:"Team key (e.g. ENG); resolved to an id if teamId omitted"},limit:{type:"number",description:"Max states when listing workspace-wide (default: 200)"}}}},{name:"linear_list_labels",description:"List issue labels, optionally scoped to a team.",input_schema:{type:"object",properties:{teamId:{type:"string",description:"Optional team uuid to scope labels"},limit:{type:"number",description:"Max labels (default: 100)"}}}},{name:"linear_list_issues",description:"List/poll Linear issues filtered by team, state, label, assignee, and an updatedAfter cursor. Returns newest-updated first.",input_schema:{type:"object",properties:{teamId:{type:"string",description:"Team uuid"},teamKey:{type:"string",description:"Team key (e.g. ENG); resolved if teamId omitted"},stateId:{type:"string",description:"Filter by workflow state uuid"},stateName:{type:"string",description:"Filter by state name (case-insensitive)"},label:{type:"string",description:"Filter by label name (case-insensitive)"},assigneeId:{type:"string",description:"Filter by assignee uuid"},updatedAfter:{type:"string",description:"ISO-8601 timestamp; only issues updated after this (polling cursor)"},limit:{type:"number",description:"Max issues (default: 30)"}}}},{name:"linear_get_issue",description:"Get a single Linear issue by identifier (e.g. ENG-123) or internal uuid \u2014 title, description, state, labels, assignee, url.",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"}}}},{name:"linear_get_comments",description:"Get comments on a Linear issue (newest first).",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},limit:{type:"number",description:"Max comments (default: 50)"}}}},{name:"linear_add_comment",description:"Add a comment to a Linear issue (markdown supported).",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},body:{type:"string",description:"Comment body (markdown)"}},required:["body"]}},{name:"linear_update_state",description:"Move a Linear issue to a different workflow state. Pass a state NAME (toStatus/stateName) and the tool resolves it to the issue's team's matching state id (exact -> type-alias -> fuzzy), or pass stateId directly. Linear has no transitions \u2014 this sets the state.",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},stateId:{type:"string",description:"Target workflow state uuid (skips name resolution)"},stateName:{type:"string",description:'Target state name (e.g. "In Progress", "Done")'},toStatus:{type:"string",description:"Alias for stateName"}}}},{name:"linear_link_attachment",description:"Attach a URL (e.g. a GitHub PR) to a Linear issue via native attachments. Use this for PR links; fall back to linear_add_comment if it fails.",input_schema:{type:"object",properties:{identifier:{type:"string",description:"Issue identifier, e.g. ENG-123"},issueId:{type:"string",description:"Internal issue uuid (alternative to identifier)"},url:{type:"string",description:"The URL to attach (e.g. a PR link)"},title:{type:"string",description:"Attachment title (defaults to the URL)"},subtitle:{type:"string",description:"Optional attachment subtitle"}},required:["url"]}}]};async function N(n){return(await f(`
101
101
  query TeamByKey($filter: TeamFilter) {
102
102
  teams(first: 1, filter: $filter) { nodes { id key } }
103
103
  }
104
- `,{filter:{key:{eq:s}}}))?.teams?.nodes?.[0]?.id||null}async function I(s,i=O){let t=String(s).trim(),e=/^([A-Za-z][A-Za-z0-9]*)-(\d+)$/.exec(t);if(e){let n=e[1].toUpperCase(),r=Number(e[2]);return(await f(`
104
+ `,{filter:{key:{eq:n}}}))?.teams?.nodes?.[0]?.id||null}async function _(n,i=O){let t=String(n).trim(),e=/^([A-Za-z][A-Za-z0-9]*)-(\d+)$/.exec(t);if(e){let a=e[1].toUpperCase(),r=Number(e[2]);return(await f(`
105
105
  query IssueByIdentifier($filter: IssueFilter) {
106
106
  issues(first: 1, filter: $filter) {
107
107
  nodes { ${i} }
108
108
  }
109
109
  }
110
- `,{filter:{number:{eq:r},team:{key:{eq:n}}}}))?.issues?.nodes?.[0]||null}return(await f(`
110
+ `,{filter:{number:{eq:r},team:{key:{eq:a}}}}))?.issues?.nodes?.[0]||null}return(await f(`
111
111
  query IssueById($id: String!) {
112
112
  issue(id: $id) { ${i} }
113
113
  }
114
- `,{id:t}))?.issue||null}var T={triage:"todo",backlog:"todo",unstarted:"todo",started:"in_progress",completed:"done",canceled:"done"},$=/\b(blocked|on[\s-]?hold|waiting|stuck)\b/i;function A(s){return s?s.name&&$.test(s.name)?"blocked":T[s.type]||"unknown":"unknown"}function w(s){let i=JSON.parse(s);if(i&&i.error)throw new Error(i.error);return i}function b(s){let i=s.state||null,t=s.stateType||null;return{id:String(s.id||s.identifier||""),key:s.identifier||String(s.id||""),title:s.title||"",body:s.description||"",state:i,stateCategory:A(i?{name:i,type:t}:null),assignee:s.assignee||null,url:s.url||null,_raw:s}}var C={id:"linear",toStateCategory:A,toNeutral:b,async listCandidates(s={}){let i=s.ctx||{},t={teamId:i.teamId,teamKey:i.teamKey,stateName:s.state,label:Array.isArray(s.labels)?s.labels[0]:s.labels,assigneeId:i.assigneeId,updatedAfter:s.updatedAfter,limit:s.limit};return(w(await _.handleToolCall("linear_list_issues",t)).issues||[]).map(b)},async getTicket(s){if(!s)throw new Error("key is required");let i=JSON.parse(await _.handleToolCall("linear_get_issue",{identifier:s}));return i.error?null:b(i)},async getComments(s){if(!s)throw new Error("key is required");return(w(await _.handleToolCall("linear_get_comments",{identifier:s})).comments||[]).map(t=>({id:String(t.id),author:t.author||"Unknown",body:t.body||"",createdAt:t.createdAt||null,updatedAt:t.updatedAt||null,_raw:t}))},async addComment(s,i){if(!s||!i)throw new Error("key and body are required");let t=w(await _.handleToolCall("linear_add_comment",{identifier:s,body:i}));return{ok:!!t.ok,id:t.commentId||null}},async transition(s,i){if(!s)throw new Error("key is required");let t=JSON.parse(await _.handleToolCall("linear_update_state",{identifier:s,toStatus:i}));if(!t.ok)return{ok:!1,error:t.error||"state update failed",_raw:t};let e=t.stateAfter||null;return{ok:!0,stateAfter:e,stateCategoryAfter:A(e?{name:e,type:t.stateTypeAfter}:null),_raw:t}},async linkPullRequest(s,i,t){if(!s||!i)throw new Error("key and prUrl are required");try{if(w(await _.handleToolCall("linear_link_attachment",{identifier:s,url:i,title:t||i})).ok)return{ok:!0,via:"attachment"};throw new Error("attachmentCreate returned ok:false")}catch(e){let a=`${t?`${t}: `:"Linked PR: "}${i}`;return{ok:!!w(await _.handleToolCall("linear_add_comment",{identifier:s,body:a})).ok,via:"comment",error:String(e?.message||e)}}}},J=C;export{J as default,C as linearAdapter};
114
+ `,{id:t}))?.issue||null}var $={triage:"todo",backlog:"todo",unstarted:"todo",started:"in_progress",completed:"done",canceled:"done"},R=/\b(blocked|on[\s-]?hold|waiting|stuck)\b/i;function w(n){return n?n.name&&R.test(n.name)?"blocked":$[n.type]||"unknown":"unknown"}function b(n){let i=JSON.parse(n);if(i&&i.error)throw new Error(i.error);return i}function A(n){let i=n.state||null,t=n.stateType||null;return{id:String(n.id||n.identifier||""),key:n.identifier||String(n.id||""),title:n.title||"",body:n.description||"",state:i,stateCategory:w(i?{name:i,type:t}:null),assignee:n.assignee||null,url:n.url||null,_raw:n}}var P={id:"linear",toStateCategory:w,toNeutral:A,async listCandidates(n={}){let i=n.ctx||{},t={teamId:i.teamId,teamKey:i.teamKey,stateName:n.state,label:Array.isArray(n.labels)?n.labels[0]:n.labels,assigneeId:i.assigneeId,updatedAfter:n.updatedAfter,limit:n.limit};return(b(await I.handleToolCall("linear_list_issues",t)).issues||[]).map(A)},async getTicket(n){if(!n)throw new Error("key is required");let i=JSON.parse(await I.handleToolCall("linear_get_issue",{identifier:n}));return i.error?null:A(i)},async getComments(n){if(!n)throw new Error("key is required");return(b(await I.handleToolCall("linear_get_comments",{identifier:n})).comments||[]).map(t=>({id:String(t.id),author:t.author||"Unknown",body:t.body||"",createdAt:t.createdAt||null,updatedAt:t.updatedAt||null,_raw:t}))},async addComment(n,i){if(!n||!i)throw new Error("key and body are required");let t=b(await I.handleToolCall("linear_add_comment",{identifier:n,body:i}));return{ok:!!t.ok,id:t.commentId||null}},async transition(n,i){if(!n)throw new Error("key is required");let t=JSON.parse(await I.handleToolCall("linear_update_state",{identifier:n,toStatus:i}));if(!t.ok)return{ok:!1,error:t.error||"state update failed",_raw:t};let e=t.stateAfter||null;return{ok:!0,stateAfter:e,stateCategoryAfter:w(e?{name:e,type:t.stateTypeAfter}:null),_raw:t}},async linkPullRequest(n,i,t){if(!n||!i)throw new Error("key and prUrl are required");try{if(b(await I.handleToolCall("linear_link_attachment",{identifier:n,url:i,title:t||i})).ok)return{ok:!0,via:"attachment"};throw new Error("attachmentCreate returned ok:false")}catch(e){let s=`${t?`${t}: `:"Linked PR: "}${i}`;return{ok:!!b(await I.handleToolCall("linear_add_comment",{identifier:n,body:s})).ok,via:"comment",error:String(e?.message||e)}}}},K=P;export{K as default,P as linearAdapter};
@@ -19,7 +19,7 @@ This is the structural difference between "deploy button on a VM" and **Zibby**.
19
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
20
  | **Activity log** | every action | One row in the app's "Agent activity" tab, with structured fields you can grep / chart. |
21
21
 
22
- Every action lands in DynamoDB as an `app-runs` record — queryable by anything from a workflow node to a Grafana dashboard.
22
+ Every action lands in DynamoDB as an `app-runs` record — queryable by anything from an agent node to a Grafana dashboard.
23
23
 
24
24
  ## See it in action
25
25
 
@@ -85,20 +85,20 @@ Per-instance agent-ops behavior is tunable via env vars (set on the app instance
85
85
  | `AGENT_OPS_AUTO_UPGRADE` | `false` | If `true`, upgrade automatically when catalog publishes a new version |
86
86
  | `AGENT_OPS_NOTIFY_WEBHOOK` | — | URL to POST run records to (any HTTPS endpoint — your own backend, n8n, etc.) |
87
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.
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 an agent trigger, into Datadog via their webhook receiver, or into your own database.
89
89
 
90
- ## Hooking agent-ops into a workflow
90
+ ## Hooking agent-ops into an agent
91
91
 
92
- The most powerful pattern: a Zibby workflow that runs **on agent-ops events**.
92
+ The most powerful pattern: a Zibby agent that runs **on agent-ops events**.
93
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:
94
+ Example: when an `oom_recovery` fires, run an agent that pulls the container's last-100-lines, classifies the crash, and pages whoever owns this app:
95
95
 
96
96
  ```bash
97
97
  zibby app env set a1b2c3d4 \
98
98
  AGENT_OPS_NOTIFY_WEBHOOK=https://api-prod.zibby.app/v1/workflows/<wf-uuid>/trigger
99
99
  ```
100
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.
101
+ The agent receives the run record as `input`, can call back to `zibby app logs` / `zibby app status`, and decides what to do. Agent-ops + agents compose into a self-operating fleet — humans only get pinged for genuinely novel failure modes.
102
102
 
103
103
  ## Upgrade orchestration
104
104
 
@@ -16,7 +16,7 @@ npm install -g @zibby/cli
16
16
  zibby login # OAuth in browser, saves session to ~/.zibby/config.json
17
17
  ```
18
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.
19
+ You also need a project. If you don't have one yet, deploy an agent 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
20
 
21
21
  ## Browse the catalog
22
22
 
@@ -24,17 +24,17 @@ There are two ways to land a container on the apps fleet, and you pick by **whet
24
24
  | Source | Curated bundle (image + EFS layout + defaults) | Free-form natural-language install |
25
25
  | Time-to-live | ~45-90 s | 2-15 min (Claude writes + runs the install script) |
26
26
  | Licensing | Pre-cleared by Zibby | You direct the install; you accept the upstream license |
27
- | Best for | Anything in the 20-app catalog | n8n, random GitHub project, anything not in the catalog |
27
+ | Best for | Anything in the 22-app catalog | n8n, random GitHub project, anything not in the catalog |
28
28
 
29
29
  Both paths land in the same shape — Fargate task, per-instance EFS volume, ALB target group, agent-ops sidecar — and look identical to every downstream `zibby app logs/status/upgrade` command. The only difference is **who wrote the install recipe**.
30
30
 
31
31
  See [Goal-mode deploys](./goal-mode) for the long form.
32
32
 
33
- ## Why apps (not workflows)
33
+ ## Why apps (not agents)
34
34
 
35
35
  Both are pillars of Zibby Cloud. Pick by **how long the thing needs to run**:
36
36
 
37
- | | **Workflow** | **App** |
37
+ | | **Agent** | **App** |
38
38
  |---|---|---|
39
39
  | Lifetime | Per-trigger (seconds to minutes) | Long-lived (24/7 or paused) |
40
40
  | Surface | A graph of agent CLI calls | A whole open-source application |
@@ -42,7 +42,7 @@ Both are pillars of Zibby Cloud. Pick by **how long the thing needs to run**:
42
42
  | Persistence | Session JSONL + S3 artifacts | Encrypted-at-rest EFS volume |
43
43
  | Best for | "When ticket lands, classify it" | "Host Grafana for the team" |
44
44
 
45
- 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.
45
+ 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 an [Agent](../recipes/).
46
46
 
47
47
  ## What you get with every app
48
48
 
@@ -57,58 +57,61 @@ If you find yourself wanting to **run an open-source web app behind a stable URL
57
57
 
58
58
  ## The catalog
59
59
 
60
- Each catalog entry is a curated bundle: container image, EFS volume layout, ALB wiring, secrets pattern, resource defaults. Today's catalog is **20 apps**, grouped by what they're for:
60
+ Each catalog entry is a curated bundle: container image, EFS volume layout, ALB wiring, secrets pattern, resource defaults. Today's catalog is **22 apps**. Run `zibby app templates` for the canonical, always-up-to-date list with live tier + hourly rate — the sample below is a snapshot, grouped by what each app is for.
61
61
 
62
62
  ### AI
63
63
 
64
- | App | Tier | Rate | What it does |
65
- |---|---|---|---|
66
- | **Open WebUI** | Heavy | $0.25/hr | ChatGPT-style UI for Ollama / OpenAI-compatible endpoints |
67
- | **OpenHands** | Heavy | $0.25/hr | AI software-engineer agent (V1) |
68
- | **Gas Town** | Light | $0.05/hr | Multi-agent workspace — coordinate Claude, Codex, Cursor, Gemini |
64
+ | App | What it does |
65
+ |---|---|
66
+ | **Open WebUI** | ChatGPT-style UI for Ollama / OpenAI-compatible endpoints |
67
+ | **OpenHands** | AI software-engineer agent drives the repo end-to-end, GitHub PR workflows |
68
+ | **Gas Town** | Multi-agent workspace — coordinate Claude, Codex, Cursor, Gemini |
69
+ | **Open Design** | Local-first design-artifact generator via your installed coding-agent CLIs |
70
+ | **Plane** | Self-hosted Jira / Linear alternative — issues, cycles, modules, pages |
69
71
 
70
- ### Data + APIs
72
+ ### Automation
71
73
 
72
- | App | Tier | Rate | What it does |
73
- |---|---|---|---|
74
- | **PostgREST** | Standard | $0.10/hr | Auto-generated REST API on top of any Postgres schema |
75
- | **Mathesar** | Heavy | $0.25/hr | Spreadsheet-style front-end for Postgres |
76
- | **PocketBase** | Light | $0.05/hr | Single-file backend (Auth + DB + file storage + realtime) |
74
+ | App | What it does |
75
+ |---|---|
76
+ | **Activepieces** | Open-source Zapier alternative visual automation, 400+ MCP servers |
77
+ | **ChangeDetection.io** | Watch any web page for changes notifies on diff |
78
+ | **Gotify** | Self-hosted push-notification + webhook server |
77
79
 
78
- ### Knowledge + docs
80
+ ### Data + APIs
79
81
 
80
- | App | Tier | Rate | What it does |
81
- |---|---|---|---|
82
- | **Docmost** | Heavy | $0.25/hr | Wiki + collaboration (multi-service: web + Postgres + Redis) |
83
- | **SiYuan** | Heavy | $0.25/hr | Notion-like knowledge base, local-first |
84
- | **draw.io** | Light | $0.05/hr | Diagrams + flowcharts (client-side editor) |
82
+ | App | What it does |
83
+ |---|---|
84
+ | **PostgREST** | Serverless REST API on top of any Postgres schema |
85
+ | **Mathesar** | Spreadsheet-style web UI for Postgres |
86
+ | **PocketBase** | Single-file backend SQLite + REST + realtime + auth + admin UI |
85
87
 
86
- ### Monitoring + observability
88
+ ### Productivity + docs
87
89
 
88
- | App | Tier | Rate | What it does |
89
- |---|---|---|---|
90
- | **Grafana** | Light | $0.05/hr | Dashboards for metrics, logs, traces |
91
- | **OpenObserve** | Heavy | $0.25/hr | Unified logs + metrics + traces |
92
- | **Uptime Kuma** | Light | $0.05/hr | Self-hosted Pingdom-alt |
93
- | **Beszel** | Light | $0.05/hr | Lightweight single-host server monitor |
94
- | **ChangeDetection.io** | Standard | $0.10/hr | Web-page change watcher |
90
+ | App | What it does |
91
+ |---|---|
92
+ | **Docmost** | Real-time collaborative wiki (multi-service: web + Postgres + Redis) |
93
+ | **SiYuan** | Privacy-first, block-based note-taking / PKM, local-first |
94
+ | **draw.io** | Client-side diagram editor (flowcharts, UML, ER, network) |
95
+ | **Glance** | Self-hosted homepage / dashboard with a feed of RSS, GitHub, monitors |
96
+ | **Homepage** | Self-hosted dashboard with service integrations + bookmarks |
95
97
 
96
- ### Identity
98
+ ### Observability
97
99
 
98
- | App | Tier | Rate | What it does |
99
- |---|---|---|---|
100
- | **Authentik** | Heavy | $0.25/hr | SSO / IdP |
101
- | **Zitadel** | Heavy | $0.25/hr | SSO / IdP (alt) |
100
+ | App | What it does |
101
+ |---|---|
102
+ | **Grafana** | Dashboards for metrics, logs, traces |
103
+ | **OpenObserve** | Petabyte-scale logs + metrics + traces in one binary |
104
+ | **Uptime Kuma** | Self-hosted uptime monitor + status page |
105
+ | **Beszel** | Lightweight server monitor with historical charts |
102
106
 
103
- ### Productivity
107
+ ### Identity
104
108
 
105
- | App | Tier | Rate | What it does |
106
- |---|---|---|---|
107
- | **Glance** | Light | $0.05/hr | Personal dashboard |
108
- | **Homepage** | Standard | $0.10/hr | Self-hosted homepage / app launcher |
109
- | **Gotify** | Light | $0.05/hr | Self-hosted push-notification + webhook server |
109
+ | App | What it does |
110
+ |---|---|
111
+ | **Authentik** | Self-hosted SSO / IdP (OAuth/SAML/LDAP) |
112
+ | **ZITADEL** | Cloud-native identity + access management (OAuth2/OIDC/SAML/LDAP) |
110
113
 
111
- `zibby app templates` is the canonical, always-up-to-date list — the table above is a snapshot.
114
+ `zibby app templates` is the canonical, always-up-to-date list — the tables above are a snapshot, and the live command is the source of truth for each app's tier and hourly rate.
112
115
 
113
116
  ### Multi-service entries
114
117
 
@@ -79,7 +79,7 @@ This picks up whatever's currently in your workspace credentials (set via [Setti
79
79
 
80
80
  ## ENV vars
81
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).
82
+ Every app instance has a per-instance encrypted env-var bag, same shape as agent env. Use it for per-instance config (e.g. `N8N_ENCRYPTION_KEY`, `DATABASE_URL` pointing at an external RDS).
83
83
 
84
84
  Set via the dashboard (Apps → instance → ENV tab) or via CLI:
85
85