kradle 0.6.6 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -8
- package/dist/commands/challenge/build.d.ts +4 -2
- package/dist/commands/challenge/build.js +40 -9
- package/dist/commands/challenge/run.d.ts +7 -0
- package/dist/commands/challenge/run.js +112 -4
- package/dist/config/releases/arena-minecraft.d.ts +1 -0
- package/dist/config/releases/arena-minecraft.js +2 -0
- package/dist/lib/api-client.d.ts +13 -0
- package/dist/lib/api-client.js +12 -0
- package/dist/lib/challenge.d.ts +12 -0
- package/dist/lib/challenge.js +29 -18
- package/dist/lib/local-runner.d.ts +62 -0
- package/dist/lib/local-runner.js +432 -0
- package/dist/lib/schemas.d.ts +1 -0
- package/dist/lib/schemas.js +1 -0
- package/dist/lib/validator.js +2 -0
- package/oclif.manifest.json +89 -58
- package/package.json +1 -1
- package/static/ai_docs/LLM_CLI_REFERENCE.md +53 -11
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
import * as tar from "tar";
|
|
9
|
+
import { MINECRAFT_ARENA_MANAGER_TAG } from "../config/releases/arena-minecraft.js";
|
|
10
|
+
const MC_SERVER_IMAGE = "marctv/minecraft-papermc-server:1.20.4";
|
|
11
|
+
const DEFAULT_ARENA_IMAGE_TEMPLATE = "us-central1-docker.pkg.dev/kradle-prod-449119/prod-arenas/minecraft:{SHA}";
|
|
12
|
+
const MC_PORT = 25565;
|
|
13
|
+
const SERVER_READY_TIMEOUT_MS = 120_000;
|
|
14
|
+
const SERVER_READY_POLL_MS = 3_000;
|
|
15
|
+
const CONTAINER_NAME_PREFIX_MC = "kradle-mc-server";
|
|
16
|
+
const CONTAINER_NAME_PREFIX_ARENA = "kradle-arena";
|
|
17
|
+
export class LocalRunner {
|
|
18
|
+
config;
|
|
19
|
+
mcContainerId = null;
|
|
20
|
+
arenaContainerId = null;
|
|
21
|
+
logProcess = null;
|
|
22
|
+
tempDir = null;
|
|
23
|
+
shutdownInProgress = false;
|
|
24
|
+
constructor(config) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Main orchestration — starts MC server + arena, streams logs, waits for completion.
|
|
29
|
+
*/
|
|
30
|
+
async run(log) {
|
|
31
|
+
this.ensureDockerAvailable();
|
|
32
|
+
const arenaImage = this.resolveArenaImage();
|
|
33
|
+
log(`Arena image: ${arenaImage}`);
|
|
34
|
+
// Pull images
|
|
35
|
+
log("Pulling Docker images (this may take a while on first run)...");
|
|
36
|
+
this.pullImageIfNeeded(MC_SERVER_IMAGE, log);
|
|
37
|
+
this.pullImageIfNeeded(arenaImage, log);
|
|
38
|
+
// Clean up orphaned containers from previous runs
|
|
39
|
+
LocalRunner.cleanupOrphans(log);
|
|
40
|
+
// Prepare server directory (marctv image mounts entire /data)
|
|
41
|
+
this.tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "kradle-local-run-"));
|
|
42
|
+
const serverDir = path.join(this.tempDir, "server");
|
|
43
|
+
const worldDir = path.join(serverDir, "world");
|
|
44
|
+
await fs.mkdir(worldDir, { recursive: true });
|
|
45
|
+
// Download world
|
|
46
|
+
const worldSlug = this.config.challengeData.world;
|
|
47
|
+
if (worldSlug) {
|
|
48
|
+
log(`Downloading world: ${worldSlug}...`);
|
|
49
|
+
await this.downloadWorld(worldDir);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
log("No world specified, using empty world.");
|
|
53
|
+
}
|
|
54
|
+
// Start Minecraft server
|
|
55
|
+
log("Starting Minecraft server...");
|
|
56
|
+
this.mcContainerId = await this.startMinecraftServer(serverDir, worldDir);
|
|
57
|
+
log(`MC server container: ${this.mcContainerId.slice(0, 12)}`);
|
|
58
|
+
// Wait for server to be ready
|
|
59
|
+
log("Waiting for Minecraft server to be ready...");
|
|
60
|
+
await this.waitForServerReady(log);
|
|
61
|
+
log("");
|
|
62
|
+
log(pc.green("========================================"));
|
|
63
|
+
log(pc.green(" Minecraft server is ready!"));
|
|
64
|
+
log(pc.green(` Connect to localhost:${MC_PORT} to join the live game`));
|
|
65
|
+
log(pc.green(" You will be automatically OP'd on join"));
|
|
66
|
+
log(pc.green("========================================"));
|
|
67
|
+
log("");
|
|
68
|
+
// Start arena-minecraft
|
|
69
|
+
log("Starting arena-minecraft...");
|
|
70
|
+
this.arenaContainerId = this.startArena(arenaImage);
|
|
71
|
+
log(`Arena container: ${this.arenaContainerId.slice(0, 12)}`);
|
|
72
|
+
// Stream arena logs
|
|
73
|
+
log("\n--- Arena logs ---\n");
|
|
74
|
+
this.logProcess = this.streamLogs(this.arenaContainerId, log);
|
|
75
|
+
// Wait for arena to finish
|
|
76
|
+
await this.waitForCompletion();
|
|
77
|
+
log("\n--- Arena finished ---\n");
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Gracefully shut down all containers and clean up temp files.
|
|
81
|
+
*/
|
|
82
|
+
async cleanup() {
|
|
83
|
+
if (this.shutdownInProgress)
|
|
84
|
+
return;
|
|
85
|
+
this.shutdownInProgress = true;
|
|
86
|
+
// Kill log streaming
|
|
87
|
+
if (this.logProcess) {
|
|
88
|
+
this.logProcess.kill();
|
|
89
|
+
this.logProcess = null;
|
|
90
|
+
}
|
|
91
|
+
// Graceful arena shutdown
|
|
92
|
+
if (this.arenaContainerId) {
|
|
93
|
+
try {
|
|
94
|
+
await fetch(`http://localhost:3002/shutdown`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
body: JSON.stringify({ reason: "CLI shutdown" }),
|
|
98
|
+
signal: AbortSignal.timeout(10_000),
|
|
99
|
+
});
|
|
100
|
+
await new Promise((r) => setTimeout(r, 3_000));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Arena may already be stopped
|
|
104
|
+
}
|
|
105
|
+
this.stopContainer(this.arenaContainerId);
|
|
106
|
+
this.arenaContainerId = null;
|
|
107
|
+
}
|
|
108
|
+
// Stop MC server
|
|
109
|
+
if (this.mcContainerId) {
|
|
110
|
+
this.stopContainer(this.mcContainerId);
|
|
111
|
+
this.mcContainerId = null;
|
|
112
|
+
}
|
|
113
|
+
// Clean up temp directory
|
|
114
|
+
if (this.tempDir) {
|
|
115
|
+
await fs.rm(this.tempDir, { recursive: true, force: true });
|
|
116
|
+
this.tempDir = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// --- Private methods ---
|
|
120
|
+
ensureDockerAvailable() {
|
|
121
|
+
try {
|
|
122
|
+
this.docker("info", { stdio: "ignore" });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new Error("Docker is required for --local runs but is not available.\n" +
|
|
126
|
+
"Make sure Docker Desktop is running, or install it from https://docker.com");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
resolveArenaImage() {
|
|
130
|
+
if (this.config.arenaImageOverride) {
|
|
131
|
+
return this.config.arenaImageOverride;
|
|
132
|
+
}
|
|
133
|
+
return DEFAULT_ARENA_IMAGE_TEMPLATE.replace("{SHA}", MINECRAFT_ARENA_MANAGER_TAG);
|
|
134
|
+
}
|
|
135
|
+
pullImageIfNeeded(image, log) {
|
|
136
|
+
try {
|
|
137
|
+
this.docker(`inspect --type=image ${image}`, { stdio: "ignore" });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
log(`Pulling ${image}...`);
|
|
141
|
+
this.docker(`pull ${image}`, { stdio: "inherit" });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async downloadWorld(worldDir) {
|
|
145
|
+
const worldSlug = this.config.challengeData.world;
|
|
146
|
+
if (!worldSlug)
|
|
147
|
+
return;
|
|
148
|
+
const api = new (await import("./api-client.js")).ApiClient(this.config.apiUrl, this.config.apiKey);
|
|
149
|
+
const { downloadUrl } = await api.getWorldDownloadUrl(worldSlug);
|
|
150
|
+
const response = await fetch(downloadUrl);
|
|
151
|
+
if (!response.ok) {
|
|
152
|
+
throw new Error(`Failed to download world "${worldSlug}": ${response.status} ${response.statusText}`);
|
|
153
|
+
}
|
|
154
|
+
// Save and extract tarball
|
|
155
|
+
const tarballPath = path.join(worldDir, "world.tar.gz");
|
|
156
|
+
const buffer = await response.arrayBuffer();
|
|
157
|
+
await fs.writeFile(tarballPath, Buffer.from(buffer));
|
|
158
|
+
// Extract to temp, then find level.dat root
|
|
159
|
+
const extractDir = path.join(worldDir, "_extract");
|
|
160
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
161
|
+
await tar.extract({ file: tarballPath, cwd: extractDir });
|
|
162
|
+
// Find the directory containing level.dat (may be nested under an ID folder)
|
|
163
|
+
const levelDatDir = await this.findLevelDat(extractDir);
|
|
164
|
+
// Move world files to worldDir root
|
|
165
|
+
const entries = await fs.readdir(levelDatDir, { withFileTypes: true });
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
const src = path.join(levelDatDir, entry.name);
|
|
168
|
+
const dst = path.join(worldDir, entry.name);
|
|
169
|
+
await fs.rename(src, dst);
|
|
170
|
+
}
|
|
171
|
+
// Clean up extraction artifacts
|
|
172
|
+
await fs.rm(extractDir, { recursive: true, force: true });
|
|
173
|
+
await fs.rm(tarballPath, { force: true });
|
|
174
|
+
}
|
|
175
|
+
async findLevelDat(dir) {
|
|
176
|
+
if (existsSync(path.join(dir, "level.dat"))) {
|
|
177
|
+
return dir;
|
|
178
|
+
}
|
|
179
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
if (entry.isDirectory()) {
|
|
182
|
+
const sub = path.join(dir, entry.name);
|
|
183
|
+
if (existsSync(path.join(sub, "level.dat"))) {
|
|
184
|
+
return sub;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`Could not find level.dat in downloaded world`);
|
|
189
|
+
}
|
|
190
|
+
async startMinecraftServer(serverDir, worldDir) {
|
|
191
|
+
const containerName = `${CONTAINER_NAME_PREFIX_MC}-${crypto.randomUUID().slice(0, 8)}`;
|
|
192
|
+
const gameMode = this.config.challengeConfig.challengeConfig?.gameMode ?? "survival";
|
|
193
|
+
// Write server.properties (marctv image has no env vars for these)
|
|
194
|
+
const serverProperties = [
|
|
195
|
+
`server-port=${MC_PORT}`,
|
|
196
|
+
`max-players=10`,
|
|
197
|
+
`gamemode=${gameMode}`,
|
|
198
|
+
`difficulty=normal`,
|
|
199
|
+
`spawn-protection=0`,
|
|
200
|
+
`online-mode=false`,
|
|
201
|
+
`enable-command-block=false`,
|
|
202
|
+
`enable-rcon=true`,
|
|
203
|
+
`rcon.password=minecraft`,
|
|
204
|
+
`rcon.port=25575`,
|
|
205
|
+
`level-name=world`,
|
|
206
|
+
`function-permission-level=4`,
|
|
207
|
+
].join("\n");
|
|
208
|
+
await fs.writeFile(path.join(serverDir, "server.properties"), serverProperties);
|
|
209
|
+
// Write ops.json with offline-mode UUIDs for watcher/viewer
|
|
210
|
+
const operators = [
|
|
211
|
+
{ name: "watcher", uuid: this.generateOfflineUUID("watcher") },
|
|
212
|
+
{ name: "KradleWebViewer", uuid: this.generateOfflineUUID("KradleWebViewer") },
|
|
213
|
+
];
|
|
214
|
+
const opsJson = operators.map((op) => ({
|
|
215
|
+
uuid: op.uuid,
|
|
216
|
+
name: op.name,
|
|
217
|
+
level: 4,
|
|
218
|
+
bypassesPlayerLimit: false,
|
|
219
|
+
}));
|
|
220
|
+
await fs.writeFile(path.join(serverDir, "ops.json"), JSON.stringify(opsJson, null, 2));
|
|
221
|
+
// Copy datapack into world dir
|
|
222
|
+
const datapackDst = path.join(worldDir, "datapacks", "kradle");
|
|
223
|
+
if (existsSync(this.config.datapackPath)) {
|
|
224
|
+
execSync(`cp -r "${this.config.datapackPath}" "${datapackDst}"`);
|
|
225
|
+
}
|
|
226
|
+
// Add auto-op datapack — ops any player who joins (local dev convenience)
|
|
227
|
+
await this.writeAutoOpDatapack(path.join(worldDir, "datapacks", "kradle-local-ops"));
|
|
228
|
+
const args = [
|
|
229
|
+
"run",
|
|
230
|
+
"-d",
|
|
231
|
+
"--name",
|
|
232
|
+
containerName,
|
|
233
|
+
"-p",
|
|
234
|
+
`${MC_PORT}:${MC_PORT}`,
|
|
235
|
+
"-v",
|
|
236
|
+
`${serverDir}:/data`,
|
|
237
|
+
"-e",
|
|
238
|
+
"MEMORYSIZE=2G",
|
|
239
|
+
MC_SERVER_IMAGE,
|
|
240
|
+
];
|
|
241
|
+
return this.docker(args.join(" ")).trim();
|
|
242
|
+
}
|
|
243
|
+
async waitForServerReady(log) {
|
|
244
|
+
const start = Date.now();
|
|
245
|
+
while (Date.now() - start < SERVER_READY_TIMEOUT_MS) {
|
|
246
|
+
try {
|
|
247
|
+
const logs = this.docker(`logs ${this.mcContainerId}`).toString();
|
|
248
|
+
if (logs.includes("Done (")) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Container may not be ready yet
|
|
254
|
+
}
|
|
255
|
+
const elapsed = Math.round((Date.now() - start) / 1000);
|
|
256
|
+
log(` Waiting... (${elapsed}s)`);
|
|
257
|
+
await new Promise((r) => setTimeout(r, SERVER_READY_POLL_MS));
|
|
258
|
+
}
|
|
259
|
+
throw new Error(`Minecraft server did not become ready within ${SERVER_READY_TIMEOUT_MS / 1000}s`);
|
|
260
|
+
}
|
|
261
|
+
startArena(arenaImage) {
|
|
262
|
+
const containerName = `${CONTAINER_NAME_PREFIX_ARENA}-${crypto.randomUUID().slice(0, 8)}`;
|
|
263
|
+
// Pass the full backend job response directly to the arena — same as Studio.
|
|
264
|
+
const jobPayload = JSON.stringify(this.config.jobPayload);
|
|
265
|
+
// Arena prepends /v0 to all API paths, so strip it from our URL to avoid /v0/v0
|
|
266
|
+
const arenaApiUrl = this.config.apiUrl.replace(/\/v0\/?$/, "");
|
|
267
|
+
// Derive agent URL from API URL (dev-api → dev-agents, api → agents, etc.)
|
|
268
|
+
const agentBaseUrl = arenaApiUrl.replace("api.kradle.ai", "agents.kradle.ai");
|
|
269
|
+
const args = [
|
|
270
|
+
"run",
|
|
271
|
+
"-d",
|
|
272
|
+
"--name",
|
|
273
|
+
containerName,
|
|
274
|
+
`--net=container:${this.mcContainerId}`,
|
|
275
|
+
"-e",
|
|
276
|
+
"PORT=3002",
|
|
277
|
+
"-e",
|
|
278
|
+
`KRADLE_API_URL=${arenaApiUrl}`,
|
|
279
|
+
"-e",
|
|
280
|
+
`PROMPT_AGENT_BASE_URL=${agentBaseUrl}`,
|
|
281
|
+
"-e",
|
|
282
|
+
"MAX_IDLE_TIME=-1",
|
|
283
|
+
arenaImage,
|
|
284
|
+
`--job=${jobPayload}`,
|
|
285
|
+
];
|
|
286
|
+
// Use execFileSync to bypass shell — the JSON payload contains quotes,
|
|
287
|
+
// parentheses, and other characters that break shell parsing.
|
|
288
|
+
const result = execFileSync("docker", args, {
|
|
289
|
+
encoding: "utf-8",
|
|
290
|
+
env: this.getDockerEnv(),
|
|
291
|
+
});
|
|
292
|
+
return (result ?? "").trim();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Write a small datapack that auto-ops any player who joins.
|
|
296
|
+
* Uses a tick function with function-permission-level=4 to run /op on new players.
|
|
297
|
+
*/
|
|
298
|
+
async writeAutoOpDatapack(datapackDir) {
|
|
299
|
+
const functionsDir = path.join(datapackDir, "data", "kradle_local", "functions");
|
|
300
|
+
const tagsDir = path.join(datapackDir, "data", "minecraft", "tags", "functions");
|
|
301
|
+
await fs.mkdir(functionsDir, { recursive: true });
|
|
302
|
+
await fs.mkdir(tagsDir, { recursive: true });
|
|
303
|
+
await fs.writeFile(path.join(datapackDir, "pack.mcmeta"), JSON.stringify({ pack: { pack_format: 26, description: "Auto-OP for local dev" } }));
|
|
304
|
+
// Tick function: op any player not yet tagged, then tag them so it only runs once
|
|
305
|
+
await fs.writeFile(path.join(functionsDir, "auto_op.mcfunction"), "execute as @a[tag=!kradle_opped] run op @s[type=player]\nexecute as @a[tag=!kradle_opped] run tag @s add kradle_opped\n");
|
|
306
|
+
// Register as a tick function
|
|
307
|
+
await fs.writeFile(path.join(tagsDir, "tick.json"), JSON.stringify({ values: ["kradle_local:auto_op"] }));
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Generate an offline-mode UUID from a Minecraft username.
|
|
311
|
+
* Replicates the Java algorithm: UUID.nameUUIDFromBytes("OfflinePlayer:" + name)
|
|
312
|
+
*/
|
|
313
|
+
generateOfflineUUID(username) {
|
|
314
|
+
const md5 = crypto.createHash("md5").update(`OfflinePlayer:${username}`).digest();
|
|
315
|
+
const bytes = [...md5];
|
|
316
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x30; // version 3
|
|
317
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 2
|
|
318
|
+
const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
|
|
319
|
+
return [
|
|
320
|
+
hex.slice(0, 4).join(""),
|
|
321
|
+
hex.slice(4, 6).join(""),
|
|
322
|
+
hex.slice(6, 8).join(""),
|
|
323
|
+
hex.slice(8, 10).join(""),
|
|
324
|
+
hex.slice(10, 16).join(""),
|
|
325
|
+
].join("-");
|
|
326
|
+
}
|
|
327
|
+
streamLogs(containerId, log) {
|
|
328
|
+
const proc = spawn("docker", ["logs", "-f", containerId], {
|
|
329
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
330
|
+
env: this.getDockerEnv(),
|
|
331
|
+
});
|
|
332
|
+
proc.stdout?.on("data", (data) => {
|
|
333
|
+
for (const line of data.toString().split("\n")) {
|
|
334
|
+
if (line.trim())
|
|
335
|
+
log(line);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
proc.stderr?.on("data", (data) => {
|
|
339
|
+
for (const line of data.toString().split("\n")) {
|
|
340
|
+
if (line.trim())
|
|
341
|
+
log(line);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
return proc;
|
|
345
|
+
}
|
|
346
|
+
async waitForCompletion() {
|
|
347
|
+
// Wait for the arena container to exit
|
|
348
|
+
return new Promise((resolve) => {
|
|
349
|
+
const check = () => {
|
|
350
|
+
try {
|
|
351
|
+
const result = this.docker(`inspect --format={{.State.Running}} ${this.arenaContainerId}`).trim();
|
|
352
|
+
if (result === "false") {
|
|
353
|
+
resolve();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// Container removed or not found — treat as finished
|
|
359
|
+
resolve();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
setTimeout(check, 3_000);
|
|
363
|
+
};
|
|
364
|
+
// Start checking after a brief delay
|
|
365
|
+
setTimeout(check, 5_000);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
stopContainer(containerId) {
|
|
369
|
+
try {
|
|
370
|
+
this.docker(`stop ${containerId}`, { timeout: 30_000 });
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
// May already be stopped
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
this.docker(`rm -f ${containerId}`);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// May already be removed
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Find and remove orphaned containers from previous local runs.
|
|
384
|
+
*/
|
|
385
|
+
static cleanupOrphans(log) {
|
|
386
|
+
const env = { ...process.env, PATH: `${process.env.PATH}:/opt/homebrew/bin:/usr/local/bin` };
|
|
387
|
+
for (const prefix of [CONTAINER_NAME_PREFIX_MC, CONTAINER_NAME_PREFIX_ARENA]) {
|
|
388
|
+
try {
|
|
389
|
+
const output = execSync(`docker ps -a --filter "name=${prefix}" --format "{{.ID}}"`, {
|
|
390
|
+
encoding: "utf-8",
|
|
391
|
+
env,
|
|
392
|
+
}).trim();
|
|
393
|
+
if (output) {
|
|
394
|
+
for (const id of output.split("\n")) {
|
|
395
|
+
if (id.trim()) {
|
|
396
|
+
log?.(`Cleaning up orphaned container: ${id.trim().slice(0, 12)}`);
|
|
397
|
+
try {
|
|
398
|
+
execSync(`docker rm -f ${id.trim()}`, { env, stdio: "ignore" });
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
// Ignore
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// Docker not available or no containers found
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Run a Docker command with PATH augmented for macOS Docker Desktop.
|
|
414
|
+
*/
|
|
415
|
+
docker(args, options) {
|
|
416
|
+
const env = this.getDockerEnv();
|
|
417
|
+
const stdio = options?.stdio ?? "pipe";
|
|
418
|
+
const result = execSync(`docker ${args}`, {
|
|
419
|
+
encoding: "utf-8",
|
|
420
|
+
env,
|
|
421
|
+
stdio,
|
|
422
|
+
timeout: options?.timeout,
|
|
423
|
+
});
|
|
424
|
+
return result ?? "";
|
|
425
|
+
}
|
|
426
|
+
getDockerEnv() {
|
|
427
|
+
return {
|
|
428
|
+
...process.env,
|
|
429
|
+
PATH: `${process.env.PATH}:/opt/homebrew/bin:/usr/local/bin`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
package/dist/lib/schemas.d.ts
CHANGED
|
@@ -192,6 +192,7 @@ export declare const JobResponseSchema: z.ZodObject<{
|
|
|
192
192
|
inputOrder: z.ZodNumber;
|
|
193
193
|
}, z.core.$strip>>>;
|
|
194
194
|
id: z.ZodOptional<z.ZodString>;
|
|
195
|
+
jobApiKey: z.ZodOptional<z.ZodString>;
|
|
195
196
|
}, z.core.$strip>;
|
|
196
197
|
export declare const RunStatusSchema: z.ZodObject<{
|
|
197
198
|
id: z.ZodString;
|
package/dist/lib/schemas.js
CHANGED
|
@@ -73,6 +73,7 @@ export const JobResponseSchema = z.object({
|
|
|
73
73
|
runIds: z.array(z.string()).optional(),
|
|
74
74
|
participants: z.record(z.string(), RunParticipantSchema).optional(),
|
|
75
75
|
id: z.string().optional(),
|
|
76
|
+
jobApiKey: z.string().optional(),
|
|
76
77
|
});
|
|
77
78
|
export const RunStatusSchema = z.object({
|
|
78
79
|
id: z.string(),
|
package/dist/lib/validator.js
CHANGED
|
@@ -12,6 +12,8 @@ import pc from "picocolors";
|
|
|
12
12
|
*/
|
|
13
13
|
const FILTERED_ERROR_MESSAGES = [
|
|
14
14
|
"Fake names cannot be longer than 40 characters", // Actually they can be longer
|
|
15
|
+
"Objective names cannot be longer than 16 characters", // Actually they can be longer
|
|
16
|
+
"Player names cannot be longer than 16 characters", // Actually they can be longer
|
|
15
17
|
];
|
|
16
18
|
/**
|
|
17
19
|
* Get a human-readable severity label with color
|
package/oclif.manifest.json
CHANGED
|
@@ -87,50 +87,6 @@
|
|
|
87
87
|
"update.js"
|
|
88
88
|
]
|
|
89
89
|
},
|
|
90
|
-
"agent:list": {
|
|
91
|
-
"aliases": [],
|
|
92
|
-
"args": {},
|
|
93
|
-
"description": "List all agents",
|
|
94
|
-
"examples": [
|
|
95
|
-
"<%= config.bin %> <%= command.id %>"
|
|
96
|
-
],
|
|
97
|
-
"flags": {
|
|
98
|
-
"api-key": {
|
|
99
|
-
"description": "Kradle API key",
|
|
100
|
-
"env": "KRADLE_API_KEY",
|
|
101
|
-
"name": "api-key",
|
|
102
|
-
"required": true,
|
|
103
|
-
"hasDynamicHelp": false,
|
|
104
|
-
"multiple": false,
|
|
105
|
-
"type": "option"
|
|
106
|
-
},
|
|
107
|
-
"api-url": {
|
|
108
|
-
"description": "Kradle Web API URL",
|
|
109
|
-
"env": "KRADLE_API_URL",
|
|
110
|
-
"name": "api-url",
|
|
111
|
-
"required": true,
|
|
112
|
-
"default": "https://api.kradle.ai/v0",
|
|
113
|
-
"hasDynamicHelp": false,
|
|
114
|
-
"multiple": false,
|
|
115
|
-
"type": "option"
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
"hasDynamicHelp": false,
|
|
119
|
-
"hiddenAliases": [],
|
|
120
|
-
"id": "agent:list",
|
|
121
|
-
"pluginAlias": "kradle",
|
|
122
|
-
"pluginName": "kradle",
|
|
123
|
-
"pluginType": "core",
|
|
124
|
-
"strict": true,
|
|
125
|
-
"enableJsonFlag": false,
|
|
126
|
-
"isESM": true,
|
|
127
|
-
"relativePath": [
|
|
128
|
-
"dist",
|
|
129
|
-
"commands",
|
|
130
|
-
"agent",
|
|
131
|
-
"list.js"
|
|
132
|
-
]
|
|
133
|
-
},
|
|
134
90
|
"ai-docs:challenges-sdk": {
|
|
135
91
|
"aliases": [],
|
|
136
92
|
"args": {
|
|
@@ -215,12 +171,12 @@
|
|
|
215
171
|
"aliases": [],
|
|
216
172
|
"args": {
|
|
217
173
|
"challengeSlug": {
|
|
218
|
-
"description": "Challenge slug to build
|
|
174
|
+
"description": "Challenge slug to build. Can be used multiple times to build multiple challenges at once. Incompatible with --all flag.",
|
|
219
175
|
"name": "challengeSlug",
|
|
220
176
|
"required": false
|
|
221
177
|
}
|
|
222
178
|
},
|
|
223
|
-
"description": "Build
|
|
179
|
+
"description": "Build challenge datapack locally, with optional upload",
|
|
224
180
|
"examples": [
|
|
225
181
|
"<%= config.bin %> <%= command.id %> my-challenge",
|
|
226
182
|
"<%= config.bin %> <%= command.id %> my-challenge my-other-challenge",
|
|
@@ -242,30 +198,34 @@
|
|
|
242
198
|
"type": "boolean"
|
|
243
199
|
},
|
|
244
200
|
"no-validate": {
|
|
245
|
-
"description": "Skip datapack validation
|
|
201
|
+
"description": "Skip datapack validation",
|
|
246
202
|
"name": "no-validate",
|
|
247
203
|
"allowNo": false,
|
|
248
204
|
"type": "boolean"
|
|
249
205
|
},
|
|
250
|
-
"
|
|
251
|
-
"description": "
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"hasDynamicHelp": false,
|
|
256
|
-
"multiple": false,
|
|
257
|
-
"type": "option"
|
|
206
|
+
"no-upload": {
|
|
207
|
+
"description": "Build datapack locally only (skip cloud config/datapack upload)",
|
|
208
|
+
"name": "no-upload",
|
|
209
|
+
"allowNo": false,
|
|
210
|
+
"type": "boolean"
|
|
258
211
|
},
|
|
259
212
|
"api-url": {
|
|
260
213
|
"description": "Kradle Web API URL",
|
|
261
214
|
"env": "KRADLE_API_URL",
|
|
262
215
|
"name": "api-url",
|
|
263
|
-
"required": true,
|
|
264
216
|
"default": "https://api.kradle.ai/v0",
|
|
265
217
|
"hasDynamicHelp": false,
|
|
266
218
|
"multiple": false,
|
|
267
219
|
"type": "option"
|
|
268
220
|
},
|
|
221
|
+
"api-key": {
|
|
222
|
+
"description": "Kradle API key",
|
|
223
|
+
"env": "KRADLE_API_KEY",
|
|
224
|
+
"name": "api-key",
|
|
225
|
+
"hasDynamicHelp": false,
|
|
226
|
+
"multiple": false,
|
|
227
|
+
"type": "option"
|
|
228
|
+
},
|
|
269
229
|
"challenges-path": {
|
|
270
230
|
"description": "Absolute path to the challenges directory",
|
|
271
231
|
"env": "KRADLE_CHALLENGES_PATH",
|
|
@@ -575,7 +535,10 @@
|
|
|
575
535
|
"<%= config.bin %> <%= command.id %> my-challenge --no-open",
|
|
576
536
|
"<%= config.bin %> <%= command.id %> my-challenge --no-wait",
|
|
577
537
|
"<%= config.bin %> <%= command.id %> my-challenge team-kradle:gemini-3-flash,team-kradle:grok-4-1-fast",
|
|
578
|
-
"<%= config.bin %> <%= command.id %> capture-the-flag red=team-kradle:gemini-3-flash blue=team-kradle:grok-4-1-fast"
|
|
538
|
+
"<%= config.bin %> <%= command.id %> capture-the-flag red=team-kradle:gemini-3-flash blue=team-kradle:grok-4-1-fast",
|
|
539
|
+
"<%= config.bin %> <%= command.id %> my-challenge --local",
|
|
540
|
+
"<%= config.bin %> <%= command.id %> my-challenge --local team-kradle:gemini-3-flash,team-kradle:grok-4-1-fast",
|
|
541
|
+
"<%= config.bin %> <%= command.id %> my-challenge --local --arena-image us-central1-docker.pkg.dev/mckradle-3c267/dev-arenas/minecraft:abc1234"
|
|
579
542
|
],
|
|
580
543
|
"flags": {
|
|
581
544
|
"studio": {
|
|
@@ -592,6 +555,21 @@
|
|
|
592
555
|
"allowNo": false,
|
|
593
556
|
"type": "boolean"
|
|
594
557
|
},
|
|
558
|
+
"local": {
|
|
559
|
+
"char": "l",
|
|
560
|
+
"description": "Run the challenge locally using Docker (spins up MC server + arena-minecraft)",
|
|
561
|
+
"name": "local",
|
|
562
|
+
"allowNo": false,
|
|
563
|
+
"type": "boolean"
|
|
564
|
+
},
|
|
565
|
+
"arena-image": {
|
|
566
|
+
"description": "Override arena-minecraft Docker image URL (for testing dev/staging builds)",
|
|
567
|
+
"env": "KRADLE_ARENA_IMAGE",
|
|
568
|
+
"name": "arena-image",
|
|
569
|
+
"hasDynamicHelp": false,
|
|
570
|
+
"multiple": false,
|
|
571
|
+
"type": "option"
|
|
572
|
+
},
|
|
595
573
|
"no-open": {
|
|
596
574
|
"description": "Don't open the run URL in the browser",
|
|
597
575
|
"name": "no-open",
|
|
@@ -664,6 +642,15 @@
|
|
|
664
642
|
"hasDynamicHelp": false,
|
|
665
643
|
"multiple": false,
|
|
666
644
|
"type": "option"
|
|
645
|
+
},
|
|
646
|
+
"challenges-path": {
|
|
647
|
+
"description": "Absolute path to the challenges directory",
|
|
648
|
+
"env": "KRADLE_CHALLENGES_PATH",
|
|
649
|
+
"name": "challenges-path",
|
|
650
|
+
"default": "~/Documents/kradle-studio/challenges",
|
|
651
|
+
"hasDynamicHelp": false,
|
|
652
|
+
"multiple": false,
|
|
653
|
+
"type": "option"
|
|
667
654
|
}
|
|
668
655
|
},
|
|
669
656
|
"hasDynamicHelp": false,
|
|
@@ -1403,6 +1390,50 @@
|
|
|
1403
1390
|
"push.js"
|
|
1404
1391
|
]
|
|
1405
1392
|
},
|
|
1393
|
+
"agent:list": {
|
|
1394
|
+
"aliases": [],
|
|
1395
|
+
"args": {},
|
|
1396
|
+
"description": "List all agents",
|
|
1397
|
+
"examples": [
|
|
1398
|
+
"<%= config.bin %> <%= command.id %>"
|
|
1399
|
+
],
|
|
1400
|
+
"flags": {
|
|
1401
|
+
"api-key": {
|
|
1402
|
+
"description": "Kradle API key",
|
|
1403
|
+
"env": "KRADLE_API_KEY",
|
|
1404
|
+
"name": "api-key",
|
|
1405
|
+
"required": true,
|
|
1406
|
+
"hasDynamicHelp": false,
|
|
1407
|
+
"multiple": false,
|
|
1408
|
+
"type": "option"
|
|
1409
|
+
},
|
|
1410
|
+
"api-url": {
|
|
1411
|
+
"description": "Kradle Web API URL",
|
|
1412
|
+
"env": "KRADLE_API_URL",
|
|
1413
|
+
"name": "api-url",
|
|
1414
|
+
"required": true,
|
|
1415
|
+
"default": "https://api.kradle.ai/v0",
|
|
1416
|
+
"hasDynamicHelp": false,
|
|
1417
|
+
"multiple": false,
|
|
1418
|
+
"type": "option"
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1421
|
+
"hasDynamicHelp": false,
|
|
1422
|
+
"hiddenAliases": [],
|
|
1423
|
+
"id": "agent:list",
|
|
1424
|
+
"pluginAlias": "kradle",
|
|
1425
|
+
"pluginName": "kradle",
|
|
1426
|
+
"pluginType": "core",
|
|
1427
|
+
"strict": true,
|
|
1428
|
+
"enableJsonFlag": false,
|
|
1429
|
+
"isESM": true,
|
|
1430
|
+
"relativePath": [
|
|
1431
|
+
"dist",
|
|
1432
|
+
"commands",
|
|
1433
|
+
"agent",
|
|
1434
|
+
"list.js"
|
|
1435
|
+
]
|
|
1436
|
+
},
|
|
1406
1437
|
"challenge:runs:get": {
|
|
1407
1438
|
"aliases": [],
|
|
1408
1439
|
"args": {
|
|
@@ -1524,5 +1555,5 @@
|
|
|
1524
1555
|
]
|
|
1525
1556
|
}
|
|
1526
1557
|
},
|
|
1527
|
-
"version": "0.6.
|
|
1558
|
+
"version": "0.6.8"
|
|
1528
1559
|
}
|