triagent 0.1.0-alpha13 → 0.1.0-alpha18

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.
Files changed (34) hide show
  1. package/package.json +3 -4
  2. package/src/cli/config.ts +96 -0
  3. package/src/index.ts +201 -3
  4. package/src/integrations/elasticsearch/client.ts +210 -0
  5. package/src/integrations/grafana/client.ts +186 -0
  6. package/src/integrations/kubernetes/multi-cluster.ts +199 -0
  7. package/src/integrations/kubernetes/types.ts +24 -0
  8. package/src/integrations/loki/client.ts +219 -0
  9. package/src/integrations/prometheus/client.ts +163 -0
  10. package/src/integrations/slack/client.ts +265 -0
  11. package/src/integrations/teams/client.ts +199 -0
  12. package/src/mastra/agents/debugger.ts +152 -108
  13. package/src/mastra/tools/approval-store.ts +180 -0
  14. package/src/mastra/tools/cli.ts +94 -2
  15. package/src/mastra/tools/cost.ts +389 -0
  16. package/src/mastra/tools/logs.ts +210 -0
  17. package/src/mastra/tools/network.ts +253 -0
  18. package/src/mastra/tools/prometheus.ts +221 -0
  19. package/src/mastra/tools/remediation.ts +365 -0
  20. package/src/mastra/tools/runbook.ts +186 -0
  21. package/src/server/routes/history.ts +207 -0
  22. package/src/server/routes/notifications.ts +236 -0
  23. package/src/server/webhook.ts +36 -2
  24. package/src/storage/index.ts +3 -0
  25. package/src/storage/investigation-history.ts +277 -0
  26. package/src/storage/runbook-index.ts +330 -0
  27. package/src/storage/types.ts +72 -0
  28. package/src/tui/app.tsx +492 -76
  29. package/src/tui/components/approval-dialog.tsx +156 -0
  30. package/src/tui/components/approval-modal.tsx +278 -0
  31. package/src/tui/components/index.ts +38 -0
  32. package/src/tui/components/styled-span.tsx +24 -0
  33. package/src/tui/components/timeline.tsx +223 -0
  34. package/src/tui/components/toast.tsx +101 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triagent",
3
- "version": "0.1.0-alpha13",
3
+ "version": "0.1.0-alpha18",
4
4
  "description": "AI-powered Kubernetes debugging agent with terminal UI",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -47,11 +47,11 @@
47
47
  "@ai-sdk/openai": "^1.3.0",
48
48
  "@bashlet/sdk": "^0.1.0-alpha1",
49
49
  "@mastra/core": "^1.0.0-beta.21",
50
+ "@opentui-ui/dialog": "^0.1.2",
51
+ "@opentui-ui/toast": "^0.0.5",
50
52
  "@opentui/core": "^0.1.72",
51
53
  "@opentui/solid": "^0.1.72",
52
54
  "hono": "^4.6.0",
53
- "marked": "^17.0.1",
54
- "marked-terminal": "^7.3.0",
55
55
  "opentui-spinner": "^0.0.6",
56
56
  "solid-js": "1.9.9",
57
57
  "triagent": "^0.1.0-alpha1",
@@ -59,7 +59,6 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/babel__core": "^7.20.5",
62
- "@types/marked-terminal": "^6.1.1",
63
62
  "@types/node": "^22.0.0",
64
63
  "bun-types": "^1.2.0",
65
64
  "typescript": "^5.7.0"
package/src/cli/config.ts CHANGED
@@ -8,6 +8,65 @@ export interface CodebaseEntry {
8
8
  path: string;
9
9
  }
10
10
 
11
+ export interface ClusterConfig {
12
+ name: string;
13
+ context: string;
14
+ kubeConfigPath?: string;
15
+ environment?: "development" | "staging" | "production";
16
+ }
17
+
18
+ export interface PrometheusConfig {
19
+ url: string;
20
+ auth?: { token: string };
21
+ }
22
+
23
+ export interface GrafanaConfig {
24
+ url: string;
25
+ apiKey: string;
26
+ }
27
+
28
+ export interface ElasticsearchConfig {
29
+ url: string;
30
+ index: string;
31
+ auth?: { apiKey: string };
32
+ }
33
+
34
+ export interface LokiConfig {
35
+ url: string;
36
+ }
37
+
38
+ export interface CloudWatchConfig {
39
+ region: string;
40
+ logGroupPrefix?: string;
41
+ }
42
+
43
+ export interface SlackConfig {
44
+ webhookUrl: string;
45
+ botToken?: string;
46
+ defaultChannel?: string;
47
+ }
48
+
49
+ export interface TeamsConfig {
50
+ webhookUrl: string;
51
+ }
52
+
53
+ export interface RunbookConfig {
54
+ paths: string[];
55
+ gitRepos?: string[];
56
+ }
57
+
58
+ export interface CostAnalysisConfig {
59
+ provider?: "aws" | "gcp" | "azure";
60
+ hourlyRates?: {
61
+ cpu: number;
62
+ memory: number;
63
+ storage: number;
64
+ };
65
+ businessImpact?: {
66
+ revenuePerMinute?: number;
67
+ };
68
+ }
69
+
11
70
  export interface StoredConfig {
12
71
  aiProvider?: AIProvider;
13
72
  aiModel?: string;
@@ -17,11 +76,35 @@ export interface StoredConfig {
17
76
  codebasePath?: string; // Deprecated: use codebasePaths instead
18
77
  codebasePaths?: CodebaseEntry[];
19
78
  kubeConfigPath?: string;
79
+
80
+ // Phase 1: Foundation
81
+ historyRetentionDays?: number;
82
+ clusters?: ClusterConfig[];
83
+ activeCluster?: string;
84
+
85
+ // Phase 2: Observability
86
+ prometheus?: PrometheusConfig;
87
+ grafana?: GrafanaConfig;
88
+ logProvider?: "elasticsearch" | "loki" | "cloudwatch";
89
+ elasticsearch?: ElasticsearchConfig;
90
+ loki?: LokiConfig;
91
+ cloudwatch?: CloudWatchConfig;
92
+
93
+ // Phase 3: Operations
94
+ runbooks?: RunbookConfig;
95
+
96
+ // Phase 4: Communication & Cost
97
+ notifications?: {
98
+ slack?: SlackConfig;
99
+ teams?: TeamsConfig;
100
+ };
101
+ costAnalysis?: CostAnalysisConfig;
20
102
  }
21
103
 
22
104
  const CONFIG_DIR = join(homedir(), ".config", "triagent");
23
105
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
24
106
  const TRIAGENT_MD_FILE = join(CONFIG_DIR, "TRIAGENT.md");
107
+ const RUNBOOK_MD_FILE = join(CONFIG_DIR, "RUNBOOK.md");
25
108
 
26
109
  export async function getConfigPath(): Promise<string> {
27
110
  return CONFIG_FILE;
@@ -40,6 +123,19 @@ export async function loadTriagentMd(): Promise<string | null> {
40
123
  }
41
124
  }
42
125
 
126
+ export async function loadRunbookMd(): Promise<string | null> {
127
+ try {
128
+ const content = await readFile(RUNBOOK_MD_FILE, "utf-8");
129
+ return content.trim();
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ export async function getRunbookMdPath(): Promise<string> {
136
+ return RUNBOOK_MD_FILE;
137
+ }
138
+
43
139
  export async function loadStoredConfig(): Promise<StoredConfig> {
44
140
  try {
45
141
  const content = await readFile(CONFIG_FILE, "utf-8");
package/src/index.ts CHANGED
@@ -16,10 +16,15 @@ import {
16
16
  import type { AIProvider } from "./config.js";
17
17
 
18
18
  interface CliArgs {
19
- command: "run" | "config";
19
+ command: "run" | "config" | "cluster";
20
20
  configAction?: "set" | "get" | "list" | "path";
21
21
  configKey?: string;
22
22
  configValue?: string;
23
+ clusterAction?: "add" | "remove" | "list" | "use" | "status";
24
+ clusterName?: string;
25
+ clusterContext?: string;
26
+ clusterKubeConfig?: string;
27
+ clusterEnvironment?: string;
23
28
  webhookOnly: boolean;
24
29
  incident: string | null;
25
30
  help: boolean;
@@ -45,6 +50,30 @@ function parseArgs(): CliArgs {
45
50
  return result;
46
51
  }
47
52
 
53
+ // Check for cluster subcommand
54
+ if (args[0] === "cluster") {
55
+ result.command = "cluster";
56
+ result.clusterAction = args[1] as "add" | "remove" | "list" | "use" | "status";
57
+
58
+ // Parse cluster sub-command arguments
59
+ for (let i = 2; i < args.length; i++) {
60
+ const arg = args[i];
61
+ if (arg === "--name" || arg === "-n") {
62
+ result.clusterName = args[++i];
63
+ } else if (arg === "--context" || arg === "-c") {
64
+ result.clusterContext = args[++i];
65
+ } else if (arg === "--kubeconfig" || arg === "-k") {
66
+ result.clusterKubeConfig = args[++i];
67
+ } else if (arg === "--environment" || arg === "-e") {
68
+ result.clusterEnvironment = args[++i];
69
+ } else if (!arg.startsWith("-")) {
70
+ // Positional argument - cluster name
71
+ result.clusterName = arg;
72
+ }
73
+ }
74
+ return result;
75
+ }
76
+
48
77
  for (let i = 0; i < args.length; i++) {
49
78
  const arg = args[i];
50
79
 
@@ -69,6 +98,7 @@ function printHelp(): void {
69
98
  USAGE:
70
99
  triagent [OPTIONS]
71
100
  triagent config <action> [key] [value]
101
+ triagent cluster <action> [options]
72
102
 
73
103
  OPTIONS:
74
104
  -h, --help Show this help message
@@ -98,6 +128,19 @@ CONFIG KEYS:
98
128
  ]
99
129
  Each codebase will be mounted at /workspace/<name> in the sandbox.
100
130
 
131
+ CLUSTER COMMANDS:
132
+ triagent cluster add <name> --context <ctx> Add a cluster
133
+ triagent cluster remove <name> Remove a cluster
134
+ triagent cluster list List all clusters
135
+ triagent cluster use <name> Set active cluster
136
+ triagent cluster status [name] Check cluster status
137
+
138
+ CLUSTER OPTIONS:
139
+ -n, --name Cluster name
140
+ -c, --context Kubernetes context name
141
+ -k, --kubeconfig Path to kubeconfig file
142
+ -e, --environment Environment (development, staging, production)
143
+
101
144
  MODES:
102
145
  Interactive (default):
103
146
  Run with no arguments to start the interactive TUI.
@@ -108,9 +151,12 @@ MODES:
108
151
  incident webhooks from alerting systems.
109
152
 
110
153
  Endpoints:
111
- POST /webhook/incident - Submit an incident
154
+ POST /webhook/incident - Submit an incident
112
155
  GET /investigations/:id - Get investigation results
113
- GET /health - Health check
156
+ GET /history - List investigation history
157
+ GET /history/:id - Get investigation details
158
+ GET /history/stats - Get investigation statistics
159
+ GET /health - Health check
114
160
 
115
161
  Direct Input:
116
162
  Use --incident "description" for one-shot debugging.
@@ -137,6 +183,11 @@ EXAMPLES:
137
183
  # Direct incident investigation
138
184
  triagent -i "API gateway returning 503 errors"
139
185
 
186
+ # Multi-cluster management
187
+ triagent cluster add prod --context prod-cluster -e production
188
+ triagent cluster use prod
189
+ triagent cluster status
190
+
140
191
  # Submit via curl (webhook mode)
141
192
  curl -X POST http://localhost:3000/webhook/incident \\
142
193
  -H "Content-Type: application/json" \\
@@ -185,6 +236,147 @@ async function runDirectIncident(description: string): Promise<void> {
185
236
  }
186
237
  }
187
238
 
239
+ import { initClusterManager, getClusterManager } from "./integrations/kubernetes/multi-cluster.js";
240
+ import type { ClusterConfig } from "./cli/config.js";
241
+
242
+ async function handleClusterCommand(args: CliArgs): Promise<void> {
243
+ const config = await loadStoredConfig();
244
+ const clusterManager = initClusterManager(config.clusters, config.activeCluster);
245
+
246
+ switch (args.clusterAction) {
247
+ case "add": {
248
+ if (!args.clusterName) {
249
+ console.error("Usage: triagent cluster add <name> --context <context>");
250
+ process.exit(1);
251
+ }
252
+ const context = args.clusterContext || args.clusterName;
253
+ const newCluster: ClusterConfig = {
254
+ name: args.clusterName,
255
+ context,
256
+ kubeConfigPath: args.clusterKubeConfig,
257
+ environment: args.clusterEnvironment as ClusterConfig["environment"],
258
+ };
259
+
260
+ await clusterManager.addCluster(newCluster);
261
+
262
+ // Save to config
263
+ config.clusters = config.clusters || [];
264
+ config.clusters.push(newCluster);
265
+ await saveStoredConfig(config);
266
+
267
+ console.log(`✅ Added cluster: ${args.clusterName} (context: ${context})`);
268
+ break;
269
+ }
270
+ case "remove": {
271
+ if (!args.clusterName) {
272
+ console.error("Usage: triagent cluster remove <name>");
273
+ process.exit(1);
274
+ }
275
+
276
+ const removed = await clusterManager.removeCluster(args.clusterName);
277
+ if (!removed) {
278
+ console.error(`Cluster not found: ${args.clusterName}`);
279
+ process.exit(1);
280
+ }
281
+
282
+ // Remove from config
283
+ config.clusters = config.clusters?.filter((c) => c.name !== args.clusterName);
284
+ if (config.activeCluster === args.clusterName) {
285
+ config.activeCluster = undefined;
286
+ }
287
+ await saveStoredConfig(config);
288
+
289
+ console.log(`✅ Removed cluster: ${args.clusterName}`);
290
+ break;
291
+ }
292
+ case "list": {
293
+ const clusters = clusterManager.listClusters();
294
+ if (clusters.length === 0) {
295
+ console.log("No clusters configured.");
296
+ console.log("\nDiscover available contexts:");
297
+ const discovered = await clusterManager.discoverClusters();
298
+ if (discovered.length > 0) {
299
+ console.log("\nAvailable Kubernetes contexts:");
300
+ for (const c of discovered) {
301
+ console.log(` - ${c.context} (${c.server})`);
302
+ }
303
+ console.log("\nAdd a cluster with: triagent cluster add <name> --context <context>");
304
+ } else {
305
+ console.log("No Kubernetes contexts found.");
306
+ }
307
+ } else {
308
+ console.log("Configured clusters:\n");
309
+ for (const c of clusters) {
310
+ const active = c.isActive ? " (active)" : "";
311
+ const env = c.environment ? ` [${c.environment}]` : "";
312
+ console.log(` ${c.name}${active}${env}`);
313
+ console.log(` context: ${c.context}`);
314
+ if (c.kubeConfigPath) {
315
+ console.log(` kubeconfig: ${c.kubeConfigPath}`);
316
+ }
317
+ }
318
+ }
319
+ break;
320
+ }
321
+ case "use": {
322
+ if (!args.clusterName) {
323
+ console.error("Usage: triagent cluster use <name>");
324
+ process.exit(1);
325
+ }
326
+
327
+ const success = await clusterManager.setActiveCluster(args.clusterName);
328
+ if (!success) {
329
+ console.error(`Cluster not found: ${args.clusterName}`);
330
+ process.exit(1);
331
+ }
332
+
333
+ // Save to config
334
+ config.activeCluster = args.clusterName;
335
+ await saveStoredConfig(config);
336
+
337
+ console.log(`✅ Active cluster set to: ${args.clusterName}`);
338
+ break;
339
+ }
340
+ case "status": {
341
+ const clusterName = args.clusterName;
342
+ if (clusterName) {
343
+ const status = await clusterManager.checkClusterStatus(clusterName);
344
+ console.log(`Cluster: ${status.name}`);
345
+ console.log(` Connected: ${status.connected ? "✅ Yes" : "❌ No"}`);
346
+ if (status.connected) {
347
+ console.log(` Version: ${status.version}`);
348
+ console.log(` Nodes: ${status.nodeCount}`);
349
+ }
350
+ if (status.error) {
351
+ console.log(` Error: ${status.error}`);
352
+ }
353
+ } else {
354
+ const clusters = clusterManager.listClusters();
355
+ if (clusters.length === 0) {
356
+ console.log("No clusters configured.");
357
+ } else {
358
+ console.log("Cluster status:\n");
359
+ for (const c of clusters) {
360
+ const status = await clusterManager.checkClusterStatus(c.name);
361
+ const active = c.isActive ? " (active)" : "";
362
+ const connected = status.connected ? "✅" : "❌";
363
+ console.log(` ${connected} ${c.name}${active}`);
364
+ if (status.connected) {
365
+ console.log(` v${status.version}, ${status.nodeCount} nodes`);
366
+ } else if (status.error) {
367
+ console.log(` Error: ${status.error}`);
368
+ }
369
+ }
370
+ }
371
+ }
372
+ break;
373
+ }
374
+ default:
375
+ console.error("Usage: triagent cluster <add|remove|list|use|status> [options]");
376
+ process.exit(1);
377
+ }
378
+ }
379
+
188
380
  async function handleConfigCommand(args: CliArgs): Promise<void> {
189
381
  const validKeys: (keyof StoredConfig)[] = [
190
382
  "aiProvider",
@@ -273,6 +465,12 @@ async function main(): Promise<void> {
273
465
  process.exit(0);
274
466
  }
275
467
 
468
+ // Handle cluster command
469
+ if (args.command === "cluster") {
470
+ await handleClusterCommand(args);
471
+ process.exit(0);
472
+ }
473
+
276
474
  // Load configuration
277
475
  let config;
278
476
  try {
@@ -0,0 +1,210 @@
1
+ import type { ElasticsearchConfig } from "../../cli/config.js";
2
+
3
+ export interface ESSearchResult {
4
+ hits: {
5
+ total: { value: number; relation: string };
6
+ hits: Array<{
7
+ _index: string;
8
+ _id: string;
9
+ _source: Record<string, unknown>;
10
+ sort?: unknown[];
11
+ }>;
12
+ };
13
+ aggregations?: Record<string, unknown>;
14
+ }
15
+
16
+ export interface ESLogEntry {
17
+ timestamp: string;
18
+ message: string;
19
+ level?: string;
20
+ pod?: string;
21
+ namespace?: string;
22
+ container?: string;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ export class ElasticsearchClient {
27
+ private baseUrl: string;
28
+ private index: string;
29
+ private apiKey?: string;
30
+
31
+ constructor(config: ElasticsearchConfig) {
32
+ this.baseUrl = config.url.replace(/\/$/, "");
33
+ this.index = config.index;
34
+ this.apiKey = config.auth?.apiKey;
35
+ }
36
+
37
+ private async request<T>(
38
+ endpoint: string,
39
+ options?: Omit<RequestInit, 'body'> & { body?: unknown }
40
+ ): Promise<T> {
41
+ const headers: Record<string, string> = {
42
+ "Accept": "application/json",
43
+ "Content-Type": "application/json",
44
+ };
45
+
46
+ if (this.apiKey) {
47
+ headers["Authorization"] = `ApiKey ${this.apiKey}`;
48
+ }
49
+
50
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
51
+ ...options,
52
+ headers: { ...headers, ...options?.headers },
53
+ body: options?.body ? JSON.stringify(options.body) : undefined,
54
+ });
55
+
56
+ if (!response.ok) {
57
+ const text = await response.text();
58
+ throw new Error(`Elasticsearch API error: ${response.status} ${text}`);
59
+ }
60
+
61
+ return response.json() as Promise<T>;
62
+ }
63
+
64
+ async search(options: {
65
+ query: string;
66
+ timeRange?: { start: string; end?: string };
67
+ limit?: number;
68
+ sort?: "asc" | "desc";
69
+ }): Promise<ESLogEntry[]> {
70
+ const { query, timeRange, limit = 100, sort = "desc" } = options;
71
+
72
+ const must: unknown[] = [
73
+ {
74
+ query_string: {
75
+ query,
76
+ default_field: "message",
77
+ },
78
+ },
79
+ ];
80
+
81
+ if (timeRange) {
82
+ const range: Record<string, string> = { gte: timeRange.start };
83
+ if (timeRange.end) {
84
+ range.lte = timeRange.end;
85
+ }
86
+ must.push({
87
+ range: {
88
+ "@timestamp": range,
89
+ },
90
+ });
91
+ }
92
+
93
+ const body = {
94
+ query: {
95
+ bool: { must },
96
+ },
97
+ size: limit,
98
+ sort: [{ "@timestamp": { order: sort } }],
99
+ };
100
+
101
+ const result = await this.request<ESSearchResult>(`/${this.index}/_search`, {
102
+ method: "POST",
103
+ body,
104
+ });
105
+
106
+ return result.hits.hits.map((hit) => {
107
+ const source = hit._source as Record<string, unknown>;
108
+ const k8s = source.kubernetes as Record<string, unknown> | undefined;
109
+ const k8sPod = k8s?.pod as Record<string, unknown> | undefined;
110
+ const k8sContainer = k8s?.container as Record<string, unknown> | undefined;
111
+ return {
112
+ timestamp: (source["@timestamp"] as string) || new Date().toISOString(),
113
+ message: (source.message as string) || JSON.stringify(source),
114
+ level: source.level as string | undefined,
115
+ pod: (k8sPod?.name || source.pod) as string | undefined,
116
+ namespace: (k8s?.namespace || source.namespace) as string | undefined,
117
+ container: (k8sContainer?.name || source.container) as string | undefined,
118
+ ...source,
119
+ };
120
+ });
121
+ }
122
+
123
+ async aggregate(options: {
124
+ query: string;
125
+ field: string;
126
+ timeRange?: { start: string; end?: string };
127
+ interval?: string;
128
+ }): Promise<Array<{ key: string; count: number }>> {
129
+ const { query, field, timeRange, interval = "1m" } = options;
130
+
131
+ const must: unknown[] = [
132
+ {
133
+ query_string: {
134
+ query,
135
+ default_field: "message",
136
+ },
137
+ },
138
+ ];
139
+
140
+ if (timeRange) {
141
+ const range: Record<string, string> = { gte: timeRange.start };
142
+ if (timeRange.end) {
143
+ range.lte = timeRange.end;
144
+ }
145
+ must.push({
146
+ range: {
147
+ "@timestamp": range,
148
+ },
149
+ });
150
+ }
151
+
152
+ const body = {
153
+ query: {
154
+ bool: { must },
155
+ },
156
+ size: 0,
157
+ aggs: {
158
+ by_field: {
159
+ terms: { field, size: 50 },
160
+ },
161
+ over_time: {
162
+ date_histogram: {
163
+ field: "@timestamp",
164
+ fixed_interval: interval,
165
+ },
166
+ },
167
+ },
168
+ };
169
+
170
+ const result = await this.request<{
171
+ aggregations: {
172
+ by_field: { buckets: Array<{ key: string; doc_count: number }> };
173
+ over_time: { buckets: Array<{ key_as_string: string; doc_count: number }> };
174
+ };
175
+ }>(`/${this.index}/_search`, {
176
+ method: "POST",
177
+ body,
178
+ });
179
+
180
+ return result.aggregations.by_field.buckets.map((bucket) => ({
181
+ key: bucket.key,
182
+ count: bucket.doc_count,
183
+ }));
184
+ }
185
+
186
+ formatLogs(logs: ESLogEntry[]): string {
187
+ const lines: string[] = [];
188
+ for (const log of logs) {
189
+ const level = log.level ? `[${log.level.toUpperCase()}]` : "";
190
+ const source = log.pod ? `${log.namespace}/${log.pod}` : "";
191
+ const prefix = [log.timestamp, level, source].filter(Boolean).join(" ");
192
+ lines.push(`${prefix}: ${log.message}`);
193
+ }
194
+ return lines.join("\n");
195
+ }
196
+ }
197
+
198
+ // Singleton instance
199
+ let esClient: ElasticsearchClient | null = null;
200
+
201
+ export function getElasticsearchClient(): ElasticsearchClient | null {
202
+ return esClient;
203
+ }
204
+
205
+ export function initElasticsearchClient(config?: ElasticsearchConfig): ElasticsearchClient | null {
206
+ if (config) {
207
+ esClient = new ElasticsearchClient(config);
208
+ }
209
+ return esClient;
210
+ }