@zibby/skills 0.1.23 → 0.1.25

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.
@@ -0,0 +1 @@
1
+ var t=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark"}),e=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"}});export{t as INTEGRATIONS,e as INTEGRATION_REGISTRY};
package/dist/jira.js CHANGED
@@ -1,22 +1,22 @@
1
- import{createRequire as $}from"module";import{resolveIntegrationToken as q,clearTokenCache as R}from"@zibby/core/backend-client.js";var K=$(import.meta.url);function T(){if(process.env.MCP_JIRA_PATH)return process.env.MCP_JIRA_PATH;try{return K.resolve("@zibby/mcp-jira/index.js")}catch{return null}}var C=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function x(c,a){if(!a||!a.length)return c;let e=c;for(let t of a)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 k(c,a=0){if(!Array.isArray(c))return"";let e=[];for(let t of c){if(t.type==="text"){e.push(x(t.text||"",t.marks));continue}if(t.type==="hardBreak"){e.push(`
1
+ import{createRequire as T}from"module";import{resolveIntegrationToken as $,clearTokenCache as q}from"@zibby/core/backend-client.js";var N=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark"}),U=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"}});var K=T(import.meta.url);function C(){if(process.env.MCP_JIRA_PATH)return process.env.MCP_JIRA_PATH;try{return K.resolve("@zibby/mcp-jira/index.js")}catch{return null}}var x=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function P(c,a){if(!a||!a.length)return c;let e=c;for(let t of a)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 S(c,a=0){if(!Array.isArray(c))return"";let e=[];for(let t of c){if(t.type==="text"){e.push(P(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 i=t.content?k(t.content,a+1):"";if(t.type==="listItem")e.push(i);else if(t.type==="bulletList"){let r=(t.content||[]).map(o=>`- ${k(o.content||[],a+1).trim()}`);e.push(`
4
+ `);continue}let i=t.content?S(t.content,a+1):"";if(t.type==="listItem")e.push(i);else if(t.type==="bulletList"){let r=(t.content||[]).map(o=>`- ${S(o.content||[],a+1).trim()}`);e.push(`
5
5
  ${r.join(`
6
6
  `)}
7
- `)}else if(t.type==="orderedList"){let r=(t.content||[]).map((o,s)=>`${s+1}. ${k(o.content||[],a+1).trim()}`);e.push(`
7
+ `)}else if(t.type==="orderedList"){let r=(t.content||[]).map((o,s)=>`${s+1}. ${S(o.content||[],a+1).trim()}`);e.push(`
8
8
  ${r.join(`
9
9
  `)}
10
10
  `)}else if(t.type==="heading"){let r=t.attrs?.level||2;e.push(`
11
11
 
12
12
  ${"#".repeat(r)} ${i.trim()}
13
13
 
14
- `)}else C.has(t.type)?e.push(`
14
+ `)}else x.has(t.type)?e.push(`
15
15
 
16
16
  ${i}
17
17
  `):e.push(i)}return e.join("").replace(/\n{3,}/g,`
18
18
 
19
- `)}function S(c){return String(c||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function I(c){return S(c).replace(/[a-z0-9]+/g,"")}function w(c,a){let e=S(c),t=S(a);if(!e||!t)return 0;if(e===t)return 1;if(e.length===1||t.length===1)return e===t?1:0;let i=u=>{let d=new Map;for(let y=0;y<u.length-1;y++){let g=u.slice(y,y+2);d.set(g,(d.get(g)||0)+1)}return d},r=i(e),o=i(t),s=0,n=0,p=0;for(let u of r.values())n+=u;for(let u of o.values())p+=u;for(let[u,d]of r.entries()){let y=o.get(u)||0;s+=Math.min(d,y)}return 2*s/Math.max(1,n+p)}function b(c){return String(c||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function P(c,a=[]){let e=Array.isArray(a)?a:[];if(e.length===0)return{requested:c||null,resolved:null,strategy:"none"};let t=e.filter(s=>!s.subtask),i=t.length>0?t:e,r=b(c);if(r){let s=i.find(u=>b(u.name)===r);if(s)return{requested:c,resolved:s,strategy:"exact"};let n={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(n)){if(!u.some(y=>b(y)===r))continue;let d=i.find(y=>u.some(g=>b(g)===b(y.name)));if(d)return{requested:c,resolved:d,strategy:"alias"}}let p=i.map(u=>({t:u,score:w(c,u.name)})).sort((u,d)=>d.score-u.score);if(p[0]&&p[0].score>=.5)return{requested:c,resolved:p[0].t,strategy:"fuzzy"}}let o=["task","story","bug","improvement","epic"];for(let s of o){let n=i.find(p=>b(p.name)===s);if(n)return{requested:c||null,resolved:n,strategy:"default-preferred"}}return{requested:c||null,resolved:i[0],strategy:"default-first"}}async function N(c){let a=`projectKeys=${encodeURIComponent(c)}&expand=projects.issuetypes`,e=await f(`/rest/api/3/issue/createmeta?${a}`),t=Array.isArray(e?.projects)?e.projects:[],r=t.find(s=>String(s?.key||"").toUpperCase()===String(c||"").toUpperCase())||t[0]||null;return(Array.isArray(r?.issuetypes)?r.issuetypes:[]).map(s=>({id:s.id,name:s.name,subtask:!!s.subtask,description:s.description||null}))}async function A(c,a){if(!c)throw new Error("projectKey is required");let e="sprint is not EMPTY";a==="active"?e="sprint in openSprints()":a==="closed"?e="sprint in closedSprints()":a==="future"&&(e="sprint in futureSprints()");let t=`project = ${c} AND ${e} ORDER BY updated DESC`,i=`jql=${encodeURIComponent(t)}&maxResults=100&fields=customfield_10020`,r=await f(`/rest/api/3/search/jql?${i}`),o=new Map;for(let s of r.issues||[])for(let n of s.fields?.customfield_10020||[])n&&!o.has(n.id)&&o.set(n.id,{id:n.id,name:n.name,state:n.state,boardId:n.boardId||null,startDate:n.startDate||null,endDate:n.endDate||null,goal:n.goal||null});return[...o.values()].sort((s,n)=>{let p={active:0,future:1,closed:2},u=(p[s.state]??3)-(p[n.state]??3);return u!==0?u:String(n.startDate||"").localeCompare(String(s.startDate||""))})}function D(c,{sprintId:a,sprintName:e,target:t}={}){let i=Array.isArray(c)?c:[];if(!i.length)return{sprint:null,selectedBy:"none"};if(a!=null&&String(a).trim()!=="")return{sprint:i.find(s=>String(s.id)===String(a))||null,selectedBy:"id"};if(e&&String(e).trim()){let o=String(e).trim(),s=i.find(p=>String(p.name||"").toLowerCase()===o.toLowerCase());if(s)return{sprint:s,selectedBy:"name-exact"};let n=i.map(p=>({s:p,score:w(o,p.name||"")})).sort((p,u)=>u.score-p.score);return n[0]&&n[0].score>=.5?{sprint:n[0].s,selectedBy:"name-fuzzy"}:{sprint:null,selectedBy:"name-none"}}let r=String(t||"current").trim().toLowerCase();return r==="active"||r==="current"||r==="latest"?{sprint:i[0],selectedBy:r}:{sprint:i[0],selectedBy:"default"}}function L(c,a){let e=c?.fields?.customfield_10020;return Array.isArray(e)?e.some(t=>String(t?.id)===String(a)):!1}async function B({issueKey:c,projectKey:a,sprintId:e,attempts:t=3,delayMs:i=450}){let r=[];for(let o=0;o<t;o++){try{let s=`project = ${a} AND key = ${c} AND sprint = ${e}`,n=`jql=${encodeURIComponent(s)}&maxResults=1&fields=key,status`,p=await f(`/rest/api/3/search/jql?${n}`);if(Number(p?.total||0)>0)return r.push({attempt:o+1,jql:!0,issueField:null}),{ok:!0,method:"jql",traces:r};let d=await f(`/rest/api/3/issue/${c}?fields=customfield_10020,status`),y=L(d,e);if(r.push({attempt:o+1,jql:!1,issueField:y}),y)return{ok:!0,method:"issue_field",traces:r}}catch(s){r.push({attempt:o+1,error:String(s?.message||s)})}o<t-1&&await new Promise(s=>setTimeout(s,i))}return{ok:!1,method:"none",traces:r}}async function O({issueKey:c,projectKey:a,sprintId:e,sprintName:t,target:i}){if(!c)return{ok:!1,error:"issueKey is required"};let r=a;if(!r&&(r=(await f(`/rest/api/3/issue/${c}?fields=project`))?.fields?.project?.key||null,!r))return{ok:!1,error:`Could not resolve project for ${c}`};let o=await A(r,"active");if(!o.length)return{ok:!1,error:`No assignable active sprint found for project ${r}`};let{sprint:s,selectedBy:n}=D(o,{sprintId:e,sprintName:t,target:i});if(!s)return{ok:!1,error:`No matching sprint found in ${r}`,requested:{sprintId:e??null,sprintName:t??null,target:i??"current"},availableSprints:o.map(d=>({id:d.id,name:d.name,state:d.state}))};await f(`/rest/api/3/issue/${c}`,{method:"PUT",body:{fields:{customfield_10020:Number(s.id)}}});let p=await B({issueKey:c,projectKey:r,sprintId:s.id}),u=p.ok;return{ok:u,issueKey:c,projectKey:r,sprintId:s.id,sprintName:s.name,selectedBy:n,verifiedBy:p.method,verified:u,verificationTrace:p.traces,warning:u?null:`Sprint assignment attempted but verification did not find ${c} in sprint ${s.id}`}}async function f(c,a={}){let e=async()=>{let{token:t,cloudId:i}=await q("jira");if(typeof t!="string"||!t)throw new Error(`Invalid jira token type: ${typeof t}`);if(!i)throw new Error("Invalid jira cloudId: missing");let r=`https://api.atlassian.com/ex/jira/${i}${c}`,o=await fetch(r,{method:a.method||"GET",headers:{Authorization:`Bearer ${t}`,Accept:"application/json",...a.body?{"Content-Type":"application/json"}:{},...a.headers},body:a.body?JSON.stringify(a.body):void 0});if(!o.ok){let n=await o.text().catch(()=>"");throw new Error(`Jira API ${o.status}: ${n.slice(0,300)}`)}let s=await o.text().catch(()=>"");if(!s||!s.trim())return{};try{return JSON.parse(s)}catch{return{raw:s}}};try{return await e()}catch(t){let i=String(t?.message||t||"").toLowerCase();if(!(i.includes("token")||i.includes("401")||i.includes("403")||i.includes("substring")))throw t;return R("jira"),e()}}var M={id:"jira",serverName:"jira",allowedTools:["mcp__jira__*"],envKeys:["ATLASSIAN_ACCESS_TOKEN","ATLASSIAN_CLOUD_ID"],description:"Zibby Jira MCP Server (OAuth Bearer)",promptFragment:`## Jira (connected)
19
+ `)}function k(c){return String(c||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function I(c){return k(c).replace(/[a-z0-9]+/g,"")}function w(c,a){let e=k(c),t=k(a);if(!e||!t)return 0;if(e===t)return 1;if(e.length===1||t.length===1)return e===t?1:0;let i=u=>{let d=new Map;for(let y=0;y<u.length-1;y++){let g=u.slice(y,y+2);d.set(g,(d.get(g)||0)+1)}return d},r=i(e),o=i(t),s=0,n=0,p=0;for(let u of r.values())n+=u;for(let u of o.values())p+=u;for(let[u,d]of r.entries()){let y=o.get(u)||0;s+=Math.min(d,y)}return 2*s/Math.max(1,n+p)}function b(c){return String(c||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function L(c,a=[]){let e=Array.isArray(a)?a:[];if(e.length===0)return{requested:c||null,resolved:null,strategy:"none"};let t=e.filter(s=>!s.subtask),i=t.length>0?t:e,r=b(c);if(r){let s=i.find(u=>b(u.name)===r);if(s)return{requested:c,resolved:s,strategy:"exact"};let n={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(n)){if(!u.some(y=>b(y)===r))continue;let d=i.find(y=>u.some(g=>b(g)===b(y.name)));if(d)return{requested:c,resolved:d,strategy:"alias"}}let p=i.map(u=>({t:u,score:w(c,u.name)})).sort((u,d)=>d.score-u.score);if(p[0]&&p[0].score>=.5)return{requested:c,resolved:p[0].t,strategy:"fuzzy"}}let o=["task","story","bug","improvement","epic"];for(let s of o){let n=i.find(p=>b(p.name)===s);if(n)return{requested:c||null,resolved:n,strategy:"default-preferred"}}return{requested:c||null,resolved:i[0],strategy:"default-first"}}async function A(c){let a=`projectKeys=${encodeURIComponent(c)}&expand=projects.issuetypes`,e=await f(`/rest/api/3/issue/createmeta?${a}`),t=Array.isArray(e?.projects)?e.projects:[],r=t.find(s=>String(s?.key||"").toUpperCase()===String(c||"").toUpperCase())||t[0]||null;return(Array.isArray(r?.issuetypes)?r.issuetypes:[]).map(s=>({id:s.id,name:s.name,subtask:!!s.subtask,description:s.description||null}))}async function R(c,a){if(!c)throw new Error("projectKey is required");let e="sprint is not EMPTY";a==="active"?e="sprint in openSprints()":a==="closed"?e="sprint in closedSprints()":a==="future"&&(e="sprint in futureSprints()");let t=`project = ${c} AND ${e} ORDER BY updated DESC`,i=`jql=${encodeURIComponent(t)}&maxResults=100&fields=customfield_10020`,r=await f(`/rest/api/3/search/jql?${i}`),o=new Map;for(let s of r.issues||[])for(let n of s.fields?.customfield_10020||[])n&&!o.has(n.id)&&o.set(n.id,{id:n.id,name:n.name,state:n.state,boardId:n.boardId||null,startDate:n.startDate||null,endDate:n.endDate||null,goal:n.goal||null});return[...o.values()].sort((s,n)=>{let p={active:0,future:1,closed:2},u=(p[s.state]??3)-(p[n.state]??3);return u!==0?u:String(n.startDate||"").localeCompare(String(s.startDate||""))})}function D(c,{sprintId:a,sprintName:e,target:t}={}){let i=Array.isArray(c)?c:[];if(!i.length)return{sprint:null,selectedBy:"none"};if(a!=null&&String(a).trim()!=="")return{sprint:i.find(s=>String(s.id)===String(a))||null,selectedBy:"id"};if(e&&String(e).trim()){let o=String(e).trim(),s=i.find(p=>String(p.name||"").toLowerCase()===o.toLowerCase());if(s)return{sprint:s,selectedBy:"name-exact"};let n=i.map(p=>({s:p,score:w(o,p.name||"")})).sort((p,u)=>u.score-p.score);return n[0]&&n[0].score>=.5?{sprint:n[0].s,selectedBy:"name-fuzzy"}:{sprint:null,selectedBy:"name-none"}}let r=String(t||"current").trim().toLowerCase();return r==="active"||r==="current"||r==="latest"?{sprint:i[0],selectedBy:r}:{sprint:i[0],selectedBy:"default"}}function E(c,a){let e=c?.fields?.customfield_10020;return Array.isArray(e)?e.some(t=>String(t?.id)===String(a)):!1}async function B({issueKey:c,projectKey:a,sprintId:e,attempts:t=3,delayMs:i=450}){let r=[];for(let o=0;o<t;o++){try{let s=`project = ${a} AND key = ${c} AND sprint = ${e}`,n=`jql=${encodeURIComponent(s)}&maxResults=1&fields=key,status`,p=await f(`/rest/api/3/search/jql?${n}`);if(Number(p?.total||0)>0)return r.push({attempt:o+1,jql:!0,issueField:null}),{ok:!0,method:"jql",traces:r};let d=await f(`/rest/api/3/issue/${c}?fields=customfield_10020,status`),y=E(d,e);if(r.push({attempt:o+1,jql:!1,issueField:y}),y)return{ok:!0,method:"issue_field",traces:r}}catch(s){r.push({attempt:o+1,error:String(s?.message||s)})}o<t-1&&await new Promise(s=>setTimeout(s,i))}return{ok:!1,method:"none",traces:r}}async function O({issueKey:c,projectKey:a,sprintId:e,sprintName:t,target:i}){if(!c)return{ok:!1,error:"issueKey is required"};let r=a;if(!r&&(r=(await f(`/rest/api/3/issue/${c}?fields=project`))?.fields?.project?.key||null,!r))return{ok:!1,error:`Could not resolve project for ${c}`};let o=await R(r,"active");if(!o.length)return{ok:!1,error:`No assignable active sprint found for project ${r}`};let{sprint:s,selectedBy:n}=D(o,{sprintId:e,sprintName:t,target:i});if(!s)return{ok:!1,error:`No matching sprint found in ${r}`,requested:{sprintId:e??null,sprintName:t??null,target:i??"current"},availableSprints:o.map(d=>({id:d.id,name:d.name,state:d.state}))};await f(`/rest/api/3/issue/${c}`,{method:"PUT",body:{fields:{customfield_10020:Number(s.id)}}});let p=await B({issueKey:c,projectKey:r,sprintId:s.id}),u=p.ok;return{ok:u,issueKey:c,projectKey:r,sprintId:s.id,sprintName:s.name,selectedBy:n,verifiedBy:p.method,verified:u,verificationTrace:p.traces,warning:u?null:`Sprint assignment attempted but verification did not find ${c} in sprint ${s.id}`}}async function f(c,a={}){let e=async()=>{let{token:t,cloudId:i}=await $("jira");if(typeof t!="string"||!t)throw new Error(`Invalid jira token type: ${typeof t}`);if(!i)throw new Error("Invalid jira cloudId: missing");let r=`https://api.atlassian.com/ex/jira/${i}${c}`,o=await fetch(r,{method:a.method||"GET",headers:{Authorization:`Bearer ${t}`,Accept:"application/json",...a.body?{"Content-Type":"application/json"}:{},...a.headers},body:a.body?JSON.stringify(a.body):void 0});if(!o.ok){let n=await o.text().catch(()=>"");throw new Error(`Jira API ${o.status}: ${n.slice(0,300)}`)}let s=await o.text().catch(()=>"");if(!s||!s.trim())return{};try{return JSON.parse(s)}catch{return{raw:s}}};try{return await e()}catch(t){let i=String(t?.message||t||"").toLowerCase();if(!(i.includes("token")||i.includes("401")||i.includes("403")||i.includes("substring")))throw t;return q("jira"),e()}}var F={id:"jira",serverName:"jira",allowedTools:["mcp__jira__*"],requiresIntegration:N.JIRA,envKeys:["ATLASSIAN_ACCESS_TOKEN","ATLASSIAN_CLOUD_ID"],description:"Zibby Jira MCP Server (OAuth Bearer)",promptFragment:`## Jira (connected)
20
20
  You have direct access to the user's Jira. Use these tools proactively:
21
21
 
22
22
  ### Issue tools
@@ -66,4 +66,4 @@ 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 c=T();if(!c)return null;let a={};for(let e of this.envKeys)process.env[e]&&(a[e]=process.env[e]);return process.env.ATLASSIAN_INSTANCE_URL&&(a.ATLASSIAN_INSTANCE_URL=process.env.ATLASSIAN_INSTANCE_URL),{command:"node",args:[c],env:a,description:this.description}},async handleToolCall(c,a){try{switch(c){case"jira_list_projects":{let e=await f("/rest/api/3/project"),t=(Array.isArray(e)?e:[]).map(i=>({id:i.id,key:i.key,name:i.name,style:i.style}));return JSON.stringify({count:t.length,projects:t})}case"jira_list_statuses":{let{projectKey:e}=a||{};if(e){let r=await f(`/rest/api/3/project/${encodeURIComponent(e)}/statuses`),o=Array.isArray(r)?r:[],s=new Map;for(let p of o)for(let u of p.statuses||[])u?.id&&(s.has(u.id)||s.set(u.id,{id:u.id,name:u.name,category:u.statusCategory?.name||null}));let n=[...s.values()].sort((p,u)=>String(p.name).localeCompare(String(u.name)));return JSON.stringify({scope:"project",projectKey:e,count:n.length,statuses:n})}let t=await f("/rest/api/3/status"),i=(Array.isArray(t)?t:[]).map(r=>({id:r.id,name:r.name,category:r.statusCategory?.name||null})).sort((r,o)=>String(r.name).localeCompare(String(o.name)));return JSON.stringify({scope:"global",count:i.length,statuses:i})}case"jira_list_issue_types":{let{projectKey:e}=a||{};if(!e)return JSON.stringify({error:"projectKey is required"});let t=await N(e);return JSON.stringify({projectKey:e,count:t.length,issueTypes:t})}case"jira_search":{let e=a.jql||"",t=a.maxResults||20;e.replace(/\s*ORDER\s+BY\s+.*/i,"").trim()||(e=`created >= -365d ${e}`.trim());let r=`jql=${encodeURIComponent(e)}&maxResults=${t}&fields=summary,status,assignee,priority,updated,issuetype,project`,s=((await f(`/rest/api/3/search/jql?${r}`)).issues||[]).map(n=>({key:n.key,project:n.fields?.project?.key,summary:n.fields?.summary,status:n.fields?.status?.name,assignee:n.fields?.assignee?.displayName||"Unassigned",priority:n.fields?.priority?.name,type:n.fields?.issuetype?.name}));return JSON.stringify({count:s.length,issues:s})}case"jira_get_issue":{let e=a.issueKey;if(!e)return JSON.stringify({error:"issueKey is required"});let t=await f(`/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:i,description:r,priority:o,labels:s,assigneeId:n,moveToSprint:p,moveToActiveSprint:u,sprintId:d,sprintName:y,target:g}=a;if(!e||!t)return JSON.stringify({error:"projectKey and summary are required"});let l={requested:i||null,resolved:null,strategy:"none"},j=[];try{j=await N(e),l=P(i,j)}catch{}let m={project:{key:e},summary:t,issuetype:l?.resolved?.id?{id:l.resolved.id}:{name:i||"Task"}};r&&(m.description={type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:r}]}]}),o&&(m.priority={name:o}),s?.length&&(m.labels=s),n&&(m.assignee={id:n});let _=await f("/rest/api/3/issue",{method:"POST",body:{fields:m}}),h={ok:!0,key:_.key,id:_.id,self:_.self};return l?.resolved&&(h.issueType=l.resolved.name,h.issueTypeResolution=l.strategy,l.strategy!=="exact"&&l.requested&&b(l.requested)!==b(l.resolved.name)&&(h.issueTypeWarning=`Requested "${l.requested}" is not available in ${e}; used "${l.resolved.name}" instead.`)),j.length>0&&(h.availableIssueTypes=j.map(v=>v.name)),(p||u)&&(h.sprintMove=await O({issueKey:_.key,projectKey:e,sprintId:d,sprintName:y,target:g})),JSON.stringify(h)}case"jira_list_sprints":{let{projectKey:e,state:t}=a,i=await A(e,t);return JSON.stringify({count:i.length,sprints:i})}case"jira_move_to_active_sprint":{let{issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o}=a||{},s=await O({issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o||"current"});return JSON.stringify(s)}case"jira_move_issue_to_sprint":{let{issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o}=a||{},s=await O({issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o});return JSON.stringify(s)}case"jira_get_sprint_issues":{let{sprintName:e,sprintId:t,projectKey:i,status:r,maxResults:o}=a;if(!e&&!t)return JSON.stringify({error:"sprintName or sprintId is required"});let s=o||50,n=t?`sprint = ${t}`:`sprint = "${e}"`,p=i?`project = ${i} AND `:"",u=r?` AND status = "${r}"`:"",d=`${p}${n}${u} ORDER BY status ASC, priority DESC`,y=`jql=${encodeURIComponent(d)}&maxResults=${s}&fields=summary,status,assignee,priority,issuetype,project`,g=await f(`/rest/api/3/search/jql?${y}`),l=(g.issues||[]).map(m=>({key:m.key,project:m.fields?.project?.key,summary:m.fields?.summary,status:m.fields?.status?.name,assignee:m.fields?.assignee?.displayName||"Unassigned",priority:m.fields?.priority?.name,type:m.fields?.issuetype?.name})),j={};for(let m of l)j[m.status]=(j[m.status]||0)+1;return JSON.stringify({count:l.length,total:g.total||l.length,statusCounts:j,issues:l})}case"jira_get_comments":{let{issueKey:e,maxResults:t}=a;if(!e)return JSON.stringify({error:"issueKey is required"});let r=await f(`/rest/api/3/issue/${e}/comment?maxResults=${t||50}&orderBy=-created`),o=(r.comments||[]).map(s=>{let n="";return s.body?.content&&(n=k(s.body.content)),{id:s.id,author:s.author?.displayName||"Unknown",body:n,created:s.created,updated:s.updated}});return JSON.stringify({count:o.length,total:r.total||o.length,comments:o})}case"jira_add_comment":{let{issueKey:e,body:t}=a;return!e||!t?JSON.stringify({error:"issueKey and body are required"}):(await f(`/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}=a;return!e||!t?JSON.stringify({error:"issueKey and fields are required"}):(await f(`/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:i,statusName:r,status:o}=a;if(!e)return JSON.stringify({error:"issueKey is required"});let s=String(i||r||o||"").trim();if(!t&&!s){let d=((await f(`/rest/api/3/issue/${e}/transitions`)).transitions||[]).map(y=>({id:y.id,name:y.name,to:y.to?.name}));return JSON.stringify({ok:!1,error:"transitionId or toStatus is required",issueKey:e,availableTransitions:d})}let n=t;if(!n){let d=(await f(`/rest/api/3/issue/${e}/transitions`)).transitions||[],y=S(s),g=d.find(l=>S(l?.name||"")===y||S(l?.to?.name||"")===y);if(!g){let l=I(s);l.length>=2&&(g=d.find(j=>{let m=I(j?.name||""),_=I(j?.to?.name||""),h=m.length>=2&&(m.includes(l)||l.includes(m)),v=_.length>=2&&(_.includes(l)||l.includes(_));return h||v}))}if(!g){let l=d.map(h=>{let v=w(s,h?.name||""),J=w(s,h?.to?.name||"");return{t:h,score:Math.max(v,J)}}).sort((h,v)=>v.score-h.score),j=l[0],m=l[1];j&&j.score>=.45&&(!m||j.score-m.score>=.12)&&(g=j.t)}if(!g?.id)return JSON.stringify({ok:!1,error:`No transition matches target status: "${s}"`,issueKey:e,availableTransitions:d.map(l=>({id:l.id,name:l.name,to:l.to?.name}))});n=g.id}await f(`/rest/api/3/issue/${e}/transitions`,{method:"POST",body:{transition:{id:n}}});let p=await f(`/rest/api/3/issue/${e}?fields=status`);return JSON.stringify({ok:!0,issueKey:e,transitionId:n,statusAfter:p?.fields?.status?.name||null})}default:return JSON.stringify({error:`Unknown tool: ${c}`})}}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"]}}]};export{M as jiraSkill};
69
+ 6. IMPORTANT: When target is clear, complete transition + verification in SAME turn. Do NOT stop after listing options.`,resolve(){let c=C();if(!c)return null;let a={};for(let e of this.envKeys)process.env[e]&&(a[e]=process.env[e]);return process.env.ATLASSIAN_INSTANCE_URL&&(a.ATLASSIAN_INSTANCE_URL=process.env.ATLASSIAN_INSTANCE_URL),{command:"node",args:[c],env:a,description:this.description}},async handleToolCall(c,a){try{switch(c){case"jira_list_projects":{let e=await f("/rest/api/3/project"),t=(Array.isArray(e)?e:[]).map(i=>({id:i.id,key:i.key,name:i.name,style:i.style}));return JSON.stringify({count:t.length,projects:t})}case"jira_list_statuses":{let{projectKey:e}=a||{};if(e){let r=await f(`/rest/api/3/project/${encodeURIComponent(e)}/statuses`),o=Array.isArray(r)?r:[],s=new Map;for(let p of o)for(let u of p.statuses||[])u?.id&&(s.has(u.id)||s.set(u.id,{id:u.id,name:u.name,category:u.statusCategory?.name||null}));let n=[...s.values()].sort((p,u)=>String(p.name).localeCompare(String(u.name)));return JSON.stringify({scope:"project",projectKey:e,count:n.length,statuses:n})}let t=await f("/rest/api/3/status"),i=(Array.isArray(t)?t:[]).map(r=>({id:r.id,name:r.name,category:r.statusCategory?.name||null})).sort((r,o)=>String(r.name).localeCompare(String(o.name)));return JSON.stringify({scope:"global",count:i.length,statuses:i})}case"jira_list_issue_types":{let{projectKey:e}=a||{};if(!e)return JSON.stringify({error:"projectKey is required"});let t=await A(e);return JSON.stringify({projectKey:e,count:t.length,issueTypes:t})}case"jira_search":{let e=a.jql||"",t=a.maxResults||20;e.replace(/\s*ORDER\s+BY\s+.*/i,"").trim()||(e=`created >= -365d ${e}`.trim());let r=`jql=${encodeURIComponent(e)}&maxResults=${t}&fields=summary,status,assignee,priority,updated,issuetype,project`,s=((await f(`/rest/api/3/search/jql?${r}`)).issues||[]).map(n=>({key:n.key,project:n.fields?.project?.key,summary:n.fields?.summary,status:n.fields?.status?.name,assignee:n.fields?.assignee?.displayName||"Unassigned",priority:n.fields?.priority?.name,type:n.fields?.issuetype?.name}));return JSON.stringify({count:s.length,issues:s})}case"jira_get_issue":{let e=a.issueKey;if(!e)return JSON.stringify({error:"issueKey is required"});let t=await f(`/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:i,description:r,priority:o,labels:s,assigneeId:n,moveToSprint:p,moveToActiveSprint:u,sprintId:d,sprintName:y,target:g}=a;if(!e||!t)return JSON.stringify({error:"projectKey and summary are required"});let l={requested:i||null,resolved:null,strategy:"none"},j=[];try{j=await A(e),l=L(i,j)}catch{}let m={project:{key:e},summary:t,issuetype:l?.resolved?.id?{id:l.resolved.id}:{name:i||"Task"}};r&&(m.description={type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:r}]}]}),o&&(m.priority={name:o}),s?.length&&(m.labels=s),n&&(m.assignee={id:n});let _=await f("/rest/api/3/issue",{method:"POST",body:{fields:m}}),h={ok:!0,key:_.key,id:_.id,self:_.self};return l?.resolved&&(h.issueType=l.resolved.name,h.issueTypeResolution=l.strategy,l.strategy!=="exact"&&l.requested&&b(l.requested)!==b(l.resolved.name)&&(h.issueTypeWarning=`Requested "${l.requested}" is not available in ${e}; used "${l.resolved.name}" instead.`)),j.length>0&&(h.availableIssueTypes=j.map(v=>v.name)),(p||u)&&(h.sprintMove=await O({issueKey:_.key,projectKey:e,sprintId:d,sprintName:y,target:g})),JSON.stringify(h)}case"jira_list_sprints":{let{projectKey:e,state:t}=a,i=await R(e,t);return JSON.stringify({count:i.length,sprints:i})}case"jira_move_to_active_sprint":{let{issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o}=a||{},s=await O({issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o||"current"});return JSON.stringify(s)}case"jira_move_issue_to_sprint":{let{issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o}=a||{},s=await O({issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o});return JSON.stringify(s)}case"jira_get_sprint_issues":{let{sprintName:e,sprintId:t,projectKey:i,status:r,maxResults:o}=a;if(!e&&!t)return JSON.stringify({error:"sprintName or sprintId is required"});let s=o||50,n=t?`sprint = ${t}`:`sprint = "${e}"`,p=i?`project = ${i} AND `:"",u=r?` AND status = "${r}"`:"",d=`${p}${n}${u} ORDER BY status ASC, priority DESC`,y=`jql=${encodeURIComponent(d)}&maxResults=${s}&fields=summary,status,assignee,priority,issuetype,project`,g=await f(`/rest/api/3/search/jql?${y}`),l=(g.issues||[]).map(m=>({key:m.key,project:m.fields?.project?.key,summary:m.fields?.summary,status:m.fields?.status?.name,assignee:m.fields?.assignee?.displayName||"Unassigned",priority:m.fields?.priority?.name,type:m.fields?.issuetype?.name})),j={};for(let m of l)j[m.status]=(j[m.status]||0)+1;return JSON.stringify({count:l.length,total:g.total||l.length,statusCounts:j,issues:l})}case"jira_get_comments":{let{issueKey:e,maxResults:t}=a;if(!e)return JSON.stringify({error:"issueKey is required"});let r=await f(`/rest/api/3/issue/${e}/comment?maxResults=${t||50}&orderBy=-created`),o=(r.comments||[]).map(s=>{let n="";return s.body?.content&&(n=S(s.body.content)),{id:s.id,author:s.author?.displayName||"Unknown",body:n,created:s.created,updated:s.updated}});return JSON.stringify({count:o.length,total:r.total||o.length,comments:o})}case"jira_add_comment":{let{issueKey:e,body:t}=a;return!e||!t?JSON.stringify({error:"issueKey and body are required"}):(await f(`/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}=a;return!e||!t?JSON.stringify({error:"issueKey and fields are required"}):(await f(`/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:i,statusName:r,status:o}=a;if(!e)return JSON.stringify({error:"issueKey is required"});let s=String(i||r||o||"").trim();if(!t&&!s){let d=((await f(`/rest/api/3/issue/${e}/transitions`)).transitions||[]).map(y=>({id:y.id,name:y.name,to:y.to?.name}));return JSON.stringify({ok:!1,error:"transitionId or toStatus is required",issueKey:e,availableTransitions:d})}let n=t;if(!n){let d=(await f(`/rest/api/3/issue/${e}/transitions`)).transitions||[],y=k(s),g=d.find(l=>k(l?.name||"")===y||k(l?.to?.name||"")===y);if(!g){let l=I(s);l.length>=2&&(g=d.find(j=>{let m=I(j?.name||""),_=I(j?.to?.name||""),h=m.length>=2&&(m.includes(l)||l.includes(m)),v=_.length>=2&&(_.includes(l)||l.includes(_));return h||v}))}if(!g){let l=d.map(h=>{let v=w(s,h?.name||""),J=w(s,h?.to?.name||"");return{t:h,score:Math.max(v,J)}}).sort((h,v)=>v.score-h.score),j=l[0],m=l[1];j&&j.score>=.45&&(!m||j.score-m.score>=.12)&&(g=j.t)}if(!g?.id)return JSON.stringify({ok:!1,error:`No transition matches target status: "${s}"`,issueKey:e,availableTransitions:d.map(l=>({id:l.id,name:l.name,to:l.to?.name}))});n=g.id}await f(`/rest/api/3/issue/${e}/transitions`,{method:"POST",body:{transition:{id:n}}});let p=await f(`/rest/api/3/issue/${e}?fields=status`);return JSON.stringify({ok:!0,issueKey:e,transitionId:n,statusAfter:p?.fields?.status?.name||null})}default:return JSON.stringify({error:`Unknown tool: ${c}`})}}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"]}}]};export{F as jiraSkill};
package/dist/lark.js CHANGED
@@ -1,7 +1,7 @@
1
- import{createRequire as p}from"module";import{resolveIntegrationToken as m}from"@zibby/core/backend-client.js";var l=p(import.meta.url);function u(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;try{return l.resolve("@zibby/skills/bin/mcp-lark.mjs")}catch{return null}}var h=6e3*1e3,a=null;async function g(){let{appId:e,appSecret:t,host:r}=await m("lark");if(a&&a.appId===e&&a.expiresAt>Date.now())return{token:a.token,host:r};let n=await(await fetch(`${r}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:e,app_secret:t})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return a={token:n.tenant_access_token,expiresAt:Date.now()+h,appId:e},{token:n.tenant_access_token,host:r}}async function c(e,t,r={}){let{token:i,host:n}=await g(),s=`${n}${t}`,_={method:e,headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json; charset=utf-8"}};e!=="GET"&&(_.body=JSON.stringify(r));let o=await(await fetch(s,_)).json();if(o.code!==0)throw new Error(`Lark API ${t} error: ${o.msg||o.code}`);return o.data||{}}function d(e){return JSON.stringify({text:e})}function y(e){return!e||typeof e!="string"||e.startsWith("oc_")?"chat_id":e.startsWith("ou_")?"open_id":e.startsWith("on_")?"union_id":e.startsWith("cli_")?"app_id":e.includes("@")?"email":"chat_id"}var T={id:"lark",serverName:"lark",allowedTools:["mcp__lark__*"],description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:[],promptFragment:`## Lark (connected)
1
+ import{existsSync as m}from"fs";import{fileURLToPath as l}from"url";import{dirname as h,resolve as g}from"path";import{resolveIntegrationToken as u}from"@zibby/core/backend-client.js";var p=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark"}),S=Object.freeze({sentry:{id:"sentry",name:"Sentry",connectPath:"/integrations?provider=sentry"},jira:{id:"jira",name:"Jira",connectPath:"/integrations?provider=jira"},github:{id:"github",name:"GitHub",connectPath:"/integrations?provider=github"},gitlab:{id:"gitlab",name:"GitLab",connectPath:"/integrations?provider=gitlab"},slack:{id:"slack",name:"Slack",connectPath:"/integrations?provider=slack"},lark:{id:"lark",name:"Lark",connectPath:"/integrations?provider=lark"}});function y(){if(process.env.MCP_LARK_PATH)return process.env.MCP_LARK_PATH;let e=h(l(import.meta.url)),t=g(e,"..","bin","mcp-lark.mjs");return m(t)?t:null}var f=6e3*1e3,s=null;async function k(){let{appId:e,appSecret:t,host:r}=await u("lark");if(s&&s.appId===e&&s.expiresAt>Date.now())return{token:s.token,host:r};let n=await(await fetch(`${r}/open-apis/auth/v3/tenant_access_token/internal`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app_id:e,app_secret:t})})).json();if(n.code!==0)throw new Error(`Lark tenant_access_token failed: ${n.msg||n.code}`);return s={token:n.tenant_access_token,expiresAt:Date.now()+f,appId:e},{token:n.tenant_access_token,host:r}}async function c(e,t,r={}){let{token:a,host:n}=await k(),i=`${n}${t}`,d={method:e,headers:{Authorization:`Bearer ${a}`,"Content-Type":"application/json; charset=utf-8"}};e!=="GET"&&(d.body=JSON.stringify(r));let o=await(await fetch(i,d)).json();if(o.code!==0)throw new Error(`Lark API ${t} error: ${o.msg||o.code}`);return o.data||{}}function _(e){return JSON.stringify({text:e})}function T(e){return!e||typeof e!="string"||e.startsWith("oc_")?"chat_id":e.startsWith("ou_")?"open_id":e.startsWith("on_")?"union_id":e.startsWith("cli_")?"app_id":e.includes("@")?"email":"chat_id"}var N={id:"lark",serverName:"lark",allowedTools:["mcp__lark__*"],requiresIntegration:p.LARK,description:"Lark / Feishu messaging \u2014 send messages and reply in threads.",envKeys:[],promptFragment:`## Lark (connected)
2
2
  You can send messages and replies on Lark. Use:
3
3
  - lark_send_message: post a message to a chat, user, or DM
4
4
  - lark_reply: reply to an existing message (threaded)
5
5
  - lark_list_chats: list chats the bot is a member of
6
6
  - lark_get_chat_history: fetch recent messages in a chat
7
- When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let e=u();if(!e)return null;let t={};for(let r of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[r]&&(t[r]=process.env[r]);return{type:"stdio",command:"node",args:[e],env:t,alwaysLoad:!0}},tools:[{name:"lark_send_message",description:"Send a text message to a Lark chat, user, or DM. receive_id can be a chat_id (oc_*), open_id (ou_*), union_id (on_*), or email.",input_schema:{type:"object",properties:{receive_id:{type:"string",description:"Target id: chat_id (oc_*), open_id (ou_*), union_id (on_*), or email"},text:{type:"string",description:"Message text"}},required:["receive_id","text"]}},{name:"lark_reply",description:"Reply to an existing Lark message (creates a thread). Use the message_id from the inbound event.",input_schema:{type:"object",properties:{message_id:{type:"string",description:"Lark message id (om_*) to reply to"},text:{type:"string",description:"Reply text"}},required:["message_id","text"]}},{name:"lark_list_chats",description:"List chats (groups + DMs) the bot is a member of.",input_schema:{type:"object",properties:{page_size:{type:"number",description:"Max results (default 50)"}}}},{name:"lark_get_chat_history",description:"Fetch recent messages in a chat.",input_schema:{type:"object",properties:{chat_id:{type:"string",description:"Chat id (oc_*)"},page_size:{type:"number",description:"Max messages (default 20)"}},required:["chat_id"]}}],async handleToolCall(e,t){try{switch(e){case"lark_send_message":{if(!t.receive_id||!t.text)return JSON.stringify({error:"receive_id and text are required"});let r=y(t.receive_id),i=await c("POST",`/open-apis/im/v1/messages?receive_id_type=${r}`,{receive_id:t.receive_id,msg_type:"text",content:d(t.text)});return JSON.stringify({ok:!0,message_id:i.message_id})}case"lark_reply":{if(!t.message_id||!t.text)return JSON.stringify({error:"message_id and text are required"});let r=await c("POST",`/open-apis/im/v1/messages/${encodeURIComponent(t.message_id)}/reply`,{msg_type:"text",content:d(t.text)});return JSON.stringify({ok:!0,message_id:r.message_id})}case"lark_list_chats":{let r=t.page_size||50,n=((await c("GET",`/open-apis/im/v1/chats?page_size=${r}`)).items||[]).map(s=>({chat_id:s.chat_id,name:s.name,description:s.description,owner_id:s.owner_id,chat_mode:s.chat_mode}));return JSON.stringify({chats:n})}case"lark_get_chat_history":{if(!t.chat_id)return JSON.stringify({error:"chat_id is required"});let r=t.page_size||20,n=((await c("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(t.chat_id)}&page_size=${r}&sort_type=ByCreateTimeDesc`)).items||[]).map(s=>({message_id:s.message_id,sender_id:s.sender?.id,sender_type:s.sender?.sender_type,msg_type:s.msg_type,content:s.body?.content,create_time:s.create_time}));return JSON.stringify({messages:n})}default:return JSON.stringify({error:`Unknown tool: ${e}`})}}catch(r){return JSON.stringify({error:r.message})}}};function x(){a=null}export{x as _resetLarkTokenCache,T as larkSkill};
7
+ When responding to an incoming event, prefer lark_reply with the source message_id so the response threads cleanly.`,resolve(){let e=y();if(!e)return null;let t={};for(let r of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[r]&&(t[r]=process.env[r]);return{type:"stdio",command:"node",args:[e],env:t,alwaysLoad:!0}},tools:[{name:"lark_send_message",description:"Send a text message to a Lark chat, user, or DM. receive_id can be a chat_id (oc_*), open_id (ou_*), union_id (on_*), or email.",input_schema:{type:"object",properties:{receive_id:{type:"string",description:"Target id: chat_id (oc_*), open_id (ou_*), union_id (on_*), or email"},text:{type:"string",description:"Message text"}},required:["receive_id","text"]}},{name:"lark_reply",description:"Reply to an existing Lark message (creates a thread). Use the message_id from the inbound event.",input_schema:{type:"object",properties:{message_id:{type:"string",description:"Lark message id (om_*) to reply to"},text:{type:"string",description:"Reply text"}},required:["message_id","text"]}},{name:"lark_list_chats",description:"List chats (groups + DMs) the bot is a member of.",input_schema:{type:"object",properties:{page_size:{type:"number",description:"Max results (default 50)"}}}},{name:"lark_get_chat_history",description:"Fetch recent messages in a chat.",input_schema:{type:"object",properties:{chat_id:{type:"string",description:"Chat id (oc_*)"},page_size:{type:"number",description:"Max messages (default 20)"}},required:["chat_id"]}}],async handleToolCall(e,t){try{switch(e){case"lark_send_message":{if(!t.receive_id||!t.text)return JSON.stringify({error:"receive_id and text are required"});let r=T(t.receive_id),a=await c("POST",`/open-apis/im/v1/messages?receive_id_type=${r}`,{receive_id:t.receive_id,msg_type:"text",content:_(t.text)});return JSON.stringify({ok:!0,message_id:a.message_id})}case"lark_reply":{if(!t.message_id||!t.text)return JSON.stringify({error:"message_id and text are required"});let r=await c("POST",`/open-apis/im/v1/messages/${encodeURIComponent(t.message_id)}/reply`,{msg_type:"text",content:_(t.text)});return JSON.stringify({ok:!0,message_id:r.message_id})}case"lark_list_chats":{let r=t.page_size||50,n=((await c("GET",`/open-apis/im/v1/chats?page_size=${r}`)).items||[]).map(i=>({chat_id:i.chat_id,name:i.name,description:i.description,owner_id:i.owner_id,chat_mode:i.chat_mode}));return JSON.stringify({chats:n})}case"lark_get_chat_history":{if(!t.chat_id)return JSON.stringify({error:"chat_id is required"});let r=t.page_size||20,n=((await c("GET",`/open-apis/im/v1/messages?container_id_type=chat&container_id=${encodeURIComponent(t.chat_id)}&page_size=${r}&sort_type=ByCreateTimeDesc`)).items||[]).map(i=>({message_id:i.message_id,sender_id:i.sender?.id,sender_type:i.sender?.sender_type,msg_type:i.msg_type,content:i.body?.content,create_time:i.create_time}));return JSON.stringify({messages:n})}default:return JSON.stringify({error:`Unknown tool: ${e}`})}}catch(r){return JSON.stringify({error:r.message})}}};function R(){s=null}export{R as _resetLarkTokenCache,N as larkSkill};
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/skills",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Built-in skill definitions for Zibby test automation framework",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,8 @@
14
14
  "./slack": "./dist/slack.js",
15
15
  "./lark": "./dist/lark.js",
16
16
  "./memory": "./dist/memory.js",
17
- "./function": "./dist/function-skill.js"
17
+ "./function": "./dist/function-skill.js",
18
+ "./integrations": "./dist/integrations.js"
18
19
  },
19
20
  "scripts": {
20
21
  "build": "node ../scripts/build.mjs",
package/dist/sentry.js CHANGED
@@ -1,5 +1,5 @@
1
- import{createRequire as l}from"module";import{resolveIntegrationToken as p}from"@zibby/core/backend-client.js";var y=l(import.meta.url);function d(){if(process.env.MCP_SENTRY_PATH)return process.env.MCP_SENTRY_PATH;try{return y.resolve("@zibby/skills/bin/mcp-sentry.mjs")}catch{return null}}async function c(o,s={}){let{token:t,organizationSlug:r}=await p("sentry"),i=`https://sentry.io/api/0/organizations/${r}${o}`,e=await fetch(i,{method:s.method||"GET",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}});if(!e.ok){let a=await e.text().catch(()=>"");throw new Error(`Sentry API ${e.status}: ${a.slice(0,300)}`)}return e.json()}var u={id:"sentry",serverName:"sentry",allowedTools:["mcp__sentry__*"],description:"Sentry error tracking \u2014 projects, issues, events",envKeys:[],tools:[],promptFragment:`## Sentry (connected)
1
+ import{existsSync as d}from"fs";import{fileURLToPath as y}from"url";import{dirname as m,resolve as f}from"path";import{resolveIntegrationToken as l}from"@zibby/core/backend-client.js";var c=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark"}),g=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"}});function S(){if(process.env.MCP_SENTRY_PATH)return process.env.MCP_SENTRY_PATH;let s=m(y(import.meta.url)),r=f(s,"..","bin","mcp-sentry.mjs");return d(r)?r:null}async function u(s,r={}){let{token:t,organizationSlug:n}=await l("sentry"),o=`https://sentry.io/api/0/organizations/${n}${s}`,e=await fetch(o,{method:r.method||"GET",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}});if(!e.ok){let a=await e.text().catch(()=>"");throw new Error(`Sentry API ${e.status}: ${a.slice(0,300)}`)}return e.json()}var p={id:"sentry",serverName:"sentry",allowedTools:["mcp__sentry__*"],requiresIntegration:c.SENTRY,description:"Sentry error tracking \u2014 projects, issues, events",envKeys:[],tools:[],promptFragment:`## Sentry (connected)
2
2
  You have access to the user's Sentry. Use these tools:
3
3
  - sentry_list_projects: List projects in the organization
4
4
  - sentry_list_issues: List errors/issues (supports Sentry search query, project filter, sort)
5
- - sentry_get_issue: Get detailed info about a specific issue (requires issueId)`,resolve(){let o=d();if(!o)return null;let s={};for(let t of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(s[t]=process.env[t]);return{type:"stdio",command:"node",args:[o],env:s,alwaysLoad:!0}},async handleToolCall(o,s={}){try{switch(o){case"sentry_list_projects":{let t=await c("/projects/?per_page=50");return JSON.stringify({projects:t.map(r=>({slug:r.slug,name:r.name,platform:r.platform}))})}case"sentry_list_issues":{let t=s.project||"",r=s.query||"is:unresolved",i=s.sort||"date",e=`/issues/?query=${encodeURIComponent(r)}&sort=${i}&per_page=${s.limit||25}`;t&&(e+=`&project=${encodeURIComponent(t)}`);let a=await c(e);return JSON.stringify({issues:a.map(n=>({id:n.id,title:n.title,culprit:n.culprit,count:n.count,firstSeen:n.firstSeen,lastSeen:n.lastSeen,level:n.level,status:n.status}))})}case"sentry_get_issue":{let{issueId:t}=s;if(!t)return JSON.stringify({error:"issueId is required"});let{token:r}=await p("sentry"),i=await fetch(`https://sentry.io/api/0/issues/${t}/`,{headers:{Authorization:`Bearer ${r}`}});if(!i.ok)throw new Error(`Sentry API ${i.status}`);let e=await i.json();return JSON.stringify({id:e.id,title:e.title,culprit:e.culprit,metadata:e.metadata,count:e.count,userCount:e.userCount,firstSeen:e.firstSeen,lastSeen:e.lastSeen,level:e.level,status:e.status,project:{slug:e.project?.slug,name:e.project?.name}})}default:return JSON.stringify({error:`Unknown tool: ${o}`})}}catch(t){return JSON.stringify({error:t.message})}},toolsForAssistant:[{name:"sentry_list_projects",description:"List Sentry projects",input_schema:{type:"object",properties:{}}},{name:"sentry_list_issues",description:"List Sentry issues (errors)",input_schema:{type:"object",properties:{project:{type:"string",description:"Project slug (optional)"},query:{type:"string",description:"Sentry search query (default: is:unresolved)"},sort:{type:"string",description:"Sort order: date, new, priority, freq, user (default: date)"},limit:{type:"number",description:"Max issues to return (default 25)"}}}},{name:"sentry_get_issue",description:"Get details of a specific Sentry issue",input_schema:{type:"object",properties:{issueId:{type:"string",description:"Sentry issue ID"}},required:["issueId"]}}]};u.tools=u.toolsForAssistant;export{u as sentrySkill};
5
+ - sentry_get_issue: Get detailed info about a specific issue (requires issueId)`,resolve(){let s=S();if(!s)return null;let r={};for(let t of["PROJECT_API_TOKEN","PROGRESS_API_URL","EXECUTION_ID","PROJECT_ID","STAGE"])process.env[t]&&(r[t]=process.env[t]);return{type:"stdio",command:"node",args:[s],env:r,alwaysLoad:!0}},async handleToolCall(s,r={}){try{switch(s){case"sentry_list_projects":{let t=await u("/projects/?per_page=50");return JSON.stringify({projects:t.map(n=>({slug:n.slug,name:n.name,platform:n.platform}))})}case"sentry_list_issues":{let t=r.project||"",n=r.query||"is:unresolved",o=r.sort||"date",e=`/issues/?query=${encodeURIComponent(n)}&sort=${o}&per_page=${r.limit||25}`;t&&(e+=`&project=${encodeURIComponent(t)}`);let a=await u(e);return JSON.stringify({issues:a.map(i=>({id:i.id,title:i.title,culprit:i.culprit,count:i.count,firstSeen:i.firstSeen,lastSeen:i.lastSeen,level:i.level,status:i.status}))})}case"sentry_get_issue":{let{issueId:t}=r;if(!t)return JSON.stringify({error:"issueId is required"});let{token:n}=await l("sentry"),o=await fetch(`https://sentry.io/api/0/issues/${t}/`,{headers:{Authorization:`Bearer ${n}`}});if(!o.ok)throw new Error(`Sentry API ${o.status}`);let e=await o.json();return JSON.stringify({id:e.id,title:e.title,culprit:e.culprit,metadata:e.metadata,count:e.count,userCount:e.userCount,firstSeen:e.firstSeen,lastSeen:e.lastSeen,level:e.level,status:e.status,project:{slug:e.project?.slug,name:e.project?.name}})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(t){return JSON.stringify({error:t.message})}},toolsForAssistant:[{name:"sentry_list_projects",description:"List Sentry projects",input_schema:{type:"object",properties:{}}},{name:"sentry_list_issues",description:"List Sentry issues (errors)",input_schema:{type:"object",properties:{project:{type:"string",description:"Project slug (optional)"},query:{type:"string",description:"Sentry search query (default: is:unresolved)"},sort:{type:"string",description:"Sort order: date, new, priority, freq, user (default: date)"},limit:{type:"number",description:"Max issues to return (default 25)"}}}},{name:"sentry_get_issue",description:"Get details of a specific Sentry issue",input_schema:{type:"object",properties:{issueId:{type:"string",description:"Sentry issue ID"}},required:["issueId"]}}]};p.tools=p.toolsForAssistant;export{p as sentrySkill};
package/dist/slack.js CHANGED
@@ -1,5 +1,5 @@
1
- import{resolveIntegrationToken as p}from"@zibby/core/backend-client.js";async function n(r,e={}){let{token:s}=await p("slack"),t=["conversations.list","users.list","users.profile.get","conversations.history","conversations.replies"].includes(r),i=`https://slack.com/api/${r}`,c={Authorization:`Bearer ${s}`},o;if(t){let l=new URLSearchParams(e).toString();l&&(i+=`?${l}`)}else c["Content-Type"]="application/json; charset=utf-8",o=JSON.stringify(e);let a=await(await fetch(i,{method:t?"GET":"POST",headers:c,body:o})).json();if(!a.ok)throw new Error(`Slack API error: ${a.error}`);return a}var _={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack (connected)
1
+ import{resolveIntegrationToken as d}from"@zibby/core/backend-client.js";var p=Object.freeze({SENTRY:"sentry",JIRA:"jira",GITHUB:"github",GITLAB:"gitlab",SLACK:"slack",LARK:"lark"}),_=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"}});async function n(s,e={}){let{token:r}=await d("slack"),t=["conversations.list","users.list","users.profile.get","conversations.history","conversations.replies"].includes(s),i=`https://slack.com/api/${s}`,c={Authorization:`Bearer ${r}`},o;if(t){let l=new URLSearchParams(e).toString();l&&(i+=`?${l}`)}else c["Content-Type"]="application/json; charset=utf-8",o=JSON.stringify(e);let a=await(await fetch(i,{method:t?"GET":"POST",headers:c,body:o})).json();if(!a.ok)throw new Error(`Slack API error: ${a.error}`);return a}var k={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],requiresIntegration:p.SLACK,envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack (connected)
2
2
  You have access to the user's Slack workspace. Use these tools:
3
3
  - slack_list_channels, slack_post_message, slack_reply_to_thread
4
4
  - slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
5
- - slack_get_users, slack_get_user_profile`,resolve(){let r={};for(let e of this.envKeys)process.env[e]&&(r[e]=process.env[e]);return{command:"npx",args:["-y","@modelcontextprotocol/server-slack@latest"],env:r}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{let s=await n("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(s.channels||[]).map(t=>({id:t.id,name:t.name,topic:t.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let s=await n("chat.postMessage",{channel:e.channel,text:e.text});return JSON.stringify({ok:!0,ts:s.ts,channel:s.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let s=await n("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:s.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await n("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let s=await n("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(s.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let s=await n("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(s.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_users":{let s=await n("users.list",{limit:100});return JSON.stringify({users:(s.members||[]).filter(t=>!t.is_bot&&!t.deleted).map(t=>({id:t.id,name:t.real_name||t.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let s=await n("users.profile.get",{user:e.user_id});return JSON.stringify({profile:s.profile})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(s){return JSON.stringify({error:s.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Message text"}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}}]};export{_ as slackSkill};
5
+ - slack_get_users, slack_get_user_profile`,resolve(){let s={};for(let e of this.envKeys)process.env[e]&&(s[e]=process.env[e]);return{command:"npx",args:["-y","@modelcontextprotocol/server-slack@latest"],env:s}},async handleToolCall(s,e){try{switch(s){case"slack_list_channels":{let r=await n("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(r.channels||[]).map(t=>({id:t.id,name:t.name,topic:t.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});let r=await n("chat.postMessage",{channel:e.channel,text:e.text});return JSON.stringify({ok:!0,ts:r.ts,channel:r.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let r=await n("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:r.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await n("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});let r=await n("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(r.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let r=await n("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(r.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_users":{let r=await n("users.list",{limit:100});return JSON.stringify({users:(r.members||[]).filter(t=>!t.is_bot&&!t.deleted).map(t=>({id:t.id,name:t.real_name||t.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});let r=await n("users.profile.get",{user:e.user_id});return JSON.stringify({profile:r.profile})}default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(r){return JSON.stringify({error:r.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Message text"}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}}]};export{k as slackSkill};
@@ -7,6 +7,8 @@ title: Skills
7
7
 
8
8
  A **skill** is a named bundle of MCP tools (and optional prompt fragments) that a node can opt into. Skills let you compose tool access per-node without giving every node every tool.
9
9
 
10
+ See also: [Skills reference](../skills/index.md) for per-skill docs (tools, setup, code samples).
11
+
10
12
  ## Built-in skills
11
13
 
12
14
  `@zibby/skills` ships these:
@@ -5,6 +5,8 @@ title: "@zibby/skills"
5
5
 
6
6
  # @zibby/skills
7
7
 
8
+ > Looking for skill-specific docs and examples? See [Skills reference](../skills/index.md).
9
+
8
10
  Built-in skill definitions for Zibby's test automation framework.
9
11
 
10
12
  ```bash
@@ -0,0 +1,97 @@
1
+ ---
2
+ sidebar_position: 1
3
+ title: Browser
4
+ ---
5
+
6
+ # Browser skill
7
+
8
+ Playwright-driven browser automation. Click, type, navigate, snapshot, record video. Used by `zibby test` and by any workflow node that needs to drive a web UI.
9
+
10
+ - **ID:** `browser`
11
+ - **MCP server:** `playwright` (tools exposed as `mcp__playwright__*`)
12
+
13
+ ## Tools provided
14
+
15
+ The Playwright MCP server exposes the full Playwright tool surface. Common tools:
16
+
17
+ | Tool | What it does |
18
+ |---|---|
19
+ | `browser_navigate` | Open a URL in the active tab |
20
+ | `browser_click` | Click an element by stable id or selector |
21
+ | `browser_type` | Type text into an input |
22
+ | `browser_press_key` | Press a key (Enter, Tab, etc.) |
23
+ | `browser_snapshot` | Accessibility snapshot of the current page (returns stable ids) |
24
+ | `browser_take_screenshot` | PNG screenshot saved to the session output dir |
25
+ | `browser_wait_for` | Wait for text, time, or selector |
26
+ | `browser_evaluate` | Run JS in the page context |
27
+ | `browser_fill_form` | Fill multiple fields in one call |
28
+ | `browser_select_option` | Pick a `<select>` option |
29
+ | `browser_hover` | Hover over an element |
30
+ | `browser_drag` | Drag from one element to another |
31
+ | `browser_tabs` | List, switch, or close tabs |
32
+ | `browser_close` | Close the browser |
33
+
34
+ Refer to `@zibby/mcp-browser` for the full list. All tools are gated by the `mcp__playwright__*` allowlist.
35
+
36
+ ## Setup
37
+
38
+ No setup needed beyond a working `@zibby/cli` install — the cloud runner image and the global CLI install both pull `@zibby/mcp-browser` automatically.
39
+
40
+ For local dev outside the CLI, install it explicitly:
41
+
42
+ ```bash
43
+ npm install @zibby/mcp-browser
44
+ ```
45
+
46
+ Override the bin path with `MCP_BROWSER_PATH` if you need to point at a local checkout.
47
+
48
+ ## Use in a workflow
49
+
50
+ ```js
51
+ import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
52
+ import { SKILLS } from '@zibby/skills';
53
+
54
+ export class LoginCheck extends WorkflowAgent {
55
+ buildGraph() {
56
+ const graph = new WorkflowGraph();
57
+ graph.addNode('login', {
58
+ agent: 'claude',
59
+ skills: [SKILLS.BROWSER],
60
+ prompt: (state) => `Go to ${state.appUrl}, sign in as test@example.com / hunter2,
61
+ then snapshot the dashboard and report whether the welcome banner is visible.`,
62
+ });
63
+ return graph;
64
+ }
65
+ }
66
+ ```
67
+
68
+ ### Config knobs
69
+
70
+ Passed via the node's skill config or env:
71
+
72
+ | Knob | Where | Effect |
73
+ |---|---|---|
74
+ | `headless: true` | per-skill config or `ZIBBY_HEADLESS=1` | Launch headless instead of headed |
75
+ | `sessionPath` | passed by the runner | Output dir for videos + screenshots |
76
+ | Viewport / video | fixed | 1280x720 |
77
+
78
+ ## Output example
79
+
80
+ `browser_snapshot` returns an accessibility tree with stable ids the agent can pass back to `browser_click`:
81
+
82
+ ```json
83
+ {
84
+ "url": "https://app.example.com/dashboard",
85
+ "title": "Dashboard",
86
+ "snapshot": [
87
+ { "id": "e7a1", "role": "button", "name": "New project" },
88
+ { "id": "e7b2", "role": "link", "name": "Settings" }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ ## Implementation notes
94
+
95
+ Resolves to `@zibby/mcp-browser/dist/bin/mcp-browser-zibby.js` via `require.resolve`. There is no fallback to `@playwright/mcp` — the upstream Microsoft binary lacks stable IDs and event recording, and silently looks for Chrome instead of Chromium, which broke cloud runs. If the bin can't be resolved, `skill.resolve()` throws with installation instructions.
96
+
97
+ The MCP server is spawned with `--isolated --save-video=1280x720 --viewport-size=1280x720 --output-dir=<sessionPath>`. Videos and screenshots land under the run's session directory and are uploaded with the rest of the artifacts.
@@ -0,0 +1,93 @@
1
+ ---
2
+ sidebar_position: 8
3
+ title: Chat memory
4
+ ---
5
+
6
+ # Chat memory skill
7
+
8
+ Persistent agent memory across sessions — facts, decisions, preferences, task history. Dolt-backed by default; pluggable to mem0 for embedding-based recall.
9
+
10
+ - **ID:** `chat-memory`
11
+ - **Runs in-process** — no MCP spawn
12
+
13
+ For test-run history (selectors, page models, prior runs) see [Memory](./memory.md).
14
+
15
+ ## Tools provided
16
+
17
+ | Tool | What it does |
18
+ |---|---|
19
+ | `memory_store` | Save a fact/decision/preference. Categories: `fact`, `decision`, `context`, `insight`, `preference`, `credential`, `url`, `error`, `workaround`. Tiers: `short` (24h), `mid` (default), `long` (permanent). Optional `memoryKey` for upserts |
20
+ | `memory_recall` | Search by `query`, `category`, `ticketKey`, or `tier`. Ranked by relevance × recency |
21
+ | `memory_brief` | Compact briefing — recent sessions + top long/mid-tier memories. Call at conversation start |
22
+ | `memory_end_session` | Save a session summary + key facts (semicolon-separated) for future recall |
23
+ | `task_log` | Record a completed task (`test_run`/`generate`/`analysis`/`research`/`other`) with status |
24
+ | `task_history` | Query past tasks by `ticketKey`, `type`, `status` |
25
+
26
+ ## Setup
27
+
28
+ **Dolt (default).** Install Dolt; the skill auto-creates `.zibby/memory/` on first use.
29
+
30
+ ```bash
31
+ brew install dolt
32
+ ```
33
+
34
+ **mem0 (optional).** Set `ZIBBY_MEMORY_BACKEND=mem0` (or `memory.backend: 'mem0'` in `.zibby.config.mjs`) and `npm install mem0ai` in your workspace. Configure with:
35
+
36
+ ```bash
37
+ ZIBBY_MEM0_OPENAI_BASE_URL=https://api.openai.com/v1
38
+ ZIBBY_MEM0_API_KEY=sk-...
39
+ ZIBBY_MEM0_LLM_MODEL=gpt-4.1-mini
40
+ ZIBBY_MEM0_EMBEDDER_MODEL=text-embedding-3-small
41
+ ZIBBY_MEM0_EMBEDDING_DIMS=1536
42
+ ```
43
+
44
+ mem0 mode skips Dolt session/task tables and relies on embedding search; `memory_end_session`, `task_log`, and `task_history` still write to Dolt for cross-session continuity.
45
+
46
+ ## Use in a workflow
47
+
48
+ ```js
49
+ import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
50
+ import { SKILLS } from '@zibby/skills';
51
+
52
+ export class ChatAgent extends WorkflowAgent {
53
+ buildGraph() {
54
+ const graph = new WorkflowGraph();
55
+ graph.addNode('respond', {
56
+ agent: 'claude',
57
+ skills: [SKILLS.CHAT_MEMORY, SKILLS.JIRA],
58
+ prompt: (state) => `At the start of this turn, call memory_brief to load
59
+ context. If you learn anything durable about the user's setup (e.g. their
60
+ default Jira project), call memory_store with category="preference", tier="long".
61
+ When the task is done, call memory_end_session with a 1-sentence summary.`,
62
+ });
63
+ return graph;
64
+ }
65
+ }
66
+ ```
67
+
68
+ The `memory_store` tool dedupes by normalized content — re-storing the same fact promotes its tier/relevance instead of inserting a duplicate. Long-tier rows decay 2%/session, mid-tier 10%, short-tier 30%, and short-tier rows older than 24h are deleted on next `memory_brief`.
69
+
70
+ ## Output example
71
+
72
+ `memory_brief`:
73
+
74
+ ```json
75
+ {
76
+ "recentSessions": [
77
+ { "session_id": "session_a1b2", "summary": "Reviewed SCRUM-123, added tests", "tickets": "SCRUM-123" }
78
+ ],
79
+ "topMemories": [
80
+ { "category": "preference", "tier": "long", "content": "Default Jira board: SCRUM" },
81
+ { "category": "fact", "tier": "mid", "content": "Auth login page is at /auth/login" }
82
+ ],
83
+ "taskStats": [
84
+ { "type": "test_run", "status": "passed", "cnt": 14 }
85
+ ]
86
+ }
87
+ ```
88
+
89
+ ## Implementation notes
90
+
91
+ `resolve()` returns `null` — this skill never spawns an MCP server. Tool calls are dispatched in-process via `handleToolCall(name, args, context)`, where `context.options.workspace` controls the Dolt directory.
92
+
93
+ The skill also implements `buildPromptContext(context, args)`, which the strategy calls at node start. It runs `memory_brief` internally and returns a markdown-formatted "Memory Context" block that gets prepended to the system prompt — so the model sees recent sessions and durable facts on every turn without needing an explicit tool call. The same call returns `debugPreview` for transcript logging.
@@ -0,0 +1,80 @@
1
+ ---
2
+ sidebar_position: 9
3
+ title: Core tools
4
+ ---
5
+
6
+ # Core tools skill
7
+
8
+ Baseline local capabilities — file read/write, directory listing, shell execution, URL opening, async wait. The equivalent of what Cursor or Claude Code gets natively.
9
+
10
+ - **ID:** `core-tools`
11
+ - **Runs in-process** — no MCP spawn
12
+
13
+ Most Claude/Cursor/Codex nodes get these tools by default from their agent strategy — you usually don't need to opt in explicitly. Add it to the `skills` array only if you're running a node where the strategy hasn't already wired them up (e.g. some custom strategies).
14
+
15
+ ## Tools provided
16
+
17
+ | Tool | What it does |
18
+ |---|---|
19
+ | `read_file` | Read a UTF-8 file. Max 256 KB; larger files return an error |
20
+ | `write_file` | Write content to a file, creating parent directories as needed |
21
+ | `list_directory` | List entries in a directory; directories suffixed with `/` |
22
+ | `run_command` | `execSync` a shell command. 30s timeout, 64 KB stdout cap, captures stderr |
23
+ | `open_url` | Open a URL in the default browser (uses `open`/`start`/`xdg-open`). Rejects non-http(s) URLs |
24
+ | `wait` | Sleep for N seconds (1–300). Respects `context.options.signal` for cancellation |
25
+
26
+ ## Setup
27
+
28
+ None. The skill has no env keys, no auth, no external dependencies.
29
+
30
+ ## Use in a workflow
31
+
32
+ ```js
33
+ import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
34
+ import { SKILLS } from '@zibby/skills';
35
+
36
+ export class RepoInspector extends WorkflowAgent {
37
+ buildGraph() {
38
+ const graph = new WorkflowGraph();
39
+ graph.addNode('inspect', {
40
+ agent: 'claude',
41
+ skills: [SKILLS.CORE_TOOLS],
42
+ prompt: (state) => `cd into ${state.repoPath}. List the top-level directory,
43
+ then read package.json and report the dependency graph at one level deep.`,
44
+ });
45
+ return graph;
46
+ }
47
+ }
48
+ ```
49
+
50
+ All paths are resolved against `context.options.workspace` (the node's working directory) — relative paths are safe to pass.
51
+
52
+ ## Output example
53
+
54
+ `list_directory` returns a newline-separated string:
55
+
56
+ ```
57
+ package.json
58
+ node_modules/
59
+ src/
60
+ README.md
61
+ ```
62
+
63
+ `run_command`:
64
+
65
+ ```
66
+ on main
67
+ nothing to commit, working tree clean
68
+ ```
69
+
70
+ `open_url`:
71
+
72
+ ```json
73
+ { "ok": true, "opened": "https://zibby.dev" }
74
+ ```
75
+
76
+ ## Implementation notes
77
+
78
+ `resolve()` returns `null` — there is no MCP server. The strategy dispatches tool calls in-process via `handleToolCall(name, args, context)`. `wait` is the only async handler and polls the abort signal every 500 ms so the run can cancel mid-sleep.
79
+
80
+ `run_command` shells out with `execSync` and captures stdout. Long-running commands hit the 30s timeout — for builds or test runs use the dedicated `runner` or `test-runner` skills instead.