@stkxp/cli 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/stkxp.mjs
CHANGED
|
@@ -93,6 +93,13 @@ program
|
|
|
93
93
|
console.log(`Webhook token (save this, shown once): ${result.webhookToken}`);
|
|
94
94
|
console.log(`Indices created: ${result.indicesCreated.join(", ") || "(none, already existed)"}`);
|
|
95
95
|
console.log(`Documents indexed: ${result.docsIndexed}`);
|
|
96
|
+
if (result.gatewayPlatformsActivated > 0) {
|
|
97
|
+
console.log(`MCP gateway platforms activated: ${result.gatewayPlatformsActivated} (relayed through the source instance)`);
|
|
98
|
+
}
|
|
99
|
+
if (result.gatewayError) {
|
|
100
|
+
console.warn(`⚠️ MCP gateway grant not redeemed: ${result.gatewayError}`);
|
|
101
|
+
console.warn(` Affected platforms have a blank URL and will fail until reconfigured manually.`);
|
|
102
|
+
}
|
|
96
103
|
} catch (e) {
|
|
97
104
|
console.error(`stkxp deploy failed: ${e.message}`);
|
|
98
105
|
process.exitCode = 1;
|
|
@@ -150,6 +157,13 @@ program
|
|
|
150
157
|
}
|
|
151
158
|
console.log(`Indices created: ${result.indicesCreated.join(", ") || "(none, already existed)"}`);
|
|
152
159
|
console.log(`Documents indexed: ${result.docsIndexed}`);
|
|
160
|
+
if (result.gatewayPlatformsActivated > 0) {
|
|
161
|
+
console.log(`MCP gateway platforms activated: ${result.gatewayPlatformsActivated} (relayed through the source instance)`);
|
|
162
|
+
}
|
|
163
|
+
if (result.gatewayError) {
|
|
164
|
+
console.warn(`⚠️ MCP gateway grant not redeemed: ${result.gatewayError}`);
|
|
165
|
+
console.warn(` Affected platforms have a blank URL and will fail until reconfigured manually.`);
|
|
166
|
+
}
|
|
153
167
|
} catch (e) {
|
|
154
168
|
console.error(`stkxp bootstrap failed: ${e.message}`);
|
|
155
169
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stkxp/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Export a Stack Expert Team as an encrypted, standalone-deployable bundle, and redeploy it on any Elasticsearch cluster.",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"html2canvas": "^1.4.1",
|
|
65
65
|
"i18next": "^25.8.13",
|
|
66
66
|
"ioredis": "^5.9.2",
|
|
67
|
+
"isolated-vm": "^5.0.4",
|
|
67
68
|
"js-yaml": "^4.1.1",
|
|
68
69
|
"jsonata": "^2.0.6",
|
|
69
70
|
"jsonwebtoken": "^9.0.3",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{MessagesAnnotation as _e,StateGraph as Ae,Annotation as J}from"@langchain/langgraph";import{AIMessage as Pe,SystemMessage as Ce}from"@langchain/core/messages";import{createLLMInstance as ne}from"../llm/providers";import{config as X}from"../config";import{loadMcpTools as Me}from"@langchain/mcp-adapters";import{createAgent as Oe}from"langchain";import{DynamicStructuredTool as G}from"@langchain/core/tools";import{z as g}from"zod";import{TavilySearch as ve}from"@langchain/tavily";import{PerformanceTracker as Se}from"../utils/performance-tracker";import{buildOptimizedSystemPrompt as $e}from"../utils/prompt-optimizer";import{promptOptimizationConfig as se}from"../app-config/prompt-optimization";import{runTopicABTest as we,generateTopicHybrid as Ee}from"./topic-variants";import{createCompareChatTool as ke}from"../tools/compare-chat";import{TOOL_RESULT_MAX_TOKENS as re}from"./utils/content-utils";import ae from"jsonwebtoken";import{Client as Le}from"@elastic/elasticsearch";import{createMcpClient as Ne}from"../mcp/client";const Re=process.env.ELASTICSEARCH_HOST||"https://51.159.174.230:9205",Ie=process.env.ELASTICSEARCH_USER||"elastic",xe=process.env.ELASTICSEARCH_PASSWORD||"",ie=".stkxp_platforms",L=new Le({node:Re,auth:{username:Ie,password:xe},tls:{rejectUnauthorized:!1},requestTimeout:3e4,maxRetries:3,maxSockets:256,maxFreeSockets:256});L.connectionPool&&typeof L.connectionPool.setMaxListeners=="function"&&L.connectionPool.setMaxListeners(50);const F=new Map,De=300*1e3;setInterval(()=>{const o=Date.now();for(const[t,r]of F.entries())r.refCount===0&&o-r.lastUsed>De&&(console.log(`[MCP Pool] Closing inactive client: ${t}`),r.client.close().catch(c=>console.error("[MCP Pool] Error closing client:",c)),F.delete(t))},120*1e3);async function Fe(o,t){const r=`${o}`,c=F.get(r);if(c)return console.log(`[MCP Pool] Reusing existing client for ${o}`),c.lastUsed=Date.now(),c.refCount++,c.client;console.log(`[MCP Pool] Creating new client for ${o}`);const i=await Ne(o,t);return i.transport&&typeof i.transport.setMaxListeners=="function"&&i.transport.setMaxListeners(50),i.client&&typeof i.client.setMaxListeners=="function"&&i.client.setMaxListeners(50),F.set(r,{client:i,lastUsed:Date.now(),refCount:1}),i}function Ue(o){const t=`${o}`,r=F.get(t);r&&(r.refCount=Math.max(0,r.refCount-1),r.lastUsed=Date.now())}async function je(o){try{const t=o.toLowerCase().replace(/\s+/g,"_").split("_")[0]||"clusters";console.log(`[MCP Discovery] Searching for servers with packageName matching "${t}" and enabled=true`);const c=(await L.search({index:ie,body:{query:{bool:{must:[{term:{enabled:!0}},{prefix:{"packageName.keyword":t}}]}},size:100,sort:[{createdAt:{order:"desc"}}]}})).hits.hits.map(i=>({id:i._id,name:i._source.name,url:i._source.url,authKey:i._source.authKey,type:i._source.type,packageName:i._source.packageName,enabled:i._source.enabled,description:i._source.description}));if(console.log(`[MCP Discovery] Found ${c.length} enabled servers for system "${t}"`),c.length>0)return console.log("[MCP Discovery] Servers:",c.map(i=>`${i.name} (${i.packageName})`).join(", ")),c;if(t!=="clusters"&&t!=="unknown"){console.log(`[MCP Discovery] No servers found for "${t}", falling back to "clusters" package`);const P=(await L.search({index:ie,body:{query:{bool:{must:[{term:{enabled:!0}},{prefix:{"packageName.keyword":"clusters"}}]}},size:100,sort:[{createdAt:{order:"desc"}}]}})).hits.hits.map(y=>({id:y._id,name:y._source.name,url:y._source.url,authKey:y._source.authKey,type:y._source.type,packageName:y._source.packageName,enabled:y._source.enabled,description:y._source.description}));if(P.length>0)return console.log(`[MCP Discovery] \u2705 Fallback successful: Found ${P.length} "clusters" server(s)`),P}return c}catch(t){return console.error("[MCP Discovery] Error fetching MCP servers from Elasticsearch:",t.message),console.error("[MCP Discovery] Falling back to no servers"),[]}}function E(o){if(!o||typeof o!="object")return o;const t={...o};return delete t.exclusiveMinimum,delete t.exclusiveMaximum,t.properties&&typeof t.properties=="object"&&(t.properties=Object.keys(t.properties).reduce((r,c)=>(r[c]=E(t.properties[c]),r),{})),t.items&&(t.items=E(t.items)),t.additionalProperties&&typeof t.additionalProperties=="object"&&(t.additionalProperties=E(t.additionalProperties)),Array.isArray(t.anyOf)&&(t.anyOf=t.anyOf.map(r=>E(r))),Array.isArray(t.oneOf)&&(t.oneOf=t.oneOf.map(r=>E(r))),Array.isArray(t.allOf)&&(t.allOf=t.allOf.map(r=>E(r))),t}function Je(o){const t=o?.content;if(typeof t=="string")return t;if(Array.isArray(t)){const r=t.find(c=>typeof c?.text=="string")?.text;if(r)return r}return t?.toString?.()??""}function Ge(o){try{const r=o?.messages?.[0]?.content?.[0]?.text;if(typeof r=="string")return r}catch{}try{const r=o?.messages?.[0]?.content?.text;if(typeof r=="string")return r}catch{}const t=o?.messages?.[0]?.content;return typeof t=="string"?t:typeof o?.content=="string"?o.content:""}function le(o){if(!o)return{data:[]};if(Array.isArray(o?.data))return{data:o.data,nextCursor:o.nextCursor,meta:o.meta};if(Array.isArray(o?.result?.data))return{data:o.result.data,nextCursor:o.nextCursor??o.result?.nextCursor,meta:o.result?.meta};const t=o.nextCursor||o?.aggregations?.by_namespace?.buckets?.[0]?.g?.after_key;return{data:[],nextCursor:t}}function qe(o){return Math.ceil(o.length/4)}function Be(o,t={}){const{maxTokens:r=re,sampleItems:c=20}=t,i=JSON.stringify(o),P=qe(i);if(P<=r)return o;const{data:y,meta:O}=le(o);let A;return Array.isArray(y)&&y.length>0&&(A=y.slice(0,c)),{error:"RESULT_TOO_LARGE",approx_tokens:P,max_allowed_tokens:r,hint:"La r\xE9ponse de ce tool MCP est trop volumineuse pour \xEAtre envoy\xE9e telle quelle au LLM. R\xE9duis ta requ\xEAte (p\xE9riode, filtres par cluster/app/host, etc.) et r\xE9essaie.",meta:{...O,has_sample:!!A},sample:A}}function ze(o,t){if(!o||typeof o!="object"||ce(o))return o;let r;try{r=JSON.parse(JSON.stringify(o))}catch(i){return console.warn(`[normalizeToolSchemaForProvider] Failed to clone schema (${i.message}), returning original`),o}delete r.$schema;const c=i=>{if(!(!i||typeof i!="object")){if((t==="openai"||t==="anthropic")&&i.additionalProperties===!1&&(i.additionalProperties=!0),i.properties&&typeof i.properties=="object")for(const P of Object.keys(i.properties))c(i.properties[P]);i.items&&c(i.items),Array.isArray(i.anyOf)&&i.anyOf.forEach(c),Array.isArray(i.oneOf)&&i.oneOf.forEach(c),Array.isArray(i.allOf)&&i.allOf.forEach(c)}};return c(r),r}function ce(o){return!!o&&(typeof o.safeParse=="function"||!!o._def)}function Ke(o){return!!o&&typeof o=="object"&&(o.type||o.properties||o.anyOf||o.oneOf||o.allOf)}function N(o){if(!o||typeof o!="object")return g.any();if(Array.isArray(o.anyOf)&&o.anyOf.length)return g.union(o.anyOf.map(N));if(Array.isArray(o.oneOf)&&o.oneOf.length)return g.union(o.oneOf.map(N));if(Array.isArray(o.allOf)&&o.allOf.length)return o.allOf.map(N).reduce((t,r)=>g.intersection(t,r));if(Array.isArray(o.enum)&&o.enum.length)return o.enum.every(t=>typeof t=="string")?g.enum(o.enum):g.union(o.enum.map(t=>g.literal(t)));switch(o.type){case"string":return g.string();case"number":return g.number();case"integer":return g.number().int();case"boolean":return g.boolean();case"array":{const t=o.items?N(o.items):g.any();return g.array(t)}case"object":default:{const t=o.properties&&typeof o.properties=="object"?o.properties:{},r=new Set(Array.isArray(o.required)?o.required:[]),c={};for(const[P,y]of Object.entries(t)){const O=N(y);c[P]=r.has(P)?O:O.optional()}let i=g.object(c);return o.additionalProperties===!1?i=i.strict():i=i.passthrough(),i}}}function Xe(o){return o.map(t=>{const r=t.schema;if(!r||ce(r))return t;if(Ke(r)){const c=N(r);return new G({name:t.name,description:t.description,schema:c,responseFormat:t.responseFormat,func:async(i,P)=>t.func(i,P)})}return new G({name:t.name,description:t.description,schema:g.record(g.any()),responseFormat:t.responseFormat,func:async(c,i)=>t.func(c,i)})})}async function We(o,t,r={}){const c=r.retries??3,i=r.baseDelayMs??800,P=new Set(r.retryOn??[429,502,503,504]);for(let y=0;y<=c;y++)try{return await o.invoke(t)}catch(O){const A=O?.status??O?.code??O?.response?.status,$=P.has(Number(A))||O?.name==="TimeoutError",W=y===c;if(console.warn(`[MCP] invoke failed (try ${y+1}/${c+1}) code=${A} transient=${$}:`,O?.message||O),!$||W)throw O;const R=i*Math.pow(2,y);await new Promise(v=>setTimeout(v,R))}}const _o=g.object({topic:g.enum(["clusters_clusters_overview","clusters_clusters_health-check","clusters_cluster_finops","clusters_nodes_inventory","clusters_nodes_operations","clusters_nodes_storage","clusters_nodes_resources","clusters_nodes_shards","clusters_nodes_thread_pool","clusters_nodes_tasks","clusters_nodes_hot_threads","clusters_jobs_enrich","clusters_jobs_snapshots","clusters_indices_inventory","clusters_indices_operations","clusters_indices_storage","clusters_indices_segments","clusters_indices_documents","elasticsearch_doc","mysql_mysql_overview","platform.core.execute_esql"])});async function Ao(o,t){console.log(`[buildAppFromDefinition] Building graph from definition: ${o.name} (v${o.version})`),console.log("[buildAppFromDefinition] Graph config:",{streaming:o.config.streaming,recursionLimit:o.config.recursionLimit,maxConcurrency:o.config.maxConcurrency});const r=t.performanceTracker||new Se(t.threadId);let c=[];const i=[];let P=!1;const y=t.queryParams?.topic||"unknown";if(t.mcp&&(Array.isArray(t.mcp)?t.mcp.length>0:!0))c=Array.isArray(t.mcp)?t.mcp:[t.mcp],console.log(`[MCP] Using ${c.length} pre-configured MCP client(s)`);else{P=!0,console.log(`[MCP] Auto-discovering servers for topic: "${y}"`);const e=await je(y);if(e.length===0)console.warn(`[MCP] \u26A0\uFE0F No enabled MCP servers found for topic "${y}"`),console.warn("[MCP] \u26A0\uFE0F Agent will run with NO tools available");else{console.log(`[MCP] Creating ${e.length} MCP client(s) for discovered servers`);for(const n of e)try{console.log(`[MCP] Connecting to: ${n.name} at ${n.url}`);const s=await Fe(n.url,{headers:{Authorization:`ApiKey ${n.authKey}`},transportType:"http",timeout:3e4});c.push(s),i.push(n.url),console.log(`[MCP] \u2705 Connected to ${n.name} (${n.packageName})`)}catch(s){console.error(`[MCP] \u274C Failed to connect to ${n.name}:`,s.message)}console.log(`[MCP] Successfully connected to ${c.length}/${e.length} servers`)}}let A=[],$;if(c.length===0)console.warn("[MCP] \u26A0\uFE0F No MCP servers available. Agent will run without tools.");else{$=c[0],console.log(`[MCP] Loading tools from ${c.length} MCP server(s)`);const n=(await Promise.all(c.map(async(a,m)=>{const d=await Me(`stkxp-mcp-server-${m}`,a.client);return console.log(`[MCP] Server ${m+1}: Loaded ${d.length} tools`),d}))).flat();console.log(`[MCP] Total loaded: ${n.length} tools`);const s=new Map;for(const a of n)s.has(a.name)||s.set(a.name,a);A=Array.from(s.values())}const R=y.toLowerCase().replace(/\s+/g,"_").split("_")[0]||"clusters";let v=A;if(P)console.log(`[MCP] \u2705 Using ${A.length} tools from auto-discovered servers`),v=A;else if(process.env.MCP_DISABLE_TOPIC_FILTERING==="true")console.log(`[MCP] \u26A0\uFE0F Topic filtering disabled, using all ${A.length} tools`),v=A;else{let n=A.filter(a=>a.name.startsWith(R+"_"));n.length===0&&(n=A.filter(a=>a.name.includes(R)));const s=A.filter(a=>a.name.startsWith("elasticsearch_"));if(s.length>0){const a=new Set(n.map(d=>d.name)),m=s.filter(d=>!a.has(d.name));m.length>0&&(console.log(`[MCP] \u{1F527} Force-adding ${m.length} elasticsearch_ tools`),n=[...n,...m])}n.length===0?(console.log(`[MCP] \u26A0\uFE0F No tools found for system "${R}", using all ${A.length} tools`),v=A):(console.log(`[MCP] \u2705 Filtered ${n.length}/${A.length} tools`),v=n)}console.log("[MCP] Server tool sample:",JSON.stringify(v[0]),v[0]);const Y=v.filter(e=>{try{if(!e.schema)return console.log(`[MCP] \u26A0\uFE0F Tool "${e.name}" has no schema, keeping it`),!0;const n=e.schema;return n._def&&typeof n._def.shape=="function"?!0:n._def&&n._def.typeName==="ZodObject"?(console.error(`[MCP] \u274C Tool "${e.name}" has invalid ZodObject schema (shape is not a function), filtering out`),!1):(console.log(`[MCP] \u26A0\uFE0F Tool "${e.name}" has non-object schema type: ${n._def?.typeName}, keeping it`),!0)}catch(n){return console.error(`[MCP] \u274C Tool "${e.name}" validation error: ${n.message}, filtering out`),!1}});console.log(`[MCP] Validated ${Y.length}/${v.length} tools`);const q=Y,de=new G({name:"mcp_paginated_query",description:"Appelle un tool MCP cibl\xE9 en boucle avec prise en charge de nextCursor",schema:g.object({toolName:g.string().describe("Nom du tool MCP \xE0 appeler"),args:g.record(g.any()).describe("Arguments \xE0 passer au tool MCP"),maxPages:g.number().int().min(1).default(10).describe("Nombre max de pages"),maxItems:g.number().int().min(1).default(5e3).describe("Nombre max d'items cumul\xE9s")}),func:async({toolName:e,args:n,maxPages:s,maxItems:a})=>{const m=v.find(f=>f.name===e)||A.find(f=>f.name===e);if(!m)throw new Error(`Tool MCP introuvable: ${e}`);let d=n?.request?.body?.cursor,l=0;const C=[];for(;l<s&&C.length<a;){const f=JSON.parse(JSON.stringify(n||{}));d&&(f.request=f.request||{},f.request.body=f.request.body||{},f.request.body.cursor=d);const u=await We(m,f,{retries:4,baseDelayMs:750,retryOn:[429,502,503,504]}),{data:b,nextCursor:_}=le(u);if(Array.isArray(b)&&b.length&&C.push(...b),l+=1,!_)break;d=_}const p=l>=s||C.length>=a||!d;return{items:C,pages:l,done:p,nextCursor:d}}}),{createLLMServices:me}=await import("../services/llm-services.js"),pe=me(L);let V="user",k=t.username;if(t.userToken)try{const e=ae.decode(t.userToken);e&&typeof e=="object"&&("roles"in e&&(V=e.roles?.[0]||"user"),!k&&"username"in e&&(k=e.username||void 0))}catch(e){console.warn("[buildAppFromDefinition] Failed to decode user token for routing:",e)}const H={userRole:V,topic:y,environment:process.env.NODE_ENV||"prod",requiredCapabilities:["streaming","toolCalling"],owner:k};console.log("[buildAppFromDefinition] Routing context:",H);let h,B;const Z=o.llmConfig?.useRoutingEngine!==!1;Z||console.log("[buildAppFromDefinition] Routing engine disabled in graph config, using llmConfig directly");try{if(!Z)throw new Error("Routing engine disabled in graph configuration");const e=await pe.getRoutingService().selectDeployment(H);if(console.log(`[buildAppFromDefinition] \u2705 Selected model: ${e.model.modelId}`),console.log(`[buildAppFromDefinition] Reason: ${e.reason}`),k&&e.provider.owner&&e.provider.owner!==k)throw new Error(`Routing selected provider owned by "${e.provider.owner}" for caller "${k}" \u2014 refusing cross-tenant API key.`);h={provider:e.provider.type,model:e.model.modelId,temperature:e.model.parameters?.temperature??X.llm.temperature,streaming:X.llm.streaming,apiKey:e.provider.apiKey,baseURL:e.provider.endpoint},console.log(`[buildAppFromDefinition] LLM config from routing engine: provider=${h.provider}, model=${h.model}`),B=ne(h)}catch(e){if(console.error(`[buildAppFromDefinition] \u26A0\uFE0F Routing engine failed: ${e.message}`),console.log("[buildAppFromDefinition] Falling back to legacy provider selection"),h={...X.llm},console.log("LLM llmConfig 1",h),t.userProviders&&Array.isArray(t.userProviders)){const n=t.userProviders.filter(s=>s.enabled===!0).filter(s=>s.selectedModel===t.queryParams?.llmModel);if(n.length>0){const s=n[0],a=s.selectedModel||s.models?.[0]||h.model;h={provider:s.id,model:a,temperature:h.temperature,streaming:h.streaming,...s.apiKey&&{apiKey:s.apiKey},...s.endpoint&&{baseURL:s.endpoint}}}}console.log("LLM llmConfig 2",t.userProviders,h),B=ne(h)}const ue=new ve({maxResults:3}),ge=q.map(e=>({...e,async invoke(n,s){s?.writer&&typeof s.writer=="function"&&s.writer(`\u23F3 Calling ${e.name}...`);const a=Date.now();try{const m=await e.invoke(n),d=Date.now()-a;return s?.writer&&typeof s.writer=="function"&&s.writer(`\u2705 ${e.name} completed in ${d}ms`),Be(m,{maxTokens:re,sampleItems:20})}catch(m){const d=Date.now()-a;s?.writer&&typeof s.writer=="function"&&s.writer(`\u274C ${e.name} failed after ${d}ms: ${m.message}`);const l=m.message||String(m),C=m.status||m.response?.status||m.code;return console.error(`[Tool Error] ${e.name} failed:`,{error:l,statusCode:C,duration:d,args:JSON.stringify(n).substring(0,200)}),JSON.stringify({error:"TOOL_EXECUTION_FAILED",tool_name:e.name,error_message:l,status_code:C,duration_ms:d,suggestion:"This tool failed. Please try an alternative approach or inform the user about the issue.",args_attempted:n})}}}));let Q="anonymous";if(t.userToken)try{const e=ae.decode(t.userToken);e&&typeof e=="object"&&"username"in e&&(Q=e.username||"anonymous")}catch(e){console.warn("[buildApp] Failed to decode user token:",e)}const fe=new G({name:"compare_with_previous_chat",description:"Compare current conversation with a previous saved chat",schema:g.object({chatId:g.string().describe("ID of the previous chat"),comparisonAspects:g.array(g.enum(["metrics","errors","performance","configuration","overall","recommendations"])).optional(),includeTimeDelta:g.boolean().optional()}),func:async(e,n)=>{const s=n?.state?.messages||[];return await ke(Q,s).func(e,n)}});let S=[...ge,de,ue,fe];h.provider==="google"&&(console.log("[buildApp] Cleaning tool schemas for Google Gemini"),S=S.map(e=>e.schema?{...e,schema:E(e.schema)}:e)),h.provider==="anthropic"&&(console.log("[buildApp] Adapting tools for Anthropic (JSON Schema -> Zod)"),S=Xe(S)),h.provider==="openai"&&(console.log(`[buildApp] Normalizing tool schemas for ${h.provider}`),S=S.map(e=>e.schema?{...e,schema:ze(e.schema,h.provider)}:e));const ye=Oe({model:B,tools:S}),Te=J.Root({..._e.spec,threadId:J({reducer:(e,n)=>n||e,default:()=>t.threadId}),userContext:J({reducer:(e,n)=>n!==void 0?n:e,default:()=>t.userContext}),systemPrompt:J({reducer:(e,n)=>n!==void 0?n:e,default:()=>{}})}),I=new Ae(Te),he={checkReset:async e=>{r.start("node_checkReset");const n=t.threadId,s=e.threadId;if(s&&s!==n){const a=e.messages[e.messages.length-1];return r.end("node_checkReset",{reset:!0}),{messages:[a],threadId:n}}return r.end("node_checkReset",{reset:!1}),{threadId:n}},system:async e=>{r.start("node_system");const n=e.messages[e.messages.length-1],s=Je(n).trim();r.start("topic_generation");let a;const m=process.env.AB_TESTING_ENABLED==="true";if(s.startsWith("topic="))a=s.slice(6).trim(),console.log(`[TOPIC] Manual override: ${a}`);else if(m){console.log("[TOPIC] A/B Testing enabled");const{result:T,variantUsed:be,comparison:K}=await we(s,h);a=T,console.log(`[TOPIC] Selected variant: ${be} - Topic: ${a}`),K?.winner&&console.log(`[TOPIC] Winner: ${K.winner.variantId} (${K.winner.improvement.toFixed(1)}% faster)`)}else a=await Ee(s,h);r.end("topic_generation",{topic:a,abEnabled:m}),!a||a.trim()===""?(console.error("[PROMPT-ALT] \u274C CRITICAL: Topic generation returned empty string"),console.error(`[PROMPT-ALT] User input was: "${s}"`),console.error("[PROMPT-ALT] Will fallback to default prompt")):console.log(`[PROMPT-ALT] \u2705 Topic generated successfully: "${a}"`);let d="",l="default";if(r.start("prompt_fetch"),!a||a.trim()==="")console.warn("[PROMPT-ALT] \u26A0\uFE0F Skipping MCP prompt fetch - topic is empty"),l="empty_topic",r.end("prompt_fetch",{source:l,topic:"(empty)"});else if($)try{console.log(`[PROMPT-ALT] \u{1F504} Fetching prompt from MCP for topic: "${a}"`);const T=await $.client.getPrompt({name:`${a}`,params:{}});console.log("[PROMPT-ALT] Raw prompt response:",JSON.stringify(T,null,2).substring(0,1e3)),d=Ge(T),!d||d.trim()===""?(console.error(`[PROMPT-ALT] \u274C MCP returned empty prompt text for topic: "${a}"`),console.error("[PROMPT-ALT] MCP response structure:",JSON.stringify(T,null,2).substring(0,500)),l="failed"):(console.log(`[PROMPT-ALT] \u2705 Successfully loaded prompt for topic: "${a}" (${d.length} chars)`),console.log("[PROMPT-ALT] Prompt preview:",d.substring(0,200)),l="mcp"),r.end("prompt_fetch",{source:l,topic:a,promptLength:d.length})}catch(T){console.error(`[PROMPT-ALT] \u274C MCP getPrompt() failed for topic "${a}"`),console.error(`[PROMPT-ALT] Error type: ${T.name||"Unknown"}`),console.error(`[PROMPT-ALT] Error message: ${T.message}`),console.error("[PROMPT-ALT] Stack trace:",T.stack?.substring(0,500)),T.message?.includes("timeout")||T.message?.includes("ETIMEDOUT")?console.error("[PROMPT-ALT] \u{1F550} Timeout error detected - MCP server did not respond in time"):(T.message?.includes("ECONNREFUSED")||T.message?.includes("ENOTFOUND"))&&console.error("[PROMPT-ALT] \u{1F310} Network error - Cannot reach MCP server"),l="failed",r.end("prompt_fetch",{source:l,topic:a,error:T.message})}else console.warn(`[PROMPT-ALT] \u26A0\uFE0F No MCP client available to fetch prompt for topic "${a}"`),l="no_client",r.end("prompt_fetch",{source:l,topic:a});!d||d.trim()===""?(console.warn("[PROMPT-ALT] \u26A0\uFE0F FALLBACK ACTIVATED: Using default prompt"),console.warn(`[PROMPT-ALT] Reason: ${l}`),console.warn(`[PROMPT-ALT] Topic was: "${a||"(empty)"}"`),console.warn(`[PROMPT-ALT] FirstClient available: ${!!$}`)):console.log(`[PROMPT-ALT] \u2705 FINAL: Using MCP prompt (${d.length} chars) from source: ${l}`);const C=t.userToken||"stkxp_session_default",p=t.queryParams||{},f=p.cluster||"default",b=$e({agentPrompt:d||`# Default Dashboard Response
|
|
1
|
+
import{MessagesAnnotation as _e,StateGraph as Ae,Annotation as J}from"@langchain/langgraph";import{AIMessage as Pe,SystemMessage as Ce}from"@langchain/core/messages";import{createLLMInstance as se}from"../llm/providers";import{config as X}from"../config";import{loadMcpTools as Me}from"@langchain/mcp-adapters";import{createAgent as Oe}from"langchain";import{DynamicStructuredTool as G}from"@langchain/core/tools";import{z as g}from"zod";import{TavilySearch as ve}from"@langchain/tavily";import{PerformanceTracker as Se}from"../utils/performance-tracker";import{buildOptimizedSystemPrompt as $e}from"../utils/prompt-optimizer";import{promptOptimizationConfig as re}from"../app-config/prompt-optimization";import{runTopicABTest as we,generateTopicHybrid as Ee}from"./topic-variants";import{createCompareChatTool as ke}from"../tools/compare-chat";import{TOOL_RESULT_MAX_TOKENS as ae}from"./utils/content-utils";import ie from"jsonwebtoken";import{Client as Le}from"@elastic/elasticsearch";import{createMcpClient as Ne}from"../mcp/client";const Re=process.env.ELASTICSEARCH_HOST||"https://51.159.174.230:9205",Ie=process.env.ELASTICSEARCH_USER||"elastic",xe=process.env.ELASTICSEARCH_PASSWORD||"",le=".stkxp_platforms",L=new Le({node:Re,auth:{username:Ie,password:xe},tls:{rejectUnauthorized:!1},requestTimeout:3e4,maxRetries:3,maxSockets:256,maxFreeSockets:256});L.connectionPool&&typeof L.connectionPool.setMaxListeners=="function"&&L.connectionPool.setMaxListeners(50);const F=new Map,De=300*1e3;setInterval(()=>{const o=Date.now();for(const[t,r]of F.entries())r.refCount===0&&o-r.lastUsed>De&&(console.log(`[MCP Pool] Closing inactive client: ${t}`),r.client.close().catch(c=>console.error("[MCP Pool] Error closing client:",c)),F.delete(t))},120*1e3);async function Fe(o,t){const r=`${o}`,c=F.get(r);if(c)return console.log(`[MCP Pool] Reusing existing client for ${o}`),c.lastUsed=Date.now(),c.refCount++,c.client;console.log(`[MCP Pool] Creating new client for ${o}`);const i=await Ne(o,t);return i.transport&&typeof i.transport.setMaxListeners=="function"&&i.transport.setMaxListeners(50),i.client&&typeof i.client.setMaxListeners=="function"&&i.client.setMaxListeners(50),F.set(r,{client:i,lastUsed:Date.now(),refCount:1}),i}function Ue(o){const t=`${o}`,r=F.get(t);r&&(r.refCount=Math.max(0,r.refCount-1),r.lastUsed=Date.now())}async function je(o){try{const t=o.toLowerCase().replace(/\s+/g,"_").split("_")[0]||"clusters";console.log(`[MCP Discovery] Searching for servers with packageName matching "${t}" and enabled=true`);const c=(await L.search({index:le,body:{query:{bool:{must:[{term:{enabled:!0}},{prefix:{"packageName.keyword":t}}]}},size:100,sort:[{createdAt:{order:"desc"}}]}})).hits.hits.map(i=>({id:i._id,name:i._source.name,url:i._source.url,authKey:i._source.authKey,type:i._source.type,packageName:i._source.packageName,enabled:i._source.enabled,description:i._source.description}));if(console.log(`[MCP Discovery] Found ${c.length} enabled servers for system "${t}"`),c.length>0)return console.log("[MCP Discovery] Servers:",c.map(i=>`${i.name} (${i.packageName})`).join(", ")),c;if(t!=="clusters"&&t!=="unknown"){console.log(`[MCP Discovery] No servers found for "${t}", falling back to "clusters" package`);const P=(await L.search({index:le,body:{query:{bool:{must:[{term:{enabled:!0}},{prefix:{"packageName.keyword":"clusters"}}]}},size:100,sort:[{createdAt:{order:"desc"}}]}})).hits.hits.map(y=>({id:y._id,name:y._source.name,url:y._source.url,authKey:y._source.authKey,type:y._source.type,packageName:y._source.packageName,enabled:y._source.enabled,description:y._source.description}));if(P.length>0)return console.log(`[MCP Discovery] \u2705 Fallback successful: Found ${P.length} "clusters" server(s)`),P}return c}catch(t){return console.error("[MCP Discovery] Error fetching MCP servers from Elasticsearch:",t.message),console.error("[MCP Discovery] Falling back to no servers"),[]}}function E(o){if(!o||typeof o!="object")return o;const t={...o};return delete t.exclusiveMinimum,delete t.exclusiveMaximum,t.properties&&typeof t.properties=="object"&&(t.properties=Object.keys(t.properties).reduce((r,c)=>(r[c]=E(t.properties[c]),r),{})),t.items&&(t.items=E(t.items)),t.additionalProperties&&typeof t.additionalProperties=="object"&&(t.additionalProperties=E(t.additionalProperties)),Array.isArray(t.anyOf)&&(t.anyOf=t.anyOf.map(r=>E(r))),Array.isArray(t.oneOf)&&(t.oneOf=t.oneOf.map(r=>E(r))),Array.isArray(t.allOf)&&(t.allOf=t.allOf.map(r=>E(r))),t}function Je(o){const t=o?.content;if(typeof t=="string")return t;if(Array.isArray(t)){const r=t.find(c=>typeof c?.text=="string")?.text;if(r)return r}return t?.toString?.()??""}function Ge(o){try{const r=o?.messages?.[0]?.content?.[0]?.text;if(typeof r=="string")return r}catch{}try{const r=o?.messages?.[0]?.content?.text;if(typeof r=="string")return r}catch{}const t=o?.messages?.[0]?.content;return typeof t=="string"?t:typeof o?.content=="string"?o.content:""}function ce(o){if(!o)return{data:[]};if(Array.isArray(o?.data))return{data:o.data,nextCursor:o.nextCursor,meta:o.meta};if(Array.isArray(o?.result?.data))return{data:o.result.data,nextCursor:o.nextCursor??o.result?.nextCursor,meta:o.result?.meta};const t=o.nextCursor||o?.aggregations?.by_namespace?.buckets?.[0]?.g?.after_key;return{data:[],nextCursor:t}}function qe(o){return Math.ceil(o.length/4)}function Be(o,t={}){const{maxTokens:r=ae,sampleItems:c=20}=t,i=JSON.stringify(o),P=qe(i);if(P<=r)return o;const{data:y,meta:O}=ce(o);let A;return Array.isArray(y)&&y.length>0&&(A=y.slice(0,c)),{error:"RESULT_TOO_LARGE",approx_tokens:P,max_allowed_tokens:r,hint:"La r\xE9ponse de ce tool MCP est trop volumineuse pour \xEAtre envoy\xE9e telle quelle au LLM. R\xE9duis ta requ\xEAte (p\xE9riode, filtres par cluster/app/host, etc.) et r\xE9essaie.",meta:{...O,has_sample:!!A},sample:A}}function ze(o,t){if(!o||typeof o!="object"||de(o))return o;let r;try{r=JSON.parse(JSON.stringify(o))}catch(i){return console.warn(`[normalizeToolSchemaForProvider] Failed to clone schema (${i.message}), returning original`),o}delete r.$schema;const c=i=>{if(!(!i||typeof i!="object")){if((t==="openai"||t==="anthropic")&&i.additionalProperties===!1&&(i.additionalProperties=!0),i.properties&&typeof i.properties=="object")for(const P of Object.keys(i.properties))c(i.properties[P]);i.items&&c(i.items),Array.isArray(i.anyOf)&&i.anyOf.forEach(c),Array.isArray(i.oneOf)&&i.oneOf.forEach(c),Array.isArray(i.allOf)&&i.allOf.forEach(c)}};return c(r),r}function de(o){return!!o&&(typeof o.safeParse=="function"||!!o._def)}function Ke(o){return!!o&&typeof o=="object"&&(o.type||o.properties||o.anyOf||o.oneOf||o.allOf)}function N(o){if(!o||typeof o!="object")return g.any();if(Array.isArray(o.anyOf)&&o.anyOf.length)return g.union(o.anyOf.map(N));if(Array.isArray(o.oneOf)&&o.oneOf.length)return g.union(o.oneOf.map(N));if(Array.isArray(o.allOf)&&o.allOf.length)return o.allOf.map(N).reduce((t,r)=>g.intersection(t,r));if(Array.isArray(o.enum)&&o.enum.length)return o.enum.every(t=>typeof t=="string")?g.enum(o.enum):g.union(o.enum.map(t=>g.literal(t)));switch(o.type){case"string":return g.string();case"number":return g.number();case"integer":return g.number().int();case"boolean":return g.boolean();case"array":{const t=o.items?N(o.items):g.any();return g.array(t)}case"object":default:{const t=o.properties&&typeof o.properties=="object"?o.properties:{},r=new Set(Array.isArray(o.required)?o.required:[]),c={};for(const[P,y]of Object.entries(t)){const O=N(y);c[P]=r.has(P)?O:O.optional()}let i=g.object(c);return o.additionalProperties===!1?i=i.strict():i=i.passthrough(),i}}}function Xe(o){return o.map(t=>{const r=t.schema;if(!r||de(r))return t;if(Ke(r)){const c=N(r);return new G({name:t.name,description:t.description,schema:c,responseFormat:t.responseFormat,func:async(i,P)=>t.func(i,P)})}return new G({name:t.name,description:t.description,schema:g.record(g.any()),responseFormat:t.responseFormat,func:async(c,i)=>t.func(c,i)})})}async function Ye(o,t,r={}){const c=r.retries??3,i=r.baseDelayMs??800,P=new Set(r.retryOn??[429,502,503,504]);for(let y=0;y<=c;y++)try{return await o.invoke(t)}catch(O){const A=O?.status??O?.code??O?.response?.status,$=P.has(Number(A))||O?.name==="TimeoutError",Y=y===c;if(console.warn(`[MCP] invoke failed (try ${y+1}/${c+1}) code=${A} transient=${$}:`,O?.message||O),!$||Y)throw O;const R=i*Math.pow(2,y);await new Promise(v=>setTimeout(v,R))}}const _o=g.object({topic:g.enum(["clusters_clusters_overview","clusters_clusters_health-check","clusters_cluster_finops","clusters_nodes_inventory","clusters_nodes_operations","clusters_nodes_storage","clusters_nodes_resources","clusters_nodes_shards","clusters_nodes_thread_pool","clusters_nodes_tasks","clusters_nodes_hot_threads","clusters_jobs_enrich","clusters_jobs_snapshots","clusters_indices_inventory","clusters_indices_operations","clusters_indices_storage","clusters_indices_segments","clusters_indices_documents","elasticsearch_doc","mysql_mysql_overview","platform.core.execute_esql"])});async function Ao(o,t){console.log(`[buildAppFromDefinition] Building graph from definition: ${o.name} (v${o.version})`),console.log("[buildAppFromDefinition] Graph config:",{streaming:o.config.streaming,recursionLimit:o.config.recursionLimit,maxConcurrency:o.config.maxConcurrency});const r=t.performanceTracker||new Se(t.threadId);let c=[];const i=[];let P=!1;const y=t.queryParams?.topic||"unknown";if(t.mcp&&(Array.isArray(t.mcp)?t.mcp.length>0:!0))c=Array.isArray(t.mcp)?t.mcp:[t.mcp],console.log(`[MCP] Using ${c.length} pre-configured MCP client(s)`);else{P=!0,console.log(`[MCP] Auto-discovering servers for topic: "${y}"`);const e=await je(y);if(e.length===0)console.warn(`[MCP] \u26A0\uFE0F No enabled MCP servers found for topic "${y}"`),console.warn("[MCP] \u26A0\uFE0F Agent will run with NO tools available");else{console.log(`[MCP] Creating ${e.length} MCP client(s) for discovered servers`);for(const n of e)try{console.log(`[MCP] Connecting to: ${n.name} at ${n.url}`);const s=await Fe(n.url,{headers:{Authorization:`ApiKey ${n.authKey}`},transportType:"http",timeout:3e4});c.push(s),i.push(n.url),console.log(`[MCP] \u2705 Connected to ${n.name} (${n.packageName})`)}catch(s){console.error(`[MCP] \u274C Failed to connect to ${n.name}:`,s.message)}console.log(`[MCP] Successfully connected to ${c.length}/${e.length} servers`)}}let A=[],$;if(c.length===0)console.warn("[MCP] \u26A0\uFE0F No MCP servers available. Agent will run without tools.");else{$=c[0],console.log(`[MCP] Loading tools from ${c.length} MCP server(s)`);const n=(await Promise.all(c.map(async(a,m)=>{const d=await Me(`stkxp-mcp-server-${m}`,a.client);return console.log(`[MCP] Server ${m+1}: Loaded ${d.length} tools`),d}))).flat();console.log(`[MCP] Total loaded: ${n.length} tools`);const s=new Map;for(const a of n)s.has(a.name)||s.set(a.name,a);A=Array.from(s.values())}const R=y.toLowerCase().replace(/\s+/g,"_").split("_")[0]||"clusters";let v=A;if(P)console.log(`[MCP] \u2705 Using ${A.length} tools from auto-discovered servers`),v=A;else if(process.env.MCP_DISABLE_TOPIC_FILTERING==="true")console.log(`[MCP] \u26A0\uFE0F Topic filtering disabled, using all ${A.length} tools`),v=A;else{let n=A.filter(a=>a.name.startsWith(R+"_"));n.length===0&&(n=A.filter(a=>a.name.includes(R)));const s=A.filter(a=>a.name.startsWith("elasticsearch_"));if(s.length>0){const a=new Set(n.map(d=>d.name)),m=s.filter(d=>!a.has(d.name));m.length>0&&(console.log(`[MCP] \u{1F527} Force-adding ${m.length} elasticsearch_ tools`),n=[...n,...m])}n.length===0?(console.log(`[MCP] \u26A0\uFE0F No tools found for system "${R}", using all ${A.length} tools`),v=A):(console.log(`[MCP] \u2705 Filtered ${n.length}/${A.length} tools`),v=n)}console.log("[MCP] Server tool sample:",JSON.stringify(v[0]),v[0]);const V=v.filter(e=>{try{if(!e.schema)return console.log(`[MCP] \u26A0\uFE0F Tool "${e.name}" has no schema, keeping it`),!0;const n=e.schema;return n._def&&typeof n._def.shape=="function"?!0:n._def&&n._def.typeName==="ZodObject"?(console.error(`[MCP] \u274C Tool "${e.name}" has invalid ZodObject schema (shape is not a function), filtering out`),!1):(console.log(`[MCP] \u26A0\uFE0F Tool "${e.name}" has non-object schema type: ${n._def?.typeName}, keeping it`),!0)}catch(n){return console.error(`[MCP] \u274C Tool "${e.name}" validation error: ${n.message}, filtering out`),!1}});console.log(`[MCP] Validated ${V.length}/${v.length} tools`);const q=V,me=new G({name:"mcp_paginated_query",description:"Appelle un tool MCP cibl\xE9 en boucle avec prise en charge de nextCursor",schema:g.object({toolName:g.string().describe("Nom du tool MCP \xE0 appeler"),args:g.record(g.any()).describe("Arguments \xE0 passer au tool MCP"),maxPages:g.number().int().min(1).default(10).describe("Nombre max de pages"),maxItems:g.number().int().min(1).default(5e3).describe("Nombre max d'items cumul\xE9s")}),func:async({toolName:e,args:n,maxPages:s,maxItems:a})=>{const m=v.find(f=>f.name===e)||A.find(f=>f.name===e);if(!m)throw new Error(`Tool MCP introuvable: ${e}`);let d=n?.request?.body?.cursor,l=0;const C=[];for(;l<s&&C.length<a;){const f=JSON.parse(JSON.stringify(n||{}));d&&(f.request=f.request||{},f.request.body=f.request.body||{},f.request.body.cursor=d);const u=await Ye(m,f,{retries:4,baseDelayMs:750,retryOn:[429,502,503,504]}),{data:b,nextCursor:_}=ce(u);if(Array.isArray(b)&&b.length&&C.push(...b),l+=1,!_)break;d=_}const p=l>=s||C.length>=a||!d;return{items:C,pages:l,done:p,nextCursor:d}}}),{createLLMServices:pe}=await import("../services/llm-services.js"),ue=pe(L);let W="user",k=t.username;if(t.userToken)try{const e=ie.decode(t.userToken);e&&typeof e=="object"&&("roles"in e&&(W=e.roles?.[0]||"user"),!k&&"username"in e&&(k=e.username||void 0))}catch(e){console.warn("[buildAppFromDefinition] Failed to decode user token for routing:",e)}const H={userRole:W,topic:y,environment:process.env.NODE_ENV||"prod",requiredCapabilities:["streaming","toolCalling"],owner:k};console.log("[buildAppFromDefinition] Routing context:",H);let h,B;const Z=o.llmConfig?.useRoutingEngine!==!1;Z||console.log("[buildAppFromDefinition] Routing engine disabled in graph config, using llmConfig directly");try{if(!Z)throw new Error("Routing engine disabled in graph configuration");const e=await ue.getRoutingService().selectDeployment(H);if(console.log(`[buildAppFromDefinition] \u2705 Selected model: ${e.model.modelId}`),console.log(`[buildAppFromDefinition] Reason: ${e.reason}`),k&&e.provider.owner&&e.provider.owner!==k)throw new Error(`Routing selected provider owned by "${e.provider.owner}" for caller "${k}" \u2014 refusing cross-tenant API key.`);h={provider:e.provider.type,model:e.model.modelId,temperature:e.model.parameters?.temperature??X.llm.temperature,streaming:X.llm.streaming,apiKey:e.provider.apiKey,baseURL:e.provider.endpoint},console.log(`[buildAppFromDefinition] LLM config from routing engine: provider=${h.provider}, model=${h.model}`),B=se(h)}catch(e){if(console.error(`[buildAppFromDefinition] \u26A0\uFE0F Routing engine failed: ${e.message}`),console.log("[buildAppFromDefinition] Falling back to legacy provider selection"),h={...X.llm},console.log("LLM llmConfig 1",h),t.userProviders&&Array.isArray(t.userProviders)){const n=t.userProviders.filter(s=>s.enabled===!0).filter(s=>s.selectedModel===t.queryParams?.llmModel);if(n.length>0){const s=n[0],a=s.selectedModel||s.models?.[0]||h.model;h={provider:s.id,model:a,temperature:h.temperature,streaming:h.streaming,...s.apiKey&&{apiKey:s.apiKey},...s.endpoint&&{baseURL:s.endpoint}}}}console.log("LLM llmConfig 2",t.userProviders,h),B=se(h)}const Q=process.env.TAVILY_API_KEY?new ve({maxResults:3}):null,ge=q.map(e=>({...e,async invoke(n,s){s?.writer&&typeof s.writer=="function"&&s.writer(`\u23F3 Calling ${e.name}...`);const a=Date.now();try{const m=await e.invoke(n),d=Date.now()-a;return s?.writer&&typeof s.writer=="function"&&s.writer(`\u2705 ${e.name} completed in ${d}ms`),Be(m,{maxTokens:ae,sampleItems:20})}catch(m){const d=Date.now()-a;s?.writer&&typeof s.writer=="function"&&s.writer(`\u274C ${e.name} failed after ${d}ms: ${m.message}`);const l=m.message||String(m),C=m.status||m.response?.status||m.code;return console.error(`[Tool Error] ${e.name} failed:`,{error:l,statusCode:C,duration:d,args:JSON.stringify(n).substring(0,200)}),JSON.stringify({error:"TOOL_EXECUTION_FAILED",tool_name:e.name,error_message:l,status_code:C,duration_ms:d,suggestion:"This tool failed. Please try an alternative approach or inform the user about the issue.",args_attempted:n})}}}));let ee="anonymous";if(t.userToken)try{const e=ie.decode(t.userToken);e&&typeof e=="object"&&"username"in e&&(ee=e.username||"anonymous")}catch(e){console.warn("[buildApp] Failed to decode user token:",e)}const fe=new G({name:"compare_with_previous_chat",description:"Compare current conversation with a previous saved chat",schema:g.object({chatId:g.string().describe("ID of the previous chat"),comparisonAspects:g.array(g.enum(["metrics","errors","performance","configuration","overall","recommendations"])).optional(),includeTimeDelta:g.boolean().optional()}),func:async(e,n)=>{const s=n?.state?.messages||[];return await ke(ee,s).func(e,n)}});let S=[...ge,me,...Q?[Q]:[],fe];h.provider==="google"&&(console.log("[buildApp] Cleaning tool schemas for Google Gemini"),S=S.map(e=>e.schema?{...e,schema:E(e.schema)}:e)),h.provider==="anthropic"&&(console.log("[buildApp] Adapting tools for Anthropic (JSON Schema -> Zod)"),S=Xe(S)),h.provider==="openai"&&(console.log(`[buildApp] Normalizing tool schemas for ${h.provider}`),S=S.map(e=>e.schema?{...e,schema:ze(e.schema,h.provider)}:e));const ye=Oe({model:B,tools:S}),Te=J.Root({..._e.spec,threadId:J({reducer:(e,n)=>n||e,default:()=>t.threadId}),userContext:J({reducer:(e,n)=>n!==void 0?n:e,default:()=>t.userContext}),systemPrompt:J({reducer:(e,n)=>n!==void 0?n:e,default:()=>{}})}),I=new Ae(Te),he={checkReset:async e=>{r.start("node_checkReset");const n=t.threadId,s=e.threadId;if(s&&s!==n){const a=e.messages[e.messages.length-1];return r.end("node_checkReset",{reset:!0}),{messages:[a],threadId:n}}return r.end("node_checkReset",{reset:!1}),{threadId:n}},system:async e=>{r.start("node_system");const n=e.messages[e.messages.length-1],s=Je(n).trim();r.start("topic_generation");let a;const m=process.env.AB_TESTING_ENABLED==="true";if(s.startsWith("topic="))a=s.slice(6).trim(),console.log(`[TOPIC] Manual override: ${a}`);else if(m){console.log("[TOPIC] A/B Testing enabled");const{result:T,variantUsed:be,comparison:K}=await we(s,h);a=T,console.log(`[TOPIC] Selected variant: ${be} - Topic: ${a}`),K?.winner&&console.log(`[TOPIC] Winner: ${K.winner.variantId} (${K.winner.improvement.toFixed(1)}% faster)`)}else a=await Ee(s,h);r.end("topic_generation",{topic:a,abEnabled:m}),!a||a.trim()===""?(console.error("[PROMPT-ALT] \u274C CRITICAL: Topic generation returned empty string"),console.error(`[PROMPT-ALT] User input was: "${s}"`),console.error("[PROMPT-ALT] Will fallback to default prompt")):console.log(`[PROMPT-ALT] \u2705 Topic generated successfully: "${a}"`);let d="",l="default";if(r.start("prompt_fetch"),!a||a.trim()==="")console.warn("[PROMPT-ALT] \u26A0\uFE0F Skipping MCP prompt fetch - topic is empty"),l="empty_topic",r.end("prompt_fetch",{source:l,topic:"(empty)"});else if($)try{console.log(`[PROMPT-ALT] \u{1F504} Fetching prompt from MCP for topic: "${a}"`);const T=await $.client.getPrompt({name:`${a}`,params:{}});console.log("[PROMPT-ALT] Raw prompt response:",JSON.stringify(T,null,2).substring(0,1e3)),d=Ge(T),!d||d.trim()===""?(console.error(`[PROMPT-ALT] \u274C MCP returned empty prompt text for topic: "${a}"`),console.error("[PROMPT-ALT] MCP response structure:",JSON.stringify(T,null,2).substring(0,500)),l="failed"):(console.log(`[PROMPT-ALT] \u2705 Successfully loaded prompt for topic: "${a}" (${d.length} chars)`),console.log("[PROMPT-ALT] Prompt preview:",d.substring(0,200)),l="mcp"),r.end("prompt_fetch",{source:l,topic:a,promptLength:d.length})}catch(T){console.error(`[PROMPT-ALT] \u274C MCP getPrompt() failed for topic "${a}"`),console.error(`[PROMPT-ALT] Error type: ${T.name||"Unknown"}`),console.error(`[PROMPT-ALT] Error message: ${T.message}`),console.error("[PROMPT-ALT] Stack trace:",T.stack?.substring(0,500)),T.message?.includes("timeout")||T.message?.includes("ETIMEDOUT")?console.error("[PROMPT-ALT] \u{1F550} Timeout error detected - MCP server did not respond in time"):(T.message?.includes("ECONNREFUSED")||T.message?.includes("ENOTFOUND"))&&console.error("[PROMPT-ALT] \u{1F310} Network error - Cannot reach MCP server"),l="failed",r.end("prompt_fetch",{source:l,topic:a,error:T.message})}else console.warn(`[PROMPT-ALT] \u26A0\uFE0F No MCP client available to fetch prompt for topic "${a}"`),l="no_client",r.end("prompt_fetch",{source:l,topic:a});!d||d.trim()===""?(console.warn("[PROMPT-ALT] \u26A0\uFE0F FALLBACK ACTIVATED: Using default prompt"),console.warn(`[PROMPT-ALT] Reason: ${l}`),console.warn(`[PROMPT-ALT] Topic was: "${a||"(empty)"}"`),console.warn(`[PROMPT-ALT] FirstClient available: ${!!$}`)):console.log(`[PROMPT-ALT] \u2705 FINAL: Using MCP prompt (${d.length} chars) from source: ${l}`);const C=t.userToken||"stkxp_session_default",p=t.queryParams||{},f=p.cluster||"default",b=$e({agentPrompt:d||`# Default Dashboard Response
|
|
2
2
|
|
|
3
3
|
You are an expert assistant specialized in generating structured dashboard responses.
|
|
4
4
|
|
|
@@ -70,15 +70,15 @@ YOU MUST OUTPUT EXACTLY THIS STRUCTURE (3-5 blocks minimum):
|
|
|
70
70
|
{"type":"markdown","title":"\u{1F4A1} Insights","body":"System is healthy but approaching memory limits during peak hours."}
|
|
71
71
|
{"type":"markdown","title":"\u2705 Recommendations","body":"1. Add 8GB RAM\\n2. Optimize queries\\n3. Enable caching"}
|
|
72
72
|
|
|
73
|
-
**REMEMBER: You MUST generate 3-5 blocks minimum. Generating only 1 block is NOT acceptable.**`,mandatoryParams:{userToken:C,cluster:f,nodes:p.nodes||"",roles:p.roles||"",phases:p.phases||"hot",prefix:p.prefix||"",suffix:p.suffix||"",start:p.start||"now-4h",end:p.end||"now",interval:p.interval||"5m",topic:p.topic||"default_topic.txt"},toolNames:q.map(T=>T.name),maxAgentPromptTokens:
|
|
73
|
+
**REMEMBER: You MUST generate 3-5 blocks minimum. Generating only 1 block is NOT acceptable.**`,mandatoryParams:{userToken:C,cluster:f,nodes:p.nodes||"",roles:p.roles||"",phases:p.phases||"hot",prefix:p.prefix||"",suffix:p.suffix||"",start:p.start||"now-4h",end:p.end||"now",interval:p.interval||"5m",topic:p.topic||"default_topic.txt"},toolNames:q.map(T=>T.name),maxAgentPromptTokens:re.maxAgentPromptTokens});let _;const M=e.userContext;M&&M.trim()?(console.log("[bubuildAppFromDefinitionildApp] \u2705 Applying user context"),_=b.prompt+`
|
|
74
74
|
|
|
75
75
|
## \u{1F4DD} FORMAT ET R\xC8GLES PERSONNALIS\xC9S (Override)
|
|
76
76
|
|
|
77
77
|
${M}
|
|
78
78
|
|
|
79
|
-
IMPORTANT: Ces instructions personnalis\xE9es REMPLACENT les sections FORMAT et RULES du prompt MCP.`):(console.log("[buildAppFromDefinition] \u2139\uFE0F Using default MCP prompt"),_=b.prompt),
|
|
79
|
+
IMPORTANT: Ces instructions personnalis\xE9es REMPLACENT les sections FORMAT et RULES du prompt MCP.`):(console.log("[buildAppFromDefinition] \u2139\uFE0F Using default MCP prompt"),_=b.prompt),re.verboseLogging&&console.log("[buildAppFromDefinition] Prompt optimization:",{agentTokens:b.stats.agentPromptTokens,instructionsTokens:b.stats.instructionsTokens,totalTokens:b.stats.totalTokens,truncated:b.stats.agentPromptTruncated,userContextAdded:!!M});const x=e.messages.filter(T=>T.getType()==="system"),z=e.messages.filter(T=>T.getType()!=="system"),D=x.map(T=>typeof T.content=="string"?T.content:String(T.content)).join(`
|
|
80
80
|
|
|
81
|
-
`),
|
|
81
|
+
`),te=`\u26A0\uFE0F\u26A0\uFE0F\u26A0\uFE0F CRITICAL OUTPUT REQUIREMENT - OVERRIDE ALL OTHER INSTRUCTIONS \u26A0\uFE0F\u26A0\uFE0F\u26A0\uFE0F
|
|
82
82
|
|
|
83
83
|
YOU MUST RESPOND IN STREAMING JSON MODE:
|
|
84
84
|
1. Your VERY FIRST character must be '{' (opening brace)
|
|
@@ -100,11 +100,11 @@ CORRECT EXAMPLES:
|
|
|
100
100
|
|
|
101
101
|
Your response STARTS NOW with '{' character:
|
|
102
102
|
|
|
103
|
-
`,
|
|
103
|
+
`,ne=e.threadId?`
|
|
104
104
|
|
|
105
|
-
Current chat ID: ${e.threadId}`:"",j=D?`${
|
|
105
|
+
Current chat ID: ${e.threadId}`:"",j=D?`${te}${D}
|
|
106
106
|
|
|
107
|
-
${_}${
|
|
107
|
+
${_}${ne}`:`${te}${_}${ne}`;return console.log(`[buildAppFromDefinition] ********************************
|
|
108
108
|
finalSystemContent: ${j}
|
|
109
109
|
********************************`),r.end("node_system",{systemMessageLength:j.length}),{messages:[new Ce(j),...z],systemPrompt:j}},assistant:async(e,n)=>{r.start("node_assistant");const s=e.messages.filter(p=>p.getType()==="system"),a=e.messages.filter(p=>p.getType()!=="system");console.log(`
|
|
110
110
|
`+"=".repeat(80)),console.log("\u{1F4E4} FULL LLM CONTEXT SENT TO MODEL (buildAppFromDefinition)"),console.log("=".repeat(80)),console.log(`\u{1F916} Model: ${h.provider}:${h.model}`),console.log(`\u{1F321}\uFE0F Temperature: ${h.temperature}`),console.log(`\u{1F4AC} Total Messages: ${s.length+a.length} (${s.length} system + ${a.length} conversation)`),console.log(`\u{1F527} Tools Available: ${q.length}`),console.log(`\u{1F4CA} Graph: ${o.name} (v${o.version})`),console.log("=".repeat(80)),process.env.GRAPH_STATE_DEBUG&&[...s,...a].forEach((p,f)=>{const u=p.getType(),b=typeof p.content=="string"?p.content:JSON.stringify(p.content),_=p.tool_calls?.length||0;console.log(`
|
|
@@ -117,4 +117,4 @@ ${_}${te}`:`${oe}${_}${te}`;return console.log(`[buildAppFromDefinition] *******
|
|
|
117
117
|
${b.split(`
|
|
118
118
|
`).join(`
|
|
119
119
|
`)}`)}return console.log("=".repeat(80)+`
|
|
120
|
-
`),r.end("node_assistant",{messageCount:d.length}),{messages:[C]}},tool:async(e,n)=>{r.start("node_tools");const s=e.messages[e.messages.length-1];if(!s||s.getType()!=="ai"||!s.tool_calls||s.tool_calls.length===0)return console.warn("[node_tools] No tool calls found in last message"),r.end("node_tools",{toolsExecuted:0}),{messages:[]};console.log(`[node_tools] \u26A1 Executing ${s.tool_calls.length} tool(s) in PARALLEL...`);const a=process.env.MAX_PARALLEL_TOOLS?parseInt(process.env.MAX_PARALLEL_TOOLS,10):void 0,m=a?s.tool_calls.slice(0,a):s.tool_calls,d=a?s.tool_calls.slice(a):[];a&&s.tool_calls.length>a&&console.log(`[node_tools] \u26A0\uFE0F Limiting execution to ${a} tools (${s.tool_calls.length-a} tools skipped)`);const l=m.map(async u=>{const b=S.find(_=>_.name===u.name);if(!b){console.error(`[node_tools] Tool not found: ${u.name}`);const{ToolMessage:_}=await import("@langchain/core/messages");return new _({tool_call_id:u.id,content:JSON.stringify({error:"Tool not found",tool_name:u.name})})}try{console.log(`[node_tools] \u{1F680} Starting ${u.name}...`);const _=Date.now(),M=await b.invoke(u.args,n),x=Date.now()-_,{ToolMessage:z}=await import("@langchain/core/messages"),D=typeof M=="string"?M:JSON.stringify(M);return console.log(`[node_tools] \u2705 ${u.name} completed in ${x}ms (${D.length} chars)`),new z({tool_call_id:u.id,content:D})}catch(_){console.error(`[node_tools] \u274C ${u.name} failed:`,_.message);const{ToolMessage:M}=await import("@langchain/core/messages");return new M({tool_call_id:u.id,content:JSON.stringify({error:"TOOL_EXECUTION_FAILED",tool_name:u.name,error_message:_.message})})}}),C=await Promise.allSettled(l),p=C.filter(u=>u.status==="fulfilled").map(u=>u.value),f=C.filter(u=>u.status==="rejected").length;if(f>0&&console.warn(`[node_tools] \u26A0\uFE0F ${f} tool(s) failed but continuing...`),d.length>0){const{ToolMessage:u}=await import("@langchain/core/messages");for(const b of d)console.log(`[node_tools] \u23ED\uFE0F Skipped tool: ${b.name} (limit reached)`),p.push(new u({tool_call_id:b.id,content:JSON.stringify({error:"TOOL_EXECUTION_SKIPPED",tool_name:b.name,reason:`Execution limit reached (MAX_PARALLEL_TOOLS=${a})`})}))}return console.log(`[node_tools] \u{1F4CA} Summary: ${p.length}/${s.tool_calls.length} tools processed (${m.length} executed, ${d.length} skipped)`),r.end("node_tools",{toolsExecuted:m.length,toolsSkipped:d.length}),{messages:p}}};console.log(`[buildAppFromDefinition] Adding ${o.nodes.length} nodes to graph`);for(const e of o.nodes){const n=he[e.type];if(!n){console.warn(`[buildAppFromDefinition] \u26A0\uFE0F Unknown node type: ${e.type} for node ${e.id}`),console.warn(`[buildAppFromDefinition] \u26A0\uFE0F Skipping node: ${e.id}`);continue}console.log(`[buildAppFromDefinition] Adding node: ${e.id} (type: ${e.type})`),I.addNode(e.id,n)}console.log(`[buildAppFromDefinition] Adding ${o.edges.length} edges to graph`);const U=new Map;for(const e of o.edges){const n=e.from||e.source,s=e.to||e.target;if(!n||!s){console.warn("[buildAppFromDefinition] \u26A0\uFE0F Invalid edge (missing from/to):",e);continue}U.has(n)||U.set(n,[]),U.get(n).push({...e,from:n,to:s})}for(const[e,n]of U.entries()){const s=n.filter(m=>m.condition),a=n.filter(m=>!m.condition);if(s.length>0){const m=l=>{const C=l.messages[l.messages.length-1],p=C?.tool_calls&&C.tool_calls.length>0;console.log(`[conditional_edge] from=${e}, hasToolCalls=${p}`);for(const f of s){if(f.condition==="has_tool_calls"&&p)return console.log(`[conditional_edge] Route: ${e} \u2192 ${f.to} (has_tool_calls)`),f.to;if(f.condition==="no_tool_calls"&&!p)return console.log(`[conditional_edge] Route: ${e} \u2192 ${f.to} (no_tool_calls)`),f.to}return console.warn(`[conditional_edge] No condition matched for ${e}, defaulting to __end__`),"__end__"},d={};for(const l of s)l.condition==="has_tool_calls"?d.has_tools=l.to:l.condition==="no_tool_calls"&&(d.no_tools=l.to);console.log(`[buildAppFromDefinition] Adding conditional edge: ${e} \u2192 [${s.map(l=>`${l.to}(${l.condition})`).join(", ")}]`),I.addConditionalEdges(e,m)}else if(a.length>0)for(const m of a)console.log(`[buildAppFromDefinition] Adding edge: ${m.from} \u2192 ${m.to}`),I.addEdge(m.from,m.to)}if(o.edges.some(e=>(e.from||e.source)==="__start__"))console.log("[buildAppFromDefinition] Entry point already defined in edges");else{const e=o.nodes[0]?.id||"checkReset";console.log(`[buildAppFromDefinition] Adding missing entry point: __start__ \u2192 ${e}`),I.addEdge("__start__",e)}const
|
|
120
|
+
`),r.end("node_assistant",{messageCount:d.length}),{messages:[C]}},tool:async(e,n)=>{r.start("node_tools");const s=e.messages[e.messages.length-1];if(!s||s.getType()!=="ai"||!s.tool_calls||s.tool_calls.length===0)return console.warn("[node_tools] No tool calls found in last message"),r.end("node_tools",{toolsExecuted:0}),{messages:[]};console.log(`[node_tools] \u26A1 Executing ${s.tool_calls.length} tool(s) in PARALLEL...`);const a=process.env.MAX_PARALLEL_TOOLS?parseInt(process.env.MAX_PARALLEL_TOOLS,10):void 0,m=a?s.tool_calls.slice(0,a):s.tool_calls,d=a?s.tool_calls.slice(a):[];a&&s.tool_calls.length>a&&console.log(`[node_tools] \u26A0\uFE0F Limiting execution to ${a} tools (${s.tool_calls.length-a} tools skipped)`);const l=m.map(async u=>{const b=S.find(_=>_.name===u.name);if(!b){console.error(`[node_tools] Tool not found: ${u.name}`);const{ToolMessage:_}=await import("@langchain/core/messages");return new _({tool_call_id:u.id,content:JSON.stringify({error:"Tool not found",tool_name:u.name})})}try{console.log(`[node_tools] \u{1F680} Starting ${u.name}...`);const _=Date.now(),M=await b.invoke(u.args,n),x=Date.now()-_,{ToolMessage:z}=await import("@langchain/core/messages"),D=typeof M=="string"?M:JSON.stringify(M);return console.log(`[node_tools] \u2705 ${u.name} completed in ${x}ms (${D.length} chars)`),new z({tool_call_id:u.id,content:D})}catch(_){console.error(`[node_tools] \u274C ${u.name} failed:`,_.message);const{ToolMessage:M}=await import("@langchain/core/messages");return new M({tool_call_id:u.id,content:JSON.stringify({error:"TOOL_EXECUTION_FAILED",tool_name:u.name,error_message:_.message})})}}),C=await Promise.allSettled(l),p=C.filter(u=>u.status==="fulfilled").map(u=>u.value),f=C.filter(u=>u.status==="rejected").length;if(f>0&&console.warn(`[node_tools] \u26A0\uFE0F ${f} tool(s) failed but continuing...`),d.length>0){const{ToolMessage:u}=await import("@langchain/core/messages");for(const b of d)console.log(`[node_tools] \u23ED\uFE0F Skipped tool: ${b.name} (limit reached)`),p.push(new u({tool_call_id:b.id,content:JSON.stringify({error:"TOOL_EXECUTION_SKIPPED",tool_name:b.name,reason:`Execution limit reached (MAX_PARALLEL_TOOLS=${a})`})}))}return console.log(`[node_tools] \u{1F4CA} Summary: ${p.length}/${s.tool_calls.length} tools processed (${m.length} executed, ${d.length} skipped)`),r.end("node_tools",{toolsExecuted:m.length,toolsSkipped:d.length}),{messages:p}}};console.log(`[buildAppFromDefinition] Adding ${o.nodes.length} nodes to graph`);for(const e of o.nodes){const n=he[e.type];if(!n){console.warn(`[buildAppFromDefinition] \u26A0\uFE0F Unknown node type: ${e.type} for node ${e.id}`),console.warn(`[buildAppFromDefinition] \u26A0\uFE0F Skipping node: ${e.id}`);continue}console.log(`[buildAppFromDefinition] Adding node: ${e.id} (type: ${e.type})`),I.addNode(e.id,n)}console.log(`[buildAppFromDefinition] Adding ${o.edges.length} edges to graph`);const U=new Map;for(const e of o.edges){const n=e.from||e.source,s=e.to||e.target;if(!n||!s){console.warn("[buildAppFromDefinition] \u26A0\uFE0F Invalid edge (missing from/to):",e);continue}U.has(n)||U.set(n,[]),U.get(n).push({...e,from:n,to:s})}for(const[e,n]of U.entries()){const s=n.filter(m=>m.condition),a=n.filter(m=>!m.condition);if(s.length>0){const m=l=>{const C=l.messages[l.messages.length-1],p=C?.tool_calls&&C.tool_calls.length>0;console.log(`[conditional_edge] from=${e}, hasToolCalls=${p}`);for(const f of s){if(f.condition==="has_tool_calls"&&p)return console.log(`[conditional_edge] Route: ${e} \u2192 ${f.to} (has_tool_calls)`),f.to;if(f.condition==="no_tool_calls"&&!p)return console.log(`[conditional_edge] Route: ${e} \u2192 ${f.to} (no_tool_calls)`),f.to}return console.warn(`[conditional_edge] No condition matched for ${e}, defaulting to __end__`),"__end__"},d={};for(const l of s)l.condition==="has_tool_calls"?d.has_tools=l.to:l.condition==="no_tool_calls"&&(d.no_tools=l.to);console.log(`[buildAppFromDefinition] Adding conditional edge: ${e} \u2192 [${s.map(l=>`${l.to}(${l.condition})`).join(", ")}]`),I.addConditionalEdges(e,m)}else if(a.length>0)for(const m of a)console.log(`[buildAppFromDefinition] Adding edge: ${m.from} \u2192 ${m.to}`),I.addEdge(m.from,m.to)}if(o.edges.some(e=>(e.from||e.source)==="__start__"))console.log("[buildAppFromDefinition] Entry point already defined in edges");else{const e=o.nodes[0]?.id||"checkReset";console.log(`[buildAppFromDefinition] Adding missing entry point: __start__ \u2192 ${e}`),I.addEdge("__start__",e)}const oe={checkpointer:o.config.checkpointer?t.memory:void 0};o.config.recursionLimit&&(oe.recursionLimit=o.config.recursionLimit);const w=I.compile(oe);return console.log(`[buildAppFromDefinition] \u2705 Graph compiled successfully: ${o.name}`),{invoke:w.invoke.bind(w),stream:w.stream.bind(w),streamEvents:w.streamEvents.bind(w),cleanup:()=>{for(const e of i)Ue(e);console.log(`[MCP Pool] Released ${i.length} client(s)`)},_compiled:w}}export{Ao as buildAppFromDefinition};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomBytes as
|
|
1
|
+
import{randomBytes as f,randomUUID as b}from"crypto";import{readFileSync as I}from"fs";const k="127.0.0.1";function T(e){return e.esInsecure===!0?{rejectUnauthorized:!1}:e.esCaPath?{rejectUnauthorized:!0,ca:I(e.esCaPath,"utf8")}:{rejectUnauthorized:!0}}async function x(e,a,n,c,l){if(!c)return{activated:0,error:"Bundle has a gateway grant but no gateway secret was provided"};let d;try{const t=`${n.sourceAppUrl.replace(/\/+$/,"")}/api/mcp-gateway/redeem`,o=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({grantId:n.grantId,secret:c,importerOwner:l})}),r=await o.json();if(!o.ok||!r.success||!r.gatewayToken)return{activated:0,error:r.error||`Gateway redeem failed with HTTP ${o.status}`};d=r.gatewayToken}catch(t){return{activated:0,error:`Could not reach ${n.sourceAppUrl} to redeem the gateway grant: ${t.message}`}}const m=new Date().toISOString();let g=0;const s="personal:";let u=null;for(const t of n.platformIds){const o=`${n.gatewayHost.replace(/\/+$/,"")}/mcp/gateway:${n.grantId}:${t}`;if(t.startsWith(s)){u=b(),await e.index({index:".stkxp_platforms",id:u,document:{id:u,name:"My MCP Tools (gateway)",type:"MCPServer",managedType:"managed",owner:l,enabled:!0,created:m,updated:m,config:{serverType:"gateway",url:o,authKey:d,protocol:"http"}},refresh:!0}),g++;continue}try{await e.update({index:".stkxp_platforms",id:t,body:{doc:{managedType:"managed",config:{serverType:"gateway",url:o,authKey:d}}},refresh:!0}),g++}catch(r){console.error(`[cli-deploy] Failed to activate gateway platform ${t}: ${r.message}`)}}if(u){const t=`__personal_tools__:${l}`;for(const o of a.docs){if(o.key!=="assistant")continue;const r=o.source?.mcp_servers_policy?.servers;if(!Array.isArray(r)||!r.some(i=>i?.id===t))continue;const y=r.map(i=>i?.id===t?{...i,id:u}:i);await e.update({index:o.index,id:o.esId,body:{doc:{mcp_servers_policy:{...o.source.mcp_servers_policy,servers:y}}},refresh:!0})}}return{activated:g}}async function C(e){process.env.ELASTICSEARCH_HOST=e.esUrl,process.env.ELASTICSEARCH_USER=e.esUser,process.env.ELASTICSEARCH_PASSWORD=e.esPassword,e.esCaPath&&(process.env.ELASTICSEARCH_CA_PATH=e.esCaPath),process.env.ELASTICSEARCH_TLS_REJECT_UNAUTHORIZED=e.esInsecure===!0?"false":"true";const{parseAndDecryptTeamBundle:a}=await import("../services/clone/team-bundle-encrypted"),{closure:n,rootName:c,sourceOwner:l,mcpGateway:d,gatewaySecret:m,indexMappings:g}=a(e.bundle,e.secret),s=n.docs.find(p=>p.key==="team");if(!s)throw new Error("Decrypted bundle has no team document \u2014 cannot determine which team to deploy");const{Client:u}=await import("@elastic/elasticsearch"),t=new u({node:e.esUrl,auth:{username:e.esUser,password:e.esPassword},tls:T(e)}),{bootstrapClosureToEs:o}=await import("../services/clone/team-bundle-bootstrap"),r=await o(t,n,g);let y=0,i;if(d){const p=await x(t,n,d,m,l);y=p.activated,i=p.error,i&&console.error(`[cli-deploy] Gateway grant redemption incomplete: ${i}`)}const w=await t.get({index:s.index,id:s.esId}).then(p=>p?._source??null).catch(()=>null);if(w?.webhookEnabled&&w?.webhookToken)return{rootName:c,teamId:s.esId,teamIndex:s.index,webhookToken:null,alreadyBootstrapped:!0,indicesCreated:r.indicesCreated,docsIndexed:r.docsIndexed,gatewayPlatformsActivated:y,gatewayError:i};const h=f(32).toString("hex");return await t.update({index:s.index,id:s.esId,body:{doc:{webhookToken:h,webhookEnabled:!0,webhookCreatedAt:new Date().toISOString()}},refresh:"wait_for"}),{rootName:c,teamId:s.esId,teamIndex:s.index,webhookToken:h,alreadyBootstrapped:!1,indicesCreated:r.indicesCreated,docsIndexed:r.docsIndexed,gatewayPlatformsActivated:y,gatewayError:i}}async function A(e){const a=await C(e),{buildTeamRunnerApp:n}=await import("./team-runner"),c=n(),l=e.webhookHost??k;return await new Promise(d=>{c.listen(e.webhookPort,l,()=>d())}),{rootName:a.rootName,teamId:a.teamId,webhookToken:a.webhookToken??"(unchanged \u2014 target was already bootstrapped, existing token not re-shown)",webhookUrl:`http://localhost:${e.webhookPort}/api/webhooks/team/${a.teamId}`,indicesCreated:a.indicesCreated,docsIndexed:a.docsIndexed,gatewayPlatformsActivated:a.gatewayPlatformsActivated,gatewayError:a.gatewayError}}export{k as DEFAULT_WEBHOOK_HOST,C as bootstrapTeam,T as buildTargetEsTls,A as runDeploy};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Client as l}from"@elastic/elasticsearch";import{assertOwner as p,getAuthUsername as c}from"../core/utils/ownership";import{listGatewayGrantsForTeam as m,revokeGatewayGrant as d,listAllowlistedTools as g,callAllowlistedTool as f}from"../services/mcp-gateway/gateway-grant-service";const
|
|
1
|
+
import{Client as l}from"@elastic/elasticsearch";import{assertOwner as p,getAuthUsername as c}from"../core/utils/ownership";import{listGatewayGrantsForTeam as m,revokeGatewayGrant as d,listAllowlistedTools as g,callAllowlistedTool as y,redeemGatewayGrant as f}from"../services/mcp-gateway/gateway-grant-service";const w=process.env.ELASTICSEARCH_HOST||"https://51.159.174.230:9205",h=process.env.ELASTICSEARCH_USER||"elastic",R=process.env.ELASTICSEARCH_PASSWORD||"diagnostics",I=".stkxp_teams",T=new l({node:w,auth:{username:h,password:R},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3});function u(t){const e=t.headers.authorization||"";return e.startsWith("ApiKey ")?e.slice(7).trim():""}async function k(t,e){const s=c(t);if(!s){e.status(401).json({success:!1,error:"Unauthorized"});return}const{teamId:n}=t.params,o=await p(t,e,T,I,n);if(!o)return;const a=o.source.owner||s,i=await m(n,a);e.json({success:!0,grants:i})}async function S(t,e){const s=c(t);if(!s){e.status(401).json({success:!1,error:"Unauthorized"});return}const{grantId:n}=t.params;if(!await d(n,s)){e.status(404).json({success:!1,error:"Gateway grant not found or not owned by you"});return}e.json({success:!0})}async function j(t,e){const{grantId:s,secret:n,importerOwner:o}=t.body||{};if(!s||!n){e.status(400).json({success:!1,error:"Missing grantId or secret"});return}const a=await f(s,n,o||"cli-deploy");if("error"in a){e.status(400).json({success:!1,error:a.error});return}e.json({success:!0,gatewayToken:a.gatewayToken})}async function A(t,e){const{grantId:s,platformId:n}=t.params,o=u(t);if(!o){e.status(401).json({success:!1,error:"Missing gateway token"});return}const a=await g(s,n,o);if("error"in a){e.status(403).json({success:!1,error:a.error});return}e.json({success:!0,tools:a.tools})}async function C(t,e){const{grantId:s,platformId:n}=t.params,o=u(t);if(!o){e.status(401).json({success:!1,error:"Missing gateway token"});return}const{name:a,arguments:i}=t.body||{};if(!a){e.status(400).json({success:!1,error:"Missing tool name"});return}const r=await y(s,n,o,a,i||{});if(r&&typeof r=="object"&&"error"in r){e.status(403).json({success:!1,error:r.error});return}e.json({success:!0,result:r})}const x=[{method:"get",path:"/api/teams/:teamId/gateway-grants",handler:k,openapi:{summary:"List MCP gateway grants issued from exports of this team",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/mcp-gateway-grants/:grantId/revoke",handler:S,openapi:{summary:"Revoke an MCP gateway grant",tags:["teams"],audience:"internal"}}],b=[{method:"post",path:"/api/mcp-gateway/redeem",handler:j,openapi:{summary:"Redeem an MCP gateway grant with its secret (used by stkxp deploy/bootstrap, no session)",tags:["teams"],audience:"hidden"}},{method:"get",path:"/api/mcp-gateway/internal/:grantId/:platformId/tools",handler:A,openapi:{summary:"Internal: list allowlisted tools for a gateway grant (called by stkxp-mcp-server)",tags:["teams"],audience:"hidden"}},{method:"post",path:"/api/mcp-gateway/internal/:grantId/:platformId/call-tool",handler:C,openapi:{summary:"Internal: relay a tool call through a gateway grant (called by stkxp-mcp-server)",tags:["teams"],audience:"hidden"}}];export{b as publicRoutes,x as routes};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Client as q}from"@elastic/elasticsearch";import{z as m}from"zod";import{randomBytes as O,randomUUID as K}from"crypto";import{assertOwner as C,getAuthUsername as x,isSuperuser as V}from"../core/utils/ownership";import{makeOwnerScopedClient as Q}from"../core/utils/owner-scope";import{resolveClosure as $,createEsStore as j,PERSONAL_TOOLS_REACHED_VIA as U}from"../services/clone/closure-resolver";import{executeClone as X,createEsCloneWriter as Z}from"../services/clone/clone-executor";import{serializeTeamBundle as J,parseTeamBundle as Y,TeamBundleParseError as ee}from"../services/clone/team-bundle";import{serializeTeamBundleEncrypted as se}from"../services/clone/team-bundle-encrypted";import{buildFullStructureGraph as te}from"../services/graph-canvas/full-structure-builder";import{MultiKibanaService as ae}from"../core/services/multi-kibana-service";import{projectAssistantTokens as ne}from"../core/services/token-projection-service";import{buildMcpAppTesterZip as oe,slugifyTeamName as re}from"../services/mcp-app-tester-package";import{createGatewayGrant as D,redeemGatewayGrant as ie,GATEWAY_UNREACHABLE_PREFIX as N}from"../services/mcp-gateway/gateway-grant-service";import{isGatewayEligiblePlatformType as z}from"../core/services/gateway-platform-types";import{syncRemoteToolsForPlatform as ce}from"../core/services/mcp-sync-service";import{snapshotBeforeUpdate as de}from"../core/services/version-snapshot-service";import{findTeamOwningCommand as le,getChannelCommandOwnership as me}from"../core/services/teams-service";const ue=new ae,F=process.env.ELASTICSEARCH_HOST||"https://51.159.174.230:9205",G=process.env.ELASTICSEARCH_USER||"elastic",B=process.env.ELASTICSEARCH_PASSWORD||"",f=".stkxp_teams",v=".stkxp_assistants",pe=".stkxp_chats",u=Q({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),S=new q({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),L=m.object({enabled:m.string().optional().transform(s=>{if(s!==void 0)return s==="true"}),page:m.string().optional().transform(s=>s?parseInt(s,10):1),pageSize:m.string().optional().transform(s=>s?parseInt(s,10):25),search:m.string().optional().describe("Hybrid lexical + semantic search across name and description"),packageName:m.string().optional(),owners:m.string().optional(),currentUser:m.string().optional(),includeAllSystem:m.string().optional().transform(s=>s==="true"),includeOwned:m.union([m.boolean(),m.string()]).optional().transform(s=>s===!0||s==="true"),assistantId:m.string().optional()});async function ge(){try{await u.indices.create({index:f,body:{mappings:{properties:{name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"},search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},owner:{type:"keyword"},enabled:{type:"boolean"},graphId:{type:"keyword"},packageName:{type:"keyword"},assistantIds:{type:"keyword"},assistants:{properties:{id:{type:"keyword"},enabled:{type:"boolean"},condition:{type:"text",index:!1},runAlways:{type:"boolean"}}},sharedContext:{properties:{context:{type:"text"},priority:{type:"keyword"},policy:{properties:{shareToAssistants:{type:"boolean"},shareToDecider:{type:"boolean"},useAssistantProfiles:{type:"boolean"},applyOnDelegation:{type:"boolean"}}}}},webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0},created_at:{type:"date"},updated_at:{type:"date"}}}}}),console.log("[Teams API] Created index:",f)}catch(s){s.meta?.statusCode!==400&&console.error("[Teams API] Error creating index:",s)}try{await u.indices.putMapping({index:f,body:{properties:{webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for webhook fields:",s?.message)}try{await u.indices.putMapping({index:f,body:{properties:{search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for search_semantic field:",s?.message)}}import{getTeamById as ds,getTeamWebhookConfig as ls,getTeamMcpConfig as ms,getTeamSharedContext as us}from"../core/services/teams-service";async function E(s){if(!s||s.length===0)return[];const t=new Map(s.map(n=>[n.id,n])),e=s.map(n=>n.id);try{return(await u.mget({index:v,body:{ids:e}})).docs.filter(a=>a.found).map(a=>{const o=t.get(a._id);return{id:a._id,name:a._source?.name||"Unknown",topic:a._source?.topic,enabled:o?.enabled??!0,condition:o?.condition??null,runAlways:o?.runAlways??!1}})}catch(n){return console.error("[Teams API] Error joining assistants:",n),e.map(a=>({id:a,name:a,enabled:t.get(a)?.enabled??!0}))}}function fe(s){return s==="clusters"||s==="stack_expert"?["clusters","stack_expert"]:[s]}async function ye(s,t){const e=fe(s);console.log(`[Teams API] Package filter: ${s} \u2192 [${e.join(", ")}]${t?` owner: ${t}`:""}`);try{const a=(await u.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":e}},size:1e4,_source:["name"]}})).hits.hits.map(p=>p._source?.name).filter(Boolean),o=[...new Set(a)];if(o.includes("stack_expert")&&o.push("clusters"),console.log(`[Teams API] Found ${a.length} package servers for packageName: ${e.join(",")} \u2192 [${o.join(", ")}]`),a.length===0)return new Set;const i={bool:{should:o.map(p=>({term:{"mcp_servers_policy.servers.name.keyword":p}})),minimum_should_match:1}},d=t?{bool:{must:[i,{bool:{should:[{term:{owner:"stkxp"}},{term:{owner:t}}],minimum_should_match:1}}]}}:{bool:{must:[i]}},c=await u.search({index:v,body:{query:d,size:1e4,_source:[]}}),r=new Set(c.hits.hits.map(p=>p._id));return console.log(`[Teams API] Found ${r.size} visible assistants for packageName: ${e.join(",")}`),r}catch(n){return console.error("[Teams API] Error fetching visible assistants for packageName filter:",n),null}}function H(s){return Array.isArray(s.assistants)&&s.assistants.length>0&&s.assistants[0]?.id!==void 0?s.assistants.map(t=>({id:t.id,enabled:t.enabled!==!1,condition:t.condition??null,runAlways:t.runAlways??!1})):(s.assistantIds||[]).map(t=>({id:t,enabled:!0,condition:null,runAlways:!1}))}async function he(s){try{const[t,e]=await Promise.all([u.search({index:v,body:{query:{term:{owner:s}},size:1e4,_source:[]}}),ue.getAllPackagesFromAllPlatforms(s,!1).catch(()=>({packages:[],errors:[]}))]),n=t.hits.hits.map(d=>d._id),a=[...new Set(e.packages.filter(d=>d.platforms?.some(c=>c.status==="enrolled")).map(d=>d.name).filter(Boolean))];a.includes("stack_expert")&&a.push("clusters"),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages from platforms`,a);let o=[];a.length>0&&(o=(await u.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":a}},size:1e4,_source:["name"]}})).hits.hits.map(c=>c._source?.name).filter(Boolean));let i=[];return o.length>0&&(i=(await u.search({index:v,body:{query:{bool:{should:o.map(c=>({term:{"mcp_servers_policy.servers.name.keyword":c}})),minimum_should_match:1}},size:1e4,_source:[]}})).hits.hits.map(c=>c._id)),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages, ${o.length} MCP servers, ${n.length} owned assistants, ${i.length} MCP-linked assistants`),{userMcpPackageNames:a,userOwnedAssistantIds:n,mcpLinkedAssistantIds:i}}catch(t){return console.error("[Teams API] Error fetching user visibility data:",t),{userMcpPackageNames:[],userOwnedAssistantIds:[],mcpLinkedAssistantIds:[]}}}async function we(s,t){try{const e=L.parse(s.query);console.log(`[Teams API] getTeams query: ${JSON.stringify(e)}`);const n=[];if(e.enabled!==void 0&&n.push({term:{enabled:e.enabled}}),e.search){const w=e.search.toLowerCase().trim(),T=w.split("||").map(_=>_.trim()).filter(Boolean),A=T[0]||w,P=[];let R=0,M=5;P.push({multi_match:{query:`*${A}*`,fields:["name^5","description^2"],type:"best_fields",operator:"or",minimum_should_match:"60%",fuzziness:"AUTO",boost:3}}),P.push({match:{description:{query:e.search}}}),P.push({semantic:{field:"search_semantic",query:e.search,boost:2}});for(const _ of T)P.push({match_phrase:{name:{query:_,slop:R++,boost:Math.max(M--,1)}}});n.push({bool:{should:P,minimum_should_match:1}})}if(e.currentUser&&e.includeAllSystem)n.push({bool:{should:[{term:{owner:e.currentUser}},{term:{owner:"stkxp"}}],minimum_should_match:1}});else if(e.currentUser&&e.includeOwned)n.push({term:{owner:e.currentUser}});else if(e.currentUser){const{mcpLinkedAssistantIds:w}=await he(e.currentUser);n.push({term:{owner:e.currentUser}}),w.length>0?n.push({terms:{assistantIds:w}}):n.push({term:{assistantIds:"__none__"}})}else if(e.owners){const w=e.owners.split(",").map(T=>T.trim()).filter(Boolean);w.length>0&&n.push({terms:{owner:w}})}let a=null;if(e.packageName&&(a=await ye(e.packageName,e.currentUser),a!==null)){if(a.size===0)return t.json({success:!0,count:0,total:0,page:e.page||1,pageSize:e.pageSize||25,totalPages:0,teams:[]});n.push({terms:{assistantIds:Array.from(a)}})}e.assistantId&&n.push({term:{assistantIds:e.assistantId}});const o=e.page||1,i=e.pageSize||25,d=(o-1)*i,c={name:"name",updated_at:"updated_at"},r=String(s.query.sortField||"name"),p=c[r]||"name",b=s.query.sortOrder==="desc"?"desc":"asc",l=p==="updated_at"?[{updated_at:{order:b,unmapped_type:"date"}}]:[{[p]:{order:b,unmapped_type:"keyword"}}],y={index:f,body:{track_scores:!0,query:{bool:{must:n.length>0?n:[{match_all:{}}]}},sort:[{_score:{order:"desc"}},...l],from:d,size:i,_source:{excludes:["webhookToken"]}}};console.log("[Teams API] getTeams query:",JSON.stringify(y.body.query));const g=await u.search(y),h=typeof g.hits.total=="number"?g.hits.total:g.hits.total?.value||0,I=await Promise.all(g.hits.hits.map(async w=>{const T=w._source,A=Array.isArray(T.assistants)&&T.assistants.length>0?T.assistants:(T.assistantIds||[]).map(_=>({id:_,enabled:!0})),P=a?A.filter(_=>a.has(_.id)):A,R=e.packageName?P.filter(_=>_.enabled!==!1):P,M=await E(R);return{id:w._id,...T,assistants:M}})),k=e.packageName?I.filter(w=>w.assistants.length>0):I;t.json({success:!0,count:k.length,total:h,page:o,pageSize:i,totalPages:Math.ceil(h/i),teams:k})}catch(e){if(console.error("[Teams API] Error fetching teams:",e),e instanceof m.ZodError)return t.status(400).json({success:!1,error:"Invalid query parameters",details:e.errors});if(e.meta?.statusCode===404)return t.json({success:!0,count:0,total:0,page:1,pageSize:25,totalPages:0,teams:[]});t.status(500).json({success:!1,error:"Failed to fetch teams",message:e.message})}}async function be(s,t){try{const{id:e}=s.params,n=await u.get({index:f,id:e,_source_excludes:["webhookToken"]});if(!n.found)return t.status(404).json({success:!1,error:"Team not found"});const a=n._source,o=Array.isArray(a.assistants)&&a.assistants.length>0?a.assistants:(a.assistantIds||[]).map(d=>({id:d,enabled:!0})),i=await E(o);t.json({success:!0,team:{id:n._id,...a,assistants:i}})}catch(e){if(console.error("[Teams API] Error fetching team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to fetch team",message:e.message})}}async function ke(s,t){try{await ge();const e=s.body,n=new Date().toISOString(),a=H(e),o=a.map(b=>b.id),d=s.auth?.username||e.owner||"system",c={name:e.name,description:e.description||"",assistantIds:o,assistants:a,owner:d,enabled:e.enabled!==void 0?e.enabled:!0,created_at:n,updated_at:n};e.sharedContext?.context?.trim()&&(c.sharedContext={context:e.sharedContext.context.trim(),priority:e.sharedContext.priority==="after"?"after":"before",...e.sharedContext.policy?{policy:e.sharedContext.policy}:{}}),e.graphId?.trim()&&(c.graphId=e.graphId.trim()),console.log("[Teams API] Creating team:",c.name);const r=await u.index({index:f,body:c,refresh:"wait_for"}),p=await E(a);t.status(201).json({success:!0,team:{id:r._id,...c,assistants:p}})}catch(e){console.error("[Teams API] Error creating team:",e),t.status(500).json({success:!1,error:"Failed to create team",message:e.message})}}async function Ie(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;await de("team",e,n.source,"manual_edit",x(s)||"system");const a=s.body,o=H(a),i=o.map(p=>p.id),d={name:a.name,description:a.description||"",assistantIds:i,assistants:o,owner:a.owner||"system",enabled:a.enabled!==void 0?a.enabled:!0,updated_at:new Date().toISOString()};a.sharedContext?.context?.trim()?d.sharedContext={context:a.sharedContext.context.trim(),priority:a.sharedContext.priority==="after"?"after":"before",...a.sharedContext.policy?{policy:a.sharedContext.policy}:{}}:d.sharedContext=null,d.graphId=a.graphId?.trim()||null,console.log("[Teams API] Updating team:",e);const c=await u.update({index:f,id:e,body:{doc:d},refresh:"wait_for"}),r=await E(o);t.json({success:!0,team:{id:c._id,...d,assistants:r}})}catch(e){if(console.error("[Teams API] Error updating team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to update team",message:e.message})}}async function Te(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;console.log("[Teams API] Deleting team:",e),await u.delete({index:f,id:e,refresh:"wait_for"}),t.json({success:!0,message:"Team deleted successfully"})}catch(e){if(console.error("[Teams API] Error deleting team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to delete team",message:e.message})}}async function Ae(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=s.body?.enabled!==!1,o=O(32).toString("hex"),i=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{webhookToken:o,webhookEnabled:a,webhookCreatedAt:i,updated_at:i}},refresh:"wait_for"}),console.log(`[Teams API] Generated webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookToken:o,webhookEnabled:a,webhookCreatedAt:i,webhookUrl:`/api/webhooks/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate webhook token",message:e.message})}}async function Ce(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{webhookToken:null,webhookEnabled:!1,webhookCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke webhook token",message:e.message})}}async function xe(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source;if(!a.mcpEnabled)return t.status(400).json({success:!1,error:"MCP server is not enabled for this team \u2014 generate/enable it in the MCP App tab first."});const o=a.name||"team",i=`${s.protocol}://${s.get("host")}/api/mcp/team/${e}`,d=await oe({teamName:o,teamId:e,mcpUrl:i,allowUnsigned:a.mcpAllowUnsigned===!0}),c=`mcp-app-tester-${re(o)}.zip`;t.set({"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${c}"`}),t.send(d)}catch(e){console.error("[Teams API] Error generating MCP App tester package:",e),t.status(500).json({success:!1,error:"Failed to generate test client package",message:e.message})}}async function _e(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=s.body?.enabled!==!1,o=s.body?.exposeAssistants!==!1,i=s.body?.allowUnsigned===!0,d=typeof s.body?.signingSecret=="string"?s.body.signingSecret.trim():void 0;if(d!==void 0&&d.length<8)return t.status(400).json({success:!1,error:"signingSecret must be at least 8 characters (use your Slack app Signing Secret to integrate with Slack)"});const c=d||O(32).toString("hex"),r=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:i,mcpCreatedAt:r,updated_at:r}},refresh:"wait_for"}),console.log(`[Teams API] Generated MCP signing secret for team: ${e}`),t.json({success:!0,teamId:e,mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:i,mcpCreatedAt:r,mcpUrl:`/api/mcp/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate MCP config",message:e.message})}}async function Se(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{mcpSigningSecret:null,mcpEnabled:!1,mcpAllowUnsigned:!1,mcpCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked MCP config for team: ${e}`),t.json({success:!0,teamId:e,mcpEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke MCP config",message:e.message})}}async function Pe(s,t){try{const{id:e,platformId:n}=s.params,a=await C(s,t,u,f,e);if(!a)return;const o=Array.isArray(s.body?.commandIds)?s.body.commandIds:[],i=typeof s.body?.namespace=="string"?s.body.namespace:void 0,d=s.body?.blockKitMode===!0,c=a.source.owner;for(const l of o){const y=await le(c,n,l,e);if(y)return t.status(409).json({success:!1,message:`Command is already assigned to team "${y.teamName}"`})}const r=new Date().toISOString(),b={...a.source.channelCommands??{},[n]:{commandIds:o,namespace:i,blockKitMode:d}};await u.update({index:f,id:e,body:{doc:{channelCommands:b,updated_at:r}},refresh:"wait_for"}),console.log(`[Teams API] Updated channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n,commandIds:o,namespace:i,blockKitMode:d})}catch(e){if(console.error("[Teams API] Error setting channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to set channel commands",message:e.message})}}async function ve(s,t){try{const{id:e,platformId:n}=s.params,a=await C(s,t,u,f,e);if(!a)return;const o=new Date().toISOString(),i={...a.source.channelCommands??{}};delete i[n],await u.update({index:f,id:e,body:{doc:{channelCommands:i,updated_at:o}},refresh:"wait_for"}),console.log(`[Teams API] Cleared channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n})}catch(e){if(console.error("[Teams API] Error clearing channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to clear channel commands",message:e.message})}}async function je(s,t){try{const e=x(s);if(!e)return t.status(401).json({success:!1,message:"Unauthorized"});const n=s.query.platformId;if(!n)return t.status(400).json({success:!1,message:"platformId is required"});const a=await me(e,n);t.json({success:!0,body:{ownership:a}})}catch(e){console.error("[Teams API] Error fetching channel command ownership:",e),t.status(500).json({success:!1,message:e.message||"Failed to fetch channel command ownership"})}}async function Ee(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source.owner||x(s)||"system",o=j(S),i=await $([{index:f,esId:e}],a,o),d=te(i);t.json({success:!0,graph:d})}catch(e){if(console.error("[Teams API] Error building full-structure graph:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to build full-structure graph",message:e.message})}}async function Re(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source.owner||x(s)||"system",o=n.source.name||"(unnamed team)",i=j(S),d=await $([{index:f,esId:e}],a,i),c=J(d,{exportedAt:new Date().toISOString(),rootName:o}),r=d.docs.filter(l=>l.key==="platform"&&z(l.source?.type)&&l.source?.owner===a).map(l=>l.esId),p=d.docs.some(l=>l.key==="tool"&&l.reachedVia?.includes(U)&&l.source?.owner===a);let b;if(r.length>0||p){const l=await D(d,a,e);if(!l)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const g of c.closure.docs)g.key==="platform"&&r.includes(g.esId)&&(g.source.config={...g.source.config,serverType:"gateway",url:""});const y=c.closure.docs.filter(g=>g.key==="tool"&&g.source?.type==="mcp_remote"&&(r.includes(g.source?.platformId)||r.includes(g.source?.mcpServerId))).length;y>0&&(c.closure.docs=c.closure.docs.filter(g=>!(g.key==="tool"&&g.source?.type==="mcp_remote"&&(r.includes(g.source?.platformId)||r.includes(g.source?.mcpServerId)))),c.summary.totalDocs-=y,c.summary.byEntity.tool=(c.summary.byEntity.tool??0)-y),c.mcpGateway={grantId:l.grantId,platformIds:l.platformIds},b=l.secret}console.log(`[Teams API] ${x(s)??"?"} exported team "${o}" (${e}): ${c.summary.totalDocs} docs, ${d.unresolved.length} unresolved ref(s)`+(b?`, gateway grant for ${r.length} platform(s)`:"")),t.json({success:!0,bundle:c,gatewaySecret:b,unresolved:d.unresolved})}catch(e){if(console.error("[Teams API] Error exporting team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if((e?.message??"").startsWith(N))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team",message:e.message})}}const W=m.object({secret:m.string().min(8)});async function Me(s,t){const e=W.safeParse(s.body);if(!e.success)return t.status(400).json({success:!1,error:'Body must include a "secret" of at least 8 characters'});const{secret:n}=e.data;try{const{id:a}=s.params,o=await C(s,t,u,f,a);if(!o)return;const i=o.source.owner||x(s)||"system",d=o.source.name||"(unnamed team)",c=j(S),r=await $([{index:f,esId:a}],i,c),p=[...new Set(r.docs.map(k=>k.index))],b={};if(p.length>0){const k=await S.indices.getMapping({index:p});for(const[w,T]of Object.entries(k))b[w]=T.mappings}const l=r.docs.filter(k=>k.key==="platform"&&z(k.source?.type)&&k.source?.owner===i).map(k=>k.esId),y=r.docs.some(k=>k.key==="tool"&&k.reachedVia?.includes(U)&&k.source?.owner===i);let g,h;if(l.length>0||y){const k=await D(r,i,a);if(!k)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const w of r.docs)w.key==="platform"&&l.includes(w.esId)&&(w.source.config={...w.source.config,serverType:"gateway",url:""});r.docs=r.docs.filter(w=>!(w.key==="tool"&&w.source?.type==="mcp_remote"&&(l.includes(w.source?.platformId)||l.includes(w.source?.mcpServerId)))),h={grantId:k.grantId,platformIds:k.platformIds},g=k.secret}const I=se(r,{exportedAt:new Date().toISOString(),rootName:d,passphrase:n,gatewaySecret:g,indexMappings:b});I.mcpGateway=h,console.log(`[Teams API] ${x(s)??"?"} exported (encrypted) team "${d}" (${a}): ${I.summary.totalDocs} docs, ${r.unresolved.length} unresolved ref(s)`),t.json({success:!0,bundle:I,unresolved:r.unresolved})}catch(a){if(console.error("[Teams API] Error exporting team (encrypted):",a),/Required dependencies/.test(a?.message??""))return t.status(422).json({success:!1,error:a.message});if((a?.message??"").startsWith(N))return t.status(422).json({success:!1,error:a.message});if(a.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team (encrypted)",message:a.message})}}async function $e(s,t){try{const e=x(s);if(!e)return t.status(401).json({success:!1,error:"Unauthorized"});const n=(s.body&&s.body.bundle)??s.body,a=s.body&&typeof s.body.gatewaySecret=="string"?s.body.gatewaySecret:void 0;let o;try{o=Y(n)}catch(p){const b=p instanceof ee?p.message:"Invalid bundle";return t.status(400).json({success:!1,error:b})}let i;if(o.mcpGateway){if(!a)return t.status(400).json({success:!1,error:"This bundle requires a gateway key to activate its MCP server platform(s). Paste the key you received from the exporting owner."});const p=await ie(o.mcpGateway.grantId,a,e);if("error"in p)return t.status(400).json({success:!1,error:p.error});i=p.gatewayToken}const d=j(S),c=Z(S),r=await X(o.closure,e,d,c,{blankSecrets:!0});if(o.mcpGateway&&i){const p=process.env.MCP_GATEWAY_HOST||"https://mcp.erretegia.com",b=new Date().toISOString();for(const y of r.created){if(y.key!=="platform"||!o.mcpGateway.platformIds.includes(y.oldEsId))continue;const g=`${p}/mcp/gateway:${o.mcpGateway.grantId}:${y.oldEsId}`;await S.update({index:y.index,id:y.newEsId,body:{doc:{managedType:"managed",config:{serverType:"gateway",url:g,authKey:i}}},refresh:!0});try{await ce(y.newEsId,{id:y.newEsId,name:y.name||"MCP Server",type:"MCPServer",managedType:"self-hosted",owner:e,created:b,updated:b,enabled:!0,config:{serverType:"gateway",url:g,authKey:i}},e)}catch(h){console.warn(`[Teams API] Post-import gateway tool sync failed for platform ${y.newEsId}: ${h.message}`)}}const l=`personal:${o.sourceOwner}`;if(o.mcpGateway.platformIds.includes(l)){const y=`${p}/mcp/gateway:${o.mcpGateway.grantId}:${l}`,g=K();await S.index({index:".stkxp_platforms",id:g,document:{id:g,name:"My MCP Tools (gateway)",type:"MCPServer",managedType:"managed",owner:e,enabled:!0,created:b,updated:b,config:{serverType:"gateway",url:y,authKey:i,protocol:"http"}},refresh:!0});const h=`__personal_tools__:${o.sourceOwner}`;for(const I of r.created){if(I.key!=="assistant")continue;const k=o.closure.docs.find(A=>A.key==="assistant"&&A.esId===I.oldEsId),w=k?.source?.mcp_servers_policy?.servers;if(!Array.isArray(w)||!w.some(A=>A?.id===h))continue;const T=w.map(A=>A?.id===h?{...A,id:g}:A);await S.update({index:I.index,id:I.newEsId,body:{doc:{mcp_servers_policy:{...k.source.mcp_servers_policy,servers:T}}},refresh:!0})}}}console.log(`[Teams API] ${e} imported team "${o.rootName}" (from owner "${o.sourceOwner}"): ${r.created.length} created, ${r.skipped.length} reused, ${r.referenced.length} referenced, ${r.droppedRefs.length} refs dropped`),t.status(201).json({success:!0,rootName:o.rootName,report:r,unresolved:o.closure.unresolved})}catch(e){if(console.error("[Teams API] Error importing team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});t.status(500).json({success:!1,error:"Failed to import team",message:e.message})}}async function Oe(s,t){try{const{id:e}=s.params;if(!x(s))return t.status(401).json({success:!1,error:"Unauthorized"});let a;try{a=(await u.get({index:f,id:e}))._source}catch(l){if(l?.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});throw l}const i=(a?.assistants||[]).filter(l=>l.enabled!==!1).map(l=>l.id);if(i.length===0)return t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:0,assistants:[]});const c=(await u.mget({index:v,body:{ids:i}})).docs.filter(l=>l.found),r=new Map,p=[];let b=0;for(const l of c){const y=await ne(u,l._id,l._source||{},r);b+=y.singlePassMax,p.push(y)}t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:b,assistants:p,note:"Single-pass upper bound. Tool loops can exceed it \u2014 multiply by an empirical factor (2-3\xD7) when sizing a budget."})}catch(e){console.error("[Teams API] Error computing token projection:",e),t.status(500).json({success:!1,error:"Failed to compute token projection",message:e.message})}}async function Ue(s,t){try{const{id:e}=s.params,n=x(s);if(!n)return t.status(401).json({success:!1,error:"Unauthorized"});const a=Math.min(parseInt(s.query.limit||"20",10)||20,100),o=[{term:{teamId:e}}];V(s)||o.push({term:{username:n}});const c=((await u.search({index:pe,body:{size:a,sort:[{createdAt:{order:"desc"}}],_source:["id","title","createdAt","metadata.tokenUsage","assistants"],query:{bool:{must:o}}}})).hits.hits||[]).map(h=>{const I=h._source||{},k=I.metadata?.tokenUsage||{};return{chatId:h._id,title:I.title,createdAt:I.createdAt,totalTokens:Number(k.totalTokens||0),inputTokens:Number(k.totalInputTokens||0),outputTokens:Number(k.totalOutputTokens||0),assistantCount:Array.isArray(I.assistants)?I.assistants.length:0}}),r=c.map(h=>h.totalTokens).filter(h=>h>0),p=r.reduce((h,I)=>h+I,0),b=[...r].sort((h,I)=>h-I),l=b.length?b[Math.min(b.length-1,Math.floor(b.length*.95))]:0,y=c.map(h=>h.inputTokens),g=c.map(h=>h.outputTokens);t.json({success:!0,runCount:r.length,avgTotalTokens:r.length?Math.round(p/r.length):0,maxTotalTokens:r.length?Math.max(...r):0,p95TotalTokens:l,avgInputTokens:y.length?Math.round(y.reduce((h,I)=>h+I,0)/y.length):0,avgOutputTokens:g.length?Math.round(g.reduce((h,I)=>h+I,0)/g.length):0,recentRuns:c})}catch(e){console.error("[Teams API] Error fetching token stats:",e),t.status(500).json({success:!1,error:"Failed to fetch token stats",message:e.message})}}const De=m.object({name:m.string().describe("Team name"),description:m.string().optional().describe("Human description"),assistantIds:m.array(m.string()).optional().describe("Ids of assistants in the team"),assistants:m.array(m.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:m.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:m.boolean().optional().describe("Enabled flag (default true)"),sharedContext:m.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),Ne=m.object({name:m.string().optional().describe("Team name"),description:m.string().optional().describe("Human description"),assistantIds:m.array(m.string()).optional().describe("Ids of assistants in the team"),assistants:m.array(m.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:m.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:m.boolean().optional().describe("Enabled flag (default true)"),sharedContext:m.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),rs=[{method:"get",path:"/api/teams",handler:we,validate:{query:L},openapi:{summary:"List teams for the caller",description:"Returns all teams owned by the authenticated user. A team groups assistants and optionally references a coordination graph (`graphId`) for multi-assistant orchestration. `search` runs a hybrid lexical + semantic query (semantic_text field `search_semantic`).",tags:["teams"]}},{method:"post",path:"/api/teams/import",handler:$e,openapi:{summary:"Import a team from an export bundle",description:"Reconstructs a team and its full dependency closure under the authenticated caller from a bundle produced by `GET /api/teams/:id/export`. New ids are minted for owned docs (links rebuilt), shared/system docs are referenced in place, same-named docs the caller already owns are reused, and secrets stay blank (re-enter LLM keys / platform authKeys and regenerate the webhook token afterwards).",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/channel-command-ownership",handler:je,openapi:{summary:"Get command\u2192team ownership map for a platform",description:"Query param `platformId`. Returns `{ [commandId]: { teamId, teamName } }` for every command on that platform currently owned by one of the caller's teams \u2014 used by the Team Edit Channels tab to disable already-claimed commands.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id",handler:be,openapi:{summary:"Get a team by id",tags:["teams"]}},{method:"get",path:"/api/teams/:id/export",handler:Re,openapi:{summary:"Export a team as a portable bundle",description:"Resolves the team's full transitive dependency closure (assistants, graphs, sub-graphs, prompts, node templates, routing rules, models, providers, platforms, tools) and returns a secret-free JSON bundle for re-import via `POST /api/teams/import`. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/export-encrypted",handler:Me,validate:{body:W},openapi:{summary:"Export a team as a fully self-contained, passphrase-encrypted bundle",description:"Like GET /api/teams/:id/export, but instead of blanking secrets (LLM keys, platform creds, webhook token), encrypts them with the caller-supplied passphrase (AES-256-GCM per field). Intended for the `stkxp export` CLI, which produces a bundle deployable standalone via `stkxp deploy` \u2014 see docs/superpowers/specs/2026-09-15-team-standalone-export-design.md.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/full-structure",handler:Ee,openapi:{summary:"Get a team's full structure as a flattened canvas graph",description:"Resolves the team's full transitive dependency closure (assistants, runtime graphs, nested subgraphs, tools, platforms, models, providers, routing rules) and returns it flattened into nodes/edges/groups for the Team Canvas Overview mode. Read-only. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams",handler:ke,validate:{body:De},openapi:{summary:"Create a team",description:"Creates a team with the supplied `name`, optional `description`, `assistantIds[]`, and optional `graphId` pointing to a `.stkxp_graphs` coordination graph.",tags:["teams"]}},{method:"put",path:"/api/teams/:id",handler:Ie,validate:{body:Ne},openapi:{summary:"Update a team",tags:["teams"]}},{method:"delete",path:"/api/teams/:id",handler:Te,openapi:{summary:"Delete a team",tags:["teams"]}},{method:"get",path:"/api/teams/:id/token-projection",handler:Oe,openapi:{summary:"Theoretical single-pass max token usage for a team",description:"Resolves each enabled assistant graph (`assistant.graphId`), applies `assistant.llm_overrides[node.type]` on top of node-level routing rules, and sums `maxTokens` across nodes with a routing rule. Single-pass upper bound only \u2014 tool loops can exceed it.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/token-stats",handler:Ue,openapi:{summary:"Aggregate token usage for the caller's recent runs of a team",description:"Reads `.stkxp_chats` filtered by `teamId` (and `username` unless superuser) and returns avg / max / p95 total tokens plus the last N runs. Used by the Team flyout to surface real consumption before setting a future budget.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/webhook-token",handler:Ae,openapi:{summary:"Generate (or rotate) a webhook token for a team",description:"Creates a new inbound webhook token. Returns the clear-text token **once** \u2014 the UI must surface it immediately to the operator. Subsequent fetches of the team do not expose the token. Optional body `{ enabled?: boolean }` (defaults to true) controls whether the webhook is active.",tags:["teams","webhooks"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/webhook-token",handler:Ce,openapi:{summary:"Revoke the webhook token for a team",description:"Clears the webhook token and disables the webhook. POSTs to `/api/webhooks/team/:id` will be rejected with 401 until a new token is generated.",tags:["teams","webhooks"],audience:"internal"}},{method:"post",path:"/api/teams/:id/mcp-config",handler:_e,openapi:{summary:"Enable (or rotate) the MCP server for a team",description:'Owner-only. Generates a signing secret (returned once) and enables the per-team MCP endpoint at /api/mcp/team/:id. Body `{ enabled?: boolean, exposeAssistants?: boolean, signingSecret?: string }` \u2014 enabled/exposeAssistants default true; if `signingSecret` is provided (\u22658 chars) it is used verbatim (e.g. a Slack app Signing Secret to integrate with Slack), otherwise a random 32-byte secret is minted. `allowUnsigned` (default false) accepts unsigned requests \u2014 needed for Slack\'s MCP client, which sends no signature for auth_type "no_auth"; a signature, when present, is always verified.',tags:["teams","mcp"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/mcp-config",handler:Se,openapi:{summary:"Disable the MCP server for a team",description:"Owner-only. Clears the signing secret and disables the MCP endpoint. Calls to /api/mcp/team/:id return 404 until re-enabled.",tags:["teams","mcp"],audience:"internal"}},{method:"post",path:"/api/teams/:id/channel-commands/:platformId",handler:Pe,openapi:{summary:"Claim one or more commands on a platform for a team",description:"Body `{ commandIds: string[], namespace?: string, blockKitMode?: boolean }`. Rejects with 409 if any commandId is already owned by a different team. A command can be owned by at most one team at a time, scoped to (platformId, commandId).",tags:["teams","channels"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/channel-commands/:platformId",handler:ve,openapi:{summary:"Clear a team's channel-command config for a platform",description:"Removes this team's ownership of any commands claimed on the given platform, freeing them for other teams.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id/mcp-app-tester.zip",handler:xe,openapi:{summary:"Download a pre-configured MCP App test client for this team",description:"Owner-only. Generates a ZIP \u2014 a minimal fork of mcp-apps-tester wired to this team's ask_ tool, with Slack-style HMAC request signing baked in. Requires the team's MCP server to be enabled (mcpEnabled). Ships without the signing secret \u2014 the caller pastes their own copy into the generated .env.",tags:["teams","mcp"],audience:"internal"}}];export{ge as ensureTeamsIndex,ds as getTeamById,ms as getTeamMcpConfig,us as getTeamSharedContext,ls as getTeamWebhookConfig,rs as routes};
|
|
1
|
+
import{Client as K}from"@elastic/elasticsearch";import{z as m}from"zod";import{randomBytes as O,randomUUID as q}from"crypto";import{assertOwner as C,getAuthUsername as x,isSuperuser as V}from"../core/utils/ownership";import{makeOwnerScopedClient as X}from"../core/utils/owner-scope";import{resolveClosure as $,createEsStore as j,PERSONAL_TOOLS_REACHED_VIA as U}from"../services/clone/closure-resolver";import{executeClone as Q,createEsCloneWriter as Z}from"../services/clone/clone-executor";import{serializeTeamBundle as J,parseTeamBundle as Y,TeamBundleParseError as ee}from"../services/clone/team-bundle";import{serializeTeamBundleEncrypted as se}from"../services/clone/team-bundle-encrypted";import{buildFullStructureGraph as te}from"../services/graph-canvas/full-structure-builder";import{MultiKibanaService as ae}from"../core/services/multi-kibana-service";import{projectAssistantTokens as ne}from"../core/services/token-projection-service";import{buildMcpAppTesterZip as oe,slugifyTeamName as re}from"../services/mcp-app-tester-package";import{createGatewayGrant as D,redeemGatewayGrant as ie,GATEWAY_UNREACHABLE_PREFIX as N}from"../services/mcp-gateway/gateway-grant-service";import{isGatewayEligiblePlatformType as z}from"../core/services/gateway-platform-types";import{syncRemoteToolsForPlatform as ce}from"../core/services/mcp-sync-service";import{snapshotBeforeUpdate as de}from"../core/services/version-snapshot-service";import{findTeamOwningCommand as le,getChannelCommandOwnership as me}from"../core/services/teams-service";const ue=new ae,F=process.env.ELASTICSEARCH_HOST||"https://51.159.174.230:9205",G=process.env.ELASTICSEARCH_USER||"elastic",B=process.env.ELASTICSEARCH_PASSWORD||"",f=".stkxp_teams",v=".stkxp_assistants",pe=".stkxp_chats",u=X({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),S=new K({node:F,auth:{username:G,password:B},tls:{rejectUnauthorized:!1},requestTimeout:6e4,maxRetries:3}),L=m.object({enabled:m.string().optional().transform(s=>{if(s!==void 0)return s==="true"}),page:m.string().optional().transform(s=>s?parseInt(s,10):1),pageSize:m.string().optional().transform(s=>s?parseInt(s,10):25),search:m.string().optional().describe("Hybrid lexical + semantic search across name and description"),packageName:m.string().optional(),owners:m.string().optional(),currentUser:m.string().optional(),includeAllSystem:m.string().optional().transform(s=>s==="true"),includeOwned:m.union([m.boolean(),m.string()]).optional().transform(s=>s===!0||s==="true"),assistantId:m.string().optional()});async function ge(){try{await u.indices.create({index:f,body:{mappings:{properties:{name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"},search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},owner:{type:"keyword"},enabled:{type:"boolean"},graphId:{type:"keyword"},packageName:{type:"keyword"},assistantIds:{type:"keyword"},assistants:{properties:{id:{type:"keyword"},enabled:{type:"boolean"},condition:{type:"text",index:!1},runAlways:{type:"boolean"}}},sharedContext:{properties:{context:{type:"text"},priority:{type:"keyword"},policy:{properties:{shareToAssistants:{type:"boolean"},shareToDecider:{type:"boolean"},useAssistantProfiles:{type:"boolean"},applyOnDelegation:{type:"boolean"}}}}},webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0},created_at:{type:"date"},updated_at:{type:"date"}}}}}),console.log("[Teams API] Created index:",f)}catch(s){s.meta?.statusCode!==400&&console.error("[Teams API] Error creating index:",s)}try{await u.indices.putMapping({index:f,body:{properties:{webhookToken:{type:"keyword",index:!1},webhookEnabled:{type:"boolean"},webhookCreatedAt:{type:"date"},scheduleCron:{type:"keyword",index:!1},mcpEnabled:{type:"boolean"},mcpSigningSecret:{type:"keyword",index:!1},mcpExposeAssistants:{type:"boolean"},mcpAllowUnsigned:{type:"boolean"},mcpCreatedAt:{type:"date"},channelCommands:{type:"object",dynamic:!0}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for webhook fields:",s?.message)}try{await u.indices.putMapping({index:f,body:{properties:{search_semantic:{type:"semantic_text",inference_id:".multilingual-e5-small-elasticsearch"},name:{type:"keyword",copy_to:"search_semantic"},description:{type:"text",copy_to:"search_semantic"}}}})}catch(s){String(s?.message||"").includes("illegal_argument_exception")||console.warn("[Teams API] putMapping warning for search_semantic field:",s?.message)}}import{getTeamById as ds,getTeamWebhookConfig as ls,getTeamMcpConfig as ms,getTeamSharedContext as us}from"../core/services/teams-service";async function E(s){if(!s||s.length===0)return[];const t=new Map(s.map(n=>[n.id,n])),e=s.map(n=>n.id);try{return(await u.mget({index:v,body:{ids:e}})).docs.filter(a=>a.found).map(a=>{const o=t.get(a._id);return{id:a._id,name:a._source?.name||"Unknown",topic:a._source?.topic,enabled:o?.enabled??!0,condition:o?.condition??null,runAlways:o?.runAlways??!1}})}catch(n){return console.error("[Teams API] Error joining assistants:",n),e.map(a=>({id:a,name:a,enabled:t.get(a)?.enabled??!0}))}}function fe(s){return s==="clusters"||s==="stack_expert"?["clusters","stack_expert"]:[s]}async function ye(s,t){const e=fe(s);console.log(`[Teams API] Package filter: ${s} \u2192 [${e.join(", ")}]${t?` owner: ${t}`:""}`);try{const a=(await u.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":e}},size:1e4,_source:["name"]}})).hits.hits.map(p=>p._source?.name).filter(Boolean),o=[...new Set(a)];if(o.includes("stack_expert")&&o.push("clusters"),console.log(`[Teams API] Found ${a.length} package servers for packageName: ${e.join(",")} \u2192 [${o.join(", ")}]`),a.length===0)return new Set;const i={bool:{should:o.map(p=>({term:{"mcp_servers_policy.servers.name.keyword":p}})),minimum_should_match:1}},d=t?{bool:{must:[i,{bool:{should:[{term:{owner:"stkxp"}},{term:{owner:t}}],minimum_should_match:1}}]}}:{bool:{must:[i]}},c=await u.search({index:v,body:{query:d,size:1e4,_source:[]}}),r=new Set(c.hits.hits.map(p=>p._id));return console.log(`[Teams API] Found ${r.size} visible assistants for packageName: ${e.join(",")}`),r}catch(n){return console.error("[Teams API] Error fetching visible assistants for packageName filter:",n),null}}function H(s){return Array.isArray(s.assistants)&&s.assistants.length>0&&s.assistants[0]?.id!==void 0?s.assistants.map(t=>({id:t.id,enabled:t.enabled!==!1,condition:t.condition??null,runAlways:t.runAlways??!1})):(s.assistantIds||[]).map(t=>({id:t,enabled:!0,condition:null,runAlways:!1}))}async function he(s){try{const[t,e]=await Promise.all([u.search({index:v,body:{query:{term:{owner:s}},size:1e4,_source:[]}}),ue.getAllPackagesFromAllPlatforms(s,!1).catch(()=>({packages:[],errors:[]}))]),n=t.hits.hits.map(d=>d._id),a=[...new Set(e.packages.filter(d=>d.platforms?.some(c=>c.status==="enrolled")).map(d=>d.name).filter(Boolean))];a.includes("stack_expert")&&a.push("clusters"),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages from platforms`,a);let o=[];a.length>0&&(o=(await u.search({index:".stkxp_platforms",body:{query:{terms:{"config.packageName":a}},size:1e4,_source:["name"]}})).hits.hits.map(c=>c._source?.name).filter(Boolean));let i=[];return o.length>0&&(i=(await u.search({index:v,body:{query:{bool:{should:o.map(c=>({term:{"mcp_servers_policy.servers.name.keyword":c}})),minimum_should_match:1}},size:1e4,_source:[]}})).hits.hits.map(c=>c._id)),console.log(`[Teams API] User "${s}": ${a.length} enrolled packages, ${o.length} MCP servers, ${n.length} owned assistants, ${i.length} MCP-linked assistants`),{userMcpPackageNames:a,userOwnedAssistantIds:n,mcpLinkedAssistantIds:i}}catch(t){return console.error("[Teams API] Error fetching user visibility data:",t),{userMcpPackageNames:[],userOwnedAssistantIds:[],mcpLinkedAssistantIds:[]}}}async function we(s,t){try{const e=L.parse(s.query);console.log(`[Teams API] getTeams query: ${JSON.stringify(e)}`);const n=[];if(e.enabled!==void 0&&n.push({term:{enabled:e.enabled}}),e.search){const w=e.search.toLowerCase().trim(),T=w.split("||").map(_=>_.trim()).filter(Boolean),A=T[0]||w,P=[];let R=0,M=5;P.push({multi_match:{query:`*${A}*`,fields:["name^5","description^2"],type:"best_fields",operator:"or",minimum_should_match:"60%",fuzziness:"AUTO",boost:3}}),P.push({match:{description:{query:e.search}}}),P.push({semantic:{field:"search_semantic",query:e.search,boost:2}});for(const _ of T)P.push({match_phrase:{name:{query:_,slop:R++,boost:Math.max(M--,1)}}});n.push({bool:{should:P,minimum_should_match:1}})}if(e.currentUser&&e.includeAllSystem)n.push({bool:{should:[{term:{owner:e.currentUser}},{term:{owner:"stkxp"}}],minimum_should_match:1}});else if(e.currentUser&&e.includeOwned)n.push({term:{owner:e.currentUser}});else if(e.currentUser){const{mcpLinkedAssistantIds:w}=await he(e.currentUser);n.push({term:{owner:e.currentUser}}),w.length>0?n.push({terms:{assistantIds:w}}):n.push({term:{assistantIds:"__none__"}})}else if(e.owners){const w=e.owners.split(",").map(T=>T.trim()).filter(Boolean);w.length>0&&n.push({terms:{owner:w}})}let a=null;if(e.packageName&&(a=await ye(e.packageName,e.currentUser),a!==null)){if(a.size===0)return t.json({success:!0,count:0,total:0,page:e.page||1,pageSize:e.pageSize||25,totalPages:0,teams:[]});n.push({terms:{assistantIds:Array.from(a)}})}e.assistantId&&n.push({term:{assistantIds:e.assistantId}});const o=e.page||1,i=e.pageSize||25,d=(o-1)*i,c={name:"name",updated_at:"updated_at"},r=String(s.query.sortField||"name"),p=c[r]||"name",b=s.query.sortOrder==="desc"?"desc":"asc",l=p==="updated_at"?[{updated_at:{order:b,unmapped_type:"date"}}]:[{[p]:{order:b,unmapped_type:"keyword"}}],y={index:f,body:{track_scores:!0,query:{bool:{must:n.length>0?n:[{match_all:{}}]}},sort:[{_score:{order:"desc"}},...l],from:d,size:i,_source:{excludes:["webhookToken"]}}};console.log("[Teams API] getTeams query:",JSON.stringify(y.body.query));const g=await u.search(y),h=typeof g.hits.total=="number"?g.hits.total:g.hits.total?.value||0,I=await Promise.all(g.hits.hits.map(async w=>{const T=w._source,A=Array.isArray(T.assistants)&&T.assistants.length>0?T.assistants:(T.assistantIds||[]).map(_=>({id:_,enabled:!0})),P=a?A.filter(_=>a.has(_.id)):A,R=e.packageName?P.filter(_=>_.enabled!==!1):P,M=await E(R);return{id:w._id,...T,assistants:M}})),k=e.packageName?I.filter(w=>w.assistants.length>0):I;t.json({success:!0,count:k.length,total:h,page:o,pageSize:i,totalPages:Math.ceil(h/i),teams:k})}catch(e){if(console.error("[Teams API] Error fetching teams:",e),e instanceof m.ZodError)return t.status(400).json({success:!1,error:"Invalid query parameters",details:e.errors});if(e.meta?.statusCode===404)return t.json({success:!0,count:0,total:0,page:1,pageSize:25,totalPages:0,teams:[]});t.status(500).json({success:!1,error:"Failed to fetch teams",message:e.message})}}async function be(s,t){try{const{id:e}=s.params,n=await u.get({index:f,id:e,_source_excludes:["webhookToken"]});if(!n.found)return t.status(404).json({success:!1,error:"Team not found"});const a=n._source,o=Array.isArray(a.assistants)&&a.assistants.length>0?a.assistants:(a.assistantIds||[]).map(d=>({id:d,enabled:!0})),i=await E(o);t.json({success:!0,team:{id:n._id,...a,assistants:i}})}catch(e){if(console.error("[Teams API] Error fetching team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to fetch team",message:e.message})}}async function ke(s,t){try{await ge();const e=s.body,n=new Date().toISOString(),a=H(e),o=a.map(b=>b.id),d=s.auth?.username||e.owner||"system",c={name:e.name,description:e.description||"",assistantIds:o,assistants:a,owner:d,enabled:e.enabled!==void 0?e.enabled:!0,created_at:n,updated_at:n};e.sharedContext?.context?.trim()&&(c.sharedContext={context:e.sharedContext.context.trim(),priority:e.sharedContext.priority==="after"?"after":"before",...e.sharedContext.policy?{policy:e.sharedContext.policy}:{}}),e.graphId?.trim()&&(c.graphId=e.graphId.trim()),console.log("[Teams API] Creating team:",c.name);const r=await u.index({index:f,body:c,refresh:"wait_for"}),p=await E(a);t.status(201).json({success:!0,team:{id:r._id,...c,assistants:p}})}catch(e){console.error("[Teams API] Error creating team:",e),t.status(500).json({success:!1,error:"Failed to create team",message:e.message})}}async function Ie(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;await de("team",e,n.source,"manual_edit",x(s)||"system");const a=s.body,o=H(a),i=o.map(p=>p.id),d={name:a.name,description:a.description||"",assistantIds:i,assistants:o,owner:a.owner||"system",enabled:a.enabled!==void 0?a.enabled:!0,updated_at:new Date().toISOString()};a.sharedContext?.context?.trim()?d.sharedContext={context:a.sharedContext.context.trim(),priority:a.sharedContext.priority==="after"?"after":"before",...a.sharedContext.policy?{policy:a.sharedContext.policy}:{}}:d.sharedContext=null,d.graphId=a.graphId?.trim()||null,console.log("[Teams API] Updating team:",e);const c=await u.update({index:f,id:e,body:{doc:d},refresh:"wait_for"}),r=await E(o);t.json({success:!0,team:{id:c._id,...d,assistants:r}})}catch(e){if(console.error("[Teams API] Error updating team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to update team",message:e.message})}}async function Te(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;console.log("[Teams API] Deleting team:",e),await u.delete({index:f,id:e,refresh:"wait_for"}),t.json({success:!0,message:"Team deleted successfully"})}catch(e){if(console.error("[Teams API] Error deleting team:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to delete team",message:e.message})}}async function Ae(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=s.body?.enabled!==!1,o=O(32).toString("hex"),i=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{webhookToken:o,webhookEnabled:a,webhookCreatedAt:i,updated_at:i}},refresh:"wait_for"}),console.log(`[Teams API] Generated webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookToken:o,webhookEnabled:a,webhookCreatedAt:i,webhookUrl:`/api/webhooks/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate webhook token",message:e.message})}}async function Ce(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{webhookToken:null,webhookEnabled:!1,webhookCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked webhook token for team: ${e}`),t.json({success:!0,teamId:e,webhookEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking webhook token:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke webhook token",message:e.message})}}async function xe(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source;if(!a.mcpEnabled)return t.status(400).json({success:!1,error:"MCP server is not enabled for this team \u2014 generate/enable it in the MCP App tab first."});const o=a.name||"team",i=`${s.protocol}://${s.get("host")}/api/mcp/team/${e}`,d=await oe({teamName:o,teamId:e,mcpUrl:i,allowUnsigned:a.mcpAllowUnsigned===!0}),c=`mcp-app-tester-${re(o)}.zip`;t.set({"Content-Type":"application/zip","Content-Disposition":`attachment; filename="${c}"`}),t.send(d)}catch(e){console.error("[Teams API] Error generating MCP App tester package:",e),t.status(500).json({success:!1,error:"Failed to generate test client package",message:e.message})}}async function _e(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=s.body?.enabled!==!1,o=s.body?.exposeAssistants!==!1,i=s.body?.allowUnsigned===!0,d=typeof s.body?.signingSecret=="string"?s.body.signingSecret.trim():void 0;if(d!==void 0&&d.length<8)return t.status(400).json({success:!1,error:"signingSecret must be at least 8 characters (use your Slack app Signing Secret to integrate with Slack)"});const c=d||O(32).toString("hex"),r=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:i,mcpCreatedAt:r,updated_at:r}},refresh:"wait_for"}),console.log(`[Teams API] Generated MCP signing secret for team: ${e}`),t.json({success:!0,teamId:e,mcpSigningSecret:c,mcpEnabled:a,mcpExposeAssistants:o,mcpAllowUnsigned:i,mcpCreatedAt:r,mcpUrl:`/api/mcp/team/${e}`})}catch(e){if(console.error("[Teams API] Error generating MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to generate MCP config",message:e.message})}}async function Se(s,t){try{const{id:e}=s.params;if(!await C(s,t,u,f,e))return;const a=new Date().toISOString();await u.update({index:f,id:e,body:{doc:{mcpSigningSecret:null,mcpEnabled:!1,mcpAllowUnsigned:!1,mcpCreatedAt:null,updated_at:a}},refresh:"wait_for"}),console.log(`[Teams API] Revoked MCP config for team: ${e}`),t.json({success:!0,teamId:e,mcpEnabled:!1})}catch(e){if(console.error("[Teams API] Error revoking MCP config:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to revoke MCP config",message:e.message})}}async function Pe(s,t){try{const{id:e,platformId:n}=s.params,a=await C(s,t,u,f,e);if(!a)return;const o=Array.isArray(s.body?.commandIds)?s.body.commandIds:[],i=typeof s.body?.namespace=="string"?s.body.namespace:void 0,d=s.body?.blockKitMode===!0,c=a.source.owner;for(const l of o){const y=await le(c,n,l,e);if(y)return t.status(409).json({success:!1,message:`Command is already assigned to team "${y.teamName}"`})}const r=new Date().toISOString(),b={...a.source.channelCommands??{},[n]:{commandIds:o,namespace:i,blockKitMode:d}};await u.update({index:f,id:e,body:{doc:{channelCommands:b,updated_at:r}},refresh:"wait_for"}),console.log(`[Teams API] Updated channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n,commandIds:o,namespace:i,blockKitMode:d})}catch(e){if(console.error("[Teams API] Error setting channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to set channel commands",message:e.message})}}async function ve(s,t){try{const{id:e,platformId:n}=s.params,a=await C(s,t,u,f,e);if(!a)return;const o=new Date().toISOString(),i={...a.source.channelCommands??{}};delete i[n],await u.update({index:f,id:e,body:{doc:{channelCommands:i,updated_at:o}},refresh:"wait_for"}),console.log(`[Teams API] Cleared channel commands for team ${e} / platform ${n}`),t.json({success:!0,teamId:e,platformId:n})}catch(e){if(console.error("[Teams API] Error clearing channel commands:",e),e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to clear channel commands",message:e.message})}}async function je(s,t){try{const e=x(s);if(!e)return t.status(401).json({success:!1,message:"Unauthorized"});const n=s.query.platformId;if(!n)return t.status(400).json({success:!1,message:"platformId is required"});const a=await me(e,n);t.json({success:!0,body:{ownership:a}})}catch(e){console.error("[Teams API] Error fetching channel command ownership:",e),t.status(500).json({success:!1,message:e.message||"Failed to fetch channel command ownership"})}}async function Ee(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source.owner||x(s)||"system",o=j(S),i=await $([{index:f,esId:e}],a,o),d=te(i);t.json({success:!0,graph:d})}catch(e){if(console.error("[Teams API] Error building full-structure graph:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to build full-structure graph",message:e.message})}}async function Re(s,t){try{const{id:e}=s.params,n=await C(s,t,u,f,e);if(!n)return;const a=n.source.owner||x(s)||"system",o=n.source.name||"(unnamed team)",i=j(S),d=await $([{index:f,esId:e}],a,i),c=J(d,{exportedAt:new Date().toISOString(),rootName:o}),r=d.docs.filter(l=>l.key==="platform"&&z(l.source?.type)&&l.source?.owner===a).map(l=>l.esId),p=d.docs.some(l=>l.key==="tool"&&l.reachedVia?.includes(U)&&l.source?.owner===a);let b;if(r.length>0||p){const l=await D(d,a,e);if(!l)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const g of c.closure.docs)g.key==="platform"&&r.includes(g.esId)&&(g.source.config={...g.source.config,serverType:"gateway",url:""});const y=c.closure.docs.filter(g=>g.key==="tool"&&g.source?.type==="mcp_remote"&&(r.includes(g.source?.platformId)||r.includes(g.source?.mcpServerId))).length;y>0&&(c.closure.docs=c.closure.docs.filter(g=>!(g.key==="tool"&&g.source?.type==="mcp_remote"&&(r.includes(g.source?.platformId)||r.includes(g.source?.mcpServerId)))),c.summary.totalDocs-=y,c.summary.byEntity.tool=(c.summary.byEntity.tool??0)-y),c.mcpGateway={grantId:l.grantId,platformIds:l.platformIds},b=l.secret}console.log(`[Teams API] ${x(s)??"?"} exported team "${o}" (${e}): ${c.summary.totalDocs} docs, ${d.unresolved.length} unresolved ref(s)`+(b?`, gateway grant for ${r.length} platform(s)`:"")),t.json({success:!0,bundle:c,gatewaySecret:b,unresolved:d.unresolved})}catch(e){if(console.error("[Teams API] Error exporting team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});if((e?.message??"").startsWith(N))return t.status(422).json({success:!1,error:e.message});if(e.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team",message:e.message})}}const W=m.object({secret:m.string().min(8)});async function Me(s,t){const e=W.safeParse(s.body);if(!e.success)return t.status(400).json({success:!1,error:'Body must include a "secret" of at least 8 characters'});const{secret:n}=e.data;try{const{id:a}=s.params,o=await C(s,t,u,f,a);if(!o)return;const i=o.source.owner||x(s)||"system",d=o.source.name||"(unnamed team)",c=j(S),r=await $([{index:f,esId:a}],i,c),p=[...new Set(r.docs.map(k=>k.index))],b={};if(p.length>0){const k=await S.indices.getMapping({index:p});for(const[w,T]of Object.entries(k))b[w]=T.mappings}const l=r.docs.filter(k=>k.key==="platform"&&z(k.source?.type)&&k.source?.owner===i).map(k=>k.esId),y=r.docs.some(k=>k.key==="tool"&&k.reachedVia?.includes(U)&&k.source?.owner===i);let g,h;if(l.length>0||y){const k=await D(r,i,a);if(!k)return t.status(422).json({success:!1,error:"Failed to create the MCP gateway grant: could not connect to one or more MCP server platforms in this team."});for(const w of r.docs)w.key==="platform"&&l.includes(w.esId)&&(w.source.config={...w.source.config,serverType:"gateway",url:""});r.docs=r.docs.filter(w=>!(w.key==="tool"&&w.source?.type==="mcp_remote"&&(l.includes(w.source?.platformId)||l.includes(w.source?.mcpServerId)))),h={grantId:k.grantId,platformIds:k.platformIds,sourceAppUrl:process.env.STKXP_APP_PUBLIC_URL||"https://app.erretegia.com",gatewayHost:process.env.MCP_GATEWAY_HOST||"https://mcp.erretegia.com"},g=k.secret}const I=se(r,{exportedAt:new Date().toISOString(),rootName:d,passphrase:n,gatewaySecret:g,indexMappings:b});I.mcpGateway=h,console.log(`[Teams API] ${x(s)??"?"} exported (encrypted) team "${d}" (${a}): ${I.summary.totalDocs} docs, ${r.unresolved.length} unresolved ref(s)`),t.json({success:!0,bundle:I,unresolved:r.unresolved})}catch(a){if(console.error("[Teams API] Error exporting team (encrypted):",a),/Required dependencies/.test(a?.message??""))return t.status(422).json({success:!1,error:a.message});if((a?.message??"").startsWith(N))return t.status(422).json({success:!1,error:a.message});if(a.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});t.status(500).json({success:!1,error:"Failed to export team (encrypted)",message:a.message})}}async function $e(s,t){try{const e=x(s);if(!e)return t.status(401).json({success:!1,error:"Unauthorized"});const n=(s.body&&s.body.bundle)??s.body,a=s.body&&typeof s.body.gatewaySecret=="string"?s.body.gatewaySecret:void 0;let o;try{o=Y(n)}catch(p){const b=p instanceof ee?p.message:"Invalid bundle";return t.status(400).json({success:!1,error:b})}let i;if(o.mcpGateway){if(!a)return t.status(400).json({success:!1,error:"This bundle requires a gateway key to activate its MCP server platform(s). Paste the key you received from the exporting owner."});const p=await ie(o.mcpGateway.grantId,a,e);if("error"in p)return t.status(400).json({success:!1,error:p.error});i=p.gatewayToken}const d=j(S),c=Z(S),r=await Q(o.closure,e,d,c,{blankSecrets:!0});if(o.mcpGateway&&i){const p=process.env.MCP_GATEWAY_HOST||"https://mcp.erretegia.com",b=new Date().toISOString();for(const y of r.created){if(y.key!=="platform"||!o.mcpGateway.platformIds.includes(y.oldEsId))continue;const g=`${p}/mcp/gateway:${o.mcpGateway.grantId}:${y.oldEsId}`;await S.update({index:y.index,id:y.newEsId,body:{doc:{managedType:"managed",config:{serverType:"gateway",url:g,authKey:i}}},refresh:!0});try{await ce(y.newEsId,{id:y.newEsId,name:y.name||"MCP Server",type:"MCPServer",managedType:"self-hosted",owner:e,created:b,updated:b,enabled:!0,config:{serverType:"gateway",url:g,authKey:i}},e)}catch(h){console.warn(`[Teams API] Post-import gateway tool sync failed for platform ${y.newEsId}: ${h.message}`)}}const l=`personal:${o.sourceOwner}`;if(o.mcpGateway.platformIds.includes(l)){const y=`${p}/mcp/gateway:${o.mcpGateway.grantId}:${l}`,g=q();await S.index({index:".stkxp_platforms",id:g,document:{id:g,name:"My MCP Tools (gateway)",type:"MCPServer",managedType:"managed",owner:e,enabled:!0,created:b,updated:b,config:{serverType:"gateway",url:y,authKey:i,protocol:"http"}},refresh:!0});const h=`__personal_tools__:${o.sourceOwner}`;for(const I of r.created){if(I.key!=="assistant")continue;const k=o.closure.docs.find(A=>A.key==="assistant"&&A.esId===I.oldEsId),w=k?.source?.mcp_servers_policy?.servers;if(!Array.isArray(w)||!w.some(A=>A?.id===h))continue;const T=w.map(A=>A?.id===h?{...A,id:g}:A);await S.update({index:I.index,id:I.newEsId,body:{doc:{mcp_servers_policy:{...k.source.mcp_servers_policy,servers:T}}},refresh:!0})}}}console.log(`[Teams API] ${e} imported team "${o.rootName}" (from owner "${o.sourceOwner}"): ${r.created.length} created, ${r.skipped.length} reused, ${r.referenced.length} referenced, ${r.droppedRefs.length} refs dropped`),t.status(201).json({success:!0,rootName:o.rootName,report:r,unresolved:o.closure.unresolved})}catch(e){if(console.error("[Teams API] Error importing team:",e),/Required dependencies/.test(e?.message??""))return t.status(422).json({success:!1,error:e.message});t.status(500).json({success:!1,error:"Failed to import team",message:e.message})}}async function Oe(s,t){try{const{id:e}=s.params;if(!x(s))return t.status(401).json({success:!1,error:"Unauthorized"});let a;try{a=(await u.get({index:f,id:e}))._source}catch(l){if(l?.meta?.statusCode===404)return t.status(404).json({success:!1,error:"Team not found"});throw l}const i=(a?.assistants||[]).filter(l=>l.enabled!==!1).map(l=>l.id);if(i.length===0)return t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:0,assistants:[]});const c=(await u.mget({index:v,body:{ids:i}})).docs.filter(l=>l.found),r=new Map,p=[];let b=0;for(const l of c){const y=await ne(u,l._id,l._source||{},r);b+=y.singlePassMax,p.push(y)}t.json({success:!0,teamId:e,teamName:a?.name,singlePassMax:b,assistants:p,note:"Single-pass upper bound. Tool loops can exceed it \u2014 multiply by an empirical factor (2-3\xD7) when sizing a budget."})}catch(e){console.error("[Teams API] Error computing token projection:",e),t.status(500).json({success:!1,error:"Failed to compute token projection",message:e.message})}}async function Ue(s,t){try{const{id:e}=s.params,n=x(s);if(!n)return t.status(401).json({success:!1,error:"Unauthorized"});const a=Math.min(parseInt(s.query.limit||"20",10)||20,100),o=[{term:{teamId:e}}];V(s)||o.push({term:{username:n}});const c=((await u.search({index:pe,body:{size:a,sort:[{createdAt:{order:"desc"}}],_source:["id","title","createdAt","metadata.tokenUsage","assistants"],query:{bool:{must:o}}}})).hits.hits||[]).map(h=>{const I=h._source||{},k=I.metadata?.tokenUsage||{};return{chatId:h._id,title:I.title,createdAt:I.createdAt,totalTokens:Number(k.totalTokens||0),inputTokens:Number(k.totalInputTokens||0),outputTokens:Number(k.totalOutputTokens||0),assistantCount:Array.isArray(I.assistants)?I.assistants.length:0}}),r=c.map(h=>h.totalTokens).filter(h=>h>0),p=r.reduce((h,I)=>h+I,0),b=[...r].sort((h,I)=>h-I),l=b.length?b[Math.min(b.length-1,Math.floor(b.length*.95))]:0,y=c.map(h=>h.inputTokens),g=c.map(h=>h.outputTokens);t.json({success:!0,runCount:r.length,avgTotalTokens:r.length?Math.round(p/r.length):0,maxTotalTokens:r.length?Math.max(...r):0,p95TotalTokens:l,avgInputTokens:y.length?Math.round(y.reduce((h,I)=>h+I,0)/y.length):0,avgOutputTokens:g.length?Math.round(g.reduce((h,I)=>h+I,0)/g.length):0,recentRuns:c})}catch(e){console.error("[Teams API] Error fetching token stats:",e),t.status(500).json({success:!1,error:"Failed to fetch token stats",message:e.message})}}const De=m.object({name:m.string().describe("Team name"),description:m.string().optional().describe("Human description"),assistantIds:m.array(m.string()).optional().describe("Ids of assistants in the team"),assistants:m.array(m.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:m.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:m.boolean().optional().describe("Enabled flag (default true)"),sharedContext:m.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),Ne=m.object({name:m.string().optional().describe("Team name"),description:m.string().optional().describe("Human description"),assistantIds:m.array(m.string()).optional().describe("Ids of assistants in the team"),assistants:m.array(m.any()).optional().describe("Full assistant objects (alternative to assistantIds)"),graphId:m.string().optional().describe("Id of a .stkxp_graphs coordination graph"),enabled:m.boolean().optional().describe("Enabled flag (default true)"),sharedContext:m.any().optional().describe("Shared context object injected into every assistant")}).passthrough(),rs=[{method:"get",path:"/api/teams",handler:we,validate:{query:L},openapi:{summary:"List teams for the caller",description:"Returns all teams owned by the authenticated user. A team groups assistants and optionally references a coordination graph (`graphId`) for multi-assistant orchestration. `search` runs a hybrid lexical + semantic query (semantic_text field `search_semantic`).",tags:["teams"]}},{method:"post",path:"/api/teams/import",handler:$e,openapi:{summary:"Import a team from an export bundle",description:"Reconstructs a team and its full dependency closure under the authenticated caller from a bundle produced by `GET /api/teams/:id/export`. New ids are minted for owned docs (links rebuilt), shared/system docs are referenced in place, same-named docs the caller already owns are reused, and secrets stay blank (re-enter LLM keys / platform authKeys and regenerate the webhook token afterwards).",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/channel-command-ownership",handler:je,openapi:{summary:"Get command\u2192team ownership map for a platform",description:"Query param `platformId`. Returns `{ [commandId]: { teamId, teamName } }` for every command on that platform currently owned by one of the caller's teams \u2014 used by the Team Edit Channels tab to disable already-claimed commands.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id",handler:be,openapi:{summary:"Get a team by id",tags:["teams"]}},{method:"get",path:"/api/teams/:id/export",handler:Re,openapi:{summary:"Export a team as a portable bundle",description:"Resolves the team's full transitive dependency closure (assistants, graphs, sub-graphs, prompts, node templates, routing rules, models, providers, platforms, tools) and returns a secret-free JSON bundle for re-import via `POST /api/teams/import`. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/export-encrypted",handler:Me,validate:{body:W},openapi:{summary:"Export a team as a fully self-contained, passphrase-encrypted bundle",description:"Like GET /api/teams/:id/export, but instead of blanking secrets (LLM keys, platform creds, webhook token), encrypts them with the caller-supplied passphrase (AES-256-GCM per field). Intended for the `stkxp export` CLI, which produces a bundle deployable standalone via `stkxp deploy` \u2014 see docs/superpowers/specs/2026-09-15-team-standalone-export-design.md.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/full-structure",handler:Ee,openapi:{summary:"Get a team's full structure as a flattened canvas graph",description:"Resolves the team's full transitive dependency closure (assistants, runtime graphs, nested subgraphs, tools, platforms, models, providers, routing rules) and returns it flattened into nodes/edges/groups for the Team Canvas Overview mode. Read-only. Limited to teams the caller owns.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams",handler:ke,validate:{body:De},openapi:{summary:"Create a team",description:"Creates a team with the supplied `name`, optional `description`, `assistantIds[]`, and optional `graphId` pointing to a `.stkxp_graphs` coordination graph.",tags:["teams"]}},{method:"put",path:"/api/teams/:id",handler:Ie,validate:{body:Ne},openapi:{summary:"Update a team",tags:["teams"]}},{method:"delete",path:"/api/teams/:id",handler:Te,openapi:{summary:"Delete a team",tags:["teams"]}},{method:"get",path:"/api/teams/:id/token-projection",handler:Oe,openapi:{summary:"Theoretical single-pass max token usage for a team",description:"Resolves each enabled assistant graph (`assistant.graphId`), applies `assistant.llm_overrides[node.type]` on top of node-level routing rules, and sums `maxTokens` across nodes with a routing rule. Single-pass upper bound only \u2014 tool loops can exceed it.",tags:["teams"],audience:"internal"}},{method:"get",path:"/api/teams/:id/token-stats",handler:Ue,openapi:{summary:"Aggregate token usage for the caller's recent runs of a team",description:"Reads `.stkxp_chats` filtered by `teamId` (and `username` unless superuser) and returns avg / max / p95 total tokens plus the last N runs. Used by the Team flyout to surface real consumption before setting a future budget.",tags:["teams"],audience:"internal"}},{method:"post",path:"/api/teams/:id/webhook-token",handler:Ae,openapi:{summary:"Generate (or rotate) a webhook token for a team",description:"Creates a new inbound webhook token. Returns the clear-text token **once** \u2014 the UI must surface it immediately to the operator. Subsequent fetches of the team do not expose the token. Optional body `{ enabled?: boolean }` (defaults to true) controls whether the webhook is active.",tags:["teams","webhooks"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/webhook-token",handler:Ce,openapi:{summary:"Revoke the webhook token for a team",description:"Clears the webhook token and disables the webhook. POSTs to `/api/webhooks/team/:id` will be rejected with 401 until a new token is generated.",tags:["teams","webhooks"],audience:"internal"}},{method:"post",path:"/api/teams/:id/mcp-config",handler:_e,openapi:{summary:"Enable (or rotate) the MCP server for a team",description:'Owner-only. Generates a signing secret (returned once) and enables the per-team MCP endpoint at /api/mcp/team/:id. Body `{ enabled?: boolean, exposeAssistants?: boolean, signingSecret?: string }` \u2014 enabled/exposeAssistants default true; if `signingSecret` is provided (\u22658 chars) it is used verbatim (e.g. a Slack app Signing Secret to integrate with Slack), otherwise a random 32-byte secret is minted. `allowUnsigned` (default false) accepts unsigned requests \u2014 needed for Slack\'s MCP client, which sends no signature for auth_type "no_auth"; a signature, when present, is always verified.',tags:["teams","mcp"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/mcp-config",handler:Se,openapi:{summary:"Disable the MCP server for a team",description:"Owner-only. Clears the signing secret and disables the MCP endpoint. Calls to /api/mcp/team/:id return 404 until re-enabled.",tags:["teams","mcp"],audience:"internal"}},{method:"post",path:"/api/teams/:id/channel-commands/:platformId",handler:Pe,openapi:{summary:"Claim one or more commands on a platform for a team",description:"Body `{ commandIds: string[], namespace?: string, blockKitMode?: boolean }`. Rejects with 409 if any commandId is already owned by a different team. A command can be owned by at most one team at a time, scoped to (platformId, commandId).",tags:["teams","channels"],audience:"internal"}},{method:"delete",path:"/api/teams/:id/channel-commands/:platformId",handler:ve,openapi:{summary:"Clear a team's channel-command config for a platform",description:"Removes this team's ownership of any commands claimed on the given platform, freeing them for other teams.",tags:["teams","channels"],audience:"internal"}},{method:"get",path:"/api/teams/:id/mcp-app-tester.zip",handler:xe,openapi:{summary:"Download a pre-configured MCP App test client for this team",description:"Owner-only. Generates a ZIP \u2014 a minimal fork of mcp-apps-tester wired to this team's ask_ tool, with Slack-style HMAC request signing baked in. Requires the team's MCP server to be enabled (mcpEnabled). Ships without the signing secret \u2014 the caller pastes their own copy into the generated .env.",tags:["teams","mcp"],audience:"internal"}}];export{ge as ensureTeamsIndex,ds as getTeamById,ms as getTeamMcpConfig,us as getTeamSharedContext,ls as getTeamWebhookConfig,rs as routes};
|