openfeelz 0.9.3 → 0.9.5
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 +3 -1
- package/dist/index.js +45 -10
- package/dist/src/classify/classifier.d.ts +2 -0
- package/dist/src/classify/classifier.js +108 -19
- package/dist/src/hook/hooks.d.ts +1 -1
- package/dist/src/hook/hooks.js +9 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -37,7 +37,9 @@ openclaw plugins install openfeelz
|
|
|
37
37
|
openclaw plugins enable openfeelz
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Restart the gateway after installing. To pin a version: `openclaw plugins install openfeelz@0.9.
|
|
40
|
+
Restart the gateway after installing. To pin a version: `openclaw plugins install openfeelz@0.9.4`. To install from a local clone (e.g. for development), run `npm run build` in the repo first, then `openclaw plugins install /path/to/openfeelz`.
|
|
41
|
+
|
|
42
|
+
When using **reasoning models** (e.g. gpt-5-mini, o1, o3), the classifier omits custom temperature so the API accepts the request. Optional classification logging can be enabled via config (see `docs/OPENFEELZ-FIX-COMPLETE.md`).
|
|
41
43
|
|
|
42
44
|
## How It Works
|
|
43
45
|
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import fs from "node:fs";
|
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { DEFAULT_CONFIG } from "./src/types.js";
|
|
18
18
|
import { StateManager } from "./src/state/state-manager.js";
|
|
19
|
-
import { resolveAgentDir, resolveAgentStatePath, listAgentIds } from "./src/paths.js";
|
|
19
|
+
import { resolveAgentDir, resolveAgentStatePath, resolveAgentWorkspaceDir, listAgentIds } from "./src/paths.js";
|
|
20
20
|
import { createEmotionTool } from "./src/tool/emotion-tool.js";
|
|
21
21
|
import { createBootstrapHook, createAgentEndHook } from "./src/hook/hooks.js";
|
|
22
22
|
import { registerEmotionCli } from "./src/cli/cli.js";
|
|
@@ -71,25 +71,48 @@ function resolveConfig(raw) {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
/**
|
|
74
|
-
* Attempt to resolve an
|
|
75
|
-
*
|
|
74
|
+
* Attempt to resolve an API key from OpenClaw's auth-profiles.json.
|
|
75
|
+
* Supports both Anthropic and OpenAI providers.
|
|
76
|
+
* Falls back gracefully if the file doesn't exist or has no matching profile.
|
|
76
77
|
*/
|
|
77
78
|
function resolveApiKeyFromAuthProfiles(api, agentId = "main") {
|
|
78
79
|
try {
|
|
79
80
|
const agentDir = resolveAgentDir(api.config, agentId);
|
|
80
81
|
const authFile = path.join(agentDir, "auth-profiles.json");
|
|
81
|
-
|
|
82
|
+
console.log(`[openfeelz] Checking auth profiles at: ${authFile}`);
|
|
83
|
+
if (!fs.existsSync(authFile)) {
|
|
84
|
+
console.log("[openfeelz] Auth profiles file does not exist");
|
|
82
85
|
return undefined;
|
|
86
|
+
}
|
|
83
87
|
const raw = JSON.parse(fs.readFileSync(authFile, "utf8"));
|
|
84
88
|
const profiles = raw?.profiles ?? {};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
const profileKeys = Object.keys(profiles);
|
|
90
|
+
console.log(`[openfeelz] Found ${profileKeys.length} auth profiles: ${profileKeys.join(", ")}`);
|
|
91
|
+
// Try to find Anthropic key first (preferred for claude models)
|
|
92
|
+
for (const [profileId, profile] of Object.entries(profiles)) {
|
|
93
|
+
if (profile?.provider === "anthropic") {
|
|
94
|
+
// Support both 'token' and 'key' fields
|
|
95
|
+
const key = profile?.token || profile?.key;
|
|
96
|
+
if (key) {
|
|
97
|
+
console.log(`[openfeelz] Using Anthropic key from profile: ${profileId}`);
|
|
98
|
+
return key;
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
102
|
+
// Fallback to OpenAI key
|
|
103
|
+
for (const [profileId, profile] of Object.entries(profiles)) {
|
|
104
|
+
if (profile?.provider === "openai") {
|
|
105
|
+
const key = profile?.token || profile?.key || profile?.apiKey;
|
|
106
|
+
if (key) {
|
|
107
|
+
console.log(`[openfeelz] Using OpenAI key from profile: ${profileId}`);
|
|
108
|
+
return key;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log("[openfeelz] No Anthropic or OpenAI keys found in auth profiles");
|
|
90
113
|
}
|
|
91
|
-
catch {
|
|
92
|
-
|
|
114
|
+
catch (err) {
|
|
115
|
+
console.error("[openfeelz] Error reading auth profiles:", err);
|
|
93
116
|
}
|
|
94
117
|
return undefined;
|
|
95
118
|
}
|
|
@@ -100,13 +123,22 @@ const emotionEnginePlugin = {
|
|
|
100
123
|
"rumination, and multi-agent awareness",
|
|
101
124
|
register(api) {
|
|
102
125
|
const config = resolveConfig(api.pluginConfig);
|
|
126
|
+
console.log("[openfeelz] Initial config - apiKey present:", !!config.apiKey, "model:", config.model);
|
|
103
127
|
// Resolve API key from OpenClaw auth profiles if not explicitly configured
|
|
104
128
|
if (!config.apiKey) {
|
|
129
|
+
console.log("[openfeelz] No API key in config, attempting to resolve from auth profiles...");
|
|
105
130
|
const resolvedKey = resolveApiKeyFromAuthProfiles(api);
|
|
106
131
|
if (resolvedKey) {
|
|
107
132
|
config.apiKey = resolvedKey;
|
|
133
|
+
console.log("[openfeelz] Successfully resolved API key from auth profiles");
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
console.warn("[openfeelz] Failed to resolve API key from auth profiles");
|
|
108
137
|
}
|
|
109
138
|
}
|
|
139
|
+
else {
|
|
140
|
+
console.log("[openfeelz] Using API key from config");
|
|
141
|
+
}
|
|
110
142
|
const cfg = api.config;
|
|
111
143
|
// Per-agent StateManager cache (state path = workspace/openfeelz.json)
|
|
112
144
|
const managerCache = new Map();
|
|
@@ -135,9 +167,12 @@ const emotionEnginePlugin = {
|
|
|
135
167
|
});
|
|
136
168
|
return result;
|
|
137
169
|
});
|
|
138
|
-
|
|
170
|
+
// Set up classification log path
|
|
171
|
+
const classificationLogPath = path.join(resolveAgentWorkspaceDir(cfg, "main"), "openfeelz-classifications.jsonl");
|
|
172
|
+
const agentEndHandler = createAgentEndHook(getManager, config, classificationLogPath);
|
|
139
173
|
api.on("agent_end", async (event) => {
|
|
140
174
|
const agentId = event.agentId ?? "main";
|
|
175
|
+
console.log("[openfeelz] agent_end hook fired for agent:", agentId);
|
|
141
176
|
await agentEndHandler({
|
|
142
177
|
success: event.success ?? true,
|
|
143
178
|
messages: event.messages ?? [],
|
|
@@ -32,6 +32,8 @@ export interface ClassifyOptions {
|
|
|
32
32
|
timeoutMs?: number;
|
|
33
33
|
/** Injectable fetch function (for testing). */
|
|
34
34
|
fetchFn?: typeof fetch;
|
|
35
|
+
/** Path to classification log file (JSONL). */
|
|
36
|
+
classificationLogPath?: string;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Build the classification prompt (shared across providers).
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Falls back to neutral on any failure (no hard crashes in classification).
|
|
13
13
|
*/
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
14
16
|
const NEUTRAL_RESULT = {
|
|
15
17
|
label: "neutral",
|
|
16
18
|
intensity: 0,
|
|
@@ -19,6 +21,37 @@ const NEUTRAL_RESULT = {
|
|
|
19
21
|
};
|
|
20
22
|
const ANTHROPIC_API_URL = "https://api.anthropic.com/v1/messages";
|
|
21
23
|
const ANTHROPIC_VERSION = "2023-06-01";
|
|
24
|
+
// Reasoning models that don't support custom temperature
|
|
25
|
+
const REASONING_MODELS = ["gpt-5", "gpt-4o-mini", "o1", "o3"];
|
|
26
|
+
/**
|
|
27
|
+
* Check if a model is a reasoning model that doesn't support custom temperature.
|
|
28
|
+
*/
|
|
29
|
+
function isReasoningModel(model) {
|
|
30
|
+
const lower = model.toLowerCase();
|
|
31
|
+
return REASONING_MODELS.some(prefix => lower.includes(prefix));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Log a classification attempt to JSONL file.
|
|
35
|
+
*/
|
|
36
|
+
function logClassification(logPath, data) {
|
|
37
|
+
if (!logPath)
|
|
38
|
+
return;
|
|
39
|
+
try {
|
|
40
|
+
const logDir = path.dirname(logPath);
|
|
41
|
+
if (!fs.existsSync(logDir)) {
|
|
42
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
const entry = {
|
|
45
|
+
...data,
|
|
46
|
+
textExcerpt: data.text.slice(0, 200) + (data.text.length > 200 ? "..." : ""),
|
|
47
|
+
};
|
|
48
|
+
delete entry.text; // Don't log full text for privacy
|
|
49
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error("[openfeelz] Failed to write classification log:", err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
22
55
|
// ---------------------------------------------------------------------------
|
|
23
56
|
// Provider Detection
|
|
24
57
|
// ---------------------------------------------------------------------------
|
|
@@ -113,27 +146,77 @@ export function coerceClassificationResult(result, labels, confidenceMin) {
|
|
|
113
146
|
export async function classifyEmotion(text, role, options) {
|
|
114
147
|
const fetchFn = options.fetchFn ?? fetch;
|
|
115
148
|
const timeoutMs = options.timeoutMs ?? 10000;
|
|
149
|
+
const startTime = Date.now();
|
|
150
|
+
const timestamp = new Date().toISOString();
|
|
116
151
|
try {
|
|
117
152
|
if (options.classifierUrl) {
|
|
118
|
-
|
|
153
|
+
const result = await classifyViaEndpoint(text, role, options.classifierUrl, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
|
|
154
|
+
logClassification(options.classificationLogPath, {
|
|
155
|
+
timestamp,
|
|
156
|
+
role,
|
|
157
|
+
text,
|
|
158
|
+
model: "external",
|
|
159
|
+
provider: "endpoint",
|
|
160
|
+
result,
|
|
161
|
+
success: true,
|
|
162
|
+
responseTimeMs: Date.now() - startTime,
|
|
163
|
+
});
|
|
164
|
+
return result;
|
|
119
165
|
}
|
|
120
166
|
if (!options.apiKey) {
|
|
121
|
-
|
|
122
|
-
"Configure apiKey or set ANTHROPIC_API_KEY / OPENAI_API_KEY in
|
|
167
|
+
const error = "Emotion classifier requires either classifierUrl or apiKey. " +
|
|
168
|
+
"Configure apiKey or set ANTHROPIC_API_KEY / OPENAI_API_KEY in environment or auth-profiles.json.";
|
|
169
|
+
console.error(`[openfeelz] ${error}`);
|
|
170
|
+
logClassification(options.classificationLogPath, {
|
|
171
|
+
timestamp,
|
|
172
|
+
role,
|
|
173
|
+
text,
|
|
174
|
+
model: options.model ?? "unknown",
|
|
175
|
+
provider: "unknown",
|
|
176
|
+
success: false,
|
|
177
|
+
error,
|
|
178
|
+
});
|
|
179
|
+
throw new Error(error);
|
|
123
180
|
}
|
|
124
181
|
const model = options.model ?? "claude-sonnet-4-5-20250514";
|
|
125
182
|
const provider = options.provider ?? detectProvider(model);
|
|
183
|
+
console.log(`[openfeelz] Classifying ${role} emotion with ${provider}/${model}`);
|
|
184
|
+
let result;
|
|
126
185
|
if (provider === "anthropic") {
|
|
127
|
-
|
|
186
|
+
result = await classifyViaAnthropic(text, role, options.apiKey, model, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
result = await classifyViaOpenAI(text, role, options.apiKey, options.baseUrl ?? "https://api.openai.com/v1", model, fetchFn, timeoutMs, options.emotionLabels, options.confidenceMin);
|
|
128
190
|
}
|
|
129
|
-
|
|
191
|
+
logClassification(options.classificationLogPath, {
|
|
192
|
+
timestamp,
|
|
193
|
+
role,
|
|
194
|
+
text,
|
|
195
|
+
model,
|
|
196
|
+
provider,
|
|
197
|
+
result,
|
|
198
|
+
success: true,
|
|
199
|
+
responseTimeMs: Date.now() - startTime,
|
|
200
|
+
});
|
|
201
|
+
return result;
|
|
130
202
|
}
|
|
131
203
|
catch (err) {
|
|
204
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
132
205
|
// If it's a configuration error (no apiKey), rethrow
|
|
133
206
|
if (err instanceof Error && err.message.includes("requires either")) {
|
|
134
207
|
throw err;
|
|
135
208
|
}
|
|
136
209
|
console.error("[openfeelz] Classification failed:", err);
|
|
210
|
+
logClassification(options.classificationLogPath, {
|
|
211
|
+
timestamp,
|
|
212
|
+
role,
|
|
213
|
+
text,
|
|
214
|
+
model: options.model ?? "unknown",
|
|
215
|
+
provider: options.provider ?? "unknown",
|
|
216
|
+
success: false,
|
|
217
|
+
error: errorMessage,
|
|
218
|
+
responseTimeMs: Date.now() - startTime,
|
|
219
|
+
});
|
|
137
220
|
return { ...NEUTRAL_RESULT };
|
|
138
221
|
}
|
|
139
222
|
}
|
|
@@ -179,8 +262,8 @@ async function classifyViaAnthropic(text, role, apiKey, model, fetchFn, timeoutM
|
|
|
179
262
|
});
|
|
180
263
|
if (!response.ok) {
|
|
181
264
|
const body = await response.text().catch(() => "");
|
|
182
|
-
console.error("[openfeelz] Anthropic classification API error:", response.status, body.slice(0,
|
|
183
|
-
throw new Error(`Anthropic returned ${response.status}: ${body}`);
|
|
265
|
+
console.error("[openfeelz] Anthropic classification API error:", response.status, body.slice(0, 800));
|
|
266
|
+
throw new Error(`Anthropic returned ${response.status}: ${body.slice(0, 200)}`);
|
|
184
267
|
}
|
|
185
268
|
const data = (await response.json());
|
|
186
269
|
const textBlock = data.content?.find((b) => b.type === "text");
|
|
@@ -196,28 +279,34 @@ async function classifyViaAnthropic(text, role, apiKey, model, fetchFn, timeoutM
|
|
|
196
279
|
// ---------------------------------------------------------------------------
|
|
197
280
|
async function classifyViaOpenAI(text, role, apiKey, baseUrl, model, fetchFn, timeoutMs, labels, confidenceMin) {
|
|
198
281
|
const prompt = buildClassifierPrompt(text, role, labels);
|
|
282
|
+
// Reasoning models (gpt-5-mini, gpt-4o-mini, o1, o3) don't support custom temperature
|
|
283
|
+
const isReasoning = isReasoningModel(model);
|
|
284
|
+
const requestBody = {
|
|
285
|
+
model,
|
|
286
|
+
messages: [
|
|
287
|
+
{ role: "system", content: buildSystemInstruction() },
|
|
288
|
+
{ role: "user", content: prompt },
|
|
289
|
+
],
|
|
290
|
+
response_format: { type: "json_object" },
|
|
291
|
+
max_completion_tokens: 1000, // reasoning models need headroom
|
|
292
|
+
};
|
|
293
|
+
// Only set temperature for non-reasoning models
|
|
294
|
+
if (!isReasoning) {
|
|
295
|
+
requestBody.temperature = 0.2;
|
|
296
|
+
}
|
|
199
297
|
const response = await fetchFn(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
|
|
200
298
|
method: "POST",
|
|
201
299
|
headers: {
|
|
202
300
|
"content-type": "application/json",
|
|
203
301
|
authorization: `Bearer ${apiKey}`,
|
|
204
302
|
},
|
|
205
|
-
body: JSON.stringify(
|
|
206
|
-
model,
|
|
207
|
-
messages: [
|
|
208
|
-
{ role: "system", content: buildSystemInstruction() },
|
|
209
|
-
{ role: "user", content: prompt },
|
|
210
|
-
],
|
|
211
|
-
temperature: 0.2,
|
|
212
|
-
response_format: { type: "json_object" },
|
|
213
|
-
max_completion_tokens: 1000, // reasoning models (e.g. gpt-5-mini) need headroom
|
|
214
|
-
}),
|
|
303
|
+
body: JSON.stringify(requestBody),
|
|
215
304
|
signal: AbortSignal.timeout(timeoutMs),
|
|
216
305
|
});
|
|
217
306
|
if (!response.ok) {
|
|
218
307
|
const body = await response.text().catch(() => "");
|
|
219
|
-
console.error("[openfeelz] OpenAI classification API error:", response.status, body.slice(0,
|
|
220
|
-
throw new Error(`OpenAI returned ${response.status}`);
|
|
308
|
+
console.error("[openfeelz] OpenAI classification API error:", response.status, body.slice(0, 800));
|
|
309
|
+
throw new Error(`OpenAI returned ${response.status}: ${body.slice(0, 200)}`);
|
|
221
310
|
}
|
|
222
311
|
const data = (await response.json());
|
|
223
312
|
const content = data.choices?.[0]?.message?.content;
|
package/dist/src/hook/hooks.d.ts
CHANGED
|
@@ -45,5 +45,5 @@ export declare function createBootstrapHook(getManager: (agentId: string) => Sta
|
|
|
45
45
|
* 4. Record in user/agent buckets
|
|
46
46
|
* 5. Persist
|
|
47
47
|
*/
|
|
48
|
-
export declare function createAgentEndHook(getManager: (agentId: string) => StateManager, config: EmotionEngineConfig, fetchFn?: typeof fetch): (event: AgentEndEvent) => Promise<void>;
|
|
48
|
+
export declare function createAgentEndHook(getManager: (agentId: string) => StateManager, config: EmotionEngineConfig, classificationLogPath?: string, fetchFn?: typeof fetch): (event: AgentEndEvent) => Promise<void>;
|
|
49
49
|
export {};
|
package/dist/src/hook/hooks.js
CHANGED
|
@@ -71,13 +71,15 @@ export function createBootstrapHook(getManager, config, openclawConfig) {
|
|
|
71
71
|
* 4. Record in user/agent buckets
|
|
72
72
|
* 5. Persist
|
|
73
73
|
*/
|
|
74
|
-
export function createAgentEndHook(getManager, config, fetchFn) {
|
|
74
|
+
export function createAgentEndHook(getManager, config, classificationLogPath, fetchFn) {
|
|
75
75
|
return async (event) => {
|
|
76
76
|
if (!event.success || !event.messages || event.messages.length === 0) {
|
|
77
|
+
console.log("[openfeelz] Skipping agent_end: no success or messages");
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
80
|
// Need either apiKey or classifierUrl to classify
|
|
80
81
|
if (!config.apiKey && !config.classifierUrl) {
|
|
82
|
+
console.warn("[openfeelz] Skipping classification: no apiKey or classifierUrl configured");
|
|
81
83
|
return;
|
|
82
84
|
}
|
|
83
85
|
const agentId = event.agentId ?? "main";
|
|
@@ -89,6 +91,7 @@ export function createAgentEndHook(getManager, config, fetchFn) {
|
|
|
89
91
|
const userMsg = findLast(event.messages, "user");
|
|
90
92
|
// Find latest assistant message
|
|
91
93
|
const assistantMsg = findLast(event.messages, "assistant");
|
|
94
|
+
console.log(`[openfeelz] Processing messages - user: ${!!userMsg}, assistant: ${!!assistantMsg}`);
|
|
92
95
|
const classifyOpts = {
|
|
93
96
|
apiKey: config.apiKey,
|
|
94
97
|
baseUrl: config.baseUrl,
|
|
@@ -97,12 +100,15 @@ export function createAgentEndHook(getManager, config, fetchFn) {
|
|
|
97
100
|
classifierUrl: config.classifierUrl,
|
|
98
101
|
emotionLabels: config.emotionLabels,
|
|
99
102
|
confidenceMin: config.confidenceMin,
|
|
103
|
+
classificationLogPath,
|
|
100
104
|
fetchFn,
|
|
101
105
|
};
|
|
102
106
|
if (userMsg) {
|
|
103
107
|
const text = extractMessageText(userMsg.content);
|
|
104
108
|
if (text) {
|
|
109
|
+
console.log(`[openfeelz] Classifying user message (${text.length} chars)`);
|
|
105
110
|
const result = await classifyEmotion(text, "user", classifyOpts);
|
|
111
|
+
console.log(`[openfeelz] User emotion: ${result.label} (intensity: ${result.intensity}, confidence: ${result.confidence})`);
|
|
106
112
|
if (result.label !== "neutral" || result.confidence > 0) {
|
|
107
113
|
state = manager.updateUserEmotion(state, userKey, result);
|
|
108
114
|
// Also apply as stimulus to the dimensional model
|
|
@@ -113,7 +119,9 @@ export function createAgentEndHook(getManager, config, fetchFn) {
|
|
|
113
119
|
if (assistantMsg) {
|
|
114
120
|
const text = extractMessageText(assistantMsg.content);
|
|
115
121
|
if (text) {
|
|
122
|
+
console.log(`[openfeelz] Classifying assistant message (${text.length} chars)`);
|
|
116
123
|
const result = await classifyEmotion(text, "assistant", classifyOpts);
|
|
124
|
+
console.log(`[openfeelz] Assistant emotion: ${result.label} (intensity: ${result.intensity}, confidence: ${result.confidence})`);
|
|
117
125
|
if (result.label !== "neutral" || result.confidence > 0) {
|
|
118
126
|
state = manager.updateAgentEmotion(state, agentId, result);
|
|
119
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openfeelz",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "PAD + Ekman + OCEAN emotional model plugin for OpenClaw agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"openclaw": ">=2026.1.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
+
"@clack/prompts": "^1.0.0",
|
|
32
33
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
33
34
|
"commander": "^14.0.3"
|
|
34
35
|
},
|