kradle 0.6.0 → 0.6.1
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 +27 -2
- package/dist/commands/challenge/create.js +8 -1
- package/dist/commands/world/pull.js +1 -0
- package/dist/lib/api-client.js +20 -3
- package/dist/lib/challenge.js +11 -1
- package/dist/lib/experiment/experimenter.js +21 -6
- package/dist/lib/schemas.d.ts +93 -0
- package/dist/lib/schemas.js +31 -0
- package/dist/lib/world.js +7 -1
- package/dist/lib/zod-errors.d.ts +27 -0
- package/dist/lib/zod-errors.js +54 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/static/ai_docs/LLM_CLI_REFERENCE.md +64 -1
- package/static/challenge.ts +3 -3
package/README.md
CHANGED
|
@@ -335,6 +335,32 @@ kradle world delete my-world # Interactive confirmation
|
|
|
335
335
|
kradle world delete my-world --yes # Skip confirmation
|
|
336
336
|
```
|
|
337
337
|
|
|
338
|
+
### World Configuration
|
|
339
|
+
|
|
340
|
+
Each world has a `config.ts` file in `worlds/<slug>/config.ts`. You can define named locations with coordinates:
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
export const config = {
|
|
344
|
+
name: "My World",
|
|
345
|
+
description: "A custom Minecraft world",
|
|
346
|
+
domain: "minecraft",
|
|
347
|
+
worldConfig: {
|
|
348
|
+
locations: {
|
|
349
|
+
spawn: {
|
|
350
|
+
name: "Spawn Point",
|
|
351
|
+
coordinates: { x: 0, y: 64, z: 0 }
|
|
352
|
+
},
|
|
353
|
+
goal: {
|
|
354
|
+
name: "Goal Location",
|
|
355
|
+
coordinates: { x: 100, y: 70, z: 200 }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Locations are synced to the cloud when you push the world.
|
|
363
|
+
|
|
338
364
|
## Agent Commands
|
|
339
365
|
|
|
340
366
|
### List Agents
|
|
@@ -404,8 +430,7 @@ The repository provides the `kradle` CLI command. It runs compiled JavaScript fr
|
|
|
404
430
|
```bash
|
|
405
431
|
npm run build # Compile TypeScript to dist/
|
|
406
432
|
npm run lint # Check for linting issues
|
|
407
|
-
npm run
|
|
408
|
-
npm run format # Format code with Biome
|
|
433
|
+
npm run format # Format code + auto-fix linting issues with Biome
|
|
409
434
|
```
|
|
410
435
|
|
|
411
436
|
### Running Tests
|
|
@@ -7,6 +7,7 @@ import { getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
|
7
7
|
import { Challenge } from "../../lib/challenge.js";
|
|
8
8
|
import { getConfigFlags } from "../../lib/flags.js";
|
|
9
9
|
import { ChallengeConfigSchema } from "../../lib/schemas.js";
|
|
10
|
+
import { formatError } from "../../lib/zod-errors.js";
|
|
10
11
|
export default class Create extends Command {
|
|
11
12
|
static description = "Create a new challenge locally and in the cloud";
|
|
12
13
|
static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
|
|
@@ -51,7 +52,13 @@ export default class Create extends Command {
|
|
|
51
52
|
task: async (_, task) => {
|
|
52
53
|
const challengeData = await api.getChallenge(args.challengeSlug);
|
|
53
54
|
// Remove fields that shouldn't be in the config file
|
|
54
|
-
|
|
55
|
+
let config;
|
|
56
|
+
try {
|
|
57
|
+
config = ChallengeConfigSchema.parse(challengeData);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
throw new Error(formatError(error, "challenge config"));
|
|
61
|
+
}
|
|
55
62
|
// Remove quotes from keys
|
|
56
63
|
const configStr = JSON.stringify(config, null, 2).replace(/"([a-zA-Z0-9_]+)":/g, "$1:");
|
|
57
64
|
await fs.writeFile(challenge.configPath, `
|
|
@@ -124,6 +124,7 @@ export default class Pull extends Command {
|
|
|
124
124
|
name: cloudWorld.name,
|
|
125
125
|
description: cloudWorld.description ?? "no description",
|
|
126
126
|
domain: "minecraft",
|
|
127
|
+
...(cloudWorld.worldConfig && { worldConfig: cloudWorld.worldConfig }),
|
|
127
128
|
};
|
|
128
129
|
const configContent = `export const config = ${JSON.stringify(config, null, "\t").replace(/"([a-zA-Z_][a-zA-Z0-9_]*)"\s*:/g, "$1:")};
|
|
129
130
|
`;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import * as tar from "tar";
|
|
6
6
|
import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, DashboardUrlResponseSchema, DownloadUrlResponseSchema, HumanSchema, JobResponseSchema, ListRunsResponseSchema, RecordingsListResponseSchema, RunLogsResponseSchema, RunResultResponseSchema, RunStatusSchema, UploadUrlResponseSchema, WorldSchema, WorldsResponseSchema, } from "./schemas.js";
|
|
7
|
+
import { formatError } from "./zod-errors.js";
|
|
7
8
|
const DEFAULT_PAGE_SIZE = 30;
|
|
8
9
|
const DEFAULT_CHALLENGE_SCHEMA = {
|
|
9
10
|
slug: "",
|
|
@@ -13,7 +14,7 @@ const DEFAULT_CHALLENGE_SCHEMA = {
|
|
|
13
14
|
world: "team-kradle:flat-world",
|
|
14
15
|
challengeConfig: { cheat: false, datapack: true, gameMode: "survival" },
|
|
15
16
|
task: ".",
|
|
16
|
-
roles: {
|
|
17
|
+
roles: { defaultrole: { description: "default role", specificTask: "do your best!" } },
|
|
17
18
|
objective: {
|
|
18
19
|
fieldName: "success_rate",
|
|
19
20
|
direction: "maximize",
|
|
@@ -54,7 +55,15 @@ export class ApiClient {
|
|
|
54
55
|
...options,
|
|
55
56
|
});
|
|
56
57
|
const data = await response.json();
|
|
57
|
-
|
|
58
|
+
if (schema) {
|
|
59
|
+
try {
|
|
60
|
+
return schema.parse(data);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(formatError(error, `API response from GET ${url}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return data;
|
|
58
67
|
}
|
|
59
68
|
async post(url, options = {}, schema) {
|
|
60
69
|
const response = await this.request(url, {
|
|
@@ -66,7 +75,15 @@ export class ApiClient {
|
|
|
66
75
|
return undefined;
|
|
67
76
|
}
|
|
68
77
|
const data = JSON.parse(text);
|
|
69
|
-
|
|
78
|
+
if (schema) {
|
|
79
|
+
try {
|
|
80
|
+
return schema.parse(data);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new Error(formatError(error, `API response from POST ${url}`));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return data;
|
|
70
87
|
}
|
|
71
88
|
async put(url, options = {}) {
|
|
72
89
|
return await this.request(url, {
|
package/dist/lib/challenge.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as tar from "tar";
|
|
|
7
7
|
import { extractShortSlug, isNamespacedSlug } from "./arguments.js";
|
|
8
8
|
import { ChallengeConfigSchema } from "./schemas.js";
|
|
9
9
|
import { executeTypescriptFile, findSimilarStrings, getStaticResourcePath, loadTypescriptExport, readDirSorted, } from "./utils.js";
|
|
10
|
+
import { formatError } from "./zod-errors.js";
|
|
10
11
|
export const SOURCE_FOLDER = ".src";
|
|
11
12
|
export class Challenge {
|
|
12
13
|
shortSlug;
|
|
@@ -89,14 +90,17 @@ export class Challenge {
|
|
|
89
90
|
// Load config if not provided (and config file exists)
|
|
90
91
|
let endStates = [];
|
|
91
92
|
let roles = [];
|
|
93
|
+
let locations = [];
|
|
92
94
|
if (config) {
|
|
93
95
|
endStates = config.endStates ? Object.keys(config.endStates) : [];
|
|
94
96
|
roles = config.roles ? Object.keys(config.roles) : [];
|
|
97
|
+
locations = config.challengeConfig?.locations ? Object.keys(config.challengeConfig.locations) : [];
|
|
95
98
|
}
|
|
96
99
|
else if (existsSync(this.configPath)) {
|
|
97
100
|
const loadedConfig = await this.loadConfig();
|
|
98
101
|
endStates = loadedConfig.endStates ? Object.keys(loadedConfig.endStates) : [];
|
|
99
102
|
roles = loadedConfig.roles ? Object.keys(loadedConfig.roles) : [];
|
|
103
|
+
locations = loadedConfig.challengeConfig?.locations ? Object.keys(loadedConfig.challengeConfig.locations) : [];
|
|
100
104
|
}
|
|
101
105
|
try {
|
|
102
106
|
await executeTypescriptFile(this.challengePath, {
|
|
@@ -104,6 +108,7 @@ export class Challenge {
|
|
|
104
108
|
NAMESPACE: "kradle",
|
|
105
109
|
KRADLE_CHALLENGE_END_STATES: JSON.stringify(endStates),
|
|
106
110
|
KRADLE_CHALLENGE_ROLES: JSON.stringify(roles),
|
|
111
|
+
KRADLE_CHALLENGE_LOCATIONS: JSON.stringify(locations),
|
|
107
112
|
}, { silent });
|
|
108
113
|
}
|
|
109
114
|
catch (error) {
|
|
@@ -131,7 +136,12 @@ export class Challenge {
|
|
|
131
136
|
KRADLE_CHALLENGES_PATH: this.kradleChallengesPath,
|
|
132
137
|
NAMESPACE: "kradle",
|
|
133
138
|
});
|
|
134
|
-
|
|
139
|
+
try {
|
|
140
|
+
return ChallengeConfigSchema.parse(config);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new Error(formatError(error, this.configPath));
|
|
144
|
+
}
|
|
135
145
|
}
|
|
136
146
|
/**
|
|
137
147
|
* Build the challenge datapack and upload it to the cloud.
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import pc from "picocolors";
|
|
4
4
|
import { Challenge } from "../challenge.js";
|
|
5
5
|
import { executeNodeCommand, openInBrowser } from "../utils.js";
|
|
6
|
+
import { formatError } from "../zod-errors.js";
|
|
6
7
|
import { Runner } from "./runner.js";
|
|
7
8
|
import { TUI } from "./tui.js";
|
|
8
9
|
import { ExperimentMetadataSchema, ManifestSchema, ProgressSchema } from "./types.js";
|
|
@@ -102,7 +103,12 @@ export class Experimenter {
|
|
|
102
103
|
try {
|
|
103
104
|
const content = await fs.readFile(this.metadataPath, "utf-8");
|
|
104
105
|
const data = JSON.parse(content);
|
|
105
|
-
|
|
106
|
+
try {
|
|
107
|
+
return ExperimentMetadataSchema.parse(data);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new Error(formatError(error, this.metadataPath));
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
catch {
|
|
108
114
|
return null;
|
|
@@ -164,13 +170,12 @@ export class Experimenter {
|
|
|
164
170
|
async loadManifest(version) {
|
|
165
171
|
const paths = this.getVersionPaths(version);
|
|
166
172
|
const content = await fs.readFile(paths.manifestPath, "utf-8");
|
|
173
|
+
const data = JSON.parse(content);
|
|
167
174
|
try {
|
|
168
|
-
const data = JSON.parse(content);
|
|
169
175
|
return ManifestSchema.parse(data);
|
|
170
176
|
}
|
|
171
177
|
catch (error) {
|
|
172
|
-
|
|
173
|
-
throw error;
|
|
178
|
+
throw new Error(formatError(error, paths.manifestPath));
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
/**
|
|
@@ -181,7 +186,12 @@ export class Experimenter {
|
|
|
181
186
|
const paths = this.getVersionPaths(version);
|
|
182
187
|
const content = await fs.readFile(paths.progressPath, "utf-8");
|
|
183
188
|
const data = JSON.parse(content);
|
|
184
|
-
|
|
189
|
+
try {
|
|
190
|
+
return ProgressSchema.parse(data);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw new Error(formatError(error, paths.progressPath));
|
|
194
|
+
}
|
|
185
195
|
}
|
|
186
196
|
catch {
|
|
187
197
|
return null;
|
|
@@ -205,7 +215,12 @@ export class Experimenter {
|
|
|
205
215
|
*/
|
|
206
216
|
async generateManifest(configPath) {
|
|
207
217
|
const manifest = await this.executeConfigFile(configPath);
|
|
208
|
-
|
|
218
|
+
try {
|
|
219
|
+
return ManifestSchema.parse(manifest);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw new Error(formatError(error, configPath));
|
|
223
|
+
}
|
|
209
224
|
}
|
|
210
225
|
/**
|
|
211
226
|
* Execute config.ts file and return the manifest
|
package/dist/lib/schemas.d.ts
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
export declare const CoordinatesSchema: z.ZodObject<{
|
|
3
|
+
x: z.ZodNumber;
|
|
4
|
+
y: z.ZodNumber;
|
|
5
|
+
z: z.ZodNumber;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare const ChallengeLocationValueSchema: z.ZodObject<{
|
|
8
|
+
world_location: z.ZodOptional<z.ZodString>;
|
|
9
|
+
coordinates: z.ZodOptional<z.ZodObject<{
|
|
10
|
+
x: z.ZodNumber;
|
|
11
|
+
y: z.ZodNumber;
|
|
12
|
+
z: z.ZodNumber;
|
|
13
|
+
}, z.core.$strip>>;
|
|
14
|
+
}, z.core.$strip>;
|
|
15
|
+
export declare const ChallengeLocationsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
16
|
+
world_location: z.ZodOptional<z.ZodString>;
|
|
17
|
+
coordinates: z.ZodOptional<z.ZodObject<{
|
|
18
|
+
x: z.ZodNumber;
|
|
19
|
+
y: z.ZodNumber;
|
|
20
|
+
z: z.ZodNumber;
|
|
21
|
+
}, z.core.$strip>>;
|
|
22
|
+
}, z.core.$strip>>;
|
|
23
|
+
export declare const WorldLocationValueSchema: z.ZodObject<{
|
|
24
|
+
name: z.ZodString;
|
|
25
|
+
coordinates: z.ZodObject<{
|
|
26
|
+
x: z.ZodNumber;
|
|
27
|
+
y: z.ZodNumber;
|
|
28
|
+
z: z.ZodNumber;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
export declare const WorldLocationsSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
32
|
+
name: z.ZodString;
|
|
33
|
+
coordinates: z.ZodObject<{
|
|
34
|
+
x: z.ZodNumber;
|
|
35
|
+
y: z.ZodNumber;
|
|
36
|
+
z: z.ZodNumber;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
}, z.core.$strip>>;
|
|
2
39
|
export declare const ChallengeSchema: z.ZodObject<{
|
|
3
40
|
id: z.ZodOptional<z.ZodString>;
|
|
4
41
|
slug: z.ZodString;
|
|
@@ -19,6 +56,14 @@ export declare const ChallengeSchema: z.ZodObject<{
|
|
|
19
56
|
adventure: "adventure";
|
|
20
57
|
spectator: "spectator";
|
|
21
58
|
}>;
|
|
59
|
+
locations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
60
|
+
world_location: z.ZodOptional<z.ZodString>;
|
|
61
|
+
coordinates: z.ZodOptional<z.ZodObject<{
|
|
62
|
+
x: z.ZodNumber;
|
|
63
|
+
y: z.ZodNumber;
|
|
64
|
+
z: z.ZodNumber;
|
|
65
|
+
}, z.core.$strip>>;
|
|
66
|
+
}, z.core.$strip>>>;
|
|
22
67
|
}, z.core.$strip>;
|
|
23
68
|
description: z.ZodOptional<z.ZodString>;
|
|
24
69
|
task: z.ZodOptional<z.ZodString>;
|
|
@@ -54,6 +99,14 @@ export declare const ChallengeConfigSchema: z.ZodObject<{
|
|
|
54
99
|
adventure: "adventure";
|
|
55
100
|
spectator: "spectator";
|
|
56
101
|
}>;
|
|
102
|
+
locations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
103
|
+
world_location: z.ZodOptional<z.ZodString>;
|
|
104
|
+
coordinates: z.ZodOptional<z.ZodObject<{
|
|
105
|
+
x: z.ZodNumber;
|
|
106
|
+
y: z.ZodNumber;
|
|
107
|
+
z: z.ZodNumber;
|
|
108
|
+
}, z.core.$strip>>;
|
|
109
|
+
}, z.core.$strip>>>;
|
|
57
110
|
}, z.core.$strip>;
|
|
58
111
|
task: z.ZodOptional<z.ZodString>;
|
|
59
112
|
roles: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
@@ -92,6 +145,14 @@ export declare const ChallengesResponseSchema: z.ZodObject<{
|
|
|
92
145
|
adventure: "adventure";
|
|
93
146
|
spectator: "spectator";
|
|
94
147
|
}>;
|
|
148
|
+
locations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
149
|
+
world_location: z.ZodOptional<z.ZodString>;
|
|
150
|
+
coordinates: z.ZodOptional<z.ZodObject<{
|
|
151
|
+
x: z.ZodNumber;
|
|
152
|
+
y: z.ZodNumber;
|
|
153
|
+
z: z.ZodNumber;
|
|
154
|
+
}, z.core.$strip>>;
|
|
155
|
+
}, z.core.$strip>>>;
|
|
95
156
|
}, z.core.$strip>;
|
|
96
157
|
description: z.ZodOptional<z.ZodString>;
|
|
97
158
|
task: z.ZodOptional<z.ZodString>;
|
|
@@ -254,12 +315,32 @@ export declare const WorldSchema: z.ZodObject<{
|
|
|
254
315
|
creationTime: z.ZodString;
|
|
255
316
|
updateTime: z.ZodString;
|
|
256
317
|
creator: z.ZodString;
|
|
318
|
+
worldConfig: z.ZodOptional<z.ZodObject<{
|
|
319
|
+
locations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
320
|
+
name: z.ZodString;
|
|
321
|
+
coordinates: z.ZodObject<{
|
|
322
|
+
x: z.ZodNumber;
|
|
323
|
+
y: z.ZodNumber;
|
|
324
|
+
z: z.ZodNumber;
|
|
325
|
+
}, z.core.$strip>;
|
|
326
|
+
}, z.core.$strip>>>;
|
|
327
|
+
}, z.core.$strip>>;
|
|
257
328
|
}, z.core.$strip>;
|
|
258
329
|
export declare const WorldConfigSchema: z.ZodObject<{
|
|
259
330
|
name: z.ZodString;
|
|
260
331
|
description: z.ZodOptional<z.ZodString>;
|
|
261
332
|
domain: z.ZodDefault<z.ZodLiteral<"minecraft">>;
|
|
262
333
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
334
|
+
worldConfig: z.ZodOptional<z.ZodObject<{
|
|
335
|
+
locations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
336
|
+
name: z.ZodString;
|
|
337
|
+
coordinates: z.ZodObject<{
|
|
338
|
+
x: z.ZodNumber;
|
|
339
|
+
y: z.ZodNumber;
|
|
340
|
+
z: z.ZodNumber;
|
|
341
|
+
}, z.core.$strip>;
|
|
342
|
+
}, z.core.$strip>>>;
|
|
343
|
+
}, z.core.$strip>>;
|
|
263
344
|
}, z.core.$strip>;
|
|
264
345
|
export declare const WorldsResponseSchema: z.ZodObject<{
|
|
265
346
|
worlds: z.ZodArray<z.ZodObject<{
|
|
@@ -274,6 +355,16 @@ export declare const WorldsResponseSchema: z.ZodObject<{
|
|
|
274
355
|
creationTime: z.ZodString;
|
|
275
356
|
updateTime: z.ZodString;
|
|
276
357
|
creator: z.ZodString;
|
|
358
|
+
worldConfig: z.ZodOptional<z.ZodObject<{
|
|
359
|
+
locations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
360
|
+
name: z.ZodString;
|
|
361
|
+
coordinates: z.ZodObject<{
|
|
362
|
+
x: z.ZodNumber;
|
|
363
|
+
y: z.ZodNumber;
|
|
364
|
+
z: z.ZodNumber;
|
|
365
|
+
}, z.core.$strip>;
|
|
366
|
+
}, z.core.$strip>>>;
|
|
367
|
+
}, z.core.$strip>>;
|
|
277
368
|
}, z.core.$strip>>;
|
|
278
369
|
nextPageToken: z.ZodOptional<z.ZodString>;
|
|
279
370
|
}, z.core.$strip>;
|
|
@@ -369,6 +460,8 @@ export type RunResultResponse = z.infer<typeof RunResultResponseSchema>;
|
|
|
369
460
|
export type DownloadUrlResponse = z.infer<typeof DownloadUrlResponseSchema>;
|
|
370
461
|
export type WorldSchemaType = z.infer<typeof WorldSchema>;
|
|
371
462
|
export type WorldConfigSchemaType = z.infer<typeof WorldConfigSchema>;
|
|
463
|
+
export type WorldLocationValue = z.infer<typeof WorldLocationValueSchema>;
|
|
464
|
+
export type WorldLocations = z.infer<typeof WorldLocationsSchema>;
|
|
372
465
|
export type WorldsResponseType = z.infer<typeof WorldsResponseSchema>;
|
|
373
466
|
export type DashboardUrlResponse = z.infer<typeof DashboardUrlResponseSchema>;
|
|
374
467
|
export type Run = z.infer<typeof RunSchema>;
|
package/dist/lib/schemas.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
// Coordinates object {x, y, z}
|
|
3
|
+
export const CoordinatesSchema = z.object({
|
|
4
|
+
x: z.number(),
|
|
5
|
+
y: z.number(),
|
|
6
|
+
z: z.number(),
|
|
7
|
+
});
|
|
8
|
+
// Temp - this will change when we have a union type
|
|
9
|
+
export const ChallengeLocationValueSchema = z.object({
|
|
10
|
+
world_location: z.string().optional(),
|
|
11
|
+
coordinates: CoordinatesSchema.optional(),
|
|
12
|
+
});
|
|
13
|
+
// Challenge locations: mapping of location key to value
|
|
14
|
+
export const ChallengeLocationsSchema = z.record(z.string(), ChallengeLocationValueSchema);
|
|
15
|
+
// World location value: name (pretty version) and coordinates
|
|
16
|
+
export const WorldLocationValueSchema = z.object({
|
|
17
|
+
name: z.string(),
|
|
18
|
+
coordinates: CoordinatesSchema,
|
|
19
|
+
});
|
|
20
|
+
// World locations: mapping of location key to value
|
|
21
|
+
export const WorldLocationsSchema = z.record(z.string(), WorldLocationValueSchema);
|
|
2
22
|
export const ChallengeSchema = z.object({
|
|
3
23
|
id: z.string().optional(),
|
|
4
24
|
slug: z.string(),
|
|
@@ -10,6 +30,7 @@ export const ChallengeSchema = z.object({
|
|
|
10
30
|
cheat: z.boolean(),
|
|
11
31
|
datapack: z.boolean(),
|
|
12
32
|
gameMode: z.enum(["survival", "creative", "adventure", "spectator"]),
|
|
33
|
+
locations: ChallengeLocationsSchema.optional(),
|
|
13
34
|
}),
|
|
14
35
|
description: z.string().optional(),
|
|
15
36
|
task: z.string().optional(),
|
|
@@ -134,12 +155,22 @@ export const WorldSchema = z.object({
|
|
|
134
155
|
creationTime: z.string(),
|
|
135
156
|
updateTime: z.string(),
|
|
136
157
|
creator: z.string(),
|
|
158
|
+
worldConfig: z
|
|
159
|
+
.object({
|
|
160
|
+
locations: WorldLocationsSchema.optional(),
|
|
161
|
+
})
|
|
162
|
+
.optional(),
|
|
137
163
|
});
|
|
138
164
|
export const WorldConfigSchema = z.object({
|
|
139
165
|
name: z.string(),
|
|
140
166
|
description: z.string().optional(),
|
|
141
167
|
domain: z.literal("minecraft").default("minecraft"),
|
|
142
168
|
metadata: z.record(z.string(), z.string()).optional(),
|
|
169
|
+
worldConfig: z
|
|
170
|
+
.object({
|
|
171
|
+
locations: WorldLocationsSchema.optional(),
|
|
172
|
+
})
|
|
173
|
+
.optional(),
|
|
143
174
|
});
|
|
144
175
|
export const WorldsResponseSchema = z.object({
|
|
145
176
|
worlds: z.array(WorldSchema),
|
package/dist/lib/world.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import * as tar from "tar";
|
|
6
6
|
import { WorldConfigSchema } from "./schemas.js";
|
|
7
7
|
import { loadTypescriptExport, readDirSorted } from "./utils.js";
|
|
8
|
+
import { formatError } from "./zod-errors.js";
|
|
8
9
|
/**
|
|
9
10
|
* Represents a Minecraft world stored locally as:
|
|
10
11
|
* worlds/<slug>/config.ts - metadata (name, visibility, etc.)
|
|
@@ -120,7 +121,12 @@ export class World {
|
|
|
120
121
|
throw new Error(`Config file not found at ${this.configPath}`);
|
|
121
122
|
}
|
|
122
123
|
const config = await loadTypescriptExport(this.configPath, "config");
|
|
123
|
-
|
|
124
|
+
try {
|
|
125
|
+
return WorldConfigSchema.parse(config);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new Error(formatError(error, this.configPath));
|
|
129
|
+
}
|
|
124
130
|
}
|
|
125
131
|
static async getLocalWorlds() {
|
|
126
132
|
const worldsDir = path.resolve(process.cwd(), "worlds");
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Format a ZodError into a human-readable string for CLI output.
|
|
4
|
+
*
|
|
5
|
+
* @param error - The ZodError to format
|
|
6
|
+
* @param context - Optional context string (e.g., "config.ts", "API response"). If an absolute path, it will be converted to a relative path from cwd.
|
|
7
|
+
* @returns A formatted error string
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // Output:
|
|
11
|
+
* // Validation failed for config.ts:
|
|
12
|
+
* // ✖ Invalid input at "objective.direction": expected "maximize" | "minimize"
|
|
13
|
+
*/
|
|
14
|
+
export declare function formatZodError(error: ZodError, context?: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Check if an error is a ZodError.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isZodError(error: unknown): error is ZodError;
|
|
19
|
+
/**
|
|
20
|
+
* Wrap an error message, formatting ZodErrors nicely.
|
|
21
|
+
* For non-Zod errors, returns the original message.
|
|
22
|
+
*
|
|
23
|
+
* @param error - The error to format
|
|
24
|
+
* @param context - Optional context string for Zod errors
|
|
25
|
+
* @returns A formatted error string
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatError(error: unknown, context?: string): string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { prettifyError, ZodError } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* Format a ZodError into a human-readable string for CLI output.
|
|
6
|
+
*
|
|
7
|
+
* @param error - The ZodError to format
|
|
8
|
+
* @param context - Optional context string (e.g., "config.ts", "API response"). If an absolute path, it will be converted to a relative path from cwd.
|
|
9
|
+
* @returns A formatted error string
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Output:
|
|
13
|
+
* // Validation failed for config.ts:
|
|
14
|
+
* // ✖ Invalid input at "objective.direction": expected "maximize" | "minimize"
|
|
15
|
+
*/
|
|
16
|
+
export function formatZodError(error, context) {
|
|
17
|
+
// Convert absolute paths to relative paths from cwd
|
|
18
|
+
let displayContext = context;
|
|
19
|
+
if (context && path.isAbsolute(context)) {
|
|
20
|
+
displayContext = path.relative(process.cwd(), context);
|
|
21
|
+
}
|
|
22
|
+
const header = displayContext ? `Validation failed for ${pc.cyan(displayContext)}:` : "Validation failed:";
|
|
23
|
+
// Use Zod 4's built-in prettifyError and add color/indentation
|
|
24
|
+
const prettyError = prettifyError(error);
|
|
25
|
+
// Indent each line and add bullet points for better CLI readability
|
|
26
|
+
const formattedLines = prettyError
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => ` ${pc.red(line)}`)
|
|
29
|
+
.join("\n");
|
|
30
|
+
return `${header}\n${formattedLines}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if an error is a ZodError.
|
|
34
|
+
*/
|
|
35
|
+
export function isZodError(error) {
|
|
36
|
+
return error instanceof ZodError;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Wrap an error message, formatting ZodErrors nicely.
|
|
40
|
+
* For non-Zod errors, returns the original message.
|
|
41
|
+
*
|
|
42
|
+
* @param error - The error to format
|
|
43
|
+
* @param context - Optional context string for Zod errors
|
|
44
|
+
* @returns A formatted error string
|
|
45
|
+
*/
|
|
46
|
+
export function formatError(error, context) {
|
|
47
|
+
if (isZodError(error)) {
|
|
48
|
+
return formatZodError(error, context);
|
|
49
|
+
}
|
|
50
|
+
if (error instanceof Error) {
|
|
51
|
+
return error.message;
|
|
52
|
+
}
|
|
53
|
+
return String(error);
|
|
54
|
+
}
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -146,7 +146,8 @@ kradle challenge build --all --visibility public
|
|
|
146
146
|
3. Executes `challenge.ts` to generate the datapack
|
|
147
147
|
- Passes `KRADLE_CHALLENGE_END_STATES` env var containing a JSON array of end state keys from `config.endStates` (e.g., `["victory", "defeat"]`)
|
|
148
148
|
- Passes `KRADLE_CHALLENGE_ROLES` env var containing a JSON array of role names from `config.roles` (e.g., `["attacker", "defender"]`)
|
|
149
|
-
-
|
|
149
|
+
- Passes `KRADLE_CHALLENGE_LOCATIONS` env var containing a JSON array of location keys from `config.challengeConfig.locations` (e.g., `["spawn", "goal"]`)
|
|
150
|
+
- The `@kradle/challenges-sdk` uses these to register valid end states, roles, and locations at build time
|
|
150
151
|
4. Compresses and uploads datapack to cloud storage
|
|
151
152
|
|
|
152
153
|
**Examples:**
|
|
@@ -1003,6 +1004,68 @@ kradle world delete my-world --yes
|
|
|
1003
1004
|
|
|
1004
1005
|
---
|
|
1005
1006
|
|
|
1007
|
+
### World Configuration
|
|
1008
|
+
|
|
1009
|
+
Each world has a `config.ts` file located at `worlds/<slug>/config.ts`.
|
|
1010
|
+
|
|
1011
|
+
**Config Structure:**
|
|
1012
|
+
```typescript
|
|
1013
|
+
export const config = {
|
|
1014
|
+
name: string; // Display name of the world
|
|
1015
|
+
description?: string; // Optional description
|
|
1016
|
+
domain: "minecraft"; // Always "minecraft"
|
|
1017
|
+
metadata?: Record<string, string>; // Optional key-value metadata
|
|
1018
|
+
worldConfig?: {
|
|
1019
|
+
locations?: {
|
|
1020
|
+
[key: string]: {
|
|
1021
|
+
name: string; // Pretty display name for the location
|
|
1022
|
+
coordinates: {
|
|
1023
|
+
x: number; // X coordinate (integer)
|
|
1024
|
+
y: number; // Y coordinate (integer)
|
|
1025
|
+
z: number; // Z coordinate (integer)
|
|
1026
|
+
};
|
|
1027
|
+
};
|
|
1028
|
+
};
|
|
1029
|
+
};
|
|
1030
|
+
};
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
**Example with Locations:**
|
|
1034
|
+
```typescript
|
|
1035
|
+
export const config = {
|
|
1036
|
+
name: "CTF Arena",
|
|
1037
|
+
description: "A capture the flag arena world",
|
|
1038
|
+
domain: "minecraft",
|
|
1039
|
+
worldConfig: {
|
|
1040
|
+
locations: {
|
|
1041
|
+
red_spawn: {
|
|
1042
|
+
name: "Red Team Spawn",
|
|
1043
|
+
coordinates: { x: 100, y: 64, z: 0 }
|
|
1044
|
+
},
|
|
1045
|
+
blue_spawn: {
|
|
1046
|
+
name: "Blue Team Spawn",
|
|
1047
|
+
coordinates: { x: -100, y: 64, z: 0 }
|
|
1048
|
+
},
|
|
1049
|
+
red_flag: {
|
|
1050
|
+
name: "Red Flag",
|
|
1051
|
+
coordinates: { x: 150, y: 64, z: 0 }
|
|
1052
|
+
},
|
|
1053
|
+
blue_flag: {
|
|
1054
|
+
name: "Blue Flag",
|
|
1055
|
+
coordinates: { x: -150, y: 64, z: 0 }
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
**Notes:**
|
|
1063
|
+
- The config is uploaded when running `kradle world push`
|
|
1064
|
+
- Locations are synced to the cloud and can be retrieved via the API
|
|
1065
|
+
- When pulling a world, the config is recreated from cloud metadata including locations
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1006
1069
|
## Agent Commands
|
|
1007
1070
|
|
|
1008
1071
|
### `kradle agent list`
|
package/static/challenge.ts
CHANGED
|
@@ -9,7 +9,7 @@ createChallenge({
|
|
|
9
9
|
name: challenge_name,
|
|
10
10
|
kradle_challenge_path: process.env.KRADLE_CHALLENGES_PATH || DEFAULT_CHALLENGE_PATH,
|
|
11
11
|
GAME_DURATION: GAME_DURATION,
|
|
12
|
-
roles: ["
|
|
12
|
+
roles: ["defaultrole"],
|
|
13
13
|
custom_variables: {},
|
|
14
14
|
})
|
|
15
15
|
.events(() => ({
|
|
@@ -26,6 +26,6 @@ createChallenge({
|
|
|
26
26
|
},
|
|
27
27
|
}))
|
|
28
28
|
.end_condition(({ alive_players }) => alive_players.equalTo(0))
|
|
29
|
-
.win_conditions(({ has_never_died }, {
|
|
30
|
-
[
|
|
29
|
+
.win_conditions(({ has_never_died }, { defaultrole }) => ({
|
|
30
|
+
[defaultrole]: has_never_died.equalTo(1),
|
|
31
31
|
}));
|