mirror of https://github.com/buster-so/buster.git
sdk and ts cli
This commit is contained in:
parent
90047a59f1
commit
3b585d09dd
|
@ -10,6 +10,7 @@
|
|||
{ "path": "../packages/data-source" },
|
||||
{ "path": "../packages/database" },
|
||||
{ "path": "../packages/rerank" },
|
||||
{ "path": "../packages/sdk" },
|
||||
{ "path": "../packages/server-shared" },
|
||||
{ "path": "../packages/slack" },
|
||||
{ "path": "../packages/stored-values" },
|
||||
|
|
28
CLAUDE.md
28
CLAUDE.md
|
@ -24,7 +24,7 @@ This is a pnpm-based monorepo using Turborepo with the following structure:
|
|||
- `apps/trigger` - Background job processing with Trigger.dev v3
|
||||
- `apps/electric-server` - Electric SQL sync server
|
||||
- `apps/api` - Rust backend API (legacy)
|
||||
- `apps/cli` - Command-line tools (Rust)
|
||||
- `apps/cli` - Command-line tools (TypeScript)
|
||||
|
||||
### Packages (`@buster/*`)
|
||||
- `packages/ai` - AI agents, tools, and workflows
|
||||
|
@ -34,6 +34,7 @@ This is a pnpm-based monorepo using Turborepo with the following structure:
|
|||
- `packages/stored-values` - Stored values management
|
||||
- `packages/rerank` - Document reranking functionality
|
||||
- `packages/server-shared` - Shared server types and utilities
|
||||
- `packages/sdk` - TypeScript SDK for Buster API (functional programming patterns)
|
||||
- `packages/test-utils` - Shared testing utilities
|
||||
- `packages/vitest-config` - Shared Vitest configuration
|
||||
- `packages/typescript-config` - Shared TypeScript configuration
|
||||
|
@ -81,6 +82,31 @@ When writing code, follow this workflow to ensure code quality:
|
|||
- Keep business logic separate from infrastructure concerns
|
||||
- Use proper error handling at each level
|
||||
|
||||
## Core Development Principles
|
||||
|
||||
### Functional Programming First - No Classes
|
||||
- **Pure functions only** - Commands and handlers are functions that accept input and return output
|
||||
- **Composable modules** - Build features by composing small, focused functions
|
||||
- **Immutable data** - Never mutate; always create new data structures
|
||||
- **Pattern from analyst-agent** - Follow the structure in `@packages/ai/src/agents/analyst-agent/analyst-agent.ts`
|
||||
- **Avoid OOP** - No classes for business logic, no inheritance, no `this` keyword
|
||||
- **Higher-order functions** - Use functions that return configured functions for dependency injection
|
||||
|
||||
### Type Safety First - Zod Everything
|
||||
- **Zod schemas are the source of truth** - Define ALL data structures as Zod schemas first
|
||||
- **Export types from schemas** - Always use `z.infer<typeof Schema>` for TypeScript types
|
||||
- **Runtime validation everywhere** - Use `.parse()` for trusted data, `.safeParse()` for user input
|
||||
- **No implicit any** - Every variable, parameter, and return type must be explicitly typed
|
||||
- **Validate at boundaries** - All user input, API responses, and file reads must be validated
|
||||
- **Leverage shared types** - Import from `@buster/server-shared` for consistency
|
||||
|
||||
### Module Organization
|
||||
- **Small files** - Each file should have a single, clear responsibility
|
||||
- **Colocate tests** - Keep `.test.ts` (unit) and `.int.test.ts` (integration) next to implementation
|
||||
- **Explicit exports** - Use named exports and create comprehensive index.ts files
|
||||
- **Deep nesting is OK** - Organize into logical subdirectories for clarity
|
||||
- **Functional module pattern** - Export factory functions that return configured function sets
|
||||
|
||||
### 3. Commit Code Frequently
|
||||
- **Commit frequently** - Make local commits often as you complete logical chunks of work
|
||||
- **Write descriptive commit messages** - Focus on what and why, not just what changed
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
# TypeScript build artifacts
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
pnpm-debug.log*
|
||||
pnpm-error.log*
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
*.lcov
|
||||
.nyc_output
|
||||
|
||||
# Node modules
|
||||
node_modules/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Environment files (SDK doesn't use env, but just in case)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Test artifacts
|
||||
junit.xml
|
||||
test-results/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*.swn
|
||||
*.bak
|
||||
|
||||
# Package manager files
|
||||
.pnpm-debug.log*
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
|
@ -0,0 +1,53 @@
|
|||
# @buster/sdk
|
||||
|
||||
Minimal TypeScript SDK for the Buster API.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add @buster/sdk
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { createBusterSDK } from '@buster/sdk';
|
||||
|
||||
const sdk = createBusterSDK({
|
||||
apiKey: 'your-api-key',
|
||||
apiUrl: 'https://api.buster.so', // optional
|
||||
});
|
||||
|
||||
// Test connection
|
||||
const health = await sdk.healthcheck();
|
||||
console.log('Server status:', health.status);
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
```typescript
|
||||
interface SDKConfig {
|
||||
apiKey: string; // Required
|
||||
apiUrl?: string; // Default: 'https://api.buster.so'
|
||||
timeout?: number; // Default: 30000ms
|
||||
retryAttempts?: number; // Default: 3
|
||||
retryDelay?: number; // Default: 1000ms
|
||||
headers?: Record<string, string>; // Optional custom headers
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { SDKError, NetworkError } from '@buster/sdk';
|
||||
|
||||
try {
|
||||
await sdk.healthcheck();
|
||||
} catch (error) {
|
||||
if (error instanceof NetworkError) {
|
||||
console.error('Connection failed:', error.message);
|
||||
} else if (error instanceof SDKError) {
|
||||
console.error('API error:', error.statusCode, error.message);
|
||||
}
|
||||
}
|
||||
```
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"extends": ["../../biome.json"],
|
||||
"files": {
|
||||
"include": ["src/**/*", "scripts/**/*"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
// SDK doesn't require any environment variables by default
|
||||
// Client applications will provide configuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@buster/sdk",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/*.d.ts",
|
||||
"default": "./dist/*.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "[ \"$SKIP_ENV_CHECK\" = \"true\" ] || tsx scripts/validate-env.ts",
|
||||
"build": "tsc",
|
||||
"build:dry-run": "tsc",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsc --watch",
|
||||
"lint": "biome check --write",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --exclude '**/*.int.test.ts' --exclude '**/*.integration.test.ts' --passWithNoTests",
|
||||
"test:integration": "vitest run **/*.int.test.ts **/*.integration.test.ts",
|
||||
"test:watch": "vitest watch",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@buster/typescript-config": "workspace:*",
|
||||
"@buster/vitest-config": "workspace:*",
|
||||
"@buster/env-utils": "workspace:*",
|
||||
"@buster/server-shared": "workspace:*",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.2.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// This script uses the shared env-utils to validate environment variables
|
||||
import { loadRootEnv, validateEnv } from '@buster/env-utils';
|
||||
|
||||
// Load environment variables from root .env file
|
||||
loadRootEnv();
|
||||
|
||||
// SDK doesn't require any environment variables by default
|
||||
// Client applications will provide configuration
|
||||
const requiredEnv = {
|
||||
// No required env vars for SDK - configuration comes from consumers
|
||||
};
|
||||
|
||||
// Validate environment variables
|
||||
const { hasErrors } = validateEnv(requiredEnv);
|
||||
|
||||
if (hasErrors) {
|
||||
process.exit(1);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './lib';
|
|
@ -0,0 +1,19 @@
|
|||
import { type SDKConfig, SDKConfigSchema } from '../config';
|
||||
import { get } from '../http';
|
||||
|
||||
// SDK instance interface
|
||||
export interface BusterSDK {
|
||||
readonly config: SDKConfig;
|
||||
healthcheck: () => Promise<{ status: string; [key: string]: unknown }>;
|
||||
}
|
||||
|
||||
// Create SDK instance
|
||||
export function createBusterSDK(config: Partial<SDKConfig>): BusterSDK {
|
||||
// Validate config with Zod
|
||||
const validatedConfig = SDKConfigSchema.parse(config);
|
||||
|
||||
return {
|
||||
config: validatedConfig,
|
||||
healthcheck: () => get(validatedConfig, '/api/healthcheck'),
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './buster-sdk';
|
|
@ -0,0 +1,13 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// SDK Configuration Schema
|
||||
export const SDKConfigSchema = z.object({
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
apiUrl: z.string().url().default('https://api.buster.so'),
|
||||
timeout: z.number().min(1000).max(60000).default(30000),
|
||||
retryAttempts: z.number().min(0).max(5).default(3),
|
||||
retryDelay: z.number().min(100).max(5000).default(1000),
|
||||
headers: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type SDKConfig = z.infer<typeof SDKConfigSchema>;
|
|
@ -0,0 +1 @@
|
|||
export * from './config-schema';
|
|
@ -0,0 +1 @@
|
|||
export * from './sdk-errors';
|
|
@ -0,0 +1,19 @@
|
|||
// Base SDK Error class
|
||||
export class SDKError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly statusCode?: number,
|
||||
public readonly code?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SDKError';
|
||||
}
|
||||
}
|
||||
|
||||
// Network/connection errors
|
||||
export class NetworkError extends SDKError {
|
||||
constructor(message = 'Network request failed') {
|
||||
super(message, undefined, 'NETWORK_ERROR');
|
||||
this.name = 'NetworkError';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import type { SDKConfig } from '../config';
|
||||
import { NetworkError, SDKError } from '../errors';
|
||||
|
||||
// Build URL with query params
|
||||
export function buildUrl(baseUrl: string, path: string, params?: Record<string, string>): string {
|
||||
const url = new URL(path, baseUrl);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Build request headers
|
||||
function buildHeaders(config: SDKConfig): Record<string, string> {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${config.apiKey}`,
|
||||
...config.headers,
|
||||
};
|
||||
}
|
||||
|
||||
// Make HTTP request with retries
|
||||
async function fetchWithRetry(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
retryCount = 3,
|
||||
retryDelay = 1000
|
||||
): Promise<Response> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let i = 0; i < retryCount; i++) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
// Don't retry on client errors (4xx) except 429
|
||||
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Return successful responses
|
||||
if (response.ok) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Save error for potential retry
|
||||
lastError = new SDKError(`HTTP ${response.status}`, response.status);
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
}
|
||||
|
||||
// Wait before retry (exponential backoff)
|
||||
if (i < retryCount - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, retryDelay * 2 ** i));
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new NetworkError();
|
||||
}
|
||||
|
||||
// Main request function
|
||||
export async function request<T = unknown>(
|
||||
config: SDKConfig,
|
||||
method: string,
|
||||
path: string,
|
||||
options?: {
|
||||
body?: unknown;
|
||||
params?: Record<string, string>;
|
||||
}
|
||||
): Promise<T> {
|
||||
const url = buildUrl(config.apiUrl, path, options?.params);
|
||||
|
||||
const requestOptions: RequestInit = {
|
||||
method,
|
||||
headers: buildHeaders(config),
|
||||
signal: AbortSignal.timeout(config.timeout),
|
||||
};
|
||||
|
||||
if (options?.body !== undefined && method !== 'GET') {
|
||||
requestOptions.body = JSON.stringify(options.body);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithRetry(
|
||||
url,
|
||||
requestOptions,
|
||||
config.retryAttempts,
|
||||
config.retryDelay
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new SDKError(`Request failed: ${response.statusText}`, response.status);
|
||||
}
|
||||
|
||||
// Handle empty responses
|
||||
if (response.status === 204 || response.headers.get('content-length') === '0') {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
if (error instanceof SDKError) {
|
||||
throw error;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
throw new NetworkError('Request timeout');
|
||||
}
|
||||
throw new NetworkError(error.message);
|
||||
}
|
||||
throw new NetworkError();
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods
|
||||
export const get = <T>(config: SDKConfig, path: string, params?: Record<string, string>) =>
|
||||
request<T>(config, 'GET', path, params ? { params } : undefined);
|
||||
|
||||
export const post = <T>(config: SDKConfig, path: string, body?: unknown) =>
|
||||
request<T>(config, 'POST', path, { body });
|
||||
|
||||
export const put = <T>(config: SDKConfig, path: string, body?: unknown) =>
|
||||
request<T>(config, 'PUT', path, { body });
|
||||
|
||||
export const del = <T>(config: SDKConfig, path: string) => request<T>(config, 'DELETE', path);
|
|
@ -0,0 +1 @@
|
|||
export * from './http-client';
|
|
@ -0,0 +1,9 @@
|
|||
// Main SDK export
|
||||
export { createBusterSDK } from './client';
|
||||
export type { BusterSDK } from './client';
|
||||
|
||||
// Config types
|
||||
export type { SDKConfig } from './config';
|
||||
|
||||
// Error types
|
||||
export { SDKError, NetworkError } from './errors';
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "@buster/typescript-config/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "dist/.cache/tsbuildinfo.json",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*", "env.d.ts"],
|
||||
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { baseConfig } from '@buster/vitest-config';
|
||||
|
||||
export default baseConfig;
|
566
pnpm-lock.yaml
566
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue