@zibby/skills 0.1.32 → 0.1.33

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.
@@ -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};
@@ -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/) (a workflow graph under the hood).
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
 
@@ -245,7 +245,7 @@ Bare init by default — writes `.zibby.config.mjs`, sets up agent credentials,
245
245
  Common options:
246
246
  - `-t, --template <name>` — workflow template to scaffold (see `zibby template list`). Default: none (config + creds only).
247
247
  - `--agent <claude|cursor|codex|gemini>` — pick the agent up front instead of prompting
248
- - `--memory-backend <dolt|mem0>` — memory backend (default: `dolt`; `mem0` is experimental)
248
+ - `--memory-backend <mem0|dolt>` — memory backend (default: `mem0` — semantic vector memory, billed through the agent run in cloud, falls back to `dolt` if the embedding proxy is unavailable; pass `dolt` for self-contained structured memory — see [Chat memory](./skills/chat-memory.md))
249
249
  - `--skip-install` / `--skip-memory` — skip `npm install` / skip memory setup
250
250
  - `-f, --force` — overwrite existing config
251
251
  - `--api-key <key>` — non-interactive Zibby API key (for `--cloud-sync`)
@@ -267,7 +267,7 @@ Options on `add`:
267
267
 
268
268
  ## App commands {#app-commands}
269
269
 
270
- `zibby app` manages [Managed App instances](./apps/) — hosted open-source tools (Grafana, Open WebUI, Docmost, OpenHands, and 16 more in the catalog, plus anything you install via [goal-mode](./apps/goal-mode)) with an autonomous agent-ops sidecar. Each verb is keyed by **instance ID** (`a1b2c3d4`-style); `zibby app list` shows IDs alongside display names.
270
+ `zibby app` manages [Managed App instances](./apps/) — hosted open-source tools (Grafana, Open WebUI, Docmost, OpenHands, and 18 more in the catalog, plus anything you install via [goal-mode](./apps/goal-mode)) with an autonomous agent-ops sidecar. Each verb is keyed by **instance ID** (`a1b2c3d4`-style); `zibby app list` shows IDs alongside display names.
271
271
 
272
272
  | Command | What it does |
273
273
  |---|---|
@@ -7,6 +7,8 @@ pagination_next: get-started/your-first-workflow
7
7
 
8
8
  # Install the CLI
9
9
 
10
+ You build **Agents** with Zibby — deployed automations, each a workflow graph under the hood. The CLI command is `zibby agent` (`zibby workflow` still works as an alias, and is used throughout these walkthroughs).
11
+
10
12
  ```bash
11
13
  npm install -g @zibby/cli
12
14
  ```
@@ -5,10 +5,13 @@ pagination_prev: get-started/install
5
5
  pagination_next: get-started/run-locally
6
6
  ---
7
7
 
8
- # Scaffold a workflow
8
+ # Scaffold your first agent
9
+
10
+ An **Agent** is a deployed automation — a workflow graph of agent-CLI calls. Scaffold one with the CLI (the command is `zibby agent`; `zibby workflow` is a still-working alias):
9
11
 
10
12
  ```bash
11
- zibby workflow new my-pipeline
13
+ zibby agent new my-pipeline
14
+ # zibby workflow new my-pipeline # alias — identical
12
15
  ```
13
16
 
14
17
  This creates:
@@ -0,0 +1,43 @@
1
+ ---
2
+ sidebar_position: 5
3
+ title: GitLab Integration
4
+ ---
5
+
6
+ # GitLab Integration
7
+
8
+ Connect a **self-hosted GitLab** instance (or GitLab SaaS) so your agents can read repos, merge requests, issues, and pipelines. GitLab uses a **Personal Access Token**.
9
+
10
+ ## How It Works
11
+
12
+ You provide your GitLab instance URL and a Personal Access Token. The `gitlab` skill then exposes GitLab's API to any node that declares it. Outbound calls to a firewalled self-hosted instance can be pinned to a [dedicated egress IP](../cloud/dedicated-egress.md).
13
+
14
+ ## Connect GitLab
15
+
16
+ 1. In GitLab, go to **User Settings → Access Tokens** and create a Personal Access Token with the `read_api` and `read_repository` scopes (form `glpat-...`)
17
+ 2. In the Zibby dashboard, go to **Settings → Integrations** and click **Connect GitLab**
18
+ 3. Enter your **GitLab Instance URL** (e.g. `https://gitlab.company.com`)
19
+ 4. Paste the **Personal Access Token**
20
+ 5. Click **Connect** — Zibby validates both before saving
21
+
22
+ | Field | Required | Notes |
23
+ |---|---|---|
24
+ | GitLab Instance URL | Yes | Your instance, e.g. `https://gitlab.company.com` (or `https://gitlab.com`) |
25
+ | Personal Access Token | Yes | `glpat-...` with `read_api` + `read_repository` scopes |
26
+
27
+ ## What Agents Can Do
28
+
29
+ Nodes that attach the [`gitlab` skill](../skills/index.md) get tools for repos, merge requests, issues, and pipelines, against either self-hosted GitLab or GitLab SaaS.
30
+
31
+ ## Reference keys (SDK / CLI)
32
+
33
+ When running workflows directly, the connector reads `GITLAB_TOKEN` (and a base URL for self-hosted instances) from the environment.
34
+
35
+ ## Firewalled instances
36
+
37
+ If your GitLab is behind a firewall, add the [dedicated egress IP addon](../cloud/dedicated-egress.md) so all outbound traffic comes from one whitelistable IP.
38
+
39
+ ## Troubleshooting
40
+
41
+ **"401 Unauthorized"** — regenerate the token with `read_api` + `read_repository` scopes; expired or under-scoped tokens fail here.
42
+
43
+ **"Could not reach instance"** — confirm the instance URL is reachable from Zibby's egress IP and that it points at the GitLab base URL, not a project page.
@@ -0,0 +1,41 @@
1
+ ---
2
+ sidebar_position: 8
3
+ title: Lark Integration
4
+ ---
5
+
6
+ # Lark Integration
7
+
8
+ Connect a Lark (Feishu) self-built app so your agents can send messages, reply, list chats, and read chat history. Lark uses an **App ID + App Secret**.
9
+
10
+ ## How It Works
11
+
12
+ You create a self-built app in the Lark Developer Console and paste its App ID and App Secret into Zibby. Optionally add a Verification Token and Encrypt Key if you want inbound Lark events to trigger Zibby workflows. The `lark` skill then exposes Lark's API to any node that declares it.
13
+
14
+ ## Connect Lark
15
+
16
+ 1. In the Lark Developer Console, open your self-built app and go to **Credentials & Basic Info**
17
+ 2. Copy the **App ID** (`cli_...`) and **App Secret**
18
+ 3. In the Zibby dashboard, go to **Settings → Integrations** and click **Connect Lark**
19
+ 4. Paste the App ID and App Secret
20
+ 5. (Optional) Add the **Verification Token** and **Encrypt Key** from **Event Subscriptions** if you want inbound events
21
+
22
+ | Field | Required | Notes |
23
+ |---|---|---|
24
+ | App ID | Yes | `cli_...` from Credentials & Basic Info |
25
+ | App Secret | Yes | From Credentials & Basic Info |
26
+ | Verification Token | No | Required only for inbound events (Event Subscriptions) |
27
+ | Encrypt Key | No | Only if Lark-side event encryption is enabled |
28
+
29
+ ## What Agents Can Do
30
+
31
+ Nodes that attach the [`lark` skill](../skills/lark.md) get `lark_send_message`, `lark_reply`, `lark_list_chats`, and `lark_get_chat_history` — used by routing recipes like [Sentry triage](../recipes/sentry-triage.md) as a Slack alternative.
32
+
33
+ ## Inbound events
34
+
35
+ Adding the Verification Token (and Encrypt Key, if enabled) lets Lark events fire your Zibby workflows. Leave them blank for outbound-only (notifications).
36
+
37
+ ## Troubleshooting
38
+
39
+ **"Invalid App Secret"** — regenerate the secret in the Developer Console and reconnect.
40
+
41
+ **Bot can't post to a chat** — the app must be added to the target chat/group and have the messaging permission scopes granted in the console.
@@ -0,0 +1,43 @@
1
+ ---
2
+ sidebar_position: 3
3
+ title: Linear Integration
4
+ ---
5
+
6
+ # Linear Integration
7
+
8
+ Connect Linear so your agents can read and update issues, comments, and workflow states. Linear uses a **personal API key** — no OAuth round-trip.
9
+
10
+ ## How It Works
11
+
12
+ You paste a single Linear personal API key. Zibby validates it (by querying the authenticated viewer) before storing it, then the `linear` skill exposes Linear's API to any node that declares it.
13
+
14
+ ## Connect Linear
15
+
16
+ 1. In Linear, go to **Settings → Security & access → Personal API keys**
17
+ 2. Create a new key — it has the form `lin_api_...`
18
+ 3. In the Zibby dashboard, go to **Settings → Integrations** and click **Connect Linear**
19
+ 4. Paste the key and click **Connect**
20
+
21
+ That's it — one field. Zibby verifies the key against Linear's GraphQL API before saving.
22
+
23
+ ## What Agents Can Do
24
+
25
+ Once connected, nodes that attach the [`linear` skill](../skills/index.md) get these tools:
26
+
27
+ | Tool | What it does |
28
+ |---|---|
29
+ | `linear_list_issues` | List issues (filter by team, state, assignee, label) |
30
+ | `linear_get_issue` | Fetch one issue with full detail |
31
+ | `linear_add_comment` | Comment on an issue |
32
+ | `linear_update_state` | Move an issue to a different workflow state |
33
+ | `linear_list_teams` / `linear_list_states` / `linear_list_labels` | Resolve names → IDs for the calls above |
34
+
35
+ ## Reference key (SDK / CLI)
36
+
37
+ When running workflows directly (local or CI), the same connector reads from the `LINEAR_API_KEY` environment variable instead of the dashboard-stored credential.
38
+
39
+ ## Troubleshooting
40
+
41
+ **"Invalid API key"** — regenerate the key in Linear (Settings → Security & access → Personal API keys) and reconnect. Keys are revoked when you remove them from Linear.
42
+
43
+ **Agent can't find a team/state** — call `linear_list_teams` / `linear_list_states` first; Linear's API keys are scoped to whatever the issuing user can see.
@@ -0,0 +1,33 @@
1
+ ---
2
+ sidebar_position: 9
3
+ title: Notion Integration
4
+ ---
5
+
6
+ # Notion Integration
7
+
8
+ Connect a Notion workspace so your agents can read and write pages and databases — for example, publishing a rich digest as Notion blocks. Notion uses **OAuth** (workspace install).
9
+
10
+ ## How It Works
11
+
12
+ You authorize Zibby against a Notion workspace via OAuth. Notion returns a long-lived bearer token (no refresh, no expiry) scoped to the pages and databases you grant. Zibby stores it encrypted with the workspace metadata. The report renderer then publishes structured digests as native Notion blocks.
13
+
14
+ ## Connect Notion
15
+
16
+ 1. In the Zibby dashboard, go to **Settings → Integrations** and click **Connect** next to Notion
17
+ 2. You're redirected to Notion to authorize Zibby
18
+ 3. Select which pages/databases Zibby may access
19
+ 4. You're redirected back to Zibby — the card shows the connected workspace
20
+
21
+ ## What Agents Can Do
22
+
23
+ Workflows can render a `report` object straight to Notion blocks (`reportToNotionBlocks`) and publish it to a connected page or database — the same digest you can route to Slack or Lark. The `notify-notion` template is the shipped example.
24
+
25
+ ## Reference key (SDK / CLI)
26
+
27
+ When running workflows directly, the connector reads `NOTION_API_KEY` from the environment (a Notion internal-integration token works for the SDK path).
28
+
29
+ ## Troubleshooting
30
+
31
+ **Agent can't see a page** — Notion access is page-scoped. Re-run the OAuth flow and grant the specific page/database, or share it with the integration from Notion's UI.
32
+
33
+ **Write fails** — confirm the granted pages include write access; read-only grants can't create blocks.
@@ -0,0 +1,46 @@
1
+ ---
2
+ sidebar_position: 4
3
+ title: Plane Integration
4
+ ---
5
+
6
+ # Plane Integration
7
+
8
+ Connect [Plane](https://plane.so) — the open-source project-management tool — so your agents can read and write projects, work items, cycles, modules, and comments. Plane uses an **API key**, not OAuth.
9
+
10
+ ## How It Works
11
+
12
+ You provide three values: an API key, a workspace slug, and (optionally) a base URL. Zibby validates them (by listing the workspace's projects) before storing, then the `plane` skill talks to Plane's official MCP server.
13
+
14
+ ## Connect Plane
15
+
16
+ 1. In Plane, go to **Workspace Settings → API tokens** and create a personal API token (form `plane_api_...`)
17
+ 2. Note your **workspace slug** — it's the segment in your Plane URL (`app.plane.so/<workspace-slug>/...`)
18
+ 3. In the Zibby dashboard, go to **Settings → Integrations** and click **Connect Plane**
19
+ 4. Paste the API key and workspace slug
20
+ 5. **Base URL** — leave blank for Plane Cloud (`https://api.plane.so`). For self-hosted or Zibby-hosted Plane, set it to your instance's API base.
21
+
22
+ Zibby lists your workspace's projects to confirm the credentials work before saving.
23
+
24
+ | Field | Required | Notes |
25
+ |---|---|---|
26
+ | API key | Yes | From Workspace Settings → API tokens |
27
+ | Workspace slug | Yes | The `<slug>` in your Plane URL |
28
+ | Base URL | No | Defaults to `https://api.plane.so` (Plane Cloud). Set for self-hosted. |
29
+
30
+ ## What Agents Can Do
31
+
32
+ Nodes that attach the [`plane` skill](../skills/index.md) get tools for projects, work items, cycles, modules, epics, and comments, backed by Plane's official MCP server.
33
+
34
+ ## Reference keys (SDK / CLI)
35
+
36
+ When running workflows directly, the connector reads `PLANE_API_KEY`, `PLANE_WORKSPACE_SLUG`, and `PLANE_BASE_URL` from the environment.
37
+
38
+ ## Pairs well with hosted Plane
39
+
40
+ Plane is also in the [Managed Apps catalog](../apps/index.md) — you can host a Plane instance on Zibby and point this connector at it via the **Base URL** field.
41
+
42
+ ## Troubleshooting
43
+
44
+ **"Workspace not found"** — double-check the workspace slug matches the segment in your Plane URL exactly.
45
+
46
+ **Self-hosted instance rejects the token** — confirm the **Base URL** points at the API base of your instance, not the web UI URL.
@@ -0,0 +1,42 @@
1
+ ---
2
+ sidebar_position: 6
3
+ title: Sentry Integration
4
+ ---
5
+
6
+ # Sentry Integration
7
+
8
+ Connect a Sentry organization so your agents can list projects, list issues, and fetch issue detail — read-only. Sentry uses **OAuth 2.0 with PKCE**.
9
+
10
+ ## How It Works
11
+
12
+ You authorize Zibby against your Sentry organization via OAuth (PKCE — no client secret). Once connected, the `sentry` skill exposes read-only Sentry tools to any node that declares it. This powers the [Sentry triage](../recipes/sentry-triage.md) agent.
13
+
14
+ ## Connect Sentry
15
+
16
+ 1. In the Zibby dashboard, go to **Settings → Integrations** and click **Connect Sentry**
17
+ 2. You're redirected to Sentry to authorize Zibby
18
+ 3. Pick the organization to grant access to
19
+ 4. You're redirected back to Zibby
20
+
21
+ ## What Agents Can Do
22
+
23
+ Nodes that attach the [`sentry` skill](../skills/sentry.md) get read-only tools:
24
+
25
+ | Tool | What it does |
26
+ |---|---|
27
+ | `sentry_list_projects` | List projects in the connected organization |
28
+ | `sentry_list_issues` | List issues (supports Sentry search syntax, sort, limit) |
29
+ | `sentry_get_issue` | Detailed info for one issue by ID |
30
+
31
+ Access is read-only by design — the triage agent reads issues and routes them via Slack/Lark rather than mutating Sentry.
32
+
33
+ ## See also
34
+
35
+ - [Sentry skill reference](../skills/sentry.md) — full tool surface
36
+ - [Sentry triage recipe](../recipes/sentry-triage.md) — the agent that consumes this connector
37
+
38
+ ## Troubleshooting
39
+
40
+ **"Reconnect Sentry"** — the OAuth grant was revoked or expired. Go to Settings → Integrations and reconnect.
41
+
42
+ **No issues returned** — check the project filter and that the org you authorized actually owns the project.