@zibby/skills 0.1.7 → 0.1.9

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/index.js CHANGED
@@ -1 +1,646 @@
1
- import{registerSkill as l}from"@zibby/core/framework/skill-registry.js";import{browserSkill as o}from"./browser.js";import{jiraSkill as t}from"./jira.js";import{githubSkill as m}from"./github.js";import{slackSkill as r}from"./slack.js";import{memorySkill as e}from"./memory.js";import{skillInstallerSkill as k}from"./skill-installer.js";import{coreToolsSkill as S}from"./core-tools.js";import{sentrySkill as s}from"./sentry.js";import{testRunnerSkill as i}from"./test-runner.js";import{gitSkill as f}from"./git.js";import{chatMemorySkill as p}from"./chat-memory.js";import{workflowBuilderSkill as n}from"./workflow-builder.js";l(o),l(t),l(m),l(r),l(s),l(e),l(i),l(f),l(k),l(S),l(p),l(n),l({...r,id:"slack_notify"});const h={BROWSER:"browser",JIRA:"jira",GITHUB:"github",GIT:"git",SLACK:"slack",SENTRY:"sentry",MEMORY:"memory",RUNNER:"runner",SKILL_INSTALLER:"skill-installer",CORE_TOOLS:"core-tools",CHAT_MEMORY:"chat-memory",WORKFLOW_BUILDER:"workflow-builder"};import{skill as _,functionSkill as d}from"./function-skill.js";import{registerSkill as B,getSkill as K,hasSkill as N,getAllSkills as C,listSkillIds as U}from"@zibby/core/framework/skill-registry.js";export{h as SKILLS,o as browserSkill,p as chatMemorySkill,S as coreToolsSkill,d as functionSkill,C as getAllSkills,K as getSkill,f as gitSkill,m as githubSkill,N as hasSkill,t as jiraSkill,U as listSkillIds,e as memorySkill,B as registerSkill,i as runnerSkill,s as sentrySkill,_ as skill,k as skillInstallerSkill,r as slackSkill,i as testRunnerSkill,n as workflowBuilderSkill};
1
+ import{registerSkill as E}from"@zibby/workflow";import{createRequire as Yt}from"module";import{join as Zt}from"path";var Vt=Yt(import.meta.url);function Qt(){if(process.env.MCP_BROWSER_PATH)return process.env.MCP_BROWSER_PATH;try{return Vt.resolve("@zibby/mcp-browser/dist/bin/mcp-browser-zibby.js")}catch{return null}}var Te="1280x720",Ce="1280x720";function Xt({headless:r}={}){if(r===!0)return!0;if(r===!1)return!1;let t=process.env.ZIBBY_HEADLESS;return t==="1"||String(t).toLowerCase()==="true"}function Ee(r,t){let e=(r||[]).filter(s=>s!=="--headless");return t?[...e,"--headless"]:e}var xe={id:"browser",serverName:"playwright",cursorKey:"playwright-official",allowedTools:["mcp__playwright__*"],sessionEnvKey:"ZIBBY_SESSION_INFO",description:"Playwright Browser MCP Server",envKeys:[],tools:[],promptFragment:`Execute this test using the browser tools available to you. You MUST make actual browser tool calls \u2014 do not fabricate results.
2
+ If you DO NOT have access to browser tools \u2192 return {"success": false, "steps": [], "browserClosed": false, "notes": "No browser tools available"}.
3
+ DO NOT return success: true unless you ACTUALLY called browser tools.`,resolve({sessionPath:r,workspace:t,nodeName:e,headless:s}={}){let n=Qt(),i=r&&e?Zt(r,e):null,o=i||r||t||"test-results",a=Xt({headless:s}),c={};return i&&(c.ZIBBY_NODE_SESSION_PATH=i),r&&(c.ZIBBY_SESSION_PATH=r),n?{command:"node",args:Ee([n,"--isolated",`--save-video=${Te}`,`--viewport-size=${Ce}`,`--output-dir=${o}`],a),env:c}:{command:"npx",args:Ee(["-y","@playwright/mcp","--isolated",`--save-video=${Te}`,`--viewport-size=${Ce}`,"--output-dir",o],a),env:c}}};import{createRequire as es}from"module";import{resolveIntegrationToken as ts,clearTokenCache as ss}from"@zibby/core/backend-client.js";var rs=es(import.meta.url);function ns(){if(process.env.MCP_JIRA_PATH)return process.env.MCP_JIRA_PATH;try{return rs.resolve("@zibby/mcp-jira/index.js")}catch{return null}}var is=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function os(r,t){if(!t||!t.length)return r;let e=r;for(let s of t)s.type==="strong"?e=`**${e}**`:s.type==="em"?e=`_${e}_`:s.type==="code"?e=`\`${e}\``:s.type==="strike"?e=`~~${e}~~`:s.type==="link"&&s.attrs?.href&&(e=`[${e}](${s.attrs.href})`);return e}function X(r,t=0){if(!Array.isArray(r))return"";let e=[];for(let s of r){if(s.type==="text"){e.push(os(s.text||"",s.marks));continue}if(s.type==="hardBreak"){e.push(`
4
+ `);continue}if(s.type==="rule"){e.push(`
5
+ ---
6
+ `);continue}let n=s.content?X(s.content,t+1):"";if(s.type==="listItem")e.push(n);else if(s.type==="bulletList"){let i=(s.content||[]).map(o=>`- ${X(o.content||[],t+1).trim()}`);e.push(`
7
+ ${i.join(`
8
+ `)}
9
+ `)}else if(s.type==="orderedList"){let i=(s.content||[]).map((o,a)=>`${a+1}. ${X(o.content||[],t+1).trim()}`);e.push(`
10
+ ${i.join(`
11
+ `)}
12
+ `)}else if(s.type==="heading"){let i=s.attrs?.level||2;e.push(`
13
+
14
+ ${"#".repeat(i)} ${n.trim()}
15
+
16
+ `)}else is.has(s.type)?e.push(`
17
+
18
+ ${n}
19
+ `):e.push(n)}return e.join("").replace(/\n{3,}/g,`
20
+
21
+ `)}function z(r){return String(r||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function de(r){return z(r).replace(/[a-z0-9]+/g,"")}function ee(r,t){let e=z(r),s=z(t);if(!e||!s)return 0;if(e===s)return 1;if(e.length===1||s.length===1)return e===s?1:0;let n=u=>{let p=new Map;for(let y=0;y<u.length-1;y++){let m=u.slice(y,y+2);p.set(m,(p.get(m)||0)+1)}return p},i=n(e),o=n(s),a=0,c=0,l=0;for(let u of i.values())c+=u;for(let u of o.values())l+=u;for(let[u,p]of i.entries()){let y=o.get(u)||0;a+=Math.min(p,y)}return 2*a/Math.max(1,c+l)}function U(r){return String(r||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function as(r,t=[]){let e=Array.isArray(t)?t:[];if(e.length===0)return{requested:r||null,resolved:null,strategy:"none"};let s=e.filter(a=>!a.subtask),n=s.length>0?s:e,i=U(r);if(i){let a=n.find(u=>U(u.name)===i);if(a)return{requested:r,resolved:a,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(y=>U(y)===i))continue;let p=n.find(y=>u.some(m=>U(m)===U(y.name)));if(p)return{requested:r,resolved:p,strategy:"alias"}}let l=n.map(u=>({t:u,score:ee(r,u.name)})).sort((u,p)=>p.score-u.score);if(l[0]&&l[0].score>=.5)return{requested:r,resolved:l[0].t,strategy:"fuzzy"}}let o=["task","story","bug","improvement","epic"];for(let a of o){let c=n.find(l=>U(l.name)===a);if(c)return{requested:r||null,resolved:c,strategy:"default-preferred"}}return{requested:r||null,resolved:n[0],strategy:"default-first"}}async function Le(r){let t=`projectKeys=${encodeURIComponent(r)}&expand=projects.issuetypes`,e=await N(`/rest/api/3/issue/createmeta?${t}`),s=Array.isArray(e?.projects)?e.projects:[],i=s.find(a=>String(a?.key||"").toUpperCase()===String(r||"").toUpperCase())||s[0]||null;return(Array.isArray(i?.issuetypes)?i.issuetypes:[]).map(a=>({id:a.id,name:a.name,subtask:!!a.subtask,description:a.description||null}))}async function Je(r,t){if(!r)throw new Error("projectKey is required");let e="sprint is not EMPTY";t==="active"?e="sprint in openSprints()":t==="closed"?e="sprint in closedSprints()":t==="future"&&(e="sprint in futureSprints()");let s=`project = ${r} AND ${e} ORDER BY updated DESC`,n=`jql=${encodeURIComponent(s)}&maxResults=100&fields=customfield_10020`,i=await N(`/rest/api/3/search/jql?${n}`),o=new Map;for(let a of i.issues||[])for(let c of a.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((a,c)=>{let l={active:0,future:1,closed:2},u=(l[a.state]??3)-(l[c.state]??3);return u!==0?u:String(c.startDate||"").localeCompare(String(a.startDate||""))})}function cs(r,{sprintId:t,sprintName:e,target:s}={}){let n=Array.isArray(r)?r:[];if(!n.length)return{sprint:null,selectedBy:"none"};if(t!=null&&String(t).trim()!=="")return{sprint:n.find(a=>String(a.id)===String(t))||null,selectedBy:"id"};if(e&&String(e).trim()){let o=String(e).trim(),a=n.find(l=>String(l.name||"").toLowerCase()===o.toLowerCase());if(a)return{sprint:a,selectedBy:"name-exact"};let c=n.map(l=>({s:l,score:ee(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(s||"current").trim().toLowerCase();return i==="active"||i==="current"||i==="latest"?{sprint:n[0],selectedBy:i}:{sprint:n[0],selectedBy:"default"}}function ls(r,t){let e=r?.fields?.customfield_10020;return Array.isArray(e)?e.some(s=>String(s?.id)===String(t)):!1}async function us({issueKey:r,projectKey:t,sprintId:e,attempts:s=3,delayMs:n=450}){let i=[];for(let o=0;o<s;o++){try{let a=`project = ${t} AND key = ${r} AND sprint = ${e}`,c=`jql=${encodeURIComponent(a)}&maxResults=1&fields=key,status`,l=await N(`/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 p=await N(`/rest/api/3/issue/${r}?fields=customfield_10020,status`),y=ls(p,e);if(i.push({attempt:o+1,jql:!1,issueField:y}),y)return{ok:!0,method:"issue_field",traces:i}}catch(a){i.push({attempt:o+1,error:String(a?.message||a)})}o<s-1&&await new Promise(a=>setTimeout(a,n))}return{ok:!1,method:"none",traces:i}}async function me({issueKey:r,projectKey:t,sprintId:e,sprintName:s,target:n}){if(!r)return{ok:!1,error:"issueKey is required"};let i=t;if(!i&&(i=(await N(`/rest/api/3/issue/${r}?fields=project`))?.fields?.project?.key||null,!i))return{ok:!1,error:`Could not resolve project for ${r}`};let o=await Je(i,"active");if(!o.length)return{ok:!1,error:`No assignable active sprint found for project ${i}`};let{sprint:a,selectedBy:c}=cs(o,{sprintId:e,sprintName:s,target:n});if(!a)return{ok:!1,error:`No matching sprint found in ${i}`,requested:{sprintId:e??null,sprintName:s??null,target:n??"current"},availableSprints:o.map(p=>({id:p.id,name:p.name,state:p.state}))};await N(`/rest/api/3/issue/${r}`,{method:"PUT",body:{fields:{customfield_10020:Number(a.id)}}});let l=await us({issueKey:r,projectKey:i,sprintId:a.id}),u=l.ok;return{ok:u,issueKey:r,projectKey:i,sprintId:a.id,sprintName:a.name,selectedBy:c,verifiedBy:l.method,verified:u,verificationTrace:l.traces,warning:u?null:`Sprint assignment attempted but verification did not find ${r} in sprint ${a.id}`}}async function N(r,t={}){let e=async()=>{let{token:s,cloudId:n}=await ts("jira");if(typeof s!="string"||!s)throw new Error(`Invalid jira token type: ${typeof s}`);if(!n)throw new Error("Invalid jira cloudId: missing");let i=`https://api.atlassian.com/ex/jira/${n}${r}`,o=await fetch(i,{method:t.method||"GET",headers:{Authorization:`Bearer ${s}`,Accept:"application/json",...t.body?{"Content-Type":"application/json"}:{},...t.headers},body:t.body?JSON.stringify(t.body):void 0});if(!o.ok){let c=await o.text().catch(()=>"");throw new Error(`Jira API ${o.status}: ${c.slice(0,300)}`)}let a=await o.text().catch(()=>"");if(!a||!a.trim())return{};try{return JSON.parse(a)}catch{return{raw:a}}};try{return await e()}catch(s){let n=String(s?.message||s||"").toLowerCase();if(!(n.includes("token")||n.includes("401")||n.includes("403")||n.includes("substring")))throw s;return ss("jira"),e()}}var De={id:"jira",serverName:"jira",allowedTools:["mcp__jira__*"],envKeys:["ATLASSIAN_ACCESS_TOKEN","ATLASSIAN_CLOUD_ID"],description:"Zibby Jira MCP Server (OAuth Bearer)",promptFragment:`## Jira (connected)
22
+ You have direct access to the user's Jira. Use these tools proactively:
23
+
24
+ ### Issue tools
25
+ - jira_search: Search issues with JQL (e.g. "project = PROJ AND status != Done ORDER BY updated DESC")
26
+ - jira_get_issue: Get full details of a ticket by key (e.g. PROJ-123)
27
+ - jira_list_statuses: List available Jira statuses (global or project-specific)
28
+ - jira_list_issue_types: List issue types allowed for issue creation in a project
29
+ - jira_create_issue: Create a new ticket (requires projectKey + summary)
30
+ - jira_get_comments: Get comments on a ticket (newest first) \u2014 use this to find testing steps, notes, etc.
31
+ - jira_add_comment: Add a comment to a ticket
32
+ - jira_edit_issue: Update fields (summary, labels, priority, story points)
33
+ - jira_transition_issue: Move a ticket to a different status (pass transitionId or toStatus)
34
+
35
+ ### Project & sprint tools
36
+ - jira_list_projects: List all projects
37
+ - jira_list_sprints: List sprints for a project (filter by state: active/closed/future)
38
+ - jira_get_sprint_issues: Get all issues in a sprint \u2014 filter by status name (e.g. "\u8FDB\u884C\u4E2D", "\u6D4B\u8BD5", "In Progress"). Returns status breakdown.
39
+ - jira_move_issue_to_sprint: Move an issue to a sprint (current/active/latest/by-id/by-name) and verify membership.
40
+
41
+ ### Sprint membership updates
42
+ - To move an issue into a sprint, use jira_edit_issue with fields.customfield_10020 set to sprint numeric id.
43
+ - Example: jira_edit_issue({ issueKey: "PROJ-123", fields: { customfield_10020: 10 } })
44
+ - Always verify by calling jira_get_sprint_issues(sprintId, projectKey) and checking the issue key is present.
45
+ - For "create and place into current sprint" requests, use a generic atomic flow:
46
+ - Prefer jira_create_issue with moveToSprint=true (optionally sprintId/sprintName/target)
47
+ - Or create first, then use jira_move_issue_to_sprint
48
+ - Always report verified sprint membership result (not just status transition)
49
+
50
+ ### Search strategy (important!)
51
+ 1. **Board/sprint first**: When the user asks about "my board", "testing tickets", or "what's in progress", ALWAYS use the sprint path: jira_list_sprints (state: active) \u2192 jira_get_sprint_issues. This finds ALL tickets regardless of age.
52
+ 2. **Project-scoped search**: If you know the project key, use "project = KEY AND status != Done ORDER BY updated DESC" \u2014 no date filter needed when scoped to a project.
53
+ 3. **Global search (last resort)**: Only use broad JQL like "created >= -365d" when you genuinely don't know the project. Never use -90d \u2014 it misses older tickets still in testing.
54
+ 4. **Remember the board**: After finding the user's project/board, store it in memory (memory_store) so you go straight there next time.
55
+ 5. **Status discovery**: NEVER use jira_search with guessed status keywords to determine whether a status exists. Use jira_list_statuses (project-scoped when possible) and/or jira_transition_issue(issueKey) without transitionId.
56
+
57
+ When the user asks about "my tickets" or "my board" and you know their project from memory, go directly to that project's active sprint.
58
+ When the user asks about projects or boards, call jira_list_projects.
59
+ When the user asks about sprints: jira_list_sprints \u2192 jira_get_sprint_issues.
60
+ When user asks to move ticket into a sprint, do NOT use status transition. Use jira_move_issue_to_sprint(issueKey, projectKey?, sprintId|sprintName|target) and report verified result.
61
+ When the user asks about testing steps, test cases, or wants to run tests for a ticket: call jira_get_comments \u2014 testing steps are typically written in the ticket's comments, not the description.
62
+ JQL must be bounded (Jira rejects unbounded queries). Use "project = KEY AND status != Done" for project queries. Use "created >= -365d ORDER BY updated DESC" for global queries.
63
+
64
+ ### Transition workflow (MANDATORY)
65
+ When user asks to move/transition ticket status:
66
+ 1. If user explicitly gives a target status (e.g. "move to \u8FDB\u884C\u4E2D", "move that in progress", "move to AI \u9A8C\u6536"), call jira_transition_issue with issueKey + toStatus directly. Do NOT call list-only mode first.
67
+ 2. If target is ambiguous or missing, call jira_transition_issue({ issueKey }) with no transitionId to list available transitions.
68
+ 3. Pick the correct transition from returned list (match by "to" status name, not guesswork), then call jira_transition_issue with transitionId.
69
+ 4. Call jira_get_issue(issueKey) to verify final status before claiming success.
70
+ 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.
71
+ 6. IMPORTANT: When target is clear, complete transition + verification in SAME turn. Do NOT stop after listing options.`,resolve(){let r=ns();if(!r)return null;let t={};for(let e of this.envKeys)process.env[e]&&(t[e]=process.env[e]);return process.env.ATLASSIAN_INSTANCE_URL&&(t.ATLASSIAN_INSTANCE_URL=process.env.ATLASSIAN_INSTANCE_URL),{command:"node",args:[r],env:t,description:this.description}},async handleToolCall(r,t){try{switch(r){case"jira_list_projects":{let e=await N("/rest/api/3/project"),s=(Array.isArray(e)?e:[]).map(n=>({id:n.id,key:n.key,name:n.name,style:n.style}));return JSON.stringify({count:s.length,projects:s})}case"jira_list_statuses":{let{projectKey:e}=t||{};if(e){let i=await N(`/rest/api/3/project/${encodeURIComponent(e)}/statuses`),o=Array.isArray(i)?i:[],a=new Map;for(let l of o)for(let u of l.statuses||[])u?.id&&(a.has(u.id)||a.set(u.id,{id:u.id,name:u.name,category:u.statusCategory?.name||null}));let c=[...a.values()].sort((l,u)=>String(l.name).localeCompare(String(u.name)));return JSON.stringify({scope:"project",projectKey:e,count:c.length,statuses:c})}let s=await N("/rest/api/3/status"),n=(Array.isArray(s)?s:[]).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:n.length,statuses:n})}case"jira_list_issue_types":{let{projectKey:e}=t||{};if(!e)return JSON.stringify({error:"projectKey is required"});let s=await Le(e);return JSON.stringify({projectKey:e,count:s.length,issueTypes:s})}case"jira_search":{let e=t.jql||"",s=t.maxResults||20;e.replace(/\s*ORDER\s+BY\s+.*/i,"").trim()||(e=`created >= -365d ${e}`.trim());let i=`jql=${encodeURIComponent(e)}&maxResults=${s}&fields=summary,status,assignee,priority,updated,issuetype,project`,a=((await N(`/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:a.length,issues:a})}case"jira_get_issue":{let e=t.issueKey;if(!e)return JSON.stringify({error:"issueKey is required"});let s=await N(`/rest/api/3/issue/${e}`);return JSON.stringify({key:s.key,project:s.fields?.project?.key,summary:s.fields?.summary,description:s.fields?.description,status:s.fields?.status?.name,assignee:s.fields?.assignee?.displayName||"Unassigned",priority:s.fields?.priority?.name,type:s.fields?.issuetype?.name,labels:s.fields?.labels,created:s.fields?.created,updated:s.fields?.updated})}case"jira_create_issue":{let{projectKey:e,summary:s,issueType:n,description:i,priority:o,labels:a,assigneeId:c,moveToSprint:l,moveToActiveSprint:u,sprintId:p,sprintName:y,target:m}=t;if(!e||!s)return JSON.stringify({error:"projectKey and summary are required"});let f={requested:n||null,resolved:null,strategy:"none"},k=[];try{k=await Le(e),f=as(n,k)}catch{}let h={project:{key:e},summary:s,issuetype:f?.resolved?.id?{id:f.resolved.id}:{name:n||"Task"}};i&&(h.description={type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:i}]}]}),o&&(h.priority={name:o}),a?.length&&(h.labels=a),c&&(h.assignee={id:c});let g=await N("/rest/api/3/issue",{method:"POST",body:{fields:h}}),_={ok:!0,key:g.key,id:g.id,self:g.self};return f?.resolved&&(_.issueType=f.resolved.name,_.issueTypeResolution=f.strategy,f.strategy!=="exact"&&f.requested&&U(f.requested)!==U(f.resolved.name)&&(_.issueTypeWarning=`Requested "${f.requested}" is not available in ${e}; used "${f.resolved.name}" instead.`)),k.length>0&&(_.availableIssueTypes=k.map(d=>d.name)),(l||u)&&(_.sprintMove=await me({issueKey:g.key,projectKey:e,sprintId:p,sprintName:y,target:m})),JSON.stringify(_)}case"jira_list_sprints":{let{projectKey:e,state:s}=t,n=await Je(e,s);return JSON.stringify({count:n.length,sprints:n})}case"jira_move_to_active_sprint":{let{issueKey:e,projectKey:s,sprintId:n,sprintName:i,target:o}=t||{},a=await me({issueKey:e,projectKey:s,sprintId:n,sprintName:i,target:o||"current"});return JSON.stringify(a)}case"jira_move_issue_to_sprint":{let{issueKey:e,projectKey:s,sprintId:n,sprintName:i,target:o}=t||{},a=await me({issueKey:e,projectKey:s,sprintId:n,sprintName:i,target:o});return JSON.stringify(a)}case"jira_get_sprint_issues":{let{sprintName:e,sprintId:s,projectKey:n,status:i,maxResults:o}=t;if(!e&&!s)return JSON.stringify({error:"sprintName or sprintId is required"});let a=o||50,c=s?`sprint = ${s}`:`sprint = "${e}"`,l=n?`project = ${n} AND `:"",u=i?` AND status = "${i}"`:"",p=`${l}${c}${u} ORDER BY status ASC, priority DESC`,y=`jql=${encodeURIComponent(p)}&maxResults=${a}&fields=summary,status,assignee,priority,issuetype,project`,m=await N(`/rest/api/3/search/jql?${y}`),f=(m.issues||[]).map(h=>({key:h.key,project:h.fields?.project?.key,summary:h.fields?.summary,status:h.fields?.status?.name,assignee:h.fields?.assignee?.displayName||"Unassigned",priority:h.fields?.priority?.name,type:h.fields?.issuetype?.name})),k={};for(let h of f)k[h.status]=(k[h.status]||0)+1;return JSON.stringify({count:f.length,total:m.total||f.length,statusCounts:k,issues:f})}case"jira_get_comments":{let{issueKey:e,maxResults:s}=t;if(!e)return JSON.stringify({error:"issueKey is required"});let i=await N(`/rest/api/3/issue/${e}/comment?maxResults=${s||50}&orderBy=-created`),o=(i.comments||[]).map(a=>{let c="";return a.body?.content&&(c=X(a.body.content)),{id:a.id,author:a.author?.displayName||"Unknown",body:c,created:a.created,updated:a.updated}});return JSON.stringify({count:o.length,total:i.total||o.length,comments:o})}case"jira_add_comment":{let{issueKey:e,body:s}=t;return!e||!s?JSON.stringify({error:"issueKey and body are required"}):(await N(`/rest/api/3/issue/${e}/comment`,{method:"POST",body:{body:{type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:s}]}]}}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_edit_issue":{let{issueKey:e,fields:s}=t;return!e||!s?JSON.stringify({error:"issueKey and fields are required"}):(await N(`/rest/api/3/issue/${e}`,{method:"PUT",body:{fields:s}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_transition_issue":{let{issueKey:e,transitionId:s,toStatus:n,statusName:i,status:o}=t;if(!e)return JSON.stringify({error:"issueKey is required"});let a=String(n||i||o||"").trim();if(!s&&!a){let p=((await N(`/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:p})}let c=s;if(!c){let p=(await N(`/rest/api/3/issue/${e}/transitions`)).transitions||[],y=z(a),m=p.find(f=>z(f?.name||"")===y||z(f?.to?.name||"")===y);if(!m){let f=de(a);f.length>=2&&(m=p.find(k=>{let h=de(k?.name||""),g=de(k?.to?.name||""),_=h.length>=2&&(h.includes(f)||f.includes(h)),d=g.length>=2&&(g.includes(f)||f.includes(g));return _||d}))}if(!m){let f=p.map(_=>{let d=ee(a,_?.name||""),b=ee(a,_?.to?.name||"");return{t:_,score:Math.max(d,b)}}).sort((_,d)=>d.score-_.score),k=f[0],h=f[1];k&&k.score>=.45&&(!h||k.score-h.score>=.12)&&(m=k.t)}if(!m?.id)return JSON.stringify({ok:!1,error:`No transition matches target status: "${a}"`,issueKey:e,availableTransitions:p.map(f=>({id:f.id,name:f.name,to:f.to?.name}))});c=m.id}await N(`/rest/api/3/issue/${e}/transitions`,{method:"POST",body:{transition:{id:c}}});let l=await N(`/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: ${r}`})}}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"]}}]};import{resolveIntegrationToken as Me}from"@zibby/core/backend-client.js";async function R(r,t={}){let{token:e}=await Me("github"),s=r.startsWith("https://")?r:`https://api.github.com${r}`,n={Authorization:`Bearer ${e}`,Accept:t.accept||"application/vnd.github.v3+json","User-Agent":"Zibby-App",...t.body?{"Content-Type":"application/json"}:{}},i=await fetch(s,{method:t.method||"GET",headers:n,body:t.body?JSON.stringify(t.body):void 0});if(!i.ok){let o=await i.text().catch(()=>"");throw new Error(`GitHub API ${i.status}: ${o.slice(0,300)}`)}return t.raw?i.text():i.json()}var Ue={id:"github",serverName:"github",allowedTools:["mcp__github__*"],envKeys:["GITHUB_TOKEN"],description:"GitHub \u2014 issues, PRs, commits, code search, file reading",promptFragment:`## GitHub (connected)
72
+ You have access to the user's GitHub repositories. Available tools:
73
+
74
+ ### Discovery
75
+ - github_list_repos: Lists ALL accessible repos (personal + orgs, private + public, up to 200 repos)
76
+ - github_search_repos: Search for a specific repo by name (e.g., "electron", "my-app")
77
+ - github_get_user: Get authenticated user's profile
78
+ - github_list_orgs: List organizations with accessible repos
79
+
80
+ ### Clone & Code Reading
81
+ - github_clone: Clone a repo locally. If the user specifies a destination (e.g. "to my Desktop", "to ~/Downloads"), pass that as the destination param. Otherwise it defaults to ~/zibby-repos/. ALWAYS show the contents field in the response.
82
+ - github_get_file: Read a file's content from a repo (without cloning)
83
+ - github_list_commits: List recent commits
84
+ - github_get_commit: Get commit details and diff
85
+
86
+ ### Issues & PRs
87
+ - github_search_issues: Search issues and PRs
88
+ - github_search_code: Search code across repos
89
+ - github_get_pr: Get PR details
90
+ - github_get_pr_diff: Get PR diff
91
+ - github_list_pr_files: List PR changed files
92
+ - github_list_pr_comments: Get PR comments
93
+ - github_create_issue: Create new issue
94
+
95
+ ### Important: "Check out" / "Clone"
96
+ When user says "check out repo-name" or "clone repo-name":
97
+ 1. Call github_clone. Pass the user's requested destination if given (e.g. "~/Downloads", "/Users/me/Desktop"). The tool handles ~ expansion and absolute paths.
98
+ 2. Display the path and contents field (directory listing)
99
+ 3. STOP. Do not offer to inspect files or ask what to do next.
100
+
101
+ When user just wants to "look at" or "read" files (not clone):
102
+ - Use github_get_file to read individual files via API`,resolve(){let r={};for(let t of this.envKeys)process.env[t]&&(r[t]=process.env[t]);return{command:"npx",args:["-y","@modelcontextprotocol/server-github@latest"],env:r}},async handleToolCall(r,t){try{switch(r){case"github_search_issues":{let e=t.query;if(!e)return JSON.stringify({error:"query is required"});let s=await R(`/search/issues?q=${encodeURIComponent(e)}&per_page=${t.limit||20}`),n=(s.items||[]).map(i=>({number:i.number,title:i.title,state:i.state,repo:i.repository_url?.split("/").slice(-2).join("/"),url:i.html_url,user:i.user?.login,isPR:!!i.pull_request,labels:(i.labels||[]).map(o=>o.name),createdAt:i.created_at}));return JSON.stringify({total:s.total_count,items:n})}case"github_search_code":{let e=t.query;if(!e)return JSON.stringify({error:"query is required"});let s=t.repo?`+repo:${t.repo}`:"",n=t.language?`+language:${t.language}`:"",i=await R(`/search/code?q=${encodeURIComponent(e)}${s}${n}&per_page=${t.limit||15}`),o=(i.items||[]).map(a=>({name:a.name,path:a.path,repo:a.repository?.full_name,url:a.html_url,score:a.score}));return JSON.stringify({total:i.total_count,items:o})}case"github_get_pr":{let{owner:e,repo:s,number:n}=t;if(!e||!s||!n)return JSON.stringify({error:"owner, repo, and number are required"});let i=await R(`/repos/${e}/${s}/pulls/${n}`);return JSON.stringify({number:i.number,title:i.title,state:i.state,merged:i.merged,body:i.body?.slice(0,5e3),user:i.user?.login,branch:i.head?.ref,base:i.base?.ref,changedFiles:i.changed_files,additions:i.additions,deletions:i.deletions,createdAt:i.created_at,mergedAt:i.merged_at,url:i.html_url,labels:(i.labels||[]).map(o=>o.name)})}case"github_get_pr_diff":{let{owner:e,repo:s,number:n}=t;if(!e||!s||!n)return JSON.stringify({error:"owner, repo, and number are required"});let i=await R(`/repos/${e}/${s}/pulls/${n}`,{accept:"application/vnd.github.v3.diff",raw:!0}),o=i.length>15e3;return JSON.stringify({number:n,diff:o?i.slice(0,15e3):i,truncated:o,totalLength:i.length})}case"github_list_pr_files":{let{owner:e,repo:s,number:n}=t;if(!e||!s||!n)return JSON.stringify({error:"owner, repo, and number are required"});let i=await R(`/repos/${e}/${s}/pulls/${n}/files?per_page=100`);return JSON.stringify({total:i.length,files:i.map(o=>({filename:o.filename,status:o.status,additions:o.additions,deletions:o.deletions,patch:o.patch?.slice(0,3e3)}))})}case"github_list_pr_comments":{let{owner:e,repo:s,number:n}=t;if(!e||!s||!n)return JSON.stringify({error:"owner, repo, and number are required"});let i=await R(`/repos/${e}/${s}/pulls/${n}/comments?per_page=50`),o=await R(`/repos/${e}/${s}/issues/${n}/comments?per_page=50`),a=[...i.map(c=>({type:"review",user:c.user?.login,body:c.body?.slice(0,1e3),path:c.path,line:c.line,createdAt:c.created_at})),...o.map(c=>({type:"issue",user:c.user?.login,body:c.body?.slice(0,1e3),createdAt:c.created_at}))].sort((c,l)=>new Date(c.createdAt)-new Date(l.createdAt));return JSON.stringify({total:a.length,comments:a})}case"github_list_commits":{let{owner:e,repo:s,branch:n,path:i,limit:o}=t;if(!e||!s)return JSON.stringify({error:"owner and repo are required"});let a=`/repos/${e}/${s}/commits?per_page=${o||20}`;n&&(a+=`&sha=${encodeURIComponent(n)}`),i&&(a+=`&path=${encodeURIComponent(i)}`);let c=await R(a);return JSON.stringify({total:c.length,commits:c.map(l=>({sha:l.sha?.slice(0,8),fullSha:l.sha,message:l.commit?.message?.slice(0,300),author:l.commit?.author?.name,date:l.commit?.author?.date,url:l.html_url}))})}case"github_get_commit":{let{owner:e,repo:s,sha:n}=t;if(!e||!s||!n)return JSON.stringify({error:"owner, repo, and sha are required"});let i=await R(`/repos/${e}/${s}/commits/${n}`);return JSON.stringify({sha:i.sha?.slice(0,8),message:i.commit?.message,author:i.commit?.author?.name,date:i.commit?.author?.date,stats:i.stats,files:(i.files||[]).map(o=>({filename:o.filename,status:o.status,additions:o.additions,deletions:o.deletions,patch:o.patch?.slice(0,3e3)}))})}case"github_get_file":{let{owner:e,repo:s,path:n,ref:i}=t;if(!e||!s||!n)return JSON.stringify({error:"owner, repo, and path are required"});let o=`/repos/${e}/${s}/contents/${encodeURIComponent(n)}`;i&&(o+=`?ref=${encodeURIComponent(i)}`);let a=await R(o);if(a.type!=="file")return Array.isArray(a)?JSON.stringify({type:"directory",path:n,entries:a.map(u=>({name:u.name,type:u.type,size:u.size,path:u.path}))}):JSON.stringify({error:`Not a file: ${a.type}`});let c=Buffer.from(a.content||"","base64").toString("utf-8"),l=c.length>2e4;return JSON.stringify({path:a.path,size:a.size,sha:a.sha?.slice(0,8),content:l?c.slice(0,2e4):c,truncated:l})}case"github_get_user":try{let e=await R("/installation/repositories?per_page=1");if(e.repositories&&e.repositories.length>0){let s=e.repositories[0],n=s.owner.login,i=s.owner.type,o=i==="Organization"?`/orgs/${n}`:`/users/${n}`,a=await R(o);return JSON.stringify({login:a.login,name:a.name||a.login,avatar:a.avatar_url,bio:a.bio||a.description,type:i,isOrg:i==="Organization",publicRepos:a.public_repos,message:"Showing GitHub App installation owner (GitHub Apps cannot access /user endpoint)"})}return JSON.stringify({error:"No repositories accessible to this GitHub App installation"})}catch(e){return JSON.stringify({error:`GitHub App cannot access /user endpoint. Use github_list_repos instead. (${e.message})`})}case"github_list_orgs":try{let s=(await R("/installation/repositories?per_page=100")).repositories||[],n=new Map;for(let o of s)o.owner.type==="Organization"&&(n.has(o.owner.login)||n.set(o.owner.login,{login:o.owner.login,description:null,url:o.owner.url}));let i=Array.from(n.values());return JSON.stringify({count:i.length,orgs:i,message:"Extracted from accessible repositories (GitHub Apps cannot access /user/orgs directly)"})}catch(e){return JSON.stringify({error:`GitHub App cannot list orgs via /user/orgs. Error: ${e.message}`})}case"github_clone":{let f=function(g){let _=g.replace(/^~(?=$|\/|\\)/,m);return a(_)},{owner:e,repo:s,destination:n}=t;if(!e||!s)return JSON.stringify({error:"owner and repo are required"});let{execSync:i}=await import("child_process"),{join:o,resolve:a}=await import("path"),{existsSync:c,mkdirSync:l}=await import("fs"),{homedir:u,platform:p}=await import("os"),{token:y}=await Me("github"),m=u(),k=n?f(n):o(m,"zibby-repos"),h=o(k,s);if(l(k,{recursive:!0}),c(h))return JSON.stringify({error:`Directory ${h} already exists. Remove it first or use a different destination.`,existingPath:h});try{let g=`https://x-access-token:${y}@github.com/${e}/${s}.git`;i(`git clone ${g} "${h}"`,{stdio:"pipe"});let _=p()==="win32",d;return _?d=i(`dir "${h}"`,{encoding:"utf-8",shell:"cmd.exe"}):d=i(`ls -la "${h}"`,{encoding:"utf-8"}),JSON.stringify({success:!0,path:h,message:`Cloned ${e}/${s} to ${h}`,contents:d.split(`
103
+ `).slice(0,30).join(`
104
+ `),instructions:"IMPORTANT: Show the contents field to the user - it contains the directory listing."})}catch(g){return JSON.stringify({error:`Clone failed: ${g.message}`})}}case"github_search_repos":{let{query:e,limit:s}=t;if(!e)return JSON.stringify({error:"query is required"});let n=await this.handleToolCall("github_list_repos",{limit:200},{}),i=JSON.parse(n);if(i.error)return JSON.stringify(i);let o=e.toLowerCase(),a=i.repos.filter(c=>c.name.toLowerCase().includes(o)||c.fullName.toLowerCase().includes(o)||c.description&&c.description.toLowerCase().includes(o));return JSON.stringify({query:e,count:a.length,repos:a.slice(0,s||20)})}case"github_list_repos":{let{owner:e,type:s,sort:n,direction:i,limit:o}=t,a=100,c=o||200,l=[];if(!e){let f=1,k=!0;for(;k&&l.length<c;){let d=`/installation/repositories?per_page=${a}&page=${f}`,I=(await R(d)).repositories||[];if(I.length===0)break;l=l.concat(I),k=I.length===a,f++}let h=l.slice(0,c).map(d=>({name:d.name,fullName:d.full_name,private:d.private,description:d.description,language:d.language,defaultBranch:d.default_branch,updatedAt:d.updated_at,stars:d.stargazers_count,url:d.html_url})),g=h.filter(d=>d.private).length,_=h.filter(d=>!d.private).length;return JSON.stringify({count:h.length,repos:h,privateCount:g,publicCount:_,message:`Found ${g} private and ${_} public repos`})}let u=await R(`/orgs/${e}`).then(()=>!0).catch(()=>!1),p=1,y=!0;for(;y&&l.length<c;){let f;u?f=`/orgs/${e}/repos?per_page=${a}&page=${p}&type=${s||"all"}&sort=${n||"updated"}&direction=${i||"desc"}`:f=`/users/${e}/repos?per_page=${a}&page=${p}&type=${s||"all"}&sort=${n||"updated"}&direction=${i||"desc"}`;let k=await R(f),h=Array.isArray(k)?k:[];if(h.length===0)break;l=l.concat(h),y=h.length===a,p++}let m=l.slice(0,c).map(f=>({name:f.name,fullName:f.full_name,private:f.private,description:f.description,language:f.language,defaultBranch:f.default_branch,updatedAt:f.updated_at,stars:f.stargazers_count,url:f.html_url}));return JSON.stringify({count:m.length,repos:m})}case"github_create_issue":{let{owner:e,repo:s,title:n,body:i}=t;if(!e||!s||!n)return JSON.stringify({error:"owner, repo, and title are required"});let o=await R(`/repos/${e}/${s}/issues`,{method:"POST",body:{title:n,body:i||""}});return JSON.stringify({number:o.number,url:o.html_url,title:o.title})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(e){return JSON.stringify({error:e.message})}},tools:[{name:"github_get_user",description:"Get the authenticated GitHub user profile and their organizations",input_schema:{type:"object",properties:{}}},{name:"github_list_orgs",description:"List GitHub organizations the authenticated user belongs to",input_schema:{type:"object",properties:{}}},{name:"github_list_repos",description:"List repositories for a user or org. If no owner given, lists the authenticated user's repos.",input_schema:{type:"object",properties:{owner:{type:"string",description:"Org or user login. Omit to list your own repos."},type:{type:"string",enum:["all","public","private","forks","sources","member"],description:"Filter by type (default: all)"},sort:{type:"string",enum:["created","updated","pushed","full_name"],description:"Sort field (default: updated)"},direction:{type:"string",enum:["asc","desc"],description:"Sort direction (default: desc)"},limit:{type:"number",description:"Max repos to return (default: 30)"}}}},{name:"github_clone",description:'Clone a GitHub repository to the local filesystem. Use when user says "check out" or "clone" a repo.',input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner (user or org name)"},repo:{type:"string",description:"Repository name"},destination:{type:"string",description:"Destination directory. Accepts absolute paths, ~-prefixed paths, or relative names. Defaults to ~/zibby-repos/<repo>."}},required:["owner","repo"]}},{name:"github_search_repos",description:"Search accessible repositories by name or description. Use this when the user asks to find a specific repo.",input_schema:{type:"object",properties:{query:{type:"string",description:'Search term to match against repo name or description (e.g., "electron", "my-app")'},limit:{type:"number",description:"Max results (default: 20)"}},required:["query"]}},{name:"github_search_issues",description:"Search GitHub issues and pull requests",input_schema:{type:"object",properties:{query:{type:"string",description:'GitHub search query (e.g. "SCRUM-123", "login bug repo:org/app")'},limit:{type:"number",description:"Max results (default: 20)"}},required:["query"]}},{name:"github_search_code",description:"Search code across GitHub repositories by keyword",input_schema:{type:"object",properties:{query:{type:"string",description:'Code search query (e.g. "handleLogin", "class AuthService")'},repo:{type:"string",description:'Scope to a specific repo (e.g. "org/app"). Optional.'},language:{type:"string",description:'Filter by language (e.g. "javascript", "python"). Optional.'},limit:{type:"number",description:"Max results (default: 15)"}},required:["query"]}},{name:"github_get_pr",description:"Get details of a pull request \u2014 title, description, branch, stats",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},number:{type:"number",description:"PR number"}},required:["owner","repo","number"]}},{name:"github_get_pr_diff",description:"Get the unified diff of a pull request \u2014 the actual code changes",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},number:{type:"number",description:"PR number"}},required:["owner","repo","number"]}},{name:"github_list_pr_files",description:"List files changed in a PR with per-file patches",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},number:{type:"number",description:"PR number"}},required:["owner","repo","number"]}},{name:"github_list_pr_comments",description:"Get all review and issue comments on a PR",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},number:{type:"number",description:"PR number"}},required:["owner","repo","number"]}},{name:"github_list_commits",description:"List recent commits on a branch, optionally filtered by file path",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},branch:{type:"string",description:"Branch name (default: repo default branch)"},path:{type:"string",description:"Filter commits touching this file path"},limit:{type:"number",description:"Max commits (default: 20)"}},required:["owner","repo"]}},{name:"github_get_commit",description:"Get details of a specific commit \u2014 message, stats, file diffs",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},sha:{type:"string",description:"Commit SHA (full or short)"}},required:["owner","repo","sha"]}},{name:"github_get_file",description:"Read a file (or list a directory) from a GitHub repo. Works on any branch/ref.",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},path:{type:"string",description:'File or directory path (e.g. "src/auth/login.ts")'},ref:{type:"string",description:"Branch, tag, or commit SHA (default: repo default branch)"}},required:["owner","repo","path"]}},{name:"github_create_issue",description:"Create a GitHub issue",input_schema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},title:{type:"string",description:"Issue title"},body:{type:"string",description:"Issue body (markdown)"}},required:["owner","repo","title"]}}]};import{resolveIntegrationToken as ps}from"@zibby/core/backend-client.js";async function q(r,t={}){let{token:e}=await ps("slack"),s=["conversations.list","users.list","users.profile.get","conversations.history","conversations.replies"].includes(r),n=`https://slack.com/api/${r}`,i={Authorization:`Bearer ${e}`},o;if(s){let l=new URLSearchParams(t).toString();l&&(n+=`?${l}`)}else i["Content-Type"]="application/json; charset=utf-8",o=JSON.stringify(t);let c=await(await fetch(n,{method:s?"GET":"POST",headers:i,body:o})).json();if(!c.ok)throw new Error(`Slack API error: ${c.error}`);return c}var fe={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack (connected)
105
+ You have access to the user's Slack workspace. Use these tools:
106
+ - slack_list_channels, slack_post_message, slack_reply_to_thread
107
+ - slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
108
+ - slack_get_users, slack_get_user_profile`,resolve(){let r={};for(let t of this.envKeys)process.env[t]&&(r[t]=process.env[t]);return{command:"npx",args:["-y","@modelcontextprotocol/server-slack@latest"],env:r}},async handleToolCall(r,t){try{switch(r){case"slack_list_channels":{let e=await q("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(e.channels||[]).map(s=>({id:s.id,name:s.name,topic:s.topic?.value}))})}case"slack_post_message":{if(!t.channel||!t.text)return JSON.stringify({error:"channel and text are required"});let e=await q("chat.postMessage",{channel:t.channel,text:t.text});return JSON.stringify({ok:!0,ts:e.ts,channel:e.channel})}case"slack_reply_to_thread":{if(!t.channel||!t.thread_ts||!t.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});let e=await q("chat.postMessage",{channel:t.channel,thread_ts:t.thread_ts,text:t.text});return JSON.stringify({ok:!0,ts:e.ts})}case"slack_add_reaction":return!t.channel||!t.timestamp||!t.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await q("reactions.add",{channel:t.channel,timestamp:t.timestamp,name:t.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!t.channel)return JSON.stringify({error:"channel is required"});let e=await q("conversations.history",{channel:t.channel,limit:t.limit||20});return JSON.stringify({messages:(e.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_thread_replies":{if(!t.channel||!t.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});let e=await q("conversations.replies",{channel:t.channel,ts:t.thread_ts});return JSON.stringify({messages:(e.messages||[]).map(s=>({user:s.user,text:s.text,ts:s.ts}))})}case"slack_get_users":{let e=await q("users.list",{limit:100});return JSON.stringify({users:(e.members||[]).filter(s=>!s.is_bot&&!s.deleted).map(s=>({id:s.id,name:s.real_name||s.name}))})}case"slack_get_user_profile":{if(!t.user_id)return JSON.stringify({error:"user_id is required"});let e=await q("users.profile.get",{user:t.user_id});return JSON.stringify({profile:e.profile})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(e){return JSON.stringify({error:e.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Message text"}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}}]};import{createRequire as ds}from"module";import{execFileSync as ms}from"child_process";import{join as qe}from"path";import{existsSync as fs}from"fs";var ys=ds(import.meta.url);function gs(){if(process.env.MCP_MEMORY_PATH)return process.env.MCP_MEMORY_PATH;try{return ys.resolve("@zibby/memory/mcp-server")}catch{return null}}var Ke={id:"memory",serverName:"memory",allowedTools:["mcp__memory__*"],envKeys:[],description:"Zibby Memory MCP Server (test history, selectors, page model)",async middleware(){try{let{createMemoryMiddleware:r}=await import("@zibby/memory");return r()}catch{return null}},promptFragment:`BEFORE executing browser actions:
109
+ - Review any test memory/history above. Prefer selectors proven to work.
110
+ - If a previous run failed, avoid the same approach.
111
+ - After setup/login completes, navigate directly to the target page instead of clicking through menus.
112
+
113
+ DURING execution \u2014 when a selector fails and you switch to a fallback:
114
+ - Call memory_save_insight IMMEDIATELY with category: selector_tip
115
+ - Include: which stableId/selector failed, which fallback worked, and the page URL.
116
+
117
+ AFTER completing the test, you MUST call memory_save_insight at least once:
118
+ - Save any useful finding: reliable selectors, timing quirks, navigation patterns, workarounds.
119
+ - Category: selector_tip | timing | navigation | workaround | flaky | general
120
+ - Be specific \u2014 future runs will read your insights.`,resolve(){let r=gs();if(!r)throw new Error(`\u274C Memory MCP server not found
121
+
122
+ Install @zibby/memory:
123
+ npm install @zibby/memory`);let t=qe(process.cwd(),".zibby","memory");if(!fs(qe(t,".dolt")))throw new Error(`\u274C Memory database not initialized
124
+
125
+ Run:
126
+ zibby init --mem`);try{let e=ms("dolt",["sql","-q","SELECT COUNT(*) AS cnt FROM test_runs","-r","json"],{cwd:t,encoding:"utf-8",timeout:5e3}),s=JSON.parse(e.trim()).rows||[];if(!s[0]||s[0].cnt===0)return console.log("[memory] Database empty \u2014 memory tools activate after first completed run"),null}catch(e){throw new Error(`\u274C Dolt not found or memory database error
127
+
128
+ Install Dolt:
129
+ https://docs.dolthub.com/introduction/installation
130
+
131
+ Error: ${e.message}`,{cause:e})}return{command:"node",args:[r,"--db-path",t],description:this.description}},tools:[{name:"memory_get_test_history",description:"Query recent test runs with pass/fail results and timing",input_schema:{type:"object",properties:{specPath:{type:"string",description:"Filter by spec path (substring match)"},limit:{type:"number",description:"Max results (default 10)"}}}},{name:"memory_get_selectors",description:"Query known selectors for a page with stability metrics",input_schema:{type:"object",properties:{pageUrl:{type:"string",description:"Filter by page URL (substring match)"},limit:{type:"number",description:"Max results (default 20)"}}}},{name:"memory_get_page_model",description:"Query page structure \u2014 elements, roles, selectors",input_schema:{type:"object",properties:{url:{type:"string",description:"Filter by page URL (substring match)"},limit:{type:"number",description:"Max results (default 20)"}}}},{name:"memory_get_navigation",description:"Query known page-to-page transitions",input_schema:{type:"object",properties:{fromUrl:{type:"string",description:"Filter by source URL (substring match)"},limit:{type:"number",description:"Max results (default 20)"}}}},{name:"memory_save_insight",description:"Save a useful observation for future runs (selector tips, timing, workarounds)",input_schema:{type:"object",properties:{category:{type:"string",enum:["selector_tip","timing","navigation","workaround","flaky","general"],description:"Type of insight"},content:{type:"string",description:"The insight text \u2014 be specific and actionable"},specPath:{type:"string",description:"Related spec path"},sessionId:{type:"string",description:"Current session ID"}},required:["category","content"]}}]};import{existsSync as hs,readFileSync as _s}from"fs";import{homedir as ks}from"os";import{join as ws}from"path";import{spawn as bs}from"child_process";var B={jira:{description:"Jira issue search, details, comments, transitions",integrationProvider:"jira",envKeys:[],setupInstructions:`To connect Jira:
132
+ 1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
133
+ 2. Click "Connect Jira" and authorize via Atlassian OAuth
134
+ 3. After OAuth completes, ask me to install Jira again`},github:{description:"GitHub issues, PRs, repository management",integrationProvider:"github",envKeys:[],setupInstructions:`To connect GitHub:
135
+ 1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
136
+ 2. Click "Connect GitHub" and authorize
137
+ 3. After OAuth completes, ask me to install GitHub again`},slack:{description:"Slack messages, channels, reactions",integrationProvider:"slack",envKeys:[],setupInstructions:`To connect Slack:
138
+ 1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
139
+ 2. Click "Connect Slack" and authorize
140
+ 3. After OAuth completes, ask me to install Slack again`},sentry:{description:"Sentry error tracking \u2014 projects, issues, events",integrationProvider:"sentry",envKeys:[],setupInstructions:`To connect Sentry:
141
+ 1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
142
+ 2. Click "Connect Sentry" and authorize
143
+ 3. After OAuth completes, ask me to install Sentry again`},runner:{description:"Run zibby test workflows from chat (parallel supported)",envKeys:[],setupInstructions:"Ready to use. Runs zibby test workflows as background processes \u2014 each with its own browser and session."},browser:{description:"Playwright browser automation (navigate, click, fill, screenshot)",envKeys:[],setupInstructions:"Ready to use. Starts a Playwright browser for web automation."},memory:{description:"Test memory database (Dolt) \u2014 history, selectors, insights",envKeys:[],setupInstructions:"Ready to use. Requires Dolt (https://docs.dolthub.com/introduction/installation) and a memory DB via `zibby init --mem`."},"chat-memory":{description:"Persistent chat memory \u2014 remembers facts, decisions, and task history across sessions (Dolt-backed)",envKeys:[],setupInstructions:'Ready to use. Requires Dolt installed. Tables auto-create on first use. Install with: "add chat memory" or "install chat-memory".'},git:{description:"Clone and explore git repositories locally for codebase analysis",envKeys:[],setupInstructions:"Ready to use. Clone repos with git_checkout, explore with git_explore. Auto-authenticates with GitHub/GitLab tokens."}};function Ss(){let r=["## Available Skills"];for(let[t,e]of Object.entries(B)){let s=e.integrationProvider?`integration: ${e.integrationProvider}`:"ready";r.push(`- ${t}: ${e.description} [${s}]`)}return r.push(""),r.push("Use the install_skill / uninstall_skill / list_available_skills tools to manage skills."),r.push(`Zibby third party Integration settings page: ${ye()}`),r.push(""),r.push("## Tool-First Policy (mandatory)"),r.push("CRITICAL RULES \u2014 follow these strictly:"),r.push("1. When user asks to do something and a matching skill is available but not installed, IMMEDIATELY call install_skill. Never ask for credentials or confirmation first."),r.push(`2. If install_skill succeeds, the skill's tools are now available. Use them RIGHT AWAY in the same turn \u2014 don't just say "it's connected", actually call the tools.`),r.push("3. If install_skill reports needsIntegration, tell the user to connect via the integration URL and try again after."),r.push("4. When the relevant skill is already installed, use its tools directly \u2014 don't ask for IDs or keys. Each skill's own instructions explain the workflow."),r.push("5. If a task needs multiple skills (e.g. data from one + execution from another), install all of them, then follow each skill's workflow instructions."),r.join(`
144
+ `)}function vs(){if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let r=ws(ks(),".zibby","config.json");return hs(r)&&JSON.parse(_s(r,"utf-8")).sessionToken||null}catch{return null}}function Ns(){return(process.env.ZIBBY_API_URL||process.env.ZIBBY_PROD_API_URL||"https://api-prod.zibby.app").replace(/\/$/,"")}function ye(){return`${(process.env.ZIBBY_FRONTEND_URL||process.env.ZIBBY_PROD_FRONTEND_URL||"https://studio.zibby.app").replace(/\/$/,"")}/integrations`}function Os(r){try{let t=process.platform;return bs(t==="darwin"?"open":t==="win32"?"cmd":"xdg-open",t==="win32"?["/c","start","",r]:[r],{detached:!0,stdio:"ignore"}).unref(),!0}catch{return!1}}async function $s(){let r=vs();if(!r)return{checked:!1,statuses:null,reason:"no-session-token"};try{let t=await fetch(`${Ns()}/integrations/status`,{method:"GET",headers:{Authorization:`Bearer ${r}`}});return t.ok?{checked:!0,statuses:await t.json()||{},reason:null}:{checked:!1,statuses:null,reason:`status-${t.status}`}}catch{return{checked:!1,statuses:null,reason:"network-error"}}}function Pe(r,t){if(!t||!r)return{connected:null};let e=r[t];return!e||typeof e.connected!="boolean"?{connected:null,details:e||null}:{connected:e.connected,details:e}}var Be={id:"skill-installer",description:"Live skill installation for chat sessions",envKeys:[],catalog:B,promptFragment:Ss,tools:[{name:"install_skill",description:"Install a skill into the current chat session so its tools become available",input_schema:{type:"object",properties:{skillId:{type:"string",description:'Skill identifier to install (e.g. "jira", "github", "browser", "memory")'}},required:["skillId"]}},{name:"uninstall_skill",description:"Remove a skill from the current chat session",input_schema:{type:"object",properties:{skillId:{type:"string",description:"Skill identifier to remove"}},required:["skillId"]}},{name:"list_available_skills",description:"List all skills that can be installed, with their env-var readiness status",input_schema:{type:"object",properties:{}}}],async handleToolCall(r,t,e){let{activeSkills:s}=e,n=await $s();if(r==="list_available_skills"){let i=Object.entries(B).map(([o,a])=>{let c=s.includes(o),l=Pe(n.statuses,a.integrationProvider);return{id:o,description:a.description,installed:c,integrationProvider:a.integrationProvider||void 0,integrationConnected:l.connected,setupInstructions:l.connected===!1?a.setupInstructions:void 0}});return JSON.stringify({skills:i})}if(r==="install_skill"){let{skillId:i}=t;if(!i)return JSON.stringify({ok:!1,error:"skillId is required"});if(s.includes(i)){let u=B[i],{getSkill:p}=await import("@zibby/workflow"),m=(p(i)?.tools||[]).map(f=>f.name);return JSON.stringify({ok:!0,alreadyInstalled:!0,skillId:i,description:u?.description,availableTools:m,integrationUrl:u?.integrationProvider?ye():void 0,hint:`${i} is already active. Tools available: ${m.join(", ")}. Use them directly.`})}if(!B[i])return JSON.stringify({ok:!1,error:`Unknown skill "${i}". Available: ${Object.keys(B).join(", ")}`});let o=B[i];if(o.integrationProvider){let u=Pe(n.statuses,o.integrationProvider),p=ye();if(n.checked&&u.connected===!1){let y=Os(p);return JSON.stringify({ok:!1,error:`${o.integrationProvider} is not connected for this Zibby account yet`,needsIntegration:!0,integrationUrl:p,openedBrowser:y,setupInstructions:`Please connect ${o.integrationProvider} first at ${p}. After you finish OAuth, ask me to install ${i} again.`})}}s.push(i);let{getSkill:a}=await import("@zibby/workflow"),l=(a(i)?.tools||[]).map(u=>u.name);return JSON.stringify({ok:!0,installed:i,description:o.description,availableTools:l,hint:`${i} is now active. You now have these tools: ${l.join(", ")}. Use them immediately to help the user \u2014 don't just confirm installation.`})}if(r==="uninstall_skill"){let{skillId:i}=t;if(!i)return JSON.stringify({ok:!1,error:"skillId is required"});if(i==="skill-installer")return JSON.stringify({ok:!1,error:"Cannot uninstall the skill installer"});let o=s.indexOf(i);return o===-1?JSON.stringify({ok:!1,error:`${i} is not installed`}):(s.splice(o,1),JSON.stringify({ok:!0,uninstalled:i}))}return JSON.stringify({error:`Unknown tool: ${r}`})},resolve(){return null}};import{readFileSync as Rs,readdirSync as Is,statSync as We,writeFileSync as js,mkdirSync as As}from"fs";import{join as ze,resolve as Ts,relative as Cs}from"path";import{execSync as Ge}from"child_process";var Fe=256*1024,Es=64*1024,He={id:"core-tools",description:"File read/write, directory listing, shell commands, open URLs, wait for async operations",envKeys:[],tools:[{name:"read_file",description:"Read the contents of a file. Returns the text content.",input_schema:{type:"object",properties:{path:{type:"string",description:"File path (relative to cwd or absolute)"}},required:["path"]}},{name:"write_file",description:"Write content to a file. Creates parent directories if needed.",input_schema:{type:"object",properties:{path:{type:"string",description:"File path (relative to cwd or absolute)"},content:{type:"string",description:"Content to write"}},required:["path","content"]}},{name:"list_directory",description:"List files and directories in a path. Returns names with type indicators (/ for dirs).",input_schema:{type:"object",properties:{path:{type:"string",description:"Directory path (relative to cwd or absolute). Defaults to cwd."}}}},{name:"run_command",description:"Run a shell command and return its output. Use for grep, git, npm, etc.",input_schema:{type:"object",properties:{command:{type:"string",description:"Shell command to execute"},cwd:{type:"string",description:"Working directory (optional, defaults to project root)"}},required:["command"]}},{name:"open_url",description:"Open a URL in the user's default browser. Use for OAuth flows, documentation, integration setup pages.",input_schema:{type:"object",properties:{url:{type:"string",description:"URL to open"}},required:["url"]}},{name:"wait",description:"Wait for N seconds. Use this for async operations (tests, builds, deploys) \u2014 wait, then check status again.",input_schema:{type:"object",properties:{seconds:{type:"number",description:"Seconds to wait (default: 5, max: 300)"},reason:{type:"string",description:"Why waiting (for logging/clarity)"}}}}],async handleToolCall(r,t,e){let s=e?.options?.workspace||process.cwd();try{switch(r){case"read_file":return xs(t,s);case"write_file":return Ls(t,s);case"list_directory":return Js(t,s);case"run_command":return Ds(t,s);case"open_url":return Ms(t);case"wait":return await Us(t,e?.options?.signal);default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(n){return JSON.stringify({error:n.message})}},resolve(){return null}};function te(r,t){return Ts(t,r)}function xs(r,t){let e=te(r.path,t),s=We(e);return s.size>Fe?JSON.stringify({error:`File too large (${(s.size/1024).toFixed(0)}KB). Max: ${Fe/1024}KB`}):Rs(e,"utf-8")}function Ls(r,t){let e=te(r.path,t),s=ze(e,"..");return As(s,{recursive:!0}),js(e,r.content,"utf-8"),JSON.stringify({ok:!0,path:Cs(t,e)})}function Js(r,t){let e=te(r.path||".",t);return Is(e).map(n=>{try{return We(ze(e,n)).isDirectory()?`${n}/`:n}catch{return n}}).join(`
145
+ `)}function Ds(r,t){let e=r.cwd?te(r.cwd,t):t;return Ge(r.command,{cwd:e,encoding:"utf-8",timeout:3e4,maxBuffer:Es,stdio:["pipe","pipe","pipe"]})||"(no output)"}function Ms(r){let{url:t}=r;if(!t||!t.startsWith("http://")&&!t.startsWith("https://"))return JSON.stringify({error:"Invalid URL \u2014 must start with http:// or https://"});let e=process.platform,s=e==="darwin"?"open":e==="win32"?"start":"xdg-open";try{return Ge(`${s} "${t}"`,{stdio:"ignore",timeout:5e3}),JSON.stringify({ok:!0,opened:t})}catch{return JSON.stringify({ok:!1,error:`Could not open browser. Please visit: ${t}`})}}async function Us(r,t){let e=Math.min(Math.max(r.seconds||5,1),300),s=r.reason||"async operation",n=500,i=Date.now()+e*1e3;for(;Date.now()<i;){if(t?.aborted)return JSON.stringify({ok:!0,waited:Math.round((e*1e3-(i-Date.now()))/1e3),reason:s,interrupted:!0});await new Promise(o=>setTimeout(o,Math.min(n,i-Date.now())))}return JSON.stringify({ok:!0,waited:e,reason:s})}import{resolveIntegrationToken as Ze}from"@zibby/core/backend-client.js";async function Ye(r,t={}){let{token:e,organizationSlug:s}=await Ze("sentry"),n=`https://sentry.io/api/0/organizations/${s}${r}`,i=await fetch(n,{method:t.method||"GET",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json"}});if(!i.ok){let o=await i.text().catch(()=>"");throw new Error(`Sentry API ${i.status}: ${o.slice(0,300)}`)}return i.json()}var Ve={id:"sentry",description:"Sentry error tracking \u2014 projects, issues, events",envKeys:[],promptFragment:`## Sentry (connected)
146
+ You have access to the user's Sentry. Use these tools:
147
+ - sentry_list_projects: List projects in the organization
148
+ - sentry_list_issues: List errors/issues (supports query, project filter, sort)
149
+ - sentry_get_issue: Get detailed info about a specific issue`,resolve(){return null},async handleToolCall(r,t){try{switch(r){case"sentry_list_projects":{let e=await Ye("/projects/?per_page=50");return JSON.stringify({projects:e.map(s=>({slug:s.slug,name:s.name,platform:s.platform}))})}case"sentry_list_issues":{let e=t.project||"",s=t.query||"is:unresolved",n=t.sort||"date",i=`/issues/?query=${encodeURIComponent(s)}&sort=${n}&per_page=${t.limit||25}`;e&&(i+=`&project=${encodeURIComponent(e)}`);let o=await Ye(i);return JSON.stringify({issues:o.map(a=>({id:a.id,title:a.title,culprit:a.culprit,count:a.count,firstSeen:a.firstSeen,lastSeen:a.lastSeen,level:a.level,status:a.status}))})}case"sentry_get_issue":{let{issueId:e}=t;if(!e)return JSON.stringify({error:"issueId is required"});let{token:s}=await Ze("sentry"),n=await fetch(`https://sentry.io/api/0/issues/${e}/`,{headers:{Authorization:`Bearer ${s}`}});if(!n.ok)throw new Error(`Sentry API ${n.status}`);let i=await n.json();return JSON.stringify({id:i.id,title:i.title,culprit:i.culprit,metadata:i.metadata,count:i.count,userCount:i.userCount,firstSeen:i.firstSeen,lastSeen:i.lastSeen,level:i.level,status:i.status,project:{slug:i.project?.slug,name:i.project?.name}})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(e){return JSON.stringify({error:e.message})}},tools:[{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"]}}]};import{spawn as at}from"child_process";import{writeFileSync as qs,mkdirSync as Qe,existsSync as L,readdirSync as ie,readFileSync as ne,unlinkSync as Ks,createWriteStream as Ps,statSync as Bs}from"fs";import{resolve as F,join as x}from"path";import{resolveMaxParallelRuns as ct}from"@zibby/core/utils/parallel-config.js";import{zibbyScratchSpecsDir as Fs}from"@zibby/core/constants/zibby-scratch.js";var _e="sessions",ke=".zibby/output",se=process.env.ZIBBY_RUNNER_NODE_PROGRESS==="1",Ws=process.env.ZIBBY_RUNNER_STATUS_STREAM==="1",lt=process.env.ZIBBY_RUNNER_SPAWN_LOGS==="1",A=new Map,K=[],zs=0,ge=0,Xe=3e3;function ut(){return`run_${++zs}_${Date.now().toString(36)}`}function et(r){let t=Math.floor(r/1e3);return t<60?`${t}s`:`${Math.floor(t/60)}m ${t%60}s`}function pt(r){return r.replace(/\x1b\[[0-9;]*[a-zA-Z]/g,"")}function j(r,t,e){if(!Ws)return;let s=`
150
+ ${t} [${r}] ${e}
151
+ `;try{process.stderr.write(s)}catch{}}function we(){K.length=0;for(let[,r]of A)if(r.status==="queued"&&(r.status="cancelled"),r.status==="running"&&r._child)try{r._child.kill("SIGTERM")}catch{}}process.on("exit",we);process.on("SIGINT",()=>{we(),process.exit(0)});process.on("SIGTERM",()=>{we(),process.exit(0)});var dt={id:"runner",description:"Run zibby test workflows from chat (parallel supported)",envKeys:[],promptFragment:`## Test Runner
152
+ You can run zibby test workflows directly from chat:
153
+
154
+ **CRITICAL: When user asks to test a ticket:**
155
+ 1. Load the issue with your tracker tools (whatever is connected)
156
+ 2. Check comments/description for test steps
157
+ 3. IF steps found \u2192 Use inline format: run_test({ spec: "inline:<steps>", ticketKey: "KEY" })
158
+ 4. IF NO steps AND local codebase \u2192 Use run_generate
159
+ 5. Tell the user tests are running \u2014 they can ask for progress anytime
160
+
161
+ **Jira-shaped \`spec\` (e.g. SCRUM-408):** If the **jira** skill is active, the runner loads that issue (description + comments) and runs it as an inline spec. Otherwise use \`inline:\`+steps or a file path. Prefer explicit \`inline:\` when you want full control.
162
+
163
+ Tools:
164
+ - **run_test**: spec = file path, inline:+steps, or Jira KEY-123 (auto-loads issue when jira skill is on).
165
+ - **run_status**: Instant progress check (runId or "all")
166
+ - **run_generate**: Generate specs from codebase. ONLY use if NO steps in ticket AND local codebase exists.
167
+ - **run_artifacts**: Read test results/logs after completion
168
+ - **run_diagnose**: Diagnose failures
169
+ - **run_cancel**: Kill running test
170
+ - **list_specs**: List spec files
171
+
172
+ ### MANDATORY: When User Asks to Test a Ticket
173
+
174
+ 1. **ALWAYS load ticket first** using Jira/issue tools (jira_get_issue, jira_get_comments)
175
+ 2. **Check comments AND description** for test steps
176
+ 3. **If steps found** \u2192 IMMEDIATELY call run_test with inline format: run_test({ spec: "inline:Navigate to...\\nClick...\\nVerify...", ticketKey: "SCRUM-123" })
177
+ 4. **DO NOT say "can't run"** until you've ACTUALLY fetched the ticket and confirmed NO steps exist
178
+ 5. **If NO steps found** \u2192 Ask user to add steps OR use run_generate (only if local codebase)
179
+
180
+ ### DECISION TREE: When User Says "Test This Ticket"
181
+
182
+ **STEP 1: Get full issue/ticket information**
183
+ ALWAYS use your connected issue tools to load the item and discussion (full description/body + comments). Tool names differ by integration (e.g. Jira vs GitHub vs Linear)\u2014use what you have.
184
+
185
+ **STEP 2: Check if testing steps exist**
186
+ Look for testing steps in:
187
+ - Comments (most common)
188
+ - Description field
189
+ - Keywords like "test steps", "testing steps", numbered lists (1. 2. 3.)
190
+
191
+ IF testing steps found:
192
+ \u2192 GOTO Workflow A (Use Existing Steps)
193
+ ELSE:
194
+ \u2192 GOTO Workflow B (Generate from Codebase)
195
+
196
+ ### Workflow A: Use Existing Steps (NO CODEBASE NEEDED)
197
+ **MANDATORY when ticket has test steps in comments/description**
198
+
199
+ CRITICAL: If you see test steps in the ticket, DO NOT call run_generate. Use inline format instead.
200
+
201
+ Steps:
202
+ 1. Extract steps from ticket (comments or description)
203
+ 2. Format as inline spec: "inline:" + steps text
204
+ 3. Call run_test({ spec: "inline:...", ticketKey: "SCRUM-123" })
205
+ 4. Tell the user tests are running
206
+ 5. Use run_status when they ask for progress
207
+
208
+ Example: If ticket SCRUM-408 comment has test steps, call: run_test({ spec: "inline:Navigate to URL\\nVerify checkboxes\\nCheck first\\nUncheck second", ticketKey: "SCRUM-408" })
209
+
210
+ ### Workflow B: Generate from Codebase (REQUIRES CODEBASE)
211
+ **ONLY use when ALL these are true:**
212
+ - \u274C NO testing steps in ticket comments/description
213
+ - \u2705 Local codebase exists (not external URL like heroku)
214
+ - \u2705 Ticket describes NEW functionality to test
215
+
216
+ **STOP AND USE WORKFLOW A IF:**
217
+ - Testing steps exist in ticket \u2192 NEVER call run_generate, use inline format
218
+ - Ticket mentions external app (heroku, cloud, demo sites) \u2192 Use inline with URL
219
+ - No local codebase \u2192 Ask user for steps
220
+
221
+ **BEFORE calling run_generate, ask yourself:**
222
+ - Did I check ALL comments? (not just description)
223
+ - Are there ANY step lists (numbered, bulleted)?
224
+ - Does ticket mention external URLs?
225
+ - If YES to any: DO NOT call run_generate!
226
+
227
+ **If checks pass:**
228
+ 1. Call run_generate({ ticket: "SCRUM-123" })
229
+ - Spawns Claude/Cursor with file access
230
+ - Explores codebase (1-3 minutes)
231
+ - Returns generated spec file paths
232
+ 2. For EACH file: run_test({ spec: "test-specs/...", ticketKey: "SCRUM-123" })
233
+ 3. Tell the user tests are running
234
+
235
+ ### Example Decision Process
236
+ (Illustration uses Jira-shaped keys/tools; substitute your session's issue integration.)
237
+
238
+ **User:** "Test SCRUM-408"
239
+
240
+ Step-by-step:
241
+ 1. jira_get_issue({ issueKey: "SCRUM-408" }) - get summary, description
242
+ 2. jira_get_comments({ issueKey: "SCRUM-408" }) - get comments
243
+ 3. Check: Comment has "Testing steps: 1. Go to /checkboxes 2. Verify..."
244
+ 4. Decision: Steps exist \u2192 Use Workflow A
245
+ 5. run_test({
246
+ spec: "inline:Navigate to https://the-internet.herokuapp.com/checkboxes, verify two checkboxes, check first, uncheck second, verify states",
247
+ ticketKey: "SCRUM-408"
248
+ })
249
+
250
+ **User:** "Test SCRUM-999 (new feature)"
251
+
252
+ Step-by-step:
253
+ 1. jira_get_issue({ issueKey: "SCRUM-999" })
254
+ 2. jira_get_comments({ issueKey: "SCRUM-999" })
255
+ 3. Check: No testing steps found in ticket
256
+ 4. Check: Codebase exists (package.json, src/ in current dir)
257
+ 5. Decision: No steps + codebase \u2192 Use Workflow B
258
+ 6. run_generate({ ticket: "SCRUM-999" })
259
+ 7. Wait for spec files...
260
+ 8. run_test for each file
261
+ 9. Tell the user tests are running
262
+
263
+ ### After Starting Runs
264
+ - run_test starts async work \u2014 tell the user tests are running.
265
+ - Use run_status to check progress when asked.
266
+ - To poll for completion, use the general wait tool (e.g. wait 20s) then run_status. Repeat until done.
267
+ - If any run failed/error/cancelled and user asks "why", call run_diagnose.
268
+
269
+ \u26A0\uFE0F NEVER AUTO-CANCEL RUNS:
270
+ - Tests take 1-5 minutes to complete. A "running" status is NORMAL \u2014 it does NOT mean stuck.
271
+ - NEVER call run_cancel unless the USER explicitly asks you to cancel/stop.
272
+ - If a run is still "running" after polling, just TELL the user it's still in progress and WAIT.
273
+ - The workflow has multiple nodes (preflight \u2192 execute_live \u2192 generate_script). Each takes time. This is expected.
274
+ - DO NOT interpret "running" as "stuck". DO NOT cancel on your own.
275
+
276
+ ### Parallelism
277
+ - Each test CASE should be its own run_test call
278
+ - Runs auto-queue past parallel.maxConcurrentRuns in .zibby.config.mjs (default 8; caps Studio Mission Control lanes too)
279
+ - If agent is Cursor and you see "Security process exited with code: 45", avoid parallel launch for that batch; run tests sequentially (one run_test + wait, then next).
280
+
281
+ ### Artifacts
282
+ Each run generates:
283
+ - result.json: Pass/fail verdict
284
+ - recording.webm: Video of session
285
+ - events.json: All browser events
286
+ - raw_stream_output.txt: Agent log
287
+
288
+ Use run_artifacts({ runId, type }) and run_diagnose({ runId }) to inspect and explain failures.`,resolve(){return null},async handleToolCall(r,t,e){let s=e?.options?.workspace||process.cwd();try{switch(r){case"run_generate":return await Gs(t,s);case"run_test":return await Xs(t,s,e);case"run_status":return er(t);case"run_cancel":return tr(t);case"run_artifacts":return nr(t,s);case"run_diagnose":return ir(t,s);case"list_specs":return or(t,s);default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(n){return JSON.stringify({error:n.message})}},tools:[{name:"run_generate",description:"Generate specs from codebase. CRITICAL: DO NOT USE if ticket has test steps in comments. Only use when: (1) NO steps in ticket AND (2) local codebase exists (not external URLs). For tickets with steps, use run_test with inline format.",input_schema:{type:"object",properties:{ticket:{type:"string",description:"Jira ticket key (e.g. SCRUM-123). Auto-fetches ticket details."},description:{type:"string",description:"Ticket description text (use if no Jira key available)"},input:{type:"string",description:"Path to a file containing ticket/requirements text"},repo:{type:"string",description:"Path to the codebase (default: current directory)"},agent:{type:"string",description:"Optional agent override (cursor, gemini, claude, codex, assistant). Omit to use configured agent."},output:{type:"string",description:"Output directory for spec files (default: test-specs)"}}}},{name:"run_test",description:"Start a test (async, returns runId). spec = file path, or inline:+steps, or a Jira-shaped issue key (e.g. PROJ-123): when Jira is connected, the runner loads that issue's description+comments into an inline spec. After starting, tell the user and let them ask for progress via run_status.",input_schema:{type:"object",properties:{spec:{type:"string",description:"Workspace file path; or inline:+steps; or Jira issue key (KEY-123) to auto-fetch from Jira when the jira skill is active."},ticketKey:{type:"string",description:"Optional label (e.g. SCRUM-123). If spec is an issue key, this defaults to that key."},agent:{type:"string",description:"Optional agent override (cursor, gemini, claude, codex, assistant). Omit to use configured agent."},headless:{type:"boolean",description:"Run browser headless (default false)"},workflow:{type:"string",description:"Workflow override (e.g. quick-smoke)"}},required:["spec"]}},{name:"run_status",description:'Instant progress check \u2014 returns immediately. Use this whenever user asks about test progress. ALWAYS use runId="all".',input_schema:{type:"object",properties:{runId:{type:"string",description:'Use "all" to see all runs in this session (recommended). Or a specific run ID if known.'}},required:["runId"]}},{name:"run_cancel",description:'Cancel/kill a running test. ONLY use when the USER explicitly asks to cancel or stop a run. NEVER auto-cancel \u2014 tests take 1-5 minutes and "running" is normal. Use runId="all" to cancel all active runs.',input_schema:{type:"object",properties:{runId:{type:"string",description:'Run ID to cancel, or "all" to cancel all active runs'}},required:["runId"]}},{name:"run_artifacts",description:"Read artifacts from a test run session. Can list files, read results/events/logs, or search across all sessions.",input_schema:{type:"object",properties:{runId:{type:"string",description:"Run ID from run_test. Omit to search across all sessions."},type:{type:"string",enum:["list","result","events","log","search"],description:'What to retrieve: "list" = all files in session, "result" = result.json, "events" = events.json, "log" = raw output tail, "search" = search text across sessions'},node:{type:"string",description:'Node name to read from (e.g. "execute_live", "generate_script"). Default: "execute_live"'},query:{type:"string",description:'Search text (only for type="search"). Searches across all session logs/events.'},tail:{type:"number",description:"Number of characters from end of log to return (default: 3000)"}},required:["type"]}},{name:"run_diagnose",description:"Diagnose one or all runs, especially failed ones. Uses run logs + known error patterns and returns likely root cause with suggested next action.",input_schema:{type:"object",properties:{runId:{type:"string",description:'Run ID from run_test, or "all" (default) to diagnose all known runs'},tail:{type:"number",description:"Characters of run log tail to inspect (default: 2000)"}}}},{name:"list_specs",description:"List available test spec files in the project",input_schema:{type:"object",properties:{directory:{type:"string",description:'Directory to scan (default: "test-specs")'}}}}]};function he(){let r=0;for(let[,t]of A)t.status==="running"&&r++;return r}function tt(){for(;K.length>0;){let r=ct(K[0]?.context?.options?.config);if(he()>=r)break;let{args:t,cwd:e,context:s}=K.shift();mt(t,e,s)}}async function Gs(r,t){let{ticket:e,description:s,input:n,repo:i,agent:o,output:a}=r,c=["generate"];e&&c.push("--ticket",e),s&&c.push("--description",s),n&&c.push("--input",n),i&&c.push("--repo",i),a&&c.push("--output",a);let l=["assistant","cursor","claude","codex","gemini"],u=o||process.env.AGENT_TYPE,p=u&&l.includes(u)?u:null;p&&c.push("--agent",p);let y=e||"generate";return j(y,"\u{1F9EA}","Starting test spec generation (real agent with codebase access)..."),new Promise(m=>{lt&&console.error(`[zibby:spawn] skill=run_generate parentPid=${process.pid} \u2192 child zibby ${c.map(g=>/\s/.test(g)?JSON.stringify(g):g).join(" ")} cwd=${t}`);let f=at("zibby",c,{cwd:t,env:{...process.env},stdio:["ignore","pipe","pipe"],detached:!1}),k="",h="";f.stdout.on("data",g=>{let _=g.toString();k+=_;for(let d of _.split(`
289
+ `)){let b=pt(d).trim();b.startsWith("\u2705")?j(y,"\u2705",b.slice(2).trim()):b.startsWith("\u2713")&&j(y,"\u2714",b.slice(2).trim())}}),f.stderr.on("data",g=>{h+=g.toString()}),f.on("close",g=>{if(g!==0){j(y,"\u274C",`Generation failed (exit ${g})`),m(JSON.stringify({error:`zibby generate failed with exit code ${g}`,stderr:h.slice(-1e3)}));return}let _=F(t,a||"test-specs"),d=[];try{let b=e?e.toLowerCase().replace(/[^a-z0-9]+/g,"-"):"";d=ie(_).filter(I=>I.endsWith(".txt")&&(!b||I.startsWith(b))).map(I=>x(_,I))}catch{}j(y,"\u2705",`Generated ${d.length} test spec files`),m(JSON.stringify({success:!0,ticketKey:e||null,specFiles:d.map(b=>b.replace(`${t}/`,"")),total:d.length,message:`Generated ${d.length} specs. Now call run_test for each file.`}))}),f.on("error",g=>{j(y,"\u274C",`Spawn error: ${g.message}`),m(JSON.stringify({error:g.message}))})})}var st=1e5,rt=/^[A-Z][A-Z0-9]+-\d+$/,Hs=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function Ys(r,t){if(!t||!t.length)return r;let e=r;for(let s of t)s.type==="strong"?e=`**${e}**`:s.type==="em"?e=`_${e}_`:s.type==="code"?e=`\`${e}\``:s.type==="strike"?e=`~~${e}~~`:s.type==="link"&&s.attrs?.href&&(e=`[${e}](${s.attrs.href})`);return e}function re(r,t=0){if(!Array.isArray(r))return"";let e=[];for(let s of r){if(s.type==="text"){e.push(Ys(s.text||"",s.marks));continue}if(s.type==="hardBreak"){e.push(`
290
+ `);continue}if(s.type==="rule"){e.push(`
291
+ ---
292
+ `);continue}let n=s.content?re(s.content,t+1):"";if(s.type==="listItem")e.push(n);else if(s.type==="bulletList"){let i=(s.content||[]).map(o=>`- ${re(o.content||[],t+1).trim()}`);e.push(`
293
+ ${i.join(`
294
+ `)}
295
+ `)}else if(s.type==="orderedList"){let i=(s.content||[]).map((o,a)=>`${a+1}. ${re(o.content||[],t+1).trim()}`);e.push(`
296
+ ${i.join(`
297
+ `)}
298
+ `)}else if(s.type==="heading"){let i=s.attrs?.level||2;e.push(`
299
+
300
+ ${"#".repeat(i)} ${n.trim()}
301
+
302
+ `)}else Hs.has(s.type)?e.push(`
303
+
304
+ ${n}
305
+ `):e.push(n)}return e.join("").replace(/\n{3,}/g,`
306
+
307
+ `)}function Zs(r){return r==null||r===""?"":typeof r=="string"?r.trim():typeof r=="object"&&Array.isArray(r.content)?re(r.content).trim():""}async function Vs(r){let{getSkill:t}=await import("@zibby/workflow"),e=t("jira");if(!e||typeof e.handleToolCall!="function")return null;try{let s=await e.handleToolCall("jira_get_issue",{issueKey:r}),n=JSON.parse(s);if(n?.error)return null;let i=await e.handleToolCall("jira_get_comments",{issueKey:r,maxResults:50}),o=JSON.parse(i);if(o?.error)return null;let a=Zs(n.description),c=[];a&&c.push(a);let l=Array.isArray(o.comments)?o.comments:[];if(l.length>0){let p=l.map(y=>String(y.body||"").trim()).filter(Boolean).join(`
308
+
309
+ `);p&&c.push(p)}let u=c.join(`
310
+
311
+ `).trim();return u?(u.length>st&&(u=`${u.slice(0,st)}
312
+
313
+ ...[truncated]`),{inlineSpec:`inline:${u}`,issueKey:r}):null}catch{return null}}function Qs(r,t){try{let e=JSON.parse(r);return JSON.stringify({...e,...t})}catch{return r}}async function Xs(r,t,e){let s={...r},n=String(s.spec??"").trim();if(!n)return JSON.stringify({error:"spec is required"});let i=null;if(rt.test(n)&&!n.startsWith("inline:")){let u=await Vs(n);u&&(n=u.inlineSpec,s.spec=n,String(s.ticketKey||"").trim()||(s.ticketKey=u.issueKey),i=u.issueKey)}let o=String(s.ticketKey||"").trim();if(o){for(let[u,p]of A.entries())if(p?.ticketKey===o&&!(p?.status!=="running"&&p?.status!=="queued"))return JSON.stringify({runId:u,ticketKey:o,status:p.status,reused:!0,message:`A run for ${o} is already ${p.status}. Reusing existing run instead of starting a duplicate.`})}if(!n.startsWith("inline:")){let u=F(t,n);if(!L(u))return rt.test(n)?JSON.stringify({error:`Invalid run_test spec: "${n}" is an issue id, not a spec.`,reason:"Jira auto-load was attempted but did not return usable text, or Jira is not configured.",doNext:["Confirm the jira skill is active and authenticated.",'Or call tracker tools yourself, then run_test with spec: "inline:" + steps.'],validExample:{spec:"inline:1. Open https://example.com \u2026 2. Verify \u2026",ticketKey:n},invalidExample:{spec:n,ticketKey:n}}):JSON.stringify({error:`Test spec not found: ${n}`,hint:'If this should be issue steps, load the issue with your tracker tools first, then run_test with spec: "inline:" + steps. Otherwise use a real file path.'})}let a=ct(e?.options?.config);if(he()>=a){let u=ut(),p=s.ticketKey||u,y={runId:u,spec:s.ticketKey?`${s.ticketKey}: ${s.spec}`:s.spec,ticketKey:s.ticketKey||null,status:"queued",startTime:Date.now(),exitCode:null,output:"",error:""};A.set(u,y),K.push({args:{...s,_queuedRunId:u},cwd:t,context:e}),j(p,"\u23F3",`Queued (${he()}/${a} running, ${K.length} queued)`);let m={runId:u,spec:y.spec,ticketKey:y.ticketKey,status:"queued",message:`Queued \u2014 will start when a slot opens (max ${a} concurrent).`};return i&&(m.resolvedFromJiraIssue=i,m.message+=` (spec built from Jira ${i})`),JSON.stringify(m)}let c=Date.now()-ge;c<Xe&&ge>0&&await new Promise(u=>setTimeout(u,Xe-c)),ge=Date.now();let l=mt(s,t,e);return i?Qs(l,{resolvedFromJiraIssue:i,message:`Spec was loaded from Jira issue ${i} (description + comments).`}):l}function mt(r,t,e){let{spec:s,ticketKey:n,agent:i,headless:o,workflow:a,_queuedRunId:c}=r,l=c||ut(),u=s,p=!1;if(s.startsWith("inline:")){p=!0;let O=Fs(t);Qe(O,{recursive:!0}),u=x(O,`${l}.txt`),qs(u,s.slice(7).trim(),"utf-8")}let y=F(t,".zibby","output","runs");Qe(y,{recursive:!0});let m=x(y,`${l}.log`),f=Ps(m,{flags:"a"}),h=i&&["assistant","cursor","claude","codex","gemini"].includes(i)?i:null,g=["test",u];h&&g.push("--agent",h),o&&g.push("--headless"),a&&g.push("--workflow",a),lt&&console.error(`[zibby:spawn] skill=run_test parentPid=${process.pid} \u2192 child zibby ${g.map(O=>/\s/.test(O)?JSON.stringify(O):O).join(" ")} cwd=${t}`);let _=at("zibby",g,{cwd:t,env:{...process.env,ZIBBY_WORKFLOW_GRAPH_LOG_MARKERS:"1"},stdio:["ignore","pipe","pipe"],detached:!1}),d={runId:l,spec:n?`${n}: ${s}`:s,ticketKey:n||null,specPath:u,logPath:m,isInline:p,pid:_.pid,status:"running",output:"",error:"",startTime:Date.now(),exitCode:null,currentNode:null,completedNodes:[]},b=n||l,I="";function Ae(O){let v=pt(O).trim();if(!v)return;if(v.startsWith("__WORKFLOW_GRAPH_LOG__")){try{let $=JSON.parse(v.slice(22));$.phase==="node_begin"?d.currentNode=$.node:$.phase==="node_end"&&($.node&&!d.completedNodes.includes($.node)&&d.completedNodes.push($.node),d.currentNode===$.node&&(d.currentNode=null))}catch{}return}let Y=v.match(/Session\s+(\S+)/);if(Y&&!d.sessionId&&(d.sessionId=Y[1],d.sessionPath=F(t,ke,_e,d.sessionId)),v.startsWith("\u250C ")||v.startsWith("\u250C ")){let $=v.slice(2).trim();d.currentNode=$,se&&j(b,"\u25B6",`${$}`)}else if(v.startsWith("\u2514 ")||v.startsWith("\u2514 ")){let $=v.slice(2).trim();$.startsWith("done")?(d.currentNode&&!d.completedNodes.includes(d.currentNode)&&d.completedNodes.push(d.currentNode),se&&j(b,"\u2714",`${d.currentNode||"node"} done ${$.replace("done","").trim()}`),d.currentNode=null):$.startsWith("failed")&&(se&&j(b,"\u2718",`${d.currentNode||"node"} failed ${$.replace("failed","").trim()}`),d.currentNode=null)}else v.includes("Workflow completed")&&(d.currentNode=null,se&&j(b,"\u2714",`Workflow completed (${et(Date.now()-d.startTime)})`))}function Ht(O){let v=O.toString();d.output+=v,f.write(v),d.output.length>5e4&&(d.output=d.output.slice(-3e4)),I+=v;let Y=I.split(`
314
+ `);I=Y.pop();for(let $ of Y)Ae($)}return _.stdout.on("data",Ht),_.stderr.on("data",O=>{let v=O.toString();d.error+=v,f.write(v),d.error.length>2e4&&(d.error=d.error.slice(-1e4))}),_.on("close",O=>{d.status=O===0?"passed":"failed",d.exitCode=O,d.endTime=Date.now(),I&&Ae(I),f.end();let v=et(Date.now()-d.startTime);if(O===0?j(b,"\u2705",`Passed (${v})`):j(b,"\u274C",`Failed (${v})`),d.isInline)try{Ks(d.specPath)}catch{}tt()}),_.on("error",O=>{d.status="error",d.error+=`
315
+ Spawn error: ${O.message}`,j(b,"\u274C",`Spawn error: ${O.message}`),f.end(),tt()}),d._child=_,A.set(l,d),JSON.stringify({runId:l,spec:d.spec,ticketKey:d.ticketKey,status:"running",pid:_.pid,logFile:m})}function nt(r){let t=Math.round(((r.endTime||Date.now())-r.startTime)/1e3),e=r.completedNodes||[],s=r.currentNode||null;if(r.status!=="running")return{elapsed:t,stage:r.status,completedNodes:e,currentNode:null};let n;return s?(n=`Actively executing node "${s}"`,e.length&&(n+=` (completed: ${e.join(", ")})`)):e.length?n=`Between nodes (completed: ${e.join(", ")})`:n="Starting up (initializing workflow)",n+=`. Elapsed: ${t}s. This is normal progress \u2014 do not cancel.`,{elapsed:t,stage:"running",currentNode:s,completedNodes:e,progress:n}}function er(r){let{runId:t}=r;if(!t)return JSON.stringify({error:"runId is required"});if(t==="all"){let i=[...A.entries()].map(([u,p])=>{let y=nt(p),m={runId:u,spec:p.spec,ticketKey:p.ticketKey,status:p.status,elapsed:y.elapsed,exitCode:p.exitCode,sessionId:p.sessionId||null};return p.status==="running"?(m.currentNode=y.currentNode,m.completedNodes=y.completedNodes,m.progress=y.progress):m.outputTail=p.output.slice(-500),m}),o=i.filter(u=>u.status==="running").length,a=i.filter(u=>u.status==="passed").length,c=i.filter(u=>u.status==="failed").length,l={total:i.length,running:o,passed:a,failed:c,runs:i};return o>0&&(l._hint="All running tests are progressing normally through their workflow nodes. Do NOT cancel, diagnose, or interpret as stuck. Just tell the user they are still running."),JSON.stringify(l)}let e=A.get(t);if(!e)return JSON.stringify({error:`Run not found: ${t}`});let s=nt(e),n={runId:t,spec:e.spec,ticketKey:e.ticketKey,status:e.status,elapsed:s.elapsed,exitCode:e.exitCode,sessionId:e.sessionId||null};return e.status==="running"?(n.currentNode=s.currentNode,n.completedNodes=s.completedNodes,n.progress=s.progress):(n.outputTail=e.output.slice(-1e3),n.errorTail=e.error.slice(-500)),e.status==="running"&&(n._hint="This run is actively progressing. Do NOT cancel, diagnose, or assume stuck. Just tell the user it is still running."),JSON.stringify(n)}function it(r,t){if(t.status==="queued"){let e=K.findIndex(s=>s.args._queuedRunId===r);return e>=0&&K.splice(e,1),t.status="cancelled",t.endTime=Date.now(),{ok:!0,runId:r,status:"cancelled"}}if(t.status!=="running")return{ok:!1,runId:r,error:`Run is not active (status: ${t.status})`};try{return t._child.kill("SIGTERM"),t.status="cancelled",t.endTime=Date.now(),{ok:!0,runId:r,status:"cancelled"}}catch(e){return{ok:!1,runId:r,error:`Failed to cancel: ${e.message}`}}}function tr(r){let{runId:t}=r;if(!t)return JSON.stringify({error:"runId is required"});if(t==="all"){let s=[];for(let[n,i]of A.entries())(i.status==="running"||i.status==="queued")&&s.push(it(n,i));return s.length===0?JSON.stringify({ok:!0,message:"No active runs to cancel"}):JSON.stringify({ok:!0,cancelled:s.length,results:s})}let e=A.get(t);return JSON.stringify(e?it(t,e):{error:`Run not found: ${t}`})}function sr(r,t){let e=A.get(r);if(e?.sessionPath&&L(e.sessionPath))return e.sessionPath;if(e?.sessionId){let s=F(t,ke,_e,e.sessionId);if(L(s))return s}return null}function ft(r,t=""){let e=[];if(!L(r))return e;for(let s of ie(r,{withFileTypes:!0})){let n=t?`${t}/${s.name}`:s.name;if(s.isDirectory())e.push(...ft(x(r,s.name),n));else{let i=Bs(x(r,s.name));e.push({path:n,size:i.size})}}return e}function ot(r){if(!L(r))return null;try{return JSON.parse(ne(r,"utf-8"))}catch{return null}}function yt(r,t=2e3){if(!r||!L(r))return"";try{return ne(r,"utf-8").slice(-Math.max(200,Number(t)||2e3))}catch{return""}}function rr({run:r,logTail:t,errorTail:e}){let n=`${t||""}
316
+ ${e||""}`.toLowerCase(),i={runId:r?.runId||null,status:r?.status||null,exitCode:r?.exitCode??null,likelyCause:"Unknown failure",confidence:"low",nextStep:'Call run_artifacts({ runId, type: "log" }) with larger tail and inspect full logs.'};return r?.status==="running"||r?.status==="queued"?{...i,likelyCause:"Run is still active; no terminal failure to diagnose yet.",confidence:"high",nextStep:'Call run_status({ runId: "all" }) to check progress.'}:n.includes("test spec not found")?{...i,likelyCause:"Invalid spec input: run_test received a non-existent spec path.",confidence:"high",nextStep:"Use spec as inline:... or a real file path from list_specs. For ticket keys, fetch steps first via Jira then build inline spec."}:n.includes("unknown command")&&n.includes("'run'")?{...i,likelyCause:"CLI command mismatch (`zibby run` unsupported in current CLI).",confidence:"high",nextStep:"Use `zibby test ...` spawn path (runner should already do this)."}:n.includes("missing openai_api_key")||n.includes("didn't provide an api key")||n.includes("401")?{...i,likelyCause:"Provider authentication/config issue (API key/proxy auth missing or rejected).",confidence:"medium",nextStep:"Verify proxy/token env and auth mode, then retry once configuration is valid."}:n.includes("spawn error")||n.includes("enoent")?{...i,likelyCause:"Failed to spawn CLI process (binary/path/environment issue).",confidence:"medium",nextStep:"Confirm `zibby` is installed and available in PATH for the chat process."}:n.includes("security command failed")||n.includes("security process exited with code: 45")||n.includes("password not found for account")?{...i,likelyCause:"Cursor agent keychain/auth failed during preflight (often transient, more common under parallel starts).",confidence:"high",nextStep:'Retry failed ticket sequentially (not parallel), or run with a different agent via run_test({ ..., agent: "codex" }).'}:i}function nr(r,t){let{runId:e,type:s,node:n="execute_live",query:i,tail:o=3e3}=r;if(s==="search"){if(!i)return JSON.stringify({error:'query is required for type="search"'});let c=F(t,ke,_e);if(!L(c))return JSON.stringify({matches:[],message:"No sessions found"});let l=[],u=i.toLowerCase();for(let p of ie(c,{withFileTypes:!0})){if(!p.isDirectory())continue;let y=x(c,p.name),m=[{file:"execute_live/result.json",label:"result"},{file:"execute_live/events.json",label:"events"},{file:"execute_live/raw_stream_output.txt",label:"log"},{file:"generate_script/raw_stream_output.txt",label:"script_log"},{file:"title.txt",label:"title"}];for(let{file:f,label:k}of m){let h=x(y,f);if(L(h))try{let g=ne(h,"utf-8");if(g.toLowerCase().includes(u)){let _=g.toLowerCase().indexOf(u),d=Math.max(0,_-100),b=Math.min(g.length,_+i.length+100);l.push({sessionId:p.name,artifact:k,snippet:g.slice(d,b)})}}catch{}}if(l.length>=20)break}return JSON.stringify({query:i,matches:l,total:l.length})}if(!e)return JSON.stringify({error:"runId is required for this type"});if(s==="log"){let c=A.get(e),l=yt(c?.logPath,o);if(l)return JSON.stringify({runId:e,source:"run-log",totalLength:l.length,tail:l})}let a=sr(e,t);if(!a)return JSON.stringify({error:`No session found for run ${e}. The run may still be starting.`});switch(s){case"list":{let c=ft(a);return JSON.stringify({sessionId:a.split("/").pop(),files:c,total:c.length})}case"result":{let c=ot(x(a,n,"result.json"));return JSON.stringify(c?{sessionId:a.split("/").pop(),node:n,result:c}:{error:`No result.json found in ${n}`})}case"events":{let c=ot(x(a,n,"events.json"));if(!c)return JSON.stringify({error:`No events.json found in ${n}`});let l=Array.isArray(c)?c:c.events||[];return JSON.stringify({sessionId:a.split("/").pop(),node:n,totalEvents:l.length,events:l.slice(-50)})}case"log":{let c=x(a,n,"raw_stream_output.txt");if(!L(c))return JSON.stringify({error:`No log found in ${n}`});let l=ne(c,"utf-8");return JSON.stringify({sessionId:a.split("/").pop(),node:n,totalLength:l.length,tail:l.slice(-o)})}default:return JSON.stringify({error:`Unknown artifact type: ${s}. Use: list, result, events, log, search`})}}function ir(r,t){let e=String(r?.runId||"all"),s=Number(r?.tail||2e3),n=e==="all"?[...A.keys()]:[e];if(n.length===0)return JSON.stringify({error:"No runs available to diagnose. Call run_test first."});let i=n.map(c=>{let l=A.get(c);if(!l)return{runId:c,error:`Run not found: ${c}`};let u=yt(l.logPath,s),p=String(l.error||"").slice(-Math.max(200,s));return{...rr({run:l,logTail:u,errorTail:p}),ticketKey:l.ticketKey||null,spec:l.spec,logTail:u,errorTail:p}}),o=i.filter(c=>c.status==="failed"||c.status==="error"),a=i.filter(c=>c.status==="running"||c.status==="queued");return JSON.stringify({total:i.length,failed:o.length,active:a.length,diagnoses:i})}function or(r,t){let e=r?.directory||"test-specs",s=F(t,e);if(!L(s))return JSON.stringify({specs:[],directory:e,message:`Directory not found: ${e}`});try{let i=function(o,a){for(let c of ie(o,{withFileTypes:!0})){let l=a?`${a}/${c.name}`:c.name;c.isDirectory()?i(x(o,c.name),l):(c.name.endsWith(".txt")||c.name.endsWith(".md"))&&n.push(l)}},n=[];return i(s,""),JSON.stringify({specs:n.map(o=>`${e}/${o}`),total:n.length,directory:e})}catch(n){return JSON.stringify({error:n.message})}}import{spawn as ar}from"child_process";import{existsSync as J,mkdirSync as cr,readdirSync as gt,statSync as lr,readFileSync as ur}from"fs";import{resolve as be,join as D,basename as pr}from"path";var Se=".zibby/repos";function oe(r,t,e={}){return new Promise((s,n)=>{let i=ar(r,{cwd:t,shell:!0,env:{...process.env,GIT_TERMINAL_PROMPT:"0",...e}}),o="",a="";i.stdout.on("data",c=>{o+=c.toString()}),i.stderr.on("data",c=>{a+=c.toString()}),i.on("close",c=>{c!==0?n(new Error(`Exit ${c}: ${a.trim()||o.trim()}`)):s(o.trim())}),i.on("error",c=>n(c))})}var ht={id:"git",description:"Clone and manage git repositories for codebase analysis",envKeys:["GITHUB_TOKEN","GITLAB_TOKEN"],promptFragment:`## Git Repositories
317
+ You can clone and explore git repositories locally for codebase analysis:
318
+ - git_checkout: Clone a repo (or pull if already cloned). Supports GitHub and GitLab with auto-auth.
319
+ - git_list_repos: List locally cloned repos
320
+ - git_explore: Quick overview of a cloned repo's structure (key files, package.json, routes, etc.)
321
+
322
+ When a test ticket lacks context, use this workflow:
323
+ 1. Clone the relevant repo with git_checkout
324
+ 2. Use git_explore to understand the project structure
325
+ 3. Use shell commands (grep, cat) to read specific files for deeper understanding
326
+ 4. Use GitHub/GitLab skills to read related PRs and commits
327
+ 5. Build well-informed test specs and save them to files before running tests`,resolve(){return null},async handleToolCall(r,t,e){let s=e?.options?.workspace||process.cwd();try{switch(r){case"git_checkout":return await dr(t,s);case"git_list_repos":return mr(t,s);case"git_explore":return fr(t,s);default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(n){return JSON.stringify({error:n.message})}},tools:[{name:"git_checkout",description:"Clone a git repository locally (or pull latest if already cloned). Auto-authenticates with GitHub/GitLab tokens if available.",input_schema:{type:"object",properties:{url:{type:"string",description:'Repository URL (e.g. "https://github.com/org/repo" or "org/repo" shorthand for GitHub)'},branch:{type:"string",description:"Branch to checkout (default: repo default branch)"},shallow:{type:"boolean",description:"Shallow clone with depth 1 (default: true, faster)"},name:{type:"string",description:"Local directory name override (default: repo name from URL)"}},required:["url"]}},{name:"git_list_repos",description:"List locally cloned repositories",input_schema:{type:"object",properties:{}}},{name:"git_explore",description:"Quick structural overview of a cloned repo: key files, package.json info, directory tree (top 2 levels), detected framework/language",input_schema:{type:"object",properties:{repo:{type:"string",description:"Repo name (as listed by git_list_repos)"},depth:{type:"number",description:"Directory tree depth (default: 2)"}},required:["repo"]}}]};async function dr(r,t){let{url:e,branch:s,shallow:n=!0,name:i}=r;!e.includes("://")&&!e.startsWith("git@")&&(e=`https://github.com/${e}`);let o=i||pr(e.replace(/\.git$/,"")),a=be(t,Se);cr(a,{recursive:!0});let c=D(a,o),l=e,u=process.env.GITHUB_TOKEN,p=process.env.GITLAB_TOKEN,y=process.env.GITLAB_URL;if(e.includes("github.com")&&u)l=e.replace("https://github.com",`https://x-access-token:${u}@github.com`);else if(p&&y)try{let k=new URL(y).host;e.includes(k)&&(l=e.replace(`https://${k}`,`https://oauth2:${p}@${k}`))}catch{}if(J(D(c,".git"))){let k=s?`git -C "${c}" fetch origin ${s} && git -C "${c}" checkout ${s} && git -C "${c}" pull origin ${s}`:`git -C "${c}" pull`;await oe(k,t);let h=await oe(`git -C "${c}" log -1 --format="%h %s"`,t);return JSON.stringify({action:"updated",repo:o,path:c,branch:s||"default",head:h})}let m=["git","clone"];n&&m.push("--depth","1"),s&&m.push("--branch",s),m.push(`"${l}"`,`"${c}"`),await oe(m.join(" "),t);let f=await oe(`git -C "${c}" log -1 --format="%h %s"`,t);return JSON.stringify({action:"cloned",repo:o,path:c,branch:s||"default",shallow:n,head:f})}function mr(r,t){let e=be(t,Se);if(!J(e))return JSON.stringify({repos:[],message:"No repos cloned yet"});let s=[];for(let n of gt(e,{withFileTypes:!0})){if(!n.isDirectory())continue;let i=D(e,n.name);if(!J(D(i,".git")))continue;let o=lr(i);s.push({name:n.name,path:i,lastModified:o.mtime.toISOString()})}return JSON.stringify({repos:s,total:s.length,directory:e})}function fr(r,t){let{repo:e,depth:s=2}=r,n=be(t,Se,e);if(!J(n))return JSON.stringify({error:`Repo not found: ${e}. Run git_checkout first.`});let i={repo:e,path:n},o=D(n,"package.json");if(J(o))try{let m=JSON.parse(ur(o,"utf-8"));i.packageJson={name:m.name,version:m.version,scripts:m.scripts?Object.keys(m.scripts):[],dependencies:m.dependencies?Object.keys(m.dependencies).slice(0,30):[],devDependencies:m.devDependencies?Object.keys(m.devDependencies).slice(0,20):[]},m.dependencies?.react?i.framework="React":m.dependencies?.next?i.framework="Next.js":m.dependencies?.vue?i.framework="Vue":m.dependencies?.angular?i.framework="Angular":m.dependencies?.express?i.framework="Express":m.dependencies?.fastify&&(i.framework="Fastify")}catch{}let a=D(n,"pyproject.toml");J(a)&&(i.language="Python");let c=D(n,"go.mod");J(c)&&(i.language="Go");let l=D(n,"Cargo.toml");J(l)&&(i.language="Rust"),J(o)&&(i.language=i.language||"JavaScript/TypeScript");let u=[];function p(m,f,k){if(k>s)return;let h;try{h=gt(m,{withFileTypes:!0})}catch{return}let g=h.filter(_=>!_.name.startsWith(".")&&_.name!=="node_modules"&&_.name!=="__pycache__"&&_.name!=="dist"&&_.name!=="build"&&_.name!==".git").sort((_,d)=>_.isDirectory()!==d.isDirectory()?_.isDirectory()?-1:1:_.name.localeCompare(d.name));for(let _ of g){let d=_.isDirectory();u.push(`${f}${d?"\u{1F4C1}":"\u{1F4C4}"} ${_.name}`),d&&k<s&&p(D(m,_.name),`${f} `,k+1)}}p(n,"",1),i.tree=u.slice(0,80),u.length>80&&(i.treeTruncated=!0);let y=["README.md","README.rst","src/App.tsx","src/App.jsx","src/App.js","src/routes.tsx","src/routes.js","app/routes.tsx","app/routes.js","src/index.tsx","src/index.ts","src/main.tsx","src/main.ts","pages/_app.tsx","pages/_app.js","app/layout.tsx","docker-compose.yml","Dockerfile",".env.example"];return i.keyFilesFound=y.filter(m=>J(D(n,m))),JSON.stringify(i)}import{execFileSync as Ot}from"child_process";import{existsSync as $t,mkdirSync as yr}from"fs";import{join as Z,basename as gr}from"path";import{randomBytes as hr}from"crypto";import{pathToFileURL as le}from"url";import{createRequire as Rt}from"module";var _t=".zibby/memory",It="dolt",jt={encoding:"utf-8",stdio:["pipe","pipe","pipe"],timeout:15e3},Oe="dolt",ve=new Map,ae=new Map,_r=Rt(import.meta.url),pe=()=>hr(8).toString("hex"),W=()=>new Date().toISOString(),kr=[`CREATE TABLE IF NOT EXISTS chat_memory (
328
+ id VARCHAR(64) PRIMARY KEY,
329
+ memory_key VARCHAR(160),
330
+ category VARCHAR(32) NOT NULL,
331
+ content TEXT NOT NULL,
332
+ source VARCHAR(64),
333
+ ticket_key VARCHAR(32),
334
+ session_id VARCHAR(64),
335
+ tier VARCHAR(16) DEFAULT 'mid',
336
+ relevance FLOAT DEFAULT 1.0,
337
+ created_at VARCHAR(32) NOT NULL
338
+ )`,`CREATE TABLE IF NOT EXISTS chat_tasks (
339
+ id VARCHAR(64) PRIMARY KEY,
340
+ ticket_key VARCHAR(32),
341
+ type VARCHAR(32) NOT NULL,
342
+ title VARCHAR(512) NOT NULL,
343
+ status VARCHAR(32) NOT NULL DEFAULT 'pending',
344
+ spec_path VARCHAR(512),
345
+ session_id VARCHAR(64),
346
+ result_summary TEXT,
347
+ created_at VARCHAR(32) NOT NULL,
348
+ finished_at VARCHAR(32)
349
+ )`,`CREATE TABLE IF NOT EXISTS chat_sessions (
350
+ session_id VARCHAR(64) PRIMARY KEY,
351
+ summary TEXT NOT NULL,
352
+ tickets TEXT,
353
+ tasks_run INT DEFAULT 0,
354
+ tasks_passed INT DEFAULT 0,
355
+ tasks_failed INT DEFAULT 0,
356
+ key_facts TEXT,
357
+ created_at VARCHAR(32) NOT NULL
358
+ )`],kt=new Set;function T(r,t){return Ot(It,t,{...jt,cwd:r})}function P(r,t){try{let e=T(r,["sql","-q",t,"-r","json"]);return JSON.parse(e.trim()).rows||[]}catch{return[]}}function C(r,t){T(r,["sql","-q",t])}function wt(r){if(kt.has(r))return!0;if(!$t(Z(r,".dolt"))){if(!wr())return!1;yr(r,{recursive:!0}),T(r,["init","--name","Zibby Chat Memory","--email","chat@zibby.app"])}let t=`${kr.join(`;
359
+ `)};`;C(r,t);try{C(r,"ALTER TABLE chat_memory ADD COLUMN tier VARCHAR(16) DEFAULT 'mid'")}catch{}try{C(r,"ALTER TABLE chat_memory ADD COLUMN memory_key VARCHAR(160)")}catch{}return kt.add(r),!0}function wr(){try{return Ot(It,["version"],{...jt,timeout:5e3}),!0}catch{return!1}}function w(r){return r==null?"NULL":`'${String(r).replace(/'/g,"''")}'`}function $e(r){return String(r||"").toLowerCase().replace(/[“”]/g,'"').replace(/[‘’]/g,"'").replace(/[\s_-]+/g," ").replace(/[^\w\s"']/g,"").replace(/\s+/g," ").trim()}function G(r){return r==="long"?3:r==="mid"?2:r==="short"?1:0}function Re(r,t){let e=["short","mid","long"].includes(r)?r:"mid";return new Set(["fact","decision","preference","credential","url","workaround"]).has(String(t||"").toLowerCase())&&e==="short"?"mid":e}function At(r){let t=new Map;for(let e of r||[]){let s=$e(e.content),n=e.memory_key?`key:${e.memory_key}`:s?`norm:${s}`:"";if(!n)continue;let i=t.get(n);if(!i){t.set(n,e);continue}let o=G(i.tier),a=G(e.tier);if(a>o){t.set(n,e);continue}a===o&&Number(e.relevance||0)>Number(i.relevance||0)&&t.set(n,e)}return[...t.values()]}function ue(r,t){let e=String(r??"");return e.length<=t?e:t<=1?e.slice(0,t):`${e.slice(0,t-1)}\u2026`}function ce(r,t){let e={recentSessions:Array.isArray(r?.recentSessions)?r.recentSessions:[],topMemories:Array.isArray(r?.topMemories)?r.topMemories:[],taskStats:Array.isArray(r?.taskStats)?r.taskStats:[],ticketFilter:r?.ticketFilter||null,backend:t||String(r?.backend||Oe),error:r?.error||null};return e.backend==="mem0"?{...e,recentSessions:[],taskStats:[]}:e}function br(r){let t=[];if(r.recentSessions?.length>0){t.push("Recent sessions:");for(let e of r.recentSessions.slice(0,3))e?.summary?.trim()&&t.push(`- ${ue(e.summary,150)}${e.tickets?` [${e.tickets}]`:""}`)}if(r.topMemories?.length>0){t.push("Known facts:");for(let e of r.topMemories.slice(0,10)){let s=e.tier==="long"?"\u2605":"\xB7";t.push(`${s} [${e.category}] ${ue(e.content,120)}`)}}return t.length===0?"":`## Memory Context
360
+ ${t.join(`
361
+ `)}`}function Ne(r){return{backend:r.backend,recentSessions:r.recentSessions.slice(0,3).map(t=>({summary:ue(String(t?.summary||""),160),tickets:t?.tickets||null,created_at:t?.created_at||null})),topMemories:r.topMemories.slice(0,8).map(t=>({category:t?.category||null,tier:t?.tier||null,content:ue(String(t?.content||""),140),source:t?.source||null})),taskStats:r.taskStats,error:r.error||null}}async function bt(r,t){let e=String(process.env.ZIBBY_MEMORY_BACKEND||"").trim().toLowerCase();if(e==="mem0"||e==="dolt")return e;let s=String(t?.options?.memoryBackend||t?.options?.config?.memory?.backend||"").trim().toLowerCase();if(s==="mem0"||s==="dolt")return s;if(ae.has(r))return ae.get(r);try{let n=Z(r,".zibby.config.mjs");if($t(n)){let i=await import(le(n).href),o=String(i?.default?.memory?.backend||"").trim().toLowerCase();if(o==="mem0"||o==="dolt")return ae.set(r,o),o}}catch{}return ae.set(r,Oe),Oe}function Tt(r){let t=String(process.env.ZIBBY_MEMORY_USER_ID||"").trim();return t||`workspace:${gr(r||process.cwd())}`}function Sr(){let r=String(process.env.ZIBBY_MEM0_OPENAI_BASE_URL||"").trim();if(!r)return null;let t=String(process.env.ZIBBY_MEM0_API_KEY||process.env.ZIBBY_USER_TOKEN||process.env.OPENAI_API_KEY||"").trim(),e=String(process.env.ZIBBY_MEM0_LLM_MODEL||"gpt-4.1-mini").trim(),s=String(process.env.ZIBBY_MEM0_EMBEDDER_MODEL||"text-embedding-3-small").trim(),n=Number(process.env.ZIBBY_MEM0_EMBEDDING_DIMS||1536);return{llm:{provider:"openai",config:{model:e,baseURL:r,...t?{apiKey:t}:{}}},embedder:{provider:"openai",config:{model:s,embeddingDims:n,baseURL:r,...t?{apiKey:t}:{}}},vectorStore:{provider:"memory",config:{dimension:n}}}}async function Ct(r){let t=r||process.cwd();if(ve.has(t))return ve.get(t);let e;try{let a=Rt(le(Z(t,"package.json")).href).resolve("mem0ai/oss");e=await import(le(a).href)}catch{try{let o=_r.resolve("mem0ai/oss");e=await import(le(o).href)}catch(o){throw new Error(`Cannot find package 'mem0ai' for workspace "${t}". Install in that project: npm install mem0ai. (${o.message})`,{cause:o})}}let s=e?.Memory;if(!s)throw new Error("mem0ai/oss does not export Memory");let n=Sr(),i=n?new s(n):new s;return ve.set(t,i),i}function St(r,t="mid"){return(Array.isArray(r)?r:Array.isArray(r?.results)?r.results:[]).map(s=>({id:s?.id||pe(),memory_key:s?.metadata?.memoryKey||s?.metadata?.memory_key||null,category:s?.metadata?.category||"fact",content:s?.memory||s?.content||"",source:s?.metadata?.source||"mem0",ticket_key:s?.metadata?.ticketKey||s?.metadata?.ticket_key||null,tier:Re(s?.metadata?.tier||t,s?.metadata?.category||"fact"),relevance:Number(s?.score??s?.metadata?.relevance??.8),created_at:s?.created_at||s?.metadata?.created_at||W()})).filter(s=>String(s.content||"").trim().length>0)}var Et={id:"chat-memory",description:"Persistent chat memory and task history (Dolt-backed)",envKeys:[],promptFragment:`## Chat Memory (persistent)
362
+ You have persistent memory across sessions. Use it to avoid losing context:
363
+ - **memory_store**: Save important facts, decisions, or context. Anything worth remembering.
364
+ - **memory_recall**: Search your memory by keyword or category. Use this at the START of conversations to recall relevant context.
365
+ - **memory_brief**: Get a compact summary of recent sessions and key facts. Load this when starting a new task to understand history.
366
+ - **memory_end_session**: Call when a task is complete. Summarizes what happened for future recall.
367
+ - **task_log**: Record a completed task (test run, analysis, etc.) for history.
368
+ - **task_history**: Query past tasks by ticket, status, or type.
369
+
370
+ ### When to use memory
371
+ - At the START of a conversation: call memory_recall or memory_brief to load relevant context
372
+ - When you learn something important: call memory_store (e.g. "SCRUM-123 login page is at /auth/login")
373
+ - When a task finishes: call task_log to record it
374
+ - When the user's request is complete: call memory_end_session
375
+
376
+ ### Categories for memory_store
377
+ fact, decision, context, insight, credential, url, error, workaround`,resolve(){return null},async buildPromptContext(r,t={}){let e=r?.options?.workspace||process.cwd(),s=Z(e,_t),n=await bt(e,r);if(n==="dolt"&&!wt(s)){let i="Dolt not available. Install: brew install dolt (macOS) or see https://docs.dolthub.com/introduction/installation";return{backend:n,brief:ce({backend:n,error:i},n),promptContext:"",debugPreview:Ne(ce({backend:n,error:i},n)),error:i}}try{let i=n==="mem0"?await Nt(t,s,e):vt(t,s),o=JSON.parse(i||"{}"),a=ce({...o,backend:n},n);return{backend:n,brief:a,promptContext:br(a),debugPreview:Ne(a),error:a.error||null}}catch(i){let o=String(i?.message||i),a=ce({backend:n,error:o},n);return{backend:n,brief:a,promptContext:"",debugPreview:Ne(a),error:o}}},async handleToolCall(r,t,e){let s=e?.options?.workspace||process.cwd(),n=Z(s,_t),i=await bt(s,e);if((i==="dolt"||["memory_end_session","task_log","task_history"].includes(r))&&!wt(n))return JSON.stringify({error:"Dolt not available. Install: brew install dolt (macOS) or see https://docs.dolthub.com/introduction/installation"});try{switch(r){case"memory_store":return i==="mem0"?await vr(t,n,s):xt(t,n);case"memory_recall":return i==="mem0"?await Lt(t,n,s):Nr(t,n);case"memory_brief":return i==="mem0"?await Nt(t,n,s):vt(t,n);case"memory_end_session":return Or(t,n);case"task_log":return $r(t,n);case"task_history":return Rr(t,n);default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(a){if(i==="mem0")throw new Error(`mem0 throw: ${a.message}`,{cause:a});return JSON.stringify({error:a.message})}},tools:[{name:"memory_store",description:"Save a fact, decision, or context to persistent memory. Survives across sessions.",input_schema:{type:"object",properties:{memoryKey:{type:"string",description:"Stable semantic identity key (e.g. user.jira.default_board)"},content:{type:"string",description:"The information to remember"},category:{type:"string",enum:["fact","decision","context","insight","preference","credential","url","error","workaround"],description:"Category of memory"},tier:{type:"string",enum:["short","mid","long"],description:"Memory tier: short (session/24h), mid (days/weeks), long (permanent)"},source:{type:"string",description:'Where this info came from (e.g. "jira", "github", "user", "test_run")'},ticketKey:{type:"string",description:"Related ticket key (optional)"}},required:["content","category"]}},{name:"memory_recall",description:"Search persistent memory by keyword, category, ticket, or tier. Returns matching facts and context.",input_schema:{type:"object",properties:{query:{type:"string",description:"Search text (matches content)"},category:{type:"string",description:"Filter by category"},ticketKey:{type:"string",description:"Filter by ticket key"},tier:{type:"string",enum:["short","mid","long"],description:"Filter by memory tier"},limit:{type:"number",description:"Max results (default: 20)"}}}},{name:"memory_brief",description:"Get a compact briefing: recent session summaries + top relevant facts. Call at the start of a conversation.",input_schema:{type:"object",properties:{ticketKey:{type:"string",description:"Focus briefing on a specific ticket (optional)"}}}},{name:"memory_end_session",description:"End the current session and save a summary for future recall. Call when a task is complete.",input_schema:{type:"object",properties:{summary:{type:"string",description:"What happened in this session (1-3 sentences)"},tickets:{type:"string",description:"Comma-separated ticket keys covered"},tasksRun:{type:"number",description:"Number of tasks/tests run"},tasksPassed:{type:"number",description:"Number passed"},tasksFailed:{type:"number",description:"Number failed"},keyFacts:{type:"string",description:"Key facts worth remembering from this session (semicolon-separated)"}},required:["summary"]}},{name:"task_log",description:"Record a completed task (test run, analysis, generation) to persistent history.",input_schema:{type:"object",properties:{title:{type:"string",description:"Task description"},type:{type:"string",enum:["test_run","generate","analysis","research","other"],description:"Task type"},status:{type:"string",enum:["passed","failed","cancelled","error"],description:"Outcome"},ticketKey:{type:"string",description:"Related ticket key"},specPath:{type:"string",description:"Spec file path (if test run)"},resultSummary:{type:"string",description:"Brief result description"}},required:["title","type","status"]}},{name:"task_history",description:"Query past tasks by ticket, status, or type. See what was done before.",input_schema:{type:"object",properties:{ticketKey:{type:"string",description:"Filter by ticket key"},type:{type:"string",description:"Filter by task type"},status:{type:"string",description:"Filter by status"},limit:{type:"number",description:"Max results (default: 20)"}}}}]};function xt(r,t){let{content:e,category:s,source:n,ticketKey:i,tier:o,memoryKey:a}=r;if(!e||!s)return JSON.stringify({error:"content and category are required"});let c=$e(e);if(!c)return JSON.stringify({error:"content is empty after normalization"});let l=Re(o,s),u=l==="long"?1:l==="mid"?.8:.5,p=String(a||"").trim().slice(0,160);if(p){let g=P(t,`SELECT id, tier, relevance
378
+ FROM chat_memory
379
+ WHERE memory_key = ${w(p)}
380
+ ORDER BY created_at DESC
381
+ LIMIT 1`)[0];if(g){let _=String(g.tier||"mid"),d=Number(g.relevance||0),b=G(l)>G(_)?l:_,I=Math.max(u,d);C(t,`UPDATE chat_memory
382
+ SET content = ${w(e)},
383
+ category = ${w(s)},
384
+ source = ${w(n)},
385
+ ticket_key = ${w(i)},
386
+ tier = ${w(b)},
387
+ relevance = ${I},
388
+ created_at = ${w(W())}
389
+ WHERE id = ${w(g.id)}`);try{T(t,["add","."]),T(t,["commit","-m",`memory upsert: ${s} \u2014 ${String(e).slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:g.id,category:s,tier:b,memoryKey:p,upserted:!0})}}let m=P(t,`SELECT id, content, tier, relevance
390
+ FROM chat_memory
391
+ WHERE category = ${w(s)}
392
+ ORDER BY created_at DESC
393
+ LIMIT 200`).find(h=>$e(h.content)===c);if(m){let h=String(m.tier||"mid"),g=Number(m.relevance||0),_=G(l)>G(h),d=u>g;if(_||d){C(t,`UPDATE chat_memory
394
+ SET tier = ${w(_?l:h)},
395
+ relevance = ${Math.max(u,g)}
396
+ WHERE id = ${w(m.id)}`);try{T(t,["add","."]),T(t,["commit","-m",`memory promote: ${s} \u2014 ${String(e).slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:m.id,category:s,tier:_?l:h,deduped:!0,promoted:!0})}return JSON.stringify({ok:!0,id:m.id,category:s,tier:h,deduped:!0,promoted:!1})}let f=pe(),k=process.env.ZIBBY_CHAT_SESSION_ID||null;C(t,`INSERT INTO chat_memory (id, memory_key, category, content, source, ticket_key, session_id, tier, relevance, created_at)
397
+ VALUES (${w(f)}, ${w(p||null)}, ${w(s)}, ${w(e)}, ${w(n)}, ${w(i)}, ${w(k)}, ${w(l)}, ${u}, ${w(W())})`);try{T(t,["add","."]),T(t,["commit","-m",`memory: ${s} \u2014 ${e.slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:f,category:s,tier:l,memoryKey:p||null,stored:e.slice(0,100)})}async function vr(r,t,e){let{content:s,category:n,source:i,ticketKey:o,tier:a,memoryKey:c}=r;if(!s||!n)return JSON.stringify({error:"content and category are required"});try{let l=await Ct(e),u=Tt(e),p=Re(a,n);return await l.add([{role:"user",content:String(s)}],{userId:u,metadata:{memoryKey:c||null,category:n,tier:p,source:i||"zibby-chat",ticketKey:o||null,created_at:W()}}),JSON.stringify({ok:!0,backend:"mem0",userId:u,category:n,tier:p,memoryKey:c||null,stored:String(s).slice(0,100)})}catch(l){throw new Error(`mem0 store failed: ${l.message}. If mem0 is not installed, run: npm install mem0ai`,{cause:l})}}function Nr(r,t){let{query:e,category:s,ticketKey:n,tier:i,limit:o=20}=r,a=[];e&&a.push(`content LIKE ${w(`%${e}%`)}`),s&&a.push(`category = ${w(s)}`),n&&a.push(`ticket_key = ${w(n)}`),i&&a.push(`tier = ${w(i)}`);let l=`SELECT id, memory_key, category, content, source, ticket_key, tier, relevance, created_at
398
+ FROM chat_memory ${a.length>0?`WHERE ${a.join(" AND ")}`:""}
399
+ ORDER BY relevance DESC, created_at DESC
400
+ LIMIT ${o}`,u=P(t,l);return JSON.stringify({total:u.length,memories:u})}async function Lt(r,t,e){let{query:s,category:n,ticketKey:i,tier:o,limit:a=20}=r;try{let c=await Ct(e),l=Tt(e),u=[];if(s&&String(s).trim()){let p=await c.search(String(s),{userId:l,limit:a});u=St(p)}else{let p=await c.getAll({userId:l,limit:Math.max(a,50)});u=St(p)}return n&&(u=u.filter(p=>p.category===n)),i&&(u=u.filter(p=>p.ticket_key===i)),o&&(u=u.filter(p=>p.tier===o)),u=u.slice(0,a),JSON.stringify({total:u.length,memories:u,backend:"mem0"})}catch(c){throw new Error(`mem0 recall failed: ${c.message}. If mem0 is not installed, run: npm install mem0ai`,{cause:c})}}function vt(r,t){let{ticketKey:e}=r;jr(t);let n=P(t,`SELECT session_id, summary, tickets, tasks_run, tasks_passed, tasks_failed, created_at
401
+ FROM chat_sessions ORDER BY created_at DESC LIMIT 5`),i=e?`AND ticket_key = ${w(e)}`:"",o=P(t,`SELECT memory_key, category, content, source, tier, relevance, created_at FROM chat_memory
402
+ WHERE tier = 'long' ${i} ORDER BY relevance DESC, created_at DESC LIMIT 10`),a=P(t,`SELECT memory_key, category, content, source, tier, relevance, created_at FROM chat_memory
403
+ WHERE tier = 'mid' ${i} ORDER BY relevance DESC, created_at DESC LIMIT 8`),l=P(t,`SELECT type, status, COUNT(*) as cnt FROM chat_tasks
404
+ GROUP BY type, status ORDER BY cnt DESC LIMIT 10`),u=At([...o,...a]);return JSON.stringify({recentSessions:n,topMemories:u,taskStats:l,ticketFilter:e||null})}async function Nt(r,t,e){let{ticketKey:s}=r,n=await Lt({limit:80},t,e),i=JSON.parse(n||"{}"),o=Array.isArray(i.memories)?i.memories:[];s&&(o=o.filter(y=>y.ticket_key===s));let a=y=>{let m=Date.parse(String(y?.created_at||""))||0;return Number(y?.relevance||0)*1e12+m},c=(y,m)=>a(m)-a(y),l=o.filter(y=>y.tier==="long").sort(c).slice(0,10),u=o.filter(y=>y.tier==="mid").sort(c).slice(0,8),p=At([...l,...u]);return JSON.stringify({recentSessions:[],topMemories:p,taskStats:[],ticketFilter:s||null,backend:"mem0"})}function Or(r,t){let{summary:e,tickets:s,tasksRun:n=0,tasksPassed:i=0,tasksFailed:o=0,keyFacts:a}=r;if(!e)return JSON.stringify({error:"summary is required"});let c=process.env.ZIBBY_CHAT_SESSION_ID||`session_${pe()}`;if(C(t,`INSERT INTO chat_sessions (session_id, summary, tickets, tasks_run, tasks_passed, tasks_failed, key_facts, created_at)
405
+ VALUES (${w(c)}, ${w(e)}, ${w(s)}, ${n}, ${i}, ${o}, ${w(a)}, ${w(W())})`),a)for(let l of a.split(";").map(u=>u.trim()).filter(Boolean))xt({content:l,category:"fact",source:"session_summary",tier:"mid"},t);Ir(t);try{T(t,["add","."]),T(t,["commit","-m",`session end: ${e.slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,sessionId:c,summary:e.slice(0,200)})}function $r(r,t){let{title:e,type:s,status:n,ticketKey:i,specPath:o,resultSummary:a}=r;if(!e||!s||!n)return JSON.stringify({error:"title, type, and status are required"});let c=pe(),l=process.env.ZIBBY_CHAT_SESSION_ID||null;C(t,`INSERT INTO chat_tasks (id, ticket_key, type, title, status, spec_path, session_id, result_summary, created_at, finished_at)
406
+ VALUES (${w(c)}, ${w(i)}, ${w(s)}, ${w(e)}, ${w(n)}, ${w(o)}, ${w(l)}, ${w(a)}, ${w(W())}, ${w(W())})`);try{T(t,["add","."]),T(t,["commit","-m",`task: ${n} \u2014 ${e.slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:c,title:e,type:s,status:n})}function Rr(r,t){let{ticketKey:e,type:s,status:n,limit:i=20}=r,o=[];e&&o.push(`ticket_key = ${w(e)}`),s&&o.push(`type = ${w(s)}`),n&&o.push(`status = ${w(n)}`);let c=`SELECT id, ticket_key, type, title, status, spec_path, result_summary, created_at, finished_at
407
+ FROM chat_tasks ${o.length>0?`WHERE ${o.join(" AND ")}`:""}
408
+ ORDER BY created_at DESC LIMIT ${i}`,l=P(t,c);return JSON.stringify({total:l.length,tasks:l})}function Ir(r){try{C(r,"UPDATE chat_memory SET relevance = relevance * 0.98 WHERE tier = 'long' AND relevance > 0.5"),C(r,"UPDATE chat_memory SET relevance = relevance * 0.90 WHERE tier = 'mid' AND relevance > 0.1"),C(r,"UPDATE chat_memory SET relevance = relevance * 0.70 WHERE tier = 'short' AND relevance > 0.05"),C(r,"DELETE FROM chat_memory WHERE relevance < 0.05")}catch{}}function jr(r){try{let t=new Date(Date.now()-864e5).toISOString();C(r,`DELETE FROM chat_memory WHERE tier = 'short' AND created_at < ${w(t)}`)}catch{}}import{existsSync as M,readFileSync as Q,readdirSync as Ie,mkdirSync as Ar,writeFileSync as H,statSync as Mt}from"fs";import{join as S,resolve as je,relative as Ut,dirname as qt}from"path";import{fileURLToPath as Tr}from"url";import{createRequire as Cr}from"module";var Er=Cr(import.meta.url),xr=`## Workflow Builder
409
+
410
+ You can help users build custom AI workflows using the Zibby workflow framework.
411
+
412
+ ### What makes Zibby workflows different
413
+ Each node invokes a **real AI agent** (Cursor, Claude, Codex, or Gemini) \u2014 not a thin LLM API wrapper.
414
+ That means every node has full agent capabilities: tool use, MCP servers (browser, GitHub, Jira, Slack),
415
+ multi-turn reasoning, and structured output validation via Zod schemas.
416
+
417
+ Key differentiators:
418
+ - **Agent-powered nodes** \u2014 each step runs a full AI agent (cursor-agent, claude, codex, gemini CLI) with tool access and MCP skills, not a simple chat completion call.
419
+ - **Structured output** \u2014 every node declares a Zod schema; the framework validates and parses the agent's response automatically.
420
+ - **Conditional routing** \u2014 edges can branch on agent-produced fields (e.g., \`state.triage.priority === 'critical'\`), enabling intelligent decision graphs.
421
+ - **MCP skill injection** \u2014 nodes declare \`skills: [SKILLS.BROWSER, SKILLS.GITHUB]\` and the framework spins up the right MCP servers automatically.
422
+ - **Deploy anywhere** \u2014 \`zibby deploy\` pushes to Zibby Cloud with an API trigger; or self-host with \`zibby start\`.
423
+ - **State accumulation** \u2014 each node's validated output is stored under its name in \`state\` (e.g., \`state.classify_ticket\`), so downstream nodes can reference upstream results.
424
+
425
+ ### What is a workflow?
426
+ A directed graph of nodes (AI agent steps) connected by edges. Each node has:
427
+ - \`name\` \u2014 unique identifier (snake_case)
428
+ - \`prompt\` \u2014 function that receives state and returns the prompt string sent to the agent
429
+ - \`outputSchema\` \u2014 Zod schema defining the structured output the agent must return
430
+ - \`skills\` (optional) \u2014 array of MCP skill IDs the node needs (e.g., \`SKILLS.BROWSER\`, \`SKILLS.GITHUB\`)
431
+ - \`timeout\` (optional) \u2014 max execution time in ms (default: 300000)
432
+ - \`model\` (optional) \u2014 override the model for this node (e.g., \`'claude-opus-4'\`)
433
+
434
+ ### File structure
435
+ \`\`\`
436
+ .zibby/workflows/<name>/
437
+ \u251C\u2500\u2500 graph.mjs \u2014 WorkflowAgent subclass with buildGraph()
438
+ \u251C\u2500\u2500 nodes/
439
+ \u2502 \u251C\u2500\u2500 index.mjs \u2014 barrel export for all nodes
440
+ \u2502 \u2514\u2500\u2500 <node>.mjs \u2014 one file per node
441
+ \u2514\u2500\u2500 workflow.json \u2014 manifest (name, description, triggers)
442
+ \`\`\`
443
+
444
+ ### Node pattern
445
+ \`\`\`javascript
446
+ import { z, SKILLS } from '@zibby/core';
447
+
448
+ const OutputSchema = z.object({
449
+ summary: z.string().describe('Brief summary'),
450
+ items: z.array(z.string()).describe('List of extracted items'),
451
+ needsReview: z.boolean().describe('Whether a human should review this'),
452
+ });
453
+
454
+ export const myNode = {
455
+ name: 'my_node',
456
+ skills: [SKILLS.GITHUB], // optional \u2014 framework injects MCP servers
457
+ timeout: 120000, // optional \u2014 2 min timeout
458
+ prompt: (state) => \\\`You are analyzing a pull request.
459
+
460
+ Input:
461
+ \\\${JSON.stringify(state.input || {}, null, 2)}
462
+
463
+ Return a JSON object matching the schema.\\\`,
464
+ outputSchema: OutputSchema,
465
+ };
466
+ \`\`\`
467
+
468
+ ### Graph pattern
469
+ \`\`\`javascript
470
+ import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
471
+ import { classifyNode, routeNode } from './nodes/index.mjs';
472
+
473
+ export class MyWorkflow extends WorkflowAgent {
474
+ buildGraph() {
475
+ const graph = new WorkflowGraph();
476
+ graph.addNode('classify', classifyNode);
477
+ graph.addNode('route', routeNode);
478
+ graph.setEntryPoint('classify');
479
+ graph.addEdge('classify', 'route');
480
+ graph.addEdge('route', 'END');
481
+ return graph;
482
+ }
483
+
484
+ async onComplete(result) {
485
+ // Post-execution hook \u2014 save artifacts, notify, etc.
486
+ console.log('Workflow complete:', result.success);
487
+ }
488
+ }
489
+ \`\`\`
490
+
491
+ Conditional edges: \`graph.addConditionalEdges('node', (state) => state.node.priority === 'high' ? 'escalate' : 'notify')\`
492
+
493
+ ### Available SKILLS constants
494
+ Import from \`@zibby/core\`: \`SKILLS.BROWSER\`, \`SKILLS.MEMORY\`, \`SKILLS.GITHUB\`, \`SKILLS.JIRA\`, \`SKILLS.SLACK\`, \`SKILLS.RUNNER\`
495
+
496
+ ### Deep documentation
497
+ Call \`explore_framework_docs\` to read detailed framework docs on demand. Use it for:
498
+ - Advanced patterns (middleware, parallel nodes, state schemas)
499
+ - Deployment & cloud triggers
500
+ - CLI commands reference
501
+ - Integration details (Jira, GitHub, etc.)
502
+ Call with no arguments to see all available topics.
503
+
504
+ ### How to use the builder tools
505
+ 1. For complex workflows, call \`explore_framework_docs("custom-workflows")\` first to learn advanced patterns.
506
+ 2. Ask the user what their workflow should do, what input it receives, and what steps are needed.
507
+ 3. Call \`design_workflow\` with the structured spec for the user to review.
508
+ 4. Once approved, call \`build_workflow\` to generate real code on disk (uses the configured agent for high-quality code generation).
509
+ 5. Remind the user: \`zibby start <name>\` to test locally, \`zibby deploy <name> --project <id>\` to deploy to cloud, \`zibby logs --workflow <name>\` to tail logs.
510
+
511
+ ### Important
512
+ - Each node prompt should be detailed and specific \u2014 tell the AI agent exactly what to do and what format to return.
513
+ - Zod schemas MUST use .describe() on every field so the agent knows what each field means.
514
+ - Node names must be snake_case (e.g., classify_ticket, generate_report).
515
+ - Workflow names must be kebab-case (e.g., ticket-triage, pr-review).
516
+ - State flows through: each node's validated output is stored under its name in state (e.g., state.classify_ticket).
517
+ - Downstream nodes reference upstream outputs in their prompt function (e.g., \\\`\\\${JSON.stringify(state.classify_ticket, null, 2)}\\\`).
518
+ - Nodes can declare skills to get MCP tool access \u2014 the framework handles server lifecycle automatically.`,Kt=/^[a-z][a-z0-9-]{0,62}[a-z0-9]$/;function Pt(r){return`${r.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join("")}Workflow`}function V(r){return`${r.replace(/_([a-z])/g,(t,e)=>e.toUpperCase())}Node`}function Lr(r){let t=r?.agent;return t?t.provider?t.provider:t.gemini?"gemini":t.codex?"codex":t.claude?"claude":t.cursor?"cursor":process.env.AGENT_TYPE||"cursor":process.env.AGENT_TYPE||"cursor"}async function Jr(r){let t=je(r,".zibby.config.mjs");if(!M(t))return{};try{return(await import(t)).default||{}}catch{return{}}}function Dr(){try{let r=qt(Er.resolve("@zibby/core/package.json")),t=S(r,"templates","browser-test-automation"),e=Q(S(t,"nodes","preflight.mjs"),"utf-8"),s=Q(S(t,"graph.mjs"),"utf-8");return{preflight:e,graph:s}}catch{return null}}var Jt=qt(Tr(import.meta.url));function Bt(){let r=je(Jt,"..","..","..","docsite","docs");if(M(r))return r;let t=je(Jt,"..","docs");return M(t)?t:null}function Dt(){let r=Bt();if(!r)return[];try{let t=(e,s="")=>{let n=[];for(let i of Ie(e)){let o=S(e,i);try{if(Mt(o).isDirectory())n=n.concat(t(o,`${s}${i}/`));else if(i.endsWith(".md")){let a=`${s}${i.replace(/\.md$/,"")}`;n.push(a)}}catch{}}return n};return t(r)}catch{return[]}}function Ft(r){let t=Bt();if(!t)return null;let e=S(t,`${r}.md`);if(!M(e))return null;try{return Q(e,"utf-8")}catch{return null}}function Mr(r){let t=r.nodes.map(o=>{let a=o.inputFields?.length?`Input fields: ${o.inputFields.join(", ")}`:"Input: receives full state",c=o.outputFields?.length?`Output fields: ${o.outputFields.join(", ")}`:"Output: determined by task",l=o.skills?.length?`Skills: ${o.skills.join(", ")}`:"";return`- ${o.name}: ${o.description}. ${a}. ${c}.${l?` ${l}`:""}`}).join(`
519
+ `),e=r.edges.map(o=>o.condition?`- ${o.from} \u2192 ${o.to} (conditional: ${o.condition})`:`- ${o.from} \u2192 ${o.to}`).join(`
520
+ `),s=Dr(),n=Ft("custom-workflows"),i="";return s&&(i+=`
521
+ ## Real working examples from the Zibby framework
522
+
523
+ ### Example node (preflight.mjs) \u2014 a prompt-only node with Zod schema and onComplete hook:
524
+ \`\`\`javascript
525
+ ${s.preflight}
526
+ \`\`\`
527
+
528
+ ### Example graph (graph.mjs) \u2014 WorkflowAgent subclass with conditional routing:
529
+ \`\`\`javascript
530
+ ${s.graph}
531
+ \`\`\`
532
+
533
+ Study these examples carefully. Your generated code must follow the same patterns exactly.
534
+ `),n&&(i+=`
535
+ ## Full framework documentation (Custom Workflows)
536
+ ${n}
537
+ `),`Generate the code for a Zibby workflow called "${r.name}".
538
+
539
+ ## Zibby Workflow Framework Reference
540
+
541
+ Zibby workflows are directed graphs where each node invokes a **real AI agent** (Cursor, Claude, Codex, or Gemini)
542
+ with full tool access, MCP server integration, and Zod-validated structured output.
543
+ This is NOT a simple LLM API wrapper \u2014 each node runs a full agent with tool-calling capabilities.
544
+
545
+ ### Architecture
546
+ - The framework calls the configured AI agent for each node.
547
+ - Each node's \`prompt\` function receives the accumulated \`state\` object and returns a prompt string.
548
+ - The agent's response is parsed and validated against the node's \`outputSchema\` (Zod).
549
+ - The validated output is stored in \`state\` under the node's name (e.g., \`state.classify_ticket\`).
550
+ - Downstream nodes access upstream results via \`state.<upstream_node_name>\`.
551
+
552
+ ### Node properties
553
+ - \`name\` (string, required) \u2014 snake_case identifier
554
+ - \`prompt\` (function, required) \u2014 \`(state) => \\\`...\\\`\` returns the prompt string
555
+ - \`outputSchema\` (Zod schema, required) \u2014 every field MUST have \`.describe()\`
556
+ - \`skills\` (array, optional) \u2014 MCP skills: \`[SKILLS.BROWSER]\`, \`[SKILLS.GITHUB]\`, etc.
557
+ - \`timeout\` (number, optional) \u2014 ms, default 300000
558
+ - \`onComplete\` (async function, optional) \u2014 \`(state, result) => {}\` post-processing hook
559
+
560
+ ### Available SKILLS constants (import from '@zibby/core')
561
+ SKILLS.BROWSER, SKILLS.MEMORY, SKILLS.GITHUB, SKILLS.JIRA, SKILLS.SLACK, SKILLS.RUNNER
562
+
563
+ ### Graph API
564
+ - \`graph.addNode(name, nodeObject)\` \u2014 register a node
565
+ - \`graph.setEntryPoint(name)\` \u2014 set the first node
566
+ - \`graph.addEdge(from, to)\` \u2014 connect nodes (use \`'END'\` to terminate)
567
+ - \`graph.addConditionalEdges(from, (state) => 'nextNode' | 'END')\` \u2014 conditional routing
568
+
569
+ ### Rules
570
+ - Import: \`import { z } from '@zibby/core';\` (add \`SKILLS\` only if the node uses skills)
571
+ - Export name: camelCase + "Node" (e.g., \`classifyTicketNode\` for name \`classify_ticket\`)
572
+ - Prompt function: template literal referencing \`state.input\` and upstream \`state.<node_name>\`
573
+ - Prompts must be detailed \u2014 tell the agent exactly what to analyze/produce
574
+ ${i}
575
+ ## Workflow to generate: "${r.name}"
576
+
577
+ ### Description
578
+ ${r.description}
579
+
580
+ ### Nodes
581
+ ${t}
582
+
583
+ ### Edges (flow)
584
+ ${e}
585
+
586
+ ## Output format
587
+
588
+ Return a JSON object with this exact structure:
589
+ {
590
+ "nodes": {
591
+ "<node_name>": {
592
+ "code": "// complete ESM module code as a string"
593
+ }
594
+ }
595
+ }
596
+
597
+ IMPORTANT: Return ONLY valid JSON. No markdown fences, no explanation outside the JSON.`}async function Wt(r,t){let e=await Jr(t),s=Lr(e);try{let{invokeAgent:n}=await import("@zibby/core"),i=Mr(r),o=await n(i,{state:{agentType:s,config:e,cwd:t,workspace:t}},{model:e?.agent?.[s]?.model||"auto",workspace:t,config:e,timeout:12e4}),c=(typeof o=="string"?o:o?.raw||JSON.stringify(o?.structured||o)).match(/\{[\s\S]*\}/);if(!c)throw new Error("Agent did not return valid JSON");return JSON.parse(c[0])}catch(n){return console.warn(`Agent code generation failed (${n.message}), using templates`),Ur(r)}}function Ur(r){let t={};for(let e of r.nodes){let s=V(e.name),n=`${e.name.split("_").map(c=>c.charAt(0).toUpperCase()+c.slice(1)).join("")}OutputSchema`,i=e.outputFields?.length?e.outputFields.map(c=>` ${c}: z.string().describe('${c}'),`).join(`
598
+ `):` summary: z.string().describe('Summary of the result'),
599
+ status: z.enum(['ok', 'warn', 'error']).describe('Overall status'),`,o=r.edges.filter(c=>c.to===e.name&&c.from!=="START").map(c=>`Previous step (${c.from}): \${JSON.stringify(state.${c.from} || {}, null, 2)}`).join(`
600
+ `),a=o?`${e.description}
601
+
602
+ Input:
603
+ \${JSON.stringify(state.input || {}, null, 2)}
604
+
605
+ ${o}`:`${e.description}
606
+
607
+ Input:
608
+ \${JSON.stringify(state.input || {}, null, 2)}`;t[e.name]={code:`import { z } from '@zibby/core';
609
+
610
+ const ${n} = z.object({
611
+ ${i}
612
+ });
613
+
614
+ export const ${s} = {
615
+ name: '${e.name}',
616
+ prompt: (state) => \`${a}\`,
617
+ outputSchema: ${n},
618
+ };
619
+ `}}return{nodes:t}}function qr(r,t,e,s){let n=t.toLowerCase(),i=Pt(n),o=S(r,".zibby","workflows",n),a=S(o,"nodes");Ar(a,{recursive:!0});let c=e.nodes.map(g=>g.name);for(let g of e.nodes){let _=s.nodes?.[g.name]?.code;_&&H(S(a,`${g.name.replace(/_/g,"-")}.mjs`),_,"utf-8")}let l=c.map(g=>{let _=V(g),d=g.replace(/_/g,"-");return`export { ${_} } from './${d}.mjs';`});H(S(a,"index.mjs"),`${l.join(`
620
+ `)}
621
+ `,"utf-8");let u=c[0],p=c.map(g=>V(g)).join(", "),y=c.map(g=>` graph.addNode('${g}', ${V(g)});`).join(`
622
+ `),m=e.edges.map(g=>g.condition?` graph.addConditionalEdges('${g.from}', (state) => {
623
+ ${g.condition}
624
+ });`:` graph.addEdge('${g.from}', '${g.to}');`).join(`
625
+ `),f=`import { WorkflowAgent, WorkflowGraph } from '@zibby/core';
626
+ import { ${p} } from './nodes/index.mjs';
627
+
628
+ export class ${i} extends WorkflowAgent {
629
+ buildGraph() {
630
+ const graph = new WorkflowGraph();
631
+
632
+ ${y}
633
+
634
+ graph.setEntryPoint('${u}');
635
+ ${m}
636
+
637
+ return graph;
638
+ }
639
+
640
+ async onComplete(result) {
641
+ console.log(\`[${n}] workflow complete \u2014 success: \${result.success !== false}\`);
642
+ }
643
+ }
644
+ `;H(S(o,"graph.mjs"),f,"utf-8");let k={name:n,description:e.description||`${i} workflow`,entryClass:i,triggers:{api:!0}};H(S(o,"workflow.json"),`${JSON.stringify(k,null,2)}
645
+ `,"utf-8");let h=["graph.mjs","workflow.json","nodes/index.mjs",...c.map(g=>`nodes/${g.replace(/_/g,"-")}.mjs`)];return{workflowDir:Ut(r,o),files:h,className:i,slug:n}}async function Kr(r){let{name:t,description:e,nodes:s,edges:n}=r;if(!t||!Kt.test(t.toLowerCase()))return JSON.stringify({error:`Invalid workflow name "${t}". Must be kebab-case, 2-64 chars, lowercase letters/numbers/hyphens.`});if(!s||s.length===0)return JSON.stringify({error:"At least one node is required."});let i={name:t.toLowerCase(),description:e||`${Pt(t.toLowerCase())} workflow`,nodes:s.map(o=>({name:o.name.replace(/-/g,"_"),description:o.description||`Process ${o.name}`,inputFields:o.inputFields||[],outputFields:o.outputFields||[]})),edges:n||[]};if(i.edges.length===0&&i.nodes.length>0){for(let o=0;o<i.nodes.length-1;o++)i.edges.push({from:i.nodes[o].name,to:i.nodes[o+1].name});i.edges.push({from:i.nodes[i.nodes.length-1].name,to:"END"})}return JSON.stringify({ok:!0,spec:i,message:`Workflow "${i.name}" designed with ${i.nodes.length} node(s). Call build_workflow to generate the code.`,preview:{nodes:i.nodes.map(o=>o.name),flow:i.edges.map(o=>o.condition?`${o.from} \u2192(if ${o.condition})\u2192 ${o.to}`:`${o.from} \u2192 ${o.to}`)}})}async function Pr(r,t){let{name:e,spec:s}=r,n=(e||s?.name||"").toLowerCase();if(!n||!Kt.test(n))return JSON.stringify({error:`Invalid workflow name "${n}".`});if(!s||!s.nodes||s.nodes.length===0)return JSON.stringify({error:"spec with nodes is required. Call design_workflow first."});let i=S(t,".zibby","workflows",n);if(M(i))return JSON.stringify({error:`Workflow "${n}" already exists at .zibby/workflows/${n}/. Delete it first or choose a different name.`});let o=await Wt(s,t),a=qr(t,n,s,o);return JSON.stringify({ok:!0,...a,message:`Workflow "${n}" created at ${a.workflowDir}/`,nextSteps:[`Test locally: zibby start ${n}`,`Deploy to cloud: zibby deploy ${n} --project <project-id>`,`Tail logs: zibby logs --workflow ${n} --project <project-id>`]})}async function Br(r,t){let{workflowName:e,nodeName:s,description:n,inputFields:i,outputFields:o}=r,a=(e||"").toLowerCase(),c=(s||"").replace(/-/g,"_"),l=S(t,".zibby","workflows",a);if(!M(l))return JSON.stringify({error:`Workflow "${a}" not found. Create it first with build_workflow.`});let u={name:a,description:"",nodes:[{name:c,description:n||`Process ${c}`,inputFields:i||[],outputFields:o||[]}],edges:[]},y=(await Wt(u,t)).nodes?.[c]?.code;if(!y)return JSON.stringify({error:"Failed to generate node code."});let m=S(l,"nodes"),f=`${c.replace(/_/g,"-")}.mjs`;H(S(m,f),y,"utf-8");let k=S(m,"index.mjs"),h=V(c),g=`export { ${h} } from './${c.replace(/_/g,"-")}.mjs';
646
+ `,_=M(k)?Q(k,"utf-8"):"";return _.includes(h)||H(k,_+g,"utf-8"),JSON.stringify({ok:!0,file:`nodes/${f}`,exportName:h,message:`Node "${c}" added. Update graph.mjs to wire it into the graph.`})}async function Fr(r,t){let{name:e,projectId:s}=r,n=(e||"").toLowerCase();if(!n)return JSON.stringify({error:"Workflow name is required."});if(!s)return JSON.stringify({error:"projectId is required."});let i=S(t,".zibby","workflows",n);if(!M(i))return JSON.stringify({error:`Workflow "${n}" not found at .zibby/workflows/${n}/`});try{let{execSync:o}=await import("child_process"),a=o(`node "${S(t,"packages/cli/bin/zibby.js")}" deploy ${n} --project ${s}`,{cwd:t,encoding:"utf-8",timeout:3e4,stdio:["pipe","pipe","pipe"]});return JSON.stringify({ok:!0,output:a.trim()})}catch{try{let{execSync:a}=await import("child_process"),c=a(`npx zibby deploy ${n} --project ${s}`,{cwd:t,encoding:"utf-8",timeout:3e4,stdio:["pipe","pipe","pipe"]});return JSON.stringify({ok:!0,output:c.trim()})}catch(a){return JSON.stringify({error:`Deploy failed: ${a.message}`})}}}function Wr(r){let t=S(r,".zibby","workflows");if(!M(t))return JSON.stringify({workflows:[],message:"No workflows found. Use build_workflow to create one."});let s=Ie(t).filter(n=>{try{return Mt(S(t,n)).isDirectory()}catch{return!1}}).map(n=>{let i=S(t,n,"workflow.json"),o={};try{o=JSON.parse(Q(i,"utf-8"))}catch{}let a=S(t,n,"nodes"),c=0;try{c=Ie(a).filter(l=>l.endsWith(".mjs")&&l!=="index.mjs").length}catch{}return{name:n,description:o.description||"",nodeCount:c,path:Ut(r,S(t,n))}});return JSON.stringify({workflows:s})}var zt={id:"workflow-builder",description:"Build, scaffold, and deploy custom AI workflows via conversation",envKeys:[],promptFragment:xr,tools:[{name:"design_workflow",description:"Design a workflow spec (nodes, edges, descriptions) for the user to review before building. Call this after understanding requirements.",input_schema:{type:"object",properties:{name:{type:"string",description:"Workflow name in kebab-case (e.g., ticket-triage)"},description:{type:"string",description:"What the workflow does"},nodes:{type:"array",items:{type:"object",properties:{name:{type:"string",description:"Node name in snake_case (e.g., classify_ticket)"},description:{type:"string",description:"What this node does \u2014 be specific about input/output"},inputFields:{type:"array",items:{type:"string"},description:"Key fields this node reads from state"},outputFields:{type:"array",items:{type:"string"},description:"Key fields this node produces"}},required:["name","description"]},description:"Workflow nodes (processing steps)"},edges:{type:"array",items:{type:"object",properties:{from:{type:"string",description:"Source node name"},to:{type:"string",description:'Target node name (or "END")'},condition:{type:"string",description:"JS expression for conditional routing (optional)"}},required:["from","to"]},description:"Edges connecting nodes. If omitted, nodes are wired linearly."}},required:["name","description","nodes"]}},{name:"build_workflow",description:"Generate real workflow code on disk from a design spec. Uses the configured AI agent for high-quality code generation.",input_schema:{type:"object",properties:{name:{type:"string",description:"Workflow name (from design_workflow)"},spec:{type:"object",description:"The full spec object returned by design_workflow",properties:{name:{type:"string"},description:{type:"string"},nodes:{type:"array",items:{type:"object"}},edges:{type:"array",items:{type:"object"}}}}},required:["name","spec"]}},{name:"add_node",description:"Add a new node to an existing workflow. Generates the node file and updates the barrel export.",input_schema:{type:"object",properties:{workflowName:{type:"string",description:"Existing workflow name (kebab-case)"},nodeName:{type:"string",description:"New node name (snake_case)"},description:{type:"string",description:"What this node does"},inputFields:{type:"array",items:{type:"string"},description:"Fields read from state"},outputFields:{type:"array",items:{type:"string"},description:"Fields produced"}},required:["workflowName","nodeName","description"]}},{name:"deploy_workflow",description:"Deploy a workflow to Zibby Cloud. Returns the trigger URL.",input_schema:{type:"object",properties:{name:{type:"string",description:"Workflow name to deploy"},projectId:{type:"string",description:"Target project ID"}},required:["name","projectId"]}},{name:"list_workflows",description:"List all local workflows in .zibby/workflows/.",input_schema:{type:"object",properties:{}}},{name:"explore_framework_docs",description:"Read Zibby framework documentation on demand. Call this before building complex workflows or when you need details on advanced patterns (middleware, conditional routing, skills, deployment, CLI commands).",input_schema:{type:"object",properties:{topic:{type:"string",description:'Doc topic to read (e.g., "workflow", "custom-workflows", "cli-reference", "packages/core", "packages/skills", "integrations/jira"). Call with no topic to list all available docs.'}}}}],async handleToolCall(r,t,e){let s=e?.options?.workspace||process.cwd();try{switch(r){case"design_workflow":return await Kr(t);case"build_workflow":return await Pr(t,s);case"add_node":return await Br(t,s);case"deploy_workflow":return await Fr(t,s);case"list_workflows":return Wr(s);case"explore_framework_docs":{let n=(t.topic||"").trim();if(!n){let o=Dt();return JSON.stringify({available:o,hint:"Call again with a topic to read its content."})}let i=Ft(n);if(!i){let o=Dt();return JSON.stringify({error:`Doc "${n}" not found.`,available:o})}return JSON.stringify({topic:n,content:i})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(n){return JSON.stringify({error:n.message})}},resolve(){return null}};import{createRequire as zr}from"module";import{fileURLToPath as Gr}from"url";import{registerHandlers as Hr}from"@zibby/core/framework/function-skill-registry.js";import{registerSkill as Yr}from"@zibby/workflow";var Zr=zr(import.meta.url);function Vr(){try{return Zr.resolve("@zibby/core/framework/function-bridge.js")}catch{return null}}var Qr=import.meta.url;function Xr(){let r=Error.prepareStackTrace;try{Error.prepareStackTrace=(s,n)=>n;let e=new Error().stack;for(let s=2;s<e.length;s++){let n=e[s].getFileName();if(n&&n!==Qr&&!n.startsWith("node:"))return n.startsWith("file://")?Gr(n):n}return null}finally{Error.prepareStackTrace=r}}function en(r){if(!r||typeof r!="object")return{type:"object",properties:{},required:[]};let t={},e=[];for(let[s,n]of Object.entries(r))if(typeof n=="string")t[s]={type:n},e.push(s);else{let{required:i,...o}=n;t[s]=o,i!==!1&&e.push(s)}return{type:"object",properties:t,required:e}}function tn(r,t,e){if(typeof e.handler!="function")throw new Error(`Skill "${r}" must have a handler function`);let s={[r]:e.handler},n=[{name:r,description:e.description||"",input_schema:en(e.input)}];return Hr(r,s,n),{id:r,type:"function",serverName:r,allowedTools:[`mcp__${r}__*`],description:e.description||`Function skill: ${r}`,envKeys:[],tools:n,resolve(){let i=Vr();return i?{command:"node",args:[i,t,r]}:null}}}function sn(r,t){return{id:r,type:"mcp",serverName:t.serverName||r,allowedTools:t.allowedTools||[`mcp__${t.serverName||r}__*`],description:t.description||`MCP skill: ${r}`,envKeys:t.envKeys||[],tools:t.tools||[],resolve:t.resolve,...t.cursorKey&&{cursorKey:t.cursorKey},...t.sessionEnvKey&&{sessionEnvKey:t.sessionEnvKey}}}function Gt(r,t){let e;if("handler"in t){if(typeof t.handler!="function")throw new Error(`Skill "${r}" must have a handler function`);let s=Xr();if(!s)throw new Error(`Could not resolve caller file for skill "${r}".`);e=tn(r,s,t)}else if(typeof t.resolve=="function")e=sn(r,t);else throw new Error(`Skill "${r}" must have either a handler (function skill) or resolve (MCP skill).`);return Yr(e),e}var rn=Gt;import{registerSkill as bi,getSkill as Si,hasSkill as vi,getAllSkills as Ni,listSkillIds as Oi}from"@zibby/workflow";E(xe);E(De);E(Ue);E(fe);E(Ve);E(Ke);E(dt);E(ht);E(Be);E(He);E(Et);E(zt);E({...fe,id:"slack_notify"});var hi={BROWSER:"browser",JIRA:"jira",GITHUB:"github",GIT:"git",SLACK:"slack",SENTRY:"sentry",MEMORY:"memory",RUNNER:"runner",SKILL_INSTALLER:"skill-installer",CORE_TOOLS:"core-tools",CHAT_MEMORY:"chat-memory",WORKFLOW_BUILDER:"workflow-builder"};export{hi as SKILLS,xe as browserSkill,Et as chatMemorySkill,He as coreToolsSkill,rn as functionSkill,Ni as getAllSkills,Si as getSkill,ht as gitSkill,Ue as githubSkill,vi as hasSkill,De as jiraSkill,Oi as listSkillIds,Ke as memorySkill,bi as registerSkill,dt as runnerSkill,Ve as sentrySkill,Gt as skill,Be as skillInstallerSkill,fe as slackSkill,dt as testRunnerSkill,zt as workflowBuilderSkill};