@zibby/core 0.5.24 → 0.5.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -150,7 +150,7 @@ ${b.join(`
150
150
 
151
151
  ${y}
152
152
  `),p.debug(`Prompt length: ${f.length} chars`),process.env.STAGE!=="prod"&&p.debug(`Full prompt:
153
- ${f}`),r.invoke(f,d)}import{SKILLS as qp}from"@zibby/agent-workflow";import{registerSkill as ef,getSkill as tf,hasSkill as rf,getAllSkills as nf,listSkillIds as of}from"@zibby/agent-workflow";var es={READ_FILE:"read_file",WRITE_FILE:"write_file",LIST_DIRECTORY:"list_directory",RUN_COMMAND:"run_command",OPEN_URL:"open_url",WAIT:"wait"},ts={LIST_PROJECTS:"jira_list_projects",SEARCH:"jira_search",GET_ISSUE:"jira_get_issue",CREATE_ISSUE:"jira_create_issue",LIST_SPRINTS:"jira_list_sprints",GET_SPRINT_ISSUES:"jira_get_sprint_issues",GET_COMMENTS:"jira_get_comments",ADD_COMMENT:"jira_add_comment",EDIT_ISSUE:"jira_edit_issue",TRANSITION_ISSUE:"jira_transition_issue"},rs={GET_USER:"github_get_user",LIST_ORGS:"github_list_orgs",LIST_REPOS:"github_list_repos",CLONE:"github_clone",SEARCH_REPOS:"github_search_repos",SEARCH_ISSUES:"github_search_issues",SEARCH_CODE:"github_search_code",GET_PR:"github_get_pr",GET_PR_DIFF:"github_get_pr_diff",LIST_PR_FILES:"github_list_pr_files",LIST_PR_COMMENTS:"github_list_pr_comments",LIST_COMMITS:"github_list_commits",GET_COMMIT:"github_get_commit",GET_FILE:"github_get_file",CREATE_ISSUE:"github_create_issue"},ns={LIST_CHANNELS:"slack_list_channels",POST_MESSAGE:"slack_post_message",REPLY_TO_THREAD:"slack_reply_to_thread",ADD_REACTION:"slack_add_reaction",GET_CHANNEL_HISTORY:"slack_get_channel_history",GET_THREAD_REPLIES:"slack_get_thread_replies",GET_USERS:"slack_get_users",GET_USER_PROFILE:"slack_get_user_profile"},os={GENERATE:"run_generate",TEST:"run_test",STATUS:"run_status",CANCEL:"run_cancel",WAIT:"run_wait",ARTIFACTS:"run_artifacts",LIST_SPECS:"list_specs"},ss={GET_TEST_HISTORY:"memory_get_test_history",GET_SELECTORS:"memory_get_selectors",GET_PAGE_MODEL:"memory_get_page_model",GET_NAVIGATION:"memory_get_navigation",SAVE_INSIGHT:"memory_save_insight"},is={STORE:"memory_store",RECALL:"memory_recall",BRIEF:"memory_brief",END_SESSION:"memory_end_session",TASK_LOG:"task_log",TASK_HISTORY:"task_history"},as={INSTALL:"install_skill",UNINSTALL:"uninstall_skill",LIST_AVAILABLE:"list_available_skills"},cc={...es,...ts,...rs,...ns,...os,...ss,...is,...as};import{existsSync as ls,readFileSync as cs}from"node:fs";import{homedir as us}from"node:os";import{join as ps}from"node:path";var ft=new Map,fs={github:"GITHUB_TOKEN",gitlab:"GITLAB_TOKEN",sentry:"SENTRY_AUTH_TOKEN",slack:"SLACK_BOT_TOKEN"};function ds(){let s=process.env.ZIBBY_SELF_HOST;return s!=null&&s!==""&&s!=="0"&&s.toLowerCase()!=="false"}function ms(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let s=ps(us(),".zibby","config.json");return ls(s)&&JSON.parse(cs(s,"utf-8")).sessionToken||null}catch{return null}}function gs(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}async function hs(s){if(ds()){let a=fs[s];if(a&&process.env[a])return{token:process.env[a]}}let e=Date.now(),t=ft.get(s);if(t&&t.expiresAt>e)return t.data;let r=ms();if(!r)throw new Error("No session token. Run `zibby login` first.");let n=`${gs()}/integrations/token/${s}`,o=await fetch(n,{method:"GET",headers:{Authorization:`Bearer ${r}`}});if(!o.ok){let a=await o.text().catch(()=>"");throw o.status===404?new Error(`${s} is not connected. Connect it at https://studio.zibby.dev/integrations`):o.status===401||o.status===403?new Error("Session expired. Run `zibby login` to re-authenticate."):new Error(`Failed to resolve ${s} token (${o.status}): ${a}`)}let i=await o.json();if(!i||typeof i!="object")throw new Error(`Invalid response from ${s} token endpoint: expected object, got ${typeof i}`);if(s==="jira"){if(!i.token||typeof i.token!="string")throw new Error(`Invalid jira token response: token is ${typeof i.token}, expected string`);if(!i.cloudId)throw new Error("Invalid jira token response: missing cloudId")}else if(s==="github"&&(!i.token||typeof i.token!="string"))throw new Error(`Invalid github token response: token is ${typeof i.token}, expected string`);let c=((i.expiresInSec||3e3)-120)*1e3;return ft.set(s,{data:i,expiresAt:e+c}),i}function ys(s){s?ft.delete(s):ft.clear()}import{readdir as bs,access as kr,copyFile as Ss,constants as Nr}from"fs/promises";import{join as dt,relative as ws}from"node:path";async function _s(s={}){let{testResultsDir:e="test-results",testsDir:t="tests",projectRoot:r=process.cwd(),verbose:n=!0}=s;n&&console.log(`\u{1F3A5} Organizing test videos...
153
+ ${f}`),r.invoke(f,d)}import{SKILL_IDS as qp}from"@zibby/skill-ids";import{registerSkill as ef,getSkill as tf,hasSkill as rf,getAllSkills as nf,listSkillIds as of}from"@zibby/agent-workflow";var es={READ_FILE:"read_file",WRITE_FILE:"write_file",LIST_DIRECTORY:"list_directory",RUN_COMMAND:"run_command",OPEN_URL:"open_url",WAIT:"wait"},ts={LIST_PROJECTS:"jira_list_projects",SEARCH:"jira_search",GET_ISSUE:"jira_get_issue",CREATE_ISSUE:"jira_create_issue",LIST_SPRINTS:"jira_list_sprints",GET_SPRINT_ISSUES:"jira_get_sprint_issues",GET_COMMENTS:"jira_get_comments",ADD_COMMENT:"jira_add_comment",EDIT_ISSUE:"jira_edit_issue",TRANSITION_ISSUE:"jira_transition_issue"},rs={GET_USER:"github_get_user",LIST_ORGS:"github_list_orgs",LIST_REPOS:"github_list_repos",CLONE:"github_clone",SEARCH_REPOS:"github_search_repos",SEARCH_ISSUES:"github_search_issues",SEARCH_CODE:"github_search_code",GET_PR:"github_get_pr",GET_PR_DIFF:"github_get_pr_diff",LIST_PR_FILES:"github_list_pr_files",LIST_PR_COMMENTS:"github_list_pr_comments",LIST_COMMITS:"github_list_commits",GET_COMMIT:"github_get_commit",GET_FILE:"github_get_file",CREATE_ISSUE:"github_create_issue"},ns={LIST_CHANNELS:"slack_list_channels",POST_MESSAGE:"slack_post_message",REPLY_TO_THREAD:"slack_reply_to_thread",ADD_REACTION:"slack_add_reaction",GET_CHANNEL_HISTORY:"slack_get_channel_history",GET_THREAD_REPLIES:"slack_get_thread_replies",GET_USERS:"slack_get_users",GET_USER_PROFILE:"slack_get_user_profile"},os={GENERATE:"run_generate",TEST:"run_test",STATUS:"run_status",CANCEL:"run_cancel",WAIT:"run_wait",ARTIFACTS:"run_artifacts",LIST_SPECS:"list_specs"},ss={GET_TEST_HISTORY:"memory_get_test_history",GET_SELECTORS:"memory_get_selectors",GET_PAGE_MODEL:"memory_get_page_model",GET_NAVIGATION:"memory_get_navigation",SAVE_INSIGHT:"memory_save_insight"},is={STORE:"memory_store",RECALL:"memory_recall",BRIEF:"memory_brief",END_SESSION:"memory_end_session",TASK_LOG:"task_log",TASK_HISTORY:"task_history"},as={INSTALL:"install_skill",UNINSTALL:"uninstall_skill",LIST_AVAILABLE:"list_available_skills"},cc={...es,...ts,...rs,...ns,...os,...ss,...is,...as};import{existsSync as ls,readFileSync as cs}from"node:fs";import{homedir as us}from"node:os";import{join as ps}from"node:path";var ft=new Map,fs={github:"GITHUB_TOKEN",gitlab:"GITLAB_TOKEN",sentry:"SENTRY_AUTH_TOKEN",slack:"SLACK_BOT_TOKEN"};function ds(){let s=process.env.ZIBBY_SELF_HOST;return s!=null&&s!==""&&s!=="0"&&s.toLowerCase()!=="false"}function ms(){if(process.env.PROJECT_API_TOKEN)return process.env.PROJECT_API_TOKEN;if(process.env.ZIBBY_USER_TOKEN)return process.env.ZIBBY_USER_TOKEN;try{let s=ps(us(),".zibby","config.json");return ls(s)&&JSON.parse(cs(s,"utf-8")).sessionToken||null}catch{return null}}function gs(){return process.env.ZIBBY_ACCOUNT_API_URL?process.env.ZIBBY_ACCOUNT_API_URL.replace(/\/$/,""):(process.env.ZIBBY_ENV||"prod")==="local"?"http://localhost:3001":process.env.ZIBBY_PROD_ACCOUNT_API_URL||"https://api-prod.zibby.app"}async function hs(s){if(ds()){let a=fs[s];if(a&&process.env[a])return{token:process.env[a]}}let e=Date.now(),t=ft.get(s);if(t&&t.expiresAt>e)return t.data;let r=ms();if(!r)throw new Error("No session token. Run `zibby login` first.");let n=`${gs()}/integrations/token/${s}`,o=await fetch(n,{method:"GET",headers:{Authorization:`Bearer ${r}`}});if(!o.ok){let a=await o.text().catch(()=>"");throw o.status===404?new Error(`${s} is not connected. Connect it at https://studio.zibby.dev/integrations`):o.status===401||o.status===403?new Error("Session expired. Run `zibby login` to re-authenticate."):new Error(`Failed to resolve ${s} token (${o.status}): ${a}`)}let i=await o.json();if(!i||typeof i!="object")throw new Error(`Invalid response from ${s} token endpoint: expected object, got ${typeof i}`);if(s==="jira"){if(!i.token||typeof i.token!="string")throw new Error(`Invalid jira token response: token is ${typeof i.token}, expected string`);if(!i.cloudId)throw new Error("Invalid jira token response: missing cloudId")}else if(s==="github"&&(!i.token||typeof i.token!="string"))throw new Error(`Invalid github token response: token is ${typeof i.token}, expected string`);let c=((i.expiresInSec||3e3)-120)*1e3;return ft.set(s,{data:i,expiresAt:e+c}),i}function ys(s){s?ft.delete(s):ft.clear()}import{readdir as bs,access as kr,copyFile as Ss,constants as Nr}from"fs/promises";import{join as dt,relative as ws}from"node:path";async function _s(s={}){let{testResultsDir:e="test-results",testsDir:t="tests",projectRoot:r=process.cwd(),verbose:n=!0}=s;n&&console.log(`\u{1F3A5} Organizing test videos...
154
154
  `);let o=dt(r,e),i=dt(r,t);try{let c=await bs(o),a=0;for(let l of c){if(l.startsWith("."))continue;let u=dt(o,l,"video.webm");try{await kr(u,Nr.F_OK)}catch{continue}let d=l.replace(/-chromium$/,"").replace(/-firefox$/,"").replace(/-webkit$/,""),m=await xs(i,d);if(m){let f=m.replace(/\.spec\.(js|ts)$/,".spec.webm");await Ss(u,f),n&&console.log(`\u2705 ${ws(r,f)}`),a++}else n&&console.log(`\u26A0\uFE0F Could not find test file for: ${l}`)}return n&&(console.log(`
155
155
  \u{1F3AC} Organized ${a} video(s)`),console.log(`\u{1F4C2} Videos are now next to their test files in ${t}/`)),{movedCount:a,success:!0}}catch(c){return n&&console.error("\u274C Error organizing videos:",c.message),{movedCount:0,success:!1,error:c.message}}}async function xs(s,e){let t=e.split("-");for(let r=t.length;r>0;r--){let o=t.slice(0,r).join("/");for(let i of["js","ts"]){let c=dt(s,`${o}.spec.${i}`);try{return await kr(c,Nr.F_OK),c}catch{}}}return null}var Ye=class{constructor(e,t={}){this.apiKey=e,this.baseUrl=t.baseUrl||process.env.ZIBBY_API_URL||"https://api-prod.zibby.app",this.enabled=!!e}isEnabled(){return this.enabled}};function Rr(){let s=process.env.ZIBBY_API_KEY;return new Ye(s)}import{execSync as Es}from"node:child_process";import{existsSync as vs,mkdirSync as Ts}from"node:fs";import{join as As}from"node:path";async function $s(s={}){let{baseDir:e="/workspace/repos",repos:t=null,depth:r=1,branch:n=null}=s,o=process.env.REPOS;if(!o)throw new Error("REPOS environment variable not set. Are you running in a Zibby workflow container?");let i;try{i=JSON.parse(o)}catch(l){throw new Error(`Failed to parse REPOS env var: ${l.message}`,{cause:l})}if(!Array.isArray(i)||i.length===0)throw new Error("No repositories configured for this project");let c=t?i.filter(l=>t.includes(l.name)):i;if(c.length===0)throw new Error(`No matching repositories found. Available: ${i.map(l=>l.name).join(", ")}`);vs(e)||Ts(e,{recursive:!0});let a={};return await Promise.all(c.map(async l=>{let u=l.provider||"github",d=u==="gitlab"?process.env.GITLAB_TOKEN:process.env.GITHUB_TOKEN;if(!d){console.error(`${u.toUpperCase()}_TOKEN not set, skipping ${l.name}`),a[l.name]=null;return}let m=l.name.replace(/\//g,"-"),f=As(e,m),g=l.cloneUrl||l.url;if(!g){console.error(`Repository "${l.name}" has no clone URL`),a[l.name]=null;return}let y;u==="gitlab"?y=g.replace(/^https:\/\//,`https://oauth2:${d}@`):y=g.replace(/^https:\/\//,`https://x-access-token:${d}@`);let h=["clone"];r>0&&h.push("--depth",r.toString()),n&&h.push("--branch",n),h.push(y,f);let b=`git ${h.join(" ")}`;console.log(`Cloning ${l.name} (${u}) to ${f}...`);try{Es(b,{stdio:"pipe",env:{...process.env,GIT_TERMINAL_PROMPT:"0"}}),console.log(`Repository ${l.name} cloned successfully`),a[l.name]=f}catch(_){let E=_.message.replace(d,"***").replace(y,g);console.error(`Failed to clone ${l.name}: ${E}`),a[l.name]=null}})),a}var Lr=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i;function Os(s){return typeof s=="string"&&Lr.test(s)}var Mr=Os;function Is(s){if(!Mr(s))throw TypeError("Invalid UUID");let e;return Uint8Array.of((e=parseInt(s.slice(0,8),16))>>>24,e>>>16&255,e>>>8&255,e&255,(e=parseInt(s.slice(9,13),16))>>>8,e&255,(e=parseInt(s.slice(14,18),16))>>>8,e&255,(e=parseInt(s.slice(19,23),16))>>>8,e&255,(e=parseInt(s.slice(24,36),16))/1099511627776&255,e/4294967296&255,e>>>24&255,e>>>16&255,e>>>8&255,e&255)}var Rt=Is;var J=[];for(let s=0;s<256;++s)J.push((s+256).toString(16).slice(1));function Dr(s,e=0){return(J[s[e+0]]+J[s[e+1]]+J[s[e+2]]+J[s[e+3]]+"-"+J[s[e+4]]+J[s[e+5]]+"-"+J[s[e+6]]+J[s[e+7]]+"-"+J[s[e+8]]+J[s[e+9]]+"-"+J[s[e+10]]+J[s[e+11]]+J[s[e+12]]+J[s[e+13]]+J[s[e+14]]+J[s[e+15]]).toLowerCase()}function Cs(s){s=unescape(encodeURIComponent(s));let e=new Uint8Array(s.length);for(let t=0;t<s.length;++t)e[t]=s.charCodeAt(t);return e}var Ur="6ba7b810-9dad-11d1-80b4-00c04fd430c8",jr="6ba7b811-9dad-11d1-80b4-00c04fd430c8";function Lt(s,e,t,r,n,o){let i=typeof t=="string"?Cs(t):t,c=typeof r=="string"?Rt(r):r;if(typeof r=="string"&&(r=Rt(r)),r?.length!==16)throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)");let a=new Uint8Array(16+i.length);if(a.set(c),a.set(i,c.length),a=e(a),a[6]=a[6]&15|s,a[8]=a[8]&63|128,n){if(o=o||0,o<0||o+16>n.length)throw new RangeError(`UUID byte range ${o}:${o+15} is out of buffer bounds`);for(let l=0;l<16;++l)n[o+l]=a[l];return n}return Dr(a)}import{createHash as Ps}from"crypto";function ks(s){return Array.isArray(s)?s=Buffer.from(s):typeof s=="string"&&(s=Buffer.from(s,"utf8")),Ps("sha1").update(s).digest()}var Fr=ks;function Mt(s,e,t,r){return Lt(80,Fr,s,e,t,r)}Mt.DNS=Ur;Mt.URL=jr;var Dt=Mt;var Br="@zibby/core ZibbySessionStore/1",He=class{constructor({apiUrl:e,apiKey:t,workflowUuid:r,fetchImpl:n=globalThis.fetch}){if(!e)throw new Error("ZibbySessionStore: apiUrl is required");if(!t)throw new Error("ZibbySessionStore: apiKey is required");if(!r)throw new Error("ZibbySessionStore: workflowUuid is required");this._apiUrl=String(e).replace(/\/+$/,""),this._apiKey=t,this._workflowUuid=r,this._fetch=n}async append(e,t){if(!Array.isArray(t)||t.length===0)return;let r=`${this._apiUrl}/workflows/${encodeURIComponent(this._workflowUuid)}/sessions/${encodeURIComponent(e.sessionId)}/append`,n=await this._fetch(r,{method:"POST",headers:{Authorization:`Bearer ${this._apiKey}`,"Content-Type":"application/json","User-Agent":Br},body:JSON.stringify({entries:t})});if(!n.ok)throw new Error(`ZibbySessionStore.append HTTP ${n.status}`)}async load(e){let t=`${this._apiUrl}/workflows/${encodeURIComponent(this._workflowUuid)}/sessions/${encodeURIComponent(e.sessionId)}`,r;try{r=await this._fetch(t,{method:"GET",headers:{Authorization:`Bearer ${this._apiKey}`,"User-Agent":Br}})}catch{return null}if(r.status===404||!r.ok)return null;let n;try{n=await r.json()}catch{return null}let o=Array.isArray(n?.entries)?n.entries:null;return o&&o.length>0?o:null}};var Ns="6ba7b810-9dad-11d1-80b4-00c04fd430c8",Gr=new Set;function Rs(){return(process.env.PROGRESS_API_URL||process.env.ZIBBY_API_BASE||"").replace(/\/executions\/?$/,"").replace(/\/+$/,"")}function Kr({namespace:s=Ns}={}){return{id:"session",description:"Persist agent conversation state across invocations (Claude-only in v1)",envKeys:[],tools:[],invokeAgentOptions(e,t){let r=e?.conversationId,n=e?.workflowUuid;if(!r||!n)return null;let o=t?.agentType;if(o&&o!=="claude")return Gr.has(o)||(console.warn(`[SKILLS.SESSION] agent type '${o}' does not support native sessions \u2014 node running stateless`),Gr.add(o)),null;let i=Dt(`${n}:${r}`,s),c=Rs(),a=process.env.PROJECT_API_TOKEN||process.env.ZIBBY_USER_TOKEN,l=null;if(c&&a)try{l=new He({apiUrl:c,apiKey:a,workflowUuid:n})}catch(m){console.warn(`[SKILLS.SESSION] failed to build SessionStore: ${m.message}`),l=null}return e.resetConversation?{sessionId:i,resume:i,forkSession:!0,...l&&{sessionStore:l}}:e.hasPriorSession?{resume:i,...l&&{sessionStore:l}}:{sessionId:i,...l&&{sessionStore:l}}}}}import{spawn as Jr}from"node:child_process";import{readFileSync as Ls,writeFileSync as Ms,existsSync as mt}from"node:fs";import{homedir as zr}from"node:os";import{join as ve}from"node:path";async function Ds(){let s=ve(zr(),".local/share/cursor-agent/versions");if(!mt(s))throw new Error(`cursor-agent not found at ${s}. Please install cursor-agent first.`);return console.log(`\u{1F527} Patching cursor-agent for CI/CD...
156
156
  `),new Promise((e,t)=>{let r=ve(__dirname,"../../scripts/patch-cursor-mcp.py");if(!mt(r)){t(new Error("Patch script not found"));return}let n=Jr("python3",[r],{stdio:"inherit"});n.on("close",o=>{o===0?e({success:!0}):t(new Error(`Patch failed with code ${o}`))}),n.on("error",o=>{t(o)})})}function Us(){let s=ve(zr(),".local/share/cursor-agent/versions");if(!mt(s))return{patched:!1,installed:!1};try{let e=fn("fs").readdirSync(s);if(e.length===0)return{patched:!1,installed:!1};let t=e.sort().reverse()[0],r=ve(s,t,"index.js");return mt(r)?{patched:Ls(r,"utf-8").includes("AUTO-APPROVE MCP TOOLS FOR CI/CD"),installed:!0,path:r}:{patched:!1,installed:!1}}catch(e){return{patched:!1,installed:!1,error:e.message}}}async function js(s){return console.log(`\u{1F511} Getting MCP approval keys...
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/core",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
4
4
  "description": "Core test automation engine with multi-agent and multi-MCP support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -70,6 +70,7 @@
70
70
  "@playwright/mcp": "^0.0.70",
71
71
  "@zibby/agent-workflow": "^0.4.13",
72
72
  "@zibby/mcp-browser": "^0.1.8",
73
+ "@zibby/skill-ids": "^0.1.0",
73
74
  "acorn": "^8.15.0",
74
75
  "acorn-walk": "^8.3.5",
75
76
  "axios": "^1.15.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zibby/core",
3
- "version": "0.5.24",
3
+ "version": "0.5.25",
4
4
  "description": "Core test automation engine with multi-agent and multi-MCP support",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -70,6 +70,7 @@
70
70
  "@playwright/mcp": "^0.0.70",
71
71
  "@zibby/agent-workflow": "^0.4.13",
72
72
  "@zibby/mcp-browser": "^0.1.8",
73
+ "@zibby/skill-ids": "^0.1.0",
73
74
  "acorn": "^8.15.0",
74
75
  "acorn-walk": "^8.3.5",
75
76
  "axios": "^1.15.0",