@zibby/skills 0.1.3 → 0.1.4
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/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 -11
- 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
package/dist/browser.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import{createRequire as w}from"module";import{join as f}from"path";const v=w(import.meta.url);function d(){if(process.env.MCP_BROWSER_PATH)return process.env.MCP_BROWSER_PATH;try{return v.resolve("@zibby/mcp-browser/bin/mcp-browser-zibby.js")}catch{return null}}const a="1280x720",c="1280x720";function S({headless:e}={}){if(e===!0)return!0;if(e===!1)return!1;const r=process.env.ZIBBY_HEADLESS;return r==="1"||String(r).toLowerCase()==="true"}function p(e,r){const o=(e||[]).filter(s=>s!=="--headless");return r?[...o,"--headless"]:o}const m={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:e,workspace:r,nodeName:o,headless:s}={}){const l=d(),n=e&&o?f(e,o):null,i=n||e||r||"test-results",u=S({headless:s}),t={};return n&&(t.ZIBBY_NODE_SESSION_PATH=n),e&&(t.ZIBBY_SESSION_PATH=e),l?{command:"node",args:p([l,"--isolated",`--save-video=${a}`,`--viewport-size=${c}`,`--output-dir=${i}`],u),env:t}:{command:"npx",args:p(["-y","@playwright/mcp","--isolated",`--save-video=${a}`,`--viewport-size=${c}`,"--output-dir",i],u),env:t}}};export{m as browserSkill};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import{execFileSync as U}from"child_process";import{existsSync as H,mkdirSync as re}from"fs";import{join as T,basename as se}from"path";import{randomBytes as ne}from"crypto";import{pathToFileURL as A}from"url";import{createRequire as K}from"module";const x=".zibby/memory",Y="dolt",q={encoding:"utf-8",stdio:["pipe","pipe","pipe"],timeout:15e3},O="dolt",I=new Map,v=new Map,oe=K(import.meta.url),M=()=>ne(8).toString("hex"),S=()=>new Date().toISOString(),ie=[`CREATE TABLE IF NOT EXISTS chat_memory (
|
|
2
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
3
|
+
memory_key VARCHAR(160),
|
|
4
|
+
category VARCHAR(32) NOT NULL,
|
|
5
|
+
content TEXT NOT NULL,
|
|
6
|
+
source VARCHAR(64),
|
|
7
|
+
ticket_key VARCHAR(32),
|
|
8
|
+
session_id VARCHAR(64),
|
|
9
|
+
tier VARCHAR(16) DEFAULT 'mid',
|
|
10
|
+
relevance FLOAT DEFAULT 1.0,
|
|
11
|
+
created_at VARCHAR(32) NOT NULL
|
|
12
|
+
)`,`CREATE TABLE IF NOT EXISTS chat_tasks (
|
|
13
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
14
|
+
ticket_key VARCHAR(32),
|
|
15
|
+
type VARCHAR(32) NOT NULL,
|
|
16
|
+
title VARCHAR(512) NOT NULL,
|
|
17
|
+
status VARCHAR(32) NOT NULL DEFAULT 'pending',
|
|
18
|
+
spec_path VARCHAR(512),
|
|
19
|
+
session_id VARCHAR(64),
|
|
20
|
+
result_summary TEXT,
|
|
21
|
+
created_at VARCHAR(32) NOT NULL,
|
|
22
|
+
finished_at VARCHAR(32)
|
|
23
|
+
)`,`CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
24
|
+
session_id VARCHAR(64) PRIMARY KEY,
|
|
25
|
+
summary TEXT NOT NULL,
|
|
26
|
+
tickets TEXT,
|
|
27
|
+
tasks_run INT DEFAULT 0,
|
|
28
|
+
tasks_passed INT DEFAULT 0,
|
|
29
|
+
tasks_failed INT DEFAULT 0,
|
|
30
|
+
key_facts TEXT,
|
|
31
|
+
created_at VARCHAR(32) NOT NULL
|
|
32
|
+
)`],L=new Set;function d(t,e){return U(Y,e,{...q,cwd:t})}function g(t,e){try{const r=d(t,["sql","-q",e,"-r","json"]);return JSON.parse(r.trim()).rows||[]}catch{return[]}}function p(t,e){d(t,["sql","-q",e])}function V(t){if(L.has(t))return!0;if(!H(T(t,".dolt"))){if(!ce())return!1;re(t,{recursive:!0}),d(t,["init","--name","Zibby Chat Memory","--email","chat@zibby.app"])}const e=`${ie.join(`;
|
|
33
|
+
`)};`;p(t,e);try{p(t,"ALTER TABLE chat_memory ADD COLUMN tier VARCHAR(16) DEFAULT 'mid'")}catch{}try{p(t,"ALTER TABLE chat_memory ADD COLUMN memory_key VARCHAR(160)")}catch{}return L.add(t),!0}function Te(){L.clear()}function ce(){try{return U(Y,["version"],{...q,timeout:5e3}),!0}catch{return!1}}function c(t){return t==null?"NULL":`'${String(t).replace(/'/g,"''")}'`}function D(t){return String(t||"").toLowerCase().replace(/[“”]/g,'"').replace(/[‘’]/g,"'").replace(/[\s_-]+/g," ").replace(/[^\w\s"']/g,"").replace(/\s+/g," ").trim()}function R(t){return t==="long"?3:t==="mid"?2:t==="short"?1:0}function B(t,e){const r=["short","mid","long"].includes(t)?t:"mid";return new Set(["fact","decision","preference","credential","url","workaround"]).has(String(e||"").toLowerCase())&&r==="short"?"mid":r}function J(t){const e=new Map;for(const r of t||[]){const s=D(r.content),n=r.memory_key?`key:${r.memory_key}`:s?`norm:${s}`:"";if(!n)continue;const o=e.get(n);if(!o){e.set(n,r);continue}const i=R(o.tier),a=R(r.tier);if(a>i){e.set(n,r);continue}a===i&&Number(r.relevance||0)>Number(o.relevance||0)&&e.set(n,r)}return[...e.values()]}function $(t,e){const r=String(t??"");return r.length<=e?r:e<=1?r.slice(0,e):`${r.slice(0,e-1)}\u2026`}function C(t,e){const r={recentSessions:Array.isArray(t?.recentSessions)?t.recentSessions:[],topMemories:Array.isArray(t?.topMemories)?t.topMemories:[],taskStats:Array.isArray(t?.taskStats)?t.taskStats:[],ticketFilter:t?.ticketFilter||null,backend:e||String(t?.backend||O),error:t?.error||null};return r.backend==="mem0"?{...r,recentSessions:[],taskStats:[]}:r}function ae(t){const e=[];if(t.recentSessions?.length>0){e.push("Recent sessions:");for(const r of t.recentSessions.slice(0,3))r?.summary?.trim()&&e.push(`- ${$(r.summary,150)}${r.tickets?` [${r.tickets}]`:""}`)}if(t.topMemories?.length>0){e.push("Known facts:");for(const r of t.topMemories.slice(0,10)){const s=r.tier==="long"?"\u2605":"\xB7";e.push(`${s} [${r.category}] ${$(r.content,120)}`)}}return e.length===0?"":`## Memory Context
|
|
34
|
+
${e.join(`
|
|
35
|
+
`)}`}function b(t){return{backend:t.backend,recentSessions:t.recentSessions.slice(0,3).map(e=>({summary:$(String(e?.summary||""),160),tickets:e?.tickets||null,created_at:e?.created_at||null})),topMemories:t.topMemories.slice(0,8).map(e=>({category:e?.category||null,tier:e?.tier||null,content:$(String(e?.content||""),140),source:e?.source||null})),taskStats:t.taskStats,error:t.error||null}}async function W(t,e){const r=String(process.env.ZIBBY_MEMORY_BACKEND||"").trim().toLowerCase();if(r==="mem0"||r==="dolt")return r;const s=String(e?.options?.memoryBackend||e?.options?.config?.memory?.backend||"").trim().toLowerCase();if(s==="mem0"||s==="dolt")return s;if(v.has(t))return v.get(t);try{const n=T(t,".zibby.config.mjs");if(H(n)){const o=await import(A(n).href),i=String(o?.default?.memory?.backend||"").trim().toLowerCase();if(i==="mem0"||i==="dolt")return v.set(t,i),i}}catch{}return v.set(t,O),O}function j(t){const e=String(process.env.ZIBBY_MEMORY_USER_ID||"").trim();return e||`workspace:${se(t||process.cwd())}`}function me(){const t=String(process.env.ZIBBY_MEM0_OPENAI_BASE_URL||"").trim();if(!t)return null;const e=String(process.env.ZIBBY_MEM0_API_KEY||process.env.ZIBBY_USER_TOKEN||process.env.OPENAI_API_KEY||"").trim(),r=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:r,baseURL:t,...e?{apiKey:e}:{}}},embedder:{provider:"openai",config:{model:s,embeddingDims:n,baseURL:t,...e?{apiKey:e}:{}}},vectorStore:{provider:"memory",config:{dimension:n}}}}async function z(t){const e=t||process.cwd();if(I.has(e))return I.get(e);let r;try{const a=K(A(T(e,"package.json")).href).resolve("mem0ai/oss");r=await import(A(a).href)}catch{try{const i=oe.resolve("mem0ai/oss");r=await import(A(i).href)}catch(i){throw new Error(`Cannot find package 'mem0ai' for workspace "${e}". Install in that project: npm install mem0ai. (${i.message})`,{cause:i})}}const s=r?.Memory;if(!s)throw new Error("mem0ai/oss does not export Memory");const n=me(),o=n?new s(n):new s;return I.set(e,o),o}function Z(t,e="mid"){return(Array.isArray(t)?t:Array.isArray(t?.results)?t.results:[]).map(s=>({id:s?.id||M(),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:B(s?.metadata?.tier||e,s?.metadata?.category||"fact"),relevance:Number(s?.score??s?.metadata?.relevance??.8),created_at:s?.created_at||s?.metadata?.created_at||S()})).filter(s=>String(s.content||"").trim().length>0)}const Ae={id:"chat-memory",description:"Persistent chat memory and task history (Dolt-backed)",envKeys:[],promptFragment:`## Chat Memory (persistent)
|
|
36
|
+
You have persistent memory across sessions. Use it to avoid losing context:
|
|
37
|
+
- **memory_store**: Save important facts, decisions, or context. Anything worth remembering.
|
|
38
|
+
- **memory_recall**: Search your memory by keyword or category. Use this at the START of conversations to recall relevant context.
|
|
39
|
+
- **memory_brief**: Get a compact summary of recent sessions and key facts. Load this when starting a new task to understand history.
|
|
40
|
+
- **memory_end_session**: Call when a task is complete. Summarizes what happened for future recall.
|
|
41
|
+
- **task_log**: Record a completed task (test run, analysis, etc.) for history.
|
|
42
|
+
- **task_history**: Query past tasks by ticket, status, or type.
|
|
43
|
+
|
|
44
|
+
### When to use memory
|
|
45
|
+
- At the START of a conversation: call memory_recall or memory_brief to load relevant context
|
|
46
|
+
- When you learn something important: call memory_store (e.g. "SCRUM-123 login page is at /auth/login")
|
|
47
|
+
- When a task finishes: call task_log to record it
|
|
48
|
+
- When the user's request is complete: call memory_end_session
|
|
49
|
+
|
|
50
|
+
### Categories for memory_store
|
|
51
|
+
fact, decision, context, insight, credential, url, error, workaround`,resolve(){return null},async buildPromptContext(t,e={}){const r=t?.options?.workspace||process.cwd(),s=T(r,x),n=await W(r,t);if(n==="dolt"&&!V(s)){const o="Dolt not available. Install: brew install dolt (macOS) or see https://docs.dolthub.com/introduction/installation";return{backend:n,brief:C({backend:n,error:o},n),promptContext:"",debugPreview:b(C({backend:n,error:o},n)),error:o}}try{const o=n==="mem0"?await Q(e,s,r):P(e,s),i=JSON.parse(o||"{}"),a=C({...i,backend:n},n);return{backend:n,brief:a,promptContext:ae(a),debugPreview:b(a),error:a.error||null}}catch(o){const i=String(o?.message||o),a=C({backend:n,error:i},n);return{backend:n,brief:a,promptContext:"",debugPreview:b(a),error:i}}},async handleToolCall(t,e,r){const s=r?.options?.workspace||process.cwd(),n=T(s,x),o=await W(s,r);if((o==="dolt"||["memory_end_session","task_log","task_history"].includes(t))&&!V(n))return JSON.stringify({error:"Dolt not available. Install: brew install dolt (macOS) or see https://docs.dolthub.com/introduction/installation"});try{switch(t){case"memory_store":return o==="mem0"?await le(e,n,s):X(e,n);case"memory_recall":return o==="mem0"?await G(e,n,s):ye(e,n);case"memory_brief":return o==="mem0"?await Q(e,n,s):P(e,n);case"memory_end_session":return ue(e,n);case"task_log":return de(e,n);case"task_history":return pe(e,n);default:return JSON.stringify({error:`Unknown tool: ${t}`})}}catch(a){if(o==="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 X(t,e){const{content:r,category:s,source:n,ticketKey:o,tier:i,memoryKey:a}=t;if(!r||!s)return JSON.stringify({error:"content and category are required"});const y=D(r);if(!y)return JSON.stringify({error:"content is empty after normalization"});const m=B(i,s),l=m==="long"?1:m==="mid"?.8:.5,u=String(a||"").trim().slice(0,160);if(u){const k=g(e,`SELECT id, tier, relevance
|
|
52
|
+
FROM chat_memory
|
|
53
|
+
WHERE memory_key = ${c(u)}
|
|
54
|
+
ORDER BY created_at DESC
|
|
55
|
+
LIMIT 1`)[0];if(k){const E=String(k.tier||"mid"),N=Number(k.relevance||0),F=R(m)>R(E)?m:E,te=Math.max(l,N);p(e,`UPDATE chat_memory
|
|
56
|
+
SET content = ${c(r)},
|
|
57
|
+
category = ${c(s)},
|
|
58
|
+
source = ${c(n)},
|
|
59
|
+
ticket_key = ${c(o)},
|
|
60
|
+
tier = ${c(F)},
|
|
61
|
+
relevance = ${te},
|
|
62
|
+
created_at = ${c(S())}
|
|
63
|
+
WHERE id = ${c(k.id)}`);try{d(e,["add","."]),d(e,["commit","-m",`memory upsert: ${s} \u2014 ${String(r).slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:k.id,category:s,tier:F,memoryKey:u,upserted:!0})}}const _=g(e,`SELECT id, content, tier, relevance
|
|
64
|
+
FROM chat_memory
|
|
65
|
+
WHERE category = ${c(s)}
|
|
66
|
+
ORDER BY created_at DESC
|
|
67
|
+
LIMIT 200`).find(h=>D(h.content)===y);if(_){const h=String(_.tier||"mid"),k=Number(_.relevance||0),E=R(m)>R(h),N=l>k;if(E||N){p(e,`UPDATE chat_memory
|
|
68
|
+
SET tier = ${c(E?m:h)},
|
|
69
|
+
relevance = ${Math.max(l,k)}
|
|
70
|
+
WHERE id = ${c(_.id)}`);try{d(e,["add","."]),d(e,["commit","-m",`memory promote: ${s} \u2014 ${String(r).slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:_.id,category:s,tier:E?m:h,deduped:!0,promoted:!0})}return JSON.stringify({ok:!0,id:_.id,category:s,tier:h,deduped:!0,promoted:!1})}const w=M(),ee=process.env.ZIBBY_CHAT_SESSION_ID||null;p(e,`INSERT INTO chat_memory (id, memory_key, category, content, source, ticket_key, session_id, tier, relevance, created_at)
|
|
71
|
+
VALUES (${c(w)}, ${c(u||null)}, ${c(s)}, ${c(r)}, ${c(n)}, ${c(o)}, ${c(ee)}, ${c(m)}, ${l}, ${c(S())})`);try{d(e,["add","."]),d(e,["commit","-m",`memory: ${s} \u2014 ${r.slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:w,category:s,tier:m,memoryKey:u||null,stored:r.slice(0,100)})}async function le(t,e,r){const{content:s,category:n,source:o,ticketKey:i,tier:a,memoryKey:y}=t;if(!s||!n)return JSON.stringify({error:"content and category are required"});try{const m=await z(r),l=j(r),u=B(a,n);return await m.add([{role:"user",content:String(s)}],{userId:l,metadata:{memoryKey:y||null,category:n,tier:u,source:o||"zibby-chat",ticketKey:i||null,created_at:S()}}),JSON.stringify({ok:!0,backend:"mem0",userId:l,category:n,tier:u,memoryKey:y||null,stored:String(s).slice(0,100)})}catch(m){throw new Error(`mem0 store failed: ${m.message}. If mem0 is not installed, run: npm install mem0ai`,{cause:m})}}function ye(t,e){const{query:r,category:s,ticketKey:n,tier:o,limit:i=20}=t,a=[];r&&a.push(`content LIKE ${c(`%${r}%`)}`),s&&a.push(`category = ${c(s)}`),n&&a.push(`ticket_key = ${c(n)}`),o&&a.push(`tier = ${c(o)}`);const m=`SELECT id, memory_key, category, content, source, ticket_key, tier, relevance, created_at
|
|
72
|
+
FROM chat_memory ${a.length>0?`WHERE ${a.join(" AND ")}`:""}
|
|
73
|
+
ORDER BY relevance DESC, created_at DESC
|
|
74
|
+
LIMIT ${i}`,l=g(e,m);return JSON.stringify({total:l.length,memories:l})}async function G(t,e,r){const{query:s,category:n,ticketKey:o,tier:i,limit:a=20}=t;try{const y=await z(r),m=j(r);let l=[];if(s&&String(s).trim()){const u=await y.search(String(s),{userId:m,limit:a});l=Z(u)}else{const u=await y.getAll({userId:m,limit:Math.max(a,50)});l=Z(u)}return n&&(l=l.filter(u=>u.category===n)),o&&(l=l.filter(u=>u.ticket_key===o)),i&&(l=l.filter(u=>u.tier===i)),l=l.slice(0,a),JSON.stringify({total:l.length,memories:l,backend:"mem0"})}catch(y){throw new Error(`mem0 recall failed: ${y.message}. If mem0 is not installed, run: npm install mem0ai`,{cause:y})}}function P(t,e){const{ticketKey:r}=t;_e(e);const n=g(e,`SELECT session_id, summary, tickets, tasks_run, tasks_passed, tasks_failed, created_at
|
|
75
|
+
FROM chat_sessions ORDER BY created_at DESC LIMIT 5`),o=r?`AND ticket_key = ${c(r)}`:"",i=g(e,`SELECT memory_key, category, content, source, tier, relevance, created_at FROM chat_memory
|
|
76
|
+
WHERE tier = 'long' ${o} ORDER BY relevance DESC, created_at DESC LIMIT 10`),a=g(e,`SELECT memory_key, category, content, source, tier, relevance, created_at FROM chat_memory
|
|
77
|
+
WHERE tier = 'mid' ${o} ORDER BY relevance DESC, created_at DESC LIMIT 8`),m=g(e,`SELECT type, status, COUNT(*) as cnt FROM chat_tasks
|
|
78
|
+
GROUP BY type, status ORDER BY cnt DESC LIMIT 10`),l=J([...i,...a]);return JSON.stringify({recentSessions:n,topMemories:l,taskStats:m,ticketFilter:r||null})}async function Q(t,e,r){const{ticketKey:s}=t,n=await G({limit:80},e,r),o=JSON.parse(n||"{}");let i=Array.isArray(o.memories)?o.memories:[];s&&(i=i.filter(f=>f.ticket_key===s));const a=f=>{const _=Date.parse(String(f?.created_at||""))||0;return Number(f?.relevance||0)*1e12+_},y=(f,_)=>a(_)-a(f),m=i.filter(f=>f.tier==="long").sort(y).slice(0,10),l=i.filter(f=>f.tier==="mid").sort(y).slice(0,8),u=J([...m,...l]);return JSON.stringify({recentSessions:[],topMemories:u,taskStats:[],ticketFilter:s||null,backend:"mem0"})}function ue(t,e){const{summary:r,tickets:s,tasksRun:n=0,tasksPassed:o=0,tasksFailed:i=0,keyFacts:a}=t;if(!r)return JSON.stringify({error:"summary is required"});const y=process.env.ZIBBY_CHAT_SESSION_ID||`session_${M()}`;if(p(e,`INSERT INTO chat_sessions (session_id, summary, tickets, tasks_run, tasks_passed, tasks_failed, key_facts, created_at)
|
|
79
|
+
VALUES (${c(y)}, ${c(r)}, ${c(s)}, ${n}, ${o}, ${i}, ${c(a)}, ${c(S())})`),a)for(const m of a.split(";").map(l=>l.trim()).filter(Boolean))X({content:m,category:"fact",source:"session_summary",tier:"mid"},e);fe(e);try{d(e,["add","."]),d(e,["commit","-m",`session end: ${r.slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,sessionId:y,summary:r.slice(0,200)})}function de(t,e){const{title:r,type:s,status:n,ticketKey:o,specPath:i,resultSummary:a}=t;if(!r||!s||!n)return JSON.stringify({error:"title, type, and status are required"});const y=M(),m=process.env.ZIBBY_CHAT_SESSION_ID||null;p(e,`INSERT INTO chat_tasks (id, ticket_key, type, title, status, spec_path, session_id, result_summary, created_at, finished_at)
|
|
80
|
+
VALUES (${c(y)}, ${c(o)}, ${c(s)}, ${c(r)}, ${c(n)}, ${c(i)}, ${c(m)}, ${c(a)}, ${c(S())}, ${c(S())})`);try{d(e,["add","."]),d(e,["commit","-m",`task: ${n} \u2014 ${r.slice(0,60)}`])}catch{}return JSON.stringify({ok:!0,id:y,title:r,type:s,status:n})}function pe(t,e){const{ticketKey:r,type:s,status:n,limit:o=20}=t,i=[];r&&i.push(`ticket_key = ${c(r)}`),s&&i.push(`type = ${c(s)}`),n&&i.push(`status = ${c(n)}`);const y=`SELECT id, ticket_key, type, title, status, spec_path, result_summary, created_at, finished_at
|
|
81
|
+
FROM chat_tasks ${i.length>0?`WHERE ${i.join(" AND ")}`:""}
|
|
82
|
+
ORDER BY created_at DESC LIMIT ${o}`,m=g(e,y);return JSON.stringify({total:m.length,tasks:m})}function fe(t){try{p(t,"UPDATE chat_memory SET relevance = relevance * 0.98 WHERE tier = 'long' AND relevance > 0.5"),p(t,"UPDATE chat_memory SET relevance = relevance * 0.90 WHERE tier = 'mid' AND relevance > 0.1"),p(t,"UPDATE chat_memory SET relevance = relevance * 0.70 WHERE tier = 'short' AND relevance > 0.05"),p(t,"DELETE FROM chat_memory WHERE relevance < 0.05")}catch{}}function _e(t){try{const e=new Date(Date.now()-864e5).toISOString();p(t,`DELETE FROM chat_memory WHERE tier = 'short' AND created_at < ${c(e)}`)}catch{}}export{Te as _resetInitCache,Ae as chatMemorySkill};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{readFileSync as l,readdirSync as h,statSync as a,writeFileSync as f,mkdirSync as m}from"fs";import{join as c,resolve as y,relative as w}from"path";import{execSync as p}from"child_process";const d=256*1024,g=64*1024,N={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){const n=e?.options?.workspace||process.cwd();try{switch(r){case"read_file":return _(t,n);case"write_file":return S(t,n);case"list_directory":return b(t,n);case"run_command":return O(t,n);case"open_url":return k(t);case"wait":return await v(t,e?.options?.signal);default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(o){return JSON.stringify({error:o.message})}},resolve(){return null}};function s(r,t){return y(t,r)}function _(r,t){const e=s(r.path,t),n=a(e);return n.size>d?JSON.stringify({error:`File too large (${(n.size/1024).toFixed(0)}KB). Max: ${d/1024}KB`}):l(e,"utf-8")}function S(r,t){const e=s(r.path,t),n=c(e,"..");return m(n,{recursive:!0}),f(e,r.content,"utf-8"),JSON.stringify({ok:!0,path:w(t,e)})}function b(r,t){const e=s(r.path||".",t);return h(e).map(o=>{try{return a(c(e,o)).isDirectory()?`${o}/`:o}catch{return o}}).join(`
|
|
2
|
+
`)}function O(r,t){const e=r.cwd?s(r.cwd,t):t;return p(r.command,{cwd:e,encoding:"utf-8",timeout:3e4,maxBuffer:g,stdio:["pipe","pipe","pipe"]})||"(no output)"}function k(r){const{url:t}=r;if(!t||!t.startsWith("http://")&&!t.startsWith("https://"))return JSON.stringify({error:"Invalid URL \u2014 must start with http:// or https://"});const e=process.platform,n=e==="darwin"?"open":e==="win32"?"start":"xdg-open";try{return p(`${n} "${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 v(r,t){const e=Math.min(Math.max(r.seconds||5,1),300),n=r.reason||"async operation",o=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:n,interrupted:!0});await new Promise(u=>setTimeout(u,Math.min(o,i-Date.now())))}return JSON.stringify({ok:!0,waited:e,reason:n})}export{N as coreToolsSkill};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createRequire as i}from"module";import{fileURLToPath as a}from"url";import{registerHandlers as u}from"@zibby/core/framework/function-skill-registry.js";import{registerSkill as c}from"@zibby/core/framework/skill-registry.js";const p=i(import.meta.url);function f(){try{return p.resolve("@zibby/core/framework/function-bridge.js")}catch{return null}}const m=import.meta.url;function h(){const e=Error.prepareStackTrace;try{Error.prepareStackTrace=(l,o)=>o;const t=new Error().stack;for(let l=2;l<t.length;l++){const o=t[l].getFileName();if(o&&o!==m&&!o.startsWith("node:"))return o.startsWith("file://")?a(o):o}return null}finally{Error.prepareStackTrace=e}}function y(e){if(!e||typeof e!="object")return{type:"object",properties:{},required:[]};const r={},t=[];for(const[l,o]of Object.entries(e))if(typeof o=="string")r[l]={type:o},t.push(l);else{const{required:s,...n}=o;r[l]=n,s!==!1&&t.push(l)}return{type:"object",properties:r,required:t}}function k(e,r,t){if(typeof t.handler!="function")throw new Error(`Skill "${e}" must have a handler function`);const l={[e]:t.handler},o=[{name:e,description:t.description||"",input_schema:y(t.input)}];return u(e,l,o),{id:e,type:"function",serverName:e,allowedTools:[`mcp__${e}__*`],description:t.description||`Function skill: ${e}`,envKeys:[],tools:o,resolve(){const s=f();return s?{command:"node",args:[s,r,e]}:null}}}function v(e,r){return{id:e,type:"mcp",serverName:r.serverName||e,allowedTools:r.allowedTools||[`mcp__${r.serverName||e}__*`],description:r.description||`MCP skill: ${e}`,envKeys:r.envKeys||[],tools:r.tools||[],resolve:r.resolve,...r.cursorKey&&{cursorKey:r.cursorKey},...r.sessionEnvKey&&{sessionEnvKey:r.sessionEnvKey}}}function d(e,r){let t;if("handler"in r){if(typeof r.handler!="function")throw new Error(`Skill "${e}" must have a handler function`);const l=h();if(!l)throw new Error(`Could not resolve caller file for skill "${e}".`);t=k(e,l,r)}else if(typeof r.resolve=="function")t=v(e,r);else throw new Error(`Skill "${e}" must have either a handler (function skill) or resolve (MCP skill).`);return c(t),t}const S=d;export{S as functionSkill,d as skill};
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import{spawn as T}from"child_process";import{existsSync as p,mkdirSync as O,readdirSync as $,statSync as j,readFileSync as R}from"fs";import{resolve as v,join as l,basename as C}from"path";const x=".zibby/repos";function w(u,n,r={}){return new Promise((o,i)=>{const e=T(u,{cwd:n,shell:!0,env:{...process.env,GIT_TERMINAL_PROMPT:"0",...r}});let c="",f="";e.stdout.on("data",s=>{c+=s.toString()}),e.stderr.on("data",s=>{f+=s.toString()}),e.on("close",s=>{s!==0?i(new Error(`Exit ${s}: ${f.trim()||c.trim()}`)):o(c.trim())}),e.on("error",s=>i(s))})}const A={id:"git",description:"Clone and manage git repositories for codebase analysis",envKeys:["GITHUB_TOKEN","GITLAB_TOKEN"],promptFragment:`## Git Repositories
|
|
2
|
+
You can clone and explore git repositories locally for codebase analysis:
|
|
3
|
+
- git_checkout: Clone a repo (or pull if already cloned). Supports GitHub and GitLab with auto-auth.
|
|
4
|
+
- git_list_repos: List locally cloned repos
|
|
5
|
+
- git_explore: Quick overview of a cloned repo's structure (key files, package.json, routes, etc.)
|
|
6
|
+
|
|
7
|
+
When a test ticket lacks context, use this workflow:
|
|
8
|
+
1. Clone the relevant repo with git_checkout
|
|
9
|
+
2. Use git_explore to understand the project structure
|
|
10
|
+
3. Use shell commands (grep, cat) to read specific files for deeper understanding
|
|
11
|
+
4. Use GitHub/GitLab skills to read related PRs and commits
|
|
12
|
+
5. Build well-informed test specs and save them to files before running tests`,resolve(){return null},async handleToolCall(u,n,r){const o=r?.options?.workspace||process.cwd();try{switch(u){case"git_checkout":return await N(n,o);case"git_list_repos":return L(n,o);case"git_explore":return D(n,o);default:return JSON.stringify({error:`Unknown tool: ${u}`})}}catch(i){return JSON.stringify({error:i.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 N(u,n){let{url:r,branch:o,shallow:i=!0,name:e}=u;!r.includes("://")&&!r.startsWith("git@")&&(r=`https://github.com/${r}`);const c=e||C(r.replace(/\.git$/,"")),f=v(n,x);O(f,{recursive:!0});const s=l(f,c);let h=r;const g=process.env.GITHUB_TOKEN,m=process.env.GITLAB_TOKEN,k=process.env.GITLAB_URL;if(r.includes("github.com")&&g)h=r.replace("https://github.com",`https://x-access-token:${g}@github.com`);else if(m&&k)try{const d=new URL(k).host;r.includes(d)&&(h=r.replace(`https://${d}`,`https://oauth2:${m}@${d}`))}catch{}if(p(l(s,".git"))){const d=o?`git -C "${s}" fetch origin ${o} && git -C "${s}" checkout ${o} && git -C "${s}" pull origin ${o}`:`git -C "${s}" pull`;await w(d,n);const b=await w(`git -C "${s}" log -1 --format="%h %s"`,n);return JSON.stringify({action:"updated",repo:c,path:s,branch:o||"default",head:b})}const t=["git","clone"];i&&t.push("--depth","1"),o&&t.push("--branch",o),t.push(`"${h}"`,`"${s}"`),await w(t.join(" "),n);const _=await w(`git -C "${s}" log -1 --format="%h %s"`,n);return JSON.stringify({action:"cloned",repo:c,path:s,branch:o||"default",shallow:i,head:_})}function L(u,n){const r=v(n,x);if(!p(r))return JSON.stringify({repos:[],message:"No repos cloned yet"});const o=[];for(const i of $(r,{withFileTypes:!0})){if(!i.isDirectory())continue;const e=l(r,i.name);if(!p(l(e,".git")))continue;const c=j(e);o.push({name:i.name,path:e,lastModified:c.mtime.toISOString()})}return JSON.stringify({repos:o,total:o.length,directory:r})}function D(u,n){const{repo:r,depth:o=2}=u,i=v(n,x,r);if(!p(i))return JSON.stringify({error:`Repo not found: ${r}. Run git_checkout first.`});const e={repo:r,path:i},c=l(i,"package.json");if(p(c))try{const t=JSON.parse(R(c,"utf-8"));e.packageJson={name:t.name,version:t.version,scripts:t.scripts?Object.keys(t.scripts):[],dependencies:t.dependencies?Object.keys(t.dependencies).slice(0,30):[],devDependencies:t.devDependencies?Object.keys(t.devDependencies).slice(0,20):[]},t.dependencies?.react?e.framework="React":t.dependencies?.next?e.framework="Next.js":t.dependencies?.vue?e.framework="Vue":t.dependencies?.angular?e.framework="Angular":t.dependencies?.express?e.framework="Express":t.dependencies?.fastify&&(e.framework="Fastify")}catch{}const f=l(i,"pyproject.toml");p(f)&&(e.language="Python");const s=l(i,"go.mod");p(s)&&(e.language="Go");const h=l(i,"Cargo.toml");p(h)&&(e.language="Rust"),p(c)&&(e.language=e.language||"JavaScript/TypeScript");const g=[];function m(t,_,d){if(d>o)return;let b;try{b=$(t,{withFileTypes:!0})}catch{return}const S=b.filter(a=>!a.name.startsWith(".")&&a.name!=="node_modules"&&a.name!=="__pycache__"&&a.name!=="dist"&&a.name!=="build"&&a.name!==".git").sort((a,y)=>a.isDirectory()!==y.isDirectory()?a.isDirectory()?-1:1:a.name.localeCompare(y.name));for(const a of S){const y=a.isDirectory();g.push(`${_}${y?"\u{1F4C1}":"\u{1F4C4}"} ${a.name}`),y&&d<o&&m(l(t,a.name),`${_} `,d+1)}}m(i,"",1),e.tree=g.slice(0,80),g.length>80&&(e.treeTruncated=!0);const k=["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 e.keyFilesFound=k.filter(t=>p(l(i,t))),JSON.stringify(e)}export{A as gitSkill};
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import{resolveIntegrationToken as S}from"@zibby/core/backend-client.js";async function d(g,n={}){const{token:t}=await S("github"),s=g.startsWith("https://")?g:`https://api.github.com${g}`,i={Authorization:`Bearer ${t}`,Accept:n.accept||"application/vnd.github.v3+json","User-Agent":"Zibby-App",...n.body?{"Content-Type":"application/json"}:{}},e=await fetch(s,{method:n.method||"GET",headers:i,body:n.body?JSON.stringify(n.body):void 0});if(!e.ok){const r=await e.text().catch(()=>"");throw new Error(`GitHub API ${e.status}: ${r.slice(0,300)}`)}return n.raw?e.text():e.json()}const N={id:"github",serverName:"github",allowedTools:["mcp__github__*"],envKeys:["GITHUB_TOKEN"],description:"GitHub \u2014 issues, PRs, commits, code search, file reading",promptFragment:`## GitHub (connected)
|
|
2
|
+
You have access to the user's GitHub repositories. Available tools:
|
|
3
|
+
|
|
4
|
+
### Discovery
|
|
5
|
+
- github_list_repos: Lists ALL accessible repos (personal + orgs, private + public, up to 200 repos)
|
|
6
|
+
- github_search_repos: Search for a specific repo by name (e.g., "electron", "my-app")
|
|
7
|
+
- github_get_user: Get authenticated user's profile
|
|
8
|
+
- github_list_orgs: List organizations with accessible repos
|
|
9
|
+
|
|
10
|
+
### Clone & Code Reading
|
|
11
|
+
- 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.
|
|
12
|
+
- github_get_file: Read a file's content from a repo (without cloning)
|
|
13
|
+
- github_list_commits: List recent commits
|
|
14
|
+
- github_get_commit: Get commit details and diff
|
|
15
|
+
|
|
16
|
+
### Issues & PRs
|
|
17
|
+
- github_search_issues: Search issues and PRs
|
|
18
|
+
- github_search_code: Search code across repos
|
|
19
|
+
- github_get_pr: Get PR details
|
|
20
|
+
- github_get_pr_diff: Get PR diff
|
|
21
|
+
- github_list_pr_files: List PR changed files
|
|
22
|
+
- github_list_pr_comments: Get PR comments
|
|
23
|
+
- github_create_issue: Create new issue
|
|
24
|
+
|
|
25
|
+
### Important: "Check out" / "Clone"
|
|
26
|
+
When user says "check out repo-name" or "clone repo-name":
|
|
27
|
+
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.
|
|
28
|
+
2. Display the path and contents field (directory listing)
|
|
29
|
+
3. STOP. Do not offer to inspect files or ask what to do next.
|
|
30
|
+
|
|
31
|
+
When user just wants to "look at" or "read" files (not clone):
|
|
32
|
+
- Use github_get_file to read individual files via API`,resolve(){const g={};for(const n of this.envKeys)process.env[n]&&(g[n]=process.env[n]);return{command:"npx",args:["-y","@modelcontextprotocol/server-github@latest"],env:g}},async handleToolCall(g,n){try{switch(g){case"github_search_issues":{const t=n.query;if(!t)return JSON.stringify({error:"query is required"});const s=await d(`/search/issues?q=${encodeURIComponent(t)}&per_page=${n.limit||20}`),i=(s.items||[]).map(e=>({number:e.number,title:e.title,state:e.state,repo:e.repository_url?.split("/").slice(-2).join("/"),url:e.html_url,user:e.user?.login,isPR:!!e.pull_request,labels:(e.labels||[]).map(r=>r.name),createdAt:e.created_at}));return JSON.stringify({total:s.total_count,items:i})}case"github_search_code":{const t=n.query;if(!t)return JSON.stringify({error:"query is required"});const s=n.repo?`+repo:${n.repo}`:"",i=n.language?`+language:${n.language}`:"",e=await d(`/search/code?q=${encodeURIComponent(t)}${s}${i}&per_page=${n.limit||15}`),r=(e.items||[]).map(o=>({name:o.name,path:o.path,repo:o.repository?.full_name,url:o.html_url,score:o.score}));return JSON.stringify({total:e.total_count,items:r})}case"github_get_pr":{const{owner:t,repo:s,number:i}=n;if(!t||!s||!i)return JSON.stringify({error:"owner, repo, and number are required"});const e=await d(`/repos/${t}/${s}/pulls/${i}`);return JSON.stringify({number:e.number,title:e.title,state:e.state,merged:e.merged,body:e.body?.slice(0,5e3),user:e.user?.login,branch:e.head?.ref,base:e.base?.ref,changedFiles:e.changed_files,additions:e.additions,deletions:e.deletions,createdAt:e.created_at,mergedAt:e.merged_at,url:e.html_url,labels:(e.labels||[]).map(r=>r.name)})}case"github_get_pr_diff":{const{owner:t,repo:s,number:i}=n;if(!t||!s||!i)return JSON.stringify({error:"owner, repo, and number are required"});const e=await d(`/repos/${t}/${s}/pulls/${i}`,{accept:"application/vnd.github.v3.diff",raw:!0}),r=e.length>15e3;return JSON.stringify({number:i,diff:r?e.slice(0,15e3):e,truncated:r,totalLength:e.length})}case"github_list_pr_files":{const{owner:t,repo:s,number:i}=n;if(!t||!s||!i)return JSON.stringify({error:"owner, repo, and number are required"});const e=await d(`/repos/${t}/${s}/pulls/${i}/files?per_page=100`);return JSON.stringify({total:e.length,files:e.map(r=>({filename:r.filename,status:r.status,additions:r.additions,deletions:r.deletions,patch:r.patch?.slice(0,3e3)}))})}case"github_list_pr_comments":{const{owner:t,repo:s,number:i}=n;if(!t||!s||!i)return JSON.stringify({error:"owner, repo, and number are required"});const e=await d(`/repos/${t}/${s}/pulls/${i}/comments?per_page=50`),r=await d(`/repos/${t}/${s}/issues/${i}/comments?per_page=50`),o=[...e.map(a=>({type:"review",user:a.user?.login,body:a.body?.slice(0,1e3),path:a.path,line:a.line,createdAt:a.created_at})),...r.map(a=>({type:"issue",user:a.user?.login,body:a.body?.slice(0,1e3),createdAt:a.created_at}))].sort((a,p)=>new Date(a.createdAt)-new Date(p.createdAt));return JSON.stringify({total:o.length,comments:o})}case"github_list_commits":{const{owner:t,repo:s,branch:i,path:e,limit:r}=n;if(!t||!s)return JSON.stringify({error:"owner and repo are required"});let o=`/repos/${t}/${s}/commits?per_page=${r||20}`;i&&(o+=`&sha=${encodeURIComponent(i)}`),e&&(o+=`&path=${encodeURIComponent(e)}`);const a=await d(o);return JSON.stringify({total:a.length,commits:a.map(p=>({sha:p.sha?.slice(0,8),fullSha:p.sha,message:p.commit?.message?.slice(0,300),author:p.commit?.author?.name,date:p.commit?.author?.date,url:p.html_url}))})}case"github_get_commit":{const{owner:t,repo:s,sha:i}=n;if(!t||!s||!i)return JSON.stringify({error:"owner, repo, and sha are required"});const e=await d(`/repos/${t}/${s}/commits/${i}`);return JSON.stringify({sha:e.sha?.slice(0,8),message:e.commit?.message,author:e.commit?.author?.name,date:e.commit?.author?.date,stats:e.stats,files:(e.files||[]).map(r=>({filename:r.filename,status:r.status,additions:r.additions,deletions:r.deletions,patch:r.patch?.slice(0,3e3)}))})}case"github_get_file":{const{owner:t,repo:s,path:i,ref:e}=n;if(!t||!s||!i)return JSON.stringify({error:"owner, repo, and path are required"});let r=`/repos/${t}/${s}/contents/${encodeURIComponent(i)}`;e&&(r+=`?ref=${encodeURIComponent(e)}`);const o=await d(r);if(o.type!=="file")return Array.isArray(o)?JSON.stringify({type:"directory",path:i,entries:o.map(m=>({name:m.name,type:m.type,size:m.size,path:m.path}))}):JSON.stringify({error:`Not a file: ${o.type}`});const a=Buffer.from(o.content||"","base64").toString("utf-8"),p=a.length>2e4;return JSON.stringify({path:o.path,size:o.size,sha:o.sha?.slice(0,8),content:p?a.slice(0,2e4):a,truncated:p})}case"github_get_user":try{const t=await d("/installation/repositories?per_page=1");if(t.repositories&&t.repositories.length>0){const s=t.repositories[0],i=s.owner.login,e=s.owner.type,r=e==="Organization"?`/orgs/${i}`:`/users/${i}`,o=await d(r);return JSON.stringify({login:o.login,name:o.name||o.login,avatar:o.avatar_url,bio:o.bio||o.description,type:e,isOrg:e==="Organization",publicRepos:o.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(t){return JSON.stringify({error:`GitHub App cannot access /user endpoint. Use github_list_repos instead. (${t.message})`})}case"github_list_orgs":try{const s=(await d("/installation/repositories?per_page=100")).repositories||[],i=new Map;for(const r of s)r.owner.type==="Organization"&&(i.has(r.owner.login)||i.set(r.owner.login,{login:r.owner.login,description:null,url:r.owner.url}));const e=Array.from(i.values());return JSON.stringify({count:e.length,orgs:e,message:"Extracted from accessible repositories (GitHub Apps cannot access /user/orgs directly)"})}catch(t){return JSON.stringify({error:`GitHub App cannot list orgs via /user/orgs. Error: ${t.message}`})}case"github_clone":{let u=function(y){const f=y.replace(/^~(?=$|\/|\\)/,_);return o(f)};const{owner:t,repo:s,destination:i}=n;if(!t||!s)return JSON.stringify({error:"owner and repo are required"});const{execSync:e}=await import("child_process"),{join:r,resolve:o}=await import("path"),{existsSync:a,mkdirSync:p}=await import("fs"),{homedir:m,platform:b}=await import("os"),{token:w}=await S("github"),_=m(),h=i?u(i):r(_,"zibby-repos"),l=r(h,s);if(p(h,{recursive:!0}),a(l))return JSON.stringify({error:`Directory ${l} already exists. Remove it first or use a different destination.`,existingPath:l});try{const y=`https://x-access-token:${w}@github.com/${t}/${s}.git`;e(`git clone ${y} "${l}"`,{stdio:"pipe"});const f=b()==="win32";let c;return f?c=e(`dir "${l}"`,{encoding:"utf-8",shell:"cmd.exe"}):c=e(`ls -la "${l}"`,{encoding:"utf-8"}),JSON.stringify({success:!0,path:l,message:`Cloned ${t}/${s} to ${l}`,contents:c.split(`
|
|
33
|
+
`).slice(0,30).join(`
|
|
34
|
+
`),instructions:"IMPORTANT: Show the contents field to the user - it contains the directory listing."})}catch(y){return JSON.stringify({error:`Clone failed: ${y.message}`})}}case"github_search_repos":{const{query:t,limit:s}=n;if(!t)return JSON.stringify({error:"query is required"});const i=await this.handleToolCall("github_list_repos",{limit:200},{}),e=JSON.parse(i);if(e.error)return JSON.stringify(e);const r=t.toLowerCase(),o=e.repos.filter(a=>a.name.toLowerCase().includes(r)||a.fullName.toLowerCase().includes(r)||a.description&&a.description.toLowerCase().includes(r));return JSON.stringify({query:t,count:o.length,repos:o.slice(0,s||20)})}case"github_list_repos":{const{owner:t,type:s,sort:i,direction:e,limit:r}=n,o=100,a=r||200;let p=[];if(!t){let u=1,h=!0;for(;h&&p.length<a;){const c=`/installation/repositories?per_page=${o}&page=${u}`,$=(await d(c)).repositories||[];if($.length===0)break;p=p.concat($),h=$.length===o,u++}const l=p.slice(0,a).map(c=>({name:c.name,fullName:c.full_name,private:c.private,description:c.description,language:c.language,defaultBranch:c.default_branch,updatedAt:c.updated_at,stars:c.stargazers_count,url:c.html_url})),y=l.filter(c=>c.private).length,f=l.filter(c=>!c.private).length;return JSON.stringify({count:l.length,repos:l,privateCount:y,publicCount:f,message:`Found ${y} private and ${f} public repos`})}const m=await d(`/orgs/${t}`).then(()=>!0).catch(()=>!1);let b=1,w=!0;for(;w&&p.length<a;){let u;m?u=`/orgs/${t}/repos?per_page=${o}&page=${b}&type=${s||"all"}&sort=${i||"updated"}&direction=${e||"desc"}`:u=`/users/${t}/repos?per_page=${o}&page=${b}&type=${s||"all"}&sort=${i||"updated"}&direction=${e||"desc"}`;const h=await d(u),l=Array.isArray(h)?h:[];if(l.length===0)break;p=p.concat(l),w=l.length===o,b++}const _=p.slice(0,a).map(u=>({name:u.name,fullName:u.full_name,private:u.private,description:u.description,language:u.language,defaultBranch:u.default_branch,updatedAt:u.updated_at,stars:u.stargazers_count,url:u.html_url}));return JSON.stringify({count:_.length,repos:_})}case"github_create_issue":{const{owner:t,repo:s,title:i,body:e}=n;if(!t||!s||!i)return JSON.stringify({error:"owner, repo, and title are required"});const r=await d(`/repos/${t}/${s}/issues`,{method:"POST",body:{title:i,body:e||""}});return JSON.stringify({number:r.number,url:r.html_url,title:r.title})}default:return JSON.stringify({error:`Unknown tool: ${g}`})}}catch(t){return JSON.stringify({error:t.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"]}}]};export{N as githubSkill};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
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 S}from"./memory.js";import{skillInstallerSkill as e}from"./skill-installer.js";import{coreToolsSkill as k}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";l(o),l(t),l(m),l(r),l(s),l(S),l(i),l(f),l(e),l(k),l(p),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"};import{skill as b,functionSkill as x}from"./function-skill.js";import{registerSkill as _,getSkill as C,hasSkill as K,getAllSkills as Y,listSkillIds as d}from"@zibby/core/framework/skill-registry.js";export{h as SKILLS,o as browserSkill,p as chatMemorySkill,k as coreToolsSkill,x as functionSkill,Y as getAllSkills,C as getSkill,f as gitSkill,m as githubSkill,K as hasSkill,t as jiraSkill,d as listSkillIds,S as memorySkill,_ as registerSkill,i as runnerSkill,s as sentrySkill,b as skill,e as skillInstallerSkill,r as slackSkill,i as testRunnerSkill};
|
package/dist/jira.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import{createRequire as $}from"module";import{resolveIntegrationToken as q,clearTokenCache as R}from"@zibby/core/backend-client.js";const K=$(import.meta.url);function T(){if(process.env.MCP_JIRA_PATH)return process.env.MCP_JIRA_PATH;try{return K.resolve("@zibby/mcp-jira/index.js")}catch{return null}}const C=new Set(["paragraph","heading","bulletList","orderedList","listItem","blockquote","codeBlock","rule","table","tableRow","tableCell","tableHeader","mediaSingle","panel"]);function x(c,a){if(!a||!a.length)return c;let e=c;for(const t of a)t.type==="strong"?e=`**${e}**`:t.type==="em"?e=`_${e}_`:t.type==="code"?e=`\`${e}\``:t.type==="strike"?e=`~~${e}~~`:t.type==="link"&&t.attrs?.href&&(e=`[${e}](${t.attrs.href})`);return e}function k(c,a=0){if(!Array.isArray(c))return"";const e=[];for(const t of c){if(t.type==="text"){e.push(x(t.text||"",t.marks));continue}if(t.type==="hardBreak"){e.push(`
|
|
2
|
+
`);continue}if(t.type==="rule"){e.push(`
|
|
3
|
+
---
|
|
4
|
+
`);continue}const i=t.content?k(t.content,a+1):"";if(t.type==="listItem")e.push(i);else if(t.type==="bulletList"){const r=(t.content||[]).map(o=>`- ${k(o.content||[],a+1).trim()}`);e.push(`
|
|
5
|
+
${r.join(`
|
|
6
|
+
`)}
|
|
7
|
+
`)}else if(t.type==="orderedList"){const r=(t.content||[]).map((o,s)=>`${s+1}. ${k(o.content||[],a+1).trim()}`);e.push(`
|
|
8
|
+
${r.join(`
|
|
9
|
+
`)}
|
|
10
|
+
`)}else if(t.type==="heading"){const r=t.attrs?.level||2;e.push(`
|
|
11
|
+
|
|
12
|
+
${"#".repeat(r)} ${i.trim()}
|
|
13
|
+
|
|
14
|
+
`)}else C.has(t.type)?e.push(`
|
|
15
|
+
|
|
16
|
+
${i}
|
|
17
|
+
`):e.push(i)}return e.join("").replace(/\n{3,}/g,`
|
|
18
|
+
|
|
19
|
+
`)}function S(c){return String(c||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function I(c){return S(c).replace(/[a-z0-9]+/g,"")}function w(c,a){const e=S(c),t=S(a);if(!e||!t)return 0;if(e===t)return 1;if(e.length===1||t.length===1)return e===t?1:0;const i=u=>{const d=new Map;for(let y=0;y<u.length-1;y++){const g=u.slice(y,y+2);d.set(g,(d.get(g)||0)+1)}return d},r=i(e),o=i(t);let s=0,n=0,p=0;for(const u of r.values())n+=u;for(const u of o.values())p+=u;for(const[u,d]of r.entries()){const y=o.get(u)||0;s+=Math.min(d,y)}return 2*s/Math.max(1,n+p)}function b(c){return String(c||"").toLowerCase().replace(/\s+/g,"").replace(/[()\-_::"'`]/g,"")}function P(c,a=[]){const e=Array.isArray(a)?a:[];if(e.length===0)return{requested:c||null,resolved:null,strategy:"none"};const t=e.filter(s=>!s.subtask),i=t.length>0?t:e,r=b(c);if(r){const s=i.find(u=>b(u.name)===r);if(s)return{requested:c,resolved:s,strategy:"exact"};const n={task:["task","\u4EFB\u52A1","\u4E8B\u9879","to do","todo"],story:["story","\u7528\u6237\u6545\u4E8B","\u9700\u6C42"],bug:["bug","\u7F3A\u9677","\u95EE\u9898"],improvement:["improvement","\u4F18\u5316","\u6539\u8FDB"],epic:["epic","\u53F2\u8BD7"]};for(const u of Object.values(n)){if(!u.some(y=>b(y)===r))continue;const d=i.find(y=>u.some(g=>b(g)===b(y.name)));if(d)return{requested:c,resolved:d,strategy:"alias"}}const p=i.map(u=>({t:u,score:w(c,u.name)})).sort((u,d)=>d.score-u.score);if(p[0]&&p[0].score>=.5)return{requested:c,resolved:p[0].t,strategy:"fuzzy"}}const o=["task","story","bug","improvement","epic"];for(const s of o){const n=i.find(p=>b(p.name)===s);if(n)return{requested:c||null,resolved:n,strategy:"default-preferred"}}return{requested:c||null,resolved:i[0],strategy:"default-first"}}async function N(c){const a=`projectKeys=${encodeURIComponent(c)}&expand=projects.issuetypes`,e=await f(`/rest/api/3/issue/createmeta?${a}`),t=Array.isArray(e?.projects)?e.projects:[],r=t.find(s=>String(s?.key||"").toUpperCase()===String(c||"").toUpperCase())||t[0]||null;return(Array.isArray(r?.issuetypes)?r.issuetypes:[]).map(s=>({id:s.id,name:s.name,subtask:!!s.subtask,description:s.description||null}))}async function A(c,a){if(!c)throw new Error("projectKey is required");let e="sprint is not EMPTY";a==="active"?e="sprint in openSprints()":a==="closed"?e="sprint in closedSprints()":a==="future"&&(e="sprint in futureSprints()");const t=`project = ${c} AND ${e} ORDER BY updated DESC`,i=`jql=${encodeURIComponent(t)}&maxResults=100&fields=customfield_10020`,r=await f(`/rest/api/3/search/jql?${i}`),o=new Map;for(const s of r.issues||[])for(const n of s.fields?.customfield_10020||[])n&&!o.has(n.id)&&o.set(n.id,{id:n.id,name:n.name,state:n.state,boardId:n.boardId||null,startDate:n.startDate||null,endDate:n.endDate||null,goal:n.goal||null});return[...o.values()].sort((s,n)=>{const p={active:0,future:1,closed:2},u=(p[s.state]??3)-(p[n.state]??3);return u!==0?u:String(n.startDate||"").localeCompare(String(s.startDate||""))})}function D(c,{sprintId:a,sprintName:e,target:t}={}){const i=Array.isArray(c)?c:[];if(!i.length)return{sprint:null,selectedBy:"none"};if(a!=null&&String(a).trim()!=="")return{sprint:i.find(s=>String(s.id)===String(a))||null,selectedBy:"id"};if(e&&String(e).trim()){const o=String(e).trim(),s=i.find(p=>String(p.name||"").toLowerCase()===o.toLowerCase());if(s)return{sprint:s,selectedBy:"name-exact"};const n=i.map(p=>({s:p,score:w(o,p.name||"")})).sort((p,u)=>u.score-p.score);return n[0]&&n[0].score>=.5?{sprint:n[0].s,selectedBy:"name-fuzzy"}:{sprint:null,selectedBy:"name-none"}}const r=String(t||"current").trim().toLowerCase();return r==="active"||r==="current"||r==="latest"?{sprint:i[0],selectedBy:r}:{sprint:i[0],selectedBy:"default"}}function L(c,a){const e=c?.fields?.customfield_10020;return Array.isArray(e)?e.some(t=>String(t?.id)===String(a)):!1}async function B({issueKey:c,projectKey:a,sprintId:e,attempts:t=3,delayMs:i=450}){const r=[];for(let o=0;o<t;o++){try{const s=`project = ${a} AND key = ${c} AND sprint = ${e}`,n=`jql=${encodeURIComponent(s)}&maxResults=1&fields=key,status`,p=await f(`/rest/api/3/search/jql?${n}`);if(Number(p?.total||0)>0)return r.push({attempt:o+1,jql:!0,issueField:null}),{ok:!0,method:"jql",traces:r};const d=await f(`/rest/api/3/issue/${c}?fields=customfield_10020,status`),y=L(d,e);if(r.push({attempt:o+1,jql:!1,issueField:y}),y)return{ok:!0,method:"issue_field",traces:r}}catch(s){r.push({attempt:o+1,error:String(s?.message||s)})}o<t-1&&await new Promise(s=>setTimeout(s,i))}return{ok:!1,method:"none",traces:r}}async function O({issueKey:c,projectKey:a,sprintId:e,sprintName:t,target:i}){if(!c)return{ok:!1,error:"issueKey is required"};let r=a;if(!r&&(r=(await f(`/rest/api/3/issue/${c}?fields=project`))?.fields?.project?.key||null,!r))return{ok:!1,error:`Could not resolve project for ${c}`};const o=await A(r,"active");if(!o.length)return{ok:!1,error:`No assignable active sprint found for project ${r}`};const{sprint:s,selectedBy:n}=D(o,{sprintId:e,sprintName:t,target:i});if(!s)return{ok:!1,error:`No matching sprint found in ${r}`,requested:{sprintId:e??null,sprintName:t??null,target:i??"current"},availableSprints:o.map(d=>({id:d.id,name:d.name,state:d.state}))};await f(`/rest/api/3/issue/${c}`,{method:"PUT",body:{fields:{customfield_10020:Number(s.id)}}});const p=await B({issueKey:c,projectKey:r,sprintId:s.id}),u=p.ok;return{ok:u,issueKey:c,projectKey:r,sprintId:s.id,sprintName:s.name,selectedBy:n,verifiedBy:p.method,verified:u,verificationTrace:p.traces,warning:u?null:`Sprint assignment attempted but verification did not find ${c} in sprint ${s.id}`}}async function f(c,a={}){const e=async()=>{const{token:t,cloudId:i}=await q("jira");if(typeof t!="string"||!t)throw new Error(`Invalid jira token type: ${typeof t}`);if(!i)throw new Error("Invalid jira cloudId: missing");const r=`https://api.atlassian.com/ex/jira/${i}${c}`,o=await fetch(r,{method:a.method||"GET",headers:{Authorization:`Bearer ${t}`,Accept:"application/json",...a.body?{"Content-Type":"application/json"}:{},...a.headers},body:a.body?JSON.stringify(a.body):void 0});if(!o.ok){const n=await o.text().catch(()=>"");throw new Error(`Jira API ${o.status}: ${n.slice(0,300)}`)}const s=await o.text().catch(()=>"");if(!s||!s.trim())return{};try{return JSON.parse(s)}catch{return{raw:s}}};try{return await e()}catch(t){const i=String(t?.message||t||"").toLowerCase();if(!(i.includes("token")||i.includes("401")||i.includes("403")||i.includes("substring")))throw t;return R("jira"),e()}}const M={id:"jira",serverName:"jira",allowedTools:["mcp__jira__*"],envKeys:["ATLASSIAN_ACCESS_TOKEN","ATLASSIAN_CLOUD_ID"],description:"Zibby Jira MCP Server (OAuth Bearer)",promptFragment:`## Jira (connected)
|
|
20
|
+
You have direct access to the user's Jira. Use these tools proactively:
|
|
21
|
+
|
|
22
|
+
### Issue tools
|
|
23
|
+
- jira_search: Search issues with JQL (e.g. "project = PROJ AND status != Done ORDER BY updated DESC")
|
|
24
|
+
- jira_get_issue: Get full details of a ticket by key (e.g. PROJ-123)
|
|
25
|
+
- jira_list_statuses: List available Jira statuses (global or project-specific)
|
|
26
|
+
- jira_list_issue_types: List issue types allowed for issue creation in a project
|
|
27
|
+
- jira_create_issue: Create a new ticket (requires projectKey + summary)
|
|
28
|
+
- jira_get_comments: Get comments on a ticket (newest first) \u2014 use this to find testing steps, notes, etc.
|
|
29
|
+
- jira_add_comment: Add a comment to a ticket
|
|
30
|
+
- jira_edit_issue: Update fields (summary, labels, priority, story points)
|
|
31
|
+
- jira_transition_issue: Move a ticket to a different status (pass transitionId or toStatus)
|
|
32
|
+
|
|
33
|
+
### Project & sprint tools
|
|
34
|
+
- jira_list_projects: List all projects
|
|
35
|
+
- jira_list_sprints: List sprints for a project (filter by state: active/closed/future)
|
|
36
|
+
- 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.
|
|
37
|
+
- jira_move_issue_to_sprint: Move an issue to a sprint (current/active/latest/by-id/by-name) and verify membership.
|
|
38
|
+
|
|
39
|
+
### Sprint membership updates
|
|
40
|
+
- To move an issue into a sprint, use jira_edit_issue with fields.customfield_10020 set to sprint numeric id.
|
|
41
|
+
- Example: jira_edit_issue({ issueKey: "PROJ-123", fields: { customfield_10020: 10 } })
|
|
42
|
+
- Always verify by calling jira_get_sprint_issues(sprintId, projectKey) and checking the issue key is present.
|
|
43
|
+
- For "create and place into current sprint" requests, use a generic atomic flow:
|
|
44
|
+
- Prefer jira_create_issue with moveToSprint=true (optionally sprintId/sprintName/target)
|
|
45
|
+
- Or create first, then use jira_move_issue_to_sprint
|
|
46
|
+
- Always report verified sprint membership result (not just status transition)
|
|
47
|
+
|
|
48
|
+
### Search strategy (important!)
|
|
49
|
+
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.
|
|
50
|
+
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.
|
|
51
|
+
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.
|
|
52
|
+
4. **Remember the board**: After finding the user's project/board, store it in memory (memory_store) so you go straight there next time.
|
|
53
|
+
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.
|
|
54
|
+
|
|
55
|
+
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.
|
|
56
|
+
When the user asks about projects or boards, call jira_list_projects.
|
|
57
|
+
When the user asks about sprints: jira_list_sprints \u2192 jira_get_sprint_issues.
|
|
58
|
+
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.
|
|
59
|
+
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.
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
### Transition workflow (MANDATORY)
|
|
63
|
+
When user asks to move/transition ticket status:
|
|
64
|
+
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.
|
|
65
|
+
2. If target is ambiguous or missing, call jira_transition_issue({ issueKey }) with no transitionId to list available transitions.
|
|
66
|
+
3. Pick the correct transition from returned list (match by "to" status name, not guesswork), then call jira_transition_issue with transitionId.
|
|
67
|
+
4. Call jira_get_issue(issueKey) to verify final status before claiming success.
|
|
68
|
+
5. If target wording differs (e.g. \u5DF2\u7ECF\u9A8C\u6536 vs \u5DF2\u9A8C\u6536), try toStatus first; only ask user to confirm when no reasonable match exists.
|
|
69
|
+
6. IMPORTANT: When target is clear, complete transition + verification in SAME turn. Do NOT stop after listing options.`,resolve(){const c=T();if(!c)return null;const a={};for(const e of this.envKeys)process.env[e]&&(a[e]=process.env[e]);return process.env.ATLASSIAN_INSTANCE_URL&&(a.ATLASSIAN_INSTANCE_URL=process.env.ATLASSIAN_INSTANCE_URL),{command:"node",args:[c],env:a,description:this.description}},async handleToolCall(c,a){try{switch(c){case"jira_list_projects":{const e=await f("/rest/api/3/project"),t=(Array.isArray(e)?e:[]).map(i=>({id:i.id,key:i.key,name:i.name,style:i.style}));return JSON.stringify({count:t.length,projects:t})}case"jira_list_statuses":{const{projectKey:e}=a||{};if(e){const r=await f(`/rest/api/3/project/${encodeURIComponent(e)}/statuses`),o=Array.isArray(r)?r:[],s=new Map;for(const p of o)for(const u of p.statuses||[])u?.id&&(s.has(u.id)||s.set(u.id,{id:u.id,name:u.name,category:u.statusCategory?.name||null}));const n=[...s.values()].sort((p,u)=>String(p.name).localeCompare(String(u.name)));return JSON.stringify({scope:"project",projectKey:e,count:n.length,statuses:n})}const t=await f("/rest/api/3/status"),i=(Array.isArray(t)?t:[]).map(r=>({id:r.id,name:r.name,category:r.statusCategory?.name||null})).sort((r,o)=>String(r.name).localeCompare(String(o.name)));return JSON.stringify({scope:"global",count:i.length,statuses:i})}case"jira_list_issue_types":{const{projectKey:e}=a||{};if(!e)return JSON.stringify({error:"projectKey is required"});const t=await N(e);return JSON.stringify({projectKey:e,count:t.length,issueTypes:t})}case"jira_search":{let e=a.jql||"";const t=a.maxResults||20;e.replace(/\s*ORDER\s+BY\s+.*/i,"").trim()||(e=`created >= -365d ${e}`.trim());const r=`jql=${encodeURIComponent(e)}&maxResults=${t}&fields=summary,status,assignee,priority,updated,issuetype,project`,s=((await f(`/rest/api/3/search/jql?${r}`)).issues||[]).map(n=>({key:n.key,project:n.fields?.project?.key,summary:n.fields?.summary,status:n.fields?.status?.name,assignee:n.fields?.assignee?.displayName||"Unassigned",priority:n.fields?.priority?.name,type:n.fields?.issuetype?.name}));return JSON.stringify({count:s.length,issues:s})}case"jira_get_issue":{const e=a.issueKey;if(!e)return JSON.stringify({error:"issueKey is required"});const t=await f(`/rest/api/3/issue/${e}`);return JSON.stringify({key:t.key,project:t.fields?.project?.key,summary:t.fields?.summary,description:t.fields?.description,status:t.fields?.status?.name,assignee:t.fields?.assignee?.displayName||"Unassigned",priority:t.fields?.priority?.name,type:t.fields?.issuetype?.name,labels:t.fields?.labels,created:t.fields?.created,updated:t.fields?.updated})}case"jira_create_issue":{const{projectKey:e,summary:t,issueType:i,description:r,priority:o,labels:s,assigneeId:n,moveToSprint:p,moveToActiveSprint:u,sprintId:d,sprintName:y,target:g}=a;if(!e||!t)return JSON.stringify({error:"projectKey and summary are required"});let l={requested:i||null,resolved:null,strategy:"none"},j=[];try{j=await N(e),l=P(i,j)}catch{}const m={project:{key:e},summary:t,issuetype:l?.resolved?.id?{id:l.resolved.id}:{name:i||"Task"}};r&&(m.description={type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:r}]}]}),o&&(m.priority={name:o}),s?.length&&(m.labels=s),n&&(m.assignee={id:n});const _=await f("/rest/api/3/issue",{method:"POST",body:{fields:m}}),h={ok:!0,key:_.key,id:_.id,self:_.self};return l?.resolved&&(h.issueType=l.resolved.name,h.issueTypeResolution=l.strategy,l.strategy!=="exact"&&l.requested&&b(l.requested)!==b(l.resolved.name)&&(h.issueTypeWarning=`Requested "${l.requested}" is not available in ${e}; used "${l.resolved.name}" instead.`)),j.length>0&&(h.availableIssueTypes=j.map(v=>v.name)),(p||u)&&(h.sprintMove=await O({issueKey:_.key,projectKey:e,sprintId:d,sprintName:y,target:g})),JSON.stringify(h)}case"jira_list_sprints":{const{projectKey:e,state:t}=a,i=await A(e,t);return JSON.stringify({count:i.length,sprints:i})}case"jira_move_to_active_sprint":{const{issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o}=a||{},s=await O({issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o||"current"});return JSON.stringify(s)}case"jira_move_issue_to_sprint":{const{issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o}=a||{},s=await O({issueKey:e,projectKey:t,sprintId:i,sprintName:r,target:o});return JSON.stringify(s)}case"jira_get_sprint_issues":{const{sprintName:e,sprintId:t,projectKey:i,status:r,maxResults:o}=a;if(!e&&!t)return JSON.stringify({error:"sprintName or sprintId is required"});const s=o||50,n=t?`sprint = ${t}`:`sprint = "${e}"`,p=i?`project = ${i} AND `:"",u=r?` AND status = "${r}"`:"",d=`${p}${n}${u} ORDER BY status ASC, priority DESC`,y=`jql=${encodeURIComponent(d)}&maxResults=${s}&fields=summary,status,assignee,priority,issuetype,project`,g=await f(`/rest/api/3/search/jql?${y}`),l=(g.issues||[]).map(m=>({key:m.key,project:m.fields?.project?.key,summary:m.fields?.summary,status:m.fields?.status?.name,assignee:m.fields?.assignee?.displayName||"Unassigned",priority:m.fields?.priority?.name,type:m.fields?.issuetype?.name})),j={};for(const m of l)j[m.status]=(j[m.status]||0)+1;return JSON.stringify({count:l.length,total:g.total||l.length,statusCounts:j,issues:l})}case"jira_get_comments":{const{issueKey:e,maxResults:t}=a;if(!e)return JSON.stringify({error:"issueKey is required"});const r=await f(`/rest/api/3/issue/${e}/comment?maxResults=${t||50}&orderBy=-created`),o=(r.comments||[]).map(s=>{let n="";return s.body?.content&&(n=k(s.body.content)),{id:s.id,author:s.author?.displayName||"Unknown",body:n,created:s.created,updated:s.updated}});return JSON.stringify({count:o.length,total:r.total||o.length,comments:o})}case"jira_add_comment":{const{issueKey:e,body:t}=a;return!e||!t?JSON.stringify({error:"issueKey and body are required"}):(await f(`/rest/api/3/issue/${e}/comment`,{method:"POST",body:{body:{type:"doc",version:1,content:[{type:"paragraph",content:[{type:"text",text:t}]}]}}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_edit_issue":{const{issueKey:e,fields:t}=a;return!e||!t?JSON.stringify({error:"issueKey and fields are required"}):(await f(`/rest/api/3/issue/${e}`,{method:"PUT",body:{fields:t}}),JSON.stringify({ok:!0,issueKey:e}))}case"jira_transition_issue":{const{issueKey:e,transitionId:t,toStatus:i,statusName:r,status:o}=a;if(!e)return JSON.stringify({error:"issueKey is required"});const s=String(i||r||o||"").trim();if(!t&&!s){const d=((await f(`/rest/api/3/issue/${e}/transitions`)).transitions||[]).map(y=>({id:y.id,name:y.name,to:y.to?.name}));return JSON.stringify({ok:!1,error:"transitionId or toStatus is required",issueKey:e,availableTransitions:d})}let n=t;if(!n){const d=(await f(`/rest/api/3/issue/${e}/transitions`)).transitions||[],y=S(s);let g=d.find(l=>S(l?.name||"")===y||S(l?.to?.name||"")===y);if(!g){const l=I(s);l.length>=2&&(g=d.find(j=>{const m=I(j?.name||""),_=I(j?.to?.name||""),h=m.length>=2&&(m.includes(l)||l.includes(m)),v=_.length>=2&&(_.includes(l)||l.includes(_));return h||v}))}if(!g){const l=d.map(h=>{const v=w(s,h?.name||""),J=w(s,h?.to?.name||"");return{t:h,score:Math.max(v,J)}}).sort((h,v)=>v.score-h.score),j=l[0],m=l[1];j&&j.score>=.45&&(!m||j.score-m.score>=.12)&&(g=j.t)}if(!g?.id)return JSON.stringify({ok:!1,error:`No transition matches target status: "${s}"`,issueKey:e,availableTransitions:d.map(l=>({id:l.id,name:l.name,to:l.to?.name}))});n=g.id}await f(`/rest/api/3/issue/${e}/transitions`,{method:"POST",body:{transition:{id:n}}});const p=await f(`/rest/api/3/issue/${e}?fields=status`);return JSON.stringify({ok:!0,issueKey:e,transitionId:n,statusAfter:p?.fields?.status?.name||null})}default:return JSON.stringify({error:`Unknown tool: ${c}`})}}catch(e){return JSON.stringify({error:e.message})}},tools:[{name:"jira_list_projects",description:"List all Jira projects accessible to the user",input_schema:{type:"object",properties:{}}},{name:"jira_list_statuses",description:"List Jira statuses. Use projectKey to get statuses applicable in that project workflow.",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Optional project key (e.g. PROJ). If omitted, returns global status catalog."}}}},{name:"jira_list_issue_types",description:"List issue types allowed for issue creation in the given project.",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"}},required:["projectKey"]}},{name:"jira_search",description:"Search Jira issues using JQL",input_schema:{type:"object",properties:{jql:{type:"string",description:'JQL query string, e.g. "project = PROJ AND status = Open"'},maxResults:{type:"number",description:"Max results to return (default 20)"}},required:["jql"]}},{name:"jira_get_issue",description:"Get details of a specific Jira issue",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"}},required:["issueKey"]}},{name:"jira_create_issue",description:"Create a new Jira issue",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"},summary:{type:"string",description:"Issue title/summary"},issueType:{type:"string",description:"Issue type (default: Task). Common: Task, Bug, Story, Epic"},description:{type:"string",description:"Issue description (plain text)"},priority:{type:"string",description:"Priority name, e.g. High, Medium, Low"},labels:{type:"array",items:{type:"string"},description:"Array of label strings"},assigneeId:{type:"string",description:"Atlassian account ID to assign to"},moveToSprint:{type:"boolean",description:"If true, move created issue to a sprint and verify."},moveToActiveSprint:{type:"boolean",description:"Backward-compatible alias for moveToSprint."},sprintId:{type:"number",description:"Optional sprint id for placement."},sprintName:{type:"string",description:"Optional sprint name for placement."},target:{type:"string",description:"Placement target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["projectKey","summary"]}},{name:"jira_list_sprints",description:"List sprints for a Jira project (returns sprint names, IDs, states, dates)",input_schema:{type:"object",properties:{projectKey:{type:"string",description:"Project key, e.g. PROJ"},state:{type:"string",description:"Filter: active, closed, future. Omit for all."}},required:["projectKey"]}},{name:"jira_get_sprint_issues",description:"Get all issues in a sprint, optionally filtered by status column name",input_schema:{type:"object",properties:{sprintName:{type:"string",description:"Sprint name (from jira_list_sprints). Use this OR sprintId."},sprintId:{type:"number",description:"Sprint ID (from jira_list_sprints). Use this OR sprintName."},projectKey:{type:"string",description:"Project key to scope the search (optional)"},status:{type:"string",description:'Filter by status name (e.g. "\u8FDB\u884C\u4E2D", "\u6D4B\u8BD5", "Done")'},maxResults:{type:"number",description:"Max issues to return (default 50)"}}}},{name:"jira_move_to_active_sprint",description:"Backward-compatible alias: move issue to sprint target and verify membership.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},projectKey:{type:"string",description:"Optional project key. If omitted, inferred from issue."},sprintId:{type:"number",description:"Optional sprint id."},sprintName:{type:"string",description:"Optional sprint name."},target:{type:"string",description:"Target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["issueKey"]}},{name:"jira_move_issue_to_sprint",description:"Move an issue to a sprint by id/name/target and verify membership.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},projectKey:{type:"string",description:"Optional project key. If omitted, inferred from issue."},sprintId:{type:"number",description:"Optional sprint id."},sprintName:{type:"string",description:"Optional sprint name."},target:{type:"string",description:"Target when sprintId/sprintName omitted: current|active|latest (default: current)."}},required:["issueKey"]}},{name:"jira_get_comments",description:"Get comments on a Jira issue (newest first)",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},maxResults:{type:"number",description:"Max comments to return (default 50)"}},required:["issueKey"]}},{name:"jira_add_comment",description:"Add a comment to a Jira issue",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},body:{type:"string",description:"Comment text (plain text)"}},required:["issueKey","body"]}},{name:"jira_edit_issue",description:"Update fields on a Jira issue (summary, story points, labels, priority)",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},fields:{type:"object",description:"Object of field names to values",additionalProperties:!0}},required:["issueKey","fields"]}},{name:"jira_transition_issue",description:"Move a Jira issue to a different status. Always pass toStatus when user gave a target; only pass issueKey alone when you explicitly need to list transitions.",input_schema:{type:"object",properties:{issueKey:{type:"string",description:"Issue key, e.g. PROJ-123"},transitionId:{type:"string",description:"Transition ID to perform (optional if toStatus is provided)"},toStatus:{type:"string",description:'Target status/column name (e.g. "\u5DF2\u7ECF\u9A8C\u6536", "Done", "In Progress"). If provided, tool resolves matching transition automatically.'}},required:["issueKey"]}}]};export{M as jiraSkill};
|
package/dist/memory.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import{createRequire as s}from"module";import{execFileSync as n}from"child_process";import{join as o}from"path";import{existsSync as a}from"fs";const c=s(import.meta.url);function m(){if(process.env.MCP_MEMORY_PATH)return process.env.MCP_MEMORY_PATH;try{return c.resolve("@zibby/mcp-memory/index.js")}catch{return null}}const d={id:"memory",serverName:"memory",allowedTools:["mcp__memory__*"],envKeys:[],description:"Zibby Memory MCP Server (test history, selectors, page model)",async middleware(){try{const{createMemoryMiddleware:e}=await import("@zibby/memory");return e()}catch{return null}},promptFragment:`BEFORE executing browser actions:
|
|
2
|
+
- Review any test memory/history above. Prefer selectors proven to work.
|
|
3
|
+
- If a previous run failed, avoid the same approach.
|
|
4
|
+
- After setup/login completes, navigate directly to the target page instead of clicking through menus.
|
|
5
|
+
|
|
6
|
+
DURING execution \u2014 when a selector fails and you switch to a fallback:
|
|
7
|
+
- Call memory_save_insight IMMEDIATELY with category: selector_tip
|
|
8
|
+
- Include: which stableId/selector failed, which fallback worked, and the page URL.
|
|
9
|
+
|
|
10
|
+
AFTER completing the test, you MUST call memory_save_insight at least once:
|
|
11
|
+
- Save any useful finding: reliable selectors, timing quirks, navigation patterns, workarounds.
|
|
12
|
+
- Category: selector_tip | timing | navigation | workaround | flaky | general
|
|
13
|
+
- Be specific \u2014 future runs will read your insights.`,resolve(){const e=m();if(!e)throw new Error(`\u274C Memory MCP server not found
|
|
14
|
+
|
|
15
|
+
Install @zibby/memory:
|
|
16
|
+
npm install @zibby/memory`);const r=o(process.cwd(),".zibby","memory");if(!a(o(r,".dolt")))throw new Error(`\u274C Memory database not initialized
|
|
17
|
+
|
|
18
|
+
Run:
|
|
19
|
+
zibby init --mem`);try{const t=n("dolt",["sql","-q","SELECT COUNT(*) AS cnt FROM test_runs","-r","json"],{cwd:r,encoding:"utf-8",timeout:5e3}),i=JSON.parse(t.trim()).rows||[];if(!i[0]||i[0].cnt===0)return console.log("[memory] Database empty \u2014 memory tools activate after first completed run"),null}catch(t){throw new Error(`\u274C Dolt not found or memory database error
|
|
20
|
+
|
|
21
|
+
Install Dolt:
|
|
22
|
+
https://docs.dolthub.com/introduction/installation
|
|
23
|
+
|
|
24
|
+
Error: ${t.message}`,{cause:t})}return{command:"node",args:[e,"--db-path",r],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"]}}]};export{d as memorySkill};
|
package/dist/sentry.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{resolveIntegrationToken as c}from"@zibby/core/backend-client.js";async function u(o,n={}){const{token:t,organizationSlug:s}=await c("sentry"),i=`https://sentry.io/api/0/organizations/${s}${o}`,e=await fetch(i,{method:n.method||"GET",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"}});if(!e.ok){const a=await e.text().catch(()=>"");throw new Error(`Sentry API ${e.status}: ${a.slice(0,300)}`)}return e.json()}const l={id:"sentry",description:"Sentry error tracking \u2014 projects, issues, events",envKeys:[],promptFragment:`## Sentry (connected)
|
|
2
|
+
You have access to the user's Sentry. Use these tools:
|
|
3
|
+
- sentry_list_projects: List projects in the organization
|
|
4
|
+
- sentry_list_issues: List errors/issues (supports query, project filter, sort)
|
|
5
|
+
- sentry_get_issue: Get detailed info about a specific issue`,resolve(){return null},async handleToolCall(o,n){try{switch(o){case"sentry_list_projects":{const t=await u("/projects/?per_page=50");return JSON.stringify({projects:t.map(s=>({slug:s.slug,name:s.name,platform:s.platform}))})}case"sentry_list_issues":{const t=n.project||"",s=n.query||"is:unresolved",i=n.sort||"date";let e=`/issues/?query=${encodeURIComponent(s)}&sort=${i}&per_page=${n.limit||25}`;t&&(e+=`&project=${encodeURIComponent(t)}`);const a=await u(e);return JSON.stringify({issues:a.map(r=>({id:r.id,title:r.title,culprit:r.culprit,count:r.count,firstSeen:r.firstSeen,lastSeen:r.lastSeen,level:r.level,status:r.status}))})}case"sentry_get_issue":{const{issueId:t}=n;if(!t)return JSON.stringify({error:"issueId is required"});const{token:s}=await c("sentry"),i=await fetch(`https://sentry.io/api/0/issues/${t}/`,{headers:{Authorization:`Bearer ${s}`}});if(!i.ok)throw new Error(`Sentry API ${i.status}`);const e=await i.json();return JSON.stringify({id:e.id,title:e.title,culprit:e.culprit,metadata:e.metadata,count:e.count,userCount:e.userCount,firstSeen:e.firstSeen,lastSeen:e.lastSeen,level:e.level,status:e.status,project:{slug:e.project?.slug,name:e.project?.name}})}default:return JSON.stringify({error:`Unknown tool: ${o}`})}}catch(t){return JSON.stringify({error:t.message})}},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"]}}]};export{l as sentrySkill};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import{existsSync as m,readFileSync as b}from"fs";import{homedir as v}from"os";import{join as I}from"path";import{spawn as w}from"child_process";const r={jira:{description:"Jira issue search, details, comments, transitions",integrationProvider:"jira",envKeys:[],setupInstructions:`To connect Jira:
|
|
2
|
+
1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
|
|
3
|
+
2. Click "Connect Jira" and authorize via Atlassian OAuth
|
|
4
|
+
3. After OAuth completes, ask me to install Jira again`},github:{description:"GitHub issues, PRs, repository management",integrationProvider:"github",envKeys:[],setupInstructions:`To connect GitHub:
|
|
5
|
+
1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
|
|
6
|
+
2. Click "Connect GitHub" and authorize
|
|
7
|
+
3. After OAuth completes, ask me to install GitHub again`},slack:{description:"Slack messages, channels, reactions",integrationProvider:"slack",envKeys:[],setupInstructions:`To connect Slack:
|
|
8
|
+
1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
|
|
9
|
+
2. Click "Connect Slack" and authorize
|
|
10
|
+
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:
|
|
11
|
+
1. Go to Settings \u2192 Integrations (https://studio.zibby.app/integrations)
|
|
12
|
+
2. Click "Connect Sentry" and authorize
|
|
13
|
+
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 S(){const t=["## Available Skills"];for(const[s,n]of Object.entries(r)){const o=n.integrationProvider?`integration: ${n.integrationProvider}`:"ready";t.push(`- ${s}: ${n.description} [${o}]`)}return t.push(""),t.push("Use the install_skill / uninstall_skill / list_available_skills tools to manage skills."),t.push(`Zibby third party Integration settings page: ${p()}`),t.push(""),t.push("## Tool-First Policy (mandatory)"),t.push("CRITICAL RULES \u2014 follow these strictly:"),t.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."),t.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.`),t.push("3. If install_skill reports needsIntegration, tell the user to connect via the integration URL and try again after."),t.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."),t.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."),t.join(`
|
|
14
|
+
`)}function _(){if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{const t=I(v(),".zibby","config.json");return m(t)&&JSON.parse(b(t,"utf-8")).sessionToken||null}catch{return null}}function O(){return(process.env.ZIBBY_API_URL||process.env.ZIBBY_PROD_API_URL||"https://api-prod.zibby.app").replace(/\/$/,"")}function p(){return`${(process.env.ZIBBY_FRONTEND_URL||process.env.ZIBBY_PROD_FRONTEND_URL||"https://studio.zibby.app").replace(/\/$/,"")}/integrations`}function P(t){try{const s=process.platform;return w(s==="darwin"?"open":s==="win32"?"cmd":"xdg-open",s==="win32"?["/c","start","",t]:[t],{detached:!0,stdio:"ignore"}).unref(),!0}catch{return!1}}async function R(){const t=_();if(!t)return{checked:!1,statuses:null,reason:"no-session-token"};try{const s=await fetch(`${O()}/integrations/status`,{method:"GET",headers:{Authorization:`Bearer ${t}`}});return s.ok?{checked:!0,statuses:await s.json()||{},reason:null}:{checked:!1,statuses:null,reason:`status-${s.status}`}}catch{return{checked:!1,statuses:null,reason:"network-error"}}}function g(t,s){if(!s||!t)return{connected:null};const n=t[s];return!n||typeof n.connected!="boolean"?{connected:null,details:n||null}:{connected:n.connected,details:n}}const j={id:"skill-installer",description:"Live skill installation for chat sessions",envKeys:[],catalog:r,promptFragment:S,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(t,s,n){const{activeSkills:o}=n,d=await R();if(t==="list_available_skills"){const e=Object.entries(r).map(([i,a])=>{const h=o.includes(i),c=g(d.statuses,a.integrationProvider);return{id:i,description:a.description,installed:h,integrationProvider:a.integrationProvider||void 0,integrationConnected:c.connected,setupInstructions:c.connected===!1?a.setupInstructions:void 0}});return JSON.stringify({skills:e})}if(t==="install_skill"){const{skillId:e}=s;if(!e)return JSON.stringify({ok:!1,error:"skillId is required"});if(o.includes(e)){const l=r[e],{getSkill:u}=await import("@zibby/core/framework/skill-registry.js"),k=(u(e)?.tools||[]).map(y=>y.name);return JSON.stringify({ok:!0,alreadyInstalled:!0,skillId:e,description:l?.description,availableTools:k,integrationUrl:l?.integrationProvider?p():void 0,hint:`${e} is already active. Tools available: ${k.join(", ")}. Use them directly.`})}if(!r[e])return JSON.stringify({ok:!1,error:`Unknown skill "${e}". Available: ${Object.keys(r).join(", ")}`});const i=r[e];if(i.integrationProvider){const l=g(d.statuses,i.integrationProvider),u=p();if(d.checked&&l.connected===!1){const f=P(u);return JSON.stringify({ok:!1,error:`${i.integrationProvider} is not connected for this Zibby account yet`,needsIntegration:!0,integrationUrl:u,openedBrowser:f,setupInstructions:`Please connect ${i.integrationProvider} first at ${u}. After you finish OAuth, ask me to install ${e} again.`})}}o.push(e);const{getSkill:a}=await import("@zibby/core/framework/skill-registry.js"),c=(a(e)?.tools||[]).map(l=>l.name);return JSON.stringify({ok:!0,installed:e,description:i.description,availableTools:c,hint:`${e} is now active. You now have these tools: ${c.join(", ")}. Use them immediately to help the user \u2014 don't just confirm installation.`})}if(t==="uninstall_skill"){const{skillId:e}=s;if(!e)return JSON.stringify({ok:!1,error:"skillId is required"});if(e==="skill-installer")return JSON.stringify({ok:!1,error:"Cannot uninstall the skill installer"});const i=o.indexOf(e);return i===-1?JSON.stringify({ok:!1,error:`${e} is not installed`}):(o.splice(i,1),JSON.stringify({ok:!0,uninstalled:e}))}return JSON.stringify({error:`Unknown tool: ${t}`})},resolve(){return null}};export{j as skillInstallerSkill};
|
package/dist/slack.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{resolveIntegrationToken as p}from"@zibby/core/backend-client.js";async function n(r,e={}){const{token:s}=await p("slack"),t=["conversations.list","users.list","users.profile.get","conversations.history","conversations.replies"].includes(r);let i=`https://slack.com/api/${r}`;const c={Authorization:`Bearer ${s}`};let o;if(t){const l=new URLSearchParams(e).toString();l&&(i+=`?${l}`)}else c["Content-Type"]="application/json; charset=utf-8",o=JSON.stringify(e);const a=await(await fetch(i,{method:t?"GET":"POST",headers:c,body:o})).json();if(!a.ok)throw new Error(`Slack API error: ${a.error}`);return a}const _={id:"slack",serverName:"slack",allowedTools:["mcp__slack__*"],envKeys:["SLACK_BOT_TOKEN","SLACK_TEAM_ID"],description:"Slack MCP Server",promptFragment:`## Slack (connected)
|
|
2
|
+
You have access to the user's Slack workspace. Use these tools:
|
|
3
|
+
- slack_list_channels, slack_post_message, slack_reply_to_thread
|
|
4
|
+
- slack_add_reaction, slack_get_channel_history, slack_get_thread_replies
|
|
5
|
+
- slack_get_users, slack_get_user_profile`,resolve(){const r={};for(const e of this.envKeys)process.env[e]&&(r[e]=process.env[e]);return{command:"npx",args:["-y","@modelcontextprotocol/server-slack@latest"],env:r}},async handleToolCall(r,e){try{switch(r){case"slack_list_channels":{const s=await n("conversations.list",{types:"public_channel",limit:100});return JSON.stringify({channels:(s.channels||[]).map(t=>({id:t.id,name:t.name,topic:t.topic?.value}))})}case"slack_post_message":{if(!e.channel||!e.text)return JSON.stringify({error:"channel and text are required"});const s=await n("chat.postMessage",{channel:e.channel,text:e.text});return JSON.stringify({ok:!0,ts:s.ts,channel:s.channel})}case"slack_reply_to_thread":{if(!e.channel||!e.thread_ts||!e.text)return JSON.stringify({error:"channel, thread_ts, and text are required"});const s=await n("chat.postMessage",{channel:e.channel,thread_ts:e.thread_ts,text:e.text});return JSON.stringify({ok:!0,ts:s.ts})}case"slack_add_reaction":return!e.channel||!e.timestamp||!e.reaction?JSON.stringify({error:"channel, timestamp, and reaction are required"}):(await n("reactions.add",{channel:e.channel,timestamp:e.timestamp,name:e.reaction}),JSON.stringify({ok:!0}));case"slack_get_channel_history":{if(!e.channel)return JSON.stringify({error:"channel is required"});const s=await n("conversations.history",{channel:e.channel,limit:e.limit||20});return JSON.stringify({messages:(s.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_thread_replies":{if(!e.channel||!e.thread_ts)return JSON.stringify({error:"channel and thread_ts are required"});const s=await n("conversations.replies",{channel:e.channel,ts:e.thread_ts});return JSON.stringify({messages:(s.messages||[]).map(t=>({user:t.user,text:t.text,ts:t.ts}))})}case"slack_get_users":{const s=await n("users.list",{limit:100});return JSON.stringify({users:(s.members||[]).filter(t=>!t.is_bot&&!t.deleted).map(t=>({id:t.id,name:t.real_name||t.name}))})}case"slack_get_user_profile":{if(!e.user_id)return JSON.stringify({error:"user_id is required"});const s=await n("users.profile.get",{user:e.user_id});return JSON.stringify({profile:s.profile})}default:return JSON.stringify({error:`Unknown tool: ${r}`})}}catch(s){return JSON.stringify({error:s.message})}},tools:[{name:"slack_list_channels",description:"List public channels in the workspace",input_schema:{type:"object",properties:{}}},{name:"slack_post_message",description:"Post a message to a Slack channel or DM",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID or name"},text:{type:"string",description:"Message text"}},required:["channel","text"]}},{name:"slack_reply_to_thread",description:"Reply to a specific message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"},text:{type:"string",description:"Reply text"}},required:["channel","thread_ts","text"]}},{name:"slack_add_reaction",description:"Add an emoji reaction to a message",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},timestamp:{type:"string",description:"Message timestamp"},reaction:{type:"string",description:"Emoji name without colons"}},required:["channel","timestamp","reaction"]}},{name:"slack_get_channel_history",description:"Get recent messages from a channel",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},limit:{type:"number",description:"Number of messages"}},required:["channel"]}},{name:"slack_get_thread_replies",description:"Get all replies in a message thread",input_schema:{type:"object",properties:{channel:{type:"string",description:"Channel ID"},thread_ts:{type:"string",description:"Thread timestamp"}},required:["channel","thread_ts"]}},{name:"slack_get_users",description:"List workspace users with basic profiles",input_schema:{type:"object",properties:{}}},{name:"slack_get_user_profile",description:"Get detailed profile for a specific user",input_schema:{type:"object",properties:{user_id:{type:"string",description:"Slack user ID"}},required:["user_id"]}}]};export{_ as slackSkill};
|