aemeathcli 1.0.0
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 +607 -0
- package/dist/App-P4MYD4QY.js +2719 -0
- package/dist/App-P4MYD4QY.js.map +1 -0
- package/dist/api-key-fallback-YQQBOQIL.js +11 -0
- package/dist/api-key-fallback-YQQBOQIL.js.map +1 -0
- package/dist/chunk-4IJD72YB.js +184 -0
- package/dist/chunk-4IJD72YB.js.map +1 -0
- package/dist/chunk-6PDJ45T4.js +325 -0
- package/dist/chunk-6PDJ45T4.js.map +1 -0
- package/dist/chunk-CARHU3DO.js +562 -0
- package/dist/chunk-CARHU3DO.js.map +1 -0
- package/dist/chunk-CGEV3ARR.js +80 -0
- package/dist/chunk-CGEV3ARR.js.map +1 -0
- package/dist/chunk-CS5X3BWX.js +27 -0
- package/dist/chunk-CS5X3BWX.js.map +1 -0
- package/dist/chunk-CYQNBB25.js +44 -0
- package/dist/chunk-CYQNBB25.js.map +1 -0
- package/dist/chunk-DAHGLHNR.js +657 -0
- package/dist/chunk-DAHGLHNR.js.map +1 -0
- package/dist/chunk-H66O5Z2V.js +305 -0
- package/dist/chunk-H66O5Z2V.js.map +1 -0
- package/dist/chunk-HCIHOHLX.js +322 -0
- package/dist/chunk-HCIHOHLX.js.map +1 -0
- package/dist/chunk-HMJRPNPZ.js +1031 -0
- package/dist/chunk-HMJRPNPZ.js.map +1 -0
- package/dist/chunk-I5PZ4JTS.js +119 -0
- package/dist/chunk-I5PZ4JTS.js.map +1 -0
- package/dist/chunk-IYW62KKR.js +255 -0
- package/dist/chunk-IYW62KKR.js.map +1 -0
- package/dist/chunk-JAXXTYID.js +51 -0
- package/dist/chunk-JAXXTYID.js.map +1 -0
- package/dist/chunk-LSOYPSAT.js +183 -0
- package/dist/chunk-LSOYPSAT.js.map +1 -0
- package/dist/chunk-MFBHNWGV.js +416 -0
- package/dist/chunk-MFBHNWGV.js.map +1 -0
- package/dist/chunk-MXZSI3AY.js +311 -0
- package/dist/chunk-MXZSI3AY.js.map +1 -0
- package/dist/chunk-NBR3GHMT.js +72 -0
- package/dist/chunk-NBR3GHMT.js.map +1 -0
- package/dist/chunk-O3ZF22SW.js +246 -0
- package/dist/chunk-O3ZF22SW.js.map +1 -0
- package/dist/chunk-SUSJPZU2.js +181 -0
- package/dist/chunk-SUSJPZU2.js.map +1 -0
- package/dist/chunk-TEVZS4FA.js +310 -0
- package/dist/chunk-TEVZS4FA.js.map +1 -0
- package/dist/chunk-UY2SYSEZ.js +211 -0
- package/dist/chunk-UY2SYSEZ.js.map +1 -0
- package/dist/chunk-WAHVZH7V.js +260 -0
- package/dist/chunk-WAHVZH7V.js.map +1 -0
- package/dist/chunk-WPP3PEDE.js +234 -0
- package/dist/chunk-WPP3PEDE.js.map +1 -0
- package/dist/chunk-Y5XVD2CD.js +1610 -0
- package/dist/chunk-Y5XVD2CD.js.map +1 -0
- package/dist/chunk-YL5XFHR3.js +56 -0
- package/dist/chunk-YL5XFHR3.js.map +1 -0
- package/dist/chunk-ZGOHARPV.js +122 -0
- package/dist/chunk-ZGOHARPV.js.map +1 -0
- package/dist/claude-adapter-QMLFMSP3.js +6 -0
- package/dist/claude-adapter-QMLFMSP3.js.map +1 -0
- package/dist/claude-login-5WELXPKT.js +324 -0
- package/dist/claude-login-5WELXPKT.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +703 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-login-7HHLJHBF.js +164 -0
- package/dist/codex-login-7HHLJHBF.js.map +1 -0
- package/dist/config-store-W6FBCQAQ.js +6 -0
- package/dist/config-store-W6FBCQAQ.js.map +1 -0
- package/dist/executor-6RIKIGXK.js +4 -0
- package/dist/executor-6RIKIGXK.js.map +1 -0
- package/dist/gemini-adapter-6JIHZ7WI.js +6 -0
- package/dist/gemini-adapter-6JIHZ7WI.js.map +1 -0
- package/dist/gemini-login-ZZLYC3J6.js +346 -0
- package/dist/gemini-login-ZZLYC3J6.js.map +1 -0
- package/dist/index.d.ts +2210 -0
- package/dist/index.js +1419 -0
- package/dist/index.js.map +1 -0
- package/dist/kimi-adapter-JN4HFFHU.js +6 -0
- package/dist/kimi-adapter-JN4HFFHU.js.map +1 -0
- package/dist/kimi-login-CZPS63NK.js +149 -0
- package/dist/kimi-login-CZPS63NK.js.map +1 -0
- package/dist/native-cli-adapters-OLW3XX57.js +6 -0
- package/dist/native-cli-adapters-OLW3XX57.js.map +1 -0
- package/dist/ollama-adapter-OJQ3FKWK.js +6 -0
- package/dist/ollama-adapter-OJQ3FKWK.js.map +1 -0
- package/dist/openai-adapter-XU46EN7B.js +6 -0
- package/dist/openai-adapter-XU46EN7B.js.map +1 -0
- package/dist/registry-4KD24ZC3.js +6 -0
- package/dist/registry-4KD24ZC3.js.map +1 -0
- package/dist/registry-H7B3AHPQ.js +5 -0
- package/dist/registry-H7B3AHPQ.js.map +1 -0
- package/dist/server-manager-PTGBHCLS.js +5 -0
- package/dist/server-manager-PTGBHCLS.js.map +1 -0
- package/dist/session-manager-ECEEACGY.js +12 -0
- package/dist/session-manager-ECEEACGY.js.map +1 -0
- package/dist/team-manager-HC4XGCFY.js +11 -0
- package/dist/team-manager-HC4XGCFY.js.map +1 -0
- package/dist/tmux-manager-GPYZ3WQH.js +6 -0
- package/dist/tmux-manager-GPYZ3WQH.js.map +1 -0
- package/dist/tools-TSMXMHIF.js +6 -0
- package/dist/tools-TSMXMHIF.js.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import pino from 'pino';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { mkdirSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
// src/utils/logger.ts
|
|
7
|
+
var LOG_DIR = join(homedir(), ".aemeathcli", "logs");
|
|
8
|
+
function ensureLogDir() {
|
|
9
|
+
try {
|
|
10
|
+
mkdirSync(LOG_DIR, { recursive: true, mode: 448 });
|
|
11
|
+
} catch {
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
var REDACT_PATHS = [
|
|
15
|
+
"token",
|
|
16
|
+
"refreshToken",
|
|
17
|
+
"apiKey",
|
|
18
|
+
"accessToken",
|
|
19
|
+
"sessionToken",
|
|
20
|
+
"password",
|
|
21
|
+
"secret",
|
|
22
|
+
"authorization",
|
|
23
|
+
"cookie",
|
|
24
|
+
"*.token",
|
|
25
|
+
"*.refreshToken",
|
|
26
|
+
"*.apiKey",
|
|
27
|
+
"*.accessToken",
|
|
28
|
+
"*.sessionToken",
|
|
29
|
+
"*.password",
|
|
30
|
+
"*.secret"
|
|
31
|
+
];
|
|
32
|
+
ensureLogDir();
|
|
33
|
+
var logger = pino({
|
|
34
|
+
name: "aemeathcli",
|
|
35
|
+
level: process.env["AEMEATHCLI_LOG_LEVEL"] ?? "error",
|
|
36
|
+
redact: {
|
|
37
|
+
paths: REDACT_PATHS,
|
|
38
|
+
censor: "[REDACTED]"
|
|
39
|
+
},
|
|
40
|
+
...process.env["NODE_ENV"] === "development" ? {
|
|
41
|
+
transport: {
|
|
42
|
+
target: "pino/file",
|
|
43
|
+
options: { destination: join(LOG_DIR, "aemeathcli.log"), mkdir: true }
|
|
44
|
+
}
|
|
45
|
+
} : {},
|
|
46
|
+
timestamp: pino.stdTimeFunctions.isoTime
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export { logger };
|
|
50
|
+
//# sourceMappingURL=chunk-JAXXTYID.js.map
|
|
51
|
+
//# sourceMappingURL=chunk-JAXXTYID.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/logger.ts"],"names":[],"mappings":";;;;;;AAUA,IAAM,OAAA,GAAU,IAAA,CAAK,OAAA,EAAQ,EAAG,eAAe,MAAM,CAAA;AAErD,SAAS,YAAA,GAAqB;AAC5B,EAAA,IAAI;AACF,IAAA,SAAA,CAAU,SAAS,EAAE,SAAA,EAAW,IAAA,EAAM,IAAA,EAAM,KAAO,CAAA;AAAA,EACrD,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;AAGA,IAAM,YAAA,GAAe;AAAA,EACnB,OAAA;AAAA,EACA,cAAA;AAAA,EACA,QAAA;AAAA,EACA,aAAA;AAAA,EACA,cAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,eAAA;AAAA,EACA,QAAA;AAAA,EACA,SAAA;AAAA,EACA,gBAAA;AAAA,EACA,UAAA;AAAA,EACA,eAAA;AAAA,EACA,gBAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAA;AAEA,YAAA,EAAa;AAEb,IAAM,SAAS,IAAA,CAAK;AAAA,EAClB,IAAA,EAAM,YAAA;AAAA,EACN,KAAA,EAAO,OAAA,CAAQ,GAAA,CAAI,sBAAsB,CAAA,IAAK,OAAA;AAAA,EAC9C,MAAA,EAAQ;AAAA,IACN,KAAA,EAAO,YAAA;AAAA,IACP,MAAA,EAAQ;AAAA,GACV;AAAA,EACA,GAAI,OAAA,CAAQ,GAAA,CAAI,UAAU,MAAM,aAAA,GAC5B;AAAA,IACE,SAAA,EAAW;AAAA,MACT,MAAA,EAAQ,WAAA;AAAA,MACR,OAAA,EAAS,EAAE,WAAA,EAAa,IAAA,CAAK,SAAS,gBAAgB,CAAA,EAAG,OAAO,IAAA;AAAK;AACvE,MAEF,EAAC;AAAA,EACL,SAAA,EAAW,KAAK,gBAAA,CAAiB;AACnC,CAAC","file":"chunk-JAXXTYID.js","sourcesContent":["/**\n * Structured logging via pino per PRD section 16.1\n * Automatic credential redaction per PRD section 14.1\n */\n\nimport pino from \"pino\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { mkdirSync } from \"node:fs\";\n\nconst LOG_DIR = join(homedir(), \".aemeathcli\", \"logs\");\n\nfunction ensureLogDir(): void {\n try {\n mkdirSync(LOG_DIR, { recursive: true, mode: 0o700 });\n } catch {\n // Log dir creation failed — fall back to stderr only\n }\n}\n\n// Redaction patterns for credentials (PRD section 14.2 REQ-CRED-04)\nconst REDACT_PATHS = [\n \"token\",\n \"refreshToken\",\n \"apiKey\",\n \"accessToken\",\n \"sessionToken\",\n \"password\",\n \"secret\",\n \"authorization\",\n \"cookie\",\n \"*.token\",\n \"*.refreshToken\",\n \"*.apiKey\",\n \"*.accessToken\",\n \"*.sessionToken\",\n \"*.password\",\n \"*.secret\",\n];\n\nensureLogDir();\n\nconst logger = pino({\n name: \"aemeathcli\",\n level: process.env[\"AEMEATHCLI_LOG_LEVEL\"] ?? \"error\",\n redact: {\n paths: REDACT_PATHS,\n censor: \"[REDACTED]\",\n },\n ...(process.env[\"NODE_ENV\"] === \"development\"\n ? {\n transport: {\n target: \"pino/file\",\n options: { destination: join(LOG_DIR, \"aemeathcli.log\"), mkdir: true },\n },\n }\n : {}),\n timestamp: pino.stdTimeFunctions.isoTime,\n});\n\nexport { logger };\n"]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { logger } from './chunk-JAXXTYID.js';
|
|
2
|
+
|
|
3
|
+
// src/skills/executor.ts
|
|
4
|
+
var SkillExecutor = class {
|
|
5
|
+
registry;
|
|
6
|
+
activeSkill = null;
|
|
7
|
+
baseAllowedTools = null;
|
|
8
|
+
constructor(registry) {
|
|
9
|
+
this.registry = registry;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Activate a skill by name.
|
|
13
|
+
*/
|
|
14
|
+
async activateByName(name, modelCapabilities) {
|
|
15
|
+
const definition = await this.registry.getByName(name);
|
|
16
|
+
if (!definition) {
|
|
17
|
+
return { success: false, errorMessage: `Skill "${name}" not found` };
|
|
18
|
+
}
|
|
19
|
+
return this.activate(definition, modelCapabilities);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Activate a skill by trigger string (e.g. "$review" or "review").
|
|
23
|
+
*/
|
|
24
|
+
async activateByTrigger(trigger, modelCapabilities) {
|
|
25
|
+
const definition = await this.registry.getByTrigger(trigger);
|
|
26
|
+
if (!definition) {
|
|
27
|
+
return {
|
|
28
|
+
success: false,
|
|
29
|
+
errorMessage: `No skill found for trigger "${trigger}"`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return this.activate(definition, modelCapabilities);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Deactivate the currently active skill, restoring previous tool set.
|
|
36
|
+
*/
|
|
37
|
+
deactivate() {
|
|
38
|
+
if (!this.activeSkill) {
|
|
39
|
+
logger.debug("No active skill to deactivate");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const skillName = this.activeSkill.definition.frontmatter.name;
|
|
43
|
+
this.baseAllowedTools = this.activeSkill.previousAllowedTools;
|
|
44
|
+
this.activeSkill = null;
|
|
45
|
+
logger.info({ skill: skillName }, "Skill deactivated");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get the currently active skill, or null if none.
|
|
49
|
+
*/
|
|
50
|
+
getActiveSkill() {
|
|
51
|
+
return this.activeSkill;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if any skill is currently active.
|
|
55
|
+
*/
|
|
56
|
+
isActive() {
|
|
57
|
+
return this.activeSkill !== null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get the active skill's body content for context injection.
|
|
61
|
+
* Returns null if no skill is active.
|
|
62
|
+
*/
|
|
63
|
+
getActiveSkillContent() {
|
|
64
|
+
return this.activeSkill?.definition.body ?? null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the name of the currently active skill, or null.
|
|
68
|
+
*/
|
|
69
|
+
getActiveSkillName() {
|
|
70
|
+
return this.activeSkill?.definition.frontmatter.name ?? null;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get the allowed tool list under the current state.
|
|
74
|
+
* When a skill is active with allowed-tools, only those tools are permitted.
|
|
75
|
+
* Returns null when no restrictions are in effect.
|
|
76
|
+
*/
|
|
77
|
+
getAllowedTools() {
|
|
78
|
+
if (this.activeSkill) {
|
|
79
|
+
return this.activeSkill.definition.frontmatter["allowed-tools"] ?? null;
|
|
80
|
+
}
|
|
81
|
+
return this.baseAllowedTools;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check if a specific tool is allowed under the current skill restrictions.
|
|
85
|
+
*/
|
|
86
|
+
isToolAllowed(toolName) {
|
|
87
|
+
const allowed = this.getAllowedTools();
|
|
88
|
+
if (!allowed) return true;
|
|
89
|
+
return allowed.includes(toolName);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Set the base allowed tools (used when no skill is active).
|
|
93
|
+
*/
|
|
94
|
+
setBaseAllowedTools(tools) {
|
|
95
|
+
this.baseAllowedTools = tools;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validate model requirements for a skill definition.
|
|
99
|
+
* Returns compatibility status and any warnings.
|
|
100
|
+
*/
|
|
101
|
+
checkModelRequirements(definition, capabilities) {
|
|
102
|
+
const warnings = [];
|
|
103
|
+
const requirements = definition.frontmatter["model-requirements"];
|
|
104
|
+
if (!requirements) {
|
|
105
|
+
return { compatible: true, warnings: [] };
|
|
106
|
+
}
|
|
107
|
+
const preferredRole = requirements["preferred-role"];
|
|
108
|
+
if (preferredRole && capabilities.currentRole) {
|
|
109
|
+
if (capabilities.currentRole !== preferredRole) {
|
|
110
|
+
warnings.push(
|
|
111
|
+
`Skill "${definition.frontmatter.name}" prefers role "${preferredRole}", current role is "${capabilities.currentRole}"`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const minContext = requirements["min-context"];
|
|
116
|
+
if (minContext !== void 0 && minContext !== null) {
|
|
117
|
+
if (capabilities.contextWindow < minContext) {
|
|
118
|
+
warnings.push(
|
|
119
|
+
`Skill "${definition.frontmatter.name}" requires ${minContext} context tokens, but current model has ${capabilities.contextWindow}`
|
|
120
|
+
);
|
|
121
|
+
return { compatible: false, warnings };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { compatible: true, warnings };
|
|
125
|
+
}
|
|
126
|
+
// ── Private ───────────────────────────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Core activation logic. Deactivates any current skill first.
|
|
129
|
+
*/
|
|
130
|
+
activate(definition, modelCapabilities) {
|
|
131
|
+
const allWarnings = [];
|
|
132
|
+
if (modelCapabilities) {
|
|
133
|
+
const { compatible, warnings } = this.checkModelRequirements(
|
|
134
|
+
definition,
|
|
135
|
+
modelCapabilities
|
|
136
|
+
);
|
|
137
|
+
allWarnings.push(...warnings);
|
|
138
|
+
for (const warning of warnings) {
|
|
139
|
+
logger.warn({ skill: definition.frontmatter.name }, warning);
|
|
140
|
+
}
|
|
141
|
+
if (!compatible) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
skill: definition,
|
|
145
|
+
errorMessage: `Model does not meet skill requirements: ${warnings.join("; ")}`,
|
|
146
|
+
warnings: allWarnings
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (this.activeSkill) {
|
|
151
|
+
logger.info(
|
|
152
|
+
{
|
|
153
|
+
current: this.activeSkill.definition.frontmatter.name,
|
|
154
|
+
next: definition.frontmatter.name
|
|
155
|
+
},
|
|
156
|
+
"Switching active skill"
|
|
157
|
+
);
|
|
158
|
+
this.deactivate();
|
|
159
|
+
}
|
|
160
|
+
this.activeSkill = {
|
|
161
|
+
definition,
|
|
162
|
+
activatedAt: /* @__PURE__ */ new Date(),
|
|
163
|
+
previousAllowedTools: this.baseAllowedTools
|
|
164
|
+
};
|
|
165
|
+
logger.info(
|
|
166
|
+
{
|
|
167
|
+
skill: definition.frontmatter.name,
|
|
168
|
+
version: definition.frontmatter.version,
|
|
169
|
+
allowedTools: definition.frontmatter["allowed-tools"]
|
|
170
|
+
},
|
|
171
|
+
"Skill activated"
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
skill: definition,
|
|
176
|
+
...allWarnings.length > 0 ? { warnings: allWarnings } : {}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export { SkillExecutor };
|
|
182
|
+
//# sourceMappingURL=chunk-LSOYPSAT.js.map
|
|
183
|
+
//# sourceMappingURL=chunk-LSOYPSAT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/skills/executor.ts"],"names":[],"mappings":"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","file":"chunk-LSOYPSAT.js","sourcesContent":["/**\n * SkillExecutor — Manages skill activation, context injection, and tool restriction.\n * Per PRD section 10.3: Load full SKILL.md content into context on activation,\n * restrict tools, and unload on deactivation.\n */\n\nimport { logger } from \"../utils/logger.js\";\nimport type { ISkillDefinition } from \"../types/config.js\";\nimport type { ModelRole } from \"../types/model.js\";\nimport type { SkillRegistry } from \"./registry.js\";\n\n// ── Types ───────────────────────────────────────────────────────────────\n\nexport interface IActiveSkill {\n readonly definition: ISkillDefinition;\n readonly activatedAt: Date;\n readonly previousAllowedTools: readonly string[] | null;\n}\n\nexport interface ISkillActivationResult {\n readonly success: boolean;\n readonly skill?: ISkillDefinition;\n readonly errorMessage?: string;\n readonly warnings?: readonly string[];\n}\n\nexport interface IModelCapabilities {\n readonly contextWindow: number;\n readonly currentRole?: ModelRole;\n}\n\n// ── SkillExecutor Class ─────────────────────────────────────────────────\n\nexport class SkillExecutor {\n private readonly registry: SkillRegistry;\n private activeSkill: IActiveSkill | null = null;\n private baseAllowedTools: readonly string[] | null = null;\n\n constructor(registry: SkillRegistry) {\n this.registry = registry;\n }\n\n /**\n * Activate a skill by name.\n */\n async activateByName(\n name: string,\n modelCapabilities?: IModelCapabilities,\n ): Promise<ISkillActivationResult> {\n const definition = await this.registry.getByName(name);\n if (!definition) {\n return { success: false, errorMessage: `Skill \"${name}\" not found` };\n }\n\n return this.activate(definition, modelCapabilities);\n }\n\n /**\n * Activate a skill by trigger string (e.g. \"$review\" or \"review\").\n */\n async activateByTrigger(\n trigger: string,\n modelCapabilities?: IModelCapabilities,\n ): Promise<ISkillActivationResult> {\n const definition = await this.registry.getByTrigger(trigger);\n if (!definition) {\n return {\n success: false,\n errorMessage: `No skill found for trigger \"${trigger}\"`,\n };\n }\n\n return this.activate(definition, modelCapabilities);\n }\n\n /**\n * Deactivate the currently active skill, restoring previous tool set.\n */\n deactivate(): void {\n if (!this.activeSkill) {\n logger.debug(\"No active skill to deactivate\");\n return;\n }\n\n const skillName = this.activeSkill.definition.frontmatter.name;\n this.baseAllowedTools = this.activeSkill.previousAllowedTools;\n this.activeSkill = null;\n\n logger.info({ skill: skillName }, \"Skill deactivated\");\n }\n\n /**\n * Get the currently active skill, or null if none.\n */\n getActiveSkill(): IActiveSkill | null {\n return this.activeSkill;\n }\n\n /**\n * Check if any skill is currently active.\n */\n isActive(): boolean {\n return this.activeSkill !== null;\n }\n\n /**\n * Get the active skill's body content for context injection.\n * Returns null if no skill is active.\n */\n getActiveSkillContent(): string | null {\n return this.activeSkill?.definition.body ?? null;\n }\n\n /**\n * Get the name of the currently active skill, or null.\n */\n getActiveSkillName(): string | null {\n return this.activeSkill?.definition.frontmatter.name ?? null;\n }\n\n /**\n * Get the allowed tool list under the current state.\n * When a skill is active with allowed-tools, only those tools are permitted.\n * Returns null when no restrictions are in effect.\n */\n getAllowedTools(): readonly string[] | null {\n if (this.activeSkill) {\n return this.activeSkill.definition.frontmatter[\"allowed-tools\"] ?? null;\n }\n return this.baseAllowedTools;\n }\n\n /**\n * Check if a specific tool is allowed under the current skill restrictions.\n */\n isToolAllowed(toolName: string): boolean {\n const allowed = this.getAllowedTools();\n if (!allowed) return true;\n return allowed.includes(toolName);\n }\n\n /**\n * Set the base allowed tools (used when no skill is active).\n */\n setBaseAllowedTools(tools: readonly string[] | null): void {\n this.baseAllowedTools = tools;\n }\n\n /**\n * Validate model requirements for a skill definition.\n * Returns compatibility status and any warnings.\n */\n checkModelRequirements(\n definition: ISkillDefinition,\n capabilities: IModelCapabilities,\n ): { compatible: boolean; warnings: readonly string[] } {\n const warnings: string[] = [];\n const requirements = definition.frontmatter[\"model-requirements\"];\n\n if (!requirements) {\n return { compatible: true, warnings: [] };\n }\n\n const preferredRole = requirements[\"preferred-role\"];\n if (preferredRole && capabilities.currentRole) {\n if (capabilities.currentRole !== preferredRole) {\n warnings.push(\n `Skill \"${definition.frontmatter.name}\" prefers role \"${preferredRole}\", ` +\n `current role is \"${capabilities.currentRole}\"`,\n );\n }\n }\n\n const minContext = requirements[\"min-context\"];\n if (minContext !== undefined && minContext !== null) {\n if (capabilities.contextWindow < minContext) {\n warnings.push(\n `Skill \"${definition.frontmatter.name}\" requires ${minContext} context tokens, ` +\n `but current model has ${capabilities.contextWindow}`,\n );\n return { compatible: false, warnings };\n }\n }\n\n return { compatible: true, warnings };\n }\n\n // ── Private ───────────────────────────────────────────────────────────\n\n /**\n * Core activation logic. Deactivates any current skill first.\n */\n private activate(\n definition: ISkillDefinition,\n modelCapabilities?: IModelCapabilities,\n ): ISkillActivationResult {\n const allWarnings: string[] = [];\n\n // Validate model requirements if capabilities are provided\n if (modelCapabilities) {\n const { compatible, warnings } = this.checkModelRequirements(\n definition,\n modelCapabilities,\n );\n allWarnings.push(...warnings);\n\n for (const warning of warnings) {\n logger.warn({ skill: definition.frontmatter.name }, warning);\n }\n\n if (!compatible) {\n return {\n success: false,\n skill: definition,\n errorMessage: `Model does not meet skill requirements: ${warnings.join(\"; \")}`,\n warnings: allWarnings,\n };\n }\n }\n\n // Deactivate current skill if one is active\n if (this.activeSkill) {\n logger.info(\n {\n current: this.activeSkill.definition.frontmatter.name,\n next: definition.frontmatter.name,\n },\n \"Switching active skill\",\n );\n this.deactivate();\n }\n\n this.activeSkill = {\n definition,\n activatedAt: new Date(),\n previousAllowedTools: this.baseAllowedTools,\n };\n\n logger.info(\n {\n skill: definition.frontmatter.name,\n version: definition.frontmatter.version,\n allowedTools: definition.frontmatter[\"allowed-tools\"],\n },\n \"Skill activated\",\n );\n\n return {\n success: true,\n skill: definition,\n ...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),\n };\n }\n}\n"]}
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { ServerConnectionError, ToolCallError } from './chunk-ZGOHARPV.js';
|
|
2
|
+
import { logger } from './chunk-JAXXTYID.js';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
|
|
6
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7
|
+
var CLIENT_NAME = "aemeathcli";
|
|
8
|
+
var CLIENT_VERSION = "1.0.0";
|
|
9
|
+
function extractText(content) {
|
|
10
|
+
if (!Array.isArray(content)) {
|
|
11
|
+
return "No output";
|
|
12
|
+
}
|
|
13
|
+
const parts = [];
|
|
14
|
+
for (const item of content) {
|
|
15
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
16
|
+
parts.push(item.text);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return parts.length > 0 ? parts.join("\n") : "No output";
|
|
20
|
+
}
|
|
21
|
+
var MCPClient = class {
|
|
22
|
+
client;
|
|
23
|
+
connected = false;
|
|
24
|
+
serverName;
|
|
25
|
+
transportConfig;
|
|
26
|
+
connectionTimeoutMs;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.serverName = options.serverName;
|
|
29
|
+
this.transportConfig = options.transport;
|
|
30
|
+
this.connectionTimeoutMs = options.connectionTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
31
|
+
}
|
|
32
|
+
get isConnected() {
|
|
33
|
+
return this.connected;
|
|
34
|
+
}
|
|
35
|
+
get name() {
|
|
36
|
+
return this.serverName;
|
|
37
|
+
}
|
|
38
|
+
/** Establish connection to the MCP server. */
|
|
39
|
+
async connect() {
|
|
40
|
+
if (this.connected) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
logger.info({ server: this.serverName }, "Connecting to MCP server");
|
|
44
|
+
try {
|
|
45
|
+
this.client = new Client(
|
|
46
|
+
{ name: CLIENT_NAME, version: CLIENT_VERSION },
|
|
47
|
+
{ capabilities: { sampling: {} } }
|
|
48
|
+
);
|
|
49
|
+
await this.connectWithTimeout();
|
|
50
|
+
this.connected = true;
|
|
51
|
+
logger.info({ server: this.serverName }, "Connected to MCP server");
|
|
52
|
+
} catch (error) {
|
|
53
|
+
this.connected = false;
|
|
54
|
+
this.client = void 0;
|
|
55
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
56
|
+
throw new ServerConnectionError(this.serverName, msg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Gracefully close the MCP connection. */
|
|
60
|
+
async disconnect() {
|
|
61
|
+
if (!this.connected || !this.client) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
logger.info({ server: this.serverName }, "Disconnecting from MCP server");
|
|
65
|
+
try {
|
|
66
|
+
await this.client.close();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
logger.warn({ server: this.serverName, error }, "Error during MCP disconnect");
|
|
69
|
+
} finally {
|
|
70
|
+
this.connected = false;
|
|
71
|
+
this.client = void 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Disconnect then reconnect to the server. */
|
|
75
|
+
async reconnect() {
|
|
76
|
+
await this.disconnect();
|
|
77
|
+
await this.connect();
|
|
78
|
+
}
|
|
79
|
+
/** List all tools exposed by this MCP server. */
|
|
80
|
+
async listTools() {
|
|
81
|
+
const client = this.requireConnected();
|
|
82
|
+
try {
|
|
83
|
+
const result = await client.listTools();
|
|
84
|
+
return result.tools.map((tool) => ({
|
|
85
|
+
name: tool.name,
|
|
86
|
+
description: tool.description ?? "",
|
|
87
|
+
inputSchema: tool.inputSchema ?? {}
|
|
88
|
+
}));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
91
|
+
throw new ServerConnectionError(this.serverName, `listTools failed: ${msg}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Call a tool on this MCP server with the given arguments. */
|
|
95
|
+
async callTool(toolName, args) {
|
|
96
|
+
const client = this.requireConnected();
|
|
97
|
+
try {
|
|
98
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
99
|
+
return {
|
|
100
|
+
content: extractText(result.content),
|
|
101
|
+
isError: result.isError === true
|
|
102
|
+
};
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
105
|
+
throw new ToolCallError(toolName, msg);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** List resources exposed by this MCP server. */
|
|
109
|
+
async listResources() {
|
|
110
|
+
const client = this.requireConnected();
|
|
111
|
+
try {
|
|
112
|
+
const result = await client.listResources();
|
|
113
|
+
return result.resources.map((r) => ({
|
|
114
|
+
uri: r.uri,
|
|
115
|
+
name: r.name ?? r.uri,
|
|
116
|
+
description: "",
|
|
117
|
+
mimeType: r.mimeType ?? "application/octet-stream"
|
|
118
|
+
}));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
121
|
+
throw new ServerConnectionError(
|
|
122
|
+
this.serverName,
|
|
123
|
+
`listResources failed: ${msg}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Read a specific resource by URI. */
|
|
128
|
+
async readResource(uri) {
|
|
129
|
+
const client = this.requireConnected();
|
|
130
|
+
try {
|
|
131
|
+
const result = await client.readResource({ uri });
|
|
132
|
+
return extractText(result.contents);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
135
|
+
throw new ServerConnectionError(
|
|
136
|
+
this.serverName,
|
|
137
|
+
`readResource(${uri}) failed: ${msg}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ── Private Helpers ─────────────────────────────────────────────────
|
|
142
|
+
async connectWithTimeout() {
|
|
143
|
+
const client = this.client;
|
|
144
|
+
if (!client) {
|
|
145
|
+
throw new Error("Client not initialised");
|
|
146
|
+
}
|
|
147
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
148
|
+
setTimeout(
|
|
149
|
+
() => reject(
|
|
150
|
+
new Error(
|
|
151
|
+
`Connection timed out after ${this.connectionTimeoutMs}ms`
|
|
152
|
+
)
|
|
153
|
+
),
|
|
154
|
+
this.connectionTimeoutMs
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
if (this.transportConfig.type === "stdio") {
|
|
158
|
+
const transport = new StdioClientTransport({
|
|
159
|
+
command: this.transportConfig.command,
|
|
160
|
+
args: [...this.transportConfig.args],
|
|
161
|
+
...this.transportConfig.env !== void 0 ? { env: { ...this.transportConfig.env } } : {}
|
|
162
|
+
});
|
|
163
|
+
await Promise.race([client.connect(transport), timeoutPromise]);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (this.transportConfig.type === "streamable-http") {
|
|
167
|
+
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
168
|
+
const transport = new StreamableHTTPClientTransport(
|
|
169
|
+
new URL(this.transportConfig.url)
|
|
170
|
+
);
|
|
171
|
+
await Promise.race([client.connect(transport), timeoutPromise]);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const _exhaustive = this.transportConfig;
|
|
175
|
+
throw new ServerConnectionError(
|
|
176
|
+
this.serverName,
|
|
177
|
+
`Unknown transport type: ${JSON.stringify(_exhaustive)}`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
requireConnected() {
|
|
181
|
+
if (!this.connected || !this.client) {
|
|
182
|
+
throw new ServerConnectionError(this.serverName, "Not connected");
|
|
183
|
+
}
|
|
184
|
+
return this.client;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/mcp/server-manager.ts
|
|
189
|
+
var DEFAULT_HEALTH_INTERVAL_MS = 6e4;
|
|
190
|
+
var DEFAULT_MAX_FAILURES = 3;
|
|
191
|
+
var DEFAULT_MAX_CALLS_PER_MINUTE = 60;
|
|
192
|
+
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
193
|
+
var MCPServerManager = class {
|
|
194
|
+
servers = /* @__PURE__ */ new Map();
|
|
195
|
+
options;
|
|
196
|
+
healthCheckTimer;
|
|
197
|
+
constructor(options) {
|
|
198
|
+
this.options = {
|
|
199
|
+
connectionTimeoutMs: options?.connectionTimeoutMs ?? 3e4,
|
|
200
|
+
healthCheckIntervalMs: options?.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS,
|
|
201
|
+
maxConsecutiveFailures: options?.maxConsecutiveFailures ?? DEFAULT_MAX_FAILURES,
|
|
202
|
+
defaultRateLimit: options?.defaultRateLimit,
|
|
203
|
+
rateLimits: options?.rateLimits
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Start all servers defined in the MCP config. */
|
|
207
|
+
async startAll(config) {
|
|
208
|
+
const entries = Object.entries(config.mcpServers);
|
|
209
|
+
if (entries.length === 0) {
|
|
210
|
+
logger.info("No MCP servers configured");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
logger.info({ count: entries.length }, "Starting MCP servers");
|
|
214
|
+
const results = await Promise.allSettled(
|
|
215
|
+
entries.map(([name, serverConfig]) => this.startServer(name, serverConfig))
|
|
216
|
+
);
|
|
217
|
+
let successCount = 0;
|
|
218
|
+
for (const result of results) {
|
|
219
|
+
if (result.status === "fulfilled") {
|
|
220
|
+
successCount++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
logger.info(
|
|
224
|
+
{ total: entries.length, connected: successCount },
|
|
225
|
+
"MCP server startup complete"
|
|
226
|
+
);
|
|
227
|
+
this.startHealthChecks();
|
|
228
|
+
}
|
|
229
|
+
/** Start a single MCP server by name and config. */
|
|
230
|
+
async startServer(name, serverConfig) {
|
|
231
|
+
if (this.servers.has(name)) {
|
|
232
|
+
logger.warn({ server: name }, "MCP server already registered, stopping first");
|
|
233
|
+
await this.stopServer(name);
|
|
234
|
+
}
|
|
235
|
+
logger.info({ server: name, command: serverConfig.command }, "Starting MCP server");
|
|
236
|
+
const transportConfig = {
|
|
237
|
+
type: "stdio",
|
|
238
|
+
command: serverConfig.command,
|
|
239
|
+
args: [...serverConfig.args],
|
|
240
|
+
...serverConfig.env !== void 0 ? { env: { ...serverConfig.env } } : {}
|
|
241
|
+
};
|
|
242
|
+
const clientOptions = {
|
|
243
|
+
serverName: name,
|
|
244
|
+
transport: transportConfig,
|
|
245
|
+
connectionTimeoutMs: this.options.connectionTimeoutMs
|
|
246
|
+
};
|
|
247
|
+
const client = new MCPClient(clientOptions);
|
|
248
|
+
const maxCalls = this.getMaxCallsPerMinute(name);
|
|
249
|
+
const entry = {
|
|
250
|
+
client,
|
|
251
|
+
status: "connecting",
|
|
252
|
+
lastHealthCheck: Date.now(),
|
|
253
|
+
consecutiveFailures: 0,
|
|
254
|
+
rateLimit: { callTimestamps: [], maxCallsPerMinute: maxCalls }
|
|
255
|
+
};
|
|
256
|
+
this.servers.set(name, entry);
|
|
257
|
+
try {
|
|
258
|
+
await client.connect();
|
|
259
|
+
entry.status = "connected";
|
|
260
|
+
logger.info({ server: name }, "MCP server connected");
|
|
261
|
+
} catch (error) {
|
|
262
|
+
entry.status = "error";
|
|
263
|
+
entry.consecutiveFailures = 1;
|
|
264
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
265
|
+
logger.error({ server: name, error: msg }, "Failed to start MCP server");
|
|
266
|
+
throw new ServerConnectionError(name, msg);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/** Stop a single server by name. */
|
|
270
|
+
async stopServer(name) {
|
|
271
|
+
const entry = this.servers.get(name);
|
|
272
|
+
if (!entry) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
logger.info({ server: name }, "Stopping MCP server");
|
|
276
|
+
try {
|
|
277
|
+
await entry.client.disconnect();
|
|
278
|
+
} catch (error) {
|
|
279
|
+
logger.warn({ server: name, error }, "Error stopping MCP server");
|
|
280
|
+
}
|
|
281
|
+
entry.status = "stopped";
|
|
282
|
+
this.servers.delete(name);
|
|
283
|
+
}
|
|
284
|
+
/** Stop all managed servers and clean up. */
|
|
285
|
+
async stopAll() {
|
|
286
|
+
this.stopHealthChecks();
|
|
287
|
+
const names = [...this.servers.keys()];
|
|
288
|
+
logger.info({ count: names.length }, "Stopping all MCP servers");
|
|
289
|
+
await Promise.allSettled(names.map((name) => this.stopServer(name)));
|
|
290
|
+
}
|
|
291
|
+
/** Get a connected client by server name. */
|
|
292
|
+
getClient(name) {
|
|
293
|
+
const entry = this.servers.get(name);
|
|
294
|
+
if (!entry || entry.status !== "connected") {
|
|
295
|
+
return void 0;
|
|
296
|
+
}
|
|
297
|
+
return entry.client;
|
|
298
|
+
}
|
|
299
|
+
/** Get all connected server names. */
|
|
300
|
+
getConnectedServers() {
|
|
301
|
+
const connected = [];
|
|
302
|
+
for (const [name, entry] of this.servers) {
|
|
303
|
+
if (entry.status === "connected") {
|
|
304
|
+
connected.push(name);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return connected;
|
|
308
|
+
}
|
|
309
|
+
/** Get the status of a server. */
|
|
310
|
+
getServerStatus(name) {
|
|
311
|
+
return this.servers.get(name)?.status;
|
|
312
|
+
}
|
|
313
|
+
/** List tools from a specific server (with rate-limit check). */
|
|
314
|
+
async listServerTools(name) {
|
|
315
|
+
const entry = this.requireServer(name);
|
|
316
|
+
this.checkRateLimit(entry, name);
|
|
317
|
+
return entry.client.listTools();
|
|
318
|
+
}
|
|
319
|
+
/** Check rate limit before allowing a call to the given server. Throws on exceeded. */
|
|
320
|
+
checkRateLimitFor(name) {
|
|
321
|
+
const entry = this.requireServer(name);
|
|
322
|
+
this.checkRateLimit(entry, name);
|
|
323
|
+
}
|
|
324
|
+
/** Record a tool call for rate-limiting purposes. */
|
|
325
|
+
recordCall(name) {
|
|
326
|
+
const entry = this.servers.get(name);
|
|
327
|
+
if (entry) {
|
|
328
|
+
entry.rateLimit.callTimestamps.push(Date.now());
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// ── Health Checking ─────────────────────────────────────────────────
|
|
332
|
+
startHealthChecks() {
|
|
333
|
+
this.stopHealthChecks();
|
|
334
|
+
this.healthCheckTimer = setInterval(() => {
|
|
335
|
+
void this.runHealthChecks();
|
|
336
|
+
}, this.options.healthCheckIntervalMs);
|
|
337
|
+
if (typeof this.healthCheckTimer === "object" && "unref" in this.healthCheckTimer) {
|
|
338
|
+
this.healthCheckTimer.unref();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
stopHealthChecks() {
|
|
342
|
+
if (this.healthCheckTimer !== void 0) {
|
|
343
|
+
clearInterval(this.healthCheckTimer);
|
|
344
|
+
this.healthCheckTimer = void 0;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async runHealthChecks() {
|
|
348
|
+
for (const [name, entry] of this.servers) {
|
|
349
|
+
if (entry.status === "stopped") {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
await entry.client.listTools();
|
|
354
|
+
entry.status = "connected";
|
|
355
|
+
entry.consecutiveFailures = 0;
|
|
356
|
+
entry.lastHealthCheck = Date.now();
|
|
357
|
+
} catch {
|
|
358
|
+
entry.consecutiveFailures++;
|
|
359
|
+
entry.status = "error";
|
|
360
|
+
logger.warn(
|
|
361
|
+
{ server: name, failures: entry.consecutiveFailures },
|
|
362
|
+
"MCP health check failed"
|
|
363
|
+
);
|
|
364
|
+
if (entry.consecutiveFailures >= this.options.maxConsecutiveFailures) {
|
|
365
|
+
logger.info({ server: name }, "Attempting MCP server restart");
|
|
366
|
+
void this.restartServer(name, entry);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async restartServer(name, entry) {
|
|
372
|
+
try {
|
|
373
|
+
await entry.client.reconnect();
|
|
374
|
+
entry.status = "connected";
|
|
375
|
+
entry.consecutiveFailures = 0;
|
|
376
|
+
logger.info({ server: name }, "MCP server restarted successfully");
|
|
377
|
+
} catch (error) {
|
|
378
|
+
entry.status = "error";
|
|
379
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
380
|
+
logger.error({ server: name, error: msg }, "MCP server restart failed");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ── Rate Limiting ─────────────────────────────────────────────────
|
|
384
|
+
checkRateLimit(entry, serverName) {
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
const windowStart = now - RATE_LIMIT_WINDOW_MS;
|
|
387
|
+
entry.rateLimit.callTimestamps = entry.rateLimit.callTimestamps.filter(
|
|
388
|
+
(ts) => ts > windowStart
|
|
389
|
+
);
|
|
390
|
+
if (entry.rateLimit.callTimestamps.length >= entry.rateLimit.maxCallsPerMinute) {
|
|
391
|
+
throw new ServerConnectionError(
|
|
392
|
+
serverName,
|
|
393
|
+
`Rate limit exceeded: ${entry.rateLimit.maxCallsPerMinute} calls/minute`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
getMaxCallsPerMinute(serverName) {
|
|
398
|
+
const perServer = this.options.rateLimits?.[serverName];
|
|
399
|
+
if (perServer) {
|
|
400
|
+
return perServer.maxCallsPerMinute;
|
|
401
|
+
}
|
|
402
|
+
return this.options.defaultRateLimit?.maxCallsPerMinute ?? DEFAULT_MAX_CALLS_PER_MINUTE;
|
|
403
|
+
}
|
|
404
|
+
// ── Helpers ───────────────────────────────────────────────────────
|
|
405
|
+
requireServer(name) {
|
|
406
|
+
const entry = this.servers.get(name);
|
|
407
|
+
if (!entry || entry.status !== "connected") {
|
|
408
|
+
throw new ServerConnectionError(name, "Server not connected");
|
|
409
|
+
}
|
|
410
|
+
return entry;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
export { MCPClient, MCPServerManager };
|
|
415
|
+
//# sourceMappingURL=chunk-MFBHNWGV.js.map
|
|
416
|
+
//# sourceMappingURL=chunk-MFBHNWGV.js.map
|