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 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 lint:fix # Auto-fix linting issues
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
- const config = ChallengeConfigSchema.parse(challengeData);
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
  `;
@@ -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: { "default-role": { description: "default-role", specificTask: "do your best!" } },
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
- return schema ? schema.parse(data) : data;
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
- return schema ? schema.parse(data) : data;
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, {
@@ -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
- return ChallengeConfigSchema.parse(config);
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
- return ExperimentMetadataSchema.parse(data);
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
- console.error(pc.red(`Error loading manifest for version ${version}: ${error instanceof Error ? error.message : String(error)}`));
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
- return ProgressSchema.parse(data);
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
- return ManifestSchema.parse(manifest);
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
@@ -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>;
@@ -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
- return WorldConfigSchema.parse(config);
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
+ }
@@ -1378,5 +1378,5 @@
1378
1378
  ]
1379
1379
  }
1380
1380
  },
1381
- "version": "0.6.0"
1381
+ "version": "0.6.1"
1382
1382
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kradle",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Kradle's CLI. Manage challenges, experiments, agents and more!",
5
5
  "keywords": [
6
6
  "cli"
@@ -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
- - The `@kradle/challenges-sdk` uses these to register valid end states and roles at build time
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`
@@ -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: ["all"],
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 }, { all }) => ({
30
- [all]: has_never_died.equalTo(1),
29
+ .win_conditions(({ has_never_died }, { defaultrole }) => ({
30
+ [defaultrole]: has_never_died.equalTo(1),
31
31
  }));