@duvoai/cli 0.0.1 → 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 +61 -0
- package/dist/api/agent-schemas.d.ts +427 -0
- package/dist/api/agent-schemas.js +20 -0
- package/dist/api/run-schemas.d.ts +621 -0
- package/dist/api/run-schemas.js +91 -0
- package/dist/bin/create-program.js +8 -1
- package/dist/commands/agents/create.d.ts +2 -0
- package/dist/commands/agents/create.js +182 -0
- package/dist/commands/agents/get.js +5 -11
- package/dist/commands/agents/index.js +2 -0
- package/dist/commands/agents/list.js +5 -11
- package/dist/commands/api.js +5 -13
- package/dist/commands/runs/get.d.ts +2 -0
- package/dist/commands/runs/get.js +59 -0
- package/dist/commands/runs/index.d.ts +2 -0
- package/dist/commands/runs/index.js +17 -0
- package/dist/commands/runs/messages.d.ts +2 -0
- package/dist/commands/runs/messages.js +80 -0
- package/dist/commands/runs/respond.d.ts +2 -0
- package/dist/commands/runs/respond.js +105 -0
- package/dist/commands/runs/send-message.d.ts +2 -0
- package/dist/commands/runs/send-message.js +41 -0
- package/dist/commands/runs/start.d.ts +2 -0
- package/dist/commands/runs/start.js +54 -0
- package/dist/commands/runs/stop.d.ts +2 -0
- package/dist/commands/runs/stop.js +41 -0
- package/dist/commands/shared/require-auth.d.ts +12 -0
- package/dist/commands/shared/require-auth.js +18 -0
- package/dist/commands/whoami.js +2 -9
- package/dist/output/green.d.ts +5 -0
- package/dist/output/green.js +18 -0
- package/package.json +4 -2
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
1
4
|
import { Command } from "commander";
|
|
2
5
|
import { registerAgents } from "../commands/agents/index.js";
|
|
3
6
|
import { registerApi } from "../commands/api.js";
|
|
4
7
|
import { registerLogin } from "../commands/login.js";
|
|
5
8
|
import { registerLogout } from "../commands/logout.js";
|
|
6
9
|
import { registerProfiles } from "../commands/profiles/index.js";
|
|
10
|
+
import { registerRuns } from "../commands/runs/index.js";
|
|
7
11
|
import { registerWhoami } from "../commands/whoami.js";
|
|
12
|
+
const packageJsonPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json");
|
|
13
|
+
const { version } = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
8
14
|
/**
|
|
9
15
|
* Build a fresh `Command` tree for the CLI. Shared between the real
|
|
10
16
|
* binary (`bin/duvo.ts`) and the test harness so tests can never
|
|
@@ -15,7 +21,7 @@ export function createProgram() {
|
|
|
15
21
|
program
|
|
16
22
|
.name("duvo")
|
|
17
23
|
.description("Command-line interface for the Duvo public API")
|
|
18
|
-
.version(
|
|
24
|
+
.version(version)
|
|
19
25
|
.option("--profile <name>", "use a specific profile for this invocation (overrides default profile)");
|
|
20
26
|
registerLogin(program);
|
|
21
27
|
registerLogout(program);
|
|
@@ -23,5 +29,6 @@ export function createProgram() {
|
|
|
23
29
|
registerProfiles(program);
|
|
24
30
|
registerApi(program);
|
|
25
31
|
registerAgents(program);
|
|
32
|
+
registerRuns(program);
|
|
26
33
|
return program;
|
|
27
34
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { confirm, isCancel, text } from "@clack/prompts";
|
|
3
|
+
import { AGENT_CONFIG_VERSION, DEFAULT_AGENT_CONFIG } from "@taskcrew/shared";
|
|
4
|
+
import { buildConfigSchema, createAgentResponseSchema, DEFAULT_BUILD_CONFIG, DEFAULT_BUILD_NAME, DEFAULT_NEW_AGENT_NAME, } from "../../api/agent-schemas.js";
|
|
5
|
+
import { httpRequest } from "../../client/client.js";
|
|
6
|
+
import { CancelCliError, CliError } from "../../errors.js";
|
|
7
|
+
import { green } from "../../output/green.js";
|
|
8
|
+
import { printJson } from "../../output/json.js";
|
|
9
|
+
import { renderKeyValue } from "../../output/text.js";
|
|
10
|
+
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
11
|
+
export function registerAgentsCreate(agents) {
|
|
12
|
+
agents
|
|
13
|
+
.command("create")
|
|
14
|
+
.description("Create a new agent")
|
|
15
|
+
.option("--name <name>", "human-readable agent name")
|
|
16
|
+
.option("--input <text>", "initial agent instructions (merged into the default build config)")
|
|
17
|
+
.option("--config <path>", "path to a JSON file with the full build config (overrides --input)")
|
|
18
|
+
.option("--no-build", "create the agent without an initial build")
|
|
19
|
+
.option("--json", "output raw JSON from the API")
|
|
20
|
+
.action(async function createAction(options) {
|
|
21
|
+
const resolved = await requireAuthenticatedConfig(this);
|
|
22
|
+
const name = await resolveName(options.name);
|
|
23
|
+
const body = { name };
|
|
24
|
+
if (options.build !== false) {
|
|
25
|
+
const config = await resolveBuildConfig(options);
|
|
26
|
+
if (config) {
|
|
27
|
+
body.build = {
|
|
28
|
+
name: DEFAULT_BUILD_NAME,
|
|
29
|
+
config,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const raw = await httpRequest({
|
|
34
|
+
method: "POST",
|
|
35
|
+
path: "/v1/agents",
|
|
36
|
+
apiKey: resolved.apiKey,
|
|
37
|
+
apiBaseUrl: resolved.apiBaseUrl,
|
|
38
|
+
body,
|
|
39
|
+
});
|
|
40
|
+
if (options.json) {
|
|
41
|
+
printJson(raw);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const parsed = createAgentResponseSchema.safeParse(raw);
|
|
45
|
+
if (!parsed.success) {
|
|
46
|
+
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
47
|
+
}
|
|
48
|
+
const { agent, build } = parsed.data;
|
|
49
|
+
const entries = [
|
|
50
|
+
{ label: "ID", value: agent.id },
|
|
51
|
+
{ label: "Name", value: agent.name },
|
|
52
|
+
{ label: "Created", value: agent.created_at ?? "" },
|
|
53
|
+
];
|
|
54
|
+
if (build) {
|
|
55
|
+
entries.push({ label: "Build", value: `${build.id} (${build.name})` });
|
|
56
|
+
}
|
|
57
|
+
console.log(green("Agent created!"));
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(renderKeyValue(entries));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function isInteractiveTerminal() {
|
|
63
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
64
|
+
}
|
|
65
|
+
async function resolveName(flag) {
|
|
66
|
+
if (flag !== undefined) {
|
|
67
|
+
const trimmed = flag.trim();
|
|
68
|
+
if (trimmed.length === 0) {
|
|
69
|
+
throw new CliError("--name must not be empty");
|
|
70
|
+
}
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
if (!isInteractiveTerminal()) {
|
|
74
|
+
return DEFAULT_NEW_AGENT_NAME;
|
|
75
|
+
}
|
|
76
|
+
const answer = await text({
|
|
77
|
+
message: "Agent name",
|
|
78
|
+
placeholder: DEFAULT_NEW_AGENT_NAME,
|
|
79
|
+
defaultValue: DEFAULT_NEW_AGENT_NAME,
|
|
80
|
+
validate: (value) => {
|
|
81
|
+
if (value && value.trim().length === 0) {
|
|
82
|
+
return "Agent name must not be blank.";
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
if (isCancel(answer)) {
|
|
88
|
+
throw new CancelCliError("Create cancelled.");
|
|
89
|
+
}
|
|
90
|
+
const trimmed = answer.trim();
|
|
91
|
+
return trimmed.length === 0 ? DEFAULT_NEW_AGENT_NAME : trimmed;
|
|
92
|
+
}
|
|
93
|
+
async function resolveBuildConfig(options) {
|
|
94
|
+
if (options.config !== undefined) {
|
|
95
|
+
const trimmed = options.config.trim();
|
|
96
|
+
if (trimmed.length === 0) {
|
|
97
|
+
throw new CliError("--config must be a non-empty file path");
|
|
98
|
+
}
|
|
99
|
+
return readConfigFromFile(trimmed);
|
|
100
|
+
}
|
|
101
|
+
if (options.input !== undefined) {
|
|
102
|
+
return buildConfigWithInput(options.input);
|
|
103
|
+
}
|
|
104
|
+
if (!isInteractiveTerminal()) {
|
|
105
|
+
return DEFAULT_BUILD_CONFIG;
|
|
106
|
+
}
|
|
107
|
+
const hasCustomConfig = await confirm({
|
|
108
|
+
message: "Do you have your own build config?",
|
|
109
|
+
initialValue: false,
|
|
110
|
+
});
|
|
111
|
+
if (isCancel(hasCustomConfig)) {
|
|
112
|
+
throw new CancelCliError("Create cancelled.");
|
|
113
|
+
}
|
|
114
|
+
if (hasCustomConfig) {
|
|
115
|
+
const path = await text({
|
|
116
|
+
message: "Path to build config JSON",
|
|
117
|
+
validate: (value) => {
|
|
118
|
+
if (!value || value.trim().length === 0) {
|
|
119
|
+
return "A file path is required.";
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
if (isCancel(path)) {
|
|
125
|
+
throw new CancelCliError("Create cancelled.");
|
|
126
|
+
}
|
|
127
|
+
return readConfigFromFile(path.trim());
|
|
128
|
+
}
|
|
129
|
+
const input = await text({
|
|
130
|
+
message: "Initial instructions for the agent (leave empty for the default)",
|
|
131
|
+
defaultValue: "",
|
|
132
|
+
});
|
|
133
|
+
if (isCancel(input)) {
|
|
134
|
+
throw new CancelCliError("Create cancelled.");
|
|
135
|
+
}
|
|
136
|
+
const trimmed = input.trim();
|
|
137
|
+
if (trimmed.length === 0) {
|
|
138
|
+
return DEFAULT_BUILD_CONFIG;
|
|
139
|
+
}
|
|
140
|
+
return buildConfigWithInput(trimmed);
|
|
141
|
+
}
|
|
142
|
+
function buildConfigWithInput(input) {
|
|
143
|
+
const trimmed = input.trim();
|
|
144
|
+
if (trimmed.length === 0) {
|
|
145
|
+
return DEFAULT_BUILD_CONFIG;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
version: AGENT_CONFIG_VERSION,
|
|
149
|
+
data: {
|
|
150
|
+
...DEFAULT_AGENT_CONFIG,
|
|
151
|
+
input: [
|
|
152
|
+
{
|
|
153
|
+
role: "system",
|
|
154
|
+
content: [{ type: "input_text", text: trimmed }],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function readConfigFromFile(path) {
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = await readFile(path, "utf8");
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
throw new CliError(`Could not read config file "${path}": ${message}`);
|
|
168
|
+
}
|
|
169
|
+
let parsed;
|
|
170
|
+
try {
|
|
171
|
+
parsed = JSON.parse(raw);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
throw new CliError(`Config file "${path}" is not valid JSON: ${message}`);
|
|
176
|
+
}
|
|
177
|
+
const result = buildConfigSchema.safeParse(parsed);
|
|
178
|
+
if (!result.success) {
|
|
179
|
+
throw new CliError(`Config file "${path}" does not match expected build config shape (version "${AGENT_CONFIG_VERSION}"): ${result.error.message}`);
|
|
180
|
+
}
|
|
181
|
+
return result.data;
|
|
182
|
+
}
|
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
import { getAgentResponseSchema } from "../../api/agent-schemas.js";
|
|
2
2
|
import { httpRequest } from "../../client/client.js";
|
|
3
|
-
import {
|
|
4
|
-
import { AuthCliError, CliError } from "../../errors.js";
|
|
3
|
+
import { CliError } from "../../errors.js";
|
|
5
4
|
import { printJson } from "../../output/json.js";
|
|
6
5
|
import { renderKeyValue } from "../../output/text.js";
|
|
7
|
-
import {
|
|
6
|
+
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
8
7
|
export function registerAgentsGet(agents) {
|
|
9
8
|
agents
|
|
10
9
|
.command("get <id>")
|
|
11
10
|
.description("Get an agent by ID")
|
|
12
11
|
.option("--json", "output raw JSON from the API")
|
|
13
12
|
.action(async function getAction(id, options) {
|
|
14
|
-
const
|
|
15
|
-
profileFlag: getProfileFlag(this),
|
|
16
|
-
});
|
|
17
|
-
if (!resolved.apiKey) {
|
|
18
|
-
throw new AuthCliError("Not logged in. Run `duvo login` to save an API key.");
|
|
19
|
-
}
|
|
13
|
+
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
20
14
|
const raw = await httpRequest({
|
|
21
15
|
method: "GET",
|
|
22
16
|
path: `/v1/agents/${encodeURIComponent(id)}`,
|
|
23
|
-
apiKey
|
|
24
|
-
apiBaseUrl
|
|
17
|
+
apiKey,
|
|
18
|
+
apiBaseUrl,
|
|
25
19
|
});
|
|
26
20
|
if (options.json) {
|
|
27
21
|
printJson(raw);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { registerAgentsCreate } from "./create.js";
|
|
1
2
|
import { registerAgentsGet } from "./get.js";
|
|
2
3
|
import { registerAgentsList } from "./list.js";
|
|
3
4
|
export function registerAgents(program) {
|
|
@@ -6,4 +7,5 @@ export function registerAgents(program) {
|
|
|
6
7
|
.description("Manage agents in the Duvo public API");
|
|
7
8
|
registerAgentsList(agents);
|
|
8
9
|
registerAgentsGet(agents);
|
|
10
|
+
registerAgentsCreate(agents);
|
|
9
11
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { listAgentsResponseSchema, } from "../../api/agent-schemas.js";
|
|
2
2
|
import { httpRequest } from "../../client/client.js";
|
|
3
|
-
import {
|
|
4
|
-
import { AuthCliError, CliError } from "../../errors.js";
|
|
3
|
+
import { CliError } from "../../errors.js";
|
|
5
4
|
import { printJson } from "../../output/json.js";
|
|
6
5
|
import { renderTable } from "../../output/table.js";
|
|
7
6
|
import { parseLimit, parseOffset } from "../shared/pagination.js";
|
|
8
|
-
import {
|
|
7
|
+
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
9
8
|
const DEFAULT_LIMIT = 20;
|
|
10
9
|
const MIN_LIMIT = 1;
|
|
11
10
|
const MAX_LIMIT = 100;
|
|
@@ -28,20 +27,15 @@ export function registerAgentsList(agents) {
|
|
|
28
27
|
max: MAX_LIMIT,
|
|
29
28
|
});
|
|
30
29
|
const offset = parseOffset(options.offset);
|
|
31
|
-
const
|
|
32
|
-
profileFlag: getProfileFlag(this),
|
|
33
|
-
});
|
|
34
|
-
if (!resolved.apiKey) {
|
|
35
|
-
throw new AuthCliError("Not logged in. Run `duvo login` to save an API key.");
|
|
36
|
-
}
|
|
30
|
+
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
37
31
|
const searchParams = new URLSearchParams();
|
|
38
32
|
searchParams.append("limit", String(limit));
|
|
39
33
|
searchParams.append("offset", String(offset));
|
|
40
34
|
const raw = await httpRequest({
|
|
41
35
|
method: "GET",
|
|
42
36
|
path: "/v1/agents",
|
|
43
|
-
apiKey
|
|
44
|
-
apiBaseUrl
|
|
37
|
+
apiKey,
|
|
38
|
+
apiBaseUrl,
|
|
45
39
|
searchParams,
|
|
46
40
|
});
|
|
47
41
|
if (options.json) {
|
package/dist/commands/api.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { buildFieldObject } from "../api/parse-fields.js";
|
|
2
2
|
import { readInputJson } from "../api/read-input.js";
|
|
3
3
|
import { httpRequest } from "../client/client.js";
|
|
4
|
-
import {
|
|
5
|
-
import { CliError, EXIT_CODE } from "../errors.js";
|
|
4
|
+
import { CliError } from "../errors.js";
|
|
6
5
|
import { renderApiResponse } from "./api-render.js";
|
|
7
|
-
import {
|
|
6
|
+
import { requireAuthenticatedConfig, } from "./shared/require-auth.js";
|
|
8
7
|
const METHODS_WITH_BODY = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
9
8
|
const VALID_METHODS = new Set([
|
|
10
9
|
"GET",
|
|
@@ -23,18 +22,11 @@ export function registerApi(program) {
|
|
|
23
22
|
.option("--input <path>", 'raw JSON body from a file, or "-" for stdin (mutually exclusive with -f / -F)')
|
|
24
23
|
.option("--json", "output raw JSON instead of the formatted view")
|
|
25
24
|
.action(async function apiAction(method, path, options) {
|
|
26
|
-
await
|
|
27
|
-
|
|
28
|
-
path,
|
|
29
|
-
options,
|
|
30
|
-
}, { profileFlag: getProfileFlag(this) });
|
|
25
|
+
const resolved = await requireAuthenticatedConfig(this);
|
|
26
|
+
await runApi({ method, path, options }, resolved);
|
|
31
27
|
});
|
|
32
28
|
}
|
|
33
|
-
async function runApi(args,
|
|
34
|
-
const resolved = await resolveConfig(resolveOptions);
|
|
35
|
-
if (!resolved.apiKey) {
|
|
36
|
-
throw new CliError("Not logged in. Run `duvo login` to save an API key.", EXIT_CODE.AUTH);
|
|
37
|
-
}
|
|
29
|
+
async function runApi(args, resolved) {
|
|
38
30
|
const method = args.method.toUpperCase();
|
|
39
31
|
if (!VALID_METHODS.has(method)) {
|
|
40
32
|
throw new CliError(`Invalid HTTP method "${args.method}". Expected one of: ${[...VALID_METHODS].join(", ")}.`);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getRunResponseSchema } from "../../api/run-schemas.js";
|
|
2
|
+
import { httpRequest } from "../../client/client.js";
|
|
3
|
+
import { CliError } from "../../errors.js";
|
|
4
|
+
import { printJson } from "../../output/json.js";
|
|
5
|
+
import { renderKeyValue } from "../../output/text.js";
|
|
6
|
+
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
7
|
+
export function registerRunsGet(runs) {
|
|
8
|
+
runs
|
|
9
|
+
.command("get <id>")
|
|
10
|
+
.description("Get an agent run by ID")
|
|
11
|
+
.option("--json", "output raw JSON from the API")
|
|
12
|
+
.action(async function getAction(id, options) {
|
|
13
|
+
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
14
|
+
const raw = await httpRequest({
|
|
15
|
+
method: "GET",
|
|
16
|
+
path: `/v1/runs/${encodeURIComponent(id)}`,
|
|
17
|
+
apiKey,
|
|
18
|
+
apiBaseUrl,
|
|
19
|
+
});
|
|
20
|
+
if (options.json) {
|
|
21
|
+
printJson(raw);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const parsed = getRunResponseSchema.safeParse(raw);
|
|
25
|
+
if (!parsed.success) {
|
|
26
|
+
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
27
|
+
}
|
|
28
|
+
const { run, pending_human_request: pendingHumanRequest } = parsed.data;
|
|
29
|
+
if (!run) {
|
|
30
|
+
throw new CliError(`Run "${id}" not found.`);
|
|
31
|
+
}
|
|
32
|
+
console.log(renderKeyValue([
|
|
33
|
+
{ label: "ID", value: run.id },
|
|
34
|
+
{ label: "Agent", value: run.agent_id },
|
|
35
|
+
{ label: "Build", value: run.build_id },
|
|
36
|
+
{ label: "Status", value: run.status },
|
|
37
|
+
{ label: "Source", value: run.source ?? "" },
|
|
38
|
+
{ label: "Sandbox", value: run.sandbox_id ?? "" },
|
|
39
|
+
{ label: "Started", value: run.started_at ?? "" },
|
|
40
|
+
{ label: "Completed", value: run.completed_at ?? "" },
|
|
41
|
+
{ label: "Created", value: run.created_at },
|
|
42
|
+
{ label: "Updated", value: run.updated_at ?? "" },
|
|
43
|
+
]));
|
|
44
|
+
if (pendingHumanRequest) {
|
|
45
|
+
console.log("");
|
|
46
|
+
console.log("Pending human request:");
|
|
47
|
+
console.log(renderKeyValue([
|
|
48
|
+
{ label: "ID", value: pendingHumanRequest.id },
|
|
49
|
+
{ label: "Type", value: pendingHumanRequest.type },
|
|
50
|
+
{ label: "Title", value: pendingHumanRequest.title },
|
|
51
|
+
{
|
|
52
|
+
label: "Description",
|
|
53
|
+
value: pendingHumanRequest.description ?? "",
|
|
54
|
+
},
|
|
55
|
+
{ label: "Created", value: pendingHumanRequest.created_at },
|
|
56
|
+
]));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { registerRunsGet } from "./get.js";
|
|
2
|
+
import { registerRunsMessages } from "./messages.js";
|
|
3
|
+
import { registerRunsRespond } from "./respond.js";
|
|
4
|
+
import { registerRunsSendMessage } from "./send-message.js";
|
|
5
|
+
import { registerRunsStart } from "./start.js";
|
|
6
|
+
import { registerRunsStop } from "./stop.js";
|
|
7
|
+
export function registerRuns(program) {
|
|
8
|
+
const runs = program
|
|
9
|
+
.command("runs")
|
|
10
|
+
.description("Manage agent runs in the Duvo public API");
|
|
11
|
+
registerRunsStart(runs);
|
|
12
|
+
registerRunsGet(runs);
|
|
13
|
+
registerRunsMessages(runs);
|
|
14
|
+
registerRunsSendMessage(runs);
|
|
15
|
+
registerRunsRespond(runs);
|
|
16
|
+
registerRunsStop(runs);
|
|
17
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { listRunMessagesResponseSchema, } from "../../api/run-schemas.js";
|
|
2
|
+
import { httpRequest } from "../../client/client.js";
|
|
3
|
+
import { CliError } from "../../errors.js";
|
|
4
|
+
import { printJson } from "../../output/json.js";
|
|
5
|
+
import { renderTable } from "../../output/table.js";
|
|
6
|
+
import { parseLimit, parseOffset } from "../shared/pagination.js";
|
|
7
|
+
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
8
|
+
const DEFAULT_LIMIT = 20;
|
|
9
|
+
const MIN_LIMIT = 1;
|
|
10
|
+
const MAX_LIMIT = 50;
|
|
11
|
+
const TEXT_PREVIEW_WIDTH = 60;
|
|
12
|
+
const TABLE_COLUMNS = [
|
|
13
|
+
{ header: "ID", cell: (row) => row.id },
|
|
14
|
+
{ header: "ROLE", cell: (row) => row.role },
|
|
15
|
+
{ header: "TYPE", cell: (row) => row.type },
|
|
16
|
+
{ header: "TIMESTAMP", cell: (row) => row.timestamp },
|
|
17
|
+
{
|
|
18
|
+
header: "PREVIEW",
|
|
19
|
+
cell: previewCell,
|
|
20
|
+
maxWidth: TEXT_PREVIEW_WIDTH,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
function previewCell(row) {
|
|
24
|
+
if (row.text_content) {
|
|
25
|
+
return row.text_content.replace(/\s+/g, " ").trim();
|
|
26
|
+
}
|
|
27
|
+
if (row.tool_call) {
|
|
28
|
+
return `tool_call ${row.tool_call.name}`;
|
|
29
|
+
}
|
|
30
|
+
if (row.tool_result) {
|
|
31
|
+
return `tool_result ${row.tool_result.tool_call_id}`;
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
export function registerRunsMessages(runs) {
|
|
36
|
+
runs
|
|
37
|
+
.command("messages <id>")
|
|
38
|
+
.description("List messages for an agent run")
|
|
39
|
+
.option("--limit <n>", `page size (${MIN_LIMIT}-${MAX_LIMIT})`)
|
|
40
|
+
.option("--offset <n>", "number of messages to skip")
|
|
41
|
+
.option("--json", "output raw JSON from the API")
|
|
42
|
+
.action(async function messagesAction(id, options) {
|
|
43
|
+
const limit = parseLimit(options.limit, {
|
|
44
|
+
defaultValue: DEFAULT_LIMIT,
|
|
45
|
+
min: MIN_LIMIT,
|
|
46
|
+
max: MAX_LIMIT,
|
|
47
|
+
});
|
|
48
|
+
const offset = parseOffset(options.offset);
|
|
49
|
+
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
50
|
+
const searchParams = new URLSearchParams();
|
|
51
|
+
searchParams.append("limit", String(limit));
|
|
52
|
+
searchParams.append("offset", String(offset));
|
|
53
|
+
const raw = await httpRequest({
|
|
54
|
+
method: "GET",
|
|
55
|
+
path: `/v1/runs/${encodeURIComponent(id)}/messages`,
|
|
56
|
+
apiKey,
|
|
57
|
+
apiBaseUrl,
|
|
58
|
+
searchParams,
|
|
59
|
+
});
|
|
60
|
+
if (options.json) {
|
|
61
|
+
printJson(raw);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const parsed = listRunMessagesResponseSchema.safeParse(raw);
|
|
65
|
+
if (!parsed.success) {
|
|
66
|
+
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
67
|
+
}
|
|
68
|
+
const response = parsed.data;
|
|
69
|
+
if (response.messages.length === 0) {
|
|
70
|
+
console.log("No messages found.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(renderTable(TABLE_COLUMNS, response.messages));
|
|
74
|
+
const resolvedOffset = response.offset ?? offset;
|
|
75
|
+
const summary = response.total === null || response.total === undefined
|
|
76
|
+
? `Showing ${response.messages.length} · offset ${resolvedOffset}`
|
|
77
|
+
: `Showing ${response.messages.length} of ${response.total} · offset ${resolvedOffset}`;
|
|
78
|
+
console.log(summary);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getRunResponseSchema, respondHumanRequestResponseSchema, } from "../../api/run-schemas.js";
|
|
2
|
+
import { httpRequest } from "../../client/client.js";
|
|
3
|
+
import { CliError } from "../../errors.js";
|
|
4
|
+
import { green } from "../../output/green.js";
|
|
5
|
+
import { printJson } from "../../output/json.js";
|
|
6
|
+
import { renderKeyValue } from "../../output/text.js";
|
|
7
|
+
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
8
|
+
function collectAnswer(value, previous = []) {
|
|
9
|
+
return [...previous, value];
|
|
10
|
+
}
|
|
11
|
+
function parseAnswers(raw) {
|
|
12
|
+
const answers = Object.create(null);
|
|
13
|
+
for (const entry of raw) {
|
|
14
|
+
const eq = entry.indexOf("=");
|
|
15
|
+
if (eq === -1 || eq === 0) {
|
|
16
|
+
throw new CliError(`Invalid --answer "${entry}". Expected "question=answer".`);
|
|
17
|
+
}
|
|
18
|
+
const question = entry.slice(0, eq);
|
|
19
|
+
const answer = entry.slice(eq + 1);
|
|
20
|
+
if (Object.prototype.hasOwnProperty.call(answers, question)) {
|
|
21
|
+
throw new CliError(`Duplicate --answer for question "${question}". Each question may be answered once.`);
|
|
22
|
+
}
|
|
23
|
+
answers[question] = answer;
|
|
24
|
+
}
|
|
25
|
+
return answers;
|
|
26
|
+
}
|
|
27
|
+
function buildBody(options) {
|
|
28
|
+
const approveSet = options.approve === true;
|
|
29
|
+
const denySet = options.deny === true;
|
|
30
|
+
const answers = options.answer ?? [];
|
|
31
|
+
if (approveSet && denySet) {
|
|
32
|
+
throw new CliError("Pass either --approve or --deny, not both.");
|
|
33
|
+
}
|
|
34
|
+
if ((approveSet || denySet) && answers.length > 0) {
|
|
35
|
+
throw new CliError("Pass either --approve/--deny (for approval-type requests) or --answer (for question-type requests), not both.");
|
|
36
|
+
}
|
|
37
|
+
if (approveSet) {
|
|
38
|
+
return { approved: true };
|
|
39
|
+
}
|
|
40
|
+
if (denySet) {
|
|
41
|
+
return { approved: false };
|
|
42
|
+
}
|
|
43
|
+
if (answers.length > 0) {
|
|
44
|
+
return { answers: parseAnswers(answers) };
|
|
45
|
+
}
|
|
46
|
+
throw new CliError("Provide --approve, --deny, or at least one --answer question=answer.");
|
|
47
|
+
}
|
|
48
|
+
async function resolvePendingRequestId(runId, apiKey, apiBaseUrl) {
|
|
49
|
+
const raw = await httpRequest({
|
|
50
|
+
method: "GET",
|
|
51
|
+
path: `/v1/runs/${encodeURIComponent(runId)}`,
|
|
52
|
+
apiKey,
|
|
53
|
+
apiBaseUrl,
|
|
54
|
+
});
|
|
55
|
+
const parsed = getRunResponseSchema.safeParse(raw);
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
58
|
+
}
|
|
59
|
+
const pending = parsed.data.pending_human_request;
|
|
60
|
+
if (!pending) {
|
|
61
|
+
throw new CliError(`Run ${runId} has no pending human request. Pass the request ID explicitly.`);
|
|
62
|
+
}
|
|
63
|
+
return pending.id;
|
|
64
|
+
}
|
|
65
|
+
export function registerRunsRespond(runs) {
|
|
66
|
+
runs
|
|
67
|
+
.command("respond <run-id> [request-id]")
|
|
68
|
+
.description("Respond to a human-in-the-loop request on a run (defaults to the run's pending request)")
|
|
69
|
+
.option("--approve", "approve an approval-type request")
|
|
70
|
+
.option("--deny", "deny an approval-type request")
|
|
71
|
+
.option("--answer <question=answer>", "answer a question-type request (repeatable)", collectAnswer, [])
|
|
72
|
+
.option("--json", "output raw JSON from the API")
|
|
73
|
+
.action(async function respondAction(runId, requestIdArg, options) {
|
|
74
|
+
const body = buildBody(options);
|
|
75
|
+
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
76
|
+
const requestId = requestIdArg ??
|
|
77
|
+
(await resolvePendingRequestId(runId, apiKey, apiBaseUrl));
|
|
78
|
+
const raw = await httpRequest({
|
|
79
|
+
method: "POST",
|
|
80
|
+
path: `/v1/runs/${encodeURIComponent(runId)}/human-requests/${encodeURIComponent(requestId)}/respond`,
|
|
81
|
+
apiKey,
|
|
82
|
+
apiBaseUrl,
|
|
83
|
+
body,
|
|
84
|
+
});
|
|
85
|
+
if (options.json) {
|
|
86
|
+
printJson(raw);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const parsed = respondHumanRequestResponseSchema.safeParse(raw);
|
|
90
|
+
if (!parsed.success) {
|
|
91
|
+
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
92
|
+
}
|
|
93
|
+
const { human_request: humanRequest } = parsed.data;
|
|
94
|
+
console.log(green("Response sent!"));
|
|
95
|
+
console.log(renderKeyValue([
|
|
96
|
+
{ label: "ID", value: humanRequest.id },
|
|
97
|
+
{ label: "Run", value: humanRequest.run_id },
|
|
98
|
+
{ label: "Title", value: humanRequest.title },
|
|
99
|
+
{ label: "Description", value: humanRequest.description ?? "" },
|
|
100
|
+
{ label: "Response", value: humanRequest.response ?? "" },
|
|
101
|
+
{ label: "Responded", value: humanRequest.responded_at ?? "" },
|
|
102
|
+
{ label: "Created", value: humanRequest.created_at },
|
|
103
|
+
]));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { postRunMessageResponseSchema } from "../../api/run-schemas.js";
|
|
2
|
+
import { httpRequest } from "../../client/client.js";
|
|
3
|
+
import { CliError } from "../../errors.js";
|
|
4
|
+
import { green } from "../../output/green.js";
|
|
5
|
+
import { printJson } from "../../output/json.js";
|
|
6
|
+
import { renderKeyValue } from "../../output/text.js";
|
|
7
|
+
import { requireAuthenticatedConfig } from "../shared/require-auth.js";
|
|
8
|
+
export function registerRunsSendMessage(runs) {
|
|
9
|
+
runs
|
|
10
|
+
.command("send-message <id>")
|
|
11
|
+
.description("Post a message to an agent run")
|
|
12
|
+
.requiredOption("--message <text>", "the message content to send")
|
|
13
|
+
.option("--json", "output raw JSON from the API")
|
|
14
|
+
.action(async function sendMessageAction(id, options) {
|
|
15
|
+
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
16
|
+
const raw = await httpRequest({
|
|
17
|
+
method: "POST",
|
|
18
|
+
path: `/v1/runs/${encodeURIComponent(id)}/messages`,
|
|
19
|
+
apiKey,
|
|
20
|
+
apiBaseUrl,
|
|
21
|
+
body: { message: options.message },
|
|
22
|
+
});
|
|
23
|
+
if (options.json) {
|
|
24
|
+
printJson(raw);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const parsed = postRunMessageResponseSchema.safeParse(raw);
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
30
|
+
}
|
|
31
|
+
const { message } = parsed.data;
|
|
32
|
+
console.log(green("Message sent!"));
|
|
33
|
+
console.log(renderKeyValue([
|
|
34
|
+
{ label: "ID", value: message.id },
|
|
35
|
+
{ label: "Role", value: message.role },
|
|
36
|
+
{ label: "Type", value: message.type },
|
|
37
|
+
{ label: "Timestamp", value: message.timestamp },
|
|
38
|
+
{ label: "Content", value: message.text_content },
|
|
39
|
+
]));
|
|
40
|
+
});
|
|
41
|
+
}
|