diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 54282d9..ba9c41f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ permissions: id-token: write jobs: - ci: + typos: runs-on: ubuntu-latest steps: - name: Checkout repository @@ -25,36 +25,56 @@ jobs: - name: Setup Nix uses: ./.github/actions/setup-nix + - name: Run Typo Check + run: nix develop --command typos . + + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Setup Nix + uses: ./.github/actions/setup-nix - name: Run Lint run: nix develop --command pnpm run lint + build-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Nix + uses: ./.github/actions/setup-nix + - name: Run Build run: nix develop --command pnpm run build - name: Run Tests - if: github.ref != 'refs/heads/main' run: nix develop --command pnpm test + coverage: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Setup Nix + uses: ./.github/actions/setup-nix - name: Run Tests with Coverage - if: github.ref == 'refs/heads/main' run: nix develop --command pnpm run coverage - - name: Create Coverage Badge - if: github.ref == 'refs/heads/main' uses: jaywcjlove/coverage-badges-cli@bd6ccbf422c0ed54c01f283019fd2bc648f58541 # v2.2.0 with: source: coverage/coverage-summary.json output: coverage/badges.svg - - name: Upload coverage artifact - if: github.ref == 'refs/heads/main' uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 with: path: coverage deploy-coverage: - if: github.ref == 'refs/heads/main' - needs: ci + needs: coverage runs-on: ubuntu-latest environment: name: github-pages diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index ceac526..9fb4bfe 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -9,7 +9,15 @@ "node": true, }, // Ignore patterns - "ignorePatterns": ["dist", "node_modules", ".claude", "tmp", "*.log", "/nix/store/**"], + "ignorePatterns": [ + "dist", + "node_modules", + ".claude", + "tmp", + "*.log", + "/nix/store/**", + "CHANGELOG.md", + ], // Rule categories - strict base configuration // - correctness (196 rules): Catch definite bugs and errors // - suspicious (47 rules): Flag potentially problematic patterns diff --git a/README.md b/README.md index 6c42758..b141736 100644 --- a/README.md +++ b/README.md @@ -458,7 +458,7 @@ You can use the `dryRun` option to return the api arguments from a tool call wit ```typescript import { StackOneToolSet } from "@stackone/ai"; -// Initialise the toolset +// Initialize the toolset const toolset = new StackOneToolSet({ baseUrl: "https://api.stackone.com", }); diff --git a/examples/ai-sdk-integration.ts b/examples/ai-sdk-integration.ts index d39b2dc..c44aabc 100644 --- a/examples/ai-sdk-integration.ts +++ b/examples/ai-sdk-integration.ts @@ -26,7 +26,7 @@ if (!apiKey) { const accountId = 'your-bamboohr-account-id'; const aiSdkIntegration = async (): Promise => { - // Initialise StackOne + // Initialize StackOne const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', diff --git a/examples/anthropic-integration.ts b/examples/anthropic-integration.ts index f92dc30..4003fc2 100644 --- a/examples/anthropic-integration.ts +++ b/examples/anthropic-integration.ts @@ -17,7 +17,7 @@ if (!apiKey) { const accountId = 'your-hris-account-id'; const anthropicIntegration = async (): Promise => { - // Initialise StackOne + // Initialize StackOne const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', @@ -29,7 +29,7 @@ const anthropicIntegration = async (): Promise => { }); const anthropicTools = tools.toAnthropic(); - // Initialise Anthropic client + // Initialize Anthropic client const anthropic = new Anthropic(); // Create a message with tool calls diff --git a/examples/claude-agent-sdk-integration.ts b/examples/claude-agent-sdk-integration.ts index 6057dc2..5008f78 100644 --- a/examples/claude-agent-sdk-integration.ts +++ b/examples/claude-agent-sdk-integration.ts @@ -21,7 +21,7 @@ if (!apiKey) { const accountId = 'your-bamboohr-account-id'; const claudeAgentSdkIntegration = async (): Promise => { - // Initialise StackOne + // Initialize StackOne const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', diff --git a/examples/fetch-tools-debug.ts b/examples/fetch-tools-debug.ts index f595e67..5195771 100644 --- a/examples/fetch-tools-debug.ts +++ b/examples/fetch-tools-debug.ts @@ -203,7 +203,7 @@ if ((typeof globalThis.Bun as any) !== 'undefined') { } const spinner = clack.spinner(); -spinner.start('Initialising StackOne client...'); +spinner.start('Initializing StackOne client...'); const toolset = new StackOneToolSet({ apiKey, diff --git a/examples/meta-tools.ts b/examples/meta-tools.ts index 42f964c..bcfb5fc 100644 --- a/examples/meta-tools.ts +++ b/examples/meta-tools.ts @@ -26,7 +26,7 @@ const accountId = 'your-bamboohr-account-id'; const metaToolsWithAISDK = async (): Promise => { console.log('šŸ” Example 1: Dynamic tool discovery with AI SDK\n'); - // Initialise StackOne toolset + // Initialize StackOne toolset const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', @@ -64,7 +64,7 @@ const metaToolsWithOpenAI = async (): Promise => { apiKey: process.env.OPENAI_API_KEY, }); - // Initialise StackOne toolset + // Initialize StackOne toolset const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', @@ -118,7 +118,7 @@ const metaToolsWithOpenAI = async (): Promise => { const directMetaToolUsage = async (): Promise => { console.log('\nšŸ› ļø Example 3: Direct meta tool usage\n'); - // Initialise toolset + // Initialize toolset const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', diff --git a/examples/openai-integration.test.ts b/examples/openai-integration.test.ts index fb59e1f..2eb93f9 100644 --- a/examples/openai-integration.test.ts +++ b/examples/openai-integration.test.ts @@ -33,7 +33,7 @@ describe('openai-integration example e2e', () => { expect(openAITools[0]).toHaveProperty('type', 'function'); expect(openAITools[0]).toHaveProperty('function'); - // Initialise OpenAI client + // Initialize OpenAI client const openai = new OpenAI(); // Create a chat completion with tool calls diff --git a/examples/openai-integration.ts b/examples/openai-integration.ts index 0a6fcdb..ed054c9 100644 --- a/examples/openai-integration.ts +++ b/examples/openai-integration.ts @@ -17,7 +17,7 @@ if (!apiKey) { const accountId = 'your-bamboohr-account-id'; const openaiIntegration = async (): Promise => { - // Initialise StackOne + // Initialize StackOne const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', @@ -27,7 +27,7 @@ const openaiIntegration = async (): Promise => { const tools = await toolset.fetchTools(); const openAITools = tools.toOpenAI(); - // Initialise OpenAI client + // Initialize OpenAI client const openai = new OpenAI(); // Create a chat completion with tool calls diff --git a/examples/openai-responses-integration.test.ts b/examples/openai-responses-integration.test.ts index 77269d5..f0bc286 100644 --- a/examples/openai-responses-integration.test.ts +++ b/examples/openai-responses-integration.test.ts @@ -32,7 +32,7 @@ describe('openai-responses-integration example e2e', () => { expect(Array.isArray(openAIResponsesTools)).toBe(true); expect(openAIResponsesTools.length).toBeGreaterThan(0); - // Initialise OpenAI client + // Initialize OpenAI client const openai = new OpenAI(); // Create a response with tool calls using the Responses API diff --git a/examples/openai-responses-integration.ts b/examples/openai-responses-integration.ts index 8dba1e6..08b5e4a 100644 --- a/examples/openai-responses-integration.ts +++ b/examples/openai-responses-integration.ts @@ -17,7 +17,7 @@ if (!apiKey) { const accountId = 'your-stackone-account-id'; const openaiResponsesIntegration = async (): Promise => { - // Initialise StackOne + // Initialize StackOne const toolset = new StackOneToolSet({ accountId }); // Fetch tools via MCP @@ -26,7 +26,7 @@ const openaiResponsesIntegration = async (): Promise => { }); const openAIResponsesTools = tools.toOpenAIResponses(); - // Initialise OpenAI client + // Initialize OpenAI client const openai = new OpenAI(); // Create a response with tool calls using the Responses API diff --git a/examples/tanstack-ai-integration.ts b/examples/tanstack-ai-integration.ts index fc20358..b95664e 100644 --- a/examples/tanstack-ai-integration.ts +++ b/examples/tanstack-ai-integration.ts @@ -23,7 +23,7 @@ if (!apiKey) { const accountId = 'your-bamboohr-account-id'; const tanstackAiIntegration = async (): Promise => { - // Initialise StackOne + // Initialize StackOne const toolset = new StackOneToolSet({ accountId, baseUrl: process.env.STACKONE_BASE_URL ?? 'https://api.stackone.com', diff --git a/flake.nix b/flake.nix index 687f23f..e953f20 100644 --- a/flake.nix +++ b/flake.nix @@ -20,9 +20,15 @@ { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ + # runtime nodejs_24 pnpm_10 + + # formatting and linting tools + similarity nixfmt-rfc-style + typos + typos-lsp ]; shellHook = '' diff --git a/lefthook.yaml b/lefthook.yaml index 30a7558..21bf81b 100644 --- a/lefthook.yaml +++ b/lefthook.yaml @@ -16,5 +16,7 @@ pre-commit: pre-push: jobs: + - name: typos + run: typos --config typos.toml - name: knip run: pnpm run lint:knip diff --git a/src/consts.ts b/src/consts.ts index a02454c..11714fa 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -12,10 +12,10 @@ export const DEFAULT_BASE_URL = 'https://api.stackone.com'; * Default weight for BM25 in hybrid BM25 + TF-IDF search. * * - alpha=0.2 means: 20% BM25 + 80% TF-IDF - * - This value was optimised through validation testing + * - This value was optimized through validation testing * - Provides 10.8% improvement in tool discovery accuracy - * - Lower values favour BM25 scoring (better keyword matching) - * - Higher values favour TF-IDF scoring (better semantic matching) + * - Lower values favor BM25 scoring (better keyword matching) + * - Higher values favor TF-IDF scoring (better semantic matching) */ export const DEFAULT_HYBRID_ALPHA = 0.2; diff --git a/src/headers.test.ts b/src/headers.test.ts index 6a59343..3bb4f09 100644 --- a/src/headers.test.ts +++ b/src/headers.test.ts @@ -1,56 +1,56 @@ -import { normaliseHeaders } from './headers'; +import { normalizeHeaders } from './headers'; -describe('normaliseHeaders', () => { +describe('normalizeHeaders', () => { it('returns empty object for undefined input', () => { - expect(normaliseHeaders(undefined)).toEqual({}); + expect(normalizeHeaders(undefined)).toEqual({}); }); it('returns empty object for empty input', () => { - expect(normaliseHeaders({})).toEqual({}); + expect(normalizeHeaders({})).toEqual({}); }); it('preserves string values', () => { - expect(normaliseHeaders({ foo: 'bar', baz: 'qux' })).toEqual({ + expect(normalizeHeaders({ foo: 'bar', baz: 'qux' })).toEqual({ foo: 'bar', baz: 'qux', }); }); it('converts numbers to strings', () => { - expect(normaliseHeaders({ port: 8080, timeout: 30 })).toEqual({ + expect(normalizeHeaders({ port: 8080, timeout: 30 })).toEqual({ port: '8080', timeout: '30', }); }); it('converts booleans to strings', () => { - expect(normaliseHeaders({ enabled: true, debug: false })).toEqual({ + expect(normalizeHeaders({ enabled: true, debug: false })).toEqual({ enabled: 'true', debug: 'false', }); }); - it('serialises objects to JSON', () => { - expect(normaliseHeaders({ config: { key: 'value' } })).toEqual({ + it('serializes objects to JSON', () => { + expect(normalizeHeaders({ config: { key: 'value' } })).toEqual({ config: '{"key":"value"}', }); }); - it('serialises arrays to JSON', () => { - expect(normaliseHeaders({ tags: ['foo', 'bar'] })).toEqual({ + it('serializes arrays to JSON', () => { + expect(normalizeHeaders({ tags: ['foo', 'bar'] })).toEqual({ tags: '["foo","bar"]', }); }); it('skips null values', () => { - expect(normaliseHeaders({ foo: 'bar', baz: null })).toEqual({ + expect(normalizeHeaders({ foo: 'bar', baz: null })).toEqual({ foo: 'bar', }); }); it('handles mixed value types', () => { expect( - normaliseHeaders({ + normalizeHeaders({ string: 'text', number: 42, boolean: true, diff --git a/src/headers.ts b/src/headers.ts index 7e412f3..f30b474 100644 --- a/src/headers.ts +++ b/src/headers.ts @@ -18,13 +18,13 @@ export const stackOneHeadersSchema = z.record(z.string(), z.string()).brand<'Sta export type StackOneHeaders = z.infer; /** - * Normalises header values from JsonObject to StackOneHeaders (branded type) - * Converts numbers and booleans to strings, and serialises objects to JSON + * Normalizes header values from JsonObject to StackOneHeaders (branded type) + * Converts numbers and booleans to strings, and serializes objects to JSON * * @param headers - Headers object with JSON value types - * @returns Normalised headers with string values only (branded type) + * @returns Normalized headers with string values only (branded type) */ -export function normaliseHeaders(headers: JsonObject | undefined): StackOneHeaders { +export function normalizeHeaders(headers: JsonObject | undefined): StackOneHeaders { if (!headers) return stackOneHeadersSchema.parse({}); const result: Record = {}; for (const [key, value] of Object.entries(headers)) { diff --git a/src/requestBuilder.test.ts b/src/requestBuilder.test.ts index 028937e..258b2ec 100644 --- a/src/requestBuilder.test.ts +++ b/src/requestBuilder.test.ts @@ -62,7 +62,7 @@ describe('RequestBuilder', () => { server.events.removeAllListeners('request:start'); }); - it('should initialise with headers from constructor', () => { + it('should initialize with headers from constructor', () => { expect(builder.getHeaders()).toEqual({ 'Initial-Header': 'test' }); }); @@ -325,7 +325,7 @@ describe('RequestBuilder', () => { }); it('should throw error when circular reference is detected', async () => { - // Test runtime behaviour when circular reference is passed + // Test runtime behavior when circular reference is passed // Note: This tests error handling for malformed input at runtime const inner: Record = { b: 'test' }; const circular: Record = { a: inner }; @@ -356,7 +356,7 @@ describe('RequestBuilder', () => { }); it('should handle special types correctly at runtime', async () => { - // Test runtime behaviour when non-JSON types are passed + // Test runtime behavior when non-JSON types are passed // Note: Date and RegExp are not valid JsonValue types, but we test // the serialiser's runtime handling of these edge cases const testDate = new Date('2023-01-01T00:00:00.000Z'); @@ -370,7 +370,7 @@ describe('RequestBuilder', () => { nullField: null, emptyString: '', }, - } as unknown as JsonObject; // Cast to test runtime serialisation + } as unknown as JsonObject; // Cast to test runtime serialization const result = await builder.execute(params, { dryRun: true }); const url = new URL(result.url as string); @@ -444,7 +444,7 @@ describe('RequestBuilder', () => { }); it('should handle nested objects with special types at runtime', async () => { - // Test runtime serialisation of nested non-JSON types + // Test runtime serialization of nested non-JSON types const params = { pathParam: 'test-value', filter: { @@ -456,7 +456,7 @@ describe('RequestBuilder', () => { }, }, }, - } as unknown as JsonObject; // Cast to test runtime serialisation + } as unknown as JsonObject; // Cast to test runtime serialization const result = await builder.execute(params, { dryRun: true }); const url = new URL(result.url as string); diff --git a/src/requestBuilder.ts b/src/requestBuilder.ts index 2549a09..61bcf11 100644 --- a/src/requestBuilder.ts +++ b/src/requestBuilder.ts @@ -332,7 +332,7 @@ export class RequestBuilder { // If dryRun is true, return the request details instead of making the API call if (options?.dryRun) { - // Convert headers to a plain object for JSON serialisation + // Convert headers to a plain object for JSON serialization const headersObj = fetchOptions.headers instanceof Headers ? Object.fromEntries(fetchOptions.headers.entries()) diff --git a/src/toolsets.test.ts b/src/toolsets.test.ts index 735083d..d01a141 100644 --- a/src/toolsets.test.ts +++ b/src/toolsets.test.ts @@ -1,6 +1,6 @@ /** * StackOneToolSet tests - comprehensive test suite covering: - * - Initialisation and configuration + * - Initialization and configuration * - Authentication (basic, bearer) * - Glob and filter matching * - MCP fetch integration @@ -21,8 +21,8 @@ describe('StackOneToolSet', () => { vi.unstubAllEnvs(); }); - describe('initialisation', () => { - it('should initialise with API key from constructor', () => { + describe('initialization', () => { + it('should initialize with API key from constructor', () => { const toolset = new StackOneToolSet({ apiKey: 'custom_key' }); expect(toolset).toBeDefined(); @@ -30,7 +30,7 @@ describe('StackOneToolSet', () => { expect(toolset.authentication?.credentials?.username).toBe('custom_key'); }); - it('should initialise with API key from environment', () => { + it('should initialize with API key from environment', () => { const toolset = new StackOneToolSet(); expect(toolset).toBeDefined(); @@ -38,7 +38,7 @@ describe('StackOneToolSet', () => { expect(toolset.authentication?.credentials?.username).toBe('test_key'); }); - it('should initialise with custom values', () => { + it('should initialize with custom values', () => { const baseUrl = 'https://api.example.com'; const headers = { 'X-Custom-Header': 'test' }; @@ -83,7 +83,7 @@ describe('StackOneToolSet', () => { expect(toolset.accountIds).toEqual(['account-1', 'account-2']); }); - it('should initialise with multiple account IDs from constructor', () => { + it('should initialize with multiple account IDs from constructor', () => { const toolset = new StackOneToolSet({ apiKey: 'custom_key', accountIds: ['account-1', 'account-2', 'account-3'], @@ -93,7 +93,7 @@ describe('StackOneToolSet', () => { expect(toolset.accountIds).toEqual(['account-1', 'account-2', 'account-3']); }); - it('should initialise with empty accountIds array when not provided', () => { + it('should initialize with empty accountIds array when not provided', () => { const toolset = new StackOneToolSet({ apiKey: 'custom_key' }); // @ts-expect-error - Accessing private property for testing diff --git a/src/toolsets.ts b/src/toolsets.ts index ad31de4..03df7a4 100644 --- a/src/toolsets.ts +++ b/src/toolsets.ts @@ -2,7 +2,7 @@ import { defu } from 'defu'; import type { MergeExclusive, SimplifyDeep } from 'type-fest'; import { DEFAULT_BASE_URL, UNIFIED_API_PREFIX } from './consts'; import { createFeedbackTool } from './feedback'; -import { type StackOneHeaders, normaliseHeaders, stackOneHeadersSchema } from './headers'; +import { type StackOneHeaders, normalizeHeaders, stackOneHeadersSchema } from './headers'; import { createMCPClient } from './mcp-client'; import { type RpcActionResponse, RpcClient } from './rpc-client'; import { BaseTool, Tools } from './tool'; @@ -171,7 +171,7 @@ export class StackOneToolSet { private accountIds: string[] = []; /** - * Initialise StackOne toolset with API key and optional account ID(s) + * Initialize StackOne toolset with API key and optional account ID(s) * @param config Configuration object containing API key and optional account ID(s) */ constructor(config?: StackOneToolSetConfig) { @@ -211,7 +211,7 @@ export class StackOneToolSet { ...(accountId ? { 'x-account-id': accountId } : {}), }; - // Initialise base properties + // Initialize base properties this.baseUrl = config?.baseUrl ?? process.env.STACKONE_BASE_URL ?? DEFAULT_BASE_URL; this.authentication = authentication; this.headers = configHeaders; @@ -499,7 +499,7 @@ export class StackOneToolSet { const pathParams = this.extractRecord(parsedParams, 'path'); const queryParams = this.extractRecord(parsedParams, 'query'); const additionalHeaders = this.extractRecord(parsedParams, 'headers'); - const extraHeaders = normaliseHeaders(additionalHeaders); + const extraHeaders = normalizeHeaders(additionalHeaders); // defu merges extraHeaders into baseHeaders, both are already branded types const actionHeaders = defu(extraHeaders, baseHeaders) as StackOneHeaders; diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000..7882ea9 --- /dev/null +++ b/typos.toml @@ -0,0 +1,17 @@ +[default] +locale = 'en-us' +extend-ignore-re = [ + "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", + "(?s)", + "(?Rm)^.*#\\s*spellchecker:disable-line$", + "(?m)^.*\\n.*$" +] + +[files] +extend-exclude = [ + "node_modules", + "package.json", + "pnpm-lock.yaml", + "pnpm-workspace.yaml", + "CHANGELOG.md", +]