@zibby/skills 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.js +3 -0
- package/dist/chat-memory.js +82 -0
- package/dist/core-tools.js +2 -0
- package/dist/function-skill.js +1 -0
- package/dist/git.js +12 -0
- package/dist/github.js +34 -0
- package/dist/index.js +1 -0
- package/dist/jira.js +69 -0
- package/dist/memory.js +24 -0
- package/dist/package.json +56 -0
- package/dist/sentry.js +5 -0
- package/dist/skill-installer.js +14 -0
- package/dist/slack.js +5 -0
- package/dist/test-runner.js +168 -0
- package/package.json +15 -12
- package/src/browser.js +0 -64
- package/src/function-skill.js +0 -160
- package/src/github.js +0 -54
- package/src/index.js +0 -35
- package/src/jira.js +0 -110
- package/src/memory.js +0 -140
- package/src/slack.js +0 -112
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import{spawn as M}from"child_process";import{writeFileSync as oe,mkdirSync as q,existsSync as I,readdirSync as $,readFileSync as E,unlinkSync as ae,createWriteStream as ce,statSync as le}from"fs";import{resolve as C,join as O}from"path";import{resolveMaxParallelRuns as G}from"@zibby/core/utils/parallel-config.js";import{zibbyScratchSpecsDir as ue}from"@zibby/core/constants/zibby-scratch.js";const L="sessions",U=".zibby/output",J=process.env.ZIBBY_RUNNER_NODE_PROGRESS==="1",pe=process.env.ZIBBY_RUNNER_STATUS_STREAM==="1",F=process.env.ZIBBY_RUNNER_SPAWN_LOGS==="1",w=new Map,T=[];let de=0,K=0;const Y=3e3;function z(){return`run_${++de}_${Date.now().toString(36)}`}function B(s){const n=Math.floor(s/1e3);return n<60?`${n}s`:`${Math.floor(n/60)}m ${n%60}s`}function V(s){return s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g,"")}function N(s,n,e){if(!pe)return;const t=`
|
|
2
|
+
${n} [${s}] ${e}
|
|
3
|
+
`;try{process.stderr.write(t)}catch{}}function j(){T.length=0;for(const[,s]of w)if(s.status==="queued"&&(s.status="cancelled"),s.status==="running"&&s._child)try{s._child.kill("SIGTERM")}catch{}}process.on("exit",j),process.on("SIGINT",()=>{j(),process.exit(0)}),process.on("SIGTERM",()=>{j(),process.exit(0)});const $e={id:"runner",description:"Run zibby test workflows from chat (parallel supported)",envKeys:[],promptFragment:`## Test Runner
|
|
4
|
+
You can run zibby test workflows directly from chat:
|
|
5
|
+
|
|
6
|
+
**CRITICAL: When user asks to test a ticket:**
|
|
7
|
+
1. Load the issue with your tracker tools (whatever is connected)
|
|
8
|
+
2. Check comments/description for test steps
|
|
9
|
+
3. IF steps found \u2192 Use inline format: run_test({ spec: "inline:<steps>", ticketKey: "KEY" })
|
|
10
|
+
4. IF NO steps AND local codebase \u2192 Use run_generate
|
|
11
|
+
5. Tell the user tests are running \u2014 they can ask for progress anytime
|
|
12
|
+
|
|
13
|
+
**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.
|
|
14
|
+
|
|
15
|
+
Tools:
|
|
16
|
+
- **run_test**: spec = file path, inline:+steps, or Jira KEY-123 (auto-loads issue when jira skill is on).
|
|
17
|
+
- **run_status**: Instant progress check (runId or "all")
|
|
18
|
+
- **run_generate**: Generate specs from codebase. ONLY use if NO steps in ticket AND local codebase exists.
|
|
19
|
+
- **run_artifacts**: Read test results/logs after completion
|
|
20
|
+
- **run_diagnose**: Diagnose failures
|
|
21
|
+
- **run_cancel**: Kill running test
|
|
22
|
+
- **list_specs**: List spec files
|
|
23
|
+
|
|
24
|
+
### MANDATORY: When User Asks to Test a Ticket
|
|
25
|
+
|
|
26
|
+
1. **ALWAYS load ticket first** using Jira/issue tools (jira_get_issue, jira_get_comments)
|
|
27
|
+
2. **Check comments AND description** for test steps
|
|
28
|
+
3. **If steps found** \u2192 IMMEDIATELY call run_test with inline format: run_test({ spec: "inline:Navigate to...\\nClick...\\nVerify...", ticketKey: "SCRUM-123" })
|
|
29
|
+
4. **DO NOT say "can't run"** until you've ACTUALLY fetched the ticket and confirmed NO steps exist
|
|
30
|
+
5. **If NO steps found** \u2192 Ask user to add steps OR use run_generate (only if local codebase)
|
|
31
|
+
|
|
32
|
+
### DECISION TREE: When User Says "Test This Ticket"
|
|
33
|
+
|
|
34
|
+
**STEP 1: Get full issue/ticket information**
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
**STEP 2: Check if testing steps exist**
|
|
38
|
+
Look for testing steps in:
|
|
39
|
+
- Comments (most common)
|
|
40
|
+
- Description field
|
|
41
|
+
- Keywords like "test steps", "testing steps", numbered lists (1. 2. 3.)
|
|
42
|
+
|
|
43
|
+
IF testing steps found:
|
|
44
|
+
\u2192 GOTO Workflow A (Use Existing Steps)
|
|
45
|
+
ELSE:
|
|
46
|
+
\u2192 GOTO Workflow B (Generate from Codebase)
|
|
47
|
+
|
|
48
|
+
### Workflow A: Use Existing Steps (NO CODEBASE NEEDED)
|
|
49
|
+
**MANDATORY when ticket has test steps in comments/description**
|
|
50
|
+
|
|
51
|
+
CRITICAL: If you see test steps in the ticket, DO NOT call run_generate. Use inline format instead.
|
|
52
|
+
|
|
53
|
+
Steps:
|
|
54
|
+
1. Extract steps from ticket (comments or description)
|
|
55
|
+
2. Format as inline spec: "inline:" + steps text
|
|
56
|
+
3. Call run_test({ spec: "inline:...", ticketKey: "SCRUM-123" })
|
|
57
|
+
4. Tell the user tests are running
|
|
58
|
+
5. Use run_status when they ask for progress
|
|
59
|
+
|
|
60
|
+
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" })
|
|
61
|
+
|
|
62
|
+
### Workflow B: Generate from Codebase (REQUIRES CODEBASE)
|
|
63
|
+
**ONLY use when ALL these are true:**
|
|
64
|
+
- \u274C NO testing steps in ticket comments/description
|
|
65
|
+
- \u2705 Local codebase exists (not external URL like heroku)
|
|
66
|
+
- \u2705 Ticket describes NEW functionality to test
|
|
67
|
+
|
|
68
|
+
**STOP AND USE WORKFLOW A IF:**
|
|
69
|
+
- Testing steps exist in ticket \u2192 NEVER call run_generate, use inline format
|
|
70
|
+
- Ticket mentions external app (heroku, cloud, demo sites) \u2192 Use inline with URL
|
|
71
|
+
- No local codebase \u2192 Ask user for steps
|
|
72
|
+
|
|
73
|
+
**BEFORE calling run_generate, ask yourself:**
|
|
74
|
+
- Did I check ALL comments? (not just description)
|
|
75
|
+
- Are there ANY step lists (numbered, bulleted)?
|
|
76
|
+
- Does ticket mention external URLs?
|
|
77
|
+
- If YES to any: DO NOT call run_generate!
|
|
78
|
+
|
|
79
|
+
**If checks pass:**
|
|
80
|
+
1. Call run_generate({ ticket: "SCRUM-123" })
|
|
81
|
+
- Spawns Claude/Cursor with file access
|
|
82
|
+
- Explores codebase (1-3 minutes)
|
|
83
|
+
- Returns generated spec file paths
|
|
84
|
+
2. For EACH file: run_test({ spec: "test-specs/...", ticketKey: "SCRUM-123" })
|
|
85
|
+
3. Tell the user tests are running
|
|
86
|
+
|
|
87
|
+
### Example Decision Process
|
|
88
|
+
(Illustration uses Jira-shaped keys/tools; substitute your session's issue integration.)
|
|
89
|
+
|
|
90
|
+
**User:** "Test SCRUM-408"
|
|
91
|
+
|
|
92
|
+
Step-by-step:
|
|
93
|
+
1. jira_get_issue({ issueKey: "SCRUM-408" }) - get summary, description
|
|
94
|
+
2. jira_get_comments({ issueKey: "SCRUM-408" }) - get comments
|
|
95
|
+
3. Check: Comment has "Testing steps: 1. Go to /checkboxes 2. Verify..."
|
|
96
|
+
4. Decision: Steps exist \u2192 Use Workflow A
|
|
97
|
+
5. run_test({
|
|
98
|
+
spec: "inline:Navigate to https://the-internet.herokuapp.com/checkboxes, verify two checkboxes, check first, uncheck second, verify states",
|
|
99
|
+
ticketKey: "SCRUM-408"
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
**User:** "Test SCRUM-999 (new feature)"
|
|
103
|
+
|
|
104
|
+
Step-by-step:
|
|
105
|
+
1. jira_get_issue({ issueKey: "SCRUM-999" })
|
|
106
|
+
2. jira_get_comments({ issueKey: "SCRUM-999" })
|
|
107
|
+
3. Check: No testing steps found in ticket
|
|
108
|
+
4. Check: Codebase exists (package.json, src/ in current dir)
|
|
109
|
+
5. Decision: No steps + codebase \u2192 Use Workflow B
|
|
110
|
+
6. run_generate({ ticket: "SCRUM-999" })
|
|
111
|
+
7. Wait for spec files...
|
|
112
|
+
8. run_test for each file
|
|
113
|
+
9. Tell the user tests are running
|
|
114
|
+
|
|
115
|
+
### After Starting Runs
|
|
116
|
+
- run_test starts async work \u2014 tell the user tests are running.
|
|
117
|
+
- Use run_status to check progress when asked.
|
|
118
|
+
- To poll for completion, use the general wait tool (e.g. wait 20s) then run_status. Repeat until done.
|
|
119
|
+
- If any run failed/error/cancelled and user asks "why", call run_diagnose.
|
|
120
|
+
|
|
121
|
+
\u26A0\uFE0F NEVER AUTO-CANCEL RUNS:
|
|
122
|
+
- Tests take 1-5 minutes to complete. A "running" status is NORMAL \u2014 it does NOT mean stuck.
|
|
123
|
+
- NEVER call run_cancel unless the USER explicitly asks you to cancel/stop.
|
|
124
|
+
- If a run is still "running" after polling, just TELL the user it's still in progress and WAIT.
|
|
125
|
+
- The workflow has multiple nodes (preflight \u2192 execute_live \u2192 generate_script). Each takes time. This is expected.
|
|
126
|
+
- DO NOT interpret "running" as "stuck". DO NOT cancel on your own.
|
|
127
|
+
|
|
128
|
+
### Parallelism
|
|
129
|
+
- Each test CASE should be its own run_test call
|
|
130
|
+
- Runs auto-queue past parallel.maxConcurrentRuns in .zibby.config.mjs (default 8; caps Studio Mission Control lanes too)
|
|
131
|
+
- 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).
|
|
132
|
+
|
|
133
|
+
### Artifacts
|
|
134
|
+
Each run generates:
|
|
135
|
+
- result.json: Pass/fail verdict
|
|
136
|
+
- recording.webm: Video of session
|
|
137
|
+
- events.json: All browser events
|
|
138
|
+
- raw_stream_output.txt: Agent log
|
|
139
|
+
|
|
140
|
+
Use run_artifacts({ runId, type }) and run_diagnose({ runId }) to inspect and explain failures.`,resolve(){return null},async handleToolCall(s,n,e){const t=e?.options?.workspace||process.cwd();try{switch(s){case"run_generate":return await fe(n,t);case"run_test":return await Se(n,t,e);case"run_status":return _e(n);case"run_cancel":return Ne(n);case"run_artifacts":return Oe(n,t);case"run_diagnose":return xe(n,t);case"list_specs":return Ie(n,t);default:return JSON.stringify({error:`Unknown tool: ${s}`})}}catch(i){return JSON.stringify({error:i.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 W(){let s=0;for(const[,n]of w)n.status==="running"&&s++;return s}function H(){for(;T.length>0;){const s=G(T[0]?.context?.options?.config);if(W()>=s)break;const{args:n,cwd:e,context:t}=T.shift();X(n,e,t)}}async function fe(s,n){const{ticket:e,description:t,input:i,repo:o,agent:d,output:u}=s,r=["generate"];e&&r.push("--ticket",e),t&&r.push("--description",t),i&&r.push("--input",i),o&&r.push("--repo",o),u&&r.push("--output",u);const l=["assistant","cursor","claude","codex","gemini"],c=d||process.env.AGENT_TYPE,p=c&&l.includes(c)?c:null;p&&r.push("--agent",p);const g=e||"generate";return N(g,"\u{1F9EA}","Starting test spec generation (real agent with codebase access)..."),new Promise(m=>{F&&console.error(`[zibby:spawn] skill=run_generate parentPid=${process.pid} \u2192 child zibby ${r.map(f=>/\s/.test(f)?JSON.stringify(f):f).join(" ")} cwd=${n}`);const b=M("zibby",r,{cwd:n,env:{...process.env},stdio:["ignore","pipe","pipe"],detached:!1});let v="",R="";b.stdout.on("data",f=>{const _=f.toString();v+=_;for(const a of _.split(`
|
|
141
|
+
`)){const y=V(a).trim();y.startsWith("\u2705")?N(g,"\u2705",y.slice(2).trim()):y.startsWith("\u2713")&&N(g,"\u2714",y.slice(2).trim())}}),b.stderr.on("data",f=>{R+=f.toString()}),b.on("close",f=>{if(f!==0){N(g,"\u274C",`Generation failed (exit ${f})`),m(JSON.stringify({error:`zibby generate failed with exit code ${f}`,stderr:R.slice(-1e3)}));return}const _=C(n,u||"test-specs");let a=[];try{const y=e?e.toLowerCase().replace(/[^a-z0-9]+/g,"-"):"";a=$(_).filter(x=>x.endsWith(".txt")&&(!y||x.startsWith(y))).map(x=>O(_,x))}catch{}N(g,"\u2705",`Generated ${a.length} test spec files`),m(JSON.stringify({success:!0,ticketKey:e||null,specFiles:a.map(y=>y.replace(`${n}/`,"")),total:a.length,message:`Generated ${a.length} specs. Now call run_test for each file.`}))}),b.on("error",f=>{N(g,"\u274C",`Spawn error: ${f.message}`),m(JSON.stringify({error:f.message}))})})}const Z=1e5,Q=/^[A-Z][A-Z0-9]+-\d+$/,ge=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function he(s,n){if(!n||!n.length)return s;let e=s;for(const t of n)t.type==="strong"?e=`**${e}**`:t.type==="em"?e=`_${e}_`:t.type==="code"?e=`\`${e}\``:t.type==="strike"?e=`~~${e}~~`:t.type==="link"&&t.attrs?.href&&(e=`[${e}](${t.attrs.href})`);return e}function D(s,n=0){if(!Array.isArray(s))return"";const e=[];for(const t of s){if(t.type==="text"){e.push(he(t.text||"",t.marks));continue}if(t.type==="hardBreak"){e.push(`
|
|
142
|
+
`);continue}if(t.type==="rule"){e.push(`
|
|
143
|
+
---
|
|
144
|
+
`);continue}const i=t.content?D(t.content,n+1):"";if(t.type==="listItem")e.push(i);else if(t.type==="bulletList"){const o=(t.content||[]).map(d=>`- ${D(d.content||[],n+1).trim()}`);e.push(`
|
|
145
|
+
${o.join(`
|
|
146
|
+
`)}
|
|
147
|
+
`)}else if(t.type==="orderedList"){const o=(t.content||[]).map((d,u)=>`${u+1}. ${D(d.content||[],n+1).trim()}`);e.push(`
|
|
148
|
+
${o.join(`
|
|
149
|
+
`)}
|
|
150
|
+
`)}else if(t.type==="heading"){const o=t.attrs?.level||2;e.push(`
|
|
151
|
+
|
|
152
|
+
${"#".repeat(o)} ${i.trim()}
|
|
153
|
+
|
|
154
|
+
`)}else ge.has(t.type)?e.push(`
|
|
155
|
+
|
|
156
|
+
${i}
|
|
157
|
+
`):e.push(i)}return e.join("").replace(/\n{3,}/g,`
|
|
158
|
+
|
|
159
|
+
`)}function me(s){return s==null||s===""?"":typeof s=="string"?s.trim():typeof s=="object"&&Array.isArray(s.content)?D(s.content).trim():""}async function ye(s){const{getSkill:n}=await import("@zibby/core/framework/skill-registry.js"),e=n("jira");if(!e||typeof e.handleToolCall!="function")return null;try{const t=await e.handleToolCall("jira_get_issue",{issueKey:s}),i=JSON.parse(t);if(i?.error)return null;const o=await e.handleToolCall("jira_get_comments",{issueKey:s,maxResults:50}),d=JSON.parse(o);if(d?.error)return null;const u=me(i.description),r=[];u&&r.push(u);const l=Array.isArray(d.comments)?d.comments:[];if(l.length>0){const p=l.map(g=>String(g.body||"").trim()).filter(Boolean).join(`
|
|
160
|
+
|
|
161
|
+
`);p&&r.push(p)}let c=r.join(`
|
|
162
|
+
|
|
163
|
+
`).trim();return c?(c.length>Z&&(c=`${c.slice(0,Z)}
|
|
164
|
+
|
|
165
|
+
...[truncated]`),{inlineSpec:`inline:${c}`,issueKey:s}):null}catch{return null}}function ke(s,n){try{const e=JSON.parse(s);return JSON.stringify({...e,...n})}catch{return s}}async function Se(s,n,e){const t={...s};let i=String(t.spec??"").trim();if(!i)return JSON.stringify({error:"spec is required"});let o=null;if(Q.test(i)&&!i.startsWith("inline:")){const c=await ye(i);c&&(i=c.inlineSpec,t.spec=i,String(t.ticketKey||"").trim()||(t.ticketKey=c.issueKey),o=c.issueKey)}const d=String(t.ticketKey||"").trim();if(d){for(const[c,p]of w.entries())if(p?.ticketKey===d&&!(p?.status!=="running"&&p?.status!=="queued"))return JSON.stringify({runId:c,ticketKey:d,status:p.status,reused:!0,message:`A run for ${d} is already ${p.status}. Reusing existing run instead of starting a duplicate.`})}if(!i.startsWith("inline:")){const c=C(n,i);if(!I(c))return Q.test(i)?JSON.stringify({error:`Invalid run_test spec: "${i}" 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:i},invalidExample:{spec:i,ticketKey:i}}):JSON.stringify({error:`Test spec not found: ${i}`,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.'})}const u=G(e?.options?.config);if(W()>=u){const c=z(),p=t.ticketKey||c,g={runId:c,spec:t.ticketKey?`${t.ticketKey}: ${t.spec}`:t.spec,ticketKey:t.ticketKey||null,status:"queued",startTime:Date.now(),exitCode:null,output:"",error:""};w.set(c,g),T.push({args:{...t,_queuedRunId:c},cwd:n,context:e}),N(p,"\u23F3",`Queued (${W()}/${u} running, ${T.length} queued)`);const m={runId:c,spec:g.spec,ticketKey:g.ticketKey,status:"queued",message:`Queued \u2014 will start when a slot opens (max ${u} concurrent).`};return o&&(m.resolvedFromJiraIssue=o,m.message+=` (spec built from Jira ${o})`),JSON.stringify(m)}const r=Date.now()-K;r<Y&&K>0&&await new Promise(c=>setTimeout(c,Y-r)),K=Date.now();const l=X(t,n,e);return o?ke(l,{resolvedFromJiraIssue:o,message:`Spec was loaded from Jira issue ${o} (description + comments).`}):l}function X(s,n,e){const{spec:t,ticketKey:i,agent:o,headless:d,workflow:u,_queuedRunId:r}=s,l=r||z();let c=t,p=!1;if(t.startsWith("inline:")){p=!0;const k=ue(n);q(k,{recursive:!0}),c=O(k,`${l}.txt`),oe(c,t.slice(7).trim(),"utf-8")}const g=C(n,".zibby","output","runs");q(g,{recursive:!0});const m=O(g,`${l}.log`),b=ce(m,{flags:"a"}),R=o&&["assistant","cursor","claude","codex","gemini"].includes(o)?o:null,f=["test",c];R&&f.push("--agent",R),d&&f.push("--headless"),u&&f.push("--workflow",u),F&&console.error(`[zibby:spawn] skill=run_test parentPid=${process.pid} \u2192 child zibby ${f.map(k=>/\s/.test(k)?JSON.stringify(k):k).join(" ")} cwd=${n}`);const _=M("zibby",f,{cwd:n,env:{...process.env,ZIBBY_WORKFLOW_GRAPH_LOG_MARKERS:"1"},stdio:["ignore","pipe","pipe"],detached:!1}),a={runId:l,spec:i?`${i}: ${t}`:t,ticketKey:i||null,specPath:c,logPath:m,isInline:p,pid:_.pid,status:"running",output:"",error:"",startTime:Date.now(),exitCode:null,currentNode:null,completedNodes:[]},y=i||l;let x="";function P(k){const h=V(k).trim();if(!h)return;if(h.startsWith("__WORKFLOW_GRAPH_LOG__")){try{const S=JSON.parse(h.slice(22));S.phase==="node_begin"?a.currentNode=S.node:S.phase==="node_end"&&(S.node&&!a.completedNodes.includes(S.node)&&a.completedNodes.push(S.node),a.currentNode===S.node&&(a.currentNode=null))}catch{}return}const A=h.match(/Session\s+(\S+)/);if(A&&!a.sessionId&&(a.sessionId=A[1],a.sessionPath=C(n,U,L,a.sessionId)),h.startsWith("\u250C ")||h.startsWith("\u250C ")){const S=h.slice(2).trim();a.currentNode=S,J&&N(y,"\u25B6",`${S}`)}else if(h.startsWith("\u2514 ")||h.startsWith("\u2514 ")){const S=h.slice(2).trim();S.startsWith("done")?(a.currentNode&&!a.completedNodes.includes(a.currentNode)&&a.completedNodes.push(a.currentNode),J&&N(y,"\u2714",`${a.currentNode||"node"} done ${S.replace("done","").trim()}`),a.currentNode=null):S.startsWith("failed")&&(J&&N(y,"\u2718",`${a.currentNode||"node"} failed ${S.replace("failed","").trim()}`),a.currentNode=null)}else h.includes("Workflow completed")&&(a.currentNode=null,J&&N(y,"\u2714",`Workflow completed (${B(Date.now()-a.startTime)})`))}function re(k){const h=k.toString();a.output+=h,b.write(h),a.output.length>5e4&&(a.output=a.output.slice(-3e4)),x+=h;const A=x.split(`
|
|
166
|
+
`);x=A.pop();for(const S of A)P(S)}return _.stdout.on("data",re),_.stderr.on("data",k=>{const h=k.toString();a.error+=h,b.write(h),a.error.length>2e4&&(a.error=a.error.slice(-1e4))}),_.on("close",k=>{a.status=k===0?"passed":"failed",a.exitCode=k,a.endTime=Date.now(),x&&P(x),b.end();const h=B(Date.now()-a.startTime);if(k===0?N(y,"\u2705",`Passed (${h})`):N(y,"\u274C",`Failed (${h})`),a.isInline)try{ae(a.specPath)}catch{}H()}),_.on("error",k=>{a.status="error",a.error+=`
|
|
167
|
+
Spawn error: ${k.message}`,N(y,"\u274C",`Spawn error: ${k.message}`),b.end(),H()}),a._child=_,w.set(l,a),JSON.stringify({runId:l,spec:a.spec,ticketKey:a.ticketKey,status:"running",pid:_.pid,logFile:m})}function ee(s){const n=Math.round(((s.endTime||Date.now())-s.startTime)/1e3),e=s.completedNodes||[],t=s.currentNode||null;if(s.status!=="running")return{elapsed:n,stage:s.status,completedNodes:e,currentNode:null};let i;return t?(i=`Actively executing node "${t}"`,e.length&&(i+=` (completed: ${e.join(", ")})`)):e.length?i=`Between nodes (completed: ${e.join(", ")})`:i="Starting up (initializing workflow)",i+=`. Elapsed: ${n}s. This is normal progress \u2014 do not cancel.`,{elapsed:n,stage:"running",currentNode:t,completedNodes:e,progress:i}}function _e(s){const{runId:n}=s;if(!n)return JSON.stringify({error:"runId is required"});if(n==="all"){const o=[...w.entries()].map(([c,p])=>{const g=ee(p),m={runId:c,spec:p.spec,ticketKey:p.ticketKey,status:p.status,elapsed:g.elapsed,exitCode:p.exitCode,sessionId:p.sessionId||null};return p.status==="running"?(m.currentNode=g.currentNode,m.completedNodes=g.completedNodes,m.progress=g.progress):m.outputTail=p.output.slice(-500),m}),d=o.filter(c=>c.status==="running").length,u=o.filter(c=>c.status==="passed").length,r=o.filter(c=>c.status==="failed").length,l={total:o.length,running:d,passed:u,failed:r,runs:o};return d>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)}const e=w.get(n);if(!e)return JSON.stringify({error:`Run not found: ${n}`});const t=ee(e),i={runId:n,spec:e.spec,ticketKey:e.ticketKey,status:e.status,elapsed:t.elapsed,exitCode:e.exitCode,sessionId:e.sessionId||null};return e.status==="running"?(i.currentNode=t.currentNode,i.completedNodes=t.completedNodes,i.progress=t.progress):(i.outputTail=e.output.slice(-1e3),i.errorTail=e.error.slice(-500)),e.status==="running"&&(i._hint="This run is actively progressing. Do NOT cancel, diagnose, or assume stuck. Just tell the user it is still running."),JSON.stringify(i)}function te(s,n){if(n.status==="queued"){const e=T.findIndex(t=>t.args._queuedRunId===s);return e>=0&&T.splice(e,1),n.status="cancelled",n.endTime=Date.now(),{ok:!0,runId:s,status:"cancelled"}}if(n.status!=="running")return{ok:!1,runId:s,error:`Run is not active (status: ${n.status})`};try{return n._child.kill("SIGTERM"),n.status="cancelled",n.endTime=Date.now(),{ok:!0,runId:s,status:"cancelled"}}catch(e){return{ok:!1,runId:s,error:`Failed to cancel: ${e.message}`}}}function Ne(s){const{runId:n}=s;if(!n)return JSON.stringify({error:"runId is required"});if(n==="all"){const t=[];for(const[i,o]of w.entries())(o.status==="running"||o.status==="queued")&&t.push(te(i,o));return t.length===0?JSON.stringify({ok:!0,message:"No active runs to cancel"}):JSON.stringify({ok:!0,cancelled:t.length,results:t})}const e=w.get(n);return JSON.stringify(e?te(n,e):{error:`Run not found: ${n}`})}function we(s,n){const e=w.get(s);if(e?.sessionPath&&I(e.sessionPath))return e.sessionPath;if(e?.sessionId){const t=C(n,U,L,e.sessionId);if(I(t))return t}return null}function se(s,n=""){const e=[];if(!I(s))return e;for(const t of $(s,{withFileTypes:!0})){const i=n?`${n}/${t.name}`:t.name;if(t.isDirectory())e.push(...se(O(s,t.name),i));else{const o=le(O(s,t.name));e.push({path:i,size:o.size})}}return e}function ne(s){if(!I(s))return null;try{return JSON.parse(E(s,"utf-8"))}catch{return null}}function ie(s,n=2e3){if(!s||!I(s))return"";try{return E(s,"utf-8").slice(-Math.max(200,Number(n)||2e3))}catch{return""}}function be({run:s,logTail:n,errorTail:e}){const i=`${n||""}
|
|
168
|
+
${e||""}`.toLowerCase(),o={runId:s?.runId||null,status:s?.status||null,exitCode:s?.exitCode??null,likelyCause:"Unknown failure",confidence:"low",nextStep:'Call run_artifacts({ runId, type: "log" }) with larger tail and inspect full logs.'};return s?.status==="running"||s?.status==="queued"?{...o,likelyCause:"Run is still active; no terminal failure to diagnose yet.",confidence:"high",nextStep:'Call run_status({ runId: "all" }) to check progress.'}:i.includes("test spec not found")?{...o,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."}:i.includes("unknown command")&&i.includes("'run'")?{...o,likelyCause:"CLI command mismatch (`zibby run` unsupported in current CLI).",confidence:"high",nextStep:"Use `zibby test ...` spawn path (runner should already do this)."}:i.includes("missing openai_api_key")||i.includes("didn't provide an api key")||i.includes("401")?{...o,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."}:i.includes("spawn error")||i.includes("enoent")?{...o,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."}:i.includes("security command failed")||i.includes("security process exited with code: 45")||i.includes("password not found for account")?{...o,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" }).'}:o}function Oe(s,n){const{runId:e,type:t,node:i="execute_live",query:o,tail:d=3e3}=s;if(t==="search"){if(!o)return JSON.stringify({error:'query is required for type="search"'});const r=C(n,U,L);if(!I(r))return JSON.stringify({matches:[],message:"No sessions found"});const l=[],c=o.toLowerCase();for(const p of $(r,{withFileTypes:!0})){if(!p.isDirectory())continue;const g=O(r,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(const{file:b,label:v}of m){const R=O(g,b);if(I(R))try{const f=E(R,"utf-8");if(f.toLowerCase().includes(c)){const _=f.toLowerCase().indexOf(c),a=Math.max(0,_-100),y=Math.min(f.length,_+o.length+100);l.push({sessionId:p.name,artifact:v,snippet:f.slice(a,y)})}}catch{}}if(l.length>=20)break}return JSON.stringify({query:o,matches:l,total:l.length})}if(!e)return JSON.stringify({error:"runId is required for this type"});if(t==="log"){const r=w.get(e),l=ie(r?.logPath,d);if(l)return JSON.stringify({runId:e,source:"run-log",totalLength:l.length,tail:l})}const u=we(e,n);if(!u)return JSON.stringify({error:`No session found for run ${e}. The run may still be starting.`});switch(t){case"list":{const r=se(u);return JSON.stringify({sessionId:u.split("/").pop(),files:r,total:r.length})}case"result":{const r=ne(O(u,i,"result.json"));return JSON.stringify(r?{sessionId:u.split("/").pop(),node:i,result:r}:{error:`No result.json found in ${i}`})}case"events":{const r=ne(O(u,i,"events.json"));if(!r)return JSON.stringify({error:`No events.json found in ${i}`});const l=Array.isArray(r)?r:r.events||[];return JSON.stringify({sessionId:u.split("/").pop(),node:i,totalEvents:l.length,events:l.slice(-50)})}case"log":{const r=O(u,i,"raw_stream_output.txt");if(!I(r))return JSON.stringify({error:`No log found in ${i}`});const l=E(r,"utf-8");return JSON.stringify({sessionId:u.split("/").pop(),node:i,totalLength:l.length,tail:l.slice(-d)})}default:return JSON.stringify({error:`Unknown artifact type: ${t}. Use: list, result, events, log, search`})}}function xe(s,n){const e=String(s?.runId||"all"),t=Number(s?.tail||2e3),i=e==="all"?[...w.keys()]:[e];if(i.length===0)return JSON.stringify({error:"No runs available to diagnose. Call run_test first."});const o=i.map(r=>{const l=w.get(r);if(!l)return{runId:r,error:`Run not found: ${r}`};const c=ie(l.logPath,t),p=String(l.error||"").slice(-Math.max(200,t));return{...be({run:l,logTail:c,errorTail:p}),ticketKey:l.ticketKey||null,spec:l.spec,logTail:c,errorTail:p}}),d=o.filter(r=>r.status==="failed"||r.status==="error"),u=o.filter(r=>r.status==="running"||r.status==="queued");return JSON.stringify({total:o.length,failed:d.length,active:u.length,diagnoses:o})}function Ie(s,n){const e=s?.directory||"test-specs",t=C(n,e);if(!I(t))return JSON.stringify({specs:[],directory:e,message:`Directory not found: ${e}`});try{let o=function(d,u){for(const r of $(d,{withFileTypes:!0})){const l=u?`${u}/${r.name}`:r.name;r.isDirectory()?o(O(d,r.name),l):(r.name.endsWith(".txt")||r.name.endsWith(".md"))&&i.push(l)}};const i=[];return o(t,""),JSON.stringify({specs:i.map(d=>`${e}/${d}`),total:i.length,directory:e})}catch(i){return JSON.stringify({error:i.message})}}export{$e as testRunnerSkill};
|
package/package.json
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zibby/skills",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Built-in skill definitions for Zibby test automation framework",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
".": "./
|
|
9
|
-
"./browser": "./
|
|
10
|
-
"./jira": "./
|
|
11
|
-
"./github": "./
|
|
12
|
-
"./slack": "./
|
|
13
|
-
"./memory": "./
|
|
14
|
-
"./function": "./
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./browser": "./dist/browser.js",
|
|
10
|
+
"./jira": "./dist/jira.js",
|
|
11
|
+
"./github": "./dist/github.js",
|
|
12
|
+
"./slack": "./dist/slack.js",
|
|
13
|
+
"./memory": "./dist/memory.js",
|
|
14
|
+
"./function": "./dist/function-skill.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
+
"build": "node ../scripts/build.mjs",
|
|
17
18
|
"lint": "eslint .",
|
|
18
19
|
"lint:fix": "eslint --fix ."
|
|
19
20
|
},
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
"url": "https://github.com/ZibbyHQ/zibby-agent/issues"
|
|
36
37
|
},
|
|
37
38
|
"files": [
|
|
38
|
-
"
|
|
39
|
+
"dist/",
|
|
39
40
|
"README.md",
|
|
40
41
|
"LICENSE"
|
|
41
42
|
],
|
|
@@ -46,8 +47,10 @@
|
|
|
46
47
|
"@zibby/core": ">=0.1.0"
|
|
47
48
|
},
|
|
48
49
|
"optionalDependencies": {
|
|
49
|
-
"@zibby/mcp-
|
|
50
|
-
"@zibby/mcp-browser": "^0.1.5",
|
|
50
|
+
"@zibby/mcp-browser": "^0.1.6",
|
|
51
51
|
"@zibby/mcp-memory": "*"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"esbuild": "^0.28.0"
|
|
52
55
|
}
|
|
53
56
|
}
|
package/src/browser.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser Skill
|
|
3
|
-
*
|
|
4
|
-
* Provides Playwright-based browser automation via MCP.
|
|
5
|
-
* Resolves to @zibby/mcp-browser (if installed) or @playwright/mcp as fallback.
|
|
6
|
-
*
|
|
7
|
-
* Call resolve({ sessionPath, workspace }) to get a ready-to-use MCP server
|
|
8
|
-
* config with session-specific args (video dir, viewport, etc.).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { createRequire } from 'module';
|
|
12
|
-
|
|
13
|
-
const _require = createRequire(import.meta.url);
|
|
14
|
-
|
|
15
|
-
function resolveBrowserBin() {
|
|
16
|
-
if (process.env.MCP_BROWSER_PATH) return process.env.MCP_BROWSER_PATH;
|
|
17
|
-
try {
|
|
18
|
-
return _require.resolve('@zibby/mcp-browser/bin/mcp-browser-zibby.js');
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const VIDEO_RESOLUTION = '1280x720';
|
|
25
|
-
const VIEWPORT_SIZE = '1280x720';
|
|
26
|
-
|
|
27
|
-
export const browserSkill = {
|
|
28
|
-
id: 'browser',
|
|
29
|
-
serverName: 'playwright',
|
|
30
|
-
cursorKey: 'playwright-official',
|
|
31
|
-
allowedTools: ['mcp__playwright__*'],
|
|
32
|
-
sessionEnvKey: 'ZIBBY_SESSION_INFO',
|
|
33
|
-
description: 'Playwright Browser MCP Server',
|
|
34
|
-
envKeys: [],
|
|
35
|
-
tools: [],
|
|
36
|
-
|
|
37
|
-
resolve({ sessionPath, workspace } = {}) {
|
|
38
|
-
const bin = resolveBrowserBin();
|
|
39
|
-
const outputDir = sessionPath || workspace || 'test-results';
|
|
40
|
-
|
|
41
|
-
if (bin) {
|
|
42
|
-
return {
|
|
43
|
-
command: 'node',
|
|
44
|
-
args: [
|
|
45
|
-
bin,
|
|
46
|
-
`--save-video=${VIDEO_RESOLUTION}`,
|
|
47
|
-
`--viewport-size=${VIEWPORT_SIZE}`,
|
|
48
|
-
`--output-dir=${outputDir}`,
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
command: 'npx',
|
|
55
|
-
args: [
|
|
56
|
-
'-y',
|
|
57
|
-
'@playwright/mcp',
|
|
58
|
-
`--save-video=${VIDEO_RESOLUTION}`,
|
|
59
|
-
`--viewport-size=${VIEWPORT_SIZE}`,
|
|
60
|
-
'--output-dir', outputDir,
|
|
61
|
-
],
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
};
|
package/src/function-skill.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified Skill Factory
|
|
3
|
-
*
|
|
4
|
-
* Function skill (one skill = one tool, flat):
|
|
5
|
-
*
|
|
6
|
-
* import { skill } from '@zibby/skills';
|
|
7
|
-
*
|
|
8
|
-
* export const add = skill('add', {
|
|
9
|
-
* description: 'Add two numbers',
|
|
10
|
-
* input: { a: 'number', b: 'number' },
|
|
11
|
-
* handler: async ({ a, b }) => ({ result: a + b })
|
|
12
|
-
* });
|
|
13
|
-
*
|
|
14
|
-
* MCP skill:
|
|
15
|
-
*
|
|
16
|
-
* import { skill } from '@zibby/skills';
|
|
17
|
-
*
|
|
18
|
-
* export const linear = skill('linear', {
|
|
19
|
-
* resolve() {
|
|
20
|
-
* if (!process.env.LINEAR_API_KEY) return null;
|
|
21
|
-
* return { command: 'npx', args: ['-y', '@linear/mcp-server'],
|
|
22
|
-
* env: { LINEAR_API_KEY: process.env.LINEAR_API_KEY } };
|
|
23
|
-
* }
|
|
24
|
-
* });
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { createRequire } from 'module';
|
|
28
|
-
import { fileURLToPath } from 'url';
|
|
29
|
-
import { registerHandlers } from '@zibby/core/framework/function-skill-registry.js';
|
|
30
|
-
import { registerSkill } from '@zibby/core/framework/skill-registry.js';
|
|
31
|
-
|
|
32
|
-
const _require = createRequire(import.meta.url);
|
|
33
|
-
|
|
34
|
-
function resolveBridgePath() {
|
|
35
|
-
try {
|
|
36
|
-
return _require.resolve('@zibby/core/framework/function-bridge.js');
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const _selfUrl = import.meta.url;
|
|
43
|
-
|
|
44
|
-
function getCallerFile() {
|
|
45
|
-
const original = Error.prepareStackTrace;
|
|
46
|
-
try {
|
|
47
|
-
Error.prepareStackTrace = (_, stack) => stack;
|
|
48
|
-
const err = new Error();
|
|
49
|
-
const stack = err.stack;
|
|
50
|
-
for (let i = 2; i < stack.length; i++) {
|
|
51
|
-
const file = stack[i].getFileName();
|
|
52
|
-
if (file && file !== _selfUrl && !file.startsWith('node:')) {
|
|
53
|
-
return file.startsWith('file://') ? fileURLToPath(file) : file;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
} finally {
|
|
58
|
-
Error.prepareStackTrace = original;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function buildInputSchema(input) {
|
|
63
|
-
if (!input || typeof input !== 'object') {
|
|
64
|
-
return { type: 'object', properties: {}, required: [] };
|
|
65
|
-
}
|
|
66
|
-
const properties = {};
|
|
67
|
-
const required = [];
|
|
68
|
-
for (const [key, def] of Object.entries(input)) {
|
|
69
|
-
if (typeof def === 'string') {
|
|
70
|
-
properties[key] = { type: def };
|
|
71
|
-
required.push(key);
|
|
72
|
-
} else {
|
|
73
|
-
const { required: isRequired, ...rest } = def;
|
|
74
|
-
properties[key] = rest;
|
|
75
|
-
if (isRequired !== false) required.push(key);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return { type: 'object', properties, required };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function buildFunctionSkill(id, modulePath, config) {
|
|
82
|
-
if (typeof config.handler !== 'function') {
|
|
83
|
-
throw new Error(`Skill "${id}" must have a handler function`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const handlers = { [id]: config.handler };
|
|
87
|
-
const tools = [{
|
|
88
|
-
name: id,
|
|
89
|
-
description: config.description || '',
|
|
90
|
-
input_schema: buildInputSchema(config.input),
|
|
91
|
-
}];
|
|
92
|
-
|
|
93
|
-
registerHandlers(id, handlers, tools);
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
id,
|
|
97
|
-
type: 'function',
|
|
98
|
-
serverName: id,
|
|
99
|
-
allowedTools: [`mcp__${id}__*`],
|
|
100
|
-
description: config.description || `Function skill: ${id}`,
|
|
101
|
-
envKeys: [],
|
|
102
|
-
tools,
|
|
103
|
-
resolve() {
|
|
104
|
-
const bridge = resolveBridgePath();
|
|
105
|
-
if (!bridge) return null;
|
|
106
|
-
return { command: 'node', args: [bridge, modulePath, id] };
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function buildMcpSkill(id, config) {
|
|
112
|
-
return {
|
|
113
|
-
id,
|
|
114
|
-
type: 'mcp',
|
|
115
|
-
serverName: config.serverName || id,
|
|
116
|
-
allowedTools: config.allowedTools || [`mcp__${config.serverName || id}__*`],
|
|
117
|
-
description: config.description || `MCP skill: ${id}`,
|
|
118
|
-
envKeys: config.envKeys || [],
|
|
119
|
-
tools: config.tools || [],
|
|
120
|
-
resolve: config.resolve,
|
|
121
|
-
...(config.cursorKey && { cursorKey: config.cursorKey }),
|
|
122
|
-
...(config.sessionEnvKey && { sessionEnvKey: config.sessionEnvKey }),
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Create and register a skill.
|
|
128
|
-
*
|
|
129
|
-
* Function skill: skill(id, { description, input, handler })
|
|
130
|
-
* MCP skill: skill(id, { resolve(), serverName?, ... })
|
|
131
|
-
*
|
|
132
|
-
* @param {string} id — Unique skill identifier
|
|
133
|
-
* @param {Object} config — Skill definition
|
|
134
|
-
* @returns {Object} A registered skill object
|
|
135
|
-
*/
|
|
136
|
-
export function skill(id, config) {
|
|
137
|
-
let skillObj;
|
|
138
|
-
|
|
139
|
-
if ('handler' in config) {
|
|
140
|
-
if (typeof config.handler !== 'function') {
|
|
141
|
-
throw new Error(`Skill "${id}" must have a handler function`);
|
|
142
|
-
}
|
|
143
|
-
const callerFile = getCallerFile();
|
|
144
|
-
if (!callerFile) {
|
|
145
|
-
throw new Error(`Could not resolve caller file for skill "${id}".`);
|
|
146
|
-
}
|
|
147
|
-
skillObj = buildFunctionSkill(id, callerFile, config);
|
|
148
|
-
} else if (typeof config.resolve === 'function') {
|
|
149
|
-
skillObj = buildMcpSkill(id, config);
|
|
150
|
-
} else {
|
|
151
|
-
throw new Error(
|
|
152
|
-
`Skill "${id}" must have either a handler (function skill) or resolve (MCP skill).`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
registerSkill(skillObj);
|
|
157
|
-
return skillObj;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export const functionSkill = skill;
|
package/src/github.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub Skill
|
|
3
|
-
*
|
|
4
|
-
* Provides GitHub issue and repository management via MCP.
|
|
5
|
-
* Requires GITHUB_TOKEN environment variable.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export const githubSkill = {
|
|
9
|
-
id: 'github',
|
|
10
|
-
serverName: 'github',
|
|
11
|
-
allowedTools: ['mcp__github__*'],
|
|
12
|
-
envKeys: ['GITHUB_TOKEN'],
|
|
13
|
-
description: 'GitHub MCP Server',
|
|
14
|
-
|
|
15
|
-
resolve() {
|
|
16
|
-
const env = {};
|
|
17
|
-
for (const key of this.envKeys) {
|
|
18
|
-
if (process.env[key]) env[key] = process.env[key];
|
|
19
|
-
}
|
|
20
|
-
return {
|
|
21
|
-
command: 'npx',
|
|
22
|
-
args: ['-y', '@modelcontextprotocol/server-github@latest'],
|
|
23
|
-
env,
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
tools: [
|
|
28
|
-
{
|
|
29
|
-
name: 'github_create_issue',
|
|
30
|
-
description: 'Create a GitHub issue',
|
|
31
|
-
input_schema: {
|
|
32
|
-
type: 'object',
|
|
33
|
-
properties: {
|
|
34
|
-
owner: { type: 'string', description: 'Repository owner' },
|
|
35
|
-
repo: { type: 'string', description: 'Repository name' },
|
|
36
|
-
title: { type: 'string', description: 'Issue title' },
|
|
37
|
-
body: { type: 'string', description: 'Issue body (markdown)' }
|
|
38
|
-
},
|
|
39
|
-
required: ['owner', 'repo', 'title']
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'github_search_issues',
|
|
44
|
-
description: 'Search GitHub issues',
|
|
45
|
-
input_schema: {
|
|
46
|
-
type: 'object',
|
|
47
|
-
properties: {
|
|
48
|
-
query: { type: 'string', description: 'GitHub search query' }
|
|
49
|
-
},
|
|
50
|
-
required: ['query']
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
]
|
|
54
|
-
};
|
package/src/index.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @zibby/skills — Built-in skill catalog
|
|
3
|
-
*
|
|
4
|
-
* Importing this module registers all built-in skills with the core
|
|
5
|
-
* skill registry. Users and community packages can register additional
|
|
6
|
-
* skills via registerSkill().
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { registerSkill } from '@zibby/core/framework/skill-registry.js';
|
|
10
|
-
import { browserSkill } from './browser.js';
|
|
11
|
-
import { jiraSkill } from './jira.js';
|
|
12
|
-
import { githubSkill } from './github.js';
|
|
13
|
-
import { slackSkill } from './slack.js';
|
|
14
|
-
import { memorySkill } from './memory.js';
|
|
15
|
-
|
|
16
|
-
registerSkill(browserSkill);
|
|
17
|
-
registerSkill(jiraSkill);
|
|
18
|
-
registerSkill(githubSkill);
|
|
19
|
-
registerSkill(slackSkill);
|
|
20
|
-
registerSkill(memorySkill);
|
|
21
|
-
|
|
22
|
-
// Backward-compat alias: MCP_SERVER_REGISTRY used 'slack_notify' as the key
|
|
23
|
-
registerSkill({ ...slackSkill, id: 'slack_notify' });
|
|
24
|
-
|
|
25
|
-
export const SKILLS = {
|
|
26
|
-
BROWSER: 'browser',
|
|
27
|
-
JIRA: 'jira',
|
|
28
|
-
GITHUB: 'github',
|
|
29
|
-
SLACK: 'slack',
|
|
30
|
-
MEMORY: 'memory',
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export { browserSkill, jiraSkill, githubSkill, slackSkill, memorySkill };
|
|
34
|
-
export { skill, functionSkill } from './function-skill.js';
|
|
35
|
-
export { registerSkill, getSkill, hasSkill, getAllSkills, listSkillIds } from '@zibby/core/framework/skill-registry.js';
|