@duvoai/cli 0.1.0 → 0.2.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 +71 -11
- package/dist/bin/duvo.js +11726 -6
- package/dist/bin/duvo.js.map +7 -0
- package/package.json +10 -8
- package/dist/api/agent-schemas.d.ts +0 -521
- package/dist/api/agent-schemas.js +0 -36
- package/dist/api/parse-fields.d.ts +0 -25
- package/dist/api/parse-fields.js +0 -86
- package/dist/api/read-input.d.ts +0 -6
- package/dist/api/read-input.js +0 -28
- package/dist/api/run-schemas.d.ts +0 -621
- package/dist/api/run-schemas.js +0 -91
- package/dist/bin/create-program.d.ts +0 -7
- package/dist/bin/create-program.js +0 -34
- package/dist/bin/duvo.d.ts +0 -2
- package/dist/client/client.d.ts +0 -23
- package/dist/client/client.js +0 -96
- package/dist/commands/agents/create.d.ts +0 -2
- package/dist/commands/agents/create.js +0 -182
- package/dist/commands/agents/get.d.ts +0 -2
- package/dist/commands/agents/get.js +0 -36
- package/dist/commands/agents/index.d.ts +0 -2
- package/dist/commands/agents/index.js +0 -11
- package/dist/commands/agents/list.d.ts +0 -2
- package/dist/commands/agents/list.js +0 -61
- package/dist/commands/api-render.d.ts +0 -8
- package/dist/commands/api-render.js +0 -72
- package/dist/commands/api.d.ts +0 -2
- package/dist/commands/api.js +0 -90
- package/dist/commands/login.d.ts +0 -2
- package/dist/commands/login.js +0 -80
- package/dist/commands/logout.d.ts +0 -2
- package/dist/commands/logout.js +0 -38
- package/dist/commands/profiles/index.d.ts +0 -2
- package/dist/commands/profiles/index.js +0 -13
- package/dist/commands/profiles/list.d.ts +0 -2
- package/dist/commands/profiles/list.js +0 -22
- package/dist/commands/profiles/remove.d.ts +0 -2
- package/dist/commands/profiles/remove.js +0 -30
- package/dist/commands/profiles/rename.d.ts +0 -2
- package/dist/commands/profiles/rename.js +0 -20
- package/dist/commands/profiles/render-list.d.ts +0 -12
- package/dist/commands/profiles/render-list.js +0 -37
- package/dist/commands/profiles/use.d.ts +0 -2
- package/dist/commands/profiles/use.js +0 -17
- package/dist/commands/runs/get.d.ts +0 -2
- package/dist/commands/runs/get.js +0 -59
- package/dist/commands/runs/index.d.ts +0 -2
- package/dist/commands/runs/index.js +0 -17
- package/dist/commands/runs/messages.d.ts +0 -2
- package/dist/commands/runs/messages.js +0 -80
- package/dist/commands/runs/respond.d.ts +0 -2
- package/dist/commands/runs/respond.js +0 -105
- package/dist/commands/runs/send-message.d.ts +0 -2
- package/dist/commands/runs/send-message.js +0 -41
- package/dist/commands/runs/start.d.ts +0 -2
- package/dist/commands/runs/start.js +0 -54
- package/dist/commands/runs/stop.d.ts +0 -2
- package/dist/commands/runs/stop.js +0 -41
- package/dist/commands/shared/confirm.d.ts +0 -19
- package/dist/commands/shared/confirm.js +0 -28
- package/dist/commands/shared/pagination.d.ts +0 -8
- package/dist/commands/shared/pagination.js +0 -24
- package/dist/commands/shared/profile-flag.d.ts +0 -8
- package/dist/commands/shared/profile-flag.js +0 -17
- package/dist/commands/shared/require-auth.d.ts +0 -12
- package/dist/commands/shared/require-auth.js +0 -18
- package/dist/commands/whoami.d.ts +0 -2
- package/dist/commands/whoami.js +0 -14
- package/dist/config/defaults.d.ts +0 -3
- package/dist/config/defaults.js +0 -4
- package/dist/config/env.d.ts +0 -17
- package/dist/config/env.js +0 -13
- package/dist/config/paths.d.ts +0 -2
- package/dist/config/paths.js +0 -14
- package/dist/config/profiles.d.ts +0 -24
- package/dist/config/profiles.js +0 -78
- package/dist/config/resolve.d.ts +0 -29
- package/dist/config/resolve.js +0 -46
- package/dist/config/store.d.ts +0 -4
- package/dist/config/store.js +0 -94
- package/dist/config/types.d.ts +0 -8
- package/dist/config/types.js +0 -1
- package/dist/errors.d.ts +0 -22
- package/dist/errors.js +0 -42
- package/dist/output/format.d.ts +0 -11
- package/dist/output/format.js +0 -26
- package/dist/output/green.d.ts +0 -5
- package/dist/output/green.js +0 -18
- package/dist/output/json.d.ts +0 -6
- package/dist/output/json.js +0 -8
- package/dist/output/table.d.ts +0 -16
- package/dist/output/table.js +0 -35
- package/dist/output/text.d.ts +0 -9
- package/dist/output/text.js +0 -13
package/dist/commands/login.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { isCancel, password, text } from "@clack/prompts";
|
|
2
|
-
import { DEFAULT_API_BASE_URL } from "../config/defaults.js";
|
|
3
|
-
import { addProfile, isValidProfileName } from "../config/profiles.js";
|
|
4
|
-
import { readConfig, writeConfig } from "../config/store.js";
|
|
5
|
-
import { AuthCliError, CancelCliError, CliError } from "../errors.js";
|
|
6
|
-
export function registerLogin(program) {
|
|
7
|
-
program
|
|
8
|
-
.command("login")
|
|
9
|
-
.description("Store an API key under a named profile")
|
|
10
|
-
.option("--name <profile>", "profile name to store this key under (prompted if omitted)")
|
|
11
|
-
.option("--api-key <key>", "API key (non-interactive; skips the masked prompt)")
|
|
12
|
-
.option("--base-url <url>", `API base URL for this profile (default: ${DEFAULT_API_BASE_URL})`)
|
|
13
|
-
.action(async (options) => {
|
|
14
|
-
await runLogin(options);
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
async function runLogin(options) {
|
|
18
|
-
const config = await readConfig();
|
|
19
|
-
const existingCount = Object.keys(config.profiles).length;
|
|
20
|
-
const name = await resolveProfileName(options.name, existingCount);
|
|
21
|
-
const apiKey = await resolveApiKey(options.apiKey);
|
|
22
|
-
const apiBaseUrl = options.baseUrl ?? DEFAULT_API_BASE_URL;
|
|
23
|
-
const next = addProfile(config, name, { apiKey, apiBaseUrl });
|
|
24
|
-
await writeConfig(next);
|
|
25
|
-
const isNew = !(name in config.profiles);
|
|
26
|
-
const verb = isNew ? "Saved" : "Updated";
|
|
27
|
-
console.log(`${verb} profile "${name}".`);
|
|
28
|
-
if (next.defaultProfile === name && config.defaultProfile !== name) {
|
|
29
|
-
console.log(`Set "${name}" as the default profile.`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
async function resolveProfileName(flag, existingCount) {
|
|
33
|
-
if (flag !== undefined) {
|
|
34
|
-
if (!isValidProfileName(flag)) {
|
|
35
|
-
throw new CliError(`Invalid profile name "${flag}". Use lowercase letters, digits, ` +
|
|
36
|
-
`".", "-" or "_", max 40 chars.`);
|
|
37
|
-
}
|
|
38
|
-
return flag;
|
|
39
|
-
}
|
|
40
|
-
const placeholder = existingCount === 0 ? "default" : "";
|
|
41
|
-
const answer = await text({
|
|
42
|
-
message: "Profile name",
|
|
43
|
-
placeholder: placeholder || "e.g. acme",
|
|
44
|
-
defaultValue: placeholder,
|
|
45
|
-
validate: (value) => {
|
|
46
|
-
if (!value || value.length === 0) {
|
|
47
|
-
return "Profile name is required.";
|
|
48
|
-
}
|
|
49
|
-
if (!isValidProfileName(value)) {
|
|
50
|
-
return 'Use lowercase letters, digits, ".", "-" or "_" only (max 40 chars).';
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
if (isCancel(answer)) {
|
|
56
|
-
throw new CancelCliError("Login cancelled.");
|
|
57
|
-
}
|
|
58
|
-
return answer;
|
|
59
|
-
}
|
|
60
|
-
async function resolveApiKey(flag) {
|
|
61
|
-
if (flag !== undefined) {
|
|
62
|
-
if (flag.length === 0) {
|
|
63
|
-
throw new AuthCliError("--api-key cannot be empty.");
|
|
64
|
-
}
|
|
65
|
-
return flag;
|
|
66
|
-
}
|
|
67
|
-
const answer = await password({
|
|
68
|
-
message: "API key",
|
|
69
|
-
validate: (value) => {
|
|
70
|
-
if (!value || value.length === 0) {
|
|
71
|
-
return "API key is required.";
|
|
72
|
-
}
|
|
73
|
-
return undefined;
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
if (isCancel(answer)) {
|
|
77
|
-
throw new CancelCliError("Login cancelled.");
|
|
78
|
-
}
|
|
79
|
-
return answer;
|
|
80
|
-
}
|
package/dist/commands/logout.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { removeProfile } from "../config/profiles.js";
|
|
2
|
-
import { readConfig, writeConfig } from "../config/store.js";
|
|
3
|
-
import { confirmDestructive } from "./shared/confirm.js";
|
|
4
|
-
export function registerLogout(program) {
|
|
5
|
-
program
|
|
6
|
-
.command("logout")
|
|
7
|
-
.description("Remove a stored profile (default: the default one)")
|
|
8
|
-
.option("--name <profile>", "profile to remove (defaults to the default profile)")
|
|
9
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
10
|
-
.action(async (options) => {
|
|
11
|
-
await runLogout(options);
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
async function runLogout(options) {
|
|
15
|
-
const config = await readConfig();
|
|
16
|
-
const target = options.name ?? config.defaultProfile;
|
|
17
|
-
if (!target) {
|
|
18
|
-
console.log("No profile to remove — you are not logged in.");
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
if (!(target in config.profiles)) {
|
|
22
|
-
console.log(`Profile "${target}" does not exist.`);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
await confirmDestructive({
|
|
26
|
-
message: `Remove profile "${target}"?`,
|
|
27
|
-
skip: options.yes ?? false,
|
|
28
|
-
});
|
|
29
|
-
const next = removeProfile(config, target);
|
|
30
|
-
await writeConfig(next);
|
|
31
|
-
console.log(`Removed profile "${target}".`);
|
|
32
|
-
if (next.defaultProfile && next.defaultProfile !== config.defaultProfile) {
|
|
33
|
-
console.log(`Default profile is now "${next.defaultProfile}".`);
|
|
34
|
-
}
|
|
35
|
-
else if (!next.defaultProfile) {
|
|
36
|
-
console.log("No profiles remaining.");
|
|
37
|
-
}
|
|
38
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { registerList } from "./list.js";
|
|
2
|
-
import { registerRemove } from "./remove.js";
|
|
3
|
-
import { registerRename } from "./rename.js";
|
|
4
|
-
import { registerUse } from "./use.js";
|
|
5
|
-
export function registerProfiles(program) {
|
|
6
|
-
const profiles = program
|
|
7
|
-
.command("profiles")
|
|
8
|
-
.description("Manage stored API-key profiles");
|
|
9
|
-
registerList(profiles);
|
|
10
|
-
registerUse(profiles);
|
|
11
|
-
registerRename(profiles);
|
|
12
|
-
registerRemove(profiles);
|
|
13
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { listProfiles } from "../../config/profiles.js";
|
|
2
|
-
import { readConfig } from "../../config/store.js";
|
|
3
|
-
import { renderProfileJson, renderProfileTable } from "./render-list.js";
|
|
4
|
-
export function registerList(profiles) {
|
|
5
|
-
profiles
|
|
6
|
-
.command("list", { isDefault: true })
|
|
7
|
-
.description("List stored profiles; mark the default one with ▶")
|
|
8
|
-
.option("--json", "output raw JSON")
|
|
9
|
-
.action(async (options) => {
|
|
10
|
-
const config = await readConfig();
|
|
11
|
-
const items = listProfiles(config);
|
|
12
|
-
if (options.json) {
|
|
13
|
-
console.log(renderProfileJson(items));
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
if (items.length === 0) {
|
|
17
|
-
console.log("No profiles stored. Run `duvo login` to add one.");
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
console.log(renderProfileTable(items));
|
|
21
|
-
});
|
|
22
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { removeProfile } from "../../config/profiles.js";
|
|
2
|
-
import { readConfig, writeConfig } from "../../config/store.js";
|
|
3
|
-
import { confirmDestructive } from "../shared/confirm.js";
|
|
4
|
-
export function registerRemove(profiles) {
|
|
5
|
-
profiles
|
|
6
|
-
.command("remove <name>")
|
|
7
|
-
.description("Remove a stored profile (alias of `duvo logout --name`)")
|
|
8
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
9
|
-
.action(async (name, options) => {
|
|
10
|
-
const config = await readConfig();
|
|
11
|
-
if (!(name in config.profiles)) {
|
|
12
|
-
console.log(`Profile "${name}" does not exist.`);
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
await confirmDestructive({
|
|
16
|
-
message: `Remove profile "${name}"?`,
|
|
17
|
-
skip: options.yes ?? false,
|
|
18
|
-
});
|
|
19
|
-
const next = removeProfile(config, name);
|
|
20
|
-
await writeConfig(next);
|
|
21
|
-
console.log(`Removed profile "${name}".`);
|
|
22
|
-
if (next.defaultProfile &&
|
|
23
|
-
next.defaultProfile !== config.defaultProfile) {
|
|
24
|
-
console.log(`Default profile is now "${next.defaultProfile}".`);
|
|
25
|
-
}
|
|
26
|
-
else if (!next.defaultProfile) {
|
|
27
|
-
console.log("No profiles remaining.");
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { renameProfile } from "../../config/profiles.js";
|
|
2
|
-
import { readConfig, writeConfig } from "../../config/store.js";
|
|
3
|
-
import { CliError, EXIT_CODE } from "../../errors.js";
|
|
4
|
-
export function registerRename(profiles) {
|
|
5
|
-
profiles
|
|
6
|
-
.command("rename <old> <new>")
|
|
7
|
-
.description("Rename a stored profile, preserving its credentials")
|
|
8
|
-
.action(async (oldName, newName) => {
|
|
9
|
-
const config = await readConfig();
|
|
10
|
-
try {
|
|
11
|
-
const next = renameProfile(config, oldName, newName);
|
|
12
|
-
await writeConfig(next);
|
|
13
|
-
console.log(`Renamed profile "${oldName}" to "${newName}".`);
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
-
throw new CliError(message, EXIT_CODE.GENERIC);
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { type ProfileSummary } from "../../config/profiles.js";
|
|
2
|
-
/**
|
|
3
|
-
* Format the profile list as an aligned text table. Header sits behind
|
|
4
|
-
* two spaces so it lines up with the single-char marker + space that
|
|
5
|
-
* prefixes each data row.
|
|
6
|
-
*/
|
|
7
|
-
export declare function renderProfileTable(items: readonly ProfileSummary[]): string;
|
|
8
|
-
/**
|
|
9
|
-
* JSON output mirrors the text view's masked keys — never emit raw
|
|
10
|
-
* credentials from a listing command.
|
|
11
|
-
*/
|
|
12
|
-
export declare function renderProfileJson(items: readonly ProfileSummary[]): string;
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { padRight, truncateApiKey } from "../../output/format.js";
|
|
2
|
-
const DEFAULT_MARKER = "▶";
|
|
3
|
-
const NON_DEFAULT_MARKER = " ";
|
|
4
|
-
function toRow(item) {
|
|
5
|
-
return {
|
|
6
|
-
marker: item.isDefault ? DEFAULT_MARKER : NON_DEFAULT_MARKER,
|
|
7
|
-
name: item.name,
|
|
8
|
-
apiKey: truncateApiKey(item.apiKey),
|
|
9
|
-
apiBaseUrl: item.apiBaseUrl,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Format the profile list as an aligned text table. Header sits behind
|
|
14
|
-
* two spaces so it lines up with the single-char marker + space that
|
|
15
|
-
* prefixes each data row.
|
|
16
|
-
*/
|
|
17
|
-
export function renderProfileTable(items) {
|
|
18
|
-
const rows = items.map(toRow);
|
|
19
|
-
const nameWidth = Math.max(4, ...rows.map((r) => r.name.length));
|
|
20
|
-
const keyWidth = Math.max(7, ...rows.map((r) => r.apiKey.length));
|
|
21
|
-
const lines = [];
|
|
22
|
-
lines.push(` ${padRight("NAME", nameWidth)} ${padRight("KEY", keyWidth)} BASE URL`);
|
|
23
|
-
for (const row of rows) {
|
|
24
|
-
lines.push(`${row.marker} ${padRight(row.name, nameWidth)} ${padRight(row.apiKey, keyWidth)} ${row.apiBaseUrl}`);
|
|
25
|
-
}
|
|
26
|
-
return lines.join("\n");
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* JSON output mirrors the text view's masked keys — never emit raw
|
|
30
|
-
* credentials from a listing command.
|
|
31
|
-
*/
|
|
32
|
-
export function renderProfileJson(items) {
|
|
33
|
-
const masked = items.map((item) => {
|
|
34
|
-
return { ...item, apiKey: truncateApiKey(item.apiKey) };
|
|
35
|
-
});
|
|
36
|
-
return JSON.stringify(masked, null, 2);
|
|
37
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { setDefaultProfile } from "../../config/profiles.js";
|
|
2
|
-
import { readConfig, writeConfig } from "../../config/store.js";
|
|
3
|
-
import { AuthCliError } from "../../errors.js";
|
|
4
|
-
export function registerUse(profiles) {
|
|
5
|
-
profiles
|
|
6
|
-
.command("use <name>")
|
|
7
|
-
.description("Switch the default profile")
|
|
8
|
-
.action(async (name) => {
|
|
9
|
-
const config = await readConfig();
|
|
10
|
-
if (!(name in config.profiles)) {
|
|
11
|
-
throw new AuthCliError(`Profile "${name}" does not exist. Run \`duvo profiles list\` to see available profiles.`);
|
|
12
|
-
}
|
|
13
|
-
const next = setDefaultProfile(config, name);
|
|
14
|
-
await writeConfig(next);
|
|
15
|
-
console.log(`Default profile is now "${name}".`);
|
|
16
|
-
});
|
|
17
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
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
|
-
}
|