toolcraft 0.0.24 → 0.0.25
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/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +146 -33
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +65 -9
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +5 -4
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +5 -1
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +29 -10
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +1 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +36 -2
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +1 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +11 -3
- package/node_modules/tiny-mcp-client/src/internal.ts +12 -3
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
- package/package.json +2 -2
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
export function buildDockerEnvArgs(input) {
|
|
3
|
+
const keys = Object.keys(input.env ?? {});
|
|
4
|
+
if (keys.length === 0) {
|
|
5
|
+
return [];
|
|
6
|
+
}
|
|
7
|
+
if (input.envFilePath !== undefined) {
|
|
8
|
+
return ["--env-file", input.envFilePath];
|
|
9
|
+
}
|
|
10
|
+
return keys.flatMap((key) => ["-e", key]);
|
|
11
|
+
}
|
|
2
12
|
export function buildDockerRunArgs(input) {
|
|
3
13
|
const args = [input.engine];
|
|
4
14
|
if (input.engine === "docker" && input.context) {
|
|
@@ -21,9 +31,7 @@ export function buildDockerRunArgs(input) {
|
|
|
21
31
|
if (input.cwd !== undefined) {
|
|
22
32
|
args.push("-w", input.cwd);
|
|
23
33
|
}
|
|
24
|
-
|
|
25
|
-
args.push("-e", `${key}=${value}`);
|
|
26
|
-
}
|
|
34
|
+
args.push(...buildDockerEnvArgs(input));
|
|
27
35
|
for (const mount of input.mounts) {
|
|
28
36
|
const volume = `${path.resolve(mount.source)}:${mount.target}${mount.readonly ? ":ro" : ""}`;
|
|
29
37
|
args.push("-v", volume);
|
|
@@ -3,9 +3,10 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { buildDockerRunArgs } from "./args.js";
|
|
6
|
+
import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
|
|
7
7
|
import { buildContextArgs, detectContext } from "./context.js";
|
|
8
8
|
import { detectEngine } from "./engine.js";
|
|
9
|
+
import { createDockerEnvFile } from "./env-file.js";
|
|
9
10
|
import { createHostRunner } from "../host/host-runner.js";
|
|
10
11
|
import { downloadWorkspace as downloadTransferredWorkspace, uploadWorkspace as uploadTransferredWorkspace } from "../workspace-transfer.js";
|
|
11
12
|
const containerCommand = ["sh", "-c", "while :; do sleep 3600; done"];
|
|
@@ -103,25 +104,33 @@ function createDockerEnv(input) {
|
|
|
103
104
|
return downloadTransferredWorkspace(workspaceTransferEnv, opts);
|
|
104
105
|
},
|
|
105
106
|
exec(spec) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
107
|
+
const envFile = createDockerEnvFile(spec.env);
|
|
108
|
+
try {
|
|
109
|
+
return cleanUpEnvFileAfterRun(input.runner.exec({
|
|
110
|
+
command: input.engine,
|
|
111
|
+
args: [
|
|
112
|
+
...buildContextArgs(input.engine, input.context),
|
|
113
|
+
"exec",
|
|
114
|
+
...(spec.stdin === "pipe" || spec.stdin === "inherit" ? ["-i"] : []),
|
|
115
|
+
...(spec.tty === true ? ["-t"] : []),
|
|
116
|
+
...(spec.cwd !== undefined ? ["-w", spec.cwd] : []),
|
|
117
|
+
...buildDockerEnvArgs({ env: spec.env, envFilePath: envFile?.path }),
|
|
118
|
+
containerRef,
|
|
119
|
+
spec.command,
|
|
120
|
+
...(spec.args ?? [])
|
|
121
|
+
],
|
|
122
|
+
stdin: spec.stdin,
|
|
123
|
+
stdout: spec.stdout,
|
|
124
|
+
stderr: spec.stderr,
|
|
125
|
+
tty: spec.tty,
|
|
126
|
+
signal: spec.signal,
|
|
127
|
+
killProcessGroup: spec.killProcessGroup
|
|
128
|
+
}), envFile?.cleanup);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
envFile?.cleanup();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
125
134
|
},
|
|
126
135
|
async detach() {
|
|
127
136
|
return createContainerJob(containerRef, input.runner, input.engine, input.context, detachedJobContext);
|
|
@@ -136,7 +145,8 @@ function createDockerEnv(input) {
|
|
|
136
145
|
stdin: "inherit",
|
|
137
146
|
stdout: "inherit",
|
|
138
147
|
stderr: "inherit",
|
|
139
|
-
tty: true
|
|
148
|
+
tty: true,
|
|
149
|
+
signal: shellSpec?.signal
|
|
140
150
|
});
|
|
141
151
|
},
|
|
142
152
|
async close() {
|
|
@@ -306,9 +316,49 @@ async function runAndRead(runner, spec) {
|
|
|
306
316
|
}
|
|
307
317
|
return output;
|
|
308
318
|
}
|
|
319
|
+
async function runAndReadBytes(runner, spec) {
|
|
320
|
+
const handle = runner.exec(spec);
|
|
321
|
+
const stdout = readStreamBytes(handle.stdout);
|
|
322
|
+
const stderr = readStream(handle.stderr);
|
|
323
|
+
const result = await handle.result;
|
|
324
|
+
const output = await stdout;
|
|
325
|
+
if (result.exitCode !== 0) {
|
|
326
|
+
const errorOutput = await stderr;
|
|
327
|
+
throw new Error(`Command failed with exit code ${result.exitCode}: ${spec.command} ${(spec.args ?? []).join(" ")}${errorOutput ? `\n${errorOutput}` : ""}`);
|
|
328
|
+
}
|
|
329
|
+
return output;
|
|
330
|
+
}
|
|
309
331
|
async function runOrThrow(runner, spec) {
|
|
310
332
|
await runAndRead(runner, spec);
|
|
311
333
|
}
|
|
334
|
+
function cleanUpEnvFileAfterRun(handle, cleanup) {
|
|
335
|
+
if (cleanup === undefined) {
|
|
336
|
+
return handle;
|
|
337
|
+
}
|
|
338
|
+
let cleanedUp = false;
|
|
339
|
+
const cleanupOnce = () => {
|
|
340
|
+
if (cleanedUp) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
cleanedUp = true;
|
|
344
|
+
try {
|
|
345
|
+
cleanup();
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
// Cleanup is best effort; preserve the command result.
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
return {
|
|
352
|
+
pid: handle.pid,
|
|
353
|
+
stdin: handle.stdin,
|
|
354
|
+
stdout: handle.stdout,
|
|
355
|
+
stderr: handle.stderr,
|
|
356
|
+
result: handle.result.finally(cleanupOnce),
|
|
357
|
+
kill(signal) {
|
|
358
|
+
handle.kill(signal);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
312
362
|
async function readStream(stream) {
|
|
313
363
|
if (stream === null) {
|
|
314
364
|
return "";
|
|
@@ -320,15 +370,19 @@ async function readStream(stream) {
|
|
|
320
370
|
}
|
|
321
371
|
return chunks.join("");
|
|
322
372
|
}
|
|
373
|
+
async function readStreamBytes(stream) {
|
|
374
|
+
if (stream === null) {
|
|
375
|
+
return Buffer.alloc(0);
|
|
376
|
+
}
|
|
377
|
+
const chunks = [];
|
|
378
|
+
for await (const chunk of stream) {
|
|
379
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk));
|
|
380
|
+
}
|
|
381
|
+
return Buffer.concat(chunks);
|
|
382
|
+
}
|
|
323
383
|
function sortedBuildArgs(buildArgs) {
|
|
324
384
|
return Object.entries(buildArgs).sort(([left], [right]) => left.localeCompare(right));
|
|
325
385
|
}
|
|
326
|
-
function buildEnvArgs(env) {
|
|
327
|
-
if (env === undefined) {
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
return Object.entries(env).flatMap(([key, value]) => ["-e", `${key}=${value}`]);
|
|
331
|
-
}
|
|
332
386
|
function createContainerName() {
|
|
333
387
|
return `poe-env-${randomBytes(6).toString("hex")}`;
|
|
334
388
|
}
|
|
@@ -383,10 +437,25 @@ function createContainerWorkspaceFileSystem(input) {
|
|
|
383
437
|
await execShell(`mkdir -p ${shellQuote(targetPath)}`);
|
|
384
438
|
},
|
|
385
439
|
async readdir(targetPath) {
|
|
386
|
-
const
|
|
440
|
+
const quotedTargetPath = shellQuote(targetPath);
|
|
441
|
+
const output = await execShell([
|
|
442
|
+
`for item in ${quotedTargetPath}/* ${quotedTargetPath}/.[!.]* ${quotedTargetPath}/..?*; do`,
|
|
443
|
+
`[ -e "$item" ] || [ -L "$item" ] || continue;`,
|
|
444
|
+
`if [ -L "$item" ]; then kind=l; size=0;`,
|
|
445
|
+
`elif [ -d "$item" ]; then kind=d; size=0;`,
|
|
446
|
+
`elif [ -f "$item" ]; then kind=f; size=$(wc -c < "$item");`,
|
|
447
|
+
`else continue; fi;`,
|
|
448
|
+
`printf '%s\\t%s\\t%s\\n' "\${item##*/}" "$kind" "$size";`,
|
|
449
|
+
`done`
|
|
450
|
+
].join(" "));
|
|
387
451
|
return output.split("\n").filter(Boolean).map((line) => {
|
|
388
452
|
const [name = "", kind = "f"] = line.split("\t");
|
|
389
|
-
return {
|
|
453
|
+
return {
|
|
454
|
+
name,
|
|
455
|
+
isFile: () => kind === "f",
|
|
456
|
+
isDirectory: () => kind === "d",
|
|
457
|
+
isSymbolicLink: () => kind === "l"
|
|
458
|
+
};
|
|
390
459
|
});
|
|
391
460
|
},
|
|
392
461
|
readFile: readFileFromContainer,
|
|
@@ -459,8 +528,10 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
459
528
|
? ""
|
|
460
529
|
: ` && test $(stat -c %Y ${logFile} 2>/dev/null || stat -f %m ${logFile}) -ge ${Math.ceil(opts.since.getTime() / 1000)}`;
|
|
461
530
|
let byteOffset = opts?.sinceByte ?? 0;
|
|
531
|
+
let pendingBytes = Buffer.alloc(0);
|
|
532
|
+
let pendingByteOffset = byteOffset;
|
|
462
533
|
while (true) {
|
|
463
|
-
const stdout = await
|
|
534
|
+
const stdout = await runAndReadBytes(runner, {
|
|
464
535
|
command: engine,
|
|
465
536
|
args: [
|
|
466
537
|
...buildContextArgs(engine, context),
|
|
@@ -473,9 +544,18 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
473
544
|
stdout: "pipe",
|
|
474
545
|
stderr: "pipe"
|
|
475
546
|
});
|
|
476
|
-
if (stdout.
|
|
477
|
-
|
|
478
|
-
|
|
547
|
+
if (stdout.byteLength > 0) {
|
|
548
|
+
const combined = pendingBytes.byteLength === 0
|
|
549
|
+
? stdout
|
|
550
|
+
: Buffer.concat([pendingBytes, stdout]);
|
|
551
|
+
const completeLength = completeUtf8PrefixLength(combined);
|
|
552
|
+
byteOffset += stdout.byteLength;
|
|
553
|
+
pendingBytes = combined.subarray(completeLength);
|
|
554
|
+
const data = combined.subarray(0, completeLength).toString("utf8");
|
|
555
|
+
if (data.length > 0) {
|
|
556
|
+
yield { byteOffset: pendingByteOffset, data };
|
|
557
|
+
pendingByteOffset += completeLength;
|
|
558
|
+
}
|
|
479
559
|
}
|
|
480
560
|
if (opts?.follow !== true || (await this.status()) !== "running") {
|
|
481
561
|
return;
|
|
@@ -517,6 +597,39 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
517
597
|
}
|
|
518
598
|
};
|
|
519
599
|
}
|
|
600
|
+
function completeUtf8PrefixLength(contents) {
|
|
601
|
+
if (contents.length === 0) {
|
|
602
|
+
return 0;
|
|
603
|
+
}
|
|
604
|
+
let leadIndex = contents.length - 1;
|
|
605
|
+
while (leadIndex >= 0 && isUtf8ContinuationByte(contents[leadIndex])) {
|
|
606
|
+
leadIndex -= 1;
|
|
607
|
+
}
|
|
608
|
+
if (leadIndex < 0) {
|
|
609
|
+
return contents.length;
|
|
610
|
+
}
|
|
611
|
+
const expectedLength = utf8SequenceLength(contents[leadIndex]);
|
|
612
|
+
if (expectedLength === 0) {
|
|
613
|
+
return contents.length;
|
|
614
|
+
}
|
|
615
|
+
const availableLength = contents.length - leadIndex;
|
|
616
|
+
return availableLength < expectedLength ? leadIndex : contents.length;
|
|
617
|
+
}
|
|
618
|
+
function isUtf8ContinuationByte(byte) {
|
|
619
|
+
return byte >= 0x80 && byte <= 0xbf;
|
|
620
|
+
}
|
|
621
|
+
function utf8SequenceLength(byte) {
|
|
622
|
+
if (byte >= 0xc2 && byte <= 0xdf) {
|
|
623
|
+
return 2;
|
|
624
|
+
}
|
|
625
|
+
if (byte >= 0xe0 && byte <= 0xef) {
|
|
626
|
+
return 3;
|
|
627
|
+
}
|
|
628
|
+
if (byte >= 0xf0 && byte <= 0xf4) {
|
|
629
|
+
return 4;
|
|
630
|
+
}
|
|
631
|
+
return 0;
|
|
632
|
+
}
|
|
520
633
|
async function readDetachedExitCode(containerId, jobId, runner, engine, context) {
|
|
521
634
|
const exitFile = shellQuote(`/tmp/poe-jobs/${jobId}.exit`);
|
|
522
635
|
const handle = runner.exec({
|
|
@@ -3,6 +3,9 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { buildDockerRunArgs } from "./args.js";
|
|
4
4
|
import { buildContextArgs, detectContext } from "./context.js";
|
|
5
5
|
import { detectEngine } from "./engine.js";
|
|
6
|
+
import { createDockerEnvFile } from "./env-file.js";
|
|
7
|
+
const DOCKER_ABORT_GRACE_MS = 10_000;
|
|
8
|
+
const DOCKER_ABORT_FORCE_GRACE_MS = 5_000;
|
|
6
9
|
export function createDockerRunner(options) {
|
|
7
10
|
const engine = options.engine ?? detectEngine();
|
|
8
11
|
const context = options.context ?? detectContext();
|
|
@@ -27,6 +30,7 @@ export function createDockerRunner(options) {
|
|
|
27
30
|
stderrMode === "inherit" &&
|
|
28
31
|
spec.tty === true;
|
|
29
32
|
const containerName = buildContainerName(options.containerName ?? spec.command);
|
|
33
|
+
const envFile = createDockerEnvFile(spec.env);
|
|
30
34
|
const runArgs = buildDockerRunArgs({
|
|
31
35
|
engine,
|
|
32
36
|
context,
|
|
@@ -35,6 +39,7 @@ export function createDockerRunner(options) {
|
|
|
35
39
|
args: spec.args ?? [],
|
|
36
40
|
cwd: spec.cwd,
|
|
37
41
|
env: spec.env,
|
|
42
|
+
envFilePath: envFile?.path,
|
|
38
43
|
mounts: options.mounts ?? [],
|
|
39
44
|
ports: options.ports ?? [],
|
|
40
45
|
network: options.network,
|
|
@@ -46,25 +51,55 @@ export function createDockerRunner(options) {
|
|
|
46
51
|
extraArgs: options.extraArgs ?? []
|
|
47
52
|
});
|
|
48
53
|
const [command, ...args] = runArgs;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
let child;
|
|
55
|
+
try {
|
|
56
|
+
child = childProcess.spawn(command, args, {
|
|
57
|
+
stdio: interactiveMode ? "inherit" : [stdinMode, stdoutMode, stderrMode]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
envFile?.cleanup();
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
52
64
|
let isResultSettled = false;
|
|
65
|
+
let exitCodeOverride = null;
|
|
53
66
|
let resolveResult = null;
|
|
67
|
+
let abortEscalationTimers = [];
|
|
54
68
|
const result = new Promise((resolve) => {
|
|
55
69
|
resolveResult = resolve;
|
|
56
70
|
});
|
|
71
|
+
const clearAbortEscalation = () => {
|
|
72
|
+
for (const timer of abortEscalationTimers) {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
abortEscalationTimers = [];
|
|
76
|
+
};
|
|
57
77
|
const settleResult = (exitCode) => {
|
|
58
78
|
if (isResultSettled) {
|
|
59
79
|
return;
|
|
60
80
|
}
|
|
61
81
|
isResultSettled = true;
|
|
62
82
|
cleanupAbort();
|
|
63
|
-
|
|
83
|
+
clearAbortEscalation();
|
|
84
|
+
envFile?.cleanup();
|
|
85
|
+
resolveResult?.({ exitCode: exitCodeOverride ?? exitCode });
|
|
86
|
+
};
|
|
87
|
+
const scheduleAbortEscalation = () => {
|
|
88
|
+
const terminateTimer = setTimeout(() => {
|
|
89
|
+
killHostDockerChild(child, "SIGTERM");
|
|
90
|
+
const forceTimer = setTimeout(() => {
|
|
91
|
+
killHostDockerChild(child, "SIGKILL");
|
|
92
|
+
}, DOCKER_ABORT_FORCE_GRACE_MS);
|
|
93
|
+
unrefTimer(forceTimer);
|
|
94
|
+
abortEscalationTimers.push(forceTimer);
|
|
95
|
+
}, DOCKER_ABORT_GRACE_MS);
|
|
96
|
+
unrefTimer(terminateTimer);
|
|
97
|
+
abortEscalationTimers.push(terminateTimer);
|
|
64
98
|
};
|
|
65
99
|
const cleanupAbort = bindAbortSignal(spec.signal, () => {
|
|
66
|
-
|
|
100
|
+
exitCodeOverride = 1;
|
|
67
101
|
spawnControlCommand(engine, context, ["stop", containerName]);
|
|
102
|
+
scheduleAbortEscalation();
|
|
68
103
|
});
|
|
69
104
|
child.once("error", () => {
|
|
70
105
|
settleResult(1);
|
|
@@ -93,6 +128,14 @@ export function createDockerRunner(options) {
|
|
|
93
128
|
}
|
|
94
129
|
};
|
|
95
130
|
}
|
|
131
|
+
function killHostDockerChild(child, signal) {
|
|
132
|
+
try {
|
|
133
|
+
child.kill(signal);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
96
139
|
function buildContainerName(name) {
|
|
97
140
|
const suffix = randomBytes(3).toString("hex").slice(0, 6);
|
|
98
141
|
const sanitizedName = sanitizeContainerName(name);
|
|
@@ -123,10 +166,15 @@ function isContainerNameCharacter(char) {
|
|
|
123
166
|
return char === "." || char === "_" || char === "-";
|
|
124
167
|
}
|
|
125
168
|
function spawnControlCommand(engine, context, args) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
169
|
+
try {
|
|
170
|
+
const child = childProcess.spawn(engine, [...buildContextArgs(engine, context), ...args], {
|
|
171
|
+
stdio: "ignore"
|
|
172
|
+
});
|
|
173
|
+
child.once("error", () => undefined);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
130
178
|
}
|
|
131
179
|
function bindAbortSignal(signal, onAbort) {
|
|
132
180
|
if (signal === undefined) {
|
|
@@ -141,3 +189,11 @@ function bindAbortSignal(signal, onAbort) {
|
|
|
141
189
|
signal.removeEventListener("abort", onAbort);
|
|
142
190
|
};
|
|
143
191
|
}
|
|
192
|
+
function unrefTimer(timer) {
|
|
193
|
+
if (typeof timer === "object" &&
|
|
194
|
+
timer !== null &&
|
|
195
|
+
"unref" in timer &&
|
|
196
|
+
typeof timer.unref === "function") {
|
|
197
|
+
timer.unref();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface DockerEnvFile {
|
|
2
|
+
path: string;
|
|
3
|
+
cleanup(): void;
|
|
4
|
+
}
|
|
5
|
+
export declare function createDockerEnvFile(env: Record<string, string> | undefined): DockerEnvFile | null;
|
|
6
|
+
export declare function serializeDockerEnvFile(entries: Array<[string, string]>): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function createDockerEnvFile(env) {
|
|
5
|
+
const entries = Object.entries(env ?? {});
|
|
6
|
+
if (entries.length === 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const directory = mkdtempSync(path.join(tmpdir(), "poe-docker-env-"));
|
|
10
|
+
const filePath = path.join(directory, "env");
|
|
11
|
+
let active = true;
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(filePath, serializeDockerEnvFile(entries), {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
flag: "wx",
|
|
16
|
+
mode: 0o600
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
rmSync(directory, { recursive: true, force: true });
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
path: filePath,
|
|
25
|
+
cleanup() {
|
|
26
|
+
if (!active) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
active = false;
|
|
30
|
+
rmSync(directory, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function serializeDockerEnvFile(entries) {
|
|
35
|
+
return (entries.map(([key, value]) => `${formatDockerEnvKey(key)}=${formatDockerEnvValue(value)}`).join("\n") +
|
|
36
|
+
"\n");
|
|
37
|
+
}
|
|
38
|
+
function formatDockerEnvKey(key) {
|
|
39
|
+
if (key.length === 0 || key.includes("=") || key.includes("\n") || key.includes("\r")) {
|
|
40
|
+
throw new Error(`Invalid Docker environment variable name: ${JSON.stringify(key)}`);
|
|
41
|
+
}
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
function formatDockerEnvValue(value) {
|
|
45
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
46
|
+
throw new Error("Docker env-file values cannot contain newline characters.");
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
2
|
export function createHostRunner(options = {}) {
|
|
3
|
-
const
|
|
3
|
+
const detachedByDefault = options.detached === true;
|
|
4
4
|
return {
|
|
5
5
|
name: "host",
|
|
6
6
|
exec(spec) {
|
|
@@ -17,6 +17,7 @@ export function createHostRunner(options = {}) {
|
|
|
17
17
|
const stdinMode = spec.stdin ?? "ignore";
|
|
18
18
|
const stdoutMode = spec.stdout ?? "pipe";
|
|
19
19
|
const stderrMode = spec.stderr ?? "pipe";
|
|
20
|
+
const killProcessGroup = detachedByDefault || spec.killProcessGroup === true;
|
|
20
21
|
const stdio = stdinMode === "inherit" && stdoutMode === "inherit" && stderrMode === "inherit"
|
|
21
22
|
? "inherit"
|
|
22
23
|
: [stdinMode, stdoutMode, stderrMode];
|
|
@@ -24,13 +25,13 @@ export function createHostRunner(options = {}) {
|
|
|
24
25
|
cwd: spec.cwd,
|
|
25
26
|
env: spec.env,
|
|
26
27
|
stdio,
|
|
27
|
-
...(
|
|
28
|
+
...(killProcessGroup ? { detached: true } : {})
|
|
28
29
|
});
|
|
29
|
-
if (
|
|
30
|
+
if (killProcessGroup) {
|
|
30
31
|
child.unref();
|
|
31
32
|
}
|
|
32
33
|
const kill = (signal) => {
|
|
33
|
-
if (
|
|
34
|
+
if (killProcessGroup && process.platform !== "win32" && child.pid !== undefined) {
|
|
34
35
|
process.kill(-child.pid, signal);
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
@@ -20,6 +20,8 @@ export interface RunSpec {
|
|
|
20
20
|
stderr?: "pipe" | "inherit";
|
|
21
21
|
tty?: boolean;
|
|
22
22
|
signal?: AbortSignal;
|
|
23
|
+
/** Start in a separate process group so kill() can signal the full group where supported. */
|
|
24
|
+
killProcessGroup?: boolean;
|
|
23
25
|
}
|
|
24
26
|
export interface Runner {
|
|
25
27
|
exec(spec: RunSpec): RunHandle;
|
|
@@ -163,6 +165,7 @@ export interface DockerRunArgs {
|
|
|
163
165
|
args: string[];
|
|
164
166
|
cwd?: string;
|
|
165
167
|
env?: Record<string, string>;
|
|
168
|
+
envFilePath?: string;
|
|
166
169
|
mounts: DockerMount[];
|
|
167
170
|
ports: DockerPortMapping[];
|
|
168
171
|
network?: string;
|
|
@@ -4,6 +4,7 @@ export interface WorkspaceTransferDirent {
|
|
|
4
4
|
name: string;
|
|
5
5
|
isFile(): boolean;
|
|
6
6
|
isDirectory(): boolean;
|
|
7
|
+
isSymbolicLink?(): boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface WorkspaceTransferStats {
|
|
9
10
|
isFile(): boolean;
|
|
@@ -20,7 +21,10 @@ export interface WorkspaceTransferFileSystem {
|
|
|
20
21
|
}): Promise<WorkspaceTransferDirent[]>;
|
|
21
22
|
readFile(path: string): Promise<Buffer>;
|
|
22
23
|
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
23
|
-
writeFile(path: string, data: string | Buffer
|
|
24
|
+
writeFile(path: string, data: string | Buffer, options?: {
|
|
25
|
+
flag?: string;
|
|
26
|
+
mode?: number;
|
|
27
|
+
}): Promise<void>;
|
|
24
28
|
stat(path: string): Promise<WorkspaceTransferStats>;
|
|
25
29
|
lstat?(path: string): Promise<WorkspaceTransferStats>;
|
|
26
30
|
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { promises as nodeFs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const uploadState = new WeakMap();
|
|
@@ -82,7 +82,7 @@ export async function downloadWorkspace(env, opts) {
|
|
|
82
82
|
const remoteFs = env.remoteFs ?? localFs;
|
|
83
83
|
const workspaceDir = env.workspaceDir ?? "/workspace";
|
|
84
84
|
const state = uploadState.get(env) ?? new Map();
|
|
85
|
-
const remoteFiles = await listFilesIfExists(remoteFs, workspaceDir);
|
|
85
|
+
const remoteFiles = await listFilesIfExists(remoteFs, workspaceDir, { rejectSymlinks: true });
|
|
86
86
|
const remotePaths = new Set(remoteFiles.map((file) => file.path));
|
|
87
87
|
const conflicts = [];
|
|
88
88
|
let files = 0;
|
|
@@ -96,7 +96,7 @@ export async function downloadWorkspace(env, opts) {
|
|
|
96
96
|
conflicts.push({ path: remoteFile.path, reason: "local_modified" });
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
|
-
await writeFileAtomically(localFs, localPath, remoteContent, ".download-tmp");
|
|
99
|
+
await writeFileAtomically(localFs, env.cwd, localPath, remoteContent, ".download-tmp");
|
|
100
100
|
state.set(remoteFile.path, {
|
|
101
101
|
hash: hashBuffer(remoteContent),
|
|
102
102
|
uploaded: true
|
|
@@ -122,9 +122,9 @@ export async function downloadWorkspace(env, opts) {
|
|
|
122
122
|
}
|
|
123
123
|
return { files, bytes, conflicts };
|
|
124
124
|
}
|
|
125
|
-
async function listFilesIfExists(fs, root) {
|
|
125
|
+
async function listFilesIfExists(fs, root, options = {}) {
|
|
126
126
|
try {
|
|
127
|
-
return await listFiles(fs, root);
|
|
127
|
+
return await listFiles(fs, root, options);
|
|
128
128
|
}
|
|
129
129
|
catch (error) {
|
|
130
130
|
if (isNotFoundError(error)) {
|
|
@@ -133,12 +133,15 @@ async function listFilesIfExists(fs, root) {
|
|
|
133
133
|
throw error;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
async function listFiles(fs, root) {
|
|
136
|
+
async function listFiles(fs, root, options = {}) {
|
|
137
137
|
const result = [];
|
|
138
138
|
async function visit(dir) {
|
|
139
139
|
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
140
140
|
for (const dirent of dirents.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
141
141
|
const absolutePath = path.join(dir, dirent.name);
|
|
142
|
+
if (options.rejectSymlinks === true && dirent.isSymbolicLink?.() === true) {
|
|
143
|
+
throw new Error("Workspace download must not follow symbolic links.");
|
|
144
|
+
}
|
|
142
145
|
if (dirent.isDirectory()) {
|
|
143
146
|
await visit(absolutePath);
|
|
144
147
|
continue;
|
|
@@ -347,15 +350,28 @@ async function renamePath(fs, sourcePath, destinationPath) {
|
|
|
347
350
|
}
|
|
348
351
|
await fs.rename(sourcePath, destinationPath);
|
|
349
352
|
}
|
|
350
|
-
async function writeFileAtomically(fs, destinationPath, data, temporarySuffix) {
|
|
351
|
-
const temporaryPath = `${destinationPath}${temporarySuffix}`;
|
|
353
|
+
async function writeFileAtomically(fs, workspacePath, destinationPath, data, temporarySuffix) {
|
|
354
|
+
const temporaryPath = `${destinationPath}.${randomUUID()}${temporarySuffix}`;
|
|
355
|
+
let temporaryCreated = false;
|
|
352
356
|
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
353
357
|
try {
|
|
354
|
-
await fs
|
|
358
|
+
await assertSafeLocalDownloadPath(fs, workspacePath, temporaryPath);
|
|
359
|
+
try {
|
|
360
|
+
await fs.writeFile(temporaryPath, data, { flag: "wx", mode: 0o600 });
|
|
361
|
+
temporaryCreated = true;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (!isAlreadyExistsError(error)) {
|
|
365
|
+
await removeFile(fs, temporaryPath).catch(() => undefined);
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
355
369
|
await renamePath(fs, temporaryPath, destinationPath);
|
|
356
370
|
}
|
|
357
371
|
catch (error) {
|
|
358
|
-
|
|
372
|
+
if (temporaryCreated) {
|
|
373
|
+
await removeFile(fs, temporaryPath).catch(() => undefined);
|
|
374
|
+
}
|
|
359
375
|
throw error;
|
|
360
376
|
}
|
|
361
377
|
}
|
|
@@ -482,3 +498,6 @@ function stripSlashes(value) {
|
|
|
482
498
|
function isNotFoundError(error) {
|
|
483
499
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
484
500
|
}
|
|
501
|
+
function isAlreadyExistsError(error) {
|
|
502
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
503
|
+
}
|
|
@@ -33,6 +33,7 @@ export interface EncryptedFileStoreInput {
|
|
|
33
33
|
export declare class EncryptedFileStore implements SecretStore {
|
|
34
34
|
private readonly fs;
|
|
35
35
|
private readonly filePath;
|
|
36
|
+
private readonly symbolicLinkCheckStartPath;
|
|
36
37
|
private readonly salt;
|
|
37
38
|
private readonly getMachineIdentity;
|
|
38
39
|
private readonly getRandomBytes;
|
|
@@ -13,6 +13,7 @@ let temporaryFileSequence = 0;
|
|
|
13
13
|
export class EncryptedFileStore {
|
|
14
14
|
fs;
|
|
15
15
|
filePath;
|
|
16
|
+
symbolicLinkCheckStartPath;
|
|
16
17
|
salt;
|
|
17
18
|
getMachineIdentity;
|
|
18
19
|
getRandomBytes;
|
|
@@ -20,7 +21,16 @@ export class EncryptedFileStore {
|
|
|
20
21
|
constructor(input) {
|
|
21
22
|
this.fs = input.fs ?? fs;
|
|
22
23
|
this.salt = input.salt;
|
|
23
|
-
|
|
24
|
+
if (input.filePath === undefined) {
|
|
25
|
+
const homeDirectory = (input.getHomeDirectory ?? homedir)();
|
|
26
|
+
const defaultDirectory = input.defaultDirectory ?? ".auth-store";
|
|
27
|
+
this.filePath = path.join(homeDirectory, defaultDirectory, input.defaultFileName ?? "credentials.enc");
|
|
28
|
+
this.symbolicLinkCheckStartPath = resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
this.filePath = input.filePath;
|
|
32
|
+
this.symbolicLinkCheckStartPath = null;
|
|
33
|
+
}
|
|
24
34
|
this.getMachineIdentity = input.getMachineIdentity ?? defaultMachineIdentity;
|
|
25
35
|
this.getRandomBytes = input.getRandomBytes ?? randomBytes;
|
|
26
36
|
}
|
|
@@ -106,7 +116,7 @@ export class EncryptedFileStore {
|
|
|
106
116
|
}
|
|
107
117
|
async assertRegularCredentialPath() {
|
|
108
118
|
const resolvedPath = path.resolve(this.filePath);
|
|
109
|
-
const protectedPaths =
|
|
119
|
+
const protectedPaths = getProtectedCredentialPaths(resolvedPath, this.symbolicLinkCheckStartPath);
|
|
110
120
|
for (const currentPath of protectedPaths) {
|
|
111
121
|
try {
|
|
112
122
|
const stats = await this.fs.lstat(currentPath);
|
|
@@ -135,6 +145,30 @@ export class EncryptedFileStore {
|
|
|
135
145
|
return this.keyPromise;
|
|
136
146
|
}
|
|
137
147
|
}
|
|
148
|
+
function resolveDefaultDirectoryCheckStart(homeDirectory, defaultDirectory) {
|
|
149
|
+
const [firstSegment] = defaultDirectory.split(/[\\/]+/).filter(Boolean);
|
|
150
|
+
return path.resolve(homeDirectory, firstSegment ?? ".");
|
|
151
|
+
}
|
|
152
|
+
function getProtectedCredentialPaths(resolvedPath, symbolicLinkCheckStartPath) {
|
|
153
|
+
if (symbolicLinkCheckStartPath === null) {
|
|
154
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
155
|
+
}
|
|
156
|
+
const resolvedStartPath = path.resolve(symbolicLinkCheckStartPath);
|
|
157
|
+
if (!isPathInsideOrEqual(resolvedPath, resolvedStartPath)) {
|
|
158
|
+
return [path.dirname(resolvedPath), resolvedPath];
|
|
159
|
+
}
|
|
160
|
+
const protectedPaths = [resolvedStartPath];
|
|
161
|
+
let currentPath = resolvedStartPath;
|
|
162
|
+
for (const segment of path.relative(resolvedStartPath, resolvedPath).split(path.sep).filter(Boolean)) {
|
|
163
|
+
currentPath = path.join(currentPath, segment);
|
|
164
|
+
protectedPaths.push(currentPath);
|
|
165
|
+
}
|
|
166
|
+
return protectedPaths;
|
|
167
|
+
}
|
|
168
|
+
function isPathInsideOrEqual(childPath, parentPath) {
|
|
169
|
+
const relativePath = path.relative(parentPath, childPath);
|
|
170
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
171
|
+
}
|
|
138
172
|
async function removeIfPresent(fileSystem, filePath) {
|
|
139
173
|
try {
|
|
140
174
|
await fileSystem.unlink(filePath);
|
|
@@ -515,6 +515,7 @@ export declare class SseParser {
|
|
|
515
515
|
export interface JsonRpcRequestOptions {
|
|
516
516
|
timeoutMs?: number;
|
|
517
517
|
onRequestId?: (requestId: RequestId) => void;
|
|
518
|
+
onTimeout?: (requestId: RequestId) => void;
|
|
518
519
|
}
|
|
519
520
|
interface JsonRpcRequestContext {
|
|
520
521
|
id: RequestId;
|
|
@@ -258,10 +258,19 @@ export class McpClient {
|
|
|
258
258
|
}
|
|
259
259
|
try {
|
|
260
260
|
let requestId;
|
|
261
|
+
let cancellationSent = false;
|
|
262
|
+
const sendCancellationNotification = () => {
|
|
263
|
+
if (requestId === undefined || cancellationSent) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
cancellationSent = true;
|
|
267
|
+
messageLayer.sendNotification("notifications/cancelled", { requestId });
|
|
268
|
+
};
|
|
261
269
|
const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
|
|
262
270
|
onRequestId: (nextRequestId) => {
|
|
263
271
|
requestId = nextRequestId;
|
|
264
272
|
},
|
|
273
|
+
onTimeout: sendCancellationNotification,
|
|
265
274
|
}).then((result) => {
|
|
266
275
|
if (!isCallToolResult(result)) {
|
|
267
276
|
throw new McpError(ERROR_INVALID_REQUEST, "Invalid tool result");
|
|
@@ -275,9 +284,7 @@ export class McpClient {
|
|
|
275
284
|
let abortListener;
|
|
276
285
|
const abortPromise = new Promise((_, reject) => {
|
|
277
286
|
const rejectWithAbortReason = () => {
|
|
278
|
-
|
|
279
|
-
messageLayer.sendNotification("notifications/cancelled", { requestId });
|
|
280
|
-
}
|
|
287
|
+
sendCancellationNotification();
|
|
281
288
|
reject(signal.reason);
|
|
282
289
|
};
|
|
283
290
|
abortListener = rejectWithAbortReason;
|
|
@@ -2147,6 +2154,7 @@ export class JsonRpcMessageLayer {
|
|
|
2147
2154
|
return new Promise((resolve, reject) => {
|
|
2148
2155
|
const timeout = setTimeout(() => {
|
|
2149
2156
|
this.pendingRequests.delete(id);
|
|
2157
|
+
options.onTimeout?.(id);
|
|
2150
2158
|
reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
|
|
2151
2159
|
}, timeoutMs);
|
|
2152
2160
|
this.pendingRequests.set(id, { resolve, reject, timeout });
|
|
@@ -449,10 +449,19 @@ export class McpClient {
|
|
|
449
449
|
|
|
450
450
|
try {
|
|
451
451
|
let requestId: RequestId | undefined;
|
|
452
|
+
let cancellationSent = false;
|
|
453
|
+
const sendCancellationNotification = () => {
|
|
454
|
+
if (requestId === undefined || cancellationSent) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
cancellationSent = true;
|
|
458
|
+
messageLayer.sendNotification("notifications/cancelled", { requestId });
|
|
459
|
+
};
|
|
452
460
|
const requestPromise = messageLayer.sendRequest("tools/call", requestParams, {
|
|
453
461
|
onRequestId: (nextRequestId) => {
|
|
454
462
|
requestId = nextRequestId;
|
|
455
463
|
},
|
|
464
|
+
onTimeout: sendCancellationNotification,
|
|
456
465
|
}).then((result) => {
|
|
457
466
|
if (!isCallToolResult(result)) {
|
|
458
467
|
throw new McpError(ERROR_INVALID_REQUEST, "Invalid tool result");
|
|
@@ -468,9 +477,7 @@ export class McpClient {
|
|
|
468
477
|
let abortListener: (() => void) | undefined;
|
|
469
478
|
const abortPromise = new Promise<CallToolResult>((_, reject) => {
|
|
470
479
|
const rejectWithAbortReason = () => {
|
|
471
|
-
|
|
472
|
-
messageLayer.sendNotification("notifications/cancelled", { requestId });
|
|
473
|
-
}
|
|
480
|
+
sendCancellationNotification();
|
|
474
481
|
reject(signal.reason);
|
|
475
482
|
};
|
|
476
483
|
|
|
@@ -3134,6 +3141,7 @@ interface ActiveIncomingRequest {
|
|
|
3134
3141
|
export interface JsonRpcRequestOptions {
|
|
3135
3142
|
timeoutMs?: number;
|
|
3136
3143
|
onRequestId?: (requestId: RequestId) => void;
|
|
3144
|
+
onTimeout?: (requestId: RequestId) => void;
|
|
3137
3145
|
}
|
|
3138
3146
|
|
|
3139
3147
|
interface JsonRpcRequestContext {
|
|
@@ -3242,6 +3250,7 @@ export class JsonRpcMessageLayer {
|
|
|
3242
3250
|
return new Promise((resolve, reject) => {
|
|
3243
3251
|
const timeout = setTimeout(() => {
|
|
3244
3252
|
this.pendingRequests.delete(id);
|
|
3253
|
+
options.onTimeout?.(id);
|
|
3245
3254
|
reject(new Error(`JSON-RPC request "${method}" timed out after ${timeoutMs}ms`));
|
|
3246
3255
|
}, timeoutMs);
|
|
3247
3256
|
|
|
@@ -293,6 +293,38 @@ describe("McpClient SDK integration callTool", () => {
|
|
|
293
293
|
}
|
|
294
294
|
});
|
|
295
295
|
|
|
296
|
+
it("cancels an in-flight slow tool call when the request timeout elapses", async () => {
|
|
297
|
+
const server = await createMockSlowToolServer({ delayMs: 1_000, pollIntervalMs: 5 });
|
|
298
|
+
const { client, cleanup } = await createSdkTestPair(server, () =>
|
|
299
|
+
new McpClient({
|
|
300
|
+
clientInfo: {
|
|
301
|
+
name: "test-client",
|
|
302
|
+
version: "1.0.0",
|
|
303
|
+
},
|
|
304
|
+
requestTimeoutMs: 100,
|
|
305
|
+
})
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const callPromise = client.callTool({
|
|
310
|
+
name: "slow",
|
|
311
|
+
arguments: {
|
|
312
|
+
delayMs: 500,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
await waitFor(() => server.wasStarted(), "Timed out waiting for slow tool to start");
|
|
317
|
+
|
|
318
|
+
await expect(callPromise).rejects.toThrow(
|
|
319
|
+
'JSON-RPC request "tools/call" timed out after 100ms'
|
|
320
|
+
);
|
|
321
|
+
await waitFor(() => server.wasCancelled(), "Timed out waiting for slow tool cancellation");
|
|
322
|
+
expect(server.getCancelledRequestIds()).toEqual(server.getStartedRequestIds());
|
|
323
|
+
} finally {
|
|
324
|
+
await cleanup();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
296
328
|
it("rejects with JSON-RPC error code and message for unknown tool names", async () => {
|
|
297
329
|
const server = await createMockErrorServer();
|
|
298
330
|
const { client, cleanup } = await createSdkTestPair(server, () =>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolcraft",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@clack/core": "^1.0.0",
|
|
47
47
|
"@clack/prompts": "^1.0.0",
|
|
48
|
-
"toolcraft-schema": "0.0.
|
|
48
|
+
"toolcraft-schema": "0.0.25",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"jose": "^6.1.2",
|
|
51
51
|
"jsonc-parser": "^3.3.1",
|