kradle 0.4.3 → 0.6.0
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 +23 -3
- package/dist/commands/challenge/build.js +7 -2
- package/dist/commands/challenge/create.js +1 -1
- package/dist/commands/challenge/delete.js +10 -9
- package/dist/commands/challenge/pull.js +0 -1
- package/dist/commands/challenge/run.d.ts +14 -0
- package/dist/commands/challenge/run.js +177 -10
- package/dist/commands/challenge/runs/get.d.ts +1 -0
- package/dist/commands/challenge/runs/get.js +11 -0
- package/dist/commands/challenge/watch.js +8 -3
- package/dist/commands/world/import.js +20 -12
- package/dist/commands/world/pull.js +7 -1
- package/dist/commands/world/push.js +24 -2
- package/dist/lib/api-client.d.ts +9 -1
- package/dist/lib/api-client.js +46 -16
- package/dist/lib/arguments.d.ts +7 -4
- package/dist/lib/arguments.js +9 -10
- package/dist/lib/challenge.d.ts +13 -1
- package/dist/lib/challenge.js +61 -3
- package/dist/lib/utils.d.ts +22 -1
- package/dist/lib/utils.js +70 -5
- package/dist/lib/world.d.ts +18 -0
- package/dist/lib/world.js +72 -0
- package/oclif.manifest.json +13 -4
- package/package.json +5 -4
- package/static/ai_docs/LLM_CLI_REFERENCE.md +78 -40
- package/static/project_template/template-run.json +0 -10
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Make sure you have [NodeJS 22.18 or higher](https://nodejs.org/en/download/curre
|
|
|
22
22
|
|
|
23
23
|
1. Install Kradle's CLI globally
|
|
24
24
|
```
|
|
25
|
-
npm i -g kradle
|
|
25
|
+
npm i -g kradle@latest
|
|
26
26
|
```
|
|
27
27
|
2. Initialize a new directory to store challenges, and other Kradle resources:
|
|
28
28
|
```
|
|
@@ -138,16 +138,34 @@ Uses file watching with debouncing (300ms) and hash comparison to minimize unnec
|
|
|
138
138
|
|
|
139
139
|
### Run Challenge
|
|
140
140
|
|
|
141
|
-
Run a challenge
|
|
141
|
+
Run a challenge with agents specified inline or via interactive selection:
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
|
+
# Interactive mode - prompts for agent selection
|
|
144
145
|
kradle challenge run <challenge-name>
|
|
146
|
+
|
|
147
|
+
# Inline agent specification
|
|
148
|
+
kradle challenge run <challenge-name> team-kradle:gemini-3-flash,team-kradle:grok-4-1-fast
|
|
149
|
+
|
|
150
|
+
# Team-based challenge with roles
|
|
151
|
+
kradle challenge run capture-the-flag \
|
|
152
|
+
red=team-kradle:gemini-3-flash \
|
|
153
|
+
blue=team-kradle:claude-sonnet-4
|
|
154
|
+
|
|
155
|
+
# Other options
|
|
145
156
|
kradle challenge run <challenge-name> --studio # Run in local studio environment
|
|
146
157
|
kradle challenge run <team-name>:<challenge-name> # Run a public challenge from another team
|
|
147
158
|
kradle challenge run <challenge-name> --no-open # Don't open browser
|
|
148
159
|
kradle challenge run <challenge-name> --no-wait # Fire and forget (don't wait for completion)
|
|
149
160
|
```
|
|
150
161
|
|
|
162
|
+
**Inline Agent Syntax:**
|
|
163
|
+
- `agent1,agent2` - Assign agents to the "default" role
|
|
164
|
+
- `role=agent1,agent2` - Assign agents to a specific role
|
|
165
|
+
- Same role can be specified multiple times and agents are merged
|
|
166
|
+
|
|
167
|
+
When no agents are specified, the command enters interactive mode, fetching the challenge configuration and prompting for agent selection for each role.
|
|
168
|
+
|
|
151
169
|
By default, the command opens the run URL in your browser and polls until the run completes, then displays the outcome.
|
|
152
170
|
|
|
153
171
|
### List Runs
|
|
@@ -165,12 +183,14 @@ Get details and logs for a specific run:
|
|
|
165
183
|
|
|
166
184
|
```bash
|
|
167
185
|
kradle challenge runs get <run-id>
|
|
168
|
-
kradle challenge runs get <run-id> --no-logs
|
|
186
|
+
kradle challenge runs get <run-id> --no-logs # Skip fetching logs
|
|
187
|
+
kradle challenge runs get <run-id> --no-summary # Skip AI summary
|
|
169
188
|
```
|
|
170
189
|
|
|
171
190
|
This displays:
|
|
172
191
|
- Run metadata (status, duration, end state)
|
|
173
192
|
- Participant results (agent, winner status, score)
|
|
193
|
+
- AI-generated summary (unless `--no-summary` is used)
|
|
174
194
|
- Log entries with timestamps and levels (unless `--no-logs` is used)
|
|
175
195
|
|
|
176
196
|
## Experiment Commands
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command, Flags, loadHelpClass } from "@oclif/core";
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import { ApiClient } from "../../lib/api-client.js";
|
|
4
|
-
import { getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
4
|
+
import { extractShortSlug, getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
5
5
|
import { Challenge } from "../../lib/challenge.js";
|
|
6
6
|
import { getConfigFlags } from "../../lib/flags.js";
|
|
7
7
|
export default class Build extends Command {
|
|
@@ -39,7 +39,12 @@ export default class Build extends Command {
|
|
|
39
39
|
const challengeSlugs = flags.all ? await Challenge.getLocalChallenges() : argv;
|
|
40
40
|
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
41
41
|
for (const challengeSlug of challengeSlugs) {
|
|
42
|
-
|
|
42
|
+
// Validate that the challenge exists locally
|
|
43
|
+
const validation = await Challenge.validateForLocalOperation(challengeSlug, flags["challenges-path"]);
|
|
44
|
+
if (!validation.isValid && validation.error) {
|
|
45
|
+
this.error(pc.red(validation.error));
|
|
46
|
+
}
|
|
47
|
+
const challenge = new Challenge(extractShortSlug(challengeSlug), flags["challenges-path"]);
|
|
43
48
|
this.log(pc.blue(`==== Building challenge: ${challenge.shortSlug} ====`));
|
|
44
49
|
try {
|
|
45
50
|
await challenge.buildAndUpload(api, flags.public);
|
|
@@ -11,7 +11,7 @@ export default class Create extends Command {
|
|
|
11
11
|
static description = "Create a new challenge locally and in the cloud";
|
|
12
12
|
static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
|
|
13
13
|
static args = {
|
|
14
|
-
challengeSlug: getChallengeSlugArgument({ description: "Challenge slug to create" }),
|
|
14
|
+
challengeSlug: getChallengeSlugArgument({ description: "Challenge slug to create", strictLocalSlug: true }),
|
|
15
15
|
};
|
|
16
16
|
static flags = {
|
|
17
17
|
verbose: Flags.boolean({ char: "v", description: "Verbose output", default: false }),
|
|
@@ -3,7 +3,7 @@ import { Command, Flags } from "@oclif/core";
|
|
|
3
3
|
import enquirer from "enquirer";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import { ApiClient } from "../../lib/api-client.js";
|
|
6
|
-
import { getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
6
|
+
import { extractShortSlug, getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
7
7
|
import { Challenge } from "../../lib/challenge.js";
|
|
8
8
|
import { getConfigFlags } from "../../lib/flags.js";
|
|
9
9
|
export default class Delete extends Command {
|
|
@@ -22,17 +22,18 @@ export default class Delete extends Command {
|
|
|
22
22
|
async run() {
|
|
23
23
|
const { args, flags } = await this.parse(Delete);
|
|
24
24
|
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
25
|
-
const
|
|
26
|
-
|
|
25
|
+
const shortSlug = extractShortSlug(args.challengeSlug);
|
|
26
|
+
const challenge = new Challenge(shortSlug, flags["challenges-path"]);
|
|
27
|
+
// Check if challenge exists locally (always uses short slug)
|
|
27
28
|
const existsLocally = challenge.exists();
|
|
28
|
-
// Check if challenge exists in cloud
|
|
29
|
-
const existsInCloud = await api.challengeExists(
|
|
29
|
+
// Check if challenge exists in cloud (use full slug to support namespaced challenges)
|
|
30
|
+
const existsInCloud = await api.challengeExists(args.challengeSlug);
|
|
30
31
|
// If challenge doesn't exist anywhere, inform user and exit
|
|
31
32
|
if (!existsLocally && !existsInCloud) {
|
|
32
|
-
this.error(pc.red(`Challenge "${
|
|
33
|
+
this.error(pc.red(`Challenge "${args.challengeSlug}" does not exist locally or in the cloud.`));
|
|
33
34
|
}
|
|
34
35
|
// Show what will be deleted
|
|
35
|
-
this.log(pc.bold(`\nChallenge: ${pc.cyan(
|
|
36
|
+
this.log(pc.bold(`\nChallenge: ${pc.cyan(args.challengeSlug)}`));
|
|
36
37
|
this.log(` 💻 Local: ${existsLocally ? pc.green("✓ exists") : pc.dim("✗ not found")}`);
|
|
37
38
|
this.log(` ☁️ Cloud: ${existsInCloud ? pc.green("✓ exists") : pc.dim("✗ not found")}`);
|
|
38
39
|
this.log("");
|
|
@@ -58,7 +59,7 @@ export default class Delete extends Command {
|
|
|
58
59
|
}
|
|
59
60
|
try {
|
|
60
61
|
this.log(pc.blue(">> Deleting from cloud..."));
|
|
61
|
-
await api.deleteChallenge(
|
|
62
|
+
await api.deleteChallenge(args.challengeSlug);
|
|
62
63
|
this.log(pc.green("✓ Deleted from cloud"));
|
|
63
64
|
}
|
|
64
65
|
catch (error) {
|
|
@@ -94,6 +95,6 @@ export default class Delete extends Command {
|
|
|
94
95
|
this.error(pc.red(`Failed to delete local folder: ${error instanceof Error ? error.message : String(error)}`));
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
this.log(pc.green(`\n✓ Challenge "${
|
|
98
|
+
this.log(pc.green(`\n✓ Challenge "${args.challengeSlug}" deleted successfully!`));
|
|
98
99
|
}
|
|
99
100
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
2
|
export default class Run extends Command {
|
|
3
3
|
static description: string;
|
|
4
|
+
static strict: boolean;
|
|
4
5
|
static examples: string[];
|
|
5
6
|
static args: {
|
|
6
7
|
challengeSlug: import("@oclif/core/interfaces").Arg<string>;
|
|
@@ -18,5 +19,18 @@ export default class Run extends Command {
|
|
|
18
19
|
};
|
|
19
20
|
private pollForCompletion;
|
|
20
21
|
private displayRunResult;
|
|
22
|
+
/**
|
|
23
|
+
* Parse inline agent arguments into participants.
|
|
24
|
+
* Supports formats:
|
|
25
|
+
* - team=agent1,agent2 -> assigns agents to a role/team
|
|
26
|
+
* - agent1,agent2 -> assigns agents to "default" role
|
|
27
|
+
* Same team can be specified multiple times and agents are merged.
|
|
28
|
+
*/
|
|
29
|
+
private parseInlineAgents;
|
|
30
|
+
/**
|
|
31
|
+
* Interactive agent selection when no agents are specified inline.
|
|
32
|
+
* Fetches challenge config from cloud and prompts user to select agents for each role.
|
|
33
|
+
*/
|
|
34
|
+
private selectAgentsInteractively;
|
|
21
35
|
run(): Promise<void>;
|
|
22
36
|
}
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import enquirer from "enquirer";
|
|
2
3
|
import pc from "picocolors";
|
|
3
4
|
import { ApiClient } from "../../lib/api-client.js";
|
|
4
5
|
import { getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
5
6
|
import { getConfigFlags } from "../../lib/flags.js";
|
|
6
|
-
import { formatDuration,
|
|
7
|
+
import { clearScreen, formatDuration, fuzzyHighlight, getRunStatusDisplay, openInBrowser } from "../../lib/utils.js";
|
|
7
8
|
const POLL_INTERVAL_MS = 2000;
|
|
8
9
|
const MAX_POLL_TIME_MS = 30 * 60 * 1000; // 30 minutes
|
|
9
10
|
const TERMINAL_STATUSES = ["finished", "game_over", "error", "completed", "cancelled", "timeout", "failed"];
|
|
10
11
|
export default class Run extends Command {
|
|
11
12
|
static description = "Run a challenge";
|
|
13
|
+
// Allow variadic arguments for inline agent specification
|
|
14
|
+
static strict = false;
|
|
12
15
|
static examples = [
|
|
13
16
|
"<%= config.bin %> <%= command.id %> my-challenge",
|
|
14
17
|
"<%= config.bin %> <%= command.id %> my-challenge --studio",
|
|
15
18
|
"<%= config.bin %> <%= command.id %> team-name:my-challenge",
|
|
16
19
|
"<%= config.bin %> <%= command.id %> my-challenge --no-open",
|
|
17
20
|
"<%= config.bin %> <%= command.id %> my-challenge --no-wait",
|
|
21
|
+
"<%= config.bin %> <%= command.id %> my-challenge team-kradle:gemini-3-flash,team-kradle:grok-4-1-fast",
|
|
22
|
+
"<%= config.bin %> <%= command.id %> capture-the-flag red=team-kradle:gemini-3-flash blue=team-kradle:grok-4-1-fast",
|
|
18
23
|
];
|
|
19
24
|
static args = {
|
|
20
25
|
challengeSlug: getChallengeSlugArgument({
|
|
21
26
|
description: "Challenge slug to run (e.g., 'my-challenge' or 'team-name:my-challenge')",
|
|
22
|
-
allowTeam: true,
|
|
23
27
|
}),
|
|
24
28
|
};
|
|
25
29
|
static flags = {
|
|
@@ -101,15 +105,178 @@ export default class Run extends Command {
|
|
|
101
105
|
this.log(result.summary);
|
|
102
106
|
}
|
|
103
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Parse inline agent arguments into participants.
|
|
110
|
+
* Supports formats:
|
|
111
|
+
* - team=agent1,agent2 -> assigns agents to a role/team
|
|
112
|
+
* - agent1,agent2 -> assigns agents to "default" role
|
|
113
|
+
* Same team can be specified multiple times and agents are merged.
|
|
114
|
+
*/
|
|
115
|
+
parseInlineAgents(agentArgs) {
|
|
116
|
+
const roleAgents = new Map();
|
|
117
|
+
for (const arg of agentArgs) {
|
|
118
|
+
let role;
|
|
119
|
+
let agentsStr;
|
|
120
|
+
if (arg.includes("=")) {
|
|
121
|
+
// Format: team=agent1,agent2
|
|
122
|
+
const eqIndex = arg.indexOf("=");
|
|
123
|
+
role = arg.slice(0, eqIndex);
|
|
124
|
+
agentsStr = arg.slice(eqIndex + 1);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Format: agent1,agent2 (no team specified, use "default")
|
|
128
|
+
role = "default";
|
|
129
|
+
agentsStr = arg;
|
|
130
|
+
}
|
|
131
|
+
const agents = agentsStr.split(",").filter((a) => a.length > 0);
|
|
132
|
+
// Merge with existing agents for this role
|
|
133
|
+
const existing = roleAgents.get(role) || [];
|
|
134
|
+
roleAgents.set(role, [...existing, ...agents]);
|
|
135
|
+
}
|
|
136
|
+
// Convert to participants array
|
|
137
|
+
const participants = [];
|
|
138
|
+
for (const [role, agents] of roleAgents) {
|
|
139
|
+
for (const agent of agents) {
|
|
140
|
+
participants.push({ agent, role });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return participants;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Interactive agent selection when no agents are specified inline.
|
|
147
|
+
* Fetches challenge config from cloud and prompts user to select agents for each role.
|
|
148
|
+
*/
|
|
149
|
+
async selectAgentsInteractively(challenge, availableAgents) {
|
|
150
|
+
const participants = [];
|
|
151
|
+
const roles = challenge.roles;
|
|
152
|
+
const roleNames = Object.keys(roles);
|
|
153
|
+
// All selected agents across all roles - this is used to display already-selected agents at the top of the prompt
|
|
154
|
+
const allSelectedAgents = [];
|
|
155
|
+
// Build agent choices for the prompt
|
|
156
|
+
const agentChoices = availableAgents
|
|
157
|
+
.filter((a) => a.username !== undefined)
|
|
158
|
+
.map((a) => ({
|
|
159
|
+
name: a.username,
|
|
160
|
+
message: a.username,
|
|
161
|
+
}));
|
|
162
|
+
if (agentChoices.length === 0) {
|
|
163
|
+
this.error("No agents available. Please check your API key and permissions.");
|
|
164
|
+
}
|
|
165
|
+
const COLORS = [pc.yellow, pc.magenta, pc.cyan, pc.green, pc.blue, pc.red];
|
|
166
|
+
for (let i = 0; i < roleNames.length; i++) {
|
|
167
|
+
const roleName = roleNames[i];
|
|
168
|
+
const color = COLORS[i % COLORS.length];
|
|
169
|
+
const roleConfig = roles[roleName];
|
|
170
|
+
const min = roleConfig.minParticipants;
|
|
171
|
+
const max = roleConfig.maxParticipants;
|
|
172
|
+
// Build prompt message
|
|
173
|
+
let message = `Select agents for role "${pc.bold(color(roleName))}"`;
|
|
174
|
+
if (min !== undefined && max !== undefined) {
|
|
175
|
+
message += ` (min: ${min}, max: ${max})`;
|
|
176
|
+
}
|
|
177
|
+
else if (min !== undefined) {
|
|
178
|
+
message += ` (min: ${min})`;
|
|
179
|
+
}
|
|
180
|
+
else if (max !== undefined) {
|
|
181
|
+
message += ` (max: ${max})`;
|
|
182
|
+
}
|
|
183
|
+
// Use autocomplete for searchable selection with multiple selections
|
|
184
|
+
const selectedAgents = [];
|
|
185
|
+
let selecting = true;
|
|
186
|
+
while (selecting) {
|
|
187
|
+
clearScreen();
|
|
188
|
+
agentChoices.sort((a, b) => {
|
|
189
|
+
// First, display already-selected agents at the top
|
|
190
|
+
if (allSelectedAgents.includes(a.name))
|
|
191
|
+
return -1;
|
|
192
|
+
if (allSelectedAgents.includes(b.name))
|
|
193
|
+
return 1;
|
|
194
|
+
// Then sort by name
|
|
195
|
+
return a.message.localeCompare(b.message);
|
|
196
|
+
});
|
|
197
|
+
const response = await enquirer.prompt({
|
|
198
|
+
type: "autocomplete",
|
|
199
|
+
name: "agent",
|
|
200
|
+
message: selectedAgents.length > 0 ? `${message} [${selectedAgents.length} selected]` : message,
|
|
201
|
+
// @ts-expect-error - somehow this is not typed correctly
|
|
202
|
+
limit: 10,
|
|
203
|
+
choices: [
|
|
204
|
+
...(selectedAgents.length >= (min ?? 1)
|
|
205
|
+
? [{ name: "__done__", message: pc.green("✓ Done selecting") }]
|
|
206
|
+
: []),
|
|
207
|
+
...agentChoices,
|
|
208
|
+
],
|
|
209
|
+
suggest: (input, choices) => {
|
|
210
|
+
if (!input)
|
|
211
|
+
return choices;
|
|
212
|
+
return choices.flatMap((choice) => {
|
|
213
|
+
if (choice.name === "__done__")
|
|
214
|
+
return [choice];
|
|
215
|
+
const highlighted = fuzzyHighlight(input, choice.message, pc.red);
|
|
216
|
+
return highlighted ? [{ ...choice, message: highlighted }] : [];
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
footer: () => {
|
|
220
|
+
let currentlySelectedString = "";
|
|
221
|
+
if (selectedAgents.length > 0) {
|
|
222
|
+
const groupedAgents = Object.groupBy(selectedAgents, (agent) => agent);
|
|
223
|
+
currentlySelectedString = `\nCurrently selected: ${Object.entries(groupedAgents)
|
|
224
|
+
.map(([agent, arr]) => (arr?.length === 1 ? agent : `${agent} x${arr?.length}`))
|
|
225
|
+
.join(", ")}`;
|
|
226
|
+
}
|
|
227
|
+
return pc.dim(`Type to search for agents, press Enter to select, or press Ctrl+C to cancel.${currentlySelectedString}`);
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
if (response.agent === "__done__") {
|
|
231
|
+
selecting = false;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
selectedAgents.push(response.agent);
|
|
235
|
+
allSelectedAgents.push(response.agent);
|
|
236
|
+
// If max is defined and reached, stop selecting
|
|
237
|
+
if (max !== undefined && selectedAgents.length >= max) {
|
|
238
|
+
selecting = false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// Validate minimum
|
|
243
|
+
if (min !== undefined && selectedAgents.length < min) {
|
|
244
|
+
this.error(`Role "${roleName}" requires at least ${min} agents, but only ${selectedAgents.length} selected.`);
|
|
245
|
+
}
|
|
246
|
+
// Add participants for this role
|
|
247
|
+
for (const agent of selectedAgents) {
|
|
248
|
+
participants.push({ agent, role: roleName });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return participants;
|
|
252
|
+
}
|
|
104
253
|
async run() {
|
|
105
|
-
const { args, flags } = await this.parse(Run);
|
|
254
|
+
const { args, flags, argv } = await this.parse(Run);
|
|
106
255
|
const apiUrl = flags.studio ? flags["studio-api-url"] : flags["api-url"];
|
|
107
|
-
const
|
|
256
|
+
const api = new ApiClient(apiUrl, flags["api-key"]);
|
|
108
257
|
const challengeSlug = args.challengeSlug;
|
|
258
|
+
// Get extra arguments (after the challenge slug) for inline agent specification
|
|
259
|
+
const agentArgs = argv.filter((arg) => arg !== challengeSlug);
|
|
260
|
+
let participants;
|
|
261
|
+
if (agentArgs.length > 0) {
|
|
262
|
+
// Parse inline agent specification
|
|
263
|
+
participants = this.parseInlineAgents(agentArgs);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// Interactive mode: fetch challenge config and prompt for agents
|
|
267
|
+
this.log(pc.blue(`>> Fetching challenge configuration...`));
|
|
268
|
+
const [challenge, availableAgents] = await Promise.all([api.getChallenge(challengeSlug), api.listKradleAgents()]);
|
|
269
|
+
this.log(pc.dim(`Challenge: ${challenge.name}`));
|
|
270
|
+
this.log(pc.dim(`Roles: ${Object.keys(challenge.roles).join(", ")}`));
|
|
271
|
+
this.log("");
|
|
272
|
+
participants = await this.selectAgentsInteractively(challenge, availableAgents);
|
|
273
|
+
}
|
|
274
|
+
if (participants.length === 0) {
|
|
275
|
+
this.error("No participants specified. Use inline syntax or select agents interactively.");
|
|
276
|
+
}
|
|
109
277
|
try {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const response = await studioApi.runChallenge({
|
|
278
|
+
this.log(pc.blue(`\n>> Running challenge: ${challengeSlug}${flags.studio ? " (studio)" : ""}...`));
|
|
279
|
+
const response = await api.runChallenge({
|
|
113
280
|
challenge: challengeSlug,
|
|
114
281
|
participants,
|
|
115
282
|
jobType: "foreground",
|
|
@@ -118,7 +285,7 @@ export default class Run extends Command {
|
|
|
118
285
|
const runId = response.runIds[0];
|
|
119
286
|
const baseUrl = flags.studio ? flags["studio-url"] : flags["web-url"];
|
|
120
287
|
const runUrl = `${baseUrl}/runs/${runId}`;
|
|
121
|
-
this.log(pc.green("\n
|
|
288
|
+
this.log(pc.green("\n✓ Challenge started!"));
|
|
122
289
|
this.log(pc.dim(`Run ID: ${runId}`));
|
|
123
290
|
this.log(pc.dim(`Run URL: ${runUrl}`));
|
|
124
291
|
if (!flags["no-open"]) {
|
|
@@ -126,11 +293,11 @@ export default class Run extends Command {
|
|
|
126
293
|
}
|
|
127
294
|
if (!flags["no-wait"]) {
|
|
128
295
|
this.log(pc.blue("\n>> Waiting for run to complete...\n"));
|
|
129
|
-
await this.pollForCompletion(
|
|
296
|
+
await this.pollForCompletion(api, runId, !flags["no-summary"]);
|
|
130
297
|
}
|
|
131
298
|
}
|
|
132
299
|
else {
|
|
133
|
-
this.log(pc.yellow("
|
|
300
|
+
this.log(pc.yellow("⚠ Challenge started but no run ID returned"));
|
|
134
301
|
}
|
|
135
302
|
}
|
|
136
303
|
catch (error) {
|
|
@@ -9,6 +9,7 @@ export default class GetRun extends Command {
|
|
|
9
9
|
"api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
"api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
"no-logs": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
"no-summary": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
13
|
};
|
|
13
14
|
run(): Promise<void>;
|
|
14
15
|
}
|
|
@@ -29,6 +29,7 @@ export default class GetRun extends Command {
|
|
|
29
29
|
static examples = [
|
|
30
30
|
"<%= config.bin %> <%= command.id %> abc123",
|
|
31
31
|
"<%= config.bin %> <%= command.id %> abc123 --no-logs",
|
|
32
|
+
"<%= config.bin %> <%= command.id %> abc123 --no-summary",
|
|
32
33
|
];
|
|
33
34
|
static args = {
|
|
34
35
|
runId: Args.string({
|
|
@@ -41,12 +42,17 @@ export default class GetRun extends Command {
|
|
|
41
42
|
description: "Skip fetching and displaying logs",
|
|
42
43
|
default: false,
|
|
43
44
|
}),
|
|
45
|
+
"no-summary": Flags.boolean({
|
|
46
|
+
description: "Skip displaying the AI-generated summary",
|
|
47
|
+
default: false,
|
|
48
|
+
}),
|
|
44
49
|
...getConfigFlags("api-key", "api-url"),
|
|
45
50
|
};
|
|
46
51
|
async run() {
|
|
47
52
|
const { args, flags } = await this.parse(GetRun);
|
|
48
53
|
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
49
54
|
const showLogs = !flags["no-logs"];
|
|
55
|
+
const showSummary = !flags["no-summary"];
|
|
50
56
|
this.log(pc.blue(`>> Loading run ${args.runId}...`));
|
|
51
57
|
try {
|
|
52
58
|
const [runResult, logs] = await Promise.all([
|
|
@@ -88,6 +94,11 @@ export default class GetRun extends Command {
|
|
|
88
94
|
this.log(`${participantId.padEnd(widths[0])} ${agentPadded} ${winner} ${score.padEnd(widths[3])} ${timeToSuccess}`);
|
|
89
95
|
}
|
|
90
96
|
}
|
|
97
|
+
// AI Summary
|
|
98
|
+
if (showSummary && runResult.summary) {
|
|
99
|
+
this.log(pc.bold("\n=== Summary ===\n"));
|
|
100
|
+
this.log(runResult.summary);
|
|
101
|
+
}
|
|
91
102
|
// Logs
|
|
92
103
|
if (showLogs) {
|
|
93
104
|
if (logs.length > 0) {
|
|
@@ -4,7 +4,7 @@ import chokidar from "chokidar";
|
|
|
4
4
|
import { Listr } from "listr2";
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
import { ApiClient } from "../../lib/api-client.js";
|
|
7
|
-
import { getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
7
|
+
import { extractShortSlug, getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
8
8
|
import { Challenge, SOURCE_FOLDER } from "../../lib/challenge.js";
|
|
9
9
|
import { getConfigFlags } from "../../lib/flags.js";
|
|
10
10
|
import { clearScreen, debounced } from "../../lib/utils.js";
|
|
@@ -20,7 +20,12 @@ export default class Watch extends Command {
|
|
|
20
20
|
};
|
|
21
21
|
async run() {
|
|
22
22
|
const { args, flags } = await this.parse(Watch);
|
|
23
|
-
|
|
23
|
+
// Validate that the challenge exists locally
|
|
24
|
+
const validation = await Challenge.validateForLocalOperation(args.challengeSlug, flags["challenges-path"]);
|
|
25
|
+
if (!validation.isValid && validation.error) {
|
|
26
|
+
this.error(pc.red(validation.error));
|
|
27
|
+
}
|
|
28
|
+
const challenge = new Challenge(extractShortSlug(args.challengeSlug), flags["challenges-path"]);
|
|
24
29
|
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
25
30
|
const debounceSeconds = 1;
|
|
26
31
|
let building = false;
|
|
@@ -57,7 +62,7 @@ export default class Watch extends Command {
|
|
|
57
62
|
{
|
|
58
63
|
title: "Building datapack",
|
|
59
64
|
task: async (_ctx, task) => {
|
|
60
|
-
await challenge.build(!flags.verbose);
|
|
65
|
+
await challenge.build(!flags.verbose, lastConfig);
|
|
61
66
|
task.title = "Datapack built";
|
|
62
67
|
},
|
|
63
68
|
},
|
|
@@ -59,6 +59,8 @@ export default class Import extends Command {
|
|
|
59
59
|
this.log(`World "${slug}" already exists. Re-import will update the tarball but preserve config.ts.`);
|
|
60
60
|
}
|
|
61
61
|
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
62
|
+
// Context to share data between tasks
|
|
63
|
+
const ctx = {};
|
|
62
64
|
const tasks = new Listr([
|
|
63
65
|
{
|
|
64
66
|
title: "Creating world directory",
|
|
@@ -82,26 +84,32 @@ export default class Import extends Command {
|
|
|
82
84
|
},
|
|
83
85
|
},
|
|
84
86
|
{
|
|
85
|
-
title: "
|
|
86
|
-
enabled: () => !isReimport,
|
|
87
|
+
title: "Ensuring world exists in cloud",
|
|
87
88
|
task: async (_ctx, task) => {
|
|
88
89
|
const exists = await api.worldExists(slug);
|
|
89
|
-
if (exists) {
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
if (!exists) {
|
|
91
|
+
const config = {
|
|
92
|
+
name: slug,
|
|
93
|
+
domain: "minecraft",
|
|
94
|
+
};
|
|
95
|
+
const cloudWorld = await api.createWorld(slug, config);
|
|
96
|
+
ctx.worldId = cloudWorld.id;
|
|
97
|
+
task.title = "Created world in cloud";
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
const cloudWorld = await api.getWorld(slug);
|
|
101
|
+
ctx.worldId = cloudWorld.id;
|
|
102
|
+
task.title = "World exists in cloud";
|
|
92
103
|
}
|
|
93
|
-
const config = {
|
|
94
|
-
name: slug,
|
|
95
|
-
domain: "minecraft",
|
|
96
|
-
};
|
|
97
|
-
await api.createWorld(slug, config);
|
|
98
|
-
task.title = "Created world in cloud";
|
|
99
104
|
},
|
|
100
105
|
},
|
|
101
106
|
{
|
|
102
107
|
title: "Uploading world to cloud",
|
|
103
108
|
task: async (_ctx, task) => {
|
|
104
|
-
|
|
109
|
+
if (!ctx.worldId) {
|
|
110
|
+
throw new Error("Missing world ID");
|
|
111
|
+
}
|
|
112
|
+
await api.uploadWorldFile(slug, sourcePath, ctx.worldId);
|
|
105
113
|
task.title = "Uploaded world to cloud";
|
|
106
114
|
},
|
|
107
115
|
},
|
|
@@ -20,7 +20,6 @@ export default class Pull extends Command {
|
|
|
20
20
|
worldSlug: getWorldSlugArgument({
|
|
21
21
|
description: "World slug to pull (interactive selection if omitted)",
|
|
22
22
|
required: false,
|
|
23
|
-
allowTeam: true,
|
|
24
23
|
}),
|
|
25
24
|
};
|
|
26
25
|
static flags = {
|
|
@@ -111,6 +110,13 @@ export default class Pull extends Command {
|
|
|
111
110
|
task.title = "Downloaded world";
|
|
112
111
|
},
|
|
113
112
|
},
|
|
113
|
+
{
|
|
114
|
+
title: "Normalizing tarball structure",
|
|
115
|
+
task: async (_ctx, task) => {
|
|
116
|
+
await world.normalizeDownloadedTarball();
|
|
117
|
+
task.title = "Tarball structure normalized";
|
|
118
|
+
},
|
|
119
|
+
},
|
|
114
120
|
{
|
|
115
121
|
title: "Creating config.ts",
|
|
116
122
|
task: async (_ctx, task) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import { Command, Flags, loadHelpClass } from "@oclif/core";
|
|
2
3
|
import { Listr } from "listr2";
|
|
3
4
|
import pc from "picocolors";
|
|
@@ -65,6 +66,8 @@ export default class Push extends Command {
|
|
|
65
66
|
continue;
|
|
66
67
|
}
|
|
67
68
|
const config = await world.loadConfig();
|
|
69
|
+
// Context to share data between tasks
|
|
70
|
+
const ctx = {};
|
|
68
71
|
const tasks = new Listr([
|
|
69
72
|
{
|
|
70
73
|
title: "Ensuring world exists in cloud",
|
|
@@ -83,6 +86,7 @@ export default class Push extends Command {
|
|
|
83
86
|
title: "Checking world visibility",
|
|
84
87
|
task: async (_ctx, task) => {
|
|
85
88
|
const cloudWorld = await api.getWorld(worldSlug);
|
|
89
|
+
ctx.worldId = cloudWorld.id;
|
|
86
90
|
if (cloudWorld.visibility === "public") {
|
|
87
91
|
task.title = "World is public, setting to private for update...";
|
|
88
92
|
await api.updateWorldVisibility(worldSlug, "private");
|
|
@@ -100,10 +104,20 @@ export default class Push extends Command {
|
|
|
100
104
|
task.title = "Config uploaded";
|
|
101
105
|
},
|
|
102
106
|
},
|
|
107
|
+
{
|
|
108
|
+
title: "Extracting world tarball",
|
|
109
|
+
task: async (_ctx, task) => {
|
|
110
|
+
ctx.extractedDir = await world.extractToTempDir();
|
|
111
|
+
task.title = "World tarball extracted";
|
|
112
|
+
},
|
|
113
|
+
},
|
|
103
114
|
{
|
|
104
115
|
title: "Uploading world",
|
|
105
116
|
task: async (_ctx, task) => {
|
|
106
|
-
|
|
117
|
+
if (!ctx.worldId || !ctx.extractedDir) {
|
|
118
|
+
throw new Error("Missing world ID or extracted directory");
|
|
119
|
+
}
|
|
120
|
+
await api.uploadWorldFile(worldSlug, ctx.extractedDir, ctx.worldId);
|
|
107
121
|
task.title = "World uploaded";
|
|
108
122
|
},
|
|
109
123
|
},
|
|
@@ -116,7 +130,15 @@ export default class Push extends Command {
|
|
|
116
130
|
},
|
|
117
131
|
},
|
|
118
132
|
]);
|
|
119
|
-
|
|
133
|
+
try {
|
|
134
|
+
await tasks.run();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
// Clean up extracted directory
|
|
138
|
+
if (ctx.extractedDir) {
|
|
139
|
+
await fs.rm(ctx.extractedDir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
120
142
|
this.log(pc.green(`✓ World pushed: ${worldSlug}`));
|
|
121
143
|
this.log();
|
|
122
144
|
}
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -150,7 +150,15 @@ export declare class ApiClient {
|
|
|
150
150
|
deleteWorld(slug: string): Promise<void>;
|
|
151
151
|
getWorldUploadUrl(slug: string): Promise<string>;
|
|
152
152
|
getWorldDownloadUrl(slug: string): Promise<DownloadUrlResponse>;
|
|
153
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Upload a world folder to Google Cloud Storage.
|
|
155
|
+
* The folder must contain level.dat at its root.
|
|
156
|
+
* The tarball will be created with the world ID as the root folder (e.g., {worldId}/level.dat).
|
|
157
|
+
* @param slug - The slug of the world (used to get the upload URL).
|
|
158
|
+
* @param worldFolderPath - Path to the world folder containing level.dat at root.
|
|
159
|
+
* @param worldId - The world ID to use as the root folder in the tarball.
|
|
160
|
+
*/
|
|
161
|
+
uploadWorldFile(slug: string, worldFolderPath: string, worldId: string): Promise<void>;
|
|
154
162
|
/**
|
|
155
163
|
* Get the dashboard URL for the current environment.
|
|
156
164
|
* @returns The dashboard URL.
|