@zibby/skills 0.2.22 → 0.2.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +3 -10
- package/dist/code-scan.d.ts +1 -1
- package/dist/index.js +81 -88
- package/dist/jira.d.ts +75 -9
- package/dist/jira.js +4 -4
- package/dist/package.json +1 -1
- package/dist/report.d.ts +51 -276
- package/dist/trackers/index.js +12 -12
- package/dist/trackers/jira-adapter.js +11 -11
- package/package.json +1 -1
- package/docs/analysis.md +0 -109
- package/docs/apps/agent-ops.md +0 -130
- package/docs/apps/auth.md +0 -158
- package/docs/apps/deploy.md +0 -207
- package/docs/apps/goal-mode.md +0 -175
- package/docs/apps/index.md +0 -149
- package/docs/apps/managing.md +0 -121
- package/docs/cli-reference.md +0 -494
- package/docs/cloning-repositories.md +0 -285
- package/docs/cloud/bundles.md +0 -92
- package/docs/cloud/dedicated-egress.md +0 -140
- package/docs/cloud/editing-prompts.md +0 -79
- package/docs/cloud/env-vars.md +0 -144
- package/docs/cloud/limits.md +0 -81
- package/docs/cloud/logs.md +0 -104
- package/docs/cloud/triggering.md +0 -128
- package/docs/concepts/agents.md +0 -112
- package/docs/concepts/designing-agents.md +0 -247
- package/docs/concepts/graph.md +0 -83
- package/docs/concepts/sessions.md +0 -70
- package/docs/concepts/skills.md +0 -86
- package/docs/concepts/state.md +0 -106
- package/docs/concepts/sub-graphs.md +0 -227
- package/docs/custom-workflows.md +0 -358
- package/docs/get-started/deploy.md +0 -75
- package/docs/get-started/install.md +0 -60
- package/docs/get-started/run-locally.md +0 -94
- package/docs/get-started/trigger-and-logs.md +0 -90
- package/docs/get-started/use-from-agents.md +0 -153
- package/docs/get-started/your-first-workflow.md +0 -69
- package/docs/getting-started.md +0 -108
- package/docs/installation.md +0 -127
- package/docs/integrations/github.md +0 -73
- package/docs/integrations/gitlab.md +0 -43
- package/docs/integrations/jira.md +0 -71
- package/docs/integrations/lark.md +0 -41
- package/docs/integrations/linear.md +0 -43
- package/docs/integrations/notion.md +0 -33
- package/docs/integrations/plane.md +0 -46
- package/docs/integrations/sentry.md +0 -42
- package/docs/integrations/slack.md +0 -33
- package/docs/intro.md +0 -86
- package/docs/legacy/test-automation.md +0 -111
- package/docs/packages/agent-workflow.md +0 -88
- package/docs/packages/cli.md +0 -73
- package/docs/packages/core.md +0 -72
- package/docs/packages/mcp-browser.md +0 -110
- package/docs/packages/mcp-cli.md +0 -176
- package/docs/packages/memory.md +0 -223
- package/docs/packages/skills.md +0 -218
- package/docs/packages/ui-memory.md +0 -245
- package/docs/recipes/bug-autofix.md +0 -85
- package/docs/recipes/github-ai-scout.md +0 -61
- package/docs/recipes/index.md +0 -68
- package/docs/recipes/pipeline-supervisor.md +0 -57
- package/docs/recipes/sentry-triage.md +0 -93
- package/docs/recipes/test.md +0 -190
- package/docs/reviewing-results.md +0 -114
- package/docs/running-tests.md +0 -134
- package/docs/self-host/backup-restore.md +0 -53
- package/docs/self-host/custom-sidecars.md +0 -136
- package/docs/self-host/index.md +0 -75
- package/docs/self-host/storage.md +0 -50
- package/docs/self-host/troubleshooting.md +0 -48
- package/docs/self-host/upgrade.md +0 -81
- package/docs/skills/browser.md +0 -97
- package/docs/skills/chat-memory.md +0 -122
- package/docs/skills/core-tools.md +0 -80
- package/docs/skills/function-skill.md +0 -93
- package/docs/skills/github.md +0 -91
- package/docs/skills/index.md +0 -46
- package/docs/skills/jira.md +0 -99
- package/docs/skills/lark.md +0 -85
- package/docs/skills/memory.md +0 -92
- package/docs/skills/sentry.md +0 -80
- package/docs/skills/slack.md +0 -89
- package/docs/tests/memory.md +0 -131
- package/docs/triggering-workflows.md +0 -552
- package/docs/workflow-artifact-layout-evaluation.md +0 -119
- package/docs/workflow.md +0 -558
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import{createRequire as K}from"module";import{resolveIntegrationToken as
|
|
1
|
+
import{createRequire as K}from"module";import{resolveIntegrationToken as E,clearTokenCache as L}from"@zibby/core/backend-client.js";var $=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark",LARK_DOCS:"lark_docs",OPENAI_BILLING:"openai_billing",ANTHROPIC_BILLING:"anthropic_billing",CURSOR_ADMIN:"cursor_admin",NOTION:"notion",GOOGLE:"google",PLANE:"plane",LINEAR:"linear",VIKUNJA:"vikunja",FIGMA:"figma",HUBSPOT:"hubspot",OPEN_DESIGN:"open_design",LINKEDIN_PERSONAL:"linkedin_personal",LINKEDIN_BUSINESS:"linkedin_business",DISCORD:"discord"}),Z=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 Chat",connectPath:"/integrations?provider=lark"},lark_docs:{id:"lark_docs",name:"Lark App",connectPath:"/integrations?provider=lark_docs"},openai_billing:{id:"openai_billing",name:"OpenAI Admin",connectPath:"/integrations?provider=openai_billing"},anthropic_billing:{id:"anthropic_billing",name:"Anthropic Admin",connectPath:"/integrations?provider=anthropic_billing"},cursor_admin:{id:"cursor_admin",name:"Cursor Admin",connectPath:"/integrations?provider=cursor_admin"},notion:{id:"notion",name:"Notion",connectPath:"/integrations?provider=notion"},google:{id:"google",name:"Google Docs",connectPath:"/integrations?provider=google"},plane:{id:"plane",name:"Plane",connectPath:"/integrations?provider=plane"},linear:{id:"linear",name:"Linear",connectPath:"/integrations?provider=linear"},vikunja:{id:"vikunja",name:"Vikunja",connectPath:"/integrations?provider=vikunja"},figma:{id:"figma",name:"Figma",connectPath:"/integrations?provider=figma"},hubspot:{id:"hubspot",name:"HubSpot",connectPath:"/integrations?provider=hubspot"},open_design:{id:"open_design",name:"OpenDesign",connectPath:"/integrations?provider=open_design"},linkedin_personal:{id:"linkedin_personal",name:"LinkedIn (Personal)",connectPath:"/integrations?provider=linkedin_personal"},linkedin_business:{id:"linkedin_business",name:"LinkedIn (Business)",connectPath:"/integrations?provider=linkedin_business"},discord:{id:"discord",name:"Discord",connectPath:"/integrations?provider=discord"}});var x=K(import.meta.url);function D(){if(process.env.MCP_JIRA_PATH)return process.env.MCP_JIRA_PATH;try{return x.resolve("@zibby/mcp-jira/index.js")}catch{return null}}var B=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function U(s,r){if(!r||!r.length)return s;let e=s;for(let t of r)t.type==="strong"?e=`**${e}**`:t.type==="em"?e=`_${e}_`:t.type==="code"?e=`\`${e}\``:t.type==="strike"?e=`~~${e}~~`:t.type==="link"&&t.attrs?.href&&(e=`[${e}](${t.attrs.href})`);return e}function w(s,r=0){if(!Array.isArray(s))return"";let e=[];for(let t of s){if(t.type==="text"){e.push(U(t.text||"",t.marks));continue}if(t.type==="hardBreak"){e.push(`
|
|
2
2
|
`);continue}if(t.type==="rule"){e.push(`
|
|
3
3
|
---
|
|
4
|
-
`);continue}let a=t.content?w(t.content,
|
|
5
|
-
${
|
|
4
|
+
`);continue}let a=t.content?w(t.content,r+1):"";if(t.type==="listItem")e.push(a);else if(t.type==="bulletList"){let i=(t.content||[]).map(o=>`- ${w(o.content||[],r+1).trim()}`);e.push(`
|
|
5
|
+
${i.join(`
|
|
6
6
|
`)}
|
|
7
|
-
`)}else if(t.type==="orderedList"){let
|
|
8
|
-
${
|
|
7
|
+
`)}else if(t.type==="orderedList"){let i=(t.content||[]).map((o,n)=>`${n+1}. ${w(o.content||[],r+1).trim()}`);e.push(`
|
|
8
|
+
${i.join(`
|
|
9
9
|
`)}
|
|
10
|
-
`)}else if(t.type==="heading"){let
|
|
10
|
+
`)}else if(t.type==="heading"){let i=t.attrs?.level||2;e.push(`
|
|
11
11
|
|
|
12
|
-
${"#".repeat(
|
|
12
|
+
${"#".repeat(i)} ${a.trim()}
|
|
13
13
|
|
|
14
14
|
`)}else B.has(t.type)?e.push(`
|
|
15
15
|
|
|
16
16
|
${a}
|
|
17
17
|
`):e.push(a)}return e.join("").replace(/\n{3,}/g,`
|
|
18
18
|
|
|
19
|
-
`)}function v(s){return String(s||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function A(s){return v(s).replace(/[a-z0-9]+/g,"")}function I(s,
|
|
19
|
+
`)}function v(s){return String(s||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function A(s){return v(s).replace(/[a-z0-9]+/g,"")}function I(s,r){let e=v(s),t=v(r);if(!e||!t)return 0;if(e===t)return 1;if(e.length===1||t.length===1)return e===t?1:0;let a=u=>{let d=new Map;for(let m=0;m<u.length-1;m++){let g=u.slice(m,m+2);d.set(g,(d.get(g)||0)+1)}return d},i=a(e),o=a(t),n=0,c=0,l=0;for(let u of i.values())c+=u;for(let u of o.values())l+=u;for(let[u,d]of i.entries()){let m=o.get(u)||0;n+=Math.min(d,m)}return 2*n/Math.max(1,c+l)}function b(s){return String(s||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function M(s,r=[]){let e=Array.isArray(r)?r:[];if(e.length===0)return{requested:s||null,resolved:null,strategy:"none"};let t=e.filter(n=>!n.subtask),a=t.length>0?t:e,i=b(s);if(i){let n=a.find(u=>b(u.name)===i);if(n)return{requested:s,resolved:n,strategy:"exact"};let c={task:["task","\u4EFB\u52A1","\u4E8B\u9879","to do","todo"],story:["story","\u7528\u6237\u6545\u4E8B","\u9700\u6C42"],bug:["bug","\u7F3A\u9677","\u95EE\u9898"],improvement:["improvement","\u4F18\u5316","\u6539\u8FDB"],epic:["epic","\u53F2\u8BD7"]};for(let u of Object.values(c)){if(!u.some(m=>b(m)===i))continue;let d=a.find(m=>u.some(g=>b(g)===b(m.name)));if(d)return{requested:s,resolved:d,strategy:"alias"}}let l=a.map(u=>({t:u,score:I(s,u.name)})).sort((u,d)=>d.score-u.score);if(l[0]&&l[0].score>=.5)return{requested:s,resolved:l[0].t,strategy:"fuzzy"}}let o=["task","story","bug","improvement","epic"];for(let n of o){let c=a.find(l=>b(l.name)===n);if(c)return{requested:s||null,resolved:c,strategy:"default-preferred"}}return{requested:s||null,resolved:a[0],strategy:"default-first"}}async function C(s){let r=`projectKeys=${encodeURIComponent(s)}&expand=projects.issuetypes`,e=await y(`/rest/api/3/issue/createmeta?${r}`),t=Array.isArray(e?.projects)?e.projects:[],i=t.find(n=>String(n?.key||"").toUpperCase()===String(s||"").toUpperCase())||t[0]||null;return(Array.isArray(i?.issuetypes)?i.issuetypes:[]).map(n=>({id:n.id,name:n.name,subtask:!!n.subtask,description:n.description||null}))}async function P(s,r){if(!s)throw new Error("projectKey is required");let e="sprint is not EMPTY";r==="active"?e="sprint in openSprints()":r==="closed"?e="sprint in closedSprints()":r==="future"&&(e="sprint in futureSprints()");let t=`project = ${s} AND ${e} ORDER BY updated DESC`,a=`jql=${encodeURIComponent(t)}&maxResults=100&fields=customfield_10020`,i=await y(`/rest/api/3/search/jql?${a}`),o=new Map;for(let n of i.issues||[])for(let c of n.fields?.customfield_10020||[])c&&!o.has(c.id)&&o.set(c.id,{id:c.id,name:c.name,state:c.state,boardId:c.boardId||null,startDate:c.startDate||null,endDate:c.endDate||null,goal:c.goal||null});return[...o.values()].sort((n,c)=>{let l={active:0,future:1,closed:2},u=(l[n.state]??3)-(l[c.state]??3);return u!==0?u:String(c.startDate||"").localeCompare(String(n.startDate||""))})}function G(s,{sprintId:r,sprintName:e,target:t}={}){let a=Array.isArray(s)?s:[];if(!a.length)return{sprint:null,selectedBy:"none"};if(r!=null&&String(r).trim()!=="")return{sprint:a.find(n=>String(n.id)===String(r))||null,selectedBy:"id"};if(e&&String(e).trim()){let o=String(e).trim(),n=a.find(l=>String(l.name||"").toLowerCase()===o.toLowerCase());if(n)return{sprint:n,selectedBy:"name-exact"};let c=a.map(l=>({s:l,score:I(o,l.name||"")})).sort((l,u)=>u.score-l.score);return c[0]&&c[0].score>=.5?{sprint:c[0].s,selectedBy:"name-fuzzy"}:{sprint:null,selectedBy:"name-none"}}let i=String(t||"current").trim().toLowerCase();return i==="active"||i==="current"||i==="latest"?{sprint:a[0],selectedBy:i}:{sprint:a[0],selectedBy:"default"}}function Y(s,r){let e=s?.fields?.customfield_10020;return Array.isArray(e)?e.some(t=>String(t?.id)===String(r)):!1}async function F({issueKey:s,projectKey:r,sprintId:e,attempts:t=3,delayMs:a=450}){let i=[];for(let o=0;o<t;o++){try{let n=`project = ${r} AND key = ${s} AND sprint = ${e}`,c=`jql=${encodeURIComponent(n)}&maxResults=1&fields=key,status`,l=await y(`/rest/api/3/search/jql?${c}`);if(Number(l?.total||0)>0)return i.push({attempt:o+1,jql:!0,issueField:null}),{ok:!0,method:"jql",traces:i};let d=await y(`/rest/api/3/issue/${s}?fields=customfield_10020,status`),m=Y(d,e);if(i.push({attempt:o+1,jql:!1,issueField:m}),m)return{ok:!0,method:"issue_field",traces:i}}catch(n){i.push({attempt:o+1,error:String(n?.message||n)})}o<t-1&&await new Promise(n=>setTimeout(n,a))}return{ok:!1,method:"none",traces:i}}async function N({issueKey:s,projectKey:r,sprintId:e,sprintName:t,target:a}){if(!s)return{ok:!1,error:"issueKey is required"};let i=r;if(!i&&(i=(await y(`/rest/api/3/issue/${s}?fields=project`))?.fields?.project?.key||null,!i))return{ok:!1,error:`Could not resolve project for ${s}`};let o=await P(i,"active");if(!o.length)return{ok:!1,error:`No assignable active sprint found for project ${i}`};let{sprint:n,selectedBy:c}=G(o,{sprintId:e,sprintName:t,target:a});if(!n)return{ok:!1,error:`No matching sprint found in ${i}`,requested:{sprintId:e??null,sprintName:t??null,target:a??"current"},availableSprints:o.map(d=>({id:d.id,name:d.name,state:d.state}))};await y(`/rest/api/3/issue/${s}`,{method:"PUT",body:{fields:{customfield_10020:Number(n.id)}}});let l=await F({issueKey:s,projectKey:i,sprintId:n.id}),u=l.ok;return{ok:u,issueKey:s,projectKey:i,sprintId:n.id,sprintName:n.name,selectedBy:c,verifiedBy:l.method,verified:u,verificationTrace:l.traces,warning:u?null:`Sprint assignment attempted but verification did not find ${s} in sprint ${n.id}`}}async function z(){let s=process.env.JIRA_API_TOKEN,r=process.env.JIRA_EMAIL,e=process.env.JIRA_BASE_URL;if(s&&r&&e)return{authType:"token",apiToken:s,email:r,baseUrl:e};let t=await E("jira");return t?.cloudId?{authType:"oauth",accessToken:t.token,cloudId:t.cloudId}:{authType:"token",apiToken:t?.token,email:r||t?.email||"",baseUrl:e||t?.instanceUrl||t?.baseUrl||process.env.ATLASSIAN_INSTANCE_URL||""}}function H(s,r,e={}){let t=s||{},a={Accept:"application/json",...e.body?{"Content-Type":"application/json"}:{},...e.headers};if(t.authType==="token"){let i=String(t.baseUrl||t.instanceUrl||"").trim().replace(/\/+$/,""),o=t.apiToken||t.accessToken||"";if(!i)throw new Error("Jira token connection has no base URL \u2014 reconnect Jira with its instance URL, or set JIRA_BASE_URL.");if(!t.email)throw new Error("Jira token connection has no account email \u2014 reconnect Jira with the account email, or set JIRA_EMAIL.");if(!o)throw new Error("Jira token connection has no API token \u2014 reconnect Jira, or set JIRA_API_TOKEN.");return{url:`${i}${r}`,headers:{Authorization:"Basic "+Buffer.from(`${t.email}:${o}`).toString("base64"),...a}}}if(typeof t.accessToken!="string"||!t.accessToken)throw new Error(`Invalid jira token type: ${typeof t.accessToken}`);if(!t.cloudId)throw new Error("Invalid jira cloudId: missing");return{url:`https://api.atlassian.com/ex/jira/${t.cloudId}${r}`,headers:{Authorization:`Bearer ${t.accessToken}`,...a}}}async function y(s,r={}){let e=async()=>{let t=await z(),{url:a,headers:i}=H(t,s,r),o=await fetch(a,{method:r.method||"GET",headers:i,body:r.body?JSON.stringify(r.body):void 0});if(!o.ok){let c=await o.text().catch(()=>"");throw new Error(`Jira API ${o.status}: ${c.slice(0,300)}`)}let n=await o.text().catch(()=>"");if(!n||!n.trim())return{};try{return JSON.parse(n)}catch{return{raw:n}}};try{return await e()}catch(t){let a=String(t?.message||t||"").toLowerCase();if(!(a.includes("token")||a.includes("401")||a.includes("403")||a.includes("substring")))throw t;return L("jira"),e()}}var S={id:"jira",callsBackend:!0,serverName:"jira",allowedTools:["mcp__jira__*"],requiresIntegration:$.JIRA,envKeys:["ATLASSIAN_ACCESS_TOKEN","ATLASSIAN_CLOUD_ID"],description:"Zibby Jira MCP Server (OAuth Bearer)",promptFragment:`## Jira
|
|
20
20
|
You have direct access to the user's Jira. Use these tools proactively:
|
|
21
21
|
|
|
22
22
|
### Issue tools
|
|
@@ -66,6 +66,6 @@ When user asks to move/transition ticket status:
|
|
|
66
66
|
3. Pick the correct transition from returned list (match by "to" status name, not guesswork), then call jira_transition_issue with transitionId.
|
|
67
67
|
4. Call jira_get_issue(issueKey) to verify final status before claiming success.
|
|
68
68
|
5. If target wording differs (e.g. \u5DF2\u7ECF\u9A8C\u6536 vs \u5DF2\u9A8C\u6536), try toStatus first; only ask user to confirm when no reasonable match exists.
|
|
69
|
-
6. IMPORTANT: When target is clear, complete transition + verification in SAME turn. Do NOT stop after listing options.`,resolve(){let s=D();if(!s)return null;let i={};for(let e of this.envKeys)process.env[e]&&(i[e]=process.env[e]);process.env.ATLASSIAN_INSTANCE_URL&&(i.ATLASSIAN_INSTANCE_URL=process.env.ATLASSIAN_INSTANCE_URL);for(let e of["JIRA_API_TOKEN","JIRA_EMAIL","JIRA_BASE_URL"])process.env[e]&&(i[e]=process.env[e]);return{command:"node",args:[s],env:i,description:this.description}},async handleToolCall(s,i){try{switch(s){case"jira_list_projects":{let e=await y("/rest/api/3/project"),t=(Array.isArray(e)?e:[]).map(a=>({id:a.id,key:a.key,name:a.name,style:a.style}));return JSON.stringify({count:t.length,projects:t})}case"jira_list_statuses":{let{projectKey:e}=i||{};if(e){let n=await y(`/rest/api/3/project/${encodeURIComponent(e)}/statuses`),o=Array.isArray(n)?n:[],r=new Map;for(let p of o)for(let u of p.statuses||[])u?.id&&(r.has(u.id)||r.set(u.id,{id:u.id,name:u.name,category:u.statusCategory?.name||null}));let c=[...r.values()].sort((p,u)=>String(p.name).localeCompare(String(u.name)));return JSON.stringify({scope:"project",projectKey:e,count:c.length,statuses:c})}let t=await y("/rest/api/3/status"),a=(Array.isArray(t)?t:[]).map(n=>({id:n.id,name:n.name,category:n.statusCategory?.name||null})).sort((n,o)=>String(n.name).localeCompare(String(o.name)));return JSON.stringify({scope:"global",count:a.length,statuses:a})}case"jira_list_issue_types":{let{projectKey:e}=i||{};if(!e)return JSON.stringify({error:"projectKey is required"});let t=await P(e);return JSON.stringify({projectKey:e,count:t.length,issueTypes:t})}case"jira_search":{let e=i.jql||"",t=i.maxResults||20;e.replace(/\s*ORDER\s+BY\s+.*/i,"").trim()||(e=`created >= -365d ${e}`.trim());let n=`jql=${encodeURIComponent(e)}&maxResults=${t}&fields=summary,status,assignee,priority,updated,issuetype,project`,r=((await y(`/rest/api/3/search/jql?${n}`)).issues||[]).map(c=>({key:c.key,project:c.fields?.project?.key,summary:c.fields?.summary,status:c.fields?.status?.name,assignee:c.fields?.assignee?.displayName||"Unassigned",priority:c.fields?.priority?.name,type:c.fields?.issuetype?.name}));return JSON.stringify({count:r.length,issues:r})}case"jira_get_issue":{let e=i.issueKey;if(!e)return JSON.stringify({error:"issueKey is required"});let t=await y(`/rest/api/3/issue/${e}`);return JSON.stringify({key:t.key,project:t.fields?.project?.key,summary:t.fields?.summary,description:t.fields?.description,status:t.fields?.status?.name,assignee:t.fields?.assignee?.displayName||"Unassigned",priority:t.fields?.priority?.name,type:t.fields?.issuetype?.name,labels:t.fields?.labels,created:t.fields?.created,updated:t.fields?.updated})}case"jira_create_issue":{let{projectKey:e,summary:t,issueType:a,description:n,priority:o,labels:r,assigneeId:c,moveToSprint:p,moveToActiveSprint:u,sprintId:d,sprintName:m,target:g}=i;if(!e||!t)return JSON.stringify({error:"projectKey and summary are required"});let l={requested:a||null,resolved:null,strategy:"none"},h=[];try{h=await P(e),l=M(a,h)}catch{}let f={project:{key:e},summary:t,issuetype:l?.resolved?.id?{id:l.resolved.id}:{name:a||"Task"}};n&&(f.description={type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:n}]}]}),o&&(f.priority={name:o}),r?.length&&(f.labels=r),c&&(f.assignee={id:c});let j=await y("/rest/api/3/issue",{method:"POST",body:{fields:f}}),_={ok:!0,key:j.key,id:j.id,self:j.self};return l?.resolved&&(_.issueType=l.resolved.name,_.issueTypeResolution=l.strategy,l.strategy!=="exact"&&l.requested&&b(l.requested)!==b(l.resolved.name)&&(_.issueTypeWarning=`Requested "${l.requested}" is not available in ${e}; used "${l.resolved.name}" instead.`)),h.length>0&&(_.availableIssueTypes=h.map(k=>k.name)),(p||u)&&(_.sprintMove=await N({issueKey:j.key,projectKey:e,sprintId:d,sprintName:m,target:g})),JSON.stringify(_)}case"jira_list_sprints":{let{projectKey:e,state:t}=i,a=await J(e,t);return JSON.stringify({count:a.length,sprints:a})}case"jira_move_to_active_sprint":{let{issueKey:e,projectKey:t,sprintId:a,sprintName:n,target:o}=i||{},r=await N({issueKey:e,projectKey:t,sprintId:a,sprintName:n,target:o||"current"});return JSON.stringify(r)}case"jira_move_issue_to_sprint":{let{issueKey:e,projectKey:t,sprintId:a,sprintName:n,target:o}=i||{},r=await N({issueKey:e,projectKey:t,sprintId:a,sprintName:n,target:o});return JSON.stringify(r)}case"jira_get_sprint_issues":{let{sprintName:e,sprintId:t,projectKey:a,status:n,maxResults:o}=i;if(!e&&!t)return JSON.stringify({error:"sprintName or sprintId is required"});let r=o||50,c=t?`sprint = ${t}`:`sprint = "${e}"`,p=a?`project = ${a} AND `:"",u=n?` AND status = "${n}"`:"",d=`${p}${c}${u} ORDER BY status ASC, priority DESC`,m=`jql=${encodeURIComponent(d)}&maxResults=${r}&fields=summary,status,assignee,priority,issuetype,project`,g=await y(`/rest/api/3/search/jql?${m}`),l=(g.issues||[]).map(f=>({key:f.key,project:f.fields?.project?.key,summary:f.fields?.summary,status:f.fields?.status?.name,assignee:f.fields?.assignee?.displayName||"Unassigned",priority:f.fields?.priority?.name,type:f.fields?.issuetype?.name})),h={};for(let f of l)h[f.status]=(h[f.status]||0)+1;return JSON.stringify({count:l.length,total:g.total||l.length,statusCounts:h,issues:l})}case"jira_get_comments":{let{issueKey:e,maxResults:t}=i;if(!e)return JSON.stringify({error:"issueKey is required"});let n=await y(`/rest/api/3/issue/${e}/comment?maxResults=${t||50}&orderBy=-created`),o=(n.comments||[]).map(r=>{let c="";return r.body?.content&&(c=w(r.body.content)),{id:r.id,author:r.author?.displayName||"Unknown",body:c,created:r.created,updated:r.updated}});return JSON.stringify({count:o.length,total:n.total||o.length,comments:o})}case"jira_add_comment":{let{issueKey:e,body:t}=i;return!e||!t?JSON.stringify({error:"issueKey and body are required"}):(await y(`/rest/api/3/issue/${e}/comment`,{method:"POST",body:{body:{type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:t}]}]}}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_edit_issue":{let{issueKey:e,fields:t}=i;return!e||!t?JSON.stringify({error:"issueKey and fields are required"}):(await y(`/rest/api/3/issue/${e}`,{method:"PUT",body:{fields:t}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_transition_issue":{let{issueKey:e,transitionId:t,toStatus:a,statusName:n,status:o}=i;if(!e)return JSON.stringify({error:"issueKey is required"});let r=String(a||n||o||"").trim();if(!t&&!r){let d=((await y(`/rest/api/3/issue/${e}/transitions`)).transitions||[]).map(m=>({id:m.id,name:m.name,to:m.to?.name}));return JSON.stringify({ok:!1,error:"transitionId or toStatus is required",issueKey:e,availableTransitions:d})}let c=t;if(!c){let d=(await y(`/rest/api/3/issue/${e}/transitions`)).transitions||[],m=v(r),g=d.find(l=>v(l?.name||"")===m||v(l?.to?.name||"")===m);if(!g){let l=A(r);l.length>=2&&(g=d.find(h=>{let f=A(h?.name||""),j=A(h?.to?.name||""),_=f.length>=2&&(f.includes(l)||l.includes(f)),k=j.length>=2&&(j.includes(l)||l.includes(j));return _||k}))}if(!g){let l=d.map(_=>{let k=I(r,_?.name||""),q=I(r,_?.to?.name||"");return{t:_,score:Math.max(k,q)}}).sort((_,k)=>k.score-_.score),h=l[0],f=l[1];h&&h.score>=.45&&(!f||h.score-f.score>=.12)&&(g=h.t)}if(!g?.id)return JSON.stringify({ok:!1,error:`No transition matches target status: "${r}"`,issueKey:e,availableTransitions:d.map(l=>({id:l.id,name:l.name,to:l.to?.name}))});c=g.id}await y(`/rest/api/3/issue/${e}/transitions`,{method:"POST",body:{transition:{id:c}}});let p=await y(`/rest/api/3/issue/${e}?fields=status`);return JSON.stringify({ok:!0,issueKey:e,transitionId:c,statusAfter:p?.fields?.status?.name||null})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(e){return JSON.stringify({error:e.message})}},tools:[{name:"jira_list_projects",description:"List all Jira projects accessible to the user",input_schema:{type:"object",properties:{}}},{name:"jira_list_statuses",description:"List Jira statuses. Use projectKey to get statuses applicable in that project workflow.",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Optional project key (e.g. PROJ). If omitted, returns global status catalog."}}}},{name:"jira_list_issue_types",description:"List issue types allowed for issue creation in the given project.",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"}},required:["projectKey"]}},{name:"jira_search",description:"Search Jira issues using JQL",input_schema:{type:"object",properties:{jql:{type:"string",description:'JQL query string, e.g. "project = PROJ AND status = Open"'},maxResults:{type:"number",description:"Max results to return (default 20)"}},required:["jql"]}},{name:"jira_get_issue",description:"Get details of a specific Jira issue",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"}},required:["issueKey"]}},{name:"jira_create_issue",description:"Create a new Jira issue",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"},summary:{type:"string",description:"Issue title/summary"},issueType:{type:"string",description:"Issue type (default: Task). Common: Task, Bug, Story, Epic"},description:{type:"string",description:"Issue description (plain text)"},priority:{type:"string",description:"Priority name, e.g. High, Medium, Low"},labels:{type:"array",items:{type:"string"},description:"Array of label strings"},assigneeId:{type:"string",description:"Atlassian account ID to assign to"},moveToSprint:{type:"boolean",description:"If true, move created issue to a sprint and verify."},moveToActiveSprint:{type:"boolean",description:"Backward-compatible alias for moveToSprint."},sprintId:{type:"number",description:"Optional sprint id for placement."},sprintName:{type:"string",description:"Optional sprint name for placement."},target:{type:"string",description:"Placement target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["projectKey","summary"]}},{name:"jira_list_sprints",description:"List sprints for a Jira project (returns sprint names, IDs, states, dates)",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"},state:{type:"string",description:"Filter: active, closed, future. Omit for all."}},required:["projectKey"]}},{name:"jira_get_sprint_issues",description:"Get all issues in a sprint, optionally filtered by status column name",input_schema:{type:"object",properties:{sprintName:{type:"string",description:"Sprint name (from jira_list_sprints). Use this OR sprintId."},sprintId:{type:"number",description:"Sprint ID (from jira_list_sprints). Use this OR sprintName."},projectKey:{type:"string",description:"Project key to scope the search (optional)"},status:{type:"string",description:'Filter by status name (e.g. "\u8FDB\u884C\u4E2D", "\u6D4B\u8BD5", "Done")'},maxResults:{type:"number",description:"Max issues to return (default 50)"}}}},{name:"jira_move_to_active_sprint",description:"Backward-compatible alias: move issue to sprint target and verify membership.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},projectKey:{type:"string",description:"Optional project key. If omitted, inferred from issue."},sprintId:{type:"number",description:"Optional sprint id."},sprintName:{type:"string",description:"Optional sprint name."},target:{type:"string",description:"Target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["issueKey"]}},{name:"jira_move_issue_to_sprint",description:"Move an issue to a sprint by id/name/target and verify membership.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},projectKey:{type:"string",description:"Optional project key. If omitted, inferred from issue."},sprintId:{type:"number",description:"Optional sprint id."},sprintName:{type:"string",description:"Optional sprint name."},target:{type:"string",description:"Target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["issueKey"]}},{name:"jira_get_comments",description:"Get comments on a Jira issue (newest first)",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},maxResults:{type:"number",description:"Max comments to return (default 50)"}},required:["issueKey"]}},{name:"jira_add_comment",description:"Add a comment to a Jira issue",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},body:{type:"string",description:"Comment text (plain text)"}},required:["issueKey","body"]}},{name:"jira_edit_issue",description:"Update fields on a Jira issue (summary, story points, labels, priority)",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},fields:{type:"object",description:"Object of field names to values",additionalProperties:!0}},required:["issueKey","fields"]}},{name:"jira_transition_issue",description:"Move a Jira issue to a different status. Always pass toStatus when user gave a target; only pass issueKey alone when you explicitly need to list transitions.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},transitionId:{type:"string",description:"Transition ID to perform (optional if toStatus is provided)"},toStatus:{type:"string",description:'Target status/column name (e.g. "\u5DF2\u7ECF\u9A8C\u6536", "Done", "In Progress"). If provided, tool resolves matching transition automatically.'}},required:["issueKey"]}}]};var z={new:"todo",indeterminate:"in_progress",done:"done"},H=/\b(blocked|on[\s-]?hold|waiting|stuck)\b/i;function T(s){if(!s)return"unknown";if(s.name&&H.test(s.name))return"blocked";let i=s.statusCategory?.key;return z[i]||"unknown"}function $(s){if(!s)return"";if(Array.isArray(s))return s.map($).join("");if(s.type==="text")return s.text||"";if(s.type==="hardBreak"||s.type==="rule")return`
|
|
70
|
-
`;let
|
|
71
|
-
`),
|
|
69
|
+
6. IMPORTANT: When target is clear, complete transition + verification in SAME turn. Do NOT stop after listing options.`,resolve(){let s=D();if(!s)return null;let r={};for(let e of this.envKeys)process.env[e]&&(r[e]=process.env[e]);process.env.ATLASSIAN_INSTANCE_URL&&(r.ATLASSIAN_INSTANCE_URL=process.env.ATLASSIAN_INSTANCE_URL);for(let e of["JIRA_API_TOKEN","JIRA_EMAIL","JIRA_BASE_URL"])process.env[e]&&(r[e]=process.env[e]);return{command:"node",args:[s],env:r,description:this.description}},async handleToolCall(s,r){try{switch(s){case"jira_list_projects":{let e=await y("/rest/api/3/project"),t=(Array.isArray(e)?e:[]).map(a=>({id:a.id,key:a.key,name:a.name,style:a.style}));return JSON.stringify({count:t.length,projects:t})}case"jira_list_statuses":{let{projectKey:e}=r||{};if(e){let i=await y(`/rest/api/3/project/${encodeURIComponent(e)}/statuses`),o=Array.isArray(i)?i:[],n=new Map;for(let l of o)for(let u of l.statuses||[])u?.id&&(n.has(u.id)||n.set(u.id,{id:u.id,name:u.name,category:u.statusCategory?.name||null}));let c=[...n.values()].sort((l,u)=>String(l.name).localeCompare(String(u.name)));return JSON.stringify({scope:"project",projectKey:e,count:c.length,statuses:c})}let t=await y("/rest/api/3/status"),a=(Array.isArray(t)?t:[]).map(i=>({id:i.id,name:i.name,category:i.statusCategory?.name||null})).sort((i,o)=>String(i.name).localeCompare(String(o.name)));return JSON.stringify({scope:"global",count:a.length,statuses:a})}case"jira_list_issue_types":{let{projectKey:e}=r||{};if(!e)return JSON.stringify({error:"projectKey is required"});let t=await C(e);return JSON.stringify({projectKey:e,count:t.length,issueTypes:t})}case"jira_search":{let e=r.jql||"",t=r.maxResults||20;e.replace(/\s*ORDER\s+BY\s+.*/i,"").trim()||(e=`created >= -365d ${e}`.trim());let i=`jql=${encodeURIComponent(e)}&maxResults=${t}&fields=summary,status,assignee,priority,updated,issuetype,project`,n=((await y(`/rest/api/3/search/jql?${i}`)).issues||[]).map(c=>({key:c.key,project:c.fields?.project?.key,summary:c.fields?.summary,status:c.fields?.status?.name,assignee:c.fields?.assignee?.displayName||"Unassigned",priority:c.fields?.priority?.name,type:c.fields?.issuetype?.name}));return JSON.stringify({count:n.length,issues:n})}case"jira_get_issue":{let e=r.issueKey;if(!e)return JSON.stringify({error:"issueKey is required"});let t=await y(`/rest/api/3/issue/${e}`);return JSON.stringify({key:t.key,project:t.fields?.project?.key,summary:t.fields?.summary,description:t.fields?.description,status:t.fields?.status?.name,assignee:t.fields?.assignee?.displayName||"Unassigned",priority:t.fields?.priority?.name,type:t.fields?.issuetype?.name,labels:t.fields?.labels,created:t.fields?.created,updated:t.fields?.updated})}case"jira_create_issue":{let{projectKey:e,summary:t,issueType:a,description:i,priority:o,labels:n,assigneeId:c,moveToSprint:l,moveToActiveSprint:u,sprintId:d,sprintName:m,target:g}=r;if(!e||!t)return JSON.stringify({error:"projectKey and summary are required"});let p={requested:a||null,resolved:null,strategy:"none"},h=[];try{h=await C(e),p=M(a,h)}catch{}let f={project:{key:e},summary:t,issuetype:p?.resolved?.id?{id:p.resolved.id}:{name:a||"Task"}};i&&(f.description={type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:i}]}]}),o&&(f.priority={name:o}),n?.length&&(f.labels=n),c&&(f.assignee={id:c});let j=await y("/rest/api/3/issue",{method:"POST",body:{fields:f}}),_={ok:!0,key:j.key,id:j.id,self:j.self};return p?.resolved&&(_.issueType=p.resolved.name,_.issueTypeResolution=p.strategy,p.strategy!=="exact"&&p.requested&&b(p.requested)!==b(p.resolved.name)&&(_.issueTypeWarning=`Requested "${p.requested}" is not available in ${e}; used "${p.resolved.name}" instead.`)),h.length>0&&(_.availableIssueTypes=h.map(k=>k.name)),(l||u)&&(_.sprintMove=await N({issueKey:j.key,projectKey:e,sprintId:d,sprintName:m,target:g})),JSON.stringify(_)}case"jira_list_sprints":{let{projectKey:e,state:t}=r,a=await P(e,t);return JSON.stringify({count:a.length,sprints:a})}case"jira_move_to_active_sprint":{let{issueKey:e,projectKey:t,sprintId:a,sprintName:i,target:o}=r||{},n=await N({issueKey:e,projectKey:t,sprintId:a,sprintName:i,target:o||"current"});return JSON.stringify(n)}case"jira_move_issue_to_sprint":{let{issueKey:e,projectKey:t,sprintId:a,sprintName:i,target:o}=r||{},n=await N({issueKey:e,projectKey:t,sprintId:a,sprintName:i,target:o});return JSON.stringify(n)}case"jira_get_sprint_issues":{let{sprintName:e,sprintId:t,projectKey:a,status:i,maxResults:o}=r;if(!e&&!t)return JSON.stringify({error:"sprintName or sprintId is required"});let n=o||50,c=t?`sprint = ${t}`:`sprint = "${e}"`,l=a?`project = ${a} AND `:"",u=i?` AND status = "${i}"`:"",d=`${l}${c}${u} ORDER BY status ASC, priority DESC`,m=`jql=${encodeURIComponent(d)}&maxResults=${n}&fields=summary,status,assignee,priority,issuetype,project`,g=await y(`/rest/api/3/search/jql?${m}`),p=(g.issues||[]).map(f=>({key:f.key,project:f.fields?.project?.key,summary:f.fields?.summary,status:f.fields?.status?.name,assignee:f.fields?.assignee?.displayName||"Unassigned",priority:f.fields?.priority?.name,type:f.fields?.issuetype?.name})),h={};for(let f of p)h[f.status]=(h[f.status]||0)+1;return JSON.stringify({count:p.length,total:g.total||p.length,statusCounts:h,issues:p})}case"jira_get_comments":{let{issueKey:e,maxResults:t}=r;if(!e)return JSON.stringify({error:"issueKey is required"});let i=await y(`/rest/api/3/issue/${e}/comment?maxResults=${t||50}&orderBy=-created`),o=(i.comments||[]).map(n=>{let c="";return n.body?.content&&(c=w(n.body.content)),{id:n.id,author:n.author?.displayName||"Unknown",body:c,created:n.created,updated:n.updated}});return JSON.stringify({count:o.length,total:i.total||o.length,comments:o})}case"jira_add_comment":{let{issueKey:e,body:t}=r;return!e||!t?JSON.stringify({error:"issueKey and body are required"}):(await y(`/rest/api/3/issue/${e}/comment`,{method:"POST",body:{body:{type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:t}]}]}}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_edit_issue":{let{issueKey:e,fields:t}=r;return!e||!t?JSON.stringify({error:"issueKey and fields are required"}):(await y(`/rest/api/3/issue/${e}`,{method:"PUT",body:{fields:t}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_transition_issue":{let{issueKey:e,transitionId:t,toStatus:a,statusName:i,status:o}=r;if(!e)return JSON.stringify({error:"issueKey is required"});let n=String(a||i||o||"").trim();if(!t&&!n){let d=((await y(`/rest/api/3/issue/${e}/transitions`)).transitions||[]).map(m=>({id:m.id,name:m.name,to:m.to?.name}));return JSON.stringify({ok:!1,error:"transitionId or toStatus is required",issueKey:e,availableTransitions:d})}let c=t;if(!c){let d=(await y(`/rest/api/3/issue/${e}/transitions`)).transitions||[],m=v(n),g=d.find(p=>v(p?.name||"")===m||v(p?.to?.name||"")===m);if(!g){let p=A(n);p.length>=2&&(g=d.find(h=>{let f=A(h?.name||""),j=A(h?.to?.name||""),_=f.length>=2&&(f.includes(p)||p.includes(f)),k=j.length>=2&&(j.includes(p)||p.includes(j));return _||k}))}if(!g){let p=d.map(_=>{let k=I(n,_?.name||""),q=I(n,_?.to?.name||"");return{t:_,score:Math.max(k,q)}}).sort((_,k)=>k.score-_.score),h=p[0],f=p[1];h&&h.score>=.45&&(!f||h.score-f.score>=.12)&&(g=h.t)}if(!g?.id)return JSON.stringify({ok:!1,error:`No transition matches target status: "${n}"`,issueKey:e,availableTransitions:d.map(p=>({id:p.id,name:p.name,to:p.to?.name}))});c=g.id}await y(`/rest/api/3/issue/${e}/transitions`,{method:"POST",body:{transition:{id:c}}});let l=await y(`/rest/api/3/issue/${e}?fields=status`);return JSON.stringify({ok:!0,issueKey:e,transitionId:c,statusAfter:l?.fields?.status?.name||null})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(e){return JSON.stringify({error:e.message})}},tools:[{name:"jira_list_projects",description:"List all Jira projects accessible to the user",input_schema:{type:"object",properties:{}}},{name:"jira_list_statuses",description:"List Jira statuses. Use projectKey to get statuses applicable in that project workflow.",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Optional project key (e.g. PROJ). If omitted, returns global status catalog."}}}},{name:"jira_list_issue_types",description:"List issue types allowed for issue creation in the given project.",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"}},required:["projectKey"]}},{name:"jira_search",description:"Search Jira issues using JQL",input_schema:{type:"object",properties:{jql:{type:"string",description:'JQL query string, e.g. "project = PROJ AND status = Open"'},maxResults:{type:"number",description:"Max results to return (default 20)"}},required:["jql"]}},{name:"jira_get_issue",description:"Get details of a specific Jira issue",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"}},required:["issueKey"]}},{name:"jira_create_issue",description:"Create a new Jira issue",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"},summary:{type:"string",description:"Issue title/summary"},issueType:{type:"string",description:"Issue type (default: Task). Common: Task, Bug, Story, Epic"},description:{type:"string",description:"Issue description (plain text)"},priority:{type:"string",description:"Priority name, e.g. High, Medium, Low"},labels:{type:"array",items:{type:"string"},description:"Array of label strings"},assigneeId:{type:"string",description:"Atlassian account ID to assign to"},moveToSprint:{type:"boolean",description:"If true, move created issue to a sprint and verify."},moveToActiveSprint:{type:"boolean",description:"Backward-compatible alias for moveToSprint."},sprintId:{type:"number",description:"Optional sprint id for placement."},sprintName:{type:"string",description:"Optional sprint name for placement."},target:{type:"string",description:"Placement target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["projectKey","summary"]}},{name:"jira_list_sprints",description:"List sprints for a Jira project (returns sprint names, IDs, states, dates)",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"},state:{type:"string",description:"Filter: active, closed, future. Omit for all."}},required:["projectKey"]}},{name:"jira_get_sprint_issues",description:"Get all issues in a sprint, optionally filtered by status column name",input_schema:{type:"object",properties:{sprintName:{type:"string",description:"Sprint name (from jira_list_sprints). Use this OR sprintId."},sprintId:{type:"number",description:"Sprint ID (from jira_list_sprints). Use this OR sprintName."},projectKey:{type:"string",description:"Project key to scope the search (optional)"},status:{type:"string",description:'Filter by status name (e.g. "\u8FDB\u884C\u4E2D", "\u6D4B\u8BD5", "Done")'},maxResults:{type:"number",description:"Max issues to return (default 50)"}}}},{name:"jira_move_to_active_sprint",description:"Backward-compatible alias: move issue to sprint target and verify membership.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},projectKey:{type:"string",description:"Optional project key. If omitted, inferred from issue."},sprintId:{type:"number",description:"Optional sprint id."},sprintName:{type:"string",description:"Optional sprint name."},target:{type:"string",description:"Target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["issueKey"]}},{name:"jira_move_issue_to_sprint",description:"Move an issue to a sprint by id/name/target and verify membership.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},projectKey:{type:"string",description:"Optional project key. If omitted, inferred from issue."},sprintId:{type:"number",description:"Optional sprint id."},sprintName:{type:"string",description:"Optional sprint name."},target:{type:"string",description:"Target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["issueKey"]}},{name:"jira_get_comments",description:"Get comments on a Jira issue (newest first)",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},maxResults:{type:"number",description:"Max comments to return (default 50)"}},required:["issueKey"]}},{name:"jira_add_comment",description:"Add a comment to a Jira issue",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},body:{type:"string",description:"Comment text (plain text)"}},required:["issueKey","body"]}},{name:"jira_edit_issue",description:"Update fields on a Jira issue (summary, story points, labels, priority)",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},fields:{type:"object",description:"Object of field names to values",additionalProperties:!0}},required:["issueKey","fields"]}},{name:"jira_transition_issue",description:"Move a Jira issue to a different status. Always pass toStatus when user gave a target; only pass issueKey alone when you explicitly need to list transitions.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},transitionId:{type:"string",description:"Transition ID to perform (optional if toStatus is provided)"},toStatus:{type:"string",description:'Target status/column name (e.g. "\u5DF2\u7ECF\u9A8C\u6536", "Done", "In Progress"). If provided, tool resolves matching transition automatically.'}},required:["issueKey"]}}]};var W={new:"todo",indeterminate:"in_progress",done:"done"},Q=/\b(blocked|on[\s-]?hold|waiting|stuck)\b/i;function T(s){if(!s)return"unknown";if(s.name&&Q.test(s.name))return"blocked";let r=s.statusCategory?.key;return W[r]||"unknown"}function J(s){if(!s)return"";if(Array.isArray(s))return s.map(J).join("");if(s.type==="text")return s.text||"";if(s.type==="hardBreak"||s.type==="rule")return`
|
|
70
|
+
`;let r=s.content?J(s.content):"";return/^(paragraph|heading|listItem|blockquote|codeBlock|panel)$/.test(s.type)&&(r+=`
|
|
71
|
+
`),r}function O(s,r){let e=s.fields||{},t=e.status||null,a=null;if(s.self&&s.key)try{a=`${new URL(s.self).origin}/browse/${s.key}`}catch{a=null}return!a&&r&&s.key&&(a=`${String(r).replace(/\/+$/,"")}/browse/${s.key}`),{id:String(s.id??s.key??""),key:s.key,title:e.summary||"",body:typeof e.description=="string"?e.description:J(e.description?.content).trim(),state:t?.name||null,stateCategory:T(t),assignee:e.assignee?.displayName||null,url:a,_raw:s}}function R(s){let r=JSON.parse(s);if(r&&r.error)throw new Error(r.error);return r}var V={id:"jira",toStateCategory:T,toNeutral:O,async listCandidates(s={}){let r=s.query||"";if(!r.trim()){let i=[];if(s.state&&i.push(`status = "${s.state}"`),s.labels){let o=Array.isArray(s.labels)?s.labels:[s.labels];for(let n of o)i.push(`labels = "${n}"`)}s.updatedAfter&&i.push(`updated >= "${s.updatedAfter}"`),r=i.length?`${i.join(" AND ")} ORDER BY updated DESC`:"ORDER BY updated DESC"}let e=Number(s.limit)||30,t=`jql=${encodeURIComponent(r)}&maxResults=${e}&fields=summary,description,status,assignee,labels,project,updated,created`;return((await y(`/rest/api/3/search/jql?${t}`)).issues||[]).map(i=>O(i))},async getTicket(s){if(!s)throw new Error("key is required");let r=await y(`/rest/api/3/issue/${encodeURIComponent(s)}`);return!r||!r.key?null:O(r)},async getComments(s){if(!s)throw new Error("key is required");return(R(await S.handleToolCall("jira_get_comments",{issueKey:s})).comments||[]).map(e=>({id:String(e.id),author:e.author||"Unknown",body:e.body||"",createdAt:e.created||null,updatedAt:e.updated||null,_raw:e}))},async addComment(s,r){if(!s||!r)throw new Error("key and body are required");return{ok:!!R(await S.handleToolCall("jira_add_comment",{issueKey:s,body:r})).ok,id:null}},async transition(s,r){if(!s)throw new Error("key is required");let e=JSON.parse(await S.handleToolCall("jira_transition_issue",{issueKey:s,toStatus:r}));if(!e.ok)return{ok:!1,error:e.error||"transition failed",_raw:e};let t=e.statusAfter||null;return{ok:!0,stateAfter:t,stateCategoryAfter:T(t?{name:t}:null),_raw:e}},async linkPullRequest(s,r,e){if(!s||!r)throw new Error("key and prUrl are required");try{return await y(`/rest/api/3/issue/${encodeURIComponent(s)}/remotelink`,{method:"POST",body:{object:{url:r,title:e||r,icon:{url16x16:"https://github.com/favicon.ico",title:"GitHub"}}}}),{ok:!0,via:"remotelink"}}catch(t){let a=`${e?`${e}: `:"Linked PR: "}${r}`;return{ok:!!R(await S.handleToolCall("jira_add_comment",{issueKey:s,body:a})).ok,via:"comment",error:String(t?.message||t)}}}},ne=V;export{ne as default,V as jiraAdapter};
|
package/package.json
CHANGED
package/docs/analysis.md
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
sidebar_position: 4
|
|
3
|
-
title: Running Analysis
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Running Analysis
|
|
7
|
-
|
|
8
|
-
Analysis is where Zibby's AI reads your Jira ticket, studies your codebase, and produces code changes and test cases.
|
|
9
|
-
|
|
10
|
-
## Starting an Analysis
|
|
11
|
-
|
|
12
|
-
1. Go to your project's **Tickets** page
|
|
13
|
-
2. Click on a ticket to open its details
|
|
14
|
-
3. Click **Analyze**
|
|
15
|
-
4. Optionally add **additional context** or select an AI model
|
|
16
|
-
5. The analysis runs in the cloud (AWS Fargate)
|
|
17
|
-
|
|
18
|
-
## Analysis Pipeline
|
|
19
|
-
|
|
20
|
-
The AI runs through these steps automatically:
|
|
21
|
-
|
|
22
|
-
### 1. Setup
|
|
23
|
-
|
|
24
|
-
- Clones your GitHub repository into a secure container
|
|
25
|
-
- Creates a git baseline for tracking changes
|
|
26
|
-
|
|
27
|
-
### 2. Analyze Ticket
|
|
28
|
-
|
|
29
|
-
The AI reads:
|
|
30
|
-
- The Jira ticket summary and description
|
|
31
|
-
- Acceptance criteria and requirements
|
|
32
|
-
- Your codebase structure and existing code
|
|
33
|
-
|
|
34
|
-
It produces a **structured analysis** with:
|
|
35
|
-
- Understanding of the requirement
|
|
36
|
-
- Affected files and components
|
|
37
|
-
- Implementation approach
|
|
38
|
-
- Edge cases and considerations
|
|
39
|
-
|
|
40
|
-
If the ticket lacks sufficient detail, the analysis may stop here with a **"needs clarification"** status and explain what information is missing.
|
|
41
|
-
|
|
42
|
-
### 3. Generate Code
|
|
43
|
-
|
|
44
|
-
Based on the analysis, the AI:
|
|
45
|
-
- Writes or modifies code to implement the ticket
|
|
46
|
-
- Captures all changes as a git diff
|
|
47
|
-
- Lists affected files
|
|
48
|
-
|
|
49
|
-
You can review the diff side-by-side in the dashboard and create a **Pull Request** directly.
|
|
50
|
-
|
|
51
|
-
### 4. Generate Test Cases
|
|
52
|
-
|
|
53
|
-
The AI produces structured test cases including:
|
|
54
|
-
- Test case title and description
|
|
55
|
-
- Step-by-step instructions
|
|
56
|
-
- Test data and credentials
|
|
57
|
-
- Expected outcomes
|
|
58
|
-
- Preconditions and postconditions
|
|
59
|
-
|
|
60
|
-
### 5. Finalize
|
|
61
|
-
|
|
62
|
-
- Compiles the final report
|
|
63
|
-
- Uploads all artifacts to the dashboard
|
|
64
|
-
- Updates the execution status
|
|
65
|
-
|
|
66
|
-
## Custom Instructions
|
|
67
|
-
|
|
68
|
-
You can add custom prompt instructions for each AI node in **Project Settings > Node Configuration**:
|
|
69
|
-
|
|
70
|
-
- **analyze_ticket** — additional context about your architecture
|
|
71
|
-
- **generate_code** — coding standards, framework preferences
|
|
72
|
-
- **generate_test_cases** — test credentials, login flows, specific data to use
|
|
73
|
-
|
|
74
|
-
For example, to ensure tests use specific credentials:
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
Use test account for login:
|
|
78
|
-
Email: test@example.com
|
|
79
|
-
Password: TestPass123
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Analysis Statuses
|
|
83
|
-
|
|
84
|
-
| Status | Meaning |
|
|
85
|
-
|---|---|
|
|
86
|
-
| **running** | Analysis is in progress |
|
|
87
|
-
| **completed** | All steps finished successfully |
|
|
88
|
-
| **failed** | A step encountered an error |
|
|
89
|
-
| **blocked** | Ticket lacks information to proceed |
|
|
90
|
-
|
|
91
|
-
## Reviewing Analysis Results
|
|
92
|
-
|
|
93
|
-
After completion, the analysis page shows:
|
|
94
|
-
|
|
95
|
-
- **Pipeline progress** — status of each step with logs
|
|
96
|
-
- **Code Changes** — side-by-side diff with file tree
|
|
97
|
-
- **Test Cases** — editable test cases you can refine
|
|
98
|
-
- **Create PR** — one-click pull request creation
|
|
99
|
-
- **Suggested Changes** — AI suggestions for the ticket itself
|
|
100
|
-
|
|
101
|
-
## Running Tests from Analysis
|
|
102
|
-
|
|
103
|
-
Once test cases are generated, you can run them locally using the CLI:
|
|
104
|
-
|
|
105
|
-
```bash
|
|
106
|
-
zibby test --sources TC001,TC002 --execution <execution-id> --sync
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Or copy the full command from the **Code** button in the dashboard.
|
package/docs/apps/agent-ops.md
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
sidebar_position: 5
|
|
3
|
-
title: Agent operator
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# The agent-ops sidecar
|
|
7
|
-
|
|
8
|
-
Every Zibby Managed App ships with **agent-ops**, an autonomous daemon sidecar that runs alongside the app container. It does what a human operator would do — checks health, restarts on crash, prunes disk, rolls upgrades — only it does it every hour, never sleeps, and never forgets to file a runbook.
|
|
9
|
-
|
|
10
|
-
This is the structural difference between "deploy button on a VM" and **Zibby**.
|
|
11
|
-
|
|
12
|
-
## What it does
|
|
13
|
-
|
|
14
|
-
| Task | Cadence | What happens |
|
|
15
|
-
|---|---|---|
|
|
16
|
-
| **Hourly health check** | every 60 min | HTTP probe + container state + EFS usage. Recorded as a structured run record. |
|
|
17
|
-
| **Self-heal on OOM** | event-driven | Container exits with OOMKilled → agent-ops triggers an ECS restart, records the recovery. |
|
|
18
|
-
| **Disk-pressure prune** | when EFS > 90% | Removes safe-to-delete caches (e.g. n8n execution history older than 30 days). Configurable. |
|
|
19
|
-
| **Upgrade orchestration** | on schedule | When a new app version lands in the catalog, agent-ops can run the in-place upgrade on a cron you set. |
|
|
20
|
-
| **Activity log** | every action | One row in the app's "Agent activity" tab, with structured fields you can grep / chart. |
|
|
21
|
-
|
|
22
|
-
Every action lands in DynamoDB as an `app-runs` record — queryable by anything from an agent node to a Grafana dashboard.
|
|
23
|
-
|
|
24
|
-
## See it in action
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
zibby app activity a1b2c3d4
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
Time Action Status Duration Notes
|
|
32
|
-
14:00:01 hourly_health_check ok 1.2s
|
|
33
|
-
13:00:01 hourly_health_check ok 1.1s
|
|
34
|
-
12:04:38 oom_recovery ok 4.8s restarted container after OOMKilled
|
|
35
|
-
12:00:01 hourly_health_check warn 2.1s container restarting
|
|
36
|
-
11:00:01 hourly_health_check ok 1.3s
|
|
37
|
-
10:00:01 hourly_health_check ok 1.0s
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
The dashboard's "Agent activity" tab shows the same records with extra context (HTTP status codes, container logs at failure time, recovery diff).
|
|
41
|
-
|
|
42
|
-
## Run records ≠ logs
|
|
43
|
-
|
|
44
|
-
A run record is **structured metadata** about something agent-ops did:
|
|
45
|
-
|
|
46
|
-
```json
|
|
47
|
-
{
|
|
48
|
-
"instanceId": "a1b2c3d4",
|
|
49
|
-
"runId": "01J9KZQF...",
|
|
50
|
-
"action": "hourly_health_check",
|
|
51
|
-
"status": "ok",
|
|
52
|
-
"startedAt": "2026-05-30T14:00:01Z",
|
|
53
|
-
"duration_ms": 1234,
|
|
54
|
-
"httpStatus": 200,
|
|
55
|
-
"containerState": "RUNNING"
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Logs are unstructured text. Run records are queryable, chartable, and aggregatable — that's why the Agent activity tab can show a 30-day uptime % without you running grep.
|
|
60
|
-
|
|
61
|
-
The CLI exposes them:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
zibby app activity a1b2c3d4 --since 7d
|
|
65
|
-
zibby app activity a1b2c3d4 --action oom_recovery
|
|
66
|
-
zibby app activity a1b2c3d4 --json | jq '.[] | select(.status == "warn")'
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## Why a sidecar, not a centralized controller?
|
|
70
|
-
|
|
71
|
-
Three properties only this shape gives you:
|
|
72
|
-
|
|
73
|
-
1. **No noisy-neighbor failure mode** — your instance's agent-ops can't be blocked by another instance's slow health check
|
|
74
|
-
2. **Per-instance customization without a central feature flag** — env vars (`AGENT_OPS_CHECK_INTERVAL_MIN=15`, `AGENT_OPS_PRUNE_THRESHOLD=80`) live on your instance and just work
|
|
75
|
-
3. **Egress identity matches the app** — outbound calls from agent-ops use the same ENI / NAT path as the app itself, so when the app has a dedicated egress IP, agent-ops's webhook callbacks come from the same IP
|
|
76
|
-
|
|
77
|
-
## Customize via env
|
|
78
|
-
|
|
79
|
-
Per-instance agent-ops behavior is tunable via env vars (set on the app instance — Apps → ENV tab — or via `zibby app env set`):
|
|
80
|
-
|
|
81
|
-
| Env var | Default | What it controls |
|
|
82
|
-
|---|---|---|
|
|
83
|
-
| `AGENT_OPS_CHECK_INTERVAL_MIN` | 60 | Minutes between hourly health checks |
|
|
84
|
-
| `AGENT_OPS_PRUNE_THRESHOLD` | 90 | EFS usage % that triggers disk-pressure prune |
|
|
85
|
-
| `AGENT_OPS_AUTO_UPGRADE` | `false` | If `true`, upgrade automatically when catalog publishes a new version |
|
|
86
|
-
| `AGENT_OPS_NOTIFY_WEBHOOK` | — | URL to POST run records to (any HTTPS endpoint — your own backend, n8n, etc.) |
|
|
87
|
-
|
|
88
|
-
`AGENT_OPS_NOTIFY_WEBHOOK` is how you wire agent-ops into your existing observability stack — fire every run record into your team's #ops Slack via an agent trigger, into Datadog via their webhook receiver, or into your own database.
|
|
89
|
-
|
|
90
|
-
## Hooking agent-ops into an agent
|
|
91
|
-
|
|
92
|
-
The most powerful pattern: a Zibby agent that runs **on agent-ops events**.
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
```bash
|
|
97
|
-
zibby app env set a1b2c3d4 \
|
|
98
|
-
AGENT_OPS_NOTIFY_WEBHOOK=https://api-prod.zibby.app/v1/workflows/<wf-uuid>/trigger
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
The 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
|
-
|
|
103
|
-
## Upgrade orchestration
|
|
104
|
-
|
|
105
|
-
When you `zibby app upgrade <id>` manually, agent-ops watches the rollout and rolls back if the new task fails health checks twice in a row. With `AGENT_OPS_AUTO_UPGRADE=true` set, the upgrade fires on a cron (default: weekly, Sunday 04:00 UTC) — agent-ops runs the same flow:
|
|
106
|
-
|
|
107
|
-
1. Register new task definition revision (catalog's latest)
|
|
108
|
-
2. Update service, watch the rollout
|
|
109
|
-
3. If 2 consecutive health checks pass on the new revision → keep it
|
|
110
|
-
4. If 2 fail → roll back to the previous revision, log a `failed_upgrade` run record
|
|
111
|
-
|
|
112
|
-
The activity log shows the full attempted upgrade timeline so you can see why a rollback happened.
|
|
113
|
-
|
|
114
|
-
## How goal-mode deploys are supervised
|
|
115
|
-
|
|
116
|
-
When you `zibby app deploy --goal "..."` (see [Goal-mode deploys](./goal-mode)), agent-ops switches into a different mode at task start: `AGENT_OPS_BOOTSTRAP_MODE=agent_script`. Instead of "run a known image and health-check it", it does:
|
|
117
|
-
|
|
118
|
-
1. **Plan** — Claude (Write+Read tools only, no Bash) writes a complete `/tmp/install.sh` from the customer's goal text + the house rules. ~2 turns, ~$0.05 in tokens.
|
|
119
|
-
2. **Supervise** — agent-ops execs the script in a process group, then every 30s sends Claude (text-only) a snapshot of stdout/stderr tail + proc status. Claude returns one JSON line: `continue`, `done`, or `intervene`. On `intervene`, agent-ops SIGTERMs the process group, gives it 5s grace, SIGKILLs, and replans with the previous failure context.
|
|
120
|
-
3. **Auto-short-circuit** — if the script exits with code 0 AND the verify port returns 2xx-499, agent-ops declares `done` without consulting the supervisor. This catches the false-positive `intervene` that fires when the planner backgrounds the app with nohup and Claude can't see "startup logs" in the snapshot.
|
|
121
|
-
|
|
122
|
-
Hard caps: 5 iterations, 30 min wall-clock, $1.00 token budget — whichever hits first wins. Every iteration's script, supervisor turns, and final status are persisted under `/var/lib/agent-ops/agent_script-state/` on the per-instance EFS volume — `zibby app logs <id>` surfaces the timeline.
|
|
123
|
-
|
|
124
|
-
### House rules
|
|
125
|
-
|
|
126
|
-
agent-ops reads `AGENT_OPS_BOOTSTRAP_SYSTEM_RULES` at task start and prepends it to the planner's system prompt. We curate ~6 rules in the backend covering things like: "Write COMPLETE bash scripts per turn, never run individual commands", "Never background installs; only the final long-running app process", "End with a curl health-check loop on the target port", "If install > 15 min, switch to a pre-built distribution". The rules are env-driven so we can iterate wording without rebuilding agent-ops — and it's why goal-mode deploys tend to converge reliably rather than oscillate between "almost done" and "container exited".
|
|
127
|
-
|
|
128
|
-
You don't set these yourself; they're operator-curated. But they're the reason the planner consistently emits an install script that ends in a working `curl localhost:<port>` health check, not a half-finished session of individual commands.
|
|
129
|
-
|
|
130
|
-
→ Done with apps. See [Goal-mode deploys](./goal-mode), [Auth proxy](./auth), or [CLI Reference](../cli-reference#app-commands) for the full `zibby app` command surface.
|
package/docs/apps/auth.md
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
sidebar_position: 4
|
|
3
|
-
title: Auth proxy
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Auth proxy
|
|
7
|
-
|
|
8
|
-
Most self-hosted apps either ship with weak default credentials (`admin/admin` Grafana) or no app-level auth at all (n8n's free tier, raw webhook receivers). The auth proxy is a per-instance opt-in **Caddy sidecar** that fronts the ALB and validates a credential before letting the request reach the app.
|
|
9
|
-
|
|
10
|
-
It's not a replacement for the app's own login system — it's a pre-filter. The app keeps its own session model; the proxy just makes sure the request never gets there without a valid credential.
|
|
11
|
-
|
|
12
|
-
## Architecture
|
|
13
|
-
|
|
14
|
-
```
|
|
15
|
-
without auth:
|
|
16
|
-
client → ALB → task:<appPort>
|
|
17
|
-
|
|
18
|
-
with --auth-type basic:
|
|
19
|
-
client → ALB → task:8888 (caddy)
|
|
20
|
-
│ basic-auth check (bcrypt hash)
|
|
21
|
-
↓
|
|
22
|
-
localhost:<appPort> (app)
|
|
23
|
-
|
|
24
|
-
with --auth-type token:
|
|
25
|
-
client → ALB → task:8888 (caddy)
|
|
26
|
-
│ Authorization: Bearer <token>
|
|
27
|
-
↓
|
|
28
|
-
localhost:<appPort> (app)
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
The Caddy image is `caddy:2-alpine` direct (no custom ECR build). The entrypoint is an inline `sh -c` script that templates the Caddyfile from env at boot. The app keeps listening on its own internal port (unchanged); ALB just routes to Caddy instead.
|
|
32
|
-
|
|
33
|
-
Cost: zero. Same Fargate task, +1 small container (~30 MB RAM, ~0 CPU).
|
|
34
|
-
|
|
35
|
-
## Storage
|
|
36
|
-
|
|
37
|
-
- **Basic auth.** Password is bcrypt-hashed at the backend; the hash lives in the encrypted env bag. Plaintext is never persisted server-side.
|
|
38
|
-
- **Token auth.** Token is KMS-encrypted at rest. Backend keeps the last 4 characters for audit (so `zibby app status` can show `Auth: token (…a3f9)` without leaking the secret). If the customer doesn't supply a token, the backend generates a 32-char URL-safe random one and returns it ONCE in the deploy response.
|
|
39
|
-
|
|
40
|
-
You can rotate either at any time without redeploying the app (see `set-auth` below) — only the Caddy container restarts.
|
|
41
|
-
|
|
42
|
-
## Deploying with auth
|
|
43
|
-
|
|
44
|
-
### Basic auth
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
zibby app deploy grafana \
|
|
48
|
-
--project <project-id> \
|
|
49
|
-
--name metrics \
|
|
50
|
-
--auth-type basic \
|
|
51
|
-
--auth-user admin \
|
|
52
|
-
--auth-password 'S0me-long-passphrase!'
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
`--auth-user` is printable ASCII (no spaces), 1-64 chars. `--auth-password` is 8-256 chars and can come from `ZIBBY_APP_AUTH_PASSWORD` env to keep it out of shell history.
|
|
56
|
-
|
|
57
|
-
Verify:
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
curl -I https://a1b2c3d4.apps.zibby.dev
|
|
61
|
-
# HTTP/2 401 ← Caddy bounces unauthenticated request
|
|
62
|
-
|
|
63
|
-
curl -I -u 'admin:S0me-long-passphrase!' https://a1b2c3d4.apps.zibby.dev
|
|
64
|
-
# HTTP/2 302 ← Grafana redirects to /login (the app's own auth, behind the proxy)
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Token auth (auto-generated)
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
zibby app deploy gotify --project <id> --name notify --auth-type token
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Response includes:
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
✔ Deployed (instanceId: f1e2d3c4)
|
|
77
|
-
→ Public URL: https://f1e2d3c4.apps.zibby.dev
|
|
78
|
-
→ Auth token: 7Kf3uL9pXmQ2vR8sT4nW6yE1bH5dG0aC
|
|
79
|
-
^^^ shown ONCE — save it now, you can't retrieve it
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Verify:
|
|
83
|
-
|
|
84
|
-
```bash
|
|
85
|
-
curl -I https://f1e2d3c4.apps.zibby.dev
|
|
86
|
-
# HTTP/2 401
|
|
87
|
-
|
|
88
|
-
curl -I -H 'Authorization: Bearer 7Kf3uL9pXmQ2vR8sT4nW6yE1bH5dG0aC' \
|
|
89
|
-
https://f1e2d3c4.apps.zibby.dev
|
|
90
|
-
# HTTP/2 200
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Token auth (caller-supplied)
|
|
94
|
-
|
|
95
|
-
If you already have a token (e.g. minted by your own auth system), pass it explicitly:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
zibby app deploy gotify \
|
|
99
|
-
--project <id> --name notify \
|
|
100
|
-
--auth-type token \
|
|
101
|
-
--auth-token "$(cat ~/.secrets/gotify-bearer.txt)"
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Also accepts `ZIBBY_APP_AUTH_TOKEN` env.
|
|
105
|
-
|
|
106
|
-
### No auth (the default)
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
zibby app deploy grafana --project <id> --name metrics
|
|
110
|
-
# No Caddy container; ALB routes straight to the app's port.
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
## Changing auth after deploy
|
|
114
|
-
|
|
115
|
-
`zibby app set-auth <instanceId>` is the rotate/replace endpoint. It has the same auth flags as `deploy`, with PATCH semantics — omitted flags preserve current state.
|
|
116
|
-
|
|
117
|
-
### Rotate just the password
|
|
118
|
-
|
|
119
|
-
```bash
|
|
120
|
-
zibby app set-auth a1b2c3d4 --auth-password 'N3w-passphrase-2026!'
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Only the password changes — auth type and username are preserved. The ECS task rolls (~60-90s) to pick up the new bcrypt hash; the app container keeps its data.
|
|
124
|
-
|
|
125
|
-
### Switch from basic to token
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
zibby app set-auth a1b2c3d4 --auth-type token --auth-token 'mY-shared-bearer-token'
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Switch from no-auth to basic
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
zibby app set-auth a1b2c3d4 \
|
|
135
|
-
--auth-type basic \
|
|
136
|
-
--auth-user admin \
|
|
137
|
-
--auth-password 'S0me-long-passphrase!'
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
This is the path most operators take: deploy quickly without auth, verify the app works, then put Caddy in front before exposing it to the world.
|
|
141
|
-
|
|
142
|
-
### Remove auth entirely
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
zibby app set-auth a1b2c3d4 --off
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
The Caddy container is stripped from the next task definition revision; the ALB target group flips back to the app's port. ~60-90s rolling replace.
|
|
149
|
-
|
|
150
|
-
## When to use which
|
|
151
|
-
|
|
152
|
-
- **Basic auth** — humans hitting a dashboard (Grafana, Uptime Kuma, draw.io, OpenObserve). One shared credential the team knows.
|
|
153
|
-
- **Token auth** — machines hitting an endpoint (webhook receivers, internal APIs, anything you're going to put in another tool's "header to send" box). Long random string, rotated on schedule.
|
|
154
|
-
- **No auth** — only when the app has its own SSO already wired up (Authentik, Zitadel) and you actively want the open URL.
|
|
155
|
-
|
|
156
|
-
The proxy doesn't replace per-user accounts inside the app — for that you still need the app's own auth (e.g. Grafana's user database, n8n's user table). Treat the proxy as a perimeter, not an identity system.
|
|
157
|
-
|
|
158
|
-
→ Next: [Goal-mode deploys](./goal-mode) or [Agent operator](./agent-ops)
|