diff --git a/.changeset/tame-wasps-carry.md b/.changeset/tame-wasps-carry.md new file mode 100644 index 0000000..0a7e1d3 --- /dev/null +++ b/.changeset/tame-wasps-carry.md @@ -0,0 +1,6 @@ +--- +"@mixedbread/cli": patch +--- + +- Fixed `--base-url` global option not working +- Migrated to zod v4 diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 0d07714..6384dc9 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -4,14 +4,14 @@ ### Minor Changes -- 37cf096: - Change max parallel upload/deletion to 200 - - Change default parallel and `parallel` in config to 100 +- 37cf096: - Changed max parallel upload/deletion to 200 + - Changed default parallel and `parallel` in config to 100 ## 1.1.0 ### Minor Changes -- 384880d: Add intelligent shell completion for vector store names +- 384880d: Added intelligent shell completion for vector store names - **Dynamic completions**: Tab completion now suggests vector store names in commands like `mxbai vs sync [TAB]`, `mxbai vs upload [TAB]`, etc. - **Multi-key support**: Completions work seamlessly with multiple API keys, showing stores for the current default key @@ -25,13 +25,13 @@ ### Patch Changes -- d60b52d: Fix metadata types before uploading +- d60b52d: Fixed metadata types before uploading ## 1.0.0 ### Major Changes -- 617f988: Add force upload option and standardize CLI flags (breaking changes) +- 617f988: Added force upload option and standardize CLI flags (breaking changes) ## Breaking Changes diff --git a/packages/cli/README.md b/packages/cli/README.md index 3fe96e9..4471aa3 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -161,10 +161,11 @@ files: When using the CLI, configuration values are resolved in the following order (highest to lowest priority): 1. **Command-line flags** - Direct CLI options (e.g., `--strategy high_quality`) -2. **Manifest entry** - File-specific settings in manifest files -3. **Manifest defaults** - Default settings in manifest files -4. **Config file** - User configuration file settings -5. **Built-in defaults** - CLI default values +2. **Environment variables** - System environment settings (e.g., `MXBAI_API_KEY`, `MXBAI_BASE_URL`, `MXBAI_CONFIG_PATH`, `MXBAI_DEBUG`) +3. **Manifest entry** - File-specific settings in manifest files +4. **Manifest defaults** - Default settings in manifest files +5. **Config file** - User configuration file settings +6. **Built-in defaults** - CLI default values This allows flexible configuration while maintaining predictable behavior. diff --git a/packages/cli/package.json b/packages/cli/package.json index 1ca08aa..90710a1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,7 +62,7 @@ "ora": "^8.0.1", "p-limit": "^6.2.0", "yaml": "^2.4.5", - "zod": "^3.25.56" + "zod": "^4.1.5" }, "devDependencies": { "@jest/globals": "^30.0.2", diff --git a/packages/cli/src/commands/config/keys.ts b/packages/cli/src/commands/config/keys.ts index 0018cfb..9d5aab0 100644 --- a/packages/cli/src/commands/config/keys.ts +++ b/packages/cli/src/commands/config/keys.ts @@ -22,7 +22,7 @@ import { } from "../../utils/global-options"; const RemoveKeySchema = z.object({ - name: z.string().min(1, { message: '"name" is required' }), + name: z.string().min(1, { error: '"name" is required' }), yes: z.boolean().optional(), }); @@ -98,7 +98,7 @@ export function createKeysCommand(): Command { // Populate completion cache for the new key const client = createClient({ apiKey: key, - baseURL: parsedOptions.baseURL, + baseUrl: parsedOptions.baseUrl, }); refreshCacheForKey(name, client); }); @@ -232,7 +232,7 @@ export function createKeysCommand(): Command { // Refresh cache for the newly set default key const client = createClient({ apiKey: config.api_keys[name], - baseURL: parsedOptions.baseURL, + baseUrl: parsedOptions.baseUrl, }); refreshCacheForKey(name, client); }); diff --git a/packages/cli/src/commands/vector-store/create.ts b/packages/cli/src/commands/vector-store/create.ts index 8be6f4d..badba82 100644 --- a/packages/cli/src/commands/vector-store/create.ts +++ b/packages/cli/src/commands/vector-store/create.ts @@ -18,12 +18,12 @@ import { validateMetadata } from "../../utils/metadata"; import { formatOutput } from "../../utils/output"; const CreateVectorStoreSchema = extendGlobalOptions({ - name: z.string().min(1, { message: '"name" is required' }), + name: z.string().min(1, { error: '"name" is required' }), description: z.string().optional(), expiresAfter: z.coerce - .number({ message: '"expires-after" must be a number' }) - .int({ message: '"expires-after" must be an integer' }) - .positive({ message: '"expires-after" must be positive' }) + .number({ error: '"expires-after" must be a number' }) + .int({ error: '"expires-after" must be an integer' }) + .positive({ error: '"expires-after" must be positive' }) .optional(), metadata: z.string().optional(), }); diff --git a/packages/cli/src/commands/vector-store/delete.ts b/packages/cli/src/commands/vector-store/delete.ts index bd05598..fe01383 100644 --- a/packages/cli/src/commands/vector-store/delete.ts +++ b/packages/cli/src/commands/vector-store/delete.ts @@ -18,7 +18,7 @@ import { import { resolveVectorStore } from "../../utils/vector-store"; const DeleteVectorStoreSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), yes: z.boolean().optional(), }); diff --git a/packages/cli/src/commands/vector-store/files/delete.ts b/packages/cli/src/commands/vector-store/files/delete.ts index 438b9e5..7f5d926 100644 --- a/packages/cli/src/commands/vector-store/files/delete.ts +++ b/packages/cli/src/commands/vector-store/files/delete.ts @@ -14,8 +14,8 @@ import { import { resolveVectorStore } from "../../../utils/vector-store"; const DeleteFileSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), - fileId: z.string().min(1, { message: '"file-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), + fileId: z.string().min(1, { error: '"file-id" is required' }), yes: z.boolean().optional(), }); diff --git a/packages/cli/src/commands/vector-store/files/get.ts b/packages/cli/src/commands/vector-store/files/get.ts index dacf556..42f7db8 100644 --- a/packages/cli/src/commands/vector-store/files/get.ts +++ b/packages/cli/src/commands/vector-store/files/get.ts @@ -14,8 +14,8 @@ import { formatBytes, formatOutput } from "../../../utils/output"; import { resolveVectorStore } from "../../../utils/vector-store"; const GetFileSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), - fileId: z.string().min(1, { message: '"file-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), + fileId: z.string().min(1, { error: '"file-id" is required' }), }); export function createGetCommand(): Command { diff --git a/packages/cli/src/commands/vector-store/files/list.ts b/packages/cli/src/commands/vector-store/files/list.ts index db31f5b..332c376 100644 --- a/packages/cli/src/commands/vector-store/files/list.ts +++ b/packages/cli/src/commands/vector-store/files/list.ts @@ -19,17 +19,17 @@ import { resolveVectorStore } from "../../../utils/vector-store"; import type { FilesOptions } from "."; const ListFilesSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), status: z .enum(["all", "completed", "in_progress", "failed"], { - message: '"status" must be one of: all, completed, in_progress, failed', + error: '"status" must be one of: all, completed, in_progress, failed', }) .optional(), limit: z.coerce - .number({ message: '"limit" must be a number' }) - .int({ message: '"limit" must be an integer' }) - .positive({ message: '"limit" must be positive' }) - .max(100, { message: '"limit" must be less than or equal to 100' }) + .number({ error: '"limit" must be a number' }) + .int({ error: '"limit" must be an integer' }) + .positive({ error: '"limit" must be positive' }) + .max(100, { error: '"limit" must be less than or equal to 100' }) .optional(), }); diff --git a/packages/cli/src/commands/vector-store/get.ts b/packages/cli/src/commands/vector-store/get.ts index 254c367..8804c37 100644 --- a/packages/cli/src/commands/vector-store/get.ts +++ b/packages/cli/src/commands/vector-store/get.ts @@ -14,7 +14,7 @@ import { formatBytes, formatOutput } from "../../utils/output"; import { resolveVectorStore } from "../../utils/vector-store"; const GetVectorStoreSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), }); interface GetOptions extends GlobalOptions {} diff --git a/packages/cli/src/commands/vector-store/list.ts b/packages/cli/src/commands/vector-store/list.ts index 30a3710..d1250f8 100644 --- a/packages/cli/src/commands/vector-store/list.ts +++ b/packages/cli/src/commands/vector-store/list.ts @@ -23,10 +23,10 @@ import { const ListVectorStoreSchema = extendGlobalOptions({ filter: z.string().optional(), limit: z.coerce - .number({ message: '"limit" must be a number' }) - .int({ message: '"limit" must be an integer' }) - .positive({ message: '"limit" must be positive' }) - .max(100, { message: '"limit" must be less than or equal to 100' }) + .number({ error: '"limit" must be a number' }) + .int({ error: '"limit" must be an integer' }) + .positive({ error: '"limit" must be positive' }) + .max(100, { error: '"limit" must be less than or equal to 100' }) .optional(), }); diff --git a/packages/cli/src/commands/vector-store/qa.ts b/packages/cli/src/commands/vector-store/qa.ts index 9d0dff6..b1139d1 100644 --- a/packages/cli/src/commands/vector-store/qa.ts +++ b/packages/cli/src/commands/vector-store/qa.ts @@ -15,18 +15,18 @@ import { formatOutput } from "../../utils/output"; import { resolveVectorStore } from "../../utils/vector-store"; const QAVectorStoreSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), - question: z.string().min(1, { message: '"question" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), + question: z.string().min(1, { error: '"question" is required' }), topK: z.coerce - .number({ message: '"top-k" must be a number' }) - .int({ message: '"top-k" must be an integer' }) - .positive({ message: '"top-k" must be positive' }) - .max(100, { message: '"top-k" must be less than or equal to 100' }) + .number({ error: '"top-k" must be a number' }) + .int({ error: '"top-k" must be an integer' }) + .positive({ error: '"top-k" must be positive' }) + .max(100, { error: '"top-k" must be less than or equal to 100' }) .optional(), threshold: z.coerce - .number({ message: '"threshold" must be a number' }) - .min(0, { message: '"threshold" must be greater than or equal to 0' }) - .max(1, { message: '"threshold" must be less than or equal to 1' }) + .number({ error: '"threshold" must be a number' }) + .min(0, { error: '"threshold" must be greater than or equal to 0' }) + .max(1, { error: '"threshold" must be less than or equal to 1' }) .optional(), cite: z.boolean().optional(), multimodal: z.boolean().optional(), diff --git a/packages/cli/src/commands/vector-store/search.ts b/packages/cli/src/commands/vector-store/search.ts index 5183316..8179f09 100644 --- a/packages/cli/src/commands/vector-store/search.ts +++ b/packages/cli/src/commands/vector-store/search.ts @@ -16,18 +16,18 @@ import { formatCountWithSuffix, formatOutput } from "../../utils/output"; import { resolveVectorStore } from "../../utils/vector-store"; const SearchVectorStoreSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), - query: z.string().min(1, { message: '"query" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), + query: z.string().min(1, { error: '"query" is required' }), topK: z.coerce - .number({ message: '"top-k" must be a number' }) - .int({ message: '"top-k" must be an integer' }) - .positive({ message: '"top-k" must be positive' }) - .max(100, { message: '"top-k" must be less than or equal to 100' }) + .number({ error: '"top-k" must be a number' }) + .int({ error: '"top-k" must be an integer' }) + .positive({ error: '"top-k" must be positive' }) + .max(100, { error: '"top-k" must be less than or equal to 100' }) .optional(), threshold: z.coerce - .number({ message: '"threshold" must be a number' }) - .min(0, { message: '"threshold" must be greater than or equal to 0' }) - .max(1, { message: '"threshold" must be less than or equal to 1' }) + .number({ error: '"threshold" must be a number' }) + .min(0, { error: '"threshold" must be greater than or equal to 0' }) + .max(1, { error: '"threshold" must be less than or equal to 1' }) .optional(), returnMetadata: z.boolean().optional(), rerank: z.boolean().optional(), diff --git a/packages/cli/src/commands/vector-store/sync.ts b/packages/cli/src/commands/vector-store/sync.ts index 1004633..e7ef3a5 100644 --- a/packages/cli/src/commands/vector-store/sync.ts +++ b/packages/cli/src/commands/vector-store/sync.ts @@ -24,13 +24,13 @@ import { getSyncedFiles } from "../../utils/sync-state"; import { resolveVectorStore } from "../../utils/vector-store"; const SyncVectorStoreSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), patterns: z .array(z.string()) - .min(1, { message: "At least one pattern is required" }), + .min(1, { error: "At least one pattern is required" }), strategy: z.enum(["fast", "high_quality"]).optional(), contextualization: z - .boolean({ message: '"contextualization" must be a boolean' }) + .boolean({ error: '"contextualization" must be a boolean' }) .optional(), fromGit: z.string().optional(), dryRun: z.boolean().optional(), @@ -38,10 +38,10 @@ const SyncVectorStoreSchema = extendGlobalOptions({ force: z.boolean().optional(), metadata: z.string().optional(), parallel: z.coerce - .number({ message: '"parallel" must be a number' }) - .int({ message: '"parallel" must be an integer' }) - .min(1, { message: '"parallel" must be at least 1' }) - .max(200, { message: '"parallel" must be less than or equal to 200' }) + .number({ error: '"parallel" must be a number' }) + .int({ error: '"parallel" must be an integer' }) + .min(1, { error: '"parallel" must be at least 1' }) + .max(200, { error: '"parallel" must be less than or equal to 200' }) .optional() .default(100), }); diff --git a/packages/cli/src/commands/vector-store/update.ts b/packages/cli/src/commands/vector-store/update.ts index 87fb2d9..58b6e03 100644 --- a/packages/cli/src/commands/vector-store/update.ts +++ b/packages/cli/src/commands/vector-store/update.ts @@ -20,13 +20,13 @@ import { formatOutput } from "../../utils/output"; import { resolveVectorStore } from "../../utils/vector-store"; const UpdateVectorStoreSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), name: z.string().optional(), description: z.string().optional(), expiresAfter: z.coerce - .number({ message: '"expires-after" must be a number' }) - .int({ message: '"expires-after" must be an integer' }) - .positive({ message: '"expires-after" must be positive' }) + .number({ error: '"expires-after" must be a number' }) + .int({ error: '"expires-after" must be an integer' }) + .positive({ error: '"expires-after" must be positive' }) .optional(), metadata: z.string().optional(), }); diff --git a/packages/cli/src/commands/vector-store/upload.ts b/packages/cli/src/commands/vector-store/upload.ts index dd6adad..8466d8b 100644 --- a/packages/cli/src/commands/vector-store/upload.ts +++ b/packages/cli/src/commands/vector-store/upload.ts @@ -24,23 +24,23 @@ import { } from "../../utils/vector-store"; const UploadVectorStoreSchema = extendGlobalOptions({ - nameOrId: z.string().min(1, { message: '"name-or-id" is required' }), + nameOrId: z.string().min(1, { error: '"name-or-id" is required' }), patterns: z.array(z.string()).optional(), strategy: z .enum(["fast", "high_quality"], { - message: '"strategy" must be either "fast" or "high_quality"', + error: '"strategy" must be either "fast" or "high_quality"', }) .optional(), contextualization: z - .boolean({ message: '"contextualization" must be a boolean' }) + .boolean({ error: '"contextualization" must be a boolean' }) .optional(), metadata: z.string().optional(), dryRun: z.boolean().optional(), parallel: z.coerce - .number({ message: '"parallel" must be a number' }) - .int({ message: '"parallel" must be an integer' }) - .min(1, { message: '"parallel" must be at least 1' }) - .max(200, { message: '"parallel" must be less than or equal to 200' }) + .number({ error: '"parallel" must be a number' }) + .int({ error: '"parallel" must be an integer' }) + .min(1, { error: '"parallel" must be at least 1' }) + .max(200, { error: '"parallel" must be less than or equal to 200' }) .optional(), unique: z.boolean().optional(), manifest: z.string().optional(), diff --git a/packages/cli/src/utils/client.ts b/packages/cli/src/utils/client.ts index 2e2022e..a8c019e 100644 --- a/packages/cli/src/utils/client.ts +++ b/packages/cli/src/utils/client.ts @@ -3,7 +3,7 @@ import { getApiKey, getBaseURL } from "./config"; export function createClient(options?: { apiKey?: string; - baseURL?: string; + baseUrl?: string; }): Mixedbread { const apiKey = getApiKey(options); const baseURL = getBaseURL(options); diff --git a/packages/cli/src/utils/completion-cache.ts b/packages/cli/src/utils/completion-cache.ts index b55f65d..aac7757 100644 --- a/packages/cli/src/utils/completion-cache.ts +++ b/packages/cli/src/utils/completion-cache.ts @@ -109,7 +109,7 @@ export async function refreshCacheForKey( } export async function refreshAllCaches(options: { - baseURL?: string; + baseUrl?: string; }): Promise { const config = loadConfig(); @@ -120,7 +120,7 @@ export async function refreshAllCaches(options: { for (const keyName of Object.keys(config.api_keys)) { const client = createClient({ apiKey: config.api_keys[keyName], - baseURL: options.baseURL, + baseUrl: options.baseUrl, }); await refreshCacheForKey(keyName, client); diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts index 42b84d3..f79371e 100644 --- a/packages/cli/src/utils/config.ts +++ b/packages/cli/src/utils/config.ts @@ -7,7 +7,7 @@ import { z } from "zod"; export const UploadDefaultsSchema = z.object({ strategy: z .enum(["fast", "high_quality"], { - message: 'Must be "fast" or "high_quality"', + error: 'Must be "fast" or "high_quality"', }) .optional(), contextualization: z @@ -17,22 +17,22 @@ export const UploadDefaultsSchema = z.object({ if (val === "false" || val === false) return false; throw new Error('Must be "true" or "false"'); }, - z.boolean({ message: 'Must be "true" or "false"' }) + z.boolean({ error: 'Must be "true" or "false"' }) ) .optional(), parallel: z.coerce - .number({ message: "Must be a number" }) - .int({ message: "Must be an integer" }) - .positive({ message: "Must be a positive number" }) - .max(200, { message: "Must be less than or equal to 200" }) + .number({ error: "Must be a number" }) + .int({ error: "Must be an integer" }) + .positive({ error: "Must be a positive number" }) + .max(200, { error: "Must be less than or equal to 200" }) .optional(), }); export const SearchDefaultsSchema = z.object({ top_k: z.coerce - .number({ message: "Must be a number" }) - .int({ message: "Must be an integer" }) - .positive({ message: "Must be a positive number" }) + .number({ error: "Must be a number" }) + .int({ error: "Must be an integer" }) + .positive({ error: "Must be a positive number" }) .optional(), rerank: z .preprocess( @@ -41,7 +41,7 @@ export const SearchDefaultsSchema = z.object({ if (val === "false" || val === false) return false; throw new Error('Must be "true" or "false"'); }, - z.boolean({ message: 'Must be "true" or "false"' }) + z.boolean({ error: 'Must be "true" or "false"' }) ) .optional(), }); @@ -60,7 +60,7 @@ export const CLIConfigSchema = z.object({ z.string().startsWith("mxb_", 'API key must start with "mxb_"') ) .optional(), - base_url: z.string().url("Base URL must be a valid URL").optional(), + base_url: z.url("Base URL must be a valid URL").optional(), defaults: DefaultsSchema.optional(), aliases: z.record(z.string(), z.string()).optional(), }); @@ -262,9 +262,9 @@ export function isMxbaiAPIKey(key: string): boolean { return key.startsWith("mxb_"); } -export function getBaseURL(options?: { baseURL?: string }): string { +export function getBaseURL(options?: { baseUrl?: string }): string { return ( - options?.baseURL || process.env.MXBAI_BASE_URL || loadConfig().base_url + options?.baseUrl || process.env.MXBAI_BASE_URL || loadConfig().base_url ); } @@ -275,9 +275,9 @@ export function resolveVectorStoreName(nameOrAlias: string): string { // Helper to resolve nested schema paths function resolveSchemaPath( - schema: z.ZodSchema, + schema: z.ZodType, path: string[] -): z.ZodSchema | null { +): z.ZodType | null { if (path.length === 0) return schema; const [head, ...tail] = path; @@ -292,12 +292,12 @@ function resolveSchemaPath( // Handle ZodRecord (for dynamic keys like aliases) if (schema instanceof z.ZodRecord) { - return resolveSchemaPath(schema._def.valueType, tail); + return resolveSchemaPath(schema._zod.def.valueType as z.ZodType, tail); } // Handle ZodOptional if (schema instanceof z.ZodOptional) { - return resolveSchemaPath(schema._def.innerType, path); + return resolveSchemaPath(schema._zod.def.innerType as z.ZodType, path); } return null; diff --git a/packages/cli/src/utils/global-options.ts b/packages/cli/src/utils/global-options.ts index 9073651..3de9da8 100644 --- a/packages/cli/src/utils/global-options.ts +++ b/packages/cli/src/utils/global-options.ts @@ -6,36 +6,36 @@ export interface GlobalOptions { apiKey?: string; savedKey?: string; format?: "table" | "json" | "csv"; - baseURL?: string; + baseUrl?: string; debug?: boolean; } const BaseGlobalOptionsSchema = z.object({ apiKey: z.string().optional(), savedKey: z.string().optional(), - baseURL: z.string().url('"base-url" must be a valid URL').optional(), + baseUrl: z.url('"base-url" must be a valid URL').optional(), format: z .enum(["table", "json", "csv"], { - message: '"format" must be either "table", "json", or "csv"', + error: '"format" must be either "table", "json", or "csv"', }) .optional(), - debug: z.boolean({ message: '"debug" must be "true" or "false"' }).optional(), + debug: z.boolean({ error: '"debug" must be "true" or "false"' }).optional(), }); export const GlobalOptionsSchema = BaseGlobalOptionsSchema.refine( (data) => !(data.apiKey && data.savedKey), { - message: "Cannot specify both --api-key and --saved-key options", + error: "Cannot specify both --api-key and --saved-key options", path: ["apiKey"], } ); // Helper function to extend global options while preserving mutual exclusivity validation export const extendGlobalOptions = (extension: T) => { - return BaseGlobalOptionsSchema.extend(extension).refine( + return BaseGlobalOptionsSchema.and(z.object(extension)).refine( (data) => !(data.apiKey && data.savedKey), { - message: "Cannot specify both --api-key and --saved-key options", + error: "Cannot specify both --api-key and --saved-key options", path: ["apiKey"], } ); diff --git a/packages/cli/src/utils/manifest.ts b/packages/cli/src/utils/manifest.ts index 6cbcb2b..32ff679 100644 --- a/packages/cli/src/utils/manifest.ts +++ b/packages/cli/src/utils/manifest.ts @@ -14,10 +14,10 @@ import { getVectorStoreFiles } from "./vector-store"; // Manifest file schema const ManifestFileEntrySchema = z.object({ - path: z.string().min(1, { message: "File path is required" }), + path: z.string().min(1, { error: "File path is required" }), strategy: z.enum(["fast", "high_quality"]).optional(), contextualization: z.boolean().optional(), - metadata: z.record(z.unknown()).optional(), + metadata: z.record(z.string(), z.unknown()).optional(), }); const ManifestSchema = z.object({ @@ -26,12 +26,12 @@ const ManifestSchema = z.object({ .object({ strategy: z.enum(["fast", "high_quality"]).optional(), contextualization: z.boolean().optional(), - metadata: z.record(z.unknown()).optional(), + metadata: z.record(z.string(), z.unknown()).optional(), }) .optional(), files: z .array(ManifestFileEntrySchema) - .min(1, { message: "At least one file entry is required" }), + .min(1, { error: "At least one file entry is required" }), }); type ManifestFile = z.infer; @@ -211,7 +211,7 @@ export async function uploadFromManifest( } catch (error) { if (error instanceof z.ZodError) { console.error(chalk.red("✗"), "Invalid manifest file format:"); - error.errors.forEach((err) => { + error.issues.forEach((err) => { console.error(chalk.red(` - ${err.path.join(".")}: ${err.message}`)); }); } else if (error instanceof Error) { diff --git a/packages/cli/tests/commands/vector-store/list.test.ts b/packages/cli/tests/commands/vector-store/list.test.ts index d570a67..7246c18 100644 --- a/packages/cli/tests/commands/vector-store/list.test.ts +++ b/packages/cli/tests/commands/vector-store/list.test.ts @@ -357,6 +357,117 @@ describe("Vector Store List Command", () => { }); }); + describe("Base URL handling", () => { + it("should pass base URL from command line to createClient", async () => { + mockClient.vectorStores.list.mockResolvedValue( + createMockCursor([], { + first_cursor: "123", + last_cursor: "456", + has_more: false, + total: 0, + }) + ); + + await command.parseAsync([ + "node", + "list", + "--base-url", + "https://custom-api.example.com", + ]); + + expect(mockCreateClient).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: "https://custom-api.example.com", + }) + ); + }); + + it("should validate base URL is a valid URL", async () => { + mockClient.vectorStores.list.mockResolvedValue( + createMockCursor([], { + first_cursor: "123", + last_cursor: "456", + has_more: false, + total: 0, + }) + ); + + await command.parseAsync([ + "node", + "list", + "--base-url", + "not-a-valid-url", + ]); + + expect(console.error).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('"base-url" must be a valid URL') + ); + expect(process.exit).toHaveBeenCalledWith(1); + }); + + it("should work with base URL and API key together", async () => { + mockClient.vectorStores.list.mockResolvedValue( + createMockCursor([], { + first_cursor: "123", + last_cursor: "456", + has_more: false, + total: 0, + }) + ); + + await command.parseAsync([ + "node", + "list", + "--base-url", + "https://custom-api.example.com", + "--api-key", + "mxb_test123", + ]); + + expect(mockCreateClient).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: "https://custom-api.example.com", + apiKey: "mxb_test123", + }) + ); + }); + + it("should prioritize CLI base URL over environment variable", async () => { + const originalEnv = process.env.MXBAI_BASE_URL; + process.env.MXBAI_BASE_URL = "https://env-api.example.com"; + + mockClient.vectorStores.list.mockResolvedValue( + createMockCursor([], { + first_cursor: "123", + last_cursor: "456", + has_more: false, + total: 0, + }) + ); + + await command.parseAsync([ + "node", + "list", + "--base-url", + "https://cli-api.example.com", + ]); + + expect(mockCreateClient).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: "https://cli-api.example.com", + }) + ); + + // Cleanup + if (originalEnv) { + process.env.MXBAI_BASE_URL = originalEnv; + } else { + delete process.env.MXBAI_BASE_URL; + } + }); + }); + describe("Edge cases", () => { it("should handle vector stores with missing fields", async () => { const mockData = [ diff --git a/packages/cli/tests/utils/completion-cache.test.ts b/packages/cli/tests/utils/completion-cache.test.ts index 2655065..c59a890 100644 --- a/packages/cli/tests/utils/completion-cache.test.ts +++ b/packages/cli/tests/utils/completion-cache.test.ts @@ -319,7 +319,7 @@ describe("Completion Cache", () => { mockCreateClient.mockReturnValue(mockClient); - await refreshAllCaches({ baseURL: "https://api.example.com" }); + await refreshAllCaches({ baseUrl: "https://api.example.com" }); const cacheContent = JSON.parse(readFileSync(cacheFile, "utf-8")); expect(cacheContent.stores.work).toEqual(["work-store"]); diff --git a/packages/cli/tests/utils/config.test.ts b/packages/cli/tests/utils/config.test.ts index 2f83924..37a67f5 100644 --- a/packages/cli/tests/utils/config.test.ts +++ b/packages/cli/tests/utils/config.test.ts @@ -5,6 +5,7 @@ import mockFs from "mock-fs"; import { type CLIConfig, getApiKey, + getBaseURL, loadConfig, parseConfigValue, resolveVectorStoreName, @@ -334,6 +335,78 @@ describe("Config Utils", () => { }); }); + describe("getBaseURL", () => { + afterEach(() => { + delete process.env.MXBAI_BASE_URL; + }); + + it("should prioritize options over environment and config", () => { + process.env.MXBAI_BASE_URL = "https://env-api.example.com"; + mockFs({ + [configFile]: JSON.stringify({ + version: "1.0", + base_url: "https://config-api.example.com", + }), + }); + + const baseUrl = getBaseURL({ + baseUrl: "https://options-api.example.com", + }); + + expect(baseUrl).toBe("https://options-api.example.com"); + }); + + it("should use environment variable when no options provided", () => { + process.env.MXBAI_BASE_URL = "https://env-api.example.com"; + mockFs({ + [configFile]: JSON.stringify({ + version: "1.0", + base_url: "https://config-api.example.com", + }), + }); + + const baseUrl = getBaseURL(); + + expect(baseUrl).toBe("https://env-api.example.com"); + }); + + it("should use config value when no options or env provided", () => { + delete process.env.MXBAI_BASE_URL; + mockFs({ + [configFile]: JSON.stringify({ + version: "1.0", + base_url: "https://config-api.example.com", + }), + }); + + const baseUrl = getBaseURL(); + + expect(baseUrl).toBe("https://config-api.example.com"); + }); + + it("should return undefined when no base URL is set anywhere", () => { + delete process.env.MXBAI_BASE_URL; + mockFs({ + [configFile]: JSON.stringify({ + version: "1.0", + }), + }); + + const baseUrl = getBaseURL(); + + expect(baseUrl).toBeUndefined(); + }); + + it("should handle missing config file", () => { + delete process.env.MXBAI_BASE_URL; + mockFs({}); + + const baseUrl = getBaseURL(); + + expect(baseUrl).toBeUndefined(); + }); + }); + describe("resolveVectorStoreName", () => { it("should return alias value if exists", () => { mockFs({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c39d4f2..0a4692a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,8 +66,8 @@ importers: specifier: ^2.4.5 version: 2.8.1 zod: - specifier: ^3.25.56 - version: 3.25.76 + specifier: ^4.1.5 + version: 4.1.5 devDependencies: '@jest/globals': specifier: ^30.0.2 @@ -3472,6 +3472,9 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.5: + resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -7399,4 +7402,6 @@ snapshots: zod@3.25.76: {} + zod@4.1.5: {} + zwitch@2.0.4: {}