@kalera/munin-runtime 1.2.5 → 1.2.8
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/.turbo/turbo-build.log +1 -1
- package/dist/env.d.ts +3 -0
- package/dist/env.js +44 -13
- package/dist/index.d.ts +2 -2
- package/dist/index.js +58 -38
- package/dist/mcp-server.d.ts +3 -1
- package/dist/mcp-server.js +5 -2
- package/package.json +2 -2
- package/src/env.ts +55 -13
- package/src/index.ts +59 -37
- package/src/mcp-server.ts +11 -2
package/.turbo/turbo-build.log
CHANGED
package/dist/env.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { parseCliArgs, loadCliEnv, resolveProjectId, executeWithRetry, safeError, } from "./index.js";
|
|
2
|
+
export type { ParsedCliArgs, CliEnv } from "./index.js";
|
|
1
3
|
export interface EnvVar {
|
|
2
4
|
key: string;
|
|
3
5
|
value: string;
|
|
@@ -10,5 +12,6 @@ export declare function resolveEncryptionKey(): string | undefined;
|
|
|
10
12
|
* Upsert key=value pairs into a .env file.
|
|
11
13
|
* - Existing keys are replaced.
|
|
12
14
|
* - New keys are appended.
|
|
15
|
+
* - Keys are matched via string split (not regex) to prevent injection.
|
|
13
16
|
*/
|
|
14
17
|
export declare function writeEnvFile(filename: string, vars: EnvVar[], cwd?: string): void;
|
package/dist/env.js
CHANGED
|
@@ -1,34 +1,65 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
+
// Re-export all shared utilities from index.ts (single source of truth)
|
|
4
|
+
export { parseCliArgs, loadCliEnv, resolveProjectId, executeWithRetry, safeError, } from "./index.js";
|
|
3
5
|
/**
|
|
4
6
|
* Resolve the encryption key for E2EE projects from MUNIN_ENCRYPTION_KEY env var.
|
|
5
7
|
*/
|
|
6
8
|
export function resolveEncryptionKey() {
|
|
7
9
|
return process.env.MUNIN_ENCRYPTION_KEY;
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Escape shell/metacharacters that could inject into a .env value.
|
|
13
|
+
* Escapes: $, backticks, \, and newlines — in addition to double quotes.
|
|
14
|
+
*/
|
|
15
|
+
function escapeEnvValue(value) {
|
|
16
|
+
return value
|
|
17
|
+
.replace(/\\/g, "\\\\") // escape backslashes first
|
|
18
|
+
.replace(/\n/g, "\\n") // escape newlines
|
|
19
|
+
.replace(/\r/g, "\\r") // escape carriage returns
|
|
20
|
+
.replace(/`/g, "\\`") // escape backticks
|
|
21
|
+
.replace(/\$/g, "\\$") // escape dollar signs
|
|
22
|
+
.replace(/"/g, '\\"'); // escape double quotes
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a .env line, returning { key, value } or null if not a valid assignment.
|
|
26
|
+
* Uses a string-based split instead of regex to avoid injection from .env content.
|
|
27
|
+
*/
|
|
28
|
+
function parseEnvLine(line) {
|
|
29
|
+
// Skip empty lines and comments
|
|
30
|
+
const trimmed = line.trim();
|
|
31
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
32
|
+
return null;
|
|
33
|
+
const eqIndex = trimmed.indexOf("=");
|
|
34
|
+
if (eqIndex === -1)
|
|
35
|
+
return null;
|
|
36
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
37
|
+
// Value is everything after the first '=' (unquoted, no regex)
|
|
38
|
+
const rawValue = trimmed.slice(eqIndex + 1);
|
|
39
|
+
return { key, value: rawValue };
|
|
40
|
+
}
|
|
9
41
|
/**
|
|
10
42
|
* Upsert key=value pairs into a .env file.
|
|
11
43
|
* - Existing keys are replaced.
|
|
12
44
|
* - New keys are appended.
|
|
45
|
+
* - Keys are matched via string split (not regex) to prevent injection.
|
|
13
46
|
*/
|
|
14
47
|
export function writeEnvFile(filename, vars, cwd = process.cwd()) {
|
|
15
48
|
const filePath = path.resolve(cwd, filename);
|
|
16
|
-
|
|
49
|
+
const lines = [];
|
|
17
50
|
if (fs.existsSync(filePath)) {
|
|
18
|
-
|
|
51
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
52
|
+
for (const line of content.split("\n")) {
|
|
53
|
+
const parsed = parseEnvLine(line);
|
|
54
|
+
// Remove lines that match any key we're about to upsert
|
|
55
|
+
if (parsed && vars.some((v) => v.key === parsed.key))
|
|
56
|
+
continue;
|
|
57
|
+
lines.push(line);
|
|
58
|
+
}
|
|
19
59
|
}
|
|
20
60
|
for (const { key, value } of vars) {
|
|
21
|
-
const escapedValue = value
|
|
22
|
-
|
|
23
|
-
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24
|
-
const regex = new RegExp(`^${escapedKey}\\s*=.+$`, "m");
|
|
25
|
-
const idx = lines.findIndex((l) => regex.test(l));
|
|
26
|
-
if (idx !== -1) {
|
|
27
|
-
lines[idx] = newLine;
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
lines.push(newLine);
|
|
31
|
-
}
|
|
61
|
+
const escapedValue = escapeEnvValue(value);
|
|
62
|
+
lines.push(`${key}="${escapedValue}"`);
|
|
32
63
|
}
|
|
33
64
|
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
|
34
65
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export declare function loadCliEnv(): CliEnv;
|
|
|
14
14
|
/**
|
|
15
15
|
* Resolve MUNIN_PROJECT from multiple sources, in priority order:
|
|
16
16
|
* 1. Explicit environment variable
|
|
17
|
-
* 2. .env.local in
|
|
18
|
-
* 3. .env in
|
|
17
|
+
* 2. .env.local in any ancestor directory (walk-up from CWD)
|
|
18
|
+
* 3. .env in any ancestor directory (walk-up from CWD)
|
|
19
19
|
*/
|
|
20
20
|
export declare function resolveProjectId(): string | undefined;
|
|
21
21
|
export declare function executeWithRetry<T>(task: () => Promise<T>, retries: number, backoffMs: number): Promise<T>;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
const REDACT_KEYS = ["apiKey", "authorization", "token", "secret", "password"];
|
|
2
4
|
export function parseCliArgs(argv, usage) {
|
|
3
5
|
const [action, payloadRaw] = argv;
|
|
@@ -21,68 +23,86 @@ export function loadCliEnv() {
|
|
|
21
23
|
return {
|
|
22
24
|
baseUrl: process.env.MUNIN_BASE_URL || "https://munin.kalera.dev",
|
|
23
25
|
apiKey: process.env.MUNIN_API_KEY,
|
|
24
|
-
timeoutMs:
|
|
25
|
-
retries:
|
|
26
|
-
backoffMs:
|
|
26
|
+
timeoutMs: safeParseInt(process.env.MUNIN_TIMEOUT_MS, 15_000),
|
|
27
|
+
retries: safeParseInt(process.env.MUNIN_RETRIES, 3),
|
|
28
|
+
backoffMs: safeParseInt(process.env.MUNIN_BACKOFF_MS, 300),
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
function safeParseInt(envVal, defaultVal) {
|
|
32
|
+
if (envVal === undefined)
|
|
33
|
+
return defaultVal;
|
|
34
|
+
const parsed = Number(envVal);
|
|
35
|
+
return isNaN(parsed) ? defaultVal : parsed;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Walk up the directory tree from `startDir` (default CWD) toward root,
|
|
39
|
+
* searching each ancestor for `filename`. Returns the value of the first match.
|
|
40
|
+
* Stops at filesystem root or when a `.git` dir is found (project boundary).
|
|
41
|
+
*/
|
|
42
|
+
function resolveEnvFileUpward(filename, startDir) {
|
|
43
|
+
let current = startDir ?? process.cwd();
|
|
44
|
+
let last = "";
|
|
45
|
+
while (current !== last) {
|
|
46
|
+
last = current;
|
|
47
|
+
const filePath = path.join(current, filename);
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(filePath)) {
|
|
50
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
51
|
+
for (const line of content.split("\n")) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed.startsWith("MUNIN_PROJECT="))
|
|
54
|
+
continue;
|
|
55
|
+
return trimmed.slice("MUNIN_PROJECT=".length).trim();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error.code !== "ENOENT") {
|
|
61
|
+
console.error(`[munin-runtime] Failed to read ${filePath}:`, error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Stop at git root (project boundary)
|
|
65
|
+
if (fs.existsSync(path.join(current, ".git")))
|
|
66
|
+
break;
|
|
67
|
+
current = path.dirname(current);
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
29
71
|
/**
|
|
30
72
|
* Resolve MUNIN_PROJECT from multiple sources, in priority order:
|
|
31
73
|
* 1. Explicit environment variable
|
|
32
|
-
* 2. .env.local in
|
|
33
|
-
* 3. .env in
|
|
74
|
+
* 2. .env.local in any ancestor directory (walk-up from CWD)
|
|
75
|
+
* 3. .env in any ancestor directory (walk-up from CWD)
|
|
34
76
|
*/
|
|
35
77
|
export function resolveProjectId() {
|
|
36
78
|
// 1. Explicit env var (highest priority)
|
|
37
79
|
if (process.env.MUNIN_PROJECT) {
|
|
38
80
|
return process.env.MUNIN_PROJECT;
|
|
39
81
|
}
|
|
40
|
-
// 2. .env.local in
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
return
|
|
44
|
-
// 3. .env in
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
return
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Read a .env file and extract MUNIN_PROJECT value.
|
|
52
|
-
* Returns undefined if file doesn't exist or value not found.
|
|
53
|
-
*/
|
|
54
|
-
function resolveEnvFile(filename) {
|
|
55
|
-
try {
|
|
56
|
-
const path = `${process.cwd()}/${filename}`;
|
|
57
|
-
const fs = require("fs");
|
|
58
|
-
if (!fs.existsSync(path))
|
|
59
|
-
return undefined;
|
|
60
|
-
const content = fs.readFileSync(path, "utf8");
|
|
61
|
-
const match = content.match(/^MUNIN_PROJECT\s*=\s*(.+)$/m);
|
|
62
|
-
if (match) {
|
|
63
|
-
return match[1].trim();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// Ignore errors — file might be unreadable
|
|
68
|
-
}
|
|
82
|
+
// 2. Walk upward from CWD — find .env.local in any ancestor dir
|
|
83
|
+
const fromLocal = resolveEnvFileUpward(".env.local");
|
|
84
|
+
if (fromLocal)
|
|
85
|
+
return fromLocal;
|
|
86
|
+
// 3. Walk upward from CWD — find .env in any ancestor dir
|
|
87
|
+
const fromEnv = resolveEnvFileUpward(".env");
|
|
88
|
+
if (fromEnv)
|
|
89
|
+
return fromEnv;
|
|
69
90
|
return undefined;
|
|
70
91
|
}
|
|
71
92
|
export async function executeWithRetry(task, retries, backoffMs) {
|
|
72
93
|
let attempt = 0;
|
|
73
94
|
let lastError;
|
|
74
|
-
while (attempt
|
|
95
|
+
while (attempt < retries) {
|
|
75
96
|
try {
|
|
76
97
|
return await task();
|
|
77
98
|
}
|
|
78
99
|
catch (error) {
|
|
79
100
|
lastError = error;
|
|
80
|
-
|
|
101
|
+
attempt += 1;
|
|
102
|
+
if (attempt >= retries)
|
|
81
103
|
break;
|
|
82
|
-
}
|
|
83
104
|
const jitter = Math.floor(Math.random() * 100);
|
|
84
105
|
await sleep(backoffMs * 2 ** attempt + jitter);
|
|
85
|
-
attempt += 1;
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
throw lastError;
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { loadCliEnv } from "./index.js";
|
|
3
|
-
export declare function createMcpServerInstance(env: ReturnType<typeof loadCliEnv
|
|
3
|
+
export declare function createMcpServerInstance(env: ReturnType<typeof loadCliEnv>, opts?: {
|
|
4
|
+
allowMissingApiKey?: boolean;
|
|
5
|
+
}): Server<{
|
|
4
6
|
method: string;
|
|
5
7
|
params?: {
|
|
6
8
|
[x: string]: unknown;
|
package/dist/mcp-server.js
CHANGED
|
@@ -3,10 +3,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { MuninClient } from "@kalera/munin-sdk";
|
|
5
5
|
import { loadCliEnv, resolveProjectId, resolveEncryptionKey, safeError } from "./index.js";
|
|
6
|
-
export function createMcpServerInstance(env) {
|
|
6
|
+
export function createMcpServerInstance(env, opts) {
|
|
7
|
+
if (!env.apiKey && !opts?.allowMissingApiKey) {
|
|
8
|
+
throw new Error("MUNIN_API_KEY is required. Set it in your environment or .env file.");
|
|
9
|
+
}
|
|
7
10
|
const client = new MuninClient({
|
|
8
11
|
baseUrl: env.baseUrl,
|
|
9
|
-
apiKey: env.apiKey
|
|
12
|
+
apiKey: env.apiKey,
|
|
10
13
|
timeoutMs: env.timeoutMs,
|
|
11
14
|
});
|
|
12
15
|
const server = new Server({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kalera/munin-runtime",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
15
|
-
"@kalera/munin-sdk": "1.2.
|
|
15
|
+
"@kalera/munin-sdk": "1.2.8"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build": "tsc -p tsconfig.json",
|
package/src/env.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
|
|
4
|
+
// Re-export all shared utilities from index.ts (single source of truth)
|
|
5
|
+
export {
|
|
6
|
+
parseCliArgs,
|
|
7
|
+
loadCliEnv,
|
|
8
|
+
resolveProjectId,
|
|
9
|
+
executeWithRetry,
|
|
10
|
+
safeError,
|
|
11
|
+
} from "./index.js";
|
|
12
|
+
|
|
13
|
+
export type { ParsedCliArgs, CliEnv } from "./index.js";
|
|
14
|
+
|
|
4
15
|
export interface EnvVar {
|
|
5
16
|
key: string;
|
|
6
17
|
value: string;
|
|
@@ -13,10 +24,44 @@ export function resolveEncryptionKey(): string | undefined {
|
|
|
13
24
|
return process.env.MUNIN_ENCRYPTION_KEY;
|
|
14
25
|
}
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Escape shell/metacharacters that could inject into a .env value.
|
|
29
|
+
* Escapes: $, backticks, \, and newlines — in addition to double quotes.
|
|
30
|
+
*/
|
|
31
|
+
function escapeEnvValue(value: string): string {
|
|
32
|
+
return value
|
|
33
|
+
.replace(/\\/g, "\\\\") // escape backslashes first
|
|
34
|
+
.replace(/\n/g, "\\n") // escape newlines
|
|
35
|
+
.replace(/\r/g, "\\r") // escape carriage returns
|
|
36
|
+
.replace(/`/g, "\\`") // escape backticks
|
|
37
|
+
.replace(/\$/g, "\\$") // escape dollar signs
|
|
38
|
+
.replace(/"/g, '\\"'); // escape double quotes
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse a .env line, returning { key, value } or null if not a valid assignment.
|
|
43
|
+
* Uses a string-based split instead of regex to avoid injection from .env content.
|
|
44
|
+
*/
|
|
45
|
+
function parseEnvLine(line: string): EnvVar | null {
|
|
46
|
+
// Skip empty lines and comments
|
|
47
|
+
const trimmed = line.trim();
|
|
48
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
49
|
+
|
|
50
|
+
const eqIndex = trimmed.indexOf("=");
|
|
51
|
+
if (eqIndex === -1) return null;
|
|
52
|
+
|
|
53
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
54
|
+
// Value is everything after the first '=' (unquoted, no regex)
|
|
55
|
+
const rawValue = trimmed.slice(eqIndex + 1);
|
|
56
|
+
|
|
57
|
+
return { key, value: rawValue };
|
|
58
|
+
}
|
|
59
|
+
|
|
16
60
|
/**
|
|
17
61
|
* Upsert key=value pairs into a .env file.
|
|
18
62
|
* - Existing keys are replaced.
|
|
19
63
|
* - New keys are appended.
|
|
64
|
+
* - Keys are matched via string split (not regex) to prevent injection.
|
|
20
65
|
*/
|
|
21
66
|
export function writeEnvFile(
|
|
22
67
|
filename: string,
|
|
@@ -24,24 +69,21 @@ export function writeEnvFile(
|
|
|
24
69
|
cwd: string = process.cwd(),
|
|
25
70
|
): void {
|
|
26
71
|
const filePath = path.resolve(cwd, filename);
|
|
27
|
-
|
|
72
|
+
const lines: string[] = [];
|
|
28
73
|
|
|
29
74
|
if (fs.existsSync(filePath)) {
|
|
30
|
-
|
|
75
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
76
|
+
for (const line of content.split("\n")) {
|
|
77
|
+
const parsed = parseEnvLine(line);
|
|
78
|
+
// Remove lines that match any key we're about to upsert
|
|
79
|
+
if (parsed && vars.some((v) => v.key === parsed.key)) continue;
|
|
80
|
+
lines.push(line);
|
|
81
|
+
}
|
|
31
82
|
}
|
|
32
83
|
|
|
33
84
|
for (const { key, value } of vars) {
|
|
34
|
-
const escapedValue = value
|
|
35
|
-
|
|
36
|
-
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37
|
-
const regex = new RegExp(`^${escapedKey}\\s*=.+$`, "m");
|
|
38
|
-
|
|
39
|
-
const idx = lines.findIndex((l) => regex.test(l));
|
|
40
|
-
if (idx !== -1) {
|
|
41
|
-
lines[idx] = newLine;
|
|
42
|
-
} else {
|
|
43
|
-
lines.push(newLine);
|
|
44
|
-
}
|
|
85
|
+
const escapedValue = escapeEnvValue(value);
|
|
86
|
+
lines.push(`${key}="${escapedValue}"`);
|
|
45
87
|
}
|
|
46
88
|
|
|
47
89
|
fs.writeFileSync(filePath, lines.join("\n"), "utf8");
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
1
4
|
export interface ParsedCliArgs {
|
|
2
5
|
action: string;
|
|
3
6
|
payload: Record<string, unknown>;
|
|
@@ -37,17 +40,58 @@ export function loadCliEnv(): CliEnv {
|
|
|
37
40
|
return {
|
|
38
41
|
baseUrl: process.env.MUNIN_BASE_URL || "https://munin.kalera.dev",
|
|
39
42
|
apiKey: process.env.MUNIN_API_KEY,
|
|
40
|
-
timeoutMs:
|
|
41
|
-
retries:
|
|
42
|
-
backoffMs:
|
|
43
|
+
timeoutMs: safeParseInt(process.env.MUNIN_TIMEOUT_MS, 15_000),
|
|
44
|
+
retries: safeParseInt(process.env.MUNIN_RETRIES, 3),
|
|
45
|
+
backoffMs: safeParseInt(process.env.MUNIN_BACKOFF_MS, 300),
|
|
43
46
|
};
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function safeParseInt(envVal: string | undefined, defaultVal: number): number {
|
|
50
|
+
if (envVal === undefined) return defaultVal;
|
|
51
|
+
const parsed = Number(envVal);
|
|
52
|
+
return isNaN(parsed) ? defaultVal : parsed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Walk up the directory tree from `startDir` (default CWD) toward root,
|
|
57
|
+
* searching each ancestor for `filename`. Returns the value of the first match.
|
|
58
|
+
* Stops at filesystem root or when a `.git` dir is found (project boundary).
|
|
59
|
+
*/
|
|
60
|
+
function resolveEnvFileUpward(filename: string, startDir?: string): string | undefined {
|
|
61
|
+
let current = startDir ?? process.cwd();
|
|
62
|
+
let last = "";
|
|
63
|
+
|
|
64
|
+
while (current !== last) {
|
|
65
|
+
last = current;
|
|
66
|
+
const filePath = path.join(current, filename);
|
|
67
|
+
try {
|
|
68
|
+
if (fs.existsSync(filePath)) {
|
|
69
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
70
|
+
for (const line of content.split("\n")) {
|
|
71
|
+
const trimmed = line.trim();
|
|
72
|
+
if (!trimmed.startsWith("MUNIN_PROJECT=")) continue;
|
|
73
|
+
return trimmed.slice("MUNIN_PROJECT=".length).trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
78
|
+
console.error(`[munin-runtime] Failed to read ${filePath}:`, error);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Stop at git root (project boundary)
|
|
83
|
+
if (fs.existsSync(path.join(current, ".git"))) break;
|
|
84
|
+
|
|
85
|
+
current = path.dirname(current);
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
46
90
|
/**
|
|
47
91
|
* Resolve MUNIN_PROJECT from multiple sources, in priority order:
|
|
48
92
|
* 1. Explicit environment variable
|
|
49
|
-
* 2. .env.local in
|
|
50
|
-
* 3. .env in
|
|
93
|
+
* 2. .env.local in any ancestor directory (walk-up from CWD)
|
|
94
|
+
* 3. .env in any ancestor directory (walk-up from CWD)
|
|
51
95
|
*/
|
|
52
96
|
export function resolveProjectId(): string | undefined {
|
|
53
97
|
// 1. Explicit env var (highest priority)
|
|
@@ -55,38 +99,17 @@ export function resolveProjectId(): string | undefined {
|
|
|
55
99
|
return process.env.MUNIN_PROJECT;
|
|
56
100
|
}
|
|
57
101
|
|
|
58
|
-
// 2. .env.local in
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
102
|
+
// 2. Walk upward from CWD — find .env.local in any ancestor dir
|
|
103
|
+
const fromLocal = resolveEnvFileUpward(".env.local");
|
|
104
|
+
if (fromLocal) return fromLocal;
|
|
61
105
|
|
|
62
|
-
// 3. .env in
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
106
|
+
// 3. Walk upward from CWD — find .env in any ancestor dir
|
|
107
|
+
const fromEnv = resolveEnvFileUpward(".env");
|
|
108
|
+
if (fromEnv) return fromEnv;
|
|
65
109
|
|
|
66
110
|
return undefined;
|
|
67
111
|
}
|
|
68
112
|
|
|
69
|
-
/**
|
|
70
|
-
* Read a .env file and extract MUNIN_PROJECT value.
|
|
71
|
-
* Returns undefined if file doesn't exist or value not found.
|
|
72
|
-
*/
|
|
73
|
-
function resolveEnvFile(filename: string): string | undefined {
|
|
74
|
-
try {
|
|
75
|
-
const path = `${process.cwd()}/${filename}`;
|
|
76
|
-
const fs = require("fs") as typeof import("fs");
|
|
77
|
-
if (!fs.existsSync(path)) return undefined;
|
|
78
|
-
|
|
79
|
-
const content = fs.readFileSync(path, "utf8");
|
|
80
|
-
const match = content.match(/^MUNIN_PROJECT\s*=\s*(.+)$/m);
|
|
81
|
-
if (match) {
|
|
82
|
-
return match[1].trim();
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
// Ignore errors — file might be unreadable
|
|
86
|
-
}
|
|
87
|
-
return undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
113
|
export async function executeWithRetry<T>(
|
|
91
114
|
task: () => Promise<T>,
|
|
92
115
|
retries: number,
|
|
@@ -95,17 +118,15 @@ export async function executeWithRetry<T>(
|
|
|
95
118
|
let attempt = 0;
|
|
96
119
|
let lastError: unknown;
|
|
97
120
|
|
|
98
|
-
while (attempt
|
|
121
|
+
while (attempt < retries) {
|
|
99
122
|
try {
|
|
100
123
|
return await task();
|
|
101
124
|
} catch (error) {
|
|
102
125
|
lastError = error;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
126
|
+
attempt += 1;
|
|
127
|
+
if (attempt >= retries) break;
|
|
106
128
|
const jitter = Math.floor(Math.random() * 100);
|
|
107
129
|
await sleep(backoffMs * 2 ** attempt + jitter);
|
|
108
|
-
attempt += 1;
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
132
|
|
|
@@ -133,5 +154,6 @@ function redactText(text: string): string {
|
|
|
133
154
|
function sleep(ms: number): Promise<void> {
|
|
134
155
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
156
|
}
|
|
157
|
+
|
|
136
158
|
export * from "./mcp-server.js";
|
|
137
159
|
export * from "./env.js";
|
package/src/mcp-server.ts
CHANGED
|
@@ -7,10 +7,19 @@ import {
|
|
|
7
7
|
import { MuninClient } from "@kalera/munin-sdk";
|
|
8
8
|
import { loadCliEnv, resolveProjectId, resolveEncryptionKey, safeError } from "./index.js";
|
|
9
9
|
|
|
10
|
-
export function createMcpServerInstance(
|
|
10
|
+
export function createMcpServerInstance(
|
|
11
|
+
env: ReturnType<typeof loadCliEnv>,
|
|
12
|
+
opts?: { allowMissingApiKey?: boolean },
|
|
13
|
+
) {
|
|
14
|
+
if (!env.apiKey && !opts?.allowMissingApiKey) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"MUNIN_API_KEY is required. Set it in your environment or .env file.",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
const client = new MuninClient({
|
|
12
21
|
baseUrl: env.baseUrl,
|
|
13
|
-
apiKey: env.apiKey
|
|
22
|
+
apiKey: env.apiKey,
|
|
14
23
|
timeoutMs: env.timeoutMs,
|
|
15
24
|
});
|
|
16
25
|
|