From 027592fc8d1b4f26eccb72583b9aceb238355058 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Oct 2026 14:23:55 +0000 Subject: [PATCH] [EPIC-D][LOU-D36] Remove unused SaaS-era utilities (quotas, ConfigManager, formatters, json-path, file-extractor) Delete modules nothing in the SDK, Agent Forge, the scaffolder or examples used, with their tests, index re-exports and the orphaned SaaS quota types. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_016bY29ibQgv3MDdjAbUT4YR --- CHANGELOG.md | 5 + src/core/ConfigManager.test.ts | 53 --------- src/core/ConfigManager.ts | 64 ----------- src/core/index.ts | 1 - src/security/index.ts | 5 +- src/security/quotas.test.ts | 132 ----------------------- src/security/quotas.ts | 68 ------------ src/security/types.ts | 35 ------ src/utils/file-extractor.test.ts | 113 ------------------- src/utils/file-extractor.ts | 91 ---------------- src/utils/formatters.test.ts | 97 ----------------- src/utils/formatters.ts | 179 ------------------------------- src/utils/index.ts | 9 -- src/utils/json-path.test.ts | 84 --------------- src/utils/json-path.ts | 75 ------------- 15 files changed, 6 insertions(+), 1005 deletions(-) delete mode 100644 src/core/ConfigManager.test.ts delete mode 100644 src/core/ConfigManager.ts delete mode 100644 src/security/quotas.test.ts delete mode 100644 src/security/quotas.ts delete mode 100644 src/utils/file-extractor.test.ts delete mode 100644 src/utils/file-extractor.ts delete mode 100644 src/utils/formatters.test.ts delete mode 100644 src/utils/formatters.ts delete mode 100644 src/utils/json-path.test.ts delete mode 100644 src/utils/json-path.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f5719e15f..ae21ef9ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Tools own their contract (LOU-D22): `ToolDescriptor` gains optional `inputSchema` (zod) and `execute(args, ctx)`, which are now the canonical fields. `defineTool()` sets both and no longer calls `ai`'s `tool()`. Argument validation, the schema sent to the model, and tool execution read them first and fall back to `tool.parameters` / `tool.execute`. `ToolDescriptor.tool` (the `ai` v4 `Tool`) is now legacy: it is still built by `defineTool()` and still accepted on hand-written descriptors this release, but new code should set `inputSchema` and `execute`. ### Removed +- BREAKING: `validateTokenQuotas()` (`src/security/quotas.ts`) and the SaaS-era types `SaaSContext`, `QuotaConfig`, `UsageStats` and `QuotaValidationResult` (LOU-D36) no longer ship from the package root. Nothing in the SDK called them. No replacement; copy the file into your project if you relied on it. +- BREAKING: `ConfigManager` and `SDKConfig` (`src/core/ConfigManager.ts`) (LOU-D36) no longer ship from the package root or `./core` (which still exports `AgentBuilder`). Nothing in the SDK used them. No replacement; copy the file into your project if you relied on it. +- BREAKING: The formatters (LOU-D36) `getCurrentTS()`, `getTS()`, `formatDate()`, `safeJsonParse()`, `removeCodeBlocks()`, `findCodeBlocks()`, `checkApiKey()` and the types `CodeBlock`, `CodeBlockError`, `CodeBlockResult` (`src/utils/formatters.ts`) no longer ship from the package root. No replacement; copy the file into your project if you relied on it. +- BREAKING: The JSON path helpers `getObjectByPath()` and `setRecursiveNames()` (`src/utils/json-path.ts`) (LOU-D36) no longer ship from the package root. No replacement; copy the file into your project if you relied on it. +- BREAKING: The file extraction helpers `getMimeType()`, `getFileExtensionFromMimeType()`, `replaceBase64Content()`, `isBinaryData()`, `extractBase64Data()`, `createDataUri()` and `ProcessFilesParams` (`src/utils/file-extractor.ts`) (LOU-D36) no longer ship from the package root. No replacement; copy the file into your project if you relied on it. - BREAKING: The flows-ai converters (LOU-D35) are gone: `convertToFlowDefinition()` and `convertFromFlowDefinition()` (`src/flows/converters.ts`) no longer ship from the package root or `./flows`. There is no replacement: the flows-ai engine they targeted was never a dependency of this package and `FlowExecutor` never used them. Run flows with `FlowExecutor` directly. - `nanoid` is no longer a dependency (LOU-D35). Generated ids (agent, run, trace, memory ids) now come from `globalThis.crypto.randomUUID()` via an internal `newId()`, so they are UUIDs instead of 21-character nanoids. Nothing public depended on the old format. - `finishReason: 'max-steps'` (LOU-U19): a run that exhausts `maxSteps` while the model still wanted to continue (last turn ended in tool calls) now resolves with `finishReason: 'max-steps'` instead of the stale last-turn reason (usually `'tool_calls'`), on `ExecutionResult`, the `finish` event and `run.done`. Resumed runs count `initialSteps` toward the budget. A run that finishes naturally within the budget is unchanged. Code that treated `'tool_calls'` as "hit maxSteps" should check `'max-steps'` instead (the eval `completed()` message and the MCP server's agent tool do). diff --git a/src/core/ConfigManager.test.ts b/src/core/ConfigManager.test.ts deleted file mode 100644 index cf2b1ce40..000000000 --- a/src/core/ConfigManager.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { ConfigManager } from './ConfigManager'; - -describe('ConfigManager', () => { - it('should create config manager with valid config', () => { - const config = { - databaseIdHash: 'test-db-hash' - }; - - const manager = new ConfigManager(config); - expect(manager.getConfig()).toEqual(config); - }); - - it('should throw error when databaseIdHash is missing', () => { - expect(() => { - new ConfigManager({} as any); - }).toThrow('databaseIdHash is required'); - }); - - it('should get specific config value', () => { - const config = { - databaseIdHash: 'test-db-hash', - storageKey: 'test-storage' - }; - - const manager = new ConfigManager(config); - expect(manager.get('storageKey')).toBe('test-storage'); - }); - - it('should update config', () => { - const manager = new ConfigManager({ - databaseIdHash: 'test-db-hash' - }); - - manager.updateConfig({ - storageKey: 'new-storage' - }); - - expect(manager.get('storageKey')).toBe('new-storage'); - }); - - it('should validate on update', () => { - const manager = new ConfigManager({ - databaseIdHash: 'test-db-hash' - }); - - expect(() => { - manager.updateConfig({ - databaseIdHash: '' - }); - }).toThrow('databaseIdHash is required'); - }); -}); diff --git a/src/core/ConfigManager.ts b/src/core/ConfigManager.ts deleted file mode 100644 index 7df2deccc..000000000 --- a/src/core/ConfigManager.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * SDK Configuration - */ -export interface SDKConfig { - databaseIdHash: string; - storageKey?: string; - llm?: { - provider: 'openai' | 'ollama' | string; - apiKey?: string; - model?: string; - baseUrl?: string; - }; - storage?: { - provider: string; - options?: Record; - }; - security?: { - encryption?: boolean; - encryptionKey?: string; - }; -} - -/** - * Configuration manager for SDK - */ -export class ConfigManager { - private config: SDKConfig; - - constructor(config: SDKConfig) { - this.validate(config); - this.config = config; - } - - /** - * Get configuration - */ - public getConfig(): SDKConfig { - return { ...this.config }; - } - - /** - * Update configuration - */ - public updateConfig(updates: Partial): void { - this.config = { ...this.config, ...updates }; - this.validate(this.config); - } - - /** - * Get specific config value - */ - public get(key: K): SDKConfig[K] { - return this.config[key]; - } - - /** - * Validate configuration - */ - private validate(config: SDKConfig): void { - if (!config.databaseIdHash) { - throw new Error('databaseIdHash is required in SDK configuration'); - } - } -} diff --git a/src/core/index.ts b/src/core/index.ts index f4dae88f2..4eca1fcf2 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,2 +1 @@ -export * from './ConfigManager'; export * from './AgentBuilder'; diff --git a/src/security/index.ts b/src/security/index.ts index ac72dad44..b8190d9b0 100644 --- a/src/security/index.ts +++ b/src/security/index.ts @@ -1,7 +1,7 @@ /** * Security module * - * Provides cryptographic utilities, quota validation, and security-related functions + * Provides cryptographic utilities, sandboxing, and security-related functions */ // Types @@ -10,8 +10,5 @@ export * from './types'; // Crypto utilities export * from './crypto'; -// Quota validation -export * from './quotas'; - // Sandboxing (LOU-F4/F5/F6) export * from './sandbox'; diff --git a/src/security/quotas.test.ts b/src/security/quotas.test.ts deleted file mode 100644 index c08bcd03e..000000000 --- a/src/security/quotas.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { validateTokenQuotas } from './quotas'; -import { SaaSContext } from './types'; - -describe('Security - Quotas', () => { - describe('validateTokenQuotas', () => { - it('should return OK when SaaS is not enabled', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: {}, - currentUsage: {}, - }; - const result = validateTokenQuotas(context, false); - expect(result.status).toBe(200); - expect(result.message).toContain('SaaS is not enabled'); - }); - - it('should reject when email is not verified', () => { - const context: SaaSContext = { - emailVerified: false, - currentQuota: {}, - currentUsage: {}, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(403); - expect(result.message).toContain('verify e-mail'); - }); - - it('should reject when results quota exceeded', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: { allowedResults: 100 }, - currentUsage: { usedResults: 150 }, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(403); - expect(result.message).toContain('limit of results'); - }); - - it('should reject when sessions quota exceeded', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: { allowedSessions: 50 }, - currentUsage: { usedSessions: 60 }, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(403); - expect(result.message).toContain('limit of sessions'); - }); - - it('should reject when USD budget quota exceeded', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: { allowedUSDBudget: 10 }, - currentUsage: { usedUSDBudget: 15 }, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(403); - expect(result.message).toContain('AI Tokens Limit'); - }); - - it('should allow when within quota limits', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: { - allowedResults: 100, - allowedSessions: 50, - allowedUSDBudget: 10, - }, - currentUsage: { - usedResults: 50, - usedSessions: 25, - usedUSDBudget: 5, - }, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(200); - expect(result.message).toBe('All OK!'); - }); - - it('should allow when quota is 0 (unlimited)', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: { - allowedResults: 0, - allowedSessions: 0, - allowedUSDBudget: 0, - }, - currentUsage: { - usedResults: 1000, - usedSessions: 500, - usedUSDBudget: 100, - }, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(200); - expect(result.message).toBe('All OK!'); - }); - - it('should allow when exactly at quota limit', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: { - allowedResults: 100, - allowedSessions: 50, - allowedUSDBudget: 10, - }, - currentUsage: { - usedResults: 100, - usedSessions: 50, - usedUSDBudget: 10, - }, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(200); - expect(result.message).toBe('All OK!'); - }); - - it('should handle missing usage values', () => { - const context: SaaSContext = { - emailVerified: true, - currentQuota: { - allowedResults: 100, - }, - currentUsage: {}, - }; - const result = validateTokenQuotas(context, true); - expect(result.status).toBe(200); - expect(result.message).toBe('All OK!'); - }); - }); -}); diff --git a/src/security/quotas.ts b/src/security/quotas.ts deleted file mode 100644 index 8e578eb2b..000000000 --- a/src/security/quotas.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Quota validation utilities for SaaS mode - */ - -import { SaaSContext, QuotaValidationResult, QuotaConfig, UsageStats } from './types'; - -/** - * Each quota dimension, in the order it is checked: the allowance on - * QuotaConfig, the matching counter on UsageStats, and the message returned - * once usage exceeds the allowance. - */ -interface QuotaCheck { - allowed: keyof QuotaConfig; - used: keyof UsageStats; - message: string; -} - -const QUOTA_CHECKS: readonly QuotaCheck[] = [ - { allowed: 'allowedResults', used: 'usedResults', message: 'You have reached the limit of results' }, - { allowed: 'allowedSessions', used: 'usedSessions', message: 'You have reached the limit of sessions' }, - { allowed: 'allowedUSDBudget', used: 'usedUSDBudget', message: 'You have reached the AI Tokens Limit' }, -]; - -/** - * A missing or zero allowance means "unlimited" (usage is not even read); - * otherwise usage must not exceed it. - */ -function exceedsAllowance(quota: QuotaConfig, usage: UsageStats, check: QuotaCheck): boolean { - const limit = quota[check.allowed] || 0; - return limit > 0 && (usage[check.used] ?? 0) > limit; -} - -/** - * Validate token quotas against current usage - * Returns a validation result with status code and message - * - * @param saasContext - SaaS context containing quota and usage information - * @param isSaaSEnabled - Whether SaaS mode is enabled (default: false) - * @returns Validation result with message and HTTP status code - */ -export function validateTokenQuotas( - saasContext: SaaSContext | undefined, - isSaaSEnabled?: boolean -): QuotaValidationResult { - if (!isSaaSEnabled) { - return { message: 'SaaS is not enabled, quotas are not validated', status: 200 }; - } - - return validateUserQuotas(saasContext); -} - -/** - * The per-user checks run once SaaS mode is on: e-mail verification first, - * then each quota dimension in QUOTA_CHECKS order. - */ -function validateUserQuotas(saasContext: SaaSContext | undefined): QuotaValidationResult { - if (!saasContext?.emailVerified) { - return { message: 'You must verify e-mail to use the AI features', status: 403 }; - } - - const { currentQuota, currentUsage } = saasContext; - const exceeded = QUOTA_CHECKS.find((check) => exceedsAllowance(currentQuota, currentUsage, check)); - if (exceeded) { - return { message: exceeded.message, status: 403 }; - } - - return { message: 'All OK!', status: 200 }; -} diff --git a/src/security/types.ts b/src/security/types.ts index 215e92c60..c15844332 100644 --- a/src/security/types.ts +++ b/src/security/types.ts @@ -18,24 +18,6 @@ export interface DTOEncryptionSettings { encryptedFields: string[]; } -/** - * Quota configuration for SaaS mode - */ -export interface QuotaConfig { - allowedResults?: number; - allowedSessions?: number; - allowedUSDBudget?: number; -} - -/** - * Current usage statistics - */ -export interface UsageStats { - usedResults?: number; - usedSessions?: number; - usedUSDBudget?: number; -} - /** * Authorization context for requests */ @@ -44,20 +26,3 @@ export interface AuthorizationContext { databaseIdHash: string; permissions: string[]; } - -/** - * SaaS context with quota and usage information - */ -export interface SaaSContext { - emailVerified: boolean; - currentQuota: QuotaConfig; - currentUsage: UsageStats; -} - -/** - * Result of quota validation - */ -export interface QuotaValidationResult { - message: string; - status: number; -} diff --git a/src/utils/file-extractor.test.ts b/src/utils/file-extractor.test.ts deleted file mode 100644 index 01c2b7d2b..000000000 --- a/src/utils/file-extractor.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - getMimeType, - getFileExtensionFromMimeType, - replaceBase64Content, - isBinaryData, - extractBase64Data, - createDataUri, -} from './file-extractor'; - -describe('File Extractor Utilities', () => { - describe('getMimeType', () => { - it('should extract MIME type from data URI', () => { - const dataUri = 'data:application/pdf;base64,JVBERi0x...'; - expect(getMimeType(dataUri)).toBe('application/pdf'); - }); - - it('should return null for invalid format', () => { - expect(getMimeType('invalid')).toBeNull(); - }); - - it('should handle image MIME types', () => { - const imageUri = 'data:image/png;base64,iVBORw0KGgo...'; - expect(getMimeType(imageUri)).toBe('image/png'); - }); - }); - - describe('getFileExtensionFromMimeType', () => { - it('should return correct extension for PDF', () => { - expect(getFileExtensionFromMimeType('application/pdf')).toBe('pdf'); - }); - - it('should return correct extension for images', () => { - expect(getFileExtensionFromMimeType('image/png')).toBe('png'); - expect(getFileExtensionFromMimeType('image/jpeg')).toBe('jpg'); - }); - - it('should return correct extension for Office documents', () => { - expect( - getFileExtensionFromMimeType( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' - ) - ).toBe('docx'); - }); - - it('should return not_convertible for unknown types', () => { - expect(getFileExtensionFromMimeType('application/unknown')).toBe( - 'not_convertible' - ); - }); - }); - - describe('replaceBase64Content', () => { - it('should replace base64 image content', () => { - const text = - 'Image: data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; - const result = replaceBase64Content(text); - expect(result).toContain('File content removed'); - expect(result).not.toContain('iVBORw0KGgo'); - }); - - it('should handle multiple images', () => { - const text = - 'First: data:image/png;base64,abc123 Second: data:image/jpeg;base64,xyz789'; - const result = replaceBase64Content(text); - const matches = (result.match(/File content removed/g) || []).length; - expect(matches).toBe(2); - }); - }); - - describe('isBinaryData', () => { - it('should detect binary data', () => { - const binaryBuffer = Buffer.from([0, 1, 2, 255, 254]); - expect(isBinaryData(binaryBuffer)).toBe(true); - }); - - it('should return false for text data', () => { - const textBuffer = Buffer.from('Hello World', 'utf-8'); - expect(isBinaryData(textBuffer)).toBe(false); - }); - - it('should allow common whitespace characters', () => { - const textWithWhitespace = Buffer.from('Hello\tWorld\nTest\r', 'utf-8'); - expect(isBinaryData(textWithWhitespace)).toBe(false); - }); - }); - - describe('extractBase64Data', () => { - it('should extract base64 data from data URI', () => { - const dataUri = 'data:image/png;base64,iVBORw0KGgo'; - expect(extractBase64Data(dataUri)).toBe('iVBORw0KGgo'); - }); - - it('should return original string if no comma found', () => { - const data = 'plaintext'; - expect(extractBase64Data(data)).toBe('plaintext'); - }); - }); - - describe('createDataUri', () => { - it('should create proper data URI', () => { - const mimeType = 'image/png'; - const base64 = 'iVBORw0KGgo'; - const result = createDataUri(mimeType, base64); - expect(result).toBe('data:image/png;base64,iVBORw0KGgo'); - }); - - it('should work with different MIME types', () => { - const result = createDataUri('application/pdf', 'JVBERi0x'); - expect(result).toBe('data:application/pdf;base64,JVBERi0x'); - }); - }); -}); diff --git a/src/utils/file-extractor.ts b/src/utils/file-extractor.ts deleted file mode 100644 index 289156f3b..000000000 --- a/src/utils/file-extractor.ts +++ /dev/null @@ -1,91 +0,0 @@ -/** - * File extraction and processing utilities - * Handles conversion of various file formats (PDF, Office docs, etc.) to text or images - */ - -// Type-safe way to access Buffer without Node.js type dependencies -declare const Buffer: any; - -export interface ProcessFilesParams { - inputObject: Record; // base64 or array of base64 - pdfExtractText?: boolean; // default false -} - -/** - * Get MIME type from base64 data URI - */ -export function getMimeType(base64Data: string): string | null { - // Expecting strings like: data:application/pdf;base64,JVBERi0x... - const match = base64Data.match(/^data:([^;]+);base64,/); - return match ? match[1] : null; -} - -/** - * Get file extension from MIME type - */ -export function getFileExtensionFromMimeType(mimeType: string): string { - const map: Record = { - 'application/pdf': 'pdf', - 'image/png': 'png', - 'image/jpeg': 'jpg', - 'image/jpg': 'jpg', - 'image/gif': 'gif', - 'image/webp': 'webp', - 'image/svg+xml': 'svg', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx', - 'text/html': 'html', - 'text/csv': 'csv', - 'application/json': 'json', - 'application/zip': 'zip', - 'text/markdown': 'md', - 'text/plain': 'txt', - }; - return map[mimeType] || 'not_convertible'; -} - -/** - * Replace base64 content in strings with a placeholder - * Useful for logging and debugging without exposing large binary data - */ -export function replaceBase64Content(data: string): string { - // Remove all base64 encoded content from the "image" fields - return data.replace( - /data:image\/[a-zA-Z]+;base64,[a-zA-Z0-9+/=]+/g, - 'File content removed' - ); -} - -/** - * Check if buffer contains binary data (non-printable characters) - */ -export function isBinaryData(buffer: any): boolean { - return buffer.some(isBinaryByte); -} - -/** Whitespace control characters (tab, LF, CR) that are legitimate in text. */ -const TEXT_CONTROL_BYTES: ReadonlySet = new Set([9, 10, 13]); - -function isBinaryByte(byte: number): boolean { - const isControlChar = byte < 32 && !TEXT_CONTROL_BYTES.has(byte); - return isControlChar || byte === 255; -} - -/** - * Extract base64 data part from data URI - */ -export function extractBase64Data(base64Str: string): string { - const parts = base64Str.split(','); - if (parts.length < 2) { - return base64Str; - } - return parts[1]; -} - -/** - * Create a data URI from MIME type and base64 data - */ -export function createDataUri(mimeType: string, base64Data: string): string { - return `data:${mimeType};base64,${base64Data}`; -} diff --git a/src/utils/formatters.test.ts b/src/utils/formatters.test.ts deleted file mode 100644 index 93c4ae126..000000000 --- a/src/utils/formatters.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - getCurrentTS, - getTS, - formatDate, - safeJsonParse, - removeCodeBlocks, - findCodeBlocks, - checkApiKey, -} from './formatters'; - -describe('formatter utilities', () => { - describe('date formatters', () => { - it('should format current timestamp', () => { - const ts = getCurrentTS(); - expect(ts).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/); - }); - - it('should format specific date', () => { - const date = new Date('2025-01-15T10:30:45'); - const ts = getTS(date); - expect(ts).toBe('2025-01-15 10:30:45'); - }); - - it('should format date to locale string', () => { - const date = new Date('2025-01-15T10:30:45'); - const formatted = formatDate(date); - expect(formatted).toBeTruthy(); - expect(typeof formatted).toBe('string'); - }); - }); - - describe('safeJsonParse', () => { - it('should parse valid JSON', () => { - const result = safeJsonParse('{"name":"test"}', {}); - expect(result).toEqual({ name: 'test' }); - }); - - it('should return default on invalid JSON', () => { - const result = safeJsonParse('invalid json', { default: true }); - expect(result).toEqual({ default: true }); - }); - - it('should handle arrays', () => { - const result = safeJsonParse('[1,2,3]', []); - expect(result).toEqual([1, 2, 3]); - }); - }); - - describe('code block utilities', () => { - it('should remove code blocks', () => { - const text = 'Hello\n```js\ncode\n```\nWorld'; - const result = removeCodeBlocks(text); - expect(result).not.toContain('```'); - }); - - it('should find code blocks', () => { - const text = '```javascript\nconst x = 1;\n```'; - const result = findCodeBlocks(text); - expect(result.blocks).toHaveLength(1); - expect(result.blocks[0].syntax).toBe('javascript'); - expect(result.blocks[0].code).toBe('const x = 1;'); - }); - - it('should find multiple code blocks', () => { - const text = '```js\ncode1\n```\ntext\n```py\ncode2\n```'; - const result = findCodeBlocks(text, false); - expect(result.blocks).toHaveLength(2); - expect(result.blocks[0].syntax).toBe('js'); - expect(result.blocks[1].syntax).toBe('py'); - }); - - it('should handle single block mode', () => { - const text = 'plain code without blocks'; - const result = findCodeBlocks(text); - expect(result.blocks).toHaveLength(1); - expect(result.blocks[0].code).toBe(text); - }); - - it('should detect code block errors', () => { - const text = 'prefix ```js\ncode\n```'; - const result = findCodeBlocks(text); - expect(result.errors.length).toBeGreaterThan(0); - }); - }); - - describe('checkApiKey', () => { - it('should return value if provided', () => { - const result = checkApiKey('OpenAI', 'OPENAI_KEY', 'sk-123'); - expect(result).toBe('sk-123'); - }); - - it('should throw if value is empty', () => { - expect(() => checkApiKey('OpenAI', 'OPENAI_KEY', '')).toThrow(); - }); - }); -}); diff --git a/src/utils/formatters.ts b/src/utils/formatters.ts deleted file mode 100644 index 9f5440b30..000000000 --- a/src/utils/formatters.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Get current timestamp in formatted string - */ -export function getCurrentTS(): string { - return getTS(); -} - -/** - * Format date to timestamp string (YYYY-MM-DD HH:MM:SS) - */ -export function getTS(now = new Date()): string { - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const day = String(now.getDate()).padStart(2, '0'); - - const hours = String(now.getHours()).padStart(2, '0'); - const minutes = String(now.getMinutes()).padStart(2, '0'); - const seconds = String(now.getSeconds()).padStart(2, '0'); - - const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; - - return formattedDate; -} - -/** - * Format date to locale string - */ -export function formatDate(date: Date): string { - return date.toLocaleString(); -} - -/** - * Safe JSON parse with default value - */ -export function safeJsonParse(str: string, defaultValue: T): T { - try { - return JSON.parse(str); - } catch { - return defaultValue; - } -} - -/** - * Remove markdown code blocks from text - */ -export function removeCodeBlocks(text: string): string { - const PATTERN = /^([A-Za-z \t]*)```([A-Za-z]*)?\n([\s\S]*?)```([A-Za-z \t]*)*$/gm; - return text.replace(PATTERN, ''); -} - -/** - * Count lines in text - */ -function countLines(text = ''): number { - return text.split('\n').length; -} - -/** - * Get line number from text and regex match - */ -function getLineNumber(text = '', matches: any): number { - return countLines(text.substr(0, matches.index)); -} - -export interface CodeBlock { - line: number; - position: number; - syntax: string; - block: string; - code: string; -} - -export interface CodeBlockError { - line: number; - position: number; - message: string; - block: string; -} - -export interface CodeBlockResult { - errors: CodeBlockError[]; - blocks: CodeBlock[]; -} - -/** - * Find and extract code blocks from markdown text - */ -export function findCodeBlocks(block: string, singleBlockMode = true): CodeBlockResult { - const PATTERN = /^([A-Za-z \t]*)```([A-Za-z]*)?\n([\s\S]*?)```([A-Za-z \t]*)*$/gm; - let matches; - const errors: CodeBlockError[] = []; - const blocks: CodeBlock[] = []; - - while ((matches = PATTERN.exec(block)) !== null) { - if (matches.index === PATTERN.lastIndex) { - PATTERN.lastIndex++; // avoid infinite loops with zero-width matches - } - - const blockErrors = validateCodeBlockMatch(block, matches); - if (blockErrors.length > 0) { - errors.push(...blockErrors); - } else { - blocks.push(toCodeBlock(block, matches)); - } - } - - return { - errors, - blocks: withSingleBlockFallback(blocks, block, singleBlockMode), - }; -} - -/** Validate a code block match: no prefix before the opening fence, no postfix after the closing one. */ -function validateCodeBlockMatch(text: string, matches: RegExpExecArray): CodeBlockError[] { - const lineNumber = getLineNumber(text, matches); - const errors = [prefixError(matches, lineNumber), postfixError(matches, lineNumber)]; - return errors.filter((error): error is CodeBlockError => error !== null); -} - -function prefixError(matches: RegExpExecArray, lineNumber: number): CodeBlockError | null { - const [match, prefix] = matches; - if (!prefix || !prefix.match(/\S/)) return null; - - return { - line: lineNumber, - position: matches.index, - message: `Prefix "${prefix}" not allowed on line ${lineNumber}. Remove it to fix the code block.`, - block: match, - }; -} - -function postfixError(matches: RegExpExecArray, lineNumber: number): CodeBlockError | null { - const [match, , , , postFix] = matches; - if (!postFix || !postFix.match(/\S/)) return null; - - const line = lineNumber + (countLines(match) - 1); - return { - line, - position: matches.index + match.length, - message: `Postfix "${postFix}" not allowed on line ${line}. Remove it to fix the code block.`, - block: match, - }; -} - -function toCodeBlock(text: string, matches: RegExpExecArray): CodeBlock { - const [match, , syntax, content] = matches; - return { - line: getLineNumber(text, matches), - position: matches.index, - syntax: syntax || 'none', - block: match, - code: content.trim(), - }; -} - -/** In single-block mode, treat the whole text as one block when no fenced blocks were found. */ -function withSingleBlockFallback(blocks: CodeBlock[], text: string, singleBlockMode: boolean): CodeBlock[] { - if (blocks.length > 0 || !singleBlockMode) return blocks; - return [ - { - line: 0, - position: 0, - syntax: '', - block: '', - code: text.trim(), - }, - ]; -} - -/** - * Check if API key is provided, throw error if not - */ -export function checkApiKey(name: string, key: string, value: string): string { - if (value) return value; - - throw new Error( - `Please provide the ${name} API key in the environment variable ${key}` - ); -} diff --git a/src/utils/index.ts b/src/utils/index.ts index a99da5bb1..d09b79792 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,17 +2,8 @@ * Utility functions for the Open Agents Builder SDK */ -// JSON path utilities -export * from './json-path'; - // Error handling export * from './errors'; -// Formatters -export * from './formatters'; - // Validators export * from './validators'; - -// File extraction utilities -export * from './file-extractor'; diff --git a/src/utils/json-path.test.ts b/src/utils/json-path.test.ts deleted file mode 100644 index abc81009c..000000000 --- a/src/utils/json-path.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { getObjectByPath, setRecursiveNames } from './json-path'; - -describe('json-path utilities', () => { - describe('getObjectByPath', () => { - it('should get value by simple path', () => { - const obj = { user: { name: 'Alice' } }; - expect(getObjectByPath(obj, '$.user.name')).toBe('Alice'); - }); - - it('should get value by array index path', () => { - const obj = { items: ['a', 'b', 'c'] }; - expect(getObjectByPath(obj, '$.items[1]')).toBe('b'); - }); - - it('should return undefined for invalid path', () => { - const obj = { user: { name: 'Alice' } }; - expect(getObjectByPath(obj, '$.user.age')).toBeUndefined(); - }); - - it('should throw error for invalid path format', () => { - const obj = { user: { name: 'Alice' } }; - expect(() => getObjectByPath(obj, 'user.name')).toThrow('Invalid path'); - }); - - it('should handle nested arrays and objects', () => { - const obj = { users: [{ name: 'Alice' }, { name: 'Bob' }] }; - expect(getObjectByPath(obj, '$.users[0].name')).toBe('Alice'); - }); - }); - - describe('setRecursiveNames', () => { - it('should set names for agent nodes', () => { - const obj = { - agent: 'root', - input: { - agent: 'child', - }, - }; - - setRecursiveNames(obj); - - expect(obj.name).toBe('root'); - expect((obj.input as any).name).toBe('root > child'); - }); - - it('should generate IDs for nodes without ID', () => { - const obj = { agent: 'test' }; - setRecursiveNames(obj); - expect(obj.id).toBeDefined(); - expect(typeof obj.id).toBe('string'); - }); - - it('should handle array inputs', () => { - const obj = { - agent: 'parent', - input: [ - { agent: 'child1' }, - { agent: 'child2' }, - ], - }; - - setRecursiveNames(obj); - - expect((obj.input[0] as any).name).toBe('parent > child1'); - expect((obj.input[1] as any).name).toBe('parent > child2'); - }); - - it('should handle item arrays', () => { - const obj = { - agent: 'parent', - item: [ - { agent: 'item1' }, - { agent: 'item2' }, - ], - }; - - setRecursiveNames(obj); - - expect((obj.item[0] as any).name).toBe('parent > item1'); - expect((obj.item[1] as any).name).toBe('parent > item2'); - }); - }); -}); diff --git a/src/utils/json-path.ts b/src/utils/json-path.ts deleted file mode 100644 index 6bed94965..000000000 --- a/src/utils/json-path.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { newId } from './id'; - -/** - * Recursively traverses the given object (and its children via "input" and "item") - * updating each node's "name" property to reflect the path of agents from the root. - */ -export function setRecursiveNames(obj: any, path: string[] = []): void { - // Skip if not an object - if (!isTraversable(obj)) { - return; - } - - if (!obj.id) { - obj.id = newId(); - } - - // If this node has an 'agent', extend our path - let childPath = path; - if (typeof obj.agent === 'string') { - childPath = [...path, obj.agent]; - // Update the node's name to be the full path joined by " > " - obj.name = childPath.join(' > '); - } - - // Go deeper through "input" and "item" whether or not this node had an 'agent' - visitChildren(obj.input, childPath); - visitChildren(obj.item, childPath); -} - -function isTraversable(value: unknown): value is Record { - return !!value && typeof value === 'object'; -} - -/** Recurse into a child slot that may hold an array of nodes or a single node. */ -function visitChildren(child: unknown, path: string[]): void { - if (Array.isArray(child)) { - for (const item of child) { - setRecursiveNames(item, path); - } - } else if (isTraversable(child)) { - setRecursiveNames(child, path); - } -} - -/** - * Get an object property by JSON path notation (e.g., "$.user.name" or "$.items[0]") - * @param obj - The object to traverse - * @param path - The JSON path string starting with "$" - * @returns The value at the specified path or undefined if not found - */ -export function getObjectByPath(obj: any, path: string): any { - if (!path.startsWith('$')) { - throw new Error("Invalid path: Path should start with '$'"); - } - - // Regex to split path correctly while keeping array indices - const keys = path - .replace(/\[(\d+)\]/g, '.$1') // Convert array indices to dot notation (e.g., "f[2]" → "f.2") - .slice(2) // Remove the "$." - .split('.'); - - let current = obj; - for (const key of keys) { - if (!hasOwnKey(current, key)) { - return undefined; // Return undefined if path is invalid - } - current = current[key]; - } - - return current; -} - -function hasOwnKey(value: unknown, key: string): boolean { - return typeof value === 'object' && value !== null && Object.prototype.hasOwnProperty.call(value, key); -}