@kradle/cli 0.0.17 → 0.1.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 +62 -65
- package/dist/commands/agent/list.d.ts +4 -0
- package/dist/commands/agent/list.js +6 -4
- package/dist/commands/challenge/build.d.ts +9 -1
- package/dist/commands/challenge/build.js +40 -12
- package/dist/commands/challenge/create.d.ts +5 -1
- package/dist/commands/challenge/create.js +17 -18
- package/dist/commands/challenge/delete.d.ts +4 -1
- package/dist/commands/challenge/delete.js +5 -5
- package/dist/commands/challenge/list.d.ts +5 -0
- package/dist/commands/challenge/list.js +9 -10
- package/dist/commands/challenge/run.d.ts +8 -1
- package/dist/commands/challenge/run.js +13 -8
- package/dist/commands/challenge/watch.d.ts +4 -1
- package/dist/commands/challenge/watch.js +8 -8
- package/dist/commands/{evaluation → experiment}/create.d.ts +4 -0
- package/dist/commands/{evaluation → experiment}/create.js +22 -21
- package/dist/commands/{evaluation → experiment}/list.js +17 -19
- package/dist/commands/{evaluation → experiment}/run.d.ts +4 -1
- package/dist/commands/experiment/run.js +61 -0
- package/dist/commands/init.js +2 -2
- package/dist/lib/api-client.d.ts +29 -10
- package/dist/lib/api-client.js +81 -37
- package/dist/lib/arguments.d.ts +3 -2
- package/dist/lib/arguments.js +5 -3
- package/dist/lib/challenge.d.ts +13 -18
- package/dist/lib/challenge.js +58 -62
- package/dist/lib/experiment/experimenter.d.ts +87 -0
- package/dist/lib/{evaluation/evaluator.js → experiment/experimenter.js} +74 -72
- package/dist/lib/{evaluation → experiment}/index.d.ts +1 -1
- package/dist/lib/{evaluation → experiment}/index.js +1 -1
- package/dist/lib/{evaluation → experiment}/runner.js +2 -1
- package/dist/lib/{evaluation → experiment}/tui.d.ts +1 -1
- package/dist/lib/{evaluation → experiment}/tui.js +3 -3
- package/dist/lib/{evaluation → experiment}/types.d.ts +6 -4
- package/dist/lib/{evaluation → experiment}/types.js +4 -3
- package/dist/lib/flags.d.ts +47 -0
- package/dist/lib/flags.js +63 -0
- package/dist/lib/schemas.d.ts +32 -0
- package/dist/lib/schemas.js +8 -0
- package/dist/lib/utils.d.ts +9 -10
- package/dist/lib/utils.js +12 -12
- package/oclif.manifest.json +342 -64
- package/package.json +5 -6
- package/static/challenge.ts +12 -13
- package/static/experiment_template.ts +114 -0
- package/static/project_template/dev.env +5 -5
- package/static/project_template/prod.env +4 -4
- package/static/project_template/tsconfig.json +1 -1
- package/dist/commands/challenge/multi-upload.d.ts +0 -6
- package/dist/commands/challenge/multi-upload.js +0 -80
- package/dist/commands/evaluation/run.js +0 -61
- package/dist/lib/config.d.ts +0 -12
- package/dist/lib/config.js +0 -49
- package/dist/lib/evaluation/evaluator.d.ts +0 -88
- package/static/evaluation_template.ts +0 -69
- /package/dist/commands/{evaluation → experiment}/list.d.ts +0 -0
- /package/dist/lib/{evaluation → experiment}/runner.d.ts +0 -0
|
@@ -6,27 +6,27 @@ import pc from "picocolors";
|
|
|
6
6
|
import { ApiClient } from "../../lib/api-client.js";
|
|
7
7
|
import { getChallengeSlugArgument } from "../../lib/arguments.js";
|
|
8
8
|
import { Challenge, SOURCE_FOLDER } from "../../lib/challenge.js";
|
|
9
|
-
import {
|
|
9
|
+
import { getConfigFlags } from "../../lib/flags.js";
|
|
10
10
|
import { clearScreen, debounced } from "../../lib/utils.js";
|
|
11
11
|
export default class Watch extends Command {
|
|
12
12
|
static description = "Watch a challenge for changes and auto-rebuild/upload";
|
|
13
13
|
static examples = ["<%= config.bin %> <%= command.id %> my-challenge"];
|
|
14
14
|
static flags = {
|
|
15
15
|
verbose: Flags.boolean({ description: "Enable verbose output", default: false }),
|
|
16
|
+
...getConfigFlags("api-key", "api-url", "challenges-path"),
|
|
16
17
|
};
|
|
17
18
|
static args = {
|
|
18
|
-
|
|
19
|
+
challengeSlug: getChallengeSlugArgument({ description: "Challenge slug to watch" }),
|
|
19
20
|
};
|
|
20
21
|
async run() {
|
|
21
22
|
const { args, flags } = await this.parse(Watch);
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const api = new ApiClient(config);
|
|
23
|
+
const challenge = new Challenge(args.challengeSlug, flags["challenges-path"]);
|
|
24
|
+
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
25
25
|
const debounceSeconds = 1;
|
|
26
26
|
let building = false;
|
|
27
27
|
// Perform the initial build and upload
|
|
28
28
|
this.log(pc.blue(`Performing initial build and upload...`));
|
|
29
|
-
let { config: lastConfig, datapackHash: lastHash } = await challenge.buildAndUpload(api);
|
|
29
|
+
let { config: lastConfig, datapackHash: lastHash } = await challenge.buildAndUpload(api, false);
|
|
30
30
|
this.log(pc.green(`✓ Initial build and upload complete`));
|
|
31
31
|
const executeRebuild = async () => {
|
|
32
32
|
building = true;
|
|
@@ -40,7 +40,7 @@ export default class Watch extends Command {
|
|
|
40
40
|
const newConfig = await challenge.loadConfig();
|
|
41
41
|
if (JSON.stringify(newConfig) !== JSON.stringify(lastConfig)) {
|
|
42
42
|
task.title = "Uploading configuration";
|
|
43
|
-
await api.updateChallenge(challenge, newConfig);
|
|
43
|
+
await api.updateChallenge(challenge.shortSlug, newConfig, "private");
|
|
44
44
|
lastConfig = newConfig;
|
|
45
45
|
task.title = "Configuration uploaded";
|
|
46
46
|
}
|
|
@@ -63,7 +63,7 @@ export default class Watch extends Command {
|
|
|
63
63
|
const newHash = await challenge.getDatapackHash();
|
|
64
64
|
if (newHash !== lastHash) {
|
|
65
65
|
task.title = "Uploading datapack";
|
|
66
|
-
await challenge.
|
|
66
|
+
await api.uploadChallengeDatapack(challenge.shortSlug, challenge.tarballPath);
|
|
67
67
|
lastHash = newHash;
|
|
68
68
|
task.title = "Datapack uploaded";
|
|
69
69
|
}
|
|
@@ -5,5 +5,9 @@ export default class Create extends Command {
|
|
|
5
5
|
static args: {
|
|
6
6
|
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
|
+
static flags: {
|
|
9
|
+
"api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
};
|
|
8
12
|
run(): Promise<void>;
|
|
9
13
|
}
|
|
@@ -5,35 +5,36 @@ import { Args, Command } from "@oclif/core";
|
|
|
5
5
|
import enquirer from "enquirer";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import { ApiClient } from "../../lib/api-client.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getConfigFlags } from "../../lib/flags.js";
|
|
9
9
|
import { getStaticResourcePath } from "../../lib/utils.js";
|
|
10
10
|
export default class Create extends Command {
|
|
11
|
-
static description = "Create a new
|
|
12
|
-
static examples = ["<%= config.bin %> <%= command.id %> my-
|
|
11
|
+
static description = "Create a new experiment";
|
|
12
|
+
static examples = ["<%= config.bin %> <%= command.id %> my-experiment"];
|
|
13
13
|
static args = {
|
|
14
14
|
name: Args.string({
|
|
15
|
-
description: "Name of the
|
|
15
|
+
description: "Name of the experiment",
|
|
16
16
|
required: true,
|
|
17
17
|
}),
|
|
18
18
|
};
|
|
19
|
+
static flags = {
|
|
20
|
+
...getConfigFlags("api-key", "api-url"),
|
|
21
|
+
};
|
|
19
22
|
async run() {
|
|
20
|
-
const { args } = await this.parse(Create);
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
// Check if evaluation already exists
|
|
23
|
+
const { args, flags } = await this.parse(Create);
|
|
24
|
+
const experimentDir = path.resolve(process.cwd(), "experiments", args.name);
|
|
25
|
+
const configPath = path.join(experimentDir, "config.ts");
|
|
26
|
+
// Check if experiment already exists
|
|
25
27
|
try {
|
|
26
|
-
await fs.access(
|
|
27
|
-
this.error(pc.red(`
|
|
28
|
+
await fs.access(experimentDir);
|
|
29
|
+
this.error(pc.red(`Experiment '${args.name}' already exists at ${experimentDir}`));
|
|
28
30
|
}
|
|
29
31
|
catch {
|
|
30
32
|
// Directory doesn't exist, which is what we want
|
|
31
33
|
}
|
|
32
|
-
// Create
|
|
33
|
-
await fs.mkdir(
|
|
34
|
-
// Ask for the slug of the challenge to
|
|
35
|
-
const
|
|
36
|
-
const api = new ApiClient(config);
|
|
34
|
+
// Create experiment directory
|
|
35
|
+
await fs.mkdir(experimentDir, { recursive: true });
|
|
36
|
+
// Ask for the slug of the challenge to run the experiment on
|
|
37
|
+
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
37
38
|
const [kradleChallenges, cloudChallenges] = await Promise.all([api.listKradleChallenges(), api.listChallenges()]);
|
|
38
39
|
const choices = [...kradleChallenges, ...cloudChallenges]
|
|
39
40
|
.map((c) => c.slug)
|
|
@@ -45,15 +46,15 @@ export default class Create extends Command {
|
|
|
45
46
|
const response = await enquirer.prompt({
|
|
46
47
|
type: "select",
|
|
47
48
|
name: "challenge",
|
|
48
|
-
message: "Select the challenge
|
|
49
|
+
message: "Select the challenge for this experiment",
|
|
49
50
|
choices: choices,
|
|
50
51
|
});
|
|
51
52
|
// Read template file and fill in the challenge slug, then write to config file
|
|
52
|
-
const templatePath = getStaticResourcePath("
|
|
53
|
+
const templatePath = getStaticResourcePath("experiment_template.ts");
|
|
53
54
|
const template = await fs.readFile(templatePath, "utf-8");
|
|
54
55
|
const filledTemplate = template.replace("[INSERT CHALLENGE SLUG HERE]", response.challenge);
|
|
55
56
|
await fs.writeFile(configPath, filledTemplate);
|
|
56
|
-
this.log(pc.green(`✓ Created
|
|
57
|
+
this.log(pc.green(`✓ Created experiment '${args.name}'`));
|
|
57
58
|
this.log(pc.dim(` Config: ${configPath}`));
|
|
58
59
|
// Offer to open in editor on macOS
|
|
59
60
|
if (process.platform === "darwin") {
|
|
@@ -73,7 +74,7 @@ export default class Create extends Command {
|
|
|
73
74
|
}
|
|
74
75
|
this.log("");
|
|
75
76
|
this.log(pc.blue(">> Next steps:"));
|
|
76
|
-
this.log(pc.dim(` 1. Edit ${path.basename(configPath)} to define your
|
|
77
|
-
this.log(pc.dim(` 2. Run: kradle
|
|
77
|
+
this.log(pc.dim(` 1. Edit ${path.basename(configPath)} to define your experiment runs`));
|
|
78
|
+
this.log(pc.dim(` 2. Run: kradle experiment run ${args.name}`));
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -2,29 +2,27 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Command } from "@oclif/core";
|
|
4
4
|
import pc from "picocolors";
|
|
5
|
-
import { loadConfig } from "../../lib/config.js";
|
|
6
5
|
export default class List extends Command {
|
|
7
|
-
static description = "List all
|
|
6
|
+
static description = "List all experiments";
|
|
8
7
|
static examples = ["<%= config.bin %> <%= command.id %>"];
|
|
9
8
|
async run() {
|
|
10
|
-
this.parse(List);
|
|
11
|
-
|
|
12
|
-
const evaluationsDir = path.resolve(process.cwd(), "evaluations");
|
|
9
|
+
await this.parse(List);
|
|
10
|
+
const experimentsDir = path.resolve(process.cwd(), "experiments");
|
|
13
11
|
try {
|
|
14
|
-
const entries = await fs.readdir(
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
17
|
-
this.log(pc.yellow("No
|
|
18
|
-
this.log(pc.dim(` Run 'kradle
|
|
12
|
+
const entries = await fs.readdir(experimentsDir, { withFileTypes: true });
|
|
13
|
+
const experiments = entries.filter((e) => e.isDirectory());
|
|
14
|
+
if (experiments.length === 0) {
|
|
15
|
+
this.log(pc.yellow("No experiments found."));
|
|
16
|
+
this.log(pc.dim(` Run 'kradle experiment create <name>' to create one.`));
|
|
19
17
|
return;
|
|
20
18
|
}
|
|
21
|
-
this.log(pc.blue(">>
|
|
19
|
+
this.log(pc.blue(">> Experiments:"));
|
|
22
20
|
this.log("");
|
|
23
|
-
for (const
|
|
24
|
-
const
|
|
25
|
-
const hasConfig = await this.fileExists(path.join(
|
|
26
|
-
const hasManifest = await this.fileExists(path.join(
|
|
27
|
-
const hasProgress = await this.fileExists(path.join(
|
|
21
|
+
for (const experiment of experiments) {
|
|
22
|
+
const expDir = path.join(experimentsDir, experiment.name);
|
|
23
|
+
const hasConfig = await this.fileExists(path.join(expDir, "config.ts"));
|
|
24
|
+
const hasManifest = await this.fileExists(path.join(expDir, "manifest.json"));
|
|
25
|
+
const hasProgress = await this.fileExists(path.join(expDir, "progress.json"));
|
|
28
26
|
let status = "";
|
|
29
27
|
if (hasProgress) {
|
|
30
28
|
status = pc.yellow(" (in progress)");
|
|
@@ -35,12 +33,12 @@ export default class List extends Command {
|
|
|
35
33
|
else if (hasConfig) {
|
|
36
34
|
status = pc.dim(" (config only)");
|
|
37
35
|
}
|
|
38
|
-
this.log(` ${pc.bold(
|
|
36
|
+
this.log(` ${pc.bold(experiment.name)}${status}`);
|
|
39
37
|
}
|
|
40
38
|
}
|
|
41
39
|
catch {
|
|
42
|
-
this.log(pc.yellow("No
|
|
43
|
-
this.log(pc.dim(` Run 'kradle
|
|
40
|
+
this.log(pc.yellow("No experiments directory found."));
|
|
41
|
+
this.log(pc.dim(` Run 'kradle experiment create <name>' to create your first experiment.`));
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
async fileExists(filePath) {
|
|
@@ -6,7 +6,10 @@ export default class Run extends Command {
|
|
|
6
6
|
name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
7
7
|
};
|
|
8
8
|
static flags: {
|
|
9
|
-
|
|
9
|
+
"api-key": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
"api-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
"web-url": import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
"new-version": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
13
|
"max-concurrent": import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
14
|
};
|
|
12
15
|
run(): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oclif/core";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { ApiClient } from "../../lib/api-client.js";
|
|
4
|
+
import { Experimenter } from "../../lib/experiment/experimenter.js";
|
|
5
|
+
import { getConfigFlags } from "../../lib/flags.js";
|
|
6
|
+
const DEFAULT_MAX_CONCURRENT = 5;
|
|
7
|
+
export default class Run extends Command {
|
|
8
|
+
static description = "Run an experiment. If the experiment had an ongoing version, it will resume from the last state.";
|
|
9
|
+
static examples = [
|
|
10
|
+
"<%= config.bin %> <%= command.id %> my-experiment",
|
|
11
|
+
"<%= config.bin %> <%= command.id %> my-experiment --new-version",
|
|
12
|
+
"<%= config.bin %> <%= command.id %> my-experiment --max-concurrent 10",
|
|
13
|
+
];
|
|
14
|
+
static args = {
|
|
15
|
+
name: Args.string({
|
|
16
|
+
description: "Name of the experiment to run",
|
|
17
|
+
required: true,
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
static flags = {
|
|
21
|
+
"new-version": Flags.boolean({
|
|
22
|
+
char: "n",
|
|
23
|
+
description: "Start a new version of the experiment",
|
|
24
|
+
default: false,
|
|
25
|
+
}),
|
|
26
|
+
"max-concurrent": Flags.integer({
|
|
27
|
+
char: "m",
|
|
28
|
+
description: "Maximum concurrent runs",
|
|
29
|
+
default: DEFAULT_MAX_CONCURRENT,
|
|
30
|
+
}),
|
|
31
|
+
...getConfigFlags("api-key", "api-url", "web-url"),
|
|
32
|
+
};
|
|
33
|
+
async run() {
|
|
34
|
+
const { args, flags } = await this.parse(Run);
|
|
35
|
+
const api = new ApiClient(flags["api-url"], flags["api-key"]);
|
|
36
|
+
const experimenter = new Experimenter(args.name, flags["web-url"], api);
|
|
37
|
+
// Check if experiment exists
|
|
38
|
+
if (!(await experimenter.exists())) {
|
|
39
|
+
this.error(pc.red(`Experiment '${args.name}' does not exist. Run 'kradle experiment create ${args.name}' first.`));
|
|
40
|
+
}
|
|
41
|
+
// Check if config.ts exists
|
|
42
|
+
if (!(await experimenter.configExists())) {
|
|
43
|
+
this.error(pc.red(`Config file not found at ${experimenter.configPath}`));
|
|
44
|
+
}
|
|
45
|
+
this.log(pc.blue(`>> Starting experiment: ${args.name}`));
|
|
46
|
+
if (flags["new-version"]) {
|
|
47
|
+
this.log(pc.yellow(" Starting a new version of the experiment."));
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
await experimenter.run({
|
|
51
|
+
new: flags["new-version"],
|
|
52
|
+
maxConcurrent: flags["max-concurrent"],
|
|
53
|
+
openMetabase: true,
|
|
54
|
+
});
|
|
55
|
+
this.log(pc.green("\n✓ Experiment complete!"));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
this.error(pc.red(`Experiment failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -34,10 +34,10 @@ export default class Init extends Command {
|
|
|
34
34
|
const nonHiddenFiles = files.filter((f) => !f.startsWith("."));
|
|
35
35
|
const useCurrentDir = nonHiddenFiles.length === 0;
|
|
36
36
|
if (useCurrentDir) {
|
|
37
|
-
this.log(pc.yellow("Current directory is empty, it will be used to store challenges and
|
|
37
|
+
this.log(pc.yellow("Current directory is empty, it will be used to store challenges and experiments."));
|
|
38
38
|
}
|
|
39
39
|
else {
|
|
40
|
-
this.log(pc.yellow("Current directory is not empty, a subdirectory will be created to store challenges and
|
|
40
|
+
this.log(pc.yellow("Current directory is not empty, a subdirectory will be created to store challenges and experiments."));
|
|
41
41
|
}
|
|
42
42
|
let projectName;
|
|
43
43
|
if (flags.name) {
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type z from "zod";
|
|
2
|
-
import type
|
|
3
|
-
import type { Config } from "./config.js";
|
|
4
|
-
import { type AgentSchemaType, type ChallengeSchemaType, HumanSchema, type RunStatusSchemaType } from "./schemas.js";
|
|
2
|
+
import { type AgentSchemaType, type ChallengeConfigSchemaType, type ChallengeSchemaType, HumanSchema, type RunStatusSchemaType } from "./schemas.js";
|
|
5
3
|
export declare class ApiClient {
|
|
6
|
-
private
|
|
7
|
-
|
|
4
|
+
private apiUrl;
|
|
5
|
+
private kradleApiKey;
|
|
6
|
+
constructor(apiUrl: string, kradleApiKey: string);
|
|
8
7
|
private request;
|
|
9
8
|
private get;
|
|
10
9
|
private post;
|
|
@@ -40,16 +39,36 @@ export declare class ApiClient {
|
|
|
40
39
|
createChallenge(slug: string): Promise<unknown>;
|
|
41
40
|
/**
|
|
42
41
|
* Update a challenge definition in the cloud.
|
|
43
|
-
* @param
|
|
44
|
-
* @param challengeConfig - The challenge config to upload.
|
|
42
|
+
* @param challengeSlug - The slug of the challenge.
|
|
43
|
+
* @param challengeConfig - The challenge config to upload.
|
|
44
|
+
* @param visibility - The visibility to set.
|
|
45
45
|
* @returns The updated challenge.
|
|
46
46
|
*/
|
|
47
|
-
updateChallenge(
|
|
48
|
-
|
|
47
|
+
updateChallenge(challengeSlug: string, challengeConfig: ChallengeConfigSchemaType, visibility: "private" | "public"): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Update the visibility of a challenge.
|
|
50
|
+
* @param challengeSlug - The slug of the challenge.
|
|
51
|
+
* @param visibility - The visibility to set.
|
|
52
|
+
* @returns The updated challenge.
|
|
53
|
+
*/
|
|
54
|
+
updateChallengeVisibility(challengeSlug: string, visibility: "private" | "public"): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Upload a challenge datapack to Google Cloud Storage.
|
|
57
|
+
* @param slug - The slug of the challenge.
|
|
58
|
+
* @param tarballPath - The path to the tarball file.
|
|
59
|
+
* @returns The upload URL.
|
|
60
|
+
*/
|
|
61
|
+
uploadChallengeDatapack(slug: string, tarballPath: string): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Get the upload URL for a challenge datapack.
|
|
64
|
+
* @param slug - The slug of the challenge.
|
|
65
|
+
* @returns The upload URL.
|
|
66
|
+
*/
|
|
67
|
+
getChallengeUploadUrl(slug: string): Promise<string>;
|
|
49
68
|
runChallenge(runData: {
|
|
50
69
|
challenge: string;
|
|
51
70
|
participants: unknown[];
|
|
52
|
-
}
|
|
71
|
+
}): Promise<{
|
|
53
72
|
runIds?: string[] | undefined;
|
|
54
73
|
}>;
|
|
55
74
|
deleteChallenge(challengeId: string): Promise<void>;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
1
3
|
import { AgentsResponseSchema, ChallengeSchema, ChallengesResponseSchema, HumanSchema, RunResponseSchema, RunStatusSchema, UploadUrlResponseSchema, } from "./schemas.js";
|
|
2
4
|
const DEFAULT_PAGE_SIZE = 30;
|
|
3
5
|
const DEFAULT_CHALLENGE_SCHEMA = {
|
|
@@ -21,37 +23,38 @@ const DEFAULT_CHALLENGE_SCHEMA = {
|
|
|
21
23
|
},*/,
|
|
22
24
|
};
|
|
23
25
|
export class ApiClient {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
apiUrl;
|
|
27
|
+
kradleApiKey;
|
|
28
|
+
constructor(apiUrl, kradleApiKey) {
|
|
29
|
+
this.apiUrl = apiUrl;
|
|
30
|
+
this.kradleApiKey = kradleApiKey;
|
|
31
|
+
}
|
|
32
|
+
async request(endpoint, options) {
|
|
33
|
+
const fullUrl = `${this.apiUrl}/${endpoint}`;
|
|
31
34
|
const response = await fetch(fullUrl, {
|
|
32
35
|
...options,
|
|
33
36
|
headers: {
|
|
34
|
-
Authorization: `Bearer ${this.
|
|
37
|
+
Authorization: `Bearer ${this.kradleApiKey}`,
|
|
35
38
|
"Content-Type": "application/json",
|
|
36
39
|
...options.headers,
|
|
37
40
|
},
|
|
38
41
|
});
|
|
39
42
|
if (!response.ok) {
|
|
40
43
|
const text = await response.text();
|
|
41
|
-
throw new Error(`API call failed: ${
|
|
44
|
+
throw new Error(`API call failed: ${fullUrl} - ${response.status} ${response.statusText}\n${text}`);
|
|
42
45
|
}
|
|
43
46
|
return response;
|
|
44
47
|
}
|
|
45
|
-
async get(
|
|
46
|
-
const response = await this.request(
|
|
48
|
+
async get(url, options = {}, schema) {
|
|
49
|
+
const response = await this.request(url, {
|
|
47
50
|
method: "GET",
|
|
48
51
|
...options,
|
|
49
52
|
});
|
|
50
53
|
const data = await response.json();
|
|
51
54
|
return schema ? schema.parse(data) : data;
|
|
52
55
|
}
|
|
53
|
-
async post(
|
|
54
|
-
const response = await this.request(
|
|
56
|
+
async post(url, options = {}, schema) {
|
|
57
|
+
const response = await this.request(url, {
|
|
55
58
|
method: "POST",
|
|
56
59
|
...options,
|
|
57
60
|
});
|
|
@@ -62,14 +65,14 @@ export class ApiClient {
|
|
|
62
65
|
const data = JSON.parse(text);
|
|
63
66
|
return schema ? schema.parse(data) : data;
|
|
64
67
|
}
|
|
65
|
-
async put(
|
|
66
|
-
return await this.request(
|
|
68
|
+
async put(url, options = {}) {
|
|
69
|
+
return await this.request(url, {
|
|
67
70
|
method: "PUT",
|
|
68
71
|
...options,
|
|
69
72
|
});
|
|
70
73
|
}
|
|
71
|
-
async delete(
|
|
72
|
-
return await this.request(
|
|
74
|
+
async delete(url, options = {}) {
|
|
75
|
+
return await this.request(url, {
|
|
73
76
|
method: "DELETE",
|
|
74
77
|
...options,
|
|
75
78
|
});
|
|
@@ -97,7 +100,7 @@ export class ApiClient {
|
|
|
97
100
|
if (pageToken)
|
|
98
101
|
params.set("page_token", pageToken);
|
|
99
102
|
params.set("page_size", String(pageSize));
|
|
100
|
-
const response = await this.get(
|
|
103
|
+
const response = await this.get(`${url}?${params}`, {
|
|
101
104
|
headers: {
|
|
102
105
|
"Content-Type": "application/json",
|
|
103
106
|
},
|
|
@@ -109,7 +112,7 @@ export class ApiClient {
|
|
|
109
112
|
}
|
|
110
113
|
async getHuman() {
|
|
111
114
|
const url = "human";
|
|
112
|
-
return this.get(
|
|
115
|
+
return this.get(url, {}, HumanSchema);
|
|
113
116
|
}
|
|
114
117
|
async listChallenges() {
|
|
115
118
|
return this.listResource("challenges", "challenges", ChallengesResponseSchema);
|
|
@@ -122,7 +125,7 @@ export class ApiClient {
|
|
|
122
125
|
}
|
|
123
126
|
async getChallenge(challengeId) {
|
|
124
127
|
const url = `challenges/${challengeId}`;
|
|
125
|
-
return this.get(
|
|
128
|
+
return this.get(url, {}, ChallengeSchema);
|
|
126
129
|
}
|
|
127
130
|
/**
|
|
128
131
|
* Check if a challenge exists in the cloud.
|
|
@@ -132,7 +135,7 @@ export class ApiClient {
|
|
|
132
135
|
async challengeExists(slug) {
|
|
133
136
|
const url = `challenges/${slug}`;
|
|
134
137
|
try {
|
|
135
|
-
const res = await this.get(
|
|
138
|
+
const res = await this.get(url, {});
|
|
136
139
|
return res !== null && res !== undefined;
|
|
137
140
|
}
|
|
138
141
|
catch (error) {
|
|
@@ -142,37 +145,78 @@ export class ApiClient {
|
|
|
142
145
|
async createChallenge(slug) {
|
|
143
146
|
const challenge = { ...DEFAULT_CHALLENGE_SCHEMA, slug: slug, name: slug };
|
|
144
147
|
const url = "challenges";
|
|
145
|
-
return this.post(
|
|
148
|
+
return this.post(url, {
|
|
146
149
|
body: JSON.stringify(challenge),
|
|
147
150
|
});
|
|
148
151
|
}
|
|
149
152
|
/**
|
|
150
153
|
* Update a challenge definition in the cloud.
|
|
151
|
-
* @param
|
|
152
|
-
* @param challengeConfig - The challenge config to upload.
|
|
154
|
+
* @param challengeSlug - The slug of the challenge.
|
|
155
|
+
* @param challengeConfig - The challenge config to upload.
|
|
156
|
+
* @param visibility - The visibility to set.
|
|
157
|
+
* @returns The updated challenge.
|
|
158
|
+
*/
|
|
159
|
+
async updateChallenge(challengeSlug, challengeConfig, visibility) {
|
|
160
|
+
const url = `challenges/${challengeSlug}`;
|
|
161
|
+
const fullConfig = { ...challengeConfig, slug: challengeSlug, visibility };
|
|
162
|
+
await this.put(url, {
|
|
163
|
+
body: JSON.stringify(fullConfig),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Update the visibility of a challenge.
|
|
168
|
+
* @param challengeSlug - The slug of the challenge.
|
|
169
|
+
* @param visibility - The visibility to set.
|
|
153
170
|
* @returns The updated challenge.
|
|
154
171
|
*/
|
|
155
|
-
async
|
|
156
|
-
const url = `challenges/${
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
172
|
+
async updateChallengeVisibility(challengeSlug, visibility) {
|
|
173
|
+
const url = `challenges/${challengeSlug}/visibility`;
|
|
174
|
+
await this.post(url, {
|
|
175
|
+
body: JSON.stringify({ visibility }),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Upload a challenge datapack to Google Cloud Storage.
|
|
180
|
+
* @param slug - The slug of the challenge.
|
|
181
|
+
* @param tarballPath - The path to the tarball file.
|
|
182
|
+
* @returns The upload URL.
|
|
183
|
+
*/
|
|
184
|
+
async uploadChallengeDatapack(slug, tarballPath) {
|
|
185
|
+
const uploadUrl = await this.getChallengeUploadUrl(slug);
|
|
186
|
+
if (!existsSync(tarballPath)) {
|
|
187
|
+
throw new Error(`Tarball not found at ${tarballPath}`);
|
|
188
|
+
}
|
|
189
|
+
const fileBuffer = await fs.readFile(tarballPath);
|
|
190
|
+
const response = await fetch(uploadUrl, {
|
|
191
|
+
method: "PUT",
|
|
192
|
+
headers: {
|
|
193
|
+
"Content-Type": "application/gzip",
|
|
194
|
+
"Content-Length": fileBuffer.length.toString(),
|
|
195
|
+
},
|
|
196
|
+
body: fileBuffer,
|
|
161
197
|
});
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
throw new Error(`Failed to upload datapack: ${response.statusText}`);
|
|
200
|
+
}
|
|
162
201
|
}
|
|
163
|
-
|
|
164
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Get the upload URL for a challenge datapack.
|
|
204
|
+
* @param slug - The slug of the challenge.
|
|
205
|
+
* @returns The upload URL.
|
|
206
|
+
*/
|
|
207
|
+
async getChallengeUploadUrl(slug) {
|
|
208
|
+
const response = await this.get(`challenges/${slug}/datapackUploadUrl`, {}, UploadUrlResponseSchema);
|
|
165
209
|
return response.uploadUrl;
|
|
166
210
|
}
|
|
167
|
-
async runChallenge(runData
|
|
211
|
+
async runChallenge(runData) {
|
|
168
212
|
const url = "jobs";
|
|
169
|
-
return this.post(
|
|
213
|
+
return this.post(url, {
|
|
170
214
|
body: JSON.stringify(runData),
|
|
171
215
|
}, RunResponseSchema);
|
|
172
216
|
}
|
|
173
217
|
async deleteChallenge(challengeId) {
|
|
174
218
|
const url = `challenges/${challengeId}`;
|
|
175
|
-
await this.delete(
|
|
219
|
+
await this.delete(url);
|
|
176
220
|
}
|
|
177
221
|
/**
|
|
178
222
|
* Get the status of a run.
|
|
@@ -181,7 +225,7 @@ export class ApiClient {
|
|
|
181
225
|
*/
|
|
182
226
|
async getRunStatus(runId) {
|
|
183
227
|
const url = `runs/${runId}`;
|
|
184
|
-
return this.get(
|
|
228
|
+
return this.get(url, {}, RunStatusSchema);
|
|
185
229
|
}
|
|
186
230
|
/**
|
|
187
231
|
* Add a tag to a run.
|
|
@@ -191,7 +235,7 @@ export class ApiClient {
|
|
|
191
235
|
*/
|
|
192
236
|
async tagRun(runId, tag) {
|
|
193
237
|
const url = `runs/${runId}/tag`;
|
|
194
|
-
await this.post(
|
|
238
|
+
await this.post(url, {
|
|
195
239
|
body: JSON.stringify({ tag }),
|
|
196
240
|
});
|
|
197
241
|
}
|
package/dist/lib/arguments.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Arg } from "@oclif/core/interfaces";
|
|
|
2
2
|
/**
|
|
3
3
|
* Returns a "challenge slug" argument, and validates it to be a valid challenge slug.
|
|
4
4
|
*/
|
|
5
|
-
export declare function getChallengeSlugArgument({ description }: {
|
|
5
|
+
export declare function getChallengeSlugArgument<R extends boolean = true>({ description, required, }: {
|
|
6
6
|
description: string;
|
|
7
|
-
|
|
7
|
+
required?: R;
|
|
8
|
+
}): Arg<R extends true ? string : string | undefined>;
|
package/dist/lib/arguments.js
CHANGED
|
@@ -3,10 +3,10 @@ const CHALLENGE_SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
|
3
3
|
/**
|
|
4
4
|
* Returns a "challenge slug" argument, and validates it to be a valid challenge slug.
|
|
5
5
|
*/
|
|
6
|
-
export function getChallengeSlugArgument({ description }) {
|
|
7
|
-
|
|
6
|
+
export function getChallengeSlugArgument({ description, required, }) {
|
|
7
|
+
const arg = Args.string({
|
|
8
8
|
description,
|
|
9
|
-
required: true,
|
|
9
|
+
required: required ?? true,
|
|
10
10
|
parse: async (input) => {
|
|
11
11
|
if (!CHALLENGE_SLUG_REGEX.test(input)) {
|
|
12
12
|
throw new Error(`Invalid challenge slug: ${input}. Challenge slugs must be lowercase alphanumeric characters and hyphens, and must not start or end with a hyphen.`);
|
|
@@ -14,4 +14,6 @@ export function getChallengeSlugArgument({ description }) {
|
|
|
14
14
|
return input;
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
|
+
// biome-ignore lint/suspicious/noExplicitAny: Typescript can't handle the conditional "required"
|
|
18
|
+
return arg;
|
|
17
19
|
}
|