@duvoai/cli 0.0.1

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.
Files changed (71) hide show
  1. package/dist/api/agent-schemas.d.ts +94 -0
  2. package/dist/api/agent-schemas.js +16 -0
  3. package/dist/api/parse-fields.d.ts +25 -0
  4. package/dist/api/parse-fields.js +86 -0
  5. package/dist/api/read-input.d.ts +6 -0
  6. package/dist/api/read-input.js +28 -0
  7. package/dist/bin/create-program.d.ts +7 -0
  8. package/dist/bin/create-program.js +27 -0
  9. package/dist/bin/duvo.d.ts +2 -0
  10. package/dist/bin/duvo.js +8 -0
  11. package/dist/client/client.d.ts +23 -0
  12. package/dist/client/client.js +96 -0
  13. package/dist/commands/agents/get.d.ts +2 -0
  14. package/dist/commands/agents/get.js +42 -0
  15. package/dist/commands/agents/index.d.ts +2 -0
  16. package/dist/commands/agents/index.js +9 -0
  17. package/dist/commands/agents/list.d.ts +2 -0
  18. package/dist/commands/agents/list.js +67 -0
  19. package/dist/commands/api-render.d.ts +8 -0
  20. package/dist/commands/api-render.js +72 -0
  21. package/dist/commands/api.d.ts +2 -0
  22. package/dist/commands/api.js +98 -0
  23. package/dist/commands/login.d.ts +2 -0
  24. package/dist/commands/login.js +80 -0
  25. package/dist/commands/logout.d.ts +2 -0
  26. package/dist/commands/logout.js +38 -0
  27. package/dist/commands/profiles/index.d.ts +2 -0
  28. package/dist/commands/profiles/index.js +13 -0
  29. package/dist/commands/profiles/list.d.ts +2 -0
  30. package/dist/commands/profiles/list.js +22 -0
  31. package/dist/commands/profiles/remove.d.ts +2 -0
  32. package/dist/commands/profiles/remove.js +30 -0
  33. package/dist/commands/profiles/rename.d.ts +2 -0
  34. package/dist/commands/profiles/rename.js +20 -0
  35. package/dist/commands/profiles/render-list.d.ts +12 -0
  36. package/dist/commands/profiles/render-list.js +37 -0
  37. package/dist/commands/profiles/use.d.ts +2 -0
  38. package/dist/commands/profiles/use.js +17 -0
  39. package/dist/commands/shared/confirm.d.ts +19 -0
  40. package/dist/commands/shared/confirm.js +28 -0
  41. package/dist/commands/shared/pagination.d.ts +8 -0
  42. package/dist/commands/shared/pagination.js +24 -0
  43. package/dist/commands/shared/profile-flag.d.ts +8 -0
  44. package/dist/commands/shared/profile-flag.js +17 -0
  45. package/dist/commands/whoami.d.ts +2 -0
  46. package/dist/commands/whoami.js +21 -0
  47. package/dist/config/defaults.d.ts +3 -0
  48. package/dist/config/defaults.js +4 -0
  49. package/dist/config/env.d.ts +17 -0
  50. package/dist/config/env.js +13 -0
  51. package/dist/config/paths.d.ts +2 -0
  52. package/dist/config/paths.js +14 -0
  53. package/dist/config/profiles.d.ts +24 -0
  54. package/dist/config/profiles.js +78 -0
  55. package/dist/config/resolve.d.ts +29 -0
  56. package/dist/config/resolve.js +46 -0
  57. package/dist/config/store.d.ts +4 -0
  58. package/dist/config/store.js +94 -0
  59. package/dist/config/types.d.ts +8 -0
  60. package/dist/config/types.js +1 -0
  61. package/dist/errors.d.ts +22 -0
  62. package/dist/errors.js +42 -0
  63. package/dist/output/format.d.ts +11 -0
  64. package/dist/output/format.js +26 -0
  65. package/dist/output/json.d.ts +6 -0
  66. package/dist/output/json.js +8 -0
  67. package/dist/output/table.d.ts +16 -0
  68. package/dist/output/table.js +35 -0
  69. package/dist/output/text.d.ts +9 -0
  70. package/dist/output/text.js +13 -0
  71. package/package.json +36 -0
@@ -0,0 +1,78 @@
1
+ const PROFILE_NAME_PATTERN = /^[a-z0-9._-]+$/;
2
+ const MAX_PROFILE_NAME_LENGTH = 40;
3
+ export function isValidProfileName(name) {
4
+ if (name.length === 0 || name.length > MAX_PROFILE_NAME_LENGTH) {
5
+ return false;
6
+ }
7
+ return PROFILE_NAME_PATTERN.test(name);
8
+ }
9
+ export function assertValidProfileName(name) {
10
+ if (!isValidProfileName(name)) {
11
+ throw new Error(`Invalid profile name "${name}". Use lowercase letters, digits, ` +
12
+ `".", "-" or "_", max ${MAX_PROFILE_NAME_LENGTH} chars.`);
13
+ }
14
+ }
15
+ /**
16
+ * Add (or replace) a profile. The first profile added becomes the
17
+ * default automatically. Returns the updated config without mutating
18
+ * the input.
19
+ */
20
+ export function addProfile(config, name, profile) {
21
+ assertValidProfileName(name);
22
+ const profiles = { ...config.profiles, [name]: profile };
23
+ const defaultProfile = config.defaultProfile === null ? name : config.defaultProfile;
24
+ return { defaultProfile, profiles };
25
+ }
26
+ /**
27
+ * Remove a profile. If the removed profile was the default, picks
28
+ * another profile alphabetically as the new default, or clears it if
29
+ * none remain.
30
+ */
31
+ export function removeProfile(config, name) {
32
+ if (!(name in config.profiles)) {
33
+ return config;
34
+ }
35
+ const profiles = { ...config.profiles };
36
+ delete profiles[name];
37
+ if (config.defaultProfile !== name) {
38
+ return { defaultProfile: config.defaultProfile, profiles };
39
+ }
40
+ const remaining = Object.keys(profiles).sort();
41
+ const defaultProfile = remaining.length > 0 ? remaining[0] : null;
42
+ return { defaultProfile, profiles };
43
+ }
44
+ export function renameProfile(config, oldName, newName) {
45
+ if (!(oldName in config.profiles)) {
46
+ throw new Error(`Profile "${oldName}" does not exist.`);
47
+ }
48
+ if (oldName === newName) {
49
+ return config;
50
+ }
51
+ assertValidProfileName(newName);
52
+ if (newName in config.profiles) {
53
+ throw new Error(`Profile "${newName}" already exists.`);
54
+ }
55
+ const profiles = { ...config.profiles };
56
+ profiles[newName] = profiles[oldName];
57
+ delete profiles[oldName];
58
+ const defaultProfile = config.defaultProfile === oldName ? newName : config.defaultProfile;
59
+ return { defaultProfile, profiles };
60
+ }
61
+ export function setDefaultProfile(config, name) {
62
+ if (!(name in config.profiles)) {
63
+ throw new Error(`Profile "${name}" does not exist.`);
64
+ }
65
+ return { ...config, defaultProfile: name };
66
+ }
67
+ export function listProfiles(config) {
68
+ return Object.entries(config.profiles)
69
+ .map(([name, profile]) => {
70
+ return {
71
+ name,
72
+ apiKey: profile.apiKey,
73
+ apiBaseUrl: profile.apiBaseUrl,
74
+ isDefault: config.defaultProfile === name,
75
+ };
76
+ })
77
+ .sort((a, b) => a.name.localeCompare(b.name));
78
+ }
@@ -0,0 +1,29 @@
1
+ import { type CliEnv } from "../config/env.js";
2
+ import { type Config } from "../config/types.js";
3
+ import { AuthCliError } from "../errors.js";
4
+ export type ResolvedConfig = {
5
+ apiKey: string | null;
6
+ apiBaseUrl: string;
7
+ /** Name of the profile we resolved to, or null if we fell through to env-only. */
8
+ profileName: string | null;
9
+ };
10
+ export type ResolveOptions = {
11
+ /** From the --profile CLI flag. Takes precedence over env and stored default. */
12
+ profileFlag?: string | undefined;
13
+ };
14
+ /**
15
+ * Selection precedence:
16
+ * profile: --profile flag > DUVO_PROFILE env > config.defaultProfile
17
+ * apiKey: DUVO_API_KEY env > profile.apiKey
18
+ * baseUrl: DUVO_API_BASE_URL env > profile.apiBaseUrl > default
19
+ *
20
+ * If --profile is given but unknown, this throws (caller maps to exit 2).
21
+ * DUVO_PROFILE being unset/unknown falls through silently so users can use
22
+ * env overrides without breaking other shells.
23
+ */
24
+ export declare function resolveConfig(options?: ResolveOptions): Promise<ResolvedConfig>;
25
+ export declare function selectEffectiveConfig(config: Config, options: ResolveOptions, env: CliEnv): ResolvedConfig;
26
+ export declare class UnknownProfileError extends AuthCliError {
27
+ readonly profileName: string;
28
+ constructor(profileName: string);
29
+ }
@@ -0,0 +1,46 @@
1
+ import { DEFAULT_API_BASE_URL } from "../config/defaults.js";
2
+ import { parseCliEnv } from "../config/env.js";
3
+ import { readConfig } from "../config/store.js";
4
+ import { AuthCliError } from "../errors.js";
5
+ /**
6
+ * Selection precedence:
7
+ * profile: --profile flag > DUVO_PROFILE env > config.defaultProfile
8
+ * apiKey: DUVO_API_KEY env > profile.apiKey
9
+ * baseUrl: DUVO_API_BASE_URL env > profile.apiBaseUrl > default
10
+ *
11
+ * If --profile is given but unknown, this throws (caller maps to exit 2).
12
+ * DUVO_PROFILE being unset/unknown falls through silently so users can use
13
+ * env overrides without breaking other shells.
14
+ */
15
+ export async function resolveConfig(options = {}) {
16
+ const config = await readConfig();
17
+ return selectEffectiveConfig(config, options, parseCliEnv(process.env));
18
+ }
19
+ export function selectEffectiveConfig(config, options, env) {
20
+ const profileName = selectProfileName(config, options, env);
21
+ const profile = profileName ? (config.profiles[profileName] ?? null) : null;
22
+ const apiKey = env.DUVO_API_KEY ?? profile?.apiKey ?? null;
23
+ const apiBaseUrl = env.DUVO_API_BASE_URL ?? profile?.apiBaseUrl ?? DEFAULT_API_BASE_URL;
24
+ return { apiKey, apiBaseUrl, profileName };
25
+ }
26
+ function selectProfileName(config, options, env) {
27
+ const flag = options.profileFlag;
28
+ if (flag && flag.length > 0) {
29
+ if (!(flag in config.profiles)) {
30
+ throw new UnknownProfileError(flag);
31
+ }
32
+ return flag;
33
+ }
34
+ if (env.DUVO_PROFILE && env.DUVO_PROFILE in config.profiles) {
35
+ return env.DUVO_PROFILE;
36
+ }
37
+ return config.defaultProfile;
38
+ }
39
+ export class UnknownProfileError extends AuthCliError {
40
+ profileName;
41
+ constructor(profileName) {
42
+ super(`Profile "${profileName}" does not exist. Run \`duvo profiles list\` to see available profiles.`);
43
+ this.profileName = profileName;
44
+ this.name = "UnknownProfileError";
45
+ }
46
+ }
@@ -0,0 +1,4 @@
1
+ import { type Config } from "../config/types.js";
2
+ export declare function readConfig(): Promise<Config>;
3
+ export declare function writeConfig(config: Config): Promise<void>;
4
+ export declare function deleteConfig(): Promise<boolean>;
@@ -0,0 +1,94 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { DEFAULT_API_BASE_URL, emptyConfig } from "../config/defaults.js";
4
+ import { getConfigPath } from "../config/paths.js";
5
+ const FILE_MODE = 0o600;
6
+ const DIR_MODE = 0o700;
7
+ export async function readConfig() {
8
+ const path = getConfigPath();
9
+ try {
10
+ const raw = await readFile(path, "utf8");
11
+ const parsed = JSON.parse(raw);
12
+ return normalizeConfig(parsed);
13
+ }
14
+ catch (error) {
15
+ if (isNotFound(error)) {
16
+ return emptyConfig();
17
+ }
18
+ throw error;
19
+ }
20
+ }
21
+ export async function writeConfig(config) {
22
+ const path = getConfigPath();
23
+ await mkdir(dirname(path), { recursive: true, mode: DIR_MODE });
24
+ await writeFile(path, JSON.stringify(config, null, 2), {
25
+ mode: FILE_MODE,
26
+ encoding: "utf8",
27
+ });
28
+ }
29
+ export async function deleteConfig() {
30
+ const path = getConfigPath();
31
+ try {
32
+ await rm(path);
33
+ return true;
34
+ }
35
+ catch (error) {
36
+ if (isNotFound(error)) {
37
+ return false;
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+ /**
43
+ * Accepts the legacy single-profile shape ({ apiKey, apiBaseUrl }) and
44
+ * migrates it into the named-profile shape under the "default" name. Any
45
+ * fields we don't recognize are dropped.
46
+ */
47
+ function normalizeConfig(parsed) {
48
+ if (parsed === null || typeof parsed !== "object") {
49
+ return emptyConfig();
50
+ }
51
+ const record = parsed;
52
+ // Legacy single-profile migration.
53
+ if (typeof record.apiKey === "string" &&
54
+ record.profiles === undefined &&
55
+ record.defaultProfile === undefined) {
56
+ const apiBaseUrl = typeof record.apiBaseUrl === "string"
57
+ ? record.apiBaseUrl
58
+ : DEFAULT_API_BASE_URL;
59
+ return {
60
+ defaultProfile: "default",
61
+ profiles: {
62
+ default: { apiKey: record.apiKey, apiBaseUrl },
63
+ },
64
+ };
65
+ }
66
+ const profiles = {};
67
+ const rawProfiles = record.profiles && typeof record.profiles === "object"
68
+ ? record.profiles
69
+ : {};
70
+ for (const [name, value] of Object.entries(rawProfiles)) {
71
+ if (value === null || typeof value !== "object") {
72
+ continue;
73
+ }
74
+ const profile = value;
75
+ if (typeof profile.apiKey !== "string") {
76
+ continue;
77
+ }
78
+ const apiBaseUrl = typeof profile.apiBaseUrl === "string"
79
+ ? profile.apiBaseUrl
80
+ : DEFAULT_API_BASE_URL;
81
+ profiles[name] = { apiKey: profile.apiKey, apiBaseUrl };
82
+ }
83
+ const defaultProfile = typeof record.defaultProfile === "string" &&
84
+ record.defaultProfile in profiles
85
+ ? record.defaultProfile
86
+ : null;
87
+ return { defaultProfile, profiles };
88
+ }
89
+ function isNotFound(error) {
90
+ return (typeof error === "object" &&
91
+ error !== null &&
92
+ "code" in error &&
93
+ error.code === "ENOENT");
94
+ }
@@ -0,0 +1,8 @@
1
+ export type Profile = {
2
+ apiKey: string;
3
+ apiBaseUrl: string;
4
+ };
5
+ export type Config = {
6
+ defaultProfile: string | null;
7
+ profiles: Record<string, Profile>;
8
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ export declare const EXIT_CODE: {
2
+ readonly SUCCESS: 0;
3
+ readonly GENERIC: 1;
4
+ readonly AUTH: 2;
5
+ readonly NOT_FOUND: 3;
6
+ };
7
+ export type ExitCode = (typeof EXIT_CODE)[keyof typeof EXIT_CODE];
8
+ export declare class CliError extends Error {
9
+ readonly exitCode: ExitCode;
10
+ constructor(message: string, exitCode?: ExitCode);
11
+ }
12
+ export declare class AuthCliError extends CliError {
13
+ constructor(message: string);
14
+ }
15
+ export declare class NotFoundCliError extends CliError {
16
+ constructor(message: string);
17
+ }
18
+ /** Signals a cooperative user-driven cancel (Ctrl-C, answering "no"). */
19
+ export declare class CancelCliError extends CliError {
20
+ constructor(message?: string);
21
+ }
22
+ export declare function handleCliError(error: unknown): never;
package/dist/errors.js ADDED
@@ -0,0 +1,42 @@
1
+ export const EXIT_CODE = {
2
+ SUCCESS: 0,
3
+ GENERIC: 1,
4
+ AUTH: 2,
5
+ NOT_FOUND: 3,
6
+ };
7
+ export class CliError extends Error {
8
+ exitCode;
9
+ constructor(message, exitCode = EXIT_CODE.GENERIC) {
10
+ super(message);
11
+ this.name = "CliError";
12
+ this.exitCode = exitCode;
13
+ }
14
+ }
15
+ export class AuthCliError extends CliError {
16
+ constructor(message) {
17
+ super(message, EXIT_CODE.AUTH);
18
+ this.name = "AuthCliError";
19
+ }
20
+ }
21
+ export class NotFoundCliError extends CliError {
22
+ constructor(message) {
23
+ super(message, EXIT_CODE.NOT_FOUND);
24
+ this.name = "NotFoundCliError";
25
+ }
26
+ }
27
+ /** Signals a cooperative user-driven cancel (Ctrl-C, answering "no"). */
28
+ export class CancelCliError extends CliError {
29
+ constructor(message = "Cancelled.") {
30
+ super(message, EXIT_CODE.GENERIC);
31
+ this.name = "CancelCliError";
32
+ }
33
+ }
34
+ export function handleCliError(error) {
35
+ if (error instanceof CliError) {
36
+ process.stderr.write(`${error.message}\n`);
37
+ process.exit(error.exitCode);
38
+ }
39
+ const message = error instanceof Error ? error.message : String(error);
40
+ process.stderr.write(`error: ${message}\n`);
41
+ process.exit(EXIT_CODE.GENERIC);
42
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Mask the secret portion of an API key, leaving the non-secret prefix
3
+ * visible so users can distinguish keys in listings.
4
+ *
5
+ * Keys have the shape `dv_<prefix>-<secret>`. Everything up to and
6
+ * including the first `-` is the public id; everything after is masked.
7
+ * Keys without a dash (shouldn't happen in practice) fall back to a
8
+ * fixed-length prefix so we never leak the tail regardless of format.
9
+ */
10
+ export declare function truncateApiKey(key: string): string;
11
+ export declare function padRight(value: string, width: number): string;
@@ -0,0 +1,26 @@
1
+ const FALLBACK_PREFIX_LEN = 11;
2
+ /**
3
+ * Mask the secret portion of an API key, leaving the non-secret prefix
4
+ * visible so users can distinguish keys in listings.
5
+ *
6
+ * Keys have the shape `dv_<prefix>-<secret>`. Everything up to and
7
+ * including the first `-` is the public id; everything after is masked.
8
+ * Keys without a dash (shouldn't happen in practice) fall back to a
9
+ * fixed-length prefix so we never leak the tail regardless of format.
10
+ */
11
+ export function truncateApiKey(key) {
12
+ const dashIdx = key.indexOf("-");
13
+ const visibleLen = dashIdx >= 0 ? dashIdx + 1 : FALLBACK_PREFIX_LEN;
14
+ if (key.length <= visibleLen) {
15
+ return key;
16
+ }
17
+ const visible = key.slice(0, visibleLen);
18
+ const masked = "*".repeat(key.length - visibleLen);
19
+ return `${visible}${masked}`;
20
+ }
21
+ export function padRight(value, width) {
22
+ if (value.length >= width) {
23
+ return value;
24
+ }
25
+ return value + " ".repeat(width - value.length);
26
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Prints a value as pretty JSON (2-space indent) to stdout. This is the
3
+ * stable output contract — scripts should always be able to `| jq` the
4
+ * result. No colour, no truncation.
5
+ */
6
+ export declare function printJson(data: unknown): void;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Prints a value as pretty JSON (2-space indent) to stdout. This is the
3
+ * stable output contract — scripts should always be able to `| jq` the
4
+ * result. No colour, no truncation.
5
+ */
6
+ export function printJson(data) {
7
+ console.log(JSON.stringify(data, null, 2));
8
+ }
@@ -0,0 +1,16 @@
1
+ export type Column<T> = {
2
+ header: string;
3
+ cell: (row: T) => string;
4
+ /**
5
+ * Optional cap on the rendered width of the column. Cells wider than
6
+ * this are truncated with a trailing `…` to keep each row on one line.
7
+ * The header is truncated too so the column never exceeds the cap.
8
+ */
9
+ maxWidth?: number;
10
+ };
11
+ /**
12
+ * Renders a fixed-width ASCII table. Columns auto-size to the widest
13
+ * cell (including the header) but never exceed `maxWidth` when set.
14
+ * No colour — non-TTY safe by default.
15
+ */
16
+ export declare function renderTable<T>(columns: readonly Column<T>[], rows: readonly T[]): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Renders a fixed-width ASCII table. Columns auto-size to the widest
3
+ * cell (including the header) but never exceed `maxWidth` when set.
4
+ * No colour — non-TTY safe by default.
5
+ */
6
+ export function renderTable(columns, rows) {
7
+ if (columns.length === 0) {
8
+ return "";
9
+ }
10
+ const cells = rows.map((row) => columns.map((col) => col.cell(row)));
11
+ const widths = columns.map((col, i) => {
12
+ const contentWidth = Math.max(col.header.length, ...cells.map((row) => row[i].length));
13
+ return col.maxWidth ? Math.min(col.maxWidth, contentWidth) : contentWidth;
14
+ });
15
+ const headerLine = columns
16
+ .map((col, i) => pad(truncate(col.header, widths[i]), widths[i]))
17
+ .join(" ");
18
+ const bodyLines = cells.map((row) => row.map((cell, i) => pad(truncate(cell, widths[i]), widths[i])).join(" "));
19
+ return [headerLine, ...bodyLines].join("\n");
20
+ }
21
+ function pad(value, width) {
22
+ if (value.length >= width) {
23
+ return value;
24
+ }
25
+ return value + " ".repeat(width - value.length);
26
+ }
27
+ function truncate(value, width) {
28
+ if (value.length <= width) {
29
+ return value;
30
+ }
31
+ if (width <= 1) {
32
+ return "…".slice(0, width);
33
+ }
34
+ return `${value.slice(0, width - 1)}…`;
35
+ }
@@ -0,0 +1,9 @@
1
+ export type KeyValueEntry = {
2
+ label: string;
3
+ value: string;
4
+ };
5
+ /**
6
+ * Renders a labelled key/value block for single-resource views.
7
+ * Labels are left-padded to the widest label so values line up.
8
+ */
9
+ export declare function renderKeyValue(entries: readonly KeyValueEntry[]): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Renders a labelled key/value block for single-resource views.
3
+ * Labels are left-padded to the widest label so values line up.
4
+ */
5
+ export function renderKeyValue(entries) {
6
+ if (entries.length === 0) {
7
+ return "";
8
+ }
9
+ const labelWidth = Math.max(...entries.map((e) => e.label.length));
10
+ return entries
11
+ .map((entry) => `${entry.label.padEnd(labelWidth)} ${entry.value}`)
12
+ .join("\n");
13
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@duvoai/cli",
3
+ "version": "0.0.1",
4
+ "description": "Command-line interface for the Duvo public API",
5
+ "type": "module",
6
+ "main": "dist/bin/duvo.js",
7
+ "bin": {
8
+ "duvo": "./dist/bin/duvo.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json && node scripts/make-bin-executable.mjs",
15
+ "dev": "tsx src/bin/duvo.ts",
16
+ "lint": "eslint .",
17
+ "typecheck": "tsc --noEmit",
18
+ "test": "vitest run --passWithNoTests",
19
+ "test:watch": "vitest"
20
+ },
21
+ "dependencies": {
22
+ "@clack/prompts": "^0.8.2",
23
+ "commander": "^12.1.0",
24
+ "zod": "^3.25.76"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^24.3.0",
28
+ "tsc-alias": "^1.8.16",
29
+ "tsx": "^4.20.3",
30
+ "typescript": "^5.7.2",
31
+ "vitest": "^3.2.4"
32
+ },
33
+ "engines": {
34
+ "node": ">=22.22.0"
35
+ }
36
+ }