diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e553007..7e4e689 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "name": "genesys-cloud-architect", "source": "./", "description": "Create, test, and debug Genesys Cloud Architect flows", - "version": "1.0.8" + "version": "1.0.9" } ] } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 7adfcd8..2796ca9 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "genesys-cloud-architect", "description": "Create, test, and debug Genesys Cloud Architect flows", - "version": "1.0.8", + "version": "1.0.9", "author": { "name": "Lucas Woodward", "url": "https://makingchatbots.com/" diff --git a/.mcp.dev.json b/.mcp.dev.json new file mode 100644 index 0000000..08df76b --- /dev/null +++ b/.mcp.dev.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "genesys-cloud-architect-mcp": { + "command": "node", + "args": ["${CLAUDE_PLUGIN_ROOT}/servers/genesys-cloud-architect-mcp.js"], + "env": { + "GENESYS_REGION": "${GENESYS_REGION}", + "GENESYS_CLIENT_ID": "${GENESYS_CLIENT_ID}", + "GENESYS_CLIENT_SECRET": "${GENESYS_CLIENT_SECRET}", + "DEPLOY_SCRIPT_PATH": "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-runner.js" + } + } + } +} diff --git a/.mcp.json b/.mcp.json index b712a38..a5426b2 100644 --- a/.mcp.json +++ b/.mcp.json @@ -6,7 +6,7 @@ "env": { "GENESYS_REGION": "${user_config.genesys_region}", "GENESYS_CLIENT_ID": "${user_config.genesys_client_id}", - "GENESYS_CLIENT_SECRET": "${user_config.genesys_client_secret}", + "GENESYS_CLIENT_SECRET": "${user_config.genesys_client_secret}", "DEPLOY_SCRIPT_PATH": "${CLAUDE_PLUGIN_ROOT}/scripts/deploy-runner.js" } } diff --git a/README.md b/README.md index d75dd18..d1cac83 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,121 @@ -# Genesys Cloud Architect Claude Code Plugin +# Claude Code Plugin: Genesys Cloud Architect -Create, debug and test Genesys Cloud Architect Flows using Claude Code. +

+ +

-**This is under heavy development. Feedback is welcome!** +Use Claude Code to create, test, diagnose and document your Genesys Cloud's Architect flows. + +This plugin allows you to: + +* [Create architect flows of any type](#create-architect-flows-of-any-type) +* [Run automated tests against Digital flows](#run-automated-tests-against-digital-flows) +* [Create and test flow expressions](#create-and-test-flow-expressions) +* [Inspect and fix issues in flows](#inspect-and-fix-issues-in-flows) +* [Document flows](#document-flows) +* _and more..._ + +## Getting started + +Follow the [installation guide](#installation), then simply tell Claude Code what you want it to do. + +Below are examples of each of its capabilities: + +### Create architect flows of any type + +Asking Claude Code to create a flow will have it create, publish and test a flow using the [Architect Scripting SDK](https://mypurecloud.github.io/purecloud-flow-scripting-api-sdk-javascript/). + +Below is an example of a simple flow, but they can be much more complex: + +> Create a Bank bot flow with two intents: "Check Account Balance" (collects an 8-digit AccountNumber slot) and "Find a Branch" (collects a 5-digit ZipCode slot). +> +> The bot: +> 1. Asks "What would you like to do?" +> 2. Detects the intent +> 3. Then asks for the relevant slot +> 4. Exit the bot flow after slot collection +> +> Include 6 utterances per intent with entity-annotated examples. +> Add intent confirmation prompts like "I think you want to [intent], is that correct?" +> +> Publish the flow, and test it frequently as you build it. + +Resulting in a flow: + + + +[Read more...](https://makingchatbots.com/i/200764669/create-your-flows-with-ai) + +### Run automated tests against Digital flows + +The plugin allows Claude Code to run tests against Digital bot flows. This is useful when +it's developing flows, or simply to test for edge-cases in existing flows: + +> Inspect the 'Bank bot' flow and run tests against it to ensure it behaves as expected. + + + +### Create and test flow expressions + +> My Genesys Architect flow needs to extract the 'author' from the JSON retrieved from a participant attribute below: +> +> { "newsletter": {"makingchatbots": {"author": "Lucas Woodward "}} +> +> Create an expression that returns the value of 'author'. However, if the property (or any of the parent properties) do not exist then return an empty value. +> +> Create a Digital Bot flow to test your expression against different test cases. + + + + +[Read more...](https://makingchatbots.com/i/200764669/create-and-test-expressions) + +### Inspect and fix issues in flows + +TODO Add example + +### Document flows + +TODO Add example ## Installation -``` -# Add the marketplace -/plugin marketplace add MakingChatbots/genesys-cloud-plugins +1. Open Claude Code +2. Type the following to add the marketplace and install the plugin: + 1. Add the marketplace + ``` + /plugin marketplace add MakingChatbots/genesys-cloud-plugins + ``` -# Install the plugin -/plugin install genesys-cloud-architect@makingchatbots-genesys-cloud-plugins -``` + 2. Install the plugin + ``` + /plugin install genesys-cloud-architect@makingchatbots-genesys-cloud-plugins + ``` +3. When asked, provide the Credentials for an OAuth Client with the following permissions: + * `Architect > Flow > *` + * `Architect > Job > *` + * `Architect > UI > *` + * `Language Understanding > NLU Domain Version > View` + * `Textbots > *` + * `Architect > Dependency Tracking > View` -## Getting Started +## Who built this? -Once you've installed the plugin you can start working with Claude Code to create your Architect flows. +This is built by [Lucas Woodward](https://makingchatbots.com/about#§who-am-i). -Try some of the following examples: +I've been building this in public - engaging with the Genesys community with each milestone. If you'd like to keep up +to date with releases then [follow me on LinkedIn](https://www.linkedin.com/in/lucas-woodward-the-dev/). -> Create a Digital Chatbot that asks the customer for their name, then welcomes them by their name. +What else I have built: +* [Genesys Cloud MCP Server](https://github.com/MakingChatbots/genesys-cloud-mcp-server) +* [Genesys Cloud n8n community node](https://github.com/MakingChatbots/n8n-nodes-genesys-cloud) +* [Genesys Cloud Chatbot Tester](https://github.com/MakingChatbots/genesys-cloud-chatbot-tester) +* _many more on [my newsletter...](https://makingchatbots.com/)_ ## Development Docs to help understand how this works, or contribute: * [docs/development.md](docs/development.md) -* [docs/architecture.md](docs/architecture.md) +* [docs/architectural-decisions.md](docs/architectural-decisions.md) diff --git a/docs/architecture.md b/docs/architectural-decisions.md similarity index 100% rename from docs/architecture.md rename to docs/architectural-decisions.md diff --git a/docs/assets/expression-result.png b/docs/assets/expression-result.png new file mode 100644 index 0000000..b36ac92 Binary files /dev/null and b/docs/assets/expression-result.png differ diff --git a/docs/assets/flow.png b/docs/assets/flow.png new file mode 100644 index 0000000..61cde9f Binary files /dev/null and b/docs/assets/flow.png differ diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000..5bd3df0 Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/docs/assets/running-tests.png b/docs/assets/running-tests.png new file mode 100644 index 0000000..16b7592 Binary files /dev/null and b/docs/assets/running-tests.png differ diff --git a/docs/development.md b/docs/development.md index e800cae..be5d735 100644 --- a/docs/development.md +++ b/docs/development.md @@ -3,15 +3,28 @@ Test the plugin locally in the Claude Code CLI: ```shell -CLAUDE_PLUGIN_ROOT=$(pwd) claude --plugin-dir . +CLAUDE_PLUGIN_ROOT=$(pwd) claude --plugin-dir . --mcp-config ./.mcp.dev.json --debug ``` -Debug the plugin: +Notable options: + * `--mcp-config ./.mcp.dev.json` - loads the MCP using a environment vars, rather than User Config keys that aren't + supported when running the plugin locally + * `--debug` - writes out logs that can be inspected if there is a problem + +## Run with 1Password CLI + +During development if you don't want the .env to contain secrets then 1Password's CLI can be used to +expand a placeholder: ```shell -CLAUDE_PLUGIN_ROOT=$(pwd) claude --plugin-dir . --debug +CLAUDE_PLUGIN_ROOT=$(pwd) op run --env-file='./.env' --no-masking -- claude --plugin-dir . --debug --mcp-config ./.mcp.dev.json ``` +Notable options: + * `--no-masking` - By default `op run` pipes the child's stdio to redact secrets, which makes Claude think it is being + scripted. This option prevents the redaction piping. + + To aid in the development of the MCP server install the MCP Server Skill: ``` diff --git a/package.json b/package.json index eef2ba2..60ae326 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "genesys-cloud-architect", - "version": "1.0.8", + "version": "1.0.9", "private": true, "packageManager": "pnpm@11.1.3+sha512.c85357fe17ca12dd23dd7071822666dfd7e3cb76fe214e3370b5ea2fb34f2a231185509b63e717f3cd0acb38dd3f8d82bcd5e8172400ae678b70ea4fbed0896d", "author": { @@ -18,14 +18,13 @@ "scripts": { "build": "pnpm run build:mcp-server && pnpm run build:deploy-runner", "build:mcp-server": "esbuild src/mcp-server/index.ts --bundle --platform=node --target=node22 --format=cjs --minify --tree-shaking=true --define:process.env.npm_package_version=\\\"$npm_package_version\\\" --outfile=servers/genesys-cloud-architect-mcp.js", - "build:deploy-runner": "esbuild src/deploy-runner/index.ts --bundle --platform=node --target=node22 --format=cjs --outfile=bin/deploy-runner.js", + "build:deploy-runner": "esbuild src/deploy-runner/index.ts --bundle --platform=node --target=node22 --format=cjs --outfile=scripts/deploy-runner.js", "typecheck": "tsc --noEmit", "lint": "biome check", "lint:fix": "biome check --write", "format": "biome format --write", "inspect-mcp": "pnpm dlx @modelcontextprotocol/inspector node servers/genesys-cloud-architect-mcp.js", "mcp-server": "node --experimental-strip-types src/mcp-server/index.ts", - "mcp-server-with-op": "op run --env-file='./.env' -- node --experimental-strip-types src/mcp-server/index.ts", "prepare": "husky" }, "dependencies": { diff --git a/servers/genesys-cloud-architect-mcp.js b/servers/genesys-cloud-architect-mcp.js index f885a35..98b0a63 100644 --- a/servers/genesys-cloud-architect-mcp.js +++ b/servers/genesys-cloud-architect-mcp.js @@ -157,7 +157,7 @@ ${s.warnings.join(` `)}`),l({content:[{type:"text",text:f.join(` `)+m}]})}else{let f=s?.error??`Deploy runner exited with code ${h}`;l({isError:!0,content:[{type:"text",text:`Deploy failed: ${f}${m}`}]})}})})}});var vt=t=>`${t}::start`,ws=(t,e)=>`${t}::${e}`;function AI(t,e,i,n){let a=new Set(e.backEdges.map(h=>`${h.from} ${h.to}`)),r=new Map,s=new Map,o=new Map,l=new Map;for(let h of t.nodes.keys())r.set(h,[]),s.set(h,new Set),o.set(h,[]),l.set(h,new Set);for(let h of t.edges){let g=a.has(`${h.from} ${h.to}`),m=r.get(h.to);if(m){let w=`${h.from} ${h.label??""} ${g}`,b=s.get(h.to);b&&!b.has(w)&&(b.add(w),m.push({id:h.from,label:h.label,backEdge:g}))}let f=o.get(h.from);if(f){let w=`${h.to} ${h.label??""} ${g}`,b=l.get(h.from);b&&!b.has(w)&&(b.add(w),f.push({id:h.to,label:h.label,backEdge:g}))}}let u=-1;for(let h of e.order.values())h>u&&(u=h);let c=u+1,p=[];for(let[h,g]of t.nodes){let m=e.order.has(h),f=m?e.order.get(h):c++;p.push({id:h,kind:g.kind,actionType:g.actionType,label:g.label,description:g.description,predecessors:r.get(h)??[],successors:o.get(h)??[],order:f,taskId:g.taskId,taskName:g.taskName,reachable:m,terminal:g.terminal})}p.sort((h,g)=>h.order-g.order);let d=i.taskOrder.flatMap(h=>{let g=i.tasks.get(h);return g?[{id:h,name:g.name,reusable:i.reusableTaskIds.has(h)}]:[]});return{flowName:i.flowName,flowType:i.flowType,entryTaskId:i.entryTaskId,reachabilityIsComplete:n,tasks:d,nodes:p}}function er(t,e){let i=t;for(let n of e.split(".")){if(i==null||typeof i!="object")return;i=i[n]}return typeof i=="string"&&i.length>0?i:void 0}function it(t,e){for(let i of e){let n=er(t,i);if(n!==void 0)return n}}function bI(t){let e=t.expression;if(e==null||typeof e!="object")return;let{text:i,type:n}=e;if(typeof i=="string"&&i.trim().length>0)return i;if(typeof n=="string"&&n.length>0)return n}function Db(t,e){let i=t.find(n=>n.outputId===e);return i?(i.terminal=!0,t):[{outputId:e,label:Gb(e),synthetic:!0,terminal:!0},...t]}var Tl={isTerminal:()=>!0,outputs:()=>[]},yI={isTerminal:()=>!0,describe:t=>it(t,["transferTo","group.text","flowName","externalNumber.text","user.text","transferTarget.text","transferTargetGroup.text","queues.0.text"]),outputs:t=>Db(vs(t).map(ir),"__SUCCESS__")},k7={isTerminal:()=>!1,outputs:t=>{let e=t.path;if(!e||typeof e!="object")return[];let i=ir(e,0);return i.outputId="__LOOP__",typeof e.label=="string"&&e.label.length>0||(i.label="Loop"),[i]}},q7={describe:()=>"Exits the innermost enclosing loop; control continues at the loop's next action (jump edge not modelled \u2014 this node's successors omit it)",outputs:()=>[]},_7={outputs:t=>Db(vs(t).map(ir),"__SUCCESS__")},H7={isTerminal:()=>!0,describe:t=>it(t,["flowName"]),outputs:t=>Db(vs(t).map(ir),"__DISCONNECT__")},I7={outputs:t=>{let e=vs(t);return e.length>0?[ir(e[0],0)]:[]}},Ni=t=>({describe:t}),id={DisconnectAction:Tl,EndFlowAction:Tl,EndTaskAction:Tl,EndStateAction:Tl,ExitBotFlowAction:Tl,LoopAction:k7,ExitLoopAction:q7,ProcessVoicemailInputAction:_7,CallBotFlowAction:H7,AskForNLUNextIntentAction:I7,WaitForInputAction:{},CallTaskAction:{},TaskAction:{},TransferMenuAction:{},TransferTaskAction:{},LoopUntilAction:{},DecisionAction:Ni(bI),SwitchAction:Ni(bI),DataAction:Ni(t=>it(t,["actionName"])),DataTableLookupAction:Ni(t=>it(t,["datatableName"])),CallCommonModuleAction:Ni(t=>it(t,["flowName"])),CallBotConnectorAction:Ni(t=>it(t,["botName"])),CallDigitalBotFlowAction:Ni(t=>it(t,["flowName"])),AddFlowMilestoneAction:Ni(t=>it(t,["milestoneName"])),ScreenPopAction:Ni(t=>it(t,["inputs.0.value.text"])),PlayAudioAction:Ni(t=>it(t,["prompts.defaultAudio.text"])),SetFlowOutcomeAction:Ni(t=>{let e=er(t,"outcomeName"),i=er(t,"outcomeValue.text");return e&&i?`${e} = ${i}`:e??i}),InitializeFlowOutcomeAction:Ni(t=>er(t,"outcomeName")),EvaluateScheduleGroupAction:Ni(t=>er(t,"scheduleGroup.text")),FindSystemPromptAction:Ni(t=>er(t,"findName.text")),SetUUIDataAction:Ni(t=>er(t,"uuiData.text")),UpdateVariableAction:{},SetAttributesAction:{},GetAttributesAction:{},EvaluateScheduleAction:{},CollectInputAction:{},GetResponseAction:{},AskForBooleanAction:{},AskForSlotAction:{},CommunicateAction:{},DigitalMenuAction:{},SendResponseAction:{},TranscriptionAction:{},FindQueueAction:{},FindUserPromptAction:{},SetWhisperAudio:{},BridgeServerAction:{}};function nd(t){return typeof t=="string"&&t.length>0}function vs(t){let e=[];if(Array.isArray(t.paths))for(let i of t.paths)i&&typeof i=="object"&&e.push(i);return t.path&&typeof t.path=="object"&&e.push(t.path),e}function Gb(t){let e=t.replace(/^__/,"").replace(/__$/,"");if(e.length===0)return t;let i=e.toLowerCase();return i.charAt(0).toUpperCase()+i.slice(1)}function ir(t,e){let i=t.outputId??`__OUTPUT_${e}__`,n=typeof t.label=="string"&&t.label.length>0?t.label:Gb(i),a={outputId:i,label:n,nextActionId:t.nextActionId,synthetic:!1,terminal:!1};return t.enabled===!1&&(a.disabled=!0),a}var R7=["taskReference","menuReference"],PI={describe:()=>{},outputs:t=>vs(t).map(ir),isTerminal:()=>!1,referencedTaskIds:t=>{let e=[];for(let i of R7){let n=t[i];typeof n=="string"&&n.length>0&&e.push(n)}return e}},z7=new Set(["TransferMenuAction","TransferTaskAction"]);function jI(t){return typeof t=="string"&&t.startsWith("Transfer")&&!z7.has(t)}function SI(t){return t?t in id||jI(t):!1}function OI(t){let e=jI(t)?{...PI,...yI}:PI,i=t&&t in id?id[t]:void 0;return i?{...e,...i}:e}function*td(t){for(let e of t.taskOrder){let i=t.tasks.get(e);if(i)for(let n of i.orderedActions)typeof n.id=="string"&&(yield{taskId:e,task:i,action:n,actionId:n.id,handler:OI(n.__type)})}}function TI(t){let e=[],i=[];for(let n of t.taskOrder){let a=t.tasks.get(n);if(a){nd(a.startAction)&&e.push({from:vt(n),to:a.startAction,kind:"fallthrough"});for(let r of a.menuChoices)e.push({from:vt(n),to:r.actionId,kind:"reference",label:r.label})}}for(let{taskId:n,actionId:a,action:r,handler:s}of td(t)){let o=s.isTerminal(r);nd(r.nextAction)&&!o&&e.push({from:a,to:r.nextAction,kind:"fallthrough"});for(let l of s.outputs(r)){let u=ws(a,l.outputId);e.push({from:a,to:u,kind:"branch",label:l.label}),nd(l.nextActionId)&&!l.terminal&&e.push({from:u,to:l.nextActionId,kind:l.outputId==="__LOOP__"?"loop":"branch"})}for(let l of s.referencedTaskIds(r)){let u=t.tasks.get(l);u?e.push({from:a,to:vt(l),kind:"reference",label:u.name}):i.push({code:"UNRESOLVED_REFERENCE",message:`Reference from ${a} to task ${l} is unresolvable.`,nodeId:a,taskId:n})}}return{edges:e,warnings:i}}function xI(t,e,i){let n=new Map;for(let u of t)n.set(u.id,u);let a=new Map;for(let u of n.keys())a.set(u,[]);let r=[],s=[],o=[];for(let u of e){let c=n.has(u.from),p=n.has(u.to);if(!c||!p){let d=c?`unknown target node "${u.to}"`:`unknown source node "${u.from}"`;s.push({...u,reason:d}),o.push({code:"DROPPED_EDGE",message:`Dropped ${u.kind} edge ${u.from} \u2192 ${u.to}: ${d}.`});continue}r.push(u),a.get(u.from)?.push(u.to)}let l=i.taskOrder.map(vt).filter(u=>n.has(u));return{graph:{nodes:n,edges:r,adjacency:a,roots:l,dropped:s},warnings:o}}function D7(t,e,i){let n=[],a=[];if(Array.isArray(t.actionList))for(let r of t.actionList)r&&typeof r=="object"&&n.push(r);if(Array.isArray(t.menuChoiceList))for(let r of t.menuChoiceList){let s=r?.action;if(!s||typeof s!="object")continue;if(typeof s.id!="string"){i.push({code:"MISSING_ACTION_ID",message:`Menu-choice action of type "${s.__type??"(missing)"}" in task ${e} has no id and was skipped.`,taskId:e});continue}n.push(s);let o=typeof r.name=="string"&&r.name.length>0?r.name:s.name??s.id;a.push({actionId:s.id,label:o})}return{orderedActions:n,menuChoices:a}}function G7(t){if(typeof t=="string")return t;if(t&&typeof t=="object"){let e=t.id;if(typeof e=="string")return e}}function MI(t){let e=[],i=new Map,n=Array.isArray(t.flowSequenceItemList)?t.flowSequenceItemList:[];for(let p of n){if(!p||typeof p!="object"||typeof p.id!="string")continue;let d=typeof p.name=="string"&&p.name.length>0?p.name:p.id,{orderedActions:h,menuChoices:g}=D7(p,p.id,e);i.set(p.id,{id:p.id,name:d,startAction:typeof p.startAction=="string"?p.startAction:void 0,orderedActions:h,menuChoices:g})}let a=new Set;for(let{id:p,orderedActions:d}of i.values())for(let h of d){if(typeof h.id!="string"){e.push({code:"MISSING_ACTION_ID",message:`Action of type "${h.__type??"(missing)"}" in task ${p} has no id and was skipped.`,taskId:p});continue}a.has(h.id)&&e.push({code:"DUPLICATE_ACTION_ID",message:`Duplicate action id "${h.id}" in task ${p}; the later definition wins.`,nodeId:h.id,taskId:p}),a.add(h.id)}let r=[...i.keys()],s=t.initialSequence,o=typeof s=="string"&&i.has(s),l=o?[s,...r.filter(p=>p!==s)]:r;typeof s=="string"&&s.length>0&&!o&&e.push({code:"UNRESOLVED_INITIAL_SEQUENCE",message:`initialSequence "${s}" matches no task; using declaration order.`});let u=new Set,c=t.uiMetaData?.task;if(Array.isArray(c))for(let p of c){let d=G7(p);d!==void 0&&u.add(d)}return{flowName:typeof t.name=="string"?t.name:"",flowType:typeof t.type=="string"?t.type:"",tasks:i,taskOrder:l,entryTaskId:o?s:void 0,reusableTaskIds:u,warnings:e}}var $7=new Set(["AskForNLUNextIntentAction","WaitForInputAction"]);function EI(t){let e=[],i=[];for(let n of t.taskOrder){let a=t.tasks.get(n);a&&e.push({id:vt(n),kind:"task-start",label:a.name,taskId:n,taskName:a.name,terminal:!1})}for(let{taskId:n,task:a,action:r,actionId:s,handler:o}of td(t)){let l=r.__type,u=typeof r.name=="string"&&r.name.length>0?r.name:l??s;e.push({id:s,kind:"action",actionType:l,label:u,description:o.describe(r),taskId:n,taskName:a.name,terminal:o.isTerminal(r)}),SI(l)||i.push({code:"UNKNOWN_ACTION_TYPE",message:`Unknown action __type "${l??"(missing)"}" \u2014 handled generically.`,nodeId:s,taskId:n}),$7.has(l??"")&&i.push({code:"UNRESOLVED_INTENT_FANOUT",message:`Intent fan-out for ${s} is not resolved: per-intent routing (nluMetaData) is not modelled.`,nodeId:s,taskId:n});for(let c of o.outputs(r))e.push({id:ws(s,c.outputId),kind:"branch-output",label:c.label,taskId:n,taskName:a.name,terminal:c.terminal}),c.disabled&&i.push({code:"DISABLED_BRANCH",message:`Output ${c.outputId} of ${s} is disabled.`,nodeId:ws(s,c.outputId),taskId:n})}return{nodes:e,warnings:i}}function $b(t){let e=new Map;for(let s of t.nodes.keys())e.set(s,0);let i=new Map,n=[],a=0;for(let s of t.roots){if(e.get(s)!==0)continue;e.set(s,1),i.set(s,a++);let o=[{node:s,index:0}];for(;o.length>0;){let l=o[o.length-1],u=t.adjacency.get(l.node)??[];if(l.indexp.code==="UNRESOLVED_INTENT_FANOUT");return{ok:!0,ir:AI(s,l,e,u),graph:s,warnings:[...e.warnings,...n,...r,...o]}}catch(e){return{ok:!1,error:{code:"INTERNAL",message:e instanceof Error?e.message:String(e)}}}}function kI(t,e){if(Array.isArray(t.actionList))for(let i of t.actionList)i&&typeof i=="object"&&e(i);if(Array.isArray(t.menuChoiceList))for(let i of t.menuChoiceList){if(!i||typeof i!="object")continue;let n=i.action;!n||typeof n!="object"||e(n,N7(i))}}function N7(t){let e={};return typeof t.id=="string"&&(e.id=t.id),typeof t.name=="string"&&(e.name=t.name),typeof t.digit=="string"&&(e.digit=t.digit),e}function qI(t){if(t===null||typeof t!="object"||Array.isArray(t))return[];let e=t.flowSequenceItemList;return Array.isArray(e)?e:[]}function Ub(t,e){let i=[],n=new Set;for(let o of Array.isArray(e)?e:[])typeof o=="string"&&!n.has(o)&&(n.add(o),i.push(o));if(i.length===0)return{found:[],notFound:[]};let a=new Map;for(let o of qI(t)){if(!o||typeof o!="object"||typeof o.id!="string")continue;let l=o.id,u=typeof o.name=="string"&&o.name.length>0?o.name:l;kI(o,(c,p)=>{if(typeof c.id!="string"||!n.has(c.id))return;let d={actionId:c.id,action:c,taskId:l,taskName:u};p&&(d.menuChoice=p);let h=a.get(c.id);h?h.push(d):a.set(c.id,[d])})}let r=[],s=[];for(let o of i){let l=a.get(o);l?r.push(...l):s.push(o)}return{found:r,notFound:s}}var U7=64;function W7(t,e){if(t instanceof RegExp){let n=new RegExp(t.source,t.flags);return a=>{n.lastIndex=0;let r=n.exec(a);return r?{index:r.index,length:r[0].length}:void 0}}if(typeof t!="string"||t.length===0)return;if(e?.caseSensitive===!0)return n=>{let a=n.indexOf(t);return a===-1?void 0:{index:a,length:t.length}};let i=t.toLowerCase();return n=>{let a=n.toLowerCase().indexOf(i);if(a===-1)return;let r=Math.min(a,n.length);return{index:r,length:Math.min(t.length,n.length-r)}}}function L7(t,e,i){let n=[],a=0,r=new Set,s=(o,l,u)=>{if(typeof o=="string"){let c=e(o);c&&(a+=1,n.length=U7||r.has(o))){if(r.add(o),Array.isArray(o))for(let c=0;cn.length}}function Wb(t,e,i){let n=W7(e,i);if(!n)return{hasMatches:!1};let a=i?.maxMatchesPerAction,r=typeof a=="number"&&a>0?Math.floor(a):Number.POSITIVE_INFINITY,s=[];for(let o of qI(t)){if(!o||typeof o!="object"||typeof o.id!="string")continue;let l=o.id,u=typeof o.name=="string"&&o.name.length>0?o.name:l;kI(o,(c,p)=>{if(typeof c.id!="string")return;let{hits:d,truncated:h}=L7(c,n,r);if(d.length===0)return;let g={actionId:c.id,taskId:l,taskName:u,matchedPaths:d};typeof c.__type=="string"&&(g.actionType=c.__type),typeof c.name=="string"&&c.name.length>0&&(g.name=c.name),p&&(g.menuChoice=p),h&&(g.truncated=!0),s.push(g)})}return s.length>0?{hasMatches:!0,matches:s}:{hasMatches:!1}}async function Cs(t,e){try{return{ok:!0,configuration:await t.getFlowLatestconfiguration(e)}}catch(i){return{ok:!1,message:B7(e,i)}}}function B7(t,e){let i=e!==null&&typeof e=="object"&&"status"in e&&typeof e.status=="number"?e.status:void 0,n=e!==null&&typeof e=="object"&&"message"in e&&typeof e.message=="string"&&e.message.length>0?e.message:void 0;return i===404?`Flow "${t}" not found. Check the flow id.`:i===401?`Not authenticated with Genesys Cloud (401) fetching flow "${t}": the access token is missing or expired. The flow may well exist \u2014 re-authenticate (restart the MCP server) and retry rather than concluding anything about the flow.`:i===403?`Not authorised to read flow "${t}" (403): the OAuth client lacks permission or division access to it. The flow exists but this client cannot see it.`:i===429?`Rate limited by Genesys Cloud (429) fetching flow "${t}". Transient \u2014 wait and retry; this says nothing about the flow.`:i!==void 0&&i>=500?`Genesys Cloud returned ${i} fetching flow "${t}". Transient server-side error \u2014 retry; this says nothing about the flow.`:`Failed to fetch flow "${t}"`+(i!==void 0?` (HTTP ${i})`:"")+(n?`: ${n}`:".")+" Likely transient (network or gateway) \u2014 retry rather than concluding the flow does not exist."}var _I=50;function F7(t){let e=[],i=new Map,n=!1;for(let a of t){let r=a.split("::")[0]||a;r!==a&&(n=!0),i.has(r)||(i.set(r,a),e.push(r))}return{lookupIds:e,requestedFor:i,strippedAny:n}}var V7={flowId:qe.string().min(1).describe("The Genesys Cloud Architect flow ID"),actionIds:qe.array(qe.string().min(1)).min(1).max(_I).describe(`Action GUIDs, taken from the \`id\` of flow_ir nodes whose \`kind\` is "action". Batch every action of interest into as few calls as possible rather than calling this tool once per action. Branch-output ids of the form \`::\` are also accepted; the suffix is stripped and the underlying action is returned. Maximum ${_I} ids per call; chunk a larger set into successive calls.`)},HI=({architectApi:t})=>({config:{description:"Retrieves the raw Architect configuration of specific actions in a Genesys Cloud Architect flow: decision expressions, prompt text, data action inputs and outputs, and any other per-action settings that the flow_ir tool omits. Use flow_ir first to find action ids and to understand the control flow, then use this tool to find out what an action actually does. Do not use the raw fields returned here to make control-flow claims: flow_ir has already resolved those, and its answer accounts for branches the raw configuration does not reflect.",annotations:{title:"Flow Action",readOnlyHint:!0,destructiveHint:!1},inputSchema:V7},handler:async({flowId:e,actionIds:i})=>{let n=await Cs(t,e);if(!n.ok)return{isError:!0,content:[{type:"text",text:n.message}]};let{lookupIds:a,requestedFor:r,strippedAny:s}=F7(i),o=Ub(n.configuration,a),l=c=>({...c,actionId:r.get(c.actionId)??c.actionId}),u=[];return s&&u.push("Some requested ids were flow_ir synthetic node ids of the form ::; the suffix was stripped and the underlying action looked up, then reported under the id as requested. A ::start id identifies a task rather than an action, so it never matches."),o.notFound.length>0&&u.push("Ids in notFound are absent from this flow's latest configuration. The flow may have been redeployed since flow_ir was called, so re-run flow_ir and re-join rather than concluding those actions no longer exist."),{content:[{type:"text",text:JSON.stringify({flowId:e,found:o.found.map(l),notFound:o.notFound.map(c=>r.get(c)??c),...u.length>0?{notes:u}:{}})}]}}});function II(t){let e=t.toUpperCase();return e.endsWith("FLOW")?e:`${e}FLOW`}function J7(t,e){let i={};for(let n of e){let a=n.type??"UNKNOWN";i[a]||(i[a]=[]),i[a].push({id:n.id??"",name:n.name??"",...n.version?{version:n.version}:{},deleted:n.deleted??!1,updated:n.updated??!1})}return{flow:{id:t.id??"",name:t.name,type:II(t.type??""),version:t.publishedVersion?.commitVersion??"1"},dependencies:i}}var Z7={flowId:qe.string().min(1).describe("The Genesys Cloud Architect flow ID")},RI=({architectApi:t})=>({config:{description:"Retrieves all dependencies consumed by a Genesys Cloud Architect flow. Returns the flow metadata and its dependencies grouped by type.",annotations:{title:"Flow Dependencies",readOnlyHint:!0,destructiveHint:!1},inputSchema:Z7},handler:async({flowId:e})=>{try{let i;try{i=await t.getFlow(e)}catch{return{isError:!0,content:[{type:"text",text:`Flow "${e}" not found.`}]}}let n=II(i.type??""),a=i.publishedVersion?.commitVersion??"1",r=[],s=1;for(;;){let l=await t.getArchitectDependencytrackingConsumedresources(i.id,a,n,{pageSize:100,pageNumber:s});if(l.entities&&r.push(...l.entities),!l.nextUri)break;s++}let o=J7(i,r);return{content:[{type:"text",text:JSON.stringify(o)}]}}catch(i){return{isError:!0,content:[{type:"text",text:`Failed to retrieve flow dependencies: ${i instanceof Error?i.message:String(i)}`}]}}}});function K7(t,e){let i=t.find(r=>r.id===e);if(i)return{match:i};let n=e.toLowerCase(),a=t.filter(r=>r.name.toLowerCase()===n);return a.length>1?{ambiguous:a}:a[0]?{match:a[0]}:void 0}var Q7={flowId:qe.string().min(1).describe("The Genesys Cloud Architect flow ID"),task:qe.string().min(1).optional().describe("Optional. Restrict the returned nodes to a single task, by task id or task name (case-insensitive). Use this to explore a large flow one task at a time. The full task list is always returned, and a node's predecessors and successors may reference nodes in other tasks, which will not appear in the filtered node list.")},zI=({architectApi:t})=>({config:{description:"Retrieves the intermediate representation (IR) of a Genesys Cloud Architect flow: a flat, ordered list of its actions with the branches connecting them. Use this to understand what an existing flow does. It answers what follows an action, which branch leads where, which actions are unreachable, and how tasks call each other.",annotations:{title:"Flow IR",readOnlyHint:!0,destructiveHint:!1},inputSchema:Q7},handler:async({flowId:e,task:i})=>{let n=await Cs(t,e);if(!n.ok)return{isError:!0,content:[{type:"text",text:n.message}]};let a=Nb(n.configuration);if(!a.ok)return{isError:!0,content:[{type:"text",text:`Failed to parse flow "${e}" (${a.error.code}): ${a.error.message}`}]};let r=a.ir;if(typeof i=="string"){let s=K7(r.tasks,i);if(!s){let o=r.tasks.map(l=>l.name).join(", ");return{isError:!0,content:[{type:"text",text:`No task matching "${i}" in flow "${e}". Available tasks: ${o||"(none)"}.`}]}}if("ambiguous"in s){let o=s.ambiguous.map(l=>`"${l.name}" (id: ${l.id})`).join(", ");return{isError:!0,content:[{type:"text",text:`Task name "${i}" is ambiguous in flow "${e}". It matches ${o}. Retry with the task id.`}]}}r={...r,nodes:r.nodes.filter(o=>o.taskId===s.match.id)}}return{content:[{type:"text",text:JSON.stringify({flowId:e,ir:r,warnings:a.warnings})}]}}});var DI=100,GI=10,xl=160,Y7=512;function X7(t,e,i){if(!e)return{ok:!0,query:t};try{return{ok:!0,query:new RegExp(t,i?"":"i")}}catch(n){let a=n instanceof Error?n.message:String(n);return{ok:!1,error:`Invalid regular expression "${t}": ${a}. Set regex to false for a literal substring search.`}}}function eY(t,e,i){if(t.length<=xl)return t;let n=e+Math.floor(i/2),a=Math.max(0,Math.min(n-Math.floor(xl/2),t.length-xl)),r=a+xl;return`${a>0?"\u2026":""}${t.slice(a,r)}${r({config:{description:'Searches the raw Architect configuration of a Genesys Cloud Architect flow for text, and returns the actions containing it along with where in each action it matched. Answers "which actions reference X" \u2014 a queue name, a data action, a variable such as Flow.DNIS, a phrase in a prompt \u2014 in a single call. Use it instead of sweeping a flow with bulk flow_action lookups, which return whole action subtrees for actions that may not be relevant at all. Each returned actionId is an Architect GUID: it joins directly to flow_ir nodes whose kind is "action", and is accepted verbatim by flow_action. Search here to find the ids, then fetch those ids with flow_action when an excerpt is not enough. Matching is against string values only, never key names. A match in a wiring field locates text and is not a control-flow claim; flow_ir owns wiring.',annotations:{title:"Search In Flow",readOnlyHint:!0,destructiveHint:!1},inputSchema:nY},handler:async({flowId:e,pattern:i,regex:n,caseSensitive:a})=>{let r=X7(i,n,a);if(!r.ok)return{isError:!0,content:[{type:"text",text:r.error}]};let s=await Cs(t,e);if(!s.ok)return{isError:!0,content:[{type:"text",text:s.message}]};let o=s.configuration;if(!iY(o))return{isError:!0,content:[{type:"text",text:`The latest configuration of flow "${e}" has no flowSequenceItemList to search \u2014 the flow type may be unsupported. Nothing was searched, so this is not a zero-match result.`}]};let l=Wb(o,r.query,{caseSensitive:a,maxMatchesPerAction:GI}),u=l.hasMatches?l.matches:[],c=u.slice(0,DI),p=c.map(h=>({...h,matchedPaths:h.matchedPaths.map(g=>({path:g.path,excerpt:eY(g.value,g.matchIndex,g.matchLength)}))})),d=[];return u.length>c.length&&d.push(`${u.length} actions matched; only the first ${DI} are returned, in encounter order. Narrow the pattern to see the rest.`),c.some(h=>h.truncated)&&d.push(`Some actions matched in more than ${GI} places; matchedPaths is not exhaustive for entries marked truncated.`),c.some(h=>h.matchedPaths.some(g=>g.value.length>xl))&&d.push("Excerpts are clipped windows around the match, not whole values; fetch the action with flow_action when the full text matters."),{content:[{type:"text",text:JSON.stringify({flowId:e,pattern:i,totalMatchedActions:u.length,matchedActions:p,...d.length>0?{notes:d}:{}})}]}}});var Lb=new Map;function tY(t){let{id:e,previousTurn:i,...n}=t;return n}async function NI(t,e,i){let n=[],a=i;for(;n.push(tY(a)),Lb.set(e,a.id),a.nextActionType==="NoOp";)a=await t.postTextbotsBotflowsSessionTurns(e,{previousTurn:{id:a.id},inputEventType:"NoOp",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:""}}]}});let r=a.nextActionType;return(r==="Disconnect"||r==="Exit")&&Lb.delete(e),{content:[{type:"text",text:JSON.stringify({sessionId:e,turns:n})}]}}var aY={flowId:qe.string().optional().describe("The bot flow ID to test. Required when starting a new session."),sessionId:qe.string().optional().describe("Session ID from a previous call. Required when continuing an existing conversation."),message:qe.string().optional().describe("User message to send to the bot. Required when continuing a session.")},UI=({textbotsApi:t})=>({config:{description:"Tests a deployed Genesys Cloud Architect Bot Flow and Digital Bot Flow by simulating a text conversation. To start: provide flowId. To continue: provide the returned sessionId and a message. Turn tracking is managed server-side \u2014 only the sessionId is needed between calls. Returns { sessionId, turns[] } where each turn contains the raw API response (prompts with segments, outputLanguage, nextActionType, and action-specific data like modeConstraints or outputData). Multiple turns are returned when the flow sends consecutive messages (NoOp turns are automatically drained). nextActionType values: WaitForInput (send another message), Disconnect/Exit (conversation ended).",annotations:{title:"Test Bot Flow",readOnlyHint:!1,destructiveHint:!1},inputSchema:aY},handler:async({flowId:e,sessionId:i,message:n})=>{try{if(i&&e)return{isError:!0,content:[{type:"text",text:"Provide either flowId (to start) or sessionId (to continue), not both."}]};if(!i&&!e)return{isError:!0,content:[{type:"text",text:"Provide flowId to start a new session or sessionId to continue an existing one."}]};if(e){let s=await t.postTextbotsBotflowsSessions({flow:{id:e},externalSessionId:"",inputData:{variables:{}},channel:{inputModes:["Text"],outputModes:["Text"],name:"Messaging",userAgent:{name:"GenesysWebWidget"}},language:""}),o=await t.postTextbotsBotflowsSessionTurns(s.id,{inputEventType:"NoOp",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:""}}]}});return NI(t,s.id,o)}if(!i||!n)return{isError:!0,content:[{type:"text",text:"A message is required when continuing an existing session."}]};let a=Lb.get(i);if(!a)return{isError:!0,content:[{type:"text",text:`No active session found for ID "${i}". It may have expired or already ended. Start a new session with a flowId.`}]};let r=await t.postTextbotsBotflowsSessionTurns(i,{previousTurn:{id:a},inputEventType:"UserInput",inputEventUserInput:{mode:"Text",alternatives:[{transcript:{text:n}}]}});return NI(t,i,r)}catch(a){return{isError:!0,content:[{type:"text",text:`Bot flow test failed: ${a instanceof Error?a.message:JSON.stringify(a)}`}]}}}});var Bb=qe.object({GENESYS_REGION:qe.string().min(1),GENESYS_CLIENT_ID:qe.string().min(1),GENESYS_CLIENT_SECRET:qe.string().min(1),DEPLOY_SCRIPT_PATH:qe.string().min(1),PREVENT_LOGIN:qe.enum(["TRUE","FALSE"]).default("FALSE").transform(t=>t==="TRUE")}).safeParse(process.env);if(!Bb.success){let t=Bb.error.issues.map(e=>e.path[0]).join(` `);console.error(`Missing required environment variables: - ${t}`),process.exit(1)}var Qt=Bb.data,nr=new vc({name:"genesys-cloud-architect",version:"1.0.8"}),rd=new ad.default.ArchitectApi,WI=RI({architectApi:rd});nr.registerTool("flow_dependencies",WI.config,WI.handler);var LI=zI({architectApi:rd});nr.registerTool("flow_ir",LI.config,LI.handler);var BI=HI({architectApi:rd});nr.registerTool("flow_action",BI.config,BI.handler);var FI=$I({architectApi:rd});nr.registerTool("search_in_flow",FI.config,FI.handler);var VI=CI({region:Qt.GENESYS_REGION,clientId:Qt.GENESYS_CLIENT_ID,clientSecret:Qt.GENESYS_CLIENT_SECRET,deployScriptPath:Qt.DEPLOY_SCRIPT_PATH});nr.registerTool("deploy_flow",VI.config,VI.handler);var JI=UI({textbotsApi:new ad.default.TextbotsApi});nr.registerTool("test_bot_flow",JI.config,JI.handler);(async()=>{if(Qt.PREVENT_LOGIN)console.warn("Login for Platform API skipped. Calling tools will result in an auth failure.");else{let e=ad.default.ApiClient.instance;e.setEnvironment(Qt.GENESYS_REGION),await e.loginClientCredentialsGrant(Qt.GENESYS_CLIENT_ID,Qt.GENESYS_CLIENT_SECRET)}let t=new Ac;await nr.connect(t)})().catch(t=>{console.error("Failed to start server:",t),process.exit(1)}); + ${t}`),process.exit(1)}var Qt=Bb.data,nr=new vc({name:"genesys-cloud-architect",version:"1.0.9"}),rd=new ad.default.ArchitectApi,WI=RI({architectApi:rd});nr.registerTool("flow_dependencies",WI.config,WI.handler);var LI=zI({architectApi:rd});nr.registerTool("flow_ir",LI.config,LI.handler);var BI=HI({architectApi:rd});nr.registerTool("flow_action",BI.config,BI.handler);var FI=$I({architectApi:rd});nr.registerTool("search_in_flow",FI.config,FI.handler);var VI=CI({region:Qt.GENESYS_REGION,clientId:Qt.GENESYS_CLIENT_ID,clientSecret:Qt.GENESYS_CLIENT_SECRET,deployScriptPath:Qt.DEPLOY_SCRIPT_PATH});nr.registerTool("deploy_flow",VI.config,VI.handler);var JI=UI({textbotsApi:new ad.default.TextbotsApi});nr.registerTool("test_bot_flow",JI.config,JI.handler);(async()=>{if(Qt.PREVENT_LOGIN)console.warn("Login for Platform API skipped. Calling tools will result in an auth failure.");else{let e=ad.default.ApiClient.instance;e.setEnvironment(Qt.GENESYS_REGION),await e.loginClientCredentialsGrant(Qt.GENESYS_CLIENT_ID,Qt.GENESYS_CLIENT_SECRET)}let t=new Ac;await nr.connect(t)})().catch(t=>{console.error("Failed to start server:",t),process.exit(1)}); /*! Bundled license information: mime-db/index.js: