@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
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
/**
|
|
3
|
-
* Build a fresh `Command` tree for the CLI. Shared between the real
|
|
4
|
-
* binary (`bin/duvo.ts`) and the test harness so tests can never
|
|
5
|
-
* silently drift out of sync with the production command set.
|
|
6
|
-
*/
|
|
7
|
-
export declare function createProgram(): Command;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { registerAgents } from "../commands/agents/index.js";
|
|
6
|
-
import { registerApi } from "../commands/api.js";
|
|
7
|
-
import { registerLogin } from "../commands/login.js";
|
|
8
|
-
import { registerLogout } from "../commands/logout.js";
|
|
9
|
-
import { registerProfiles } from "../commands/profiles/index.js";
|
|
10
|
-
import { registerRuns } from "../commands/runs/index.js";
|
|
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"));
|
|
14
|
-
/**
|
|
15
|
-
* Build a fresh `Command` tree for the CLI. Shared between the real
|
|
16
|
-
* binary (`bin/duvo.ts`) and the test harness so tests can never
|
|
17
|
-
* silently drift out of sync with the production command set.
|
|
18
|
-
*/
|
|
19
|
-
export function createProgram() {
|
|
20
|
-
const program = new Command();
|
|
21
|
-
program
|
|
22
|
-
.name("duvo")
|
|
23
|
-
.description("Command-line interface for the Duvo public API")
|
|
24
|
-
.version(version)
|
|
25
|
-
.option("--profile <name>", "use a specific profile for this invocation (overrides default profile)");
|
|
26
|
-
registerLogin(program);
|
|
27
|
-
registerLogout(program);
|
|
28
|
-
registerWhoami(program);
|
|
29
|
-
registerProfiles(program);
|
|
30
|
-
registerApi(program);
|
|
31
|
-
registerAgents(program);
|
|
32
|
-
registerRuns(program);
|
|
33
|
-
return program;
|
|
34
|
-
}
|
package/dist/bin/duvo.d.ts
DELETED
package/dist/client/client.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
2
|
-
export type HttpRequestOptions = {
|
|
3
|
-
method: string;
|
|
4
|
-
path: string;
|
|
5
|
-
apiKey: string;
|
|
6
|
-
apiBaseUrl: string;
|
|
7
|
-
/** JSON body. Omitted for GET/HEAD-style requests. */
|
|
8
|
-
body?: unknown;
|
|
9
|
-
/** Appended to the resolved URL. */
|
|
10
|
-
searchParams?: URLSearchParams;
|
|
11
|
-
/** Request timeout in ms. Defaults to {@link DEFAULT_REQUEST_TIMEOUT_MS}. */
|
|
12
|
-
timeoutMs?: number;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Makes an authenticated request to the Duvo public API and returns the
|
|
16
|
-
* parsed JSON response. Maps well-known status codes to `CliError` with
|
|
17
|
-
* the right exit code; other non-2xx responses throw a generic
|
|
18
|
-
* `CliError` containing the server's response body.
|
|
19
|
-
*
|
|
20
|
-
* Empty response bodies resolve to `null`. Non-JSON responses resolve to
|
|
21
|
-
* the raw text — the caller decides how to render it.
|
|
22
|
-
*/
|
|
23
|
-
export declare function httpRequest(options: HttpRequestOptions): Promise<unknown>;
|
package/dist/client/client.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { CliError, EXIT_CODE } from "../errors.js";
|
|
2
|
-
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
3
|
-
/**
|
|
4
|
-
* Makes an authenticated request to the Duvo public API and returns the
|
|
5
|
-
* parsed JSON response. Maps well-known status codes to `CliError` with
|
|
6
|
-
* the right exit code; other non-2xx responses throw a generic
|
|
7
|
-
* `CliError` containing the server's response body.
|
|
8
|
-
*
|
|
9
|
-
* Empty response bodies resolve to `null`. Non-JSON responses resolve to
|
|
10
|
-
* the raw text — the caller decides how to render it.
|
|
11
|
-
*/
|
|
12
|
-
export async function httpRequest(options) {
|
|
13
|
-
const url = buildUrl(options.apiBaseUrl, options.path, options.searchParams);
|
|
14
|
-
const headers = {
|
|
15
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
16
|
-
Accept: "application/json",
|
|
17
|
-
};
|
|
18
|
-
const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
19
|
-
const init = {
|
|
20
|
-
method: options.method,
|
|
21
|
-
headers,
|
|
22
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
23
|
-
};
|
|
24
|
-
if (options.body !== undefined) {
|
|
25
|
-
headers["Content-Type"] = "application/json";
|
|
26
|
-
init.body = JSON.stringify(options.body);
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const response = await fetch(url, init);
|
|
30
|
-
return await handleResponse(response, options.method, options.path);
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
if (isTimeoutError(error)) {
|
|
34
|
-
throw new CliError(`Request timed out after ${timeoutMs}ms: ${options.method} ${options.path}`);
|
|
35
|
-
}
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function isTimeoutError(error) {
|
|
40
|
-
return (error instanceof DOMException &&
|
|
41
|
-
(error.name === "TimeoutError" || error.name === "AbortError"));
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Resolves an API path against the configured base URL. The base URL must
|
|
45
|
-
* be an origin only (scheme + host + optional port) — any path component
|
|
46
|
-
* is discarded, because `new URL("/v1/agents", "https://host/foo/")`
|
|
47
|
-
* resolves to `https://host/v1/agents`, not `https://host/foo/v1/agents`.
|
|
48
|
-
*/
|
|
49
|
-
const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//;
|
|
50
|
-
function buildUrl(base, path, searchParams) {
|
|
51
|
-
if (ABSOLUTE_URL.test(path)) {
|
|
52
|
-
throw new CliError(`Invalid path "${path}". Expected an API path like "/v1/agents" — absolute URLs are not allowed.`);
|
|
53
|
-
}
|
|
54
|
-
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
55
|
-
const url = new URL(normalized, ensureTrailingSlash(base));
|
|
56
|
-
if (searchParams) {
|
|
57
|
-
for (const [key, value] of searchParams) {
|
|
58
|
-
url.searchParams.append(key, value);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return url;
|
|
62
|
-
}
|
|
63
|
-
function ensureTrailingSlash(base) {
|
|
64
|
-
return base.endsWith("/") ? base : `${base}/`;
|
|
65
|
-
}
|
|
66
|
-
async function handleResponse(response, method, path) {
|
|
67
|
-
if (response.status === 401) {
|
|
68
|
-
throw new CliError("Unauthorized. Check that the API key is valid.", EXIT_CODE.AUTH);
|
|
69
|
-
}
|
|
70
|
-
if (response.status === 404) {
|
|
71
|
-
throw new CliError(`Not found: ${method} ${path}`, EXIT_CODE.NOT_FOUND);
|
|
72
|
-
}
|
|
73
|
-
if (!response.ok) {
|
|
74
|
-
const detail = await safeReadText(response);
|
|
75
|
-
const suffix = detail.length > 0 ? `: ${detail}` : "";
|
|
76
|
-
throw new CliError(`Request failed (${response.status} ${response.statusText})${suffix}`);
|
|
77
|
-
}
|
|
78
|
-
const text = await safeReadText(response);
|
|
79
|
-
if (text.length === 0) {
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
try {
|
|
83
|
-
return JSON.parse(text);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
return text;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
async function safeReadText(response) {
|
|
90
|
-
try {
|
|
91
|
-
return await response.text();
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return "";
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,182 +0,0 @@
|
|
|
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,36 +0,0 @@
|
|
|
1
|
-
import { getAgentResponseSchema } from "../../api/agent-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 registerAgentsGet(agents) {
|
|
8
|
-
agents
|
|
9
|
-
.command("get <id>")
|
|
10
|
-
.description("Get an agent 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/agents/${encodeURIComponent(id)}`,
|
|
17
|
-
apiKey,
|
|
18
|
-
apiBaseUrl,
|
|
19
|
-
});
|
|
20
|
-
if (options.json) {
|
|
21
|
-
printJson(raw);
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const parsed = getAgentResponseSchema.safeParse(raw);
|
|
25
|
-
if (!parsed.success) {
|
|
26
|
-
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
27
|
-
}
|
|
28
|
-
const { agent } = parsed.data;
|
|
29
|
-
console.log(renderKeyValue([
|
|
30
|
-
{ label: "ID", value: agent.id },
|
|
31
|
-
{ label: "Name", value: agent.name },
|
|
32
|
-
{ label: "Created", value: agent.created_at ?? "" },
|
|
33
|
-
{ label: "Updated", value: agent.updated_at ?? "" },
|
|
34
|
-
]));
|
|
35
|
-
});
|
|
36
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { registerAgentsCreate } from "./create.js";
|
|
2
|
-
import { registerAgentsGet } from "./get.js";
|
|
3
|
-
import { registerAgentsList } from "./list.js";
|
|
4
|
-
export function registerAgents(program) {
|
|
5
|
-
const agents = program
|
|
6
|
-
.command("agents")
|
|
7
|
-
.description("Manage agents in the Duvo public API");
|
|
8
|
-
registerAgentsList(agents);
|
|
9
|
-
registerAgentsGet(agents);
|
|
10
|
-
registerAgentsCreate(agents);
|
|
11
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { listAgentsResponseSchema, } from "../../api/agent-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 = 100;
|
|
11
|
-
const TABLE_COLUMNS = [
|
|
12
|
-
{ header: "ID", cell: (row) => row.id },
|
|
13
|
-
{ header: "NAME", cell: (row) => row.name },
|
|
14
|
-
{ header: "CREATED", cell: (row) => row.created_at ?? "" },
|
|
15
|
-
];
|
|
16
|
-
export function registerAgentsList(agents) {
|
|
17
|
-
agents
|
|
18
|
-
.command("list", { isDefault: true })
|
|
19
|
-
.description("List agents for your team")
|
|
20
|
-
.option("--limit <n>", `page size (${MIN_LIMIT}-${MAX_LIMIT})`)
|
|
21
|
-
.option("--offset <n>", "number of agents to skip")
|
|
22
|
-
.option("--json", "output raw JSON from the API")
|
|
23
|
-
.action(async function listAction(options) {
|
|
24
|
-
const limit = parseLimit(options.limit, {
|
|
25
|
-
defaultValue: DEFAULT_LIMIT,
|
|
26
|
-
min: MIN_LIMIT,
|
|
27
|
-
max: MAX_LIMIT,
|
|
28
|
-
});
|
|
29
|
-
const offset = parseOffset(options.offset);
|
|
30
|
-
const { apiKey, apiBaseUrl } = await requireAuthenticatedConfig(this);
|
|
31
|
-
const searchParams = new URLSearchParams();
|
|
32
|
-
searchParams.append("limit", String(limit));
|
|
33
|
-
searchParams.append("offset", String(offset));
|
|
34
|
-
const raw = await httpRequest({
|
|
35
|
-
method: "GET",
|
|
36
|
-
path: "/v1/agents",
|
|
37
|
-
apiKey,
|
|
38
|
-
apiBaseUrl,
|
|
39
|
-
searchParams,
|
|
40
|
-
});
|
|
41
|
-
if (options.json) {
|
|
42
|
-
printJson(raw);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
const parsed = listAgentsResponseSchema.safeParse(raw);
|
|
46
|
-
if (!parsed.success) {
|
|
47
|
-
throw new CliError(`Unexpected response from the API: ${parsed.error.message}`);
|
|
48
|
-
}
|
|
49
|
-
const response = parsed.data;
|
|
50
|
-
if (response.agents.length === 0) {
|
|
51
|
-
console.log("No agents found.");
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
console.log(renderTable(TABLE_COLUMNS, response.agents));
|
|
55
|
-
const resolvedOffset = response.offset ?? offset;
|
|
56
|
-
const summary = response.total === null || response.total === undefined
|
|
57
|
-
? `Showing ${response.agents.length} · offset ${resolvedOffset}`
|
|
58
|
-
: `Showing ${response.agents.length} of ${response.total} · offset ${resolvedOffset}`;
|
|
59
|
-
console.log(summary);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Routes the response through the shared output pipeline per GUIDE.md:
|
|
3
|
-
* collections render as a table, single objects as a key/value block,
|
|
4
|
-
* everything else (and `--json`) falls back to pretty JSON. `duvo api`
|
|
5
|
-
* has no schema, so columns and labels come straight from the response
|
|
6
|
-
* keys.
|
|
7
|
-
*/
|
|
8
|
-
export declare function renderApiResponse(response: unknown, asJson: boolean): void;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { printJson } from "../output/json.js";
|
|
2
|
-
import { renderTable } from "../output/table.js";
|
|
3
|
-
import { renderKeyValue } from "../output/text.js";
|
|
4
|
-
const FALLBACK_TERMINAL_WIDTH = 120;
|
|
5
|
-
const MIN_COLUMN_WIDTH = 8;
|
|
6
|
-
const COLUMN_GUTTER = 2;
|
|
7
|
-
/**
|
|
8
|
-
* Routes the response through the shared output pipeline per GUIDE.md:
|
|
9
|
-
* collections render as a table, single objects as a key/value block,
|
|
10
|
-
* everything else (and `--json`) falls back to pretty JSON. `duvo api`
|
|
11
|
-
* has no schema, so columns and labels come straight from the response
|
|
12
|
-
* keys.
|
|
13
|
-
*/
|
|
14
|
-
export function renderApiResponse(response, asJson) {
|
|
15
|
-
if (asJson) {
|
|
16
|
-
printJson(response);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (Array.isArray(response)) {
|
|
20
|
-
if (response.length === 0) {
|
|
21
|
-
console.log("No results.");
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (response.every(isRecord)) {
|
|
25
|
-
renderTableResponse(response);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
if (isRecord(response)) {
|
|
30
|
-
const entries = Object.entries(response).map(([key, value]) => {
|
|
31
|
-
return { label: key, value: formatScalar(value) };
|
|
32
|
-
});
|
|
33
|
-
console.log(renderKeyValue(entries));
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
printJson(response);
|
|
37
|
-
}
|
|
38
|
-
function renderTableResponse(rows) {
|
|
39
|
-
const keys = Array.from(new Set(rows.flatMap((row) => Object.keys(row))));
|
|
40
|
-
const maxWidth = perColumnMaxWidth(keys.length);
|
|
41
|
-
const columns = keys.map((key) => {
|
|
42
|
-
return {
|
|
43
|
-
header: key.toUpperCase(),
|
|
44
|
-
cell: (row) => formatScalar(row[key]),
|
|
45
|
-
maxWidth,
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
console.log(renderTable(columns, rows));
|
|
49
|
-
}
|
|
50
|
-
function isRecord(value) {
|
|
51
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
-
}
|
|
53
|
-
function perColumnMaxWidth(columnCount) {
|
|
54
|
-
if (columnCount === 0) {
|
|
55
|
-
return MIN_COLUMN_WIDTH;
|
|
56
|
-
}
|
|
57
|
-
const terminalWidth = process.stdout.columns ?? FALLBACK_TERMINAL_WIDTH;
|
|
58
|
-
const budget = terminalWidth - COLUMN_GUTTER * (columnCount - 1);
|
|
59
|
-
return Math.max(MIN_COLUMN_WIDTH, Math.floor(budget / columnCount));
|
|
60
|
-
}
|
|
61
|
-
function formatScalar(value) {
|
|
62
|
-
if (value === null || value === undefined) {
|
|
63
|
-
return "";
|
|
64
|
-
}
|
|
65
|
-
if (typeof value === "string") {
|
|
66
|
-
return value;
|
|
67
|
-
}
|
|
68
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
69
|
-
return String(value);
|
|
70
|
-
}
|
|
71
|
-
return JSON.stringify(value);
|
|
72
|
-
}
|
package/dist/commands/api.d.ts
DELETED
package/dist/commands/api.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { buildFieldObject } from "../api/parse-fields.js";
|
|
2
|
-
import { readInputJson } from "../api/read-input.js";
|
|
3
|
-
import { httpRequest } from "../client/client.js";
|
|
4
|
-
import { CliError } from "../errors.js";
|
|
5
|
-
import { renderApiResponse } from "./api-render.js";
|
|
6
|
-
import { requireAuthenticatedConfig, } from "./shared/require-auth.js";
|
|
7
|
-
const METHODS_WITH_BODY = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
8
|
-
const VALID_METHODS = new Set([
|
|
9
|
-
"GET",
|
|
10
|
-
"HEAD",
|
|
11
|
-
"POST",
|
|
12
|
-
"PUT",
|
|
13
|
-
"PATCH",
|
|
14
|
-
"DELETE",
|
|
15
|
-
]);
|
|
16
|
-
export function registerApi(program) {
|
|
17
|
-
program
|
|
18
|
-
.command("api <method> <path>")
|
|
19
|
-
.description("Make an authenticated request to the Duvo public API")
|
|
20
|
-
.option("-f, --field <key=value>", "string field; repeat for multiple (body on POST/PUT/PATCH/DELETE, query on GET/HEAD)", collect, [])
|
|
21
|
-
.option("-F, --raw-field <key=value>", "typed field (boolean, number, null, or @file for JSON); repeat for multiple", collect, [])
|
|
22
|
-
.option("--input <path>", 'raw JSON body from a file, or "-" for stdin (mutually exclusive with -f / -F)')
|
|
23
|
-
.option("--json", "output raw JSON instead of the formatted view")
|
|
24
|
-
.action(async function apiAction(method, path, options) {
|
|
25
|
-
const resolved = await requireAuthenticatedConfig(this);
|
|
26
|
-
await runApi({ method, path, options }, resolved);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
async function runApi(args, resolved) {
|
|
30
|
-
const method = args.method.toUpperCase();
|
|
31
|
-
if (!VALID_METHODS.has(method)) {
|
|
32
|
-
throw new CliError(`Invalid HTTP method "${args.method}". Expected one of: ${[...VALID_METHODS].join(", ")}.`);
|
|
33
|
-
}
|
|
34
|
-
const { body, searchParams } = prepareRequestData(method, args.options);
|
|
35
|
-
const response = await httpRequest({
|
|
36
|
-
method,
|
|
37
|
-
path: args.path,
|
|
38
|
-
apiKey: resolved.apiKey,
|
|
39
|
-
apiBaseUrl: resolved.apiBaseUrl,
|
|
40
|
-
body,
|
|
41
|
-
searchParams,
|
|
42
|
-
});
|
|
43
|
-
renderApiResponse(response, args.options.json ?? false);
|
|
44
|
-
}
|
|
45
|
-
function prepareRequestData(method, options) {
|
|
46
|
-
const hasFieldArgs = (options.field?.length ?? 0) > 0 || (options.rawField?.length ?? 0) > 0;
|
|
47
|
-
if (options.input !== undefined && hasFieldArgs) {
|
|
48
|
-
throw new CliError("Cannot combine --input with -f / -F flags.");
|
|
49
|
-
}
|
|
50
|
-
if (options.input !== undefined) {
|
|
51
|
-
if (!METHODS_WITH_BODY.has(method)) {
|
|
52
|
-
throw new CliError(`--input is only supported for ${[...METHODS_WITH_BODY].join(", ")} requests.`);
|
|
53
|
-
}
|
|
54
|
-
return { body: readInputJson(options.input), searchParams: undefined };
|
|
55
|
-
}
|
|
56
|
-
if (!hasFieldArgs) {
|
|
57
|
-
return { body: undefined, searchParams: undefined };
|
|
58
|
-
}
|
|
59
|
-
const fields = buildFieldObject(options.field, options.rawField);
|
|
60
|
-
if (METHODS_WITH_BODY.has(method)) {
|
|
61
|
-
return { body: fields, searchParams: undefined };
|
|
62
|
-
}
|
|
63
|
-
return { body: undefined, searchParams: fieldsToSearchParams(fields) };
|
|
64
|
-
}
|
|
65
|
-
function collect(value, previous) {
|
|
66
|
-
return [...previous, value];
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Encodes typed fields as query string values. Scalars pass through as
|
|
70
|
-
* their string form; objects and arrays are JSON-stringified so an
|
|
71
|
-
* `-F filter=@file.json` round-trips to a usable `?filter=…` rather
|
|
72
|
-
* than `[object Object]`. `null` / `undefined` keys are dropped — query
|
|
73
|
-
* strings have no native null encoding.
|
|
74
|
-
*/
|
|
75
|
-
function fieldsToSearchParams(fields) {
|
|
76
|
-
const params = new URLSearchParams();
|
|
77
|
-
for (const [key, value] of Object.entries(fields)) {
|
|
78
|
-
if (value === null || value === undefined) {
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (typeof value === "string" ||
|
|
82
|
-
typeof value === "number" ||
|
|
83
|
-
typeof value === "boolean") {
|
|
84
|
-
params.append(key, String(value));
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
params.append(key, JSON.stringify(value));
|
|
88
|
-
}
|
|
89
|
-
return params;
|
|
90
|
-
}
|
package/dist/commands/login.d.ts
DELETED