@havesomecode/kibana-mcp-server 1.1.2 → 1.2.1
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/README.md +72 -18
- package/dist/src/bootstrap.js +426 -0
- package/dist/src/cli.js +172 -9
- package/dist/src/client_registration.js +197 -0
- package/dist/src/config.js +43 -4
- package/dist/src/kibana_client.js +280 -92
- package/dist/src/profile_store.js +20 -2
- package/dist/src/schema_catalog.js +20 -6
- package/dist/src/secret_store.js +42 -31
- package/dist/src/server.js +86 -14
- package/dist/src/setup_flow.js +48 -36
- package/dist/src/source_catalog.js +3 -0
- package/dist/src/state_lock.js +370 -0
- package/dist/src/tools/configure_index.js +104 -0
- package/dist/src/tools/describe_fields.js +2 -2
- package/dist/src/tools/discover.js +5 -0
- package/dist/src/tools/filter.js +1 -0
- package/dist/src/tools/query.js +5 -2
- package/docs/project/compatibility-matrix.md +2 -1
- package/docs/project/distribution-strategy.md +17 -4
- package/package.json +2 -1
- package/plugins/kibana-log-investigation/.codex-plugin/plugin.json +3 -3
- package/skills/kibana-log-investigation/SKILL.md +99 -0
package/dist/src/secret_store.js
CHANGED
|
@@ -36,15 +36,8 @@ class MacOsSecretStore {
|
|
|
36
36
|
return parseStoredSecret(result.stdout);
|
|
37
37
|
}
|
|
38
38
|
async save(profileId, secret) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"-s",
|
|
42
|
-
this.serviceName,
|
|
43
|
-
"-a",
|
|
44
|
-
profileId,
|
|
45
|
-
"-w",
|
|
46
|
-
serializeSecret(secret),
|
|
47
|
-
]);
|
|
39
|
+
const serialized = serializeSecret(secret);
|
|
40
|
+
await this.execute("add-generic-password", ["-U", "-s", this.serviceName, "-a", profileId, "-w"], `${serialized}\n${serialized}\n`);
|
|
48
41
|
}
|
|
49
42
|
async delete(profileId) {
|
|
50
43
|
try {
|
|
@@ -57,9 +50,9 @@ class MacOsSecretStore {
|
|
|
57
50
|
throw error;
|
|
58
51
|
}
|
|
59
52
|
}
|
|
60
|
-
async execute(subcommand, args) {
|
|
53
|
+
async execute(subcommand, args, stdin) {
|
|
61
54
|
try {
|
|
62
|
-
const result = await this.runner("security", [subcommand, ...args]);
|
|
55
|
+
const result = await this.runner("security", [subcommand, ...args], { stdin });
|
|
63
56
|
if (result.exitCode === 44) {
|
|
64
57
|
throw new SecretStoreError(`No saved credentials found for profile '${args.at(-1)}'.`, "NOT_FOUND");
|
|
65
58
|
}
|
|
@@ -110,7 +103,10 @@ class LinuxSecretStore {
|
|
|
110
103
|
const result = await this.runner("secret-tool", [command, ...args], { stdin });
|
|
111
104
|
if (result.exitCode !== 0) {
|
|
112
105
|
const stderr = result.stderr.trim().toLowerCase();
|
|
113
|
-
|
|
106
|
+
const missingWithoutMessage = (command === "lookup" || command === "clear") &&
|
|
107
|
+
result.exitCode === 1 &&
|
|
108
|
+
stderr.length === 0;
|
|
109
|
+
if (stderr.includes("not found") || missingWithoutMessage) {
|
|
114
110
|
throw new SecretStoreError(`No saved credentials found for profile '${args.at(-1) ?? "unknown"}'.`, "NOT_FOUND");
|
|
115
111
|
}
|
|
116
112
|
throw new SecretStoreError(result.stderr.trim() || "Secret Service command failed.", "OPERATION_FAILED");
|
|
@@ -133,42 +129,45 @@ class WindowsSecretStore {
|
|
|
133
129
|
const result = await this.execute(`
|
|
134
130
|
[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
|
|
135
131
|
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
132
|
+
$payload = ConvertFrom-Json ([Console]::In.ReadToEnd())
|
|
136
133
|
try {
|
|
137
|
-
$credential = $vault.Retrieve($
|
|
134
|
+
$credential = $vault.Retrieve($payload.serviceName, $payload.profileId)
|
|
138
135
|
$credential.RetrievePassword()
|
|
139
136
|
[Console]::Out.Write($credential.Password)
|
|
140
137
|
exit 0
|
|
141
138
|
} catch {
|
|
142
139
|
exit 3
|
|
143
140
|
}
|
|
144
|
-
`,
|
|
141
|
+
`, profileId);
|
|
145
142
|
return parseStoredSecret(result.stdout);
|
|
146
143
|
}
|
|
147
144
|
async save(profileId, secret) {
|
|
148
145
|
await this.execute(`
|
|
149
146
|
[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
|
|
150
147
|
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
148
|
+
$payload = ConvertFrom-Json ([Console]::In.ReadToEnd())
|
|
151
149
|
try {
|
|
152
|
-
$existing = $vault.Retrieve($
|
|
150
|
+
$existing = $vault.Retrieve($payload.serviceName, $payload.profileId)
|
|
153
151
|
$vault.Remove($existing)
|
|
154
152
|
} catch {}
|
|
155
|
-
$credential = New-Object Windows.Security.Credentials.PasswordCredential($
|
|
153
|
+
$credential = New-Object Windows.Security.Credentials.PasswordCredential($payload.serviceName, $payload.profileId, $payload.secret)
|
|
156
154
|
$vault.Add($credential)
|
|
157
|
-
`,
|
|
155
|
+
`, profileId, secret);
|
|
158
156
|
}
|
|
159
157
|
async delete(profileId) {
|
|
160
158
|
try {
|
|
161
159
|
await this.execute(`
|
|
162
160
|
[void][Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime]
|
|
163
161
|
$vault = New-Object Windows.Security.Credentials.PasswordVault
|
|
162
|
+
$payload = ConvertFrom-Json ([Console]::In.ReadToEnd())
|
|
164
163
|
try {
|
|
165
|
-
$credential = $vault.Retrieve($
|
|
164
|
+
$credential = $vault.Retrieve($payload.serviceName, $payload.profileId)
|
|
166
165
|
$vault.Remove($credential)
|
|
167
166
|
exit 0
|
|
168
167
|
} catch {
|
|
169
168
|
exit 3
|
|
170
169
|
}
|
|
171
|
-
`,
|
|
170
|
+
`, profileId);
|
|
172
171
|
}
|
|
173
172
|
catch (error) {
|
|
174
173
|
if (error instanceof SecretStoreError && error.code === "NOT_FOUND") {
|
|
@@ -177,17 +176,16 @@ try {
|
|
|
177
176
|
throw error;
|
|
178
177
|
}
|
|
179
178
|
}
|
|
180
|
-
async execute(script,
|
|
179
|
+
async execute(script, profileId, secret) {
|
|
181
180
|
try {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
]);
|
|
181
|
+
const stdin = JSON.stringify({
|
|
182
|
+
serviceName: this.serviceName,
|
|
183
|
+
profileId,
|
|
184
|
+
...(secret ? { secret: serializeSecret(secret) } : {}),
|
|
185
|
+
});
|
|
186
|
+
const result = await this.runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script.trim()], { stdin });
|
|
189
187
|
if (result.exitCode === 3) {
|
|
190
|
-
throw new SecretStoreError(`No saved credentials found for profile '${
|
|
188
|
+
throw new SecretStoreError(`No saved credentials found for profile '${profileId}'.`, "NOT_FOUND");
|
|
191
189
|
}
|
|
192
190
|
if (result.exitCode !== 0) {
|
|
193
191
|
throw new SecretStoreError(result.stderr.trim() || "Windows Credential Manager command failed.", "OPERATION_FAILED");
|
|
@@ -237,6 +235,18 @@ async function runCommand(command, args, options = {}) {
|
|
|
237
235
|
});
|
|
238
236
|
let stdout = "";
|
|
239
237
|
let stderr = "";
|
|
238
|
+
let settled = false;
|
|
239
|
+
const finish = (callback) => {
|
|
240
|
+
if (settled)
|
|
241
|
+
return;
|
|
242
|
+
settled = true;
|
|
243
|
+
clearTimeout(timeout);
|
|
244
|
+
callback();
|
|
245
|
+
};
|
|
246
|
+
const timeout = setTimeout(() => {
|
|
247
|
+
child.kill("SIGTERM");
|
|
248
|
+
finish(() => reject(new Error("Credential-store command timed out after 30000ms.")));
|
|
249
|
+
}, 30000);
|
|
240
250
|
child.stdout.setEncoding("utf8");
|
|
241
251
|
child.stderr.setEncoding("utf8");
|
|
242
252
|
child.stdout.on("data", (chunk) => {
|
|
@@ -245,13 +255,14 @@ async function runCommand(command, args, options = {}) {
|
|
|
245
255
|
child.stderr.on("data", (chunk) => {
|
|
246
256
|
stderr += chunk;
|
|
247
257
|
});
|
|
248
|
-
child.on("error", reject);
|
|
258
|
+
child.on("error", (error) => finish(() => reject(error)));
|
|
259
|
+
child.stdin.on("error", (error) => finish(() => reject(error)));
|
|
249
260
|
child.on("close", (exitCode) => {
|
|
250
|
-
resolve({
|
|
261
|
+
finish(() => resolve({
|
|
251
262
|
stdout,
|
|
252
263
|
stderr,
|
|
253
264
|
exitCode: exitCode ?? 1,
|
|
254
|
-
});
|
|
265
|
+
}));
|
|
255
266
|
});
|
|
256
267
|
if (options.stdin) {
|
|
257
268
|
child.stdin.write(options.stdin);
|
package/dist/src/server.js
CHANGED
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
-
import { KibanaClient } from "./kibana_client.js";
|
|
2
|
+
import { KibanaClient, KibanaRequestError } from "./kibana_client.js";
|
|
3
3
|
import { SchemaCatalog } from "./schema_catalog.js";
|
|
4
4
|
import { SourceCatalog } from "./source_catalog.js";
|
|
5
5
|
import { configureInputSchema, configureOutputSchema, createConfigureCallToolResult, executeConfigure, } from "./tools/configure.js";
|
|
6
|
+
import { configureIndexInputSchema, configureIndexOutputSchema, createConfigureIndexCallToolResult, executeConfigureIndex, } from "./tools/configure_index.js";
|
|
6
7
|
import { createDescribeFieldsCallToolResult, describeFieldsInputSchema, describeFieldsOutputSchema, executeDescribeFields, } from "./tools/describe_fields.js";
|
|
7
8
|
import { createDiscoverCallToolResult, discoverInputSchema, discoverOutputSchema, executeDiscover, } from "./tools/discover.js";
|
|
8
9
|
import { createFilterCallToolResult, executeFilter, filterInputSchema, filterOutputSchema, } from "./tools/filter.js";
|
|
9
10
|
import { createQueryCallToolResult, executeQuery, queryInputSchema, queryOutputSchema, } from "./tools/query.js";
|
|
11
|
+
async function withKibanaRequestDiagnostics(operation) {
|
|
12
|
+
try {
|
|
13
|
+
return await operation();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (!(error instanceof KibanaRequestError)) {
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: error.message }],
|
|
21
|
+
isError: true,
|
|
22
|
+
_meta: {
|
|
23
|
+
kibana_request_error: {
|
|
24
|
+
code: error.code,
|
|
25
|
+
phase: error.phase,
|
|
26
|
+
source_id: error.sourceId,
|
|
27
|
+
timeout_ms: error.timeoutMs,
|
|
28
|
+
...(error.status === undefined ? {} : { status: error.status }),
|
|
29
|
+
...(error.causeCode === undefined ? {} : { cause_code: error.causeCode }),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
10
35
|
export function createApplication(initialConfig, dependencies) {
|
|
11
36
|
const server = new McpServer({
|
|
12
37
|
name: "kibana-log-investigation",
|
|
@@ -20,6 +45,24 @@ export function createApplication(initialConfig, dependencies) {
|
|
|
20
45
|
? (dependencies?.kibanaClient ?? kibanaClientFactory(activeConfig.kibana))
|
|
21
46
|
: null;
|
|
22
47
|
let schemaCatalog = kibanaClient ? new SchemaCatalog(kibanaClient) : null;
|
|
48
|
+
async function withKibanaToolDeadline(callerSignal, operation) {
|
|
49
|
+
const timeoutMs = activeConfig?.kibana.timeoutMs;
|
|
50
|
+
if (timeoutMs === undefined) {
|
|
51
|
+
throw new Error("Server is not configured. Call the 'configure' tool first.");
|
|
52
|
+
}
|
|
53
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
54
|
+
const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal;
|
|
55
|
+
try {
|
|
56
|
+
return await operation(signal);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (callerSignal?.aborted || !timeoutSignal.aborted) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
const requestError = error instanceof KibanaRequestError ? error : undefined;
|
|
63
|
+
throw new KibanaRequestError("KIBANA_TIMEOUT", requestError?.phase ?? "request", requestError?.sourceId, timeoutMs, `Kibana tool call exceeded its ${timeoutMs} ms backend deadline.`, error, requestError?.status, requestError?.causeCode);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
23
66
|
function requireConfigured() {
|
|
24
67
|
if (!sourceCatalog || !kibanaClient || !schemaCatalog) {
|
|
25
68
|
throw new Error("Server is not configured. Call the 'configure' tool first.");
|
|
@@ -41,24 +84,52 @@ export function createApplication(initialConfig, dependencies) {
|
|
|
41
84
|
schemaCatalog = new SchemaCatalog(kibanaClient);
|
|
42
85
|
return result;
|
|
43
86
|
};
|
|
44
|
-
const
|
|
87
|
+
const configureIndexHandler = async (input, callerSignal) => {
|
|
88
|
+
if (!activeConfig || !kibanaClient) {
|
|
89
|
+
throw new Error("No saved Kibana connection is available. Run connection bootstrap first.");
|
|
90
|
+
}
|
|
91
|
+
const configured = activeConfig;
|
|
92
|
+
const client = kibanaClient;
|
|
93
|
+
const configureIndexFn = dependencies?.configureIndexFn ?? executeConfigureIndex;
|
|
94
|
+
const { sources, result } = await withKibanaToolDeadline(callerSignal, (signal) => configureIndexFn(configureIndexInputSchema.parse(input), {
|
|
95
|
+
config: configured,
|
|
96
|
+
kibanaClient: client,
|
|
97
|
+
callerSignal: signal,
|
|
98
|
+
}));
|
|
99
|
+
activeConfig = { ...activeConfig, sources };
|
|
100
|
+
sourceCatalog = new SourceCatalog(sources);
|
|
101
|
+
schemaCatalog = new SchemaCatalog(client);
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
const describeFieldsHandler = (input, callerSignal) => withKibanaToolDeadline(callerSignal, (signal) => executeDescribeFields(describeFieldsInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().schemaCatalog, signal));
|
|
45
105
|
const discoverHandler = (input) => executeDiscover(discoverInputSchema.parse(input), requireConfigured().sourceCatalog);
|
|
46
|
-
const filterHandler = (input) => executeFilter(filterInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().kibanaClient, {
|
|
106
|
+
const filterHandler = (input, callerSignal) => withKibanaToolDeadline(callerSignal, (signal) => executeFilter(filterInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().kibanaClient, {
|
|
47
107
|
schemaCatalog: requireConfigured().schemaCatalog,
|
|
48
|
-
|
|
49
|
-
|
|
108
|
+
callerSignal: signal,
|
|
109
|
+
}));
|
|
110
|
+
const queryHandler = (input, callerSignal) => withKibanaToolDeadline(callerSignal, (signal) => executeQuery(queryInputSchema.parse(input), requireConfigured().sourceCatalog, requireConfigured().kibanaClient, {
|
|
50
111
|
schemaCatalog: requireConfigured().schemaCatalog,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
112
|
+
callerSignal: signal,
|
|
113
|
+
}));
|
|
114
|
+
if (!initialConfig) {
|
|
115
|
+
server.registerTool("configure", {
|
|
116
|
+
description: "Advanced legacy setup for a server started without a saved profile. Replaces the Kibana connection and full source catalog.",
|
|
117
|
+
inputSchema: configureInputSchema,
|
|
118
|
+
outputSchema: configureOutputSchema,
|
|
119
|
+
}, async (input) => createConfigureCallToolResult(await configureHandler(input)));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
server.registerTool("configure_index", {
|
|
123
|
+
description: "Configure exactly one Kibana index or index pattern after the user has explicitly named it. Never guess, list, scan, or auto-select indexes before calling this tool.",
|
|
124
|
+
inputSchema: configureIndexInputSchema,
|
|
125
|
+
outputSchema: configureIndexOutputSchema,
|
|
126
|
+
}, async (input, context) => withKibanaRequestDiagnostics(async () => createConfigureIndexCallToolResult(await configureIndexHandler(input, context.mcpReq.signal))));
|
|
127
|
+
}
|
|
57
128
|
server.registerTool("describe_fields", {
|
|
58
129
|
description: "Describe the effective field capabilities for a configured logical source.",
|
|
59
130
|
inputSchema: describeFieldsInputSchema,
|
|
60
131
|
outputSchema: describeFieldsOutputSchema,
|
|
61
|
-
}, async (input) => createDescribeFieldsCallToolResult(await describeFieldsHandler(input)));
|
|
132
|
+
}, async (input, context) => withKibanaRequestDiagnostics(async () => createDescribeFieldsCallToolResult(await describeFieldsHandler(input, context.mcpReq.signal))));
|
|
62
133
|
server.registerTool("discover", {
|
|
63
134
|
description: "List configured logical log sources and field hints for investigation work.",
|
|
64
135
|
inputSchema: discoverInputSchema,
|
|
@@ -68,16 +139,17 @@ export function createApplication(initialConfig, dependencies) {
|
|
|
68
139
|
description: "Run an exact-field filter when the field name is already known, bypassing alias resolution.",
|
|
69
140
|
inputSchema: filterInputSchema,
|
|
70
141
|
outputSchema: filterOutputSchema,
|
|
71
|
-
}, async (input) => createFilterCallToolResult(await filterHandler(input)));
|
|
142
|
+
}, async (input, context) => withKibanaRequestDiagnostics(async () => createFilterCallToolResult(await filterHandler(input, context.mcpReq.signal))));
|
|
72
143
|
server.registerTool("query", {
|
|
73
144
|
description: "Query one or more logical log sources over an absolute time window with text, filters, and aggregate modes.",
|
|
74
145
|
inputSchema: queryInputSchema,
|
|
75
146
|
outputSchema: queryOutputSchema,
|
|
76
|
-
}, async (input) => createQueryCallToolResult(await queryHandler(input)));
|
|
147
|
+
}, async (input, context) => withKibanaRequestDiagnostics(async () => createQueryCallToolResult(await queryHandler(input, context.mcpReq.signal))));
|
|
77
148
|
return {
|
|
78
149
|
server,
|
|
79
150
|
handlers: {
|
|
80
151
|
configure: configureHandler,
|
|
152
|
+
configure_index: configureIndexHandler,
|
|
81
153
|
describe_fields: describeFieldsHandler,
|
|
82
154
|
discover: discoverHandler,
|
|
83
155
|
filter: filterHandler,
|
package/dist/src/setup_flow.js
CHANGED
|
@@ -5,6 +5,7 @@ import { sourceCatalogSchema } from "./config.js";
|
|
|
5
5
|
import { resolveProfilePaths } from "./profile_paths.js";
|
|
6
6
|
import { ProfileStore, deriveProfileId } from "./profile_store.js";
|
|
7
7
|
import { SecretStoreError, createSecretStore } from "./secret_store.js";
|
|
8
|
+
import { acquireStateLock } from "./state_lock.js";
|
|
8
9
|
const DEFAULT_TIMEOUT_MS = 10000;
|
|
9
10
|
const KNOWN_ENDPOINT_PATHS = [
|
|
10
11
|
"/internal/search/es",
|
|
@@ -24,44 +25,52 @@ export async function runSetupFlow(prompter, dependencies = {}) {
|
|
|
24
25
|
await prompter.info("Guided setup will save one machine-level Kibana profile and reuse it in later threads.");
|
|
25
26
|
let continueAddingProfiles = true;
|
|
26
27
|
while (continueAddingProfiles) {
|
|
27
|
-
const
|
|
28
|
+
const stateBeforePrompts = await profileStore.load();
|
|
29
|
+
const existingProfiles = stateBeforePrompts.profiles;
|
|
28
30
|
const profileName = await promptForProfileName(prompter, existingProfiles.map((profile) => profile.name));
|
|
29
|
-
const existingProfile = existingProfiles.find((profile) => profile.name.toLowerCase() === profileName.toLowerCase());
|
|
30
|
-
const profileId = existingProfile?.id ??
|
|
31
|
-
deriveProfileId(profileName, existingProfiles.map((profile) => profile.id));
|
|
32
31
|
const baseUrl = await promptForBaseUrl(prompter);
|
|
33
32
|
const username = await promptForRequiredValue(prompter, "Kibana username");
|
|
34
33
|
const password = await promptForRequiredValue(prompter, "Kibana password", { secret: true });
|
|
35
34
|
const importPath = await promptForSourceCatalogPath(prompter, cwd);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
const makeDefault = existingProfiles.length === 0
|
|
36
|
+
? true
|
|
37
|
+
: await prompter.confirm(`Make '${profileName}' the default environment for new threads?`, false);
|
|
38
|
+
const releaseLock = await acquireStateLock(paths.stateRoot);
|
|
39
|
+
let profile;
|
|
40
|
+
let importedCatalogPath;
|
|
42
41
|
try {
|
|
43
|
-
await
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (error instanceof SecretStoreError) {
|
|
47
|
-
throw new Error(`Could not save Kibana credentials securely: ${error.message}`);
|
|
42
|
+
const currentState = await profileStore.load();
|
|
43
|
+
if (JSON.stringify(currentState) !== JSON.stringify(stateBeforePrompts)) {
|
|
44
|
+
throw new Error("Saved Kibana profiles changed while setup was collecting input. Re-run setup to avoid overwriting concurrent changes.");
|
|
48
45
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
const existingProfile = currentState.profiles.find((candidate) => candidate.name.toLowerCase() === profileName.toLowerCase());
|
|
47
|
+
const profileId = existingProfile?.id ??
|
|
48
|
+
deriveProfileId(profileName, currentState.profiles.map((candidate) => candidate.id));
|
|
49
|
+
importedCatalogPath = join(paths.sourceCatalogsDir, `${profileId}.json`);
|
|
50
|
+
await copyValidatedSourceCatalog(importPath, importedCatalogPath, {
|
|
51
|
+
readFile: readFileImpl,
|
|
52
|
+
writeFile: writeFileImpl,
|
|
53
|
+
mkdir: mkdirImpl,
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
await secretStore.save(profileId, { username, password });
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof SecretStoreError) {
|
|
60
|
+
throw new Error(`Could not save Kibana credentials securely: ${error.message}`);
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
64
63
|
}
|
|
64
|
+
profile = await profileStore.upsertProfile({
|
|
65
|
+
id: profileId,
|
|
66
|
+
name: profileName,
|
|
67
|
+
baseUrl,
|
|
68
|
+
timeoutMs: DEFAULT_TIMEOUT_MS,
|
|
69
|
+
sourceCatalogPath: importedCatalogPath,
|
|
70
|
+
}, { makeDefault });
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await releaseLock();
|
|
65
74
|
}
|
|
66
75
|
savedProfileNames.push(profile.name);
|
|
67
76
|
savedCatalogPaths.push(importedCatalogPath);
|
|
@@ -123,18 +132,21 @@ async function promptForRequiredValue(prompter, label, options = {}) {
|
|
|
123
132
|
}
|
|
124
133
|
}
|
|
125
134
|
async function promptForSourceCatalogPath(prompter, cwd) {
|
|
126
|
-
const bundledCatalogPath = await resolveBundledSourceCatalogPath();
|
|
127
135
|
while (true) {
|
|
128
|
-
const rawValue = (await prompter.prompt("Source catalog path to import"
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
const rawValue = (await prompter.prompt("Source catalog path to import (or type 'example')")).trim();
|
|
137
|
+
if (!rawValue) {
|
|
138
|
+
await prompter.info("A source catalog is required. Enter a JSON path or type 'example' explicitly.");
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const importPath = rawValue.toLowerCase() === "example"
|
|
142
|
+
? await resolveBundledSourceCatalogPath()
|
|
143
|
+
: resolve(cwd, rawValue);
|
|
132
144
|
try {
|
|
133
145
|
await access(importPath);
|
|
134
146
|
return importPath;
|
|
135
147
|
}
|
|
136
148
|
catch {
|
|
137
|
-
await prompter.info(`Source catalog not found at ${importPath}
|
|
149
|
+
await prompter.info(`Source catalog not found at ${importPath}.`);
|
|
138
150
|
}
|
|
139
151
|
}
|
|
140
152
|
}
|
|
@@ -5,6 +5,9 @@ export class SourceCatalog {
|
|
|
5
5
|
this.sources = sources;
|
|
6
6
|
this.sourceMap = new Map(sources.map((source) => [source.id, source]));
|
|
7
7
|
}
|
|
8
|
+
count() {
|
|
9
|
+
return this.sources.length;
|
|
10
|
+
}
|
|
8
11
|
list(query, limit = 20) {
|
|
9
12
|
const normalizedQuery = query?.trim().toLowerCase();
|
|
10
13
|
const filtered = normalizedQuery
|