svamp-cli 0.1.73 → 0.1.75
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/dist/cli.mjs +18 -18
- package/dist/{commands-CPy4F-7w.mjs → commands-Dq8WSqvt.mjs} +2 -2
- package/dist/{commands-gghG16ZB.mjs → commands-a7p1jW-t.mjs} +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{package-BAXhI1Iz.mjs → package-BFnad6d1.mjs} +2 -1
- package/dist/{run-C2UwUIgR.mjs → run-Dy5lxT3M.mjs} +1 -1
- package/dist/{run-DWTcdCOS.mjs → run-lhAjX4NB.mjs} +128 -0
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as stopDaemon, s as startDaemon, d as daemonStatus } from './run-
|
|
1
|
+
import { b as stopDaemon, s as startDaemon, d as daemonStatus } from './run-lhAjX4NB.mjs';
|
|
2
2
|
import 'os';
|
|
3
3
|
import 'fs/promises';
|
|
4
4
|
import 'fs';
|
|
@@ -109,7 +109,7 @@ async function main() {
|
|
|
109
109
|
const { handleServiceCommand } = await import('./commands-BLjcT1Vl.mjs');
|
|
110
110
|
await handleServiceCommand();
|
|
111
111
|
} else if (subcommand === "process" || subcommand === "proc") {
|
|
112
|
-
const { processCommand } = await import('./commands-
|
|
112
|
+
const { processCommand } = await import('./commands-Dq8WSqvt.mjs');
|
|
113
113
|
let machineId;
|
|
114
114
|
const processArgs = args.slice(1);
|
|
115
115
|
const mIdx = processArgs.findIndex((a) => a === "--machine" || a === "-m");
|
|
@@ -127,7 +127,7 @@ async function main() {
|
|
|
127
127
|
} else if (!subcommand || subcommand === "start") {
|
|
128
128
|
await handleInteractiveCommand();
|
|
129
129
|
} else if (subcommand === "--version" || subcommand === "-v") {
|
|
130
|
-
const pkg = await import('./package-
|
|
130
|
+
const pkg = await import('./package-BFnad6d1.mjs').catch(() => ({ default: { version: "unknown" } }));
|
|
131
131
|
console.log(`svamp version: ${pkg.default.version}`);
|
|
132
132
|
} else {
|
|
133
133
|
console.error(`Unknown command: ${subcommand}`);
|
|
@@ -136,7 +136,7 @@ async function main() {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
async function handleInteractiveCommand() {
|
|
139
|
-
const { runInteractive } = await import('./run-
|
|
139
|
+
const { runInteractive } = await import('./run-Dy5lxT3M.mjs');
|
|
140
140
|
const interactiveArgs = subcommand === "start" ? args.slice(1) : args;
|
|
141
141
|
let directory = process.cwd();
|
|
142
142
|
let resumeSessionId;
|
|
@@ -181,7 +181,7 @@ async function handleAgentCommand() {
|
|
|
181
181
|
return;
|
|
182
182
|
}
|
|
183
183
|
if (agentArgs[0] === "list") {
|
|
184
|
-
const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-
|
|
184
|
+
const { KNOWN_ACP_AGENTS, KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS2 } = await import('./run-lhAjX4NB.mjs').then(function (n) { return n.i; });
|
|
185
185
|
console.log("Known agents:");
|
|
186
186
|
for (const [name, config2] of Object.entries(KNOWN_ACP_AGENTS)) {
|
|
187
187
|
console.log(` ${name.padEnd(12)} ${config2.command} ${config2.args.join(" ")} (ACP)`);
|
|
@@ -193,7 +193,7 @@ async function handleAgentCommand() {
|
|
|
193
193
|
console.log('Use "svamp agent -- <command> [args]" for a custom ACP agent.');
|
|
194
194
|
return;
|
|
195
195
|
}
|
|
196
|
-
const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-
|
|
196
|
+
const { resolveAcpAgentConfig, KNOWN_MCP_AGENTS } = await import('./run-lhAjX4NB.mjs').then(function (n) { return n.i; });
|
|
197
197
|
let cwd = process.cwd();
|
|
198
198
|
const filteredArgs = [];
|
|
199
199
|
for (let i = 0; i < agentArgs.length; i++) {
|
|
@@ -217,12 +217,12 @@ async function handleAgentCommand() {
|
|
|
217
217
|
console.log(`Starting ${config.agentName} agent in ${cwd}...`);
|
|
218
218
|
let backend;
|
|
219
219
|
if (KNOWN_MCP_AGENTS[config.agentName]) {
|
|
220
|
-
const { CodexMcpBackend } = await import('./run-
|
|
220
|
+
const { CodexMcpBackend } = await import('./run-lhAjX4NB.mjs').then(function (n) { return n.j; });
|
|
221
221
|
backend = new CodexMcpBackend({ cwd, log: logFn });
|
|
222
222
|
} else {
|
|
223
|
-
const { AcpBackend } = await import('./run-
|
|
224
|
-
const { GeminiTransport } = await import('./run-
|
|
225
|
-
const { DefaultTransport } = await import('./run-
|
|
223
|
+
const { AcpBackend } = await import('./run-lhAjX4NB.mjs').then(function (n) { return n.h; });
|
|
224
|
+
const { GeminiTransport } = await import('./run-lhAjX4NB.mjs').then(function (n) { return n.G; });
|
|
225
|
+
const { DefaultTransport } = await import('./run-lhAjX4NB.mjs').then(function (n) { return n.D; });
|
|
226
226
|
const transportHandler = config.agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(config.agentName);
|
|
227
227
|
backend = new AcpBackend({
|
|
228
228
|
agentName: config.agentName,
|
|
@@ -340,7 +340,7 @@ async function handleSessionCommand() {
|
|
|
340
340
|
printSessionHelp();
|
|
341
341
|
return;
|
|
342
342
|
}
|
|
343
|
-
const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionQueueAdd, sessionQueueList, sessionQueueClear } = await import('./commands-
|
|
343
|
+
const { sessionList, sessionSpawn, sessionStop, sessionInfo, sessionMessages, sessionAttach, sessionMachines, sessionSend, sessionWait, sessionShare, sessionRalphStart, sessionRalphCancel, sessionRalphStatus, sessionQueueAdd, sessionQueueList, sessionQueueClear } = await import('./commands-a7p1jW-t.mjs');
|
|
344
344
|
const parseFlagStr = (flag, shortFlag) => {
|
|
345
345
|
for (let i = 1; i < sessionArgs.length; i++) {
|
|
346
346
|
if ((sessionArgs[i] === flag || shortFlag) && i + 1 < sessionArgs.length) {
|
|
@@ -400,7 +400,7 @@ async function handleSessionCommand() {
|
|
|
400
400
|
allowDomain.push(sessionArgs[++i]);
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
|
-
const { parseShareArg } = await import('./commands-
|
|
403
|
+
const { parseShareArg } = await import('./commands-a7p1jW-t.mjs');
|
|
404
404
|
const shareEntries = share.map((s) => parseShareArg(s));
|
|
405
405
|
await sessionSpawn(agent, dir, targetMachineId, {
|
|
406
406
|
message,
|
|
@@ -484,7 +484,7 @@ async function handleSessionCommand() {
|
|
|
484
484
|
console.error("Usage: svamp session approve <session-id> [request-id] [--json]");
|
|
485
485
|
process.exit(1);
|
|
486
486
|
}
|
|
487
|
-
const { sessionApprove } = await import('./commands-
|
|
487
|
+
const { sessionApprove } = await import('./commands-a7p1jW-t.mjs');
|
|
488
488
|
const approveReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
489
489
|
await sessionApprove(sessionArgs[1], approveReqId, targetMachineId, {
|
|
490
490
|
json: hasFlag("--json")
|
|
@@ -494,7 +494,7 @@ async function handleSessionCommand() {
|
|
|
494
494
|
console.error("Usage: svamp session deny <session-id> [request-id] [--json]");
|
|
495
495
|
process.exit(1);
|
|
496
496
|
}
|
|
497
|
-
const { sessionDeny } = await import('./commands-
|
|
497
|
+
const { sessionDeny } = await import('./commands-a7p1jW-t.mjs');
|
|
498
498
|
const denyReqId = sessionArgs[2] && !sessionArgs[2].startsWith("--") ? sessionArgs[2] : void 0;
|
|
499
499
|
await sessionDeny(sessionArgs[1], denyReqId, targetMachineId, {
|
|
500
500
|
json: hasFlag("--json")
|
|
@@ -597,7 +597,7 @@ async function handleMachineCommand() {
|
|
|
597
597
|
return;
|
|
598
598
|
}
|
|
599
599
|
if (machineSubcommand === "share") {
|
|
600
|
-
const { machineShare } = await import('./commands-
|
|
600
|
+
const { machineShare } = await import('./commands-a7p1jW-t.mjs');
|
|
601
601
|
let machineId;
|
|
602
602
|
const shareArgs = [];
|
|
603
603
|
for (let i = 1; i < machineArgs.length; i++) {
|
|
@@ -627,7 +627,7 @@ async function handleMachineCommand() {
|
|
|
627
627
|
}
|
|
628
628
|
await machineShare(machineId, { add, remove, list, configPath, showConfig });
|
|
629
629
|
} else if (machineSubcommand === "exec") {
|
|
630
|
-
const { machineExec } = await import('./commands-
|
|
630
|
+
const { machineExec } = await import('./commands-a7p1jW-t.mjs');
|
|
631
631
|
let machineId;
|
|
632
632
|
let cwd;
|
|
633
633
|
const cmdParts = [];
|
|
@@ -647,7 +647,7 @@ async function handleMachineCommand() {
|
|
|
647
647
|
}
|
|
648
648
|
await machineExec(machineId, command, cwd);
|
|
649
649
|
} else if (machineSubcommand === "info") {
|
|
650
|
-
const { machineInfo } = await import('./commands-
|
|
650
|
+
const { machineInfo } = await import('./commands-a7p1jW-t.mjs');
|
|
651
651
|
let machineId;
|
|
652
652
|
for (let i = 1; i < machineArgs.length; i++) {
|
|
653
653
|
if ((machineArgs[i] === "--machine" || machineArgs[i] === "-m") && i + 1 < machineArgs.length) {
|
|
@@ -670,7 +670,7 @@ async function handleMachineCommand() {
|
|
|
670
670
|
const { machineNotify } = await import('./agentCommands-C6iGblcL.mjs');
|
|
671
671
|
await machineNotify(message, level);
|
|
672
672
|
} else if (machineSubcommand === "ls") {
|
|
673
|
-
const { machineLs } = await import('./commands-
|
|
673
|
+
const { machineLs } = await import('./commands-a7p1jW-t.mjs');
|
|
674
674
|
let machineId;
|
|
675
675
|
let showHidden = false;
|
|
676
676
|
let path;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { connectAndGetMachine } from './commands-
|
|
3
|
+
import { connectAndGetMachine } from './commands-a7p1jW-t.mjs';
|
|
4
4
|
import 'node:fs';
|
|
5
5
|
import 'node:child_process';
|
|
6
6
|
import 'node:path';
|
|
7
7
|
import 'node:os';
|
|
8
|
-
import './run-
|
|
8
|
+
import './run-lhAjX4NB.mjs';
|
|
9
9
|
import 'os';
|
|
10
10
|
import 'fs/promises';
|
|
11
11
|
import 'url';
|
|
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { execSync } from 'node:child_process';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
5
|
-
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-
|
|
5
|
+
import { l as loadSecurityContextConfig, e as resolveSecurityContext, f as buildSecurityContextFromFlags, m as mergeSecurityContexts, c as connectToHypha } from './run-lhAjX4NB.mjs';
|
|
6
6
|
import 'os';
|
|
7
7
|
import 'fs/promises';
|
|
8
8
|
import 'fs';
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-
|
|
1
|
+
export { c as connectToHypha, d as daemonStatus, g as getHyphaServerUrl, r as registerMachineService, a as registerSessionService, s as startDaemon, b as stopDaemon } from './run-lhAjX4NB.mjs';
|
|
2
2
|
import 'os';
|
|
3
3
|
import 'fs/promises';
|
|
4
4
|
import 'fs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
var name = "svamp-cli";
|
|
2
|
-
var version = "0.1.
|
|
2
|
+
var version = "0.1.75";
|
|
3
3
|
var description = "Svamp CLI — AI workspace daemon on Hypha Cloud";
|
|
4
4
|
var author = "Amun AI AB";
|
|
5
5
|
var license = "SEE LICENSE IN LICENSE";
|
|
@@ -29,6 +29,7 @@ var dependencies = {
|
|
|
29
29
|
"@agentclientprotocol/sdk": "^0.14.1",
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
31
31
|
"hypha-rpc": "0.21.29",
|
|
32
|
+
"node-pty": "^1.1.0",
|
|
32
33
|
ws: "^8.18.0",
|
|
33
34
|
yaml: "^2.8.2",
|
|
34
35
|
zod: "^3.24.4"
|
|
@@ -2,7 +2,7 @@ import{createRequire as _pkgrollCR}from"node:module";const require=_pkgrollCR(im
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { mkdirSync, writeFileSync, existsSync, unlinkSync, readFileSync, watch } from 'node:fs';
|
|
5
|
-
import { c as connectToHypha, a as registerSessionService } from './run-
|
|
5
|
+
import { c as connectToHypha, a as registerSessionService } from './run-lhAjX4NB.mjs';
|
|
6
6
|
import { createServer } from 'node:http';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { createInterface } from 'node:readline';
|
|
@@ -294,6 +294,33 @@ function applySecurityContext(baseConfig, context) {
|
|
|
294
294
|
return config;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
const terminalSessions = /* @__PURE__ */ new Map();
|
|
298
|
+
let ptyModule = null;
|
|
299
|
+
async function getPtyModule() {
|
|
300
|
+
if (!ptyModule) {
|
|
301
|
+
try {
|
|
302
|
+
ptyModule = await import('node-pty');
|
|
303
|
+
} catch {
|
|
304
|
+
throw new Error("node-pty is not available. Install it with: npm install node-pty");
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return ptyModule;
|
|
308
|
+
}
|
|
309
|
+
function generateTerminalId() {
|
|
310
|
+
return `term-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
311
|
+
}
|
|
312
|
+
const ALLOWED_CONTROL_CHARS = /* @__PURE__ */ new Set([7, 8, 9, 10, 11, 12, 13, 27]);
|
|
313
|
+
function filterForXterm(text) {
|
|
314
|
+
if (!text) return text;
|
|
315
|
+
let result = "";
|
|
316
|
+
for (let i = 0; i < text.length; i++) {
|
|
317
|
+
const code = text.charCodeAt(i);
|
|
318
|
+
if (code >= 32 && code <= 126 || ALLOWED_CONTROL_CHARS.has(code) || code > 127) {
|
|
319
|
+
result += text[i];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
323
|
+
}
|
|
297
324
|
function getMachineMetadataPath(svampHomeDir) {
|
|
298
325
|
return join(svampHomeDir, "machine-metadata.json");
|
|
299
326
|
}
|
|
@@ -667,6 +694,107 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
667
694
|
});
|
|
668
695
|
});
|
|
669
696
|
},
|
|
697
|
+
// ── Terminal PTY RPC ──────────────────────────────────────────────
|
|
698
|
+
/** Start a new terminal PTY session. Returns { sessionId, cols, rows }. */
|
|
699
|
+
terminalStart: async (params = {}, context) => {
|
|
700
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
701
|
+
const pty = await getPtyModule();
|
|
702
|
+
const { homedir: getHomedir } = await import('os');
|
|
703
|
+
const cols = params.cols || 80;
|
|
704
|
+
const rows = params.rows || 24;
|
|
705
|
+
const cwd = params.cwd || getHomedir();
|
|
706
|
+
const shell = params.shell || process.env.SHELL || "bash";
|
|
707
|
+
const sessionId = generateTerminalId();
|
|
708
|
+
const ptyProcess = pty.spawn(shell, [], {
|
|
709
|
+
name: "xterm-256color",
|
|
710
|
+
cols,
|
|
711
|
+
rows,
|
|
712
|
+
cwd,
|
|
713
|
+
env: { ...process.env, TERM: "xterm-256color" }
|
|
714
|
+
});
|
|
715
|
+
const session = {
|
|
716
|
+
id: sessionId,
|
|
717
|
+
pty: ptyProcess,
|
|
718
|
+
listeners: [],
|
|
719
|
+
cols,
|
|
720
|
+
rows,
|
|
721
|
+
createdAt: Date.now(),
|
|
722
|
+
cwd
|
|
723
|
+
};
|
|
724
|
+
terminalSessions.set(sessionId, session);
|
|
725
|
+
ptyProcess.onData((data) => {
|
|
726
|
+
const filtered = filterForXterm(data);
|
|
727
|
+
if (!filtered) return;
|
|
728
|
+
const update = { type: "output", content: filtered, sessionId };
|
|
729
|
+
for (const listener of session.listeners) {
|
|
730
|
+
try {
|
|
731
|
+
listener(update);
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
737
|
+
const update = { type: "exit", content: "", sessionId, exitCode, signal };
|
|
738
|
+
for (const listener of session.listeners) {
|
|
739
|
+
try {
|
|
740
|
+
listener(update);
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
terminalSessions.delete(sessionId);
|
|
745
|
+
});
|
|
746
|
+
return { sessionId, cols, rows };
|
|
747
|
+
},
|
|
748
|
+
/** Write data (keystrokes) to a terminal session. */
|
|
749
|
+
terminalWrite: async (params, context) => {
|
|
750
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
751
|
+
const session = terminalSessions.get(params.sessionId);
|
|
752
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
753
|
+
session.pty.write(params.data);
|
|
754
|
+
return { success: true };
|
|
755
|
+
},
|
|
756
|
+
/** Resize a terminal session. */
|
|
757
|
+
terminalResize: async (params, context) => {
|
|
758
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
759
|
+
const session = terminalSessions.get(params.sessionId);
|
|
760
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
761
|
+
session.pty.resize(params.cols, params.rows);
|
|
762
|
+
session.cols = params.cols;
|
|
763
|
+
session.rows = params.rows;
|
|
764
|
+
return { success: true };
|
|
765
|
+
},
|
|
766
|
+
/**
|
|
767
|
+
* Attach to a terminal session for real-time output streaming.
|
|
768
|
+
* The onData callback receives { type: 'output'|'exit', content, sessionId }.
|
|
769
|
+
*/
|
|
770
|
+
terminalAttach: async (params, context) => {
|
|
771
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
772
|
+
const session = terminalSessions.get(params.sessionId);
|
|
773
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
774
|
+
if (typeof params.onData !== "function") throw new Error("onData callback is required");
|
|
775
|
+
session.listeners.push(params.onData);
|
|
776
|
+
return { success: true, cols: session.cols, rows: session.rows };
|
|
777
|
+
},
|
|
778
|
+
/** Stop (kill) a terminal session. */
|
|
779
|
+
terminalStop: async (params, context) => {
|
|
780
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
781
|
+
const session = terminalSessions.get(params.sessionId);
|
|
782
|
+
if (!session) throw new Error(`Terminal session ${params.sessionId} not found`);
|
|
783
|
+
session.pty.kill();
|
|
784
|
+
terminalSessions.delete(params.sessionId);
|
|
785
|
+
return { success: true };
|
|
786
|
+
},
|
|
787
|
+
/** List active terminal sessions. */
|
|
788
|
+
terminalList: async (context) => {
|
|
789
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
790
|
+
return Array.from(terminalSessions.values()).map((s) => ({
|
|
791
|
+
sessionId: s.id,
|
|
792
|
+
cols: s.cols,
|
|
793
|
+
rows: s.rows,
|
|
794
|
+
cwd: s.cwd,
|
|
795
|
+
createdAt: s.createdAt
|
|
796
|
+
}));
|
|
797
|
+
},
|
|
670
798
|
// Machine-level directory listing (read-only, view role)
|
|
671
799
|
listDirectory: async (path, options, context) => {
|
|
672
800
|
authorizeRequest(context, currentMetadata.sharing, "view");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svamp-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.75",
|
|
4
4
|
"description": "Svamp CLI — AI workspace daemon on Hypha Cloud",
|
|
5
5
|
"author": "Amun AI AB",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@agentclientprotocol/sdk": "^0.14.1",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
32
32
|
"hypha-rpc": "0.21.29",
|
|
33
|
+
"node-pty": "^1.1.0",
|
|
33
34
|
"ws": "^8.18.0",
|
|
34
35
|
"yaml": "^2.8.2",
|
|
35
36
|
"zod": "^3.24.4"
|