diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af03700..ac025bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,10 @@ on: - 'integrated/**' - 'stl-preview-head/**' - 'stl-preview-base/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: lint: diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e7ca613..64f3cdd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.7.0" + ".": "0.8.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3c2da..c8206ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## 0.8.0 (2025-06-24) + +Full Changelog: [v0.7.0...v0.8.0](https://github.com/ContextualAI/contextual-client-node/compare/v0.7.0...v0.8.0) + +### Features + +* **client:** add support for endpoint-specific base URLs ([4375198](https://github.com/ContextualAI/contextual-client-node/commit/4375198ddd324eadac0053fbf0e9284f0686f6a5)) + + +### Bug Fixes + +* publish script — handle NPM errors correctly ([9855ea4](https://github.com/ContextualAI/contextual-client-node/commit/9855ea41c55355a17d1a6282bc8d158b72da2f63)) + + +### Chores + +* **ci:** enable for pull requests ([23d5fa7](https://github.com/ContextualAI/contextual-client-node/commit/23d5fa79636f991e3d4ff7a454ee973bb1fc2691)) +* **docs:** grammar improvements ([92210b2](https://github.com/ContextualAI/contextual-client-node/commit/92210b2f9cbfd930dfb7fcc8c6464a8d0c89bda7)) +* **docs:** use top-level-await in example snippets ([39783f4](https://github.com/ContextualAI/contextual-client-node/commit/39783f455c8d9d669d42a0671b239b51a196f743)) +* improve publish-npm script --latest tag logic ([bf8c320](https://github.com/ContextualAI/contextual-client-node/commit/bf8c320845e3781558b5c89c2d0dfda127138d13)) +* **internal:** make base APIResource abstract ([122895d](https://github.com/ContextualAI/contextual-client-node/commit/122895d96bcc6de97dc918dca7b62d8ababb0af5)) + + +### Refactors + +* **types:** replace Record with mapped types ([b50e9f1](https://github.com/ContextualAI/contextual-client-node/commit/b50e9f1b6306889aff9ee34fb2d29dcad6d8c3f7)) + ## 0.7.0 (2025-05-13) Full Changelog: [v0.6.0...v0.7.0](https://github.com/ContextualAI/contextual-client-node/compare/v0.6.0...v0.7.0) diff --git a/README.md b/README.md index db698e4..4d38e40 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,9 @@ const client = new ContextualAI({ apiKey: process.env['CONTEXTUAL_API_KEY'], // This is the default and can be omitted }); -async function main() { - const createAgentOutput = await client.agents.create({ name: 'Example' }); +const createAgentOutput = await client.agents.create({ name: 'Example' }); - console.log(createAgentOutput.id); -} - -main(); +console.log(createAgentOutput.id); ``` ### Request & Response types @@ -45,12 +41,8 @@ const client = new ContextualAI({ apiKey: process.env['CONTEXTUAL_API_KEY'], // This is the default and can be omitted }); -async function main() { - const params: ContextualAI.AgentCreateParams = { name: 'Example' }; - const createAgentOutput: ContextualAI.CreateAgentOutput = await client.agents.create(params); -} - -main(); +const params: ContextualAI.AgentCreateParams = { name: 'Example' }; +const createAgentOutput: ContextualAI.CreateAgentOutput = await client.agents.create(params); ``` Documentation for each method, request param, and response field are available in docstrings and will appear on hover in most modern editors. @@ -103,19 +95,15 @@ a subclass of `APIError` will be thrown: ```ts -async function main() { - const createAgentOutput = await client.agents.create({ name: 'Example' }).catch(async (err) => { - if (err instanceof ContextualAI.APIError) { - console.log(err.status); // 400 - console.log(err.name); // BadRequestError - console.log(err.headers); // {server: 'nginx', ...} - } else { - throw err; - } - }); -} - -main(); +const createAgentOutput = await client.agents.create({ name: 'Example' }).catch(async (err) => { + if (err instanceof ContextualAI.APIError) { + console.log(err.status); // 400 + console.log(err.name); // BadRequestError + console.log(err.headers); // {server: 'nginx', ...} + } else { + throw err; + } +}); ``` Error codes are as follows: diff --git a/SECURITY.md b/SECURITY.md index 92a473f..97e18f0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,11 +16,11 @@ before making any information public. ## Reporting Non-SDK Related Security Issues If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Contextual AI please follow the respective company's security reporting guidelines. +or products provided by Contextual AI, please follow the respective company's security reporting guidelines. ### Contextual AI Terms and Policies -Please contact support@contextual.ai for any questions or concerns regarding security of our services. +Please contact support@contextual.ai for any questions or concerns regarding the security of our services. --- diff --git a/bin/publish-npm b/bin/publish-npm index 4c21181..fa2243d 100644 --- a/bin/publish-npm +++ b/bin/publish-npm @@ -4,19 +4,55 @@ set -eux npm config set '//registry.npmjs.org/:_authToken' "$NPM_TOKEN" -# Build the project yarn build - -# Navigate to the dist directory cd dist -# Get the version from package.json -VERSION="$(node -p "require('./package.json').version")" +# Get package name and version from package.json +PACKAGE_NAME="$(jq -r -e '.name' ./package.json)" +VERSION="$(jq -r -e '.version' ./package.json)" + +# Get latest version from npm +# +# If the package doesn't exist, npm will return: +# { +# "error": { +# "code": "E404", +# "summary": "Unpublished on 2025-06-05T09:54:53.528Z", +# "detail": "'the_package' is not in this registry..." +# } +# } +NPM_INFO="$(npm view "$PACKAGE_NAME" version --json 2>/dev/null || true)" + +# Check if we got an E404 error +if echo "$NPM_INFO" | jq -e '.error.code == "E404"' > /dev/null 2>&1; then + # Package doesn't exist yet, no last version + LAST_VERSION="" +elif echo "$NPM_INFO" | jq -e '.error' > /dev/null 2>&1; then + # Report other errors + echo "ERROR: npm returned unexpected data:" + echo "$NPM_INFO" + exit 1 +else + # Success - get the version + LAST_VERSION=$(echo "$NPM_INFO" | jq -r '.') # strip quotes +fi -# Extract the pre-release tag if it exists +# Check if current version is pre-release (e.g. alpha / beta / rc) +CURRENT_IS_PRERELEASE=false if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then - # Extract the part before any dot in the pre-release identifier - TAG="${BASH_REMATCH[1]}" + CURRENT_IS_PRERELEASE=true + CURRENT_TAG="${BASH_REMATCH[1]}" +fi + +# Check if last version is a stable release +LAST_IS_STABLE_RELEASE=true +if [[ -z "$LAST_VERSION" || "$LAST_VERSION" =~ -([a-zA-Z]+) ]]; then + LAST_IS_STABLE_RELEASE=false +fi + +# Use a corresponding alpha/beta tag if there already is a stable release and we're publishing a prerelease. +if $CURRENT_IS_PRERELEASE && $LAST_IS_STABLE_RELEASE; then + TAG="$CURRENT_TAG" else TAG="latest" fi diff --git a/package.json b/package.json index 7837a69..ce28838 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "contextual-client", - "version": "0.7.0", + "version": "0.8.0", "description": "The official TypeScript library for the Contextual AI API", "author": "Contextual AI ", "types": "dist/index.d.ts", diff --git a/scripts/build b/scripts/build index d4a6875..dba69bb 100755 --- a/scripts/build +++ b/scripts/build @@ -28,7 +28,7 @@ fi node scripts/utils/make-dist-package-json.cjs > dist/package.json # build to .js/.mjs/.d.ts files -npm exec tsc-multi +./node_modules/.bin/tsc-multi # copy over handwritten .js/.mjs/.d.ts files cp src/_shims/*.{d.ts,js,mjs,md} dist/_shims cp src/_shims/auto/*.{d.ts,js,mjs} dist/_shims/auto diff --git a/src/core.ts b/src/core.ts index 57e9e88..8c08fb6 100644 --- a/src/core.ts +++ b/src/core.ts @@ -170,6 +170,7 @@ export class APIPromise extends Promise { export abstract class APIClient { baseURL: string; + #baseURLOverridden: boolean; maxRetries: number; timeout: number; httpAgent: Agent | undefined; @@ -179,18 +180,21 @@ export abstract class APIClient { constructor({ baseURL, + baseURLOverridden, maxRetries = 2, timeout = 60000, // 1 minute httpAgent, fetch: overriddenFetch, }: { baseURL: string; + baseURLOverridden: boolean; maxRetries?: number | undefined; timeout: number | undefined; httpAgent: Agent | undefined; fetch: Fetch | undefined; }) { this.baseURL = baseURL; + this.#baseURLOverridden = baseURLOverridden; this.maxRetries = validatePositiveInteger('maxRetries', maxRetries); this.timeout = validatePositiveInteger('timeout', timeout); this.httpAgent = httpAgent; @@ -300,7 +304,7 @@ export abstract class APIClient { { retryCount = 0 }: { retryCount?: number } = {}, ): { req: RequestInit; url: string; timeout: number } { const options = { ...inputOptions }; - const { method, path, query, headers: headers = {} } = options; + const { method, path, query, defaultBaseURL, headers: headers = {} } = options; const body = ArrayBuffer.isView(options.body) || (options.__binaryRequest && typeof options.body === 'string') ? @@ -310,7 +314,7 @@ export abstract class APIClient { : null; const contentLength = this.calculateContentLength(body); - const url = this.buildURL(path!, query); + const url = this.buildURL(path!, query, defaultBaseURL); if ('timeout' in options) validatePositiveInteger('timeout', options.timeout); options.timeout = options.timeout ?? this.timeout; const httpAgent = options.httpAgent ?? this.httpAgent ?? getDefaultAgent(url); @@ -503,11 +507,12 @@ export abstract class APIClient { return new PagePromise(this, request, Page); } - buildURL(path: string, query: Req | null | undefined): string { + buildURL(path: string, query: Req | null | undefined, defaultBaseURL?: string | undefined): string { + const baseURL = (!this.#baseURLOverridden && defaultBaseURL) || this.baseURL; const url = isAbsoluteURL(path) ? new URL(path) - : new URL(this.baseURL + (this.baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); + : new URL(baseURL + (baseURL.endsWith('/') && path.startsWith('/') ? path.slice(1) : path)); const defaultQuery = this.defaultQuery(); if (!isEmptyObj(defaultQuery)) { @@ -792,6 +797,7 @@ export type RequestOptions< query?: Req | undefined; body?: Req | null | undefined; headers?: Headers | undefined; + defaultBaseURL?: string | undefined; maxRetries?: number; stream?: boolean | undefined; @@ -813,6 +819,7 @@ const requestOptionsKeys: KeysEnum = { query: true, body: true, headers: true, + defaultBaseURL: true, maxRetries: true, stream: true, diff --git a/src/index.ts b/src/index.ts index f17be44..b476f78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -178,6 +178,7 @@ export class ContextualAI extends Core.APIClient { super({ baseURL: options.baseURL!, + baseURLOverridden: baseURL ? baseURL !== 'https://api.contextual.ai/v1' : false, timeout: options.timeout ?? 60000 /* 1 minute */, httpAgent: options.httpAgent, maxRetries: options.maxRetries, @@ -197,6 +198,13 @@ export class ContextualAI extends Core.APIClient { generate: API.Generate = new API.Generate(this); parse: API.Parse = new API.Parse(this); + /** + * Check whether the base URL is set to its default. + */ + #baseURLOverridden(): boolean { + return this.baseURL !== 'https://api.contextual.ai/v1'; + } + protected override defaultQuery(): Core.DefaultQuery | undefined { return this._options.defaultQuery; } diff --git a/src/resource.ts b/src/resource.ts index 7f9b100..adf0750 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -2,7 +2,7 @@ import type { ContextualAI } from './index'; -export class APIResource { +export abstract class APIResource { protected _client: ContextualAI; constructor(client: ContextualAI) { diff --git a/src/resources/datastores/documents.ts b/src/resources/datastores/documents.ts index f1c7b2a..b99fc48 100644 --- a/src/resources/datastores/documents.ts +++ b/src/resources/datastores/documents.ts @@ -181,7 +181,7 @@ export interface DocumentMetadata { */ status: 'pending' | 'processing' | 'retrying' | 'completed' | 'failed' | 'cancelled'; - custom_metadata?: Record; + custom_metadata?: { [key: string]: boolean | number | string }; /** * Timestamp of when the document was modified in ISO format. @@ -282,7 +282,7 @@ export interface DocumentIngestParams { } export interface DocumentSetMetadataParams { - custom_metadata?: Record; + custom_metadata?: { [key: string]: boolean | number | string }; } Documents.DocumentMetadataDocumentsPage = DocumentMetadataDocumentsPage; diff --git a/src/resources/users.ts b/src/resources/users.ts index 8339bf6..3edbb31 100644 --- a/src/resources/users.ts +++ b/src/resources/users.ts @@ -59,7 +59,7 @@ export interface InviteUsersResponse { * Details of the errors occurred while inviting users, where keys are the emails * and values are the error messages */ - error_details: Record; + error_details: { [key: string]: string }; /** * List of emails of the invited users diff --git a/src/version.ts b/src/version.ts index d9da9f7..23f967c 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.7.0'; // x-release-please-version +export const VERSION = '0.8.0'; // x-release-please-version diff --git a/tests/index.test.ts b/tests/index.test.ts index 90855ce..3266e41 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -188,6 +188,28 @@ describe('instantiate client', () => { const client = new ContextualAI({ apiKey: 'My API Key' }); expect(client.baseURL).toEqual('https://api.contextual.ai/v1'); }); + + test('in request options', () => { + const client = new ContextualAI({ apiKey: 'My API Key' }); + expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( + 'http://localhost:5000/option/foo', + ); + }); + + test('in request options overridden by client options', () => { + const client = new ContextualAI({ apiKey: 'My API Key', baseURL: 'http://localhost:5000/client' }); + expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( + 'http://localhost:5000/client/foo', + ); + }); + + test('in request options overridden by env variable', () => { + process.env['CONTEXTUAL_AI_BASE_URL'] = 'http://localhost:5000/env'; + const client = new ContextualAI({ apiKey: 'My API Key' }); + expect(client.buildURL('/foo', null, 'http://localhost:5000/option')).toEqual( + 'http://localhost:5000/env/foo', + ); + }); }); test('maxRetries option is correctly set', () => {