From 0bc5b97903ecaedb08fd79191e469bb24519cedb Mon Sep 17 00:00:00 2001 From: Codebuff Date: Tue, 21 Oct 2025 14:23:26 -0700 Subject: [PATCH 01/10] Update array.ts --- common/src/util/array.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/util/array.ts b/common/src/util/array.ts index 44ac05dc2..57ac29d4d 100644 --- a/common/src/util/array.ts +++ b/common/src/util/array.ts @@ -1,3 +1,4 @@ +// TODO: Replace lodash functions with native equivalents (e.g., use array.filter(Boolean) instead of compact, array.flat(Infinity) instead of flattenDeep, JSON.stringify-based deep compare instead of isEqual). import { compact, flattenDeep, isEqual } from 'lodash' export function filterDefined(array: (T | null | undefined)[]) { From f9380349f4cb1b90b9e8787393b2fa1e4f9e470f Mon Sep 17 00:00:00 2001 From: Codebuff Date: Tue, 21 Oct 2025 14:25:23 -0700 Subject: [PATCH 02/10] Add TODO comment for replacing lodash functions in object.ts --- common/src/util/object.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/common/src/util/object.ts b/common/src/util/object.ts index 3232adcb3..8a6524de3 100644 --- a/common/src/util/object.ts +++ b/common/src/util/object.ts @@ -1,3 +1,4 @@ +// TODO: Replace lodash functions isEqual, mapValues, union with native JS equivalents (e.g., use JSON.stringify(a) === JSON.stringify(b) for deep equality, Object.fromEntries(Object.entries(obj).map(([k,v]) => v)) to map values, and Array.from(new Set([...arr1,...arr2])) for union). import { isEqual, mapValues, union } from 'lodash' export const removeUndefinedProps = ( From 21631ec273490ffcd46667fd0e5a2bd5581cb875 Mon Sep 17 00:00:00 2001 From: Codebuff Date: Tue, 21 Oct 2025 14:26:05 -0700 Subject: [PATCH 03/10] Add TODO comment for replacing lodash shuffle in analyze-edit-blocks.ts --- scripts/analyze-edit-blocks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/analyze-edit-blocks.ts b/scripts/analyze-edit-blocks.ts index fa3835df3..b993c58d1 100644 --- a/scripts/analyze-edit-blocks.ts +++ b/scripts/analyze-edit-blocks.ts @@ -1,3 +1,4 @@ +// TODO: Replace lodash shuffle with a native shuffle implementation using Fisher-Yates algorithm (e.g., for arrays). import fs from 'fs' import path from 'path' From d95b2e11dc84d2bfa5738f97834bfc6e6a00dea3 Mon Sep 17 00:00:00 2001 From: Codebuff Date: Tue, 21 Oct 2025 14:27:14 -0700 Subject: [PATCH 04/10] Add TODO comment for replacing lodash has in dev-process-manager.ts --- npm-app/src/dev-process-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/npm-app/src/dev-process-manager.ts b/npm-app/src/dev-process-manager.ts index 954829291..8730df8bd 100644 --- a/npm-app/src/dev-process-manager.ts +++ b/npm-app/src/dev-process-manager.ts @@ -1,3 +1,4 @@ +// TODO: Replace lodash has with native property check using optional chaining or Object.prototype.hasOwnProperty.call. import path from 'path' import { codebuffConfigFile } from '@codebuff/common/json-config/constants' From ba665a89b885a211addfb46b5c79ef2ebc122fc5 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 21 Oct 2025 14:56:37 -0700 Subject: [PATCH 05/10] Remove lodash dependency and replace with native JavaScript MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced all lodash functions with native JavaScript equivalents: - compact/flattenDeep → custom buildArray with manual flattening - isEqual → JSON.stringify comparison - cloneDeep → JSON.parse(JSON.stringify()) - shuffle → Fisher-Yates algorithm - uniq → Array.from(new Set()) - sortBy → native .sort() with comparator - sumBy → .reduce() with accumulator - mapValues → Object.fromEntries() + .map() - union → Array.from(new Set([...arr1, ...arr2])) - partition → custom implementation with type guards - has → 'key' in obj checks - isError → instanceof Error - range → Array.from({ length }, (_, i) => i) Removed lodash and @types/lodash from all package.json files. All tests passing for modified packages. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- backend/package.json | 1 - backend/src/get-documentation-for-query.ts | 13 ++++---- backend/src/main-prompt.ts | 12 +++---- backend/src/run-agent-step.ts | 6 +++- backend/src/run-programmatic-step.ts | 6 +++- backend/src/tools/stream-parser.ts | 6 +++- backend/src/tools/tool-executor.ts | 6 +++- backend/src/websockets/server.ts | 3 +- bun.lock | 10 ------ common/package.json | 1 - common/src/project-file-tree.ts | 8 ++--- common/src/util/array.ts | 28 ++++++++++++++--- common/src/util/messages.ts | 30 ++++++++++++++---- common/src/util/object.ts | 31 ++++++++++++++++--- common/src/util/string.ts | 5 ++- evals/git-evals/run-git-evals.ts | 6 +++- evals/package.json | 1 - npm-app/package.json | 1 - npm-app/src/cli.ts | 25 ++++++++------- npm-app/src/dev-process-manager.ts | 5 ++- npm-app/src/json-config/hooks.ts | 4 +-- package.json | 2 -- .../src/find-files/request-files-prompt.ts | 25 ++++++++++++--- .../src/get-file-reading-updates.ts | 4 +-- .../src/tools/handlers/tool/write-file.ts | 24 ++++++++++++-- packages/agent-runtime/src/util/messages.ts | 15 ++++++++- .../src/util/simplify-tool-results.ts | 6 +++- scripts/analyze-edit-blocks.ts | 12 +++++-- scripts/package.json | 4 +-- sdk/src/run-state.ts | 12 ++++--- sdk/src/run.ts | 8 +++-- 31 files changed, 224 insertions(+), 96 deletions(-) diff --git a/backend/package.json b/backend/package.json index 4232c3f29..f973f7eed 100644 --- a/backend/package.json +++ b/backend/package.json @@ -40,7 +40,6 @@ "express": "4.19.2", "gpt-tokenizer": "2.8.1", "ignore": "5.3.2", - "lodash": "*", "micromatch": "^4.0.8", "openai": "^4.78.1", "pino": "9.4.0", diff --git a/backend/src/get-documentation-for-query.ts b/backend/src/get-documentation-for-query.ts index 4755857b6..86b5505fb 100644 --- a/backend/src/get-documentation-for-query.ts +++ b/backend/src/get-documentation-for-query.ts @@ -1,6 +1,5 @@ import { models } from '@codebuff/common/old-constants' import { closeXml } from '@codebuff/common/util/xml' -import { uniq } from 'lodash' import { z } from 'zod/v4' import { fetchContext7LibraryDocumentation } from '@codebuff/agent-runtime/llm-api/context7-api' @@ -75,11 +74,13 @@ export async function getDocumentationForQuery( ).flat() const maxChunks = 25 - const allUniqueChunks = uniq( - allRawChunks - .filter((chunk) => chunk !== null) - .join(DELIMITER) - .split(DELIMITER), + const allUniqueChunks = Array.from( + new Set( + allRawChunks + .filter((chunk) => chunk !== null) + .join(DELIMITER) + .split(DELIMITER), + ), ).slice(0, maxChunks) if (allUniqueChunks.length === 0) { diff --git a/backend/src/main-prompt.ts b/backend/src/main-prompt.ts index 9d301ac15..1dca6c2bb 100644 --- a/backend/src/main-prompt.ts +++ b/backend/src/main-prompt.ts @@ -2,7 +2,6 @@ import { getAgentTemplate } from '@codebuff/agent-runtime/templates/agent-regist import { expireMessages } from '@codebuff/agent-runtime/util/messages' import { AgentTemplateTypes } from '@codebuff/common/types/session-state' import { generateCompactId } from '@codebuff/common/util/string' -import { uniq } from 'lodash' import { checkTerminalCommand } from './check-terminal-command' import { loopAgentSteps } from './run-agent-step' @@ -141,12 +140,13 @@ export const mainPrompt = async ( } = fileContext.codebuffConfig ?? {} updatedSubagents = spawnableAgents ?? - uniq([...mainAgentTemplate.spawnableAgents, ...availableAgents]) + Array.from( + new Set([...mainAgentTemplate.spawnableAgents, ...availableAgents]), + ) - updatedSubagents = uniq([ - ...updatedSubagents, - ...addedSpawnableAgents, - ]).filter((subagent) => !removedSpawnableAgents.includes(subagent)) + updatedSubagents = Array.from( + new Set([...updatedSubagents, ...addedSpawnableAgents]), + ).filter((subagent) => !removedSpawnableAgents.includes(subagent)) } mainAgentTemplate.spawnableAgents = updatedSubagents localAgentTemplates[agentType] = mainAgentTemplate diff --git a/backend/src/run-agent-step.ts b/backend/src/run-agent-step.ts index f4cd3411d..f7a4a8ea7 100644 --- a/backend/src/run-agent-step.ts +++ b/backend/src/run-agent-step.ts @@ -19,7 +19,11 @@ import { supportsCacheControl } from '@codebuff/common/old-constants' import { TOOLS_WHICH_WONT_FORCE_NEXT_STEP } from '@codebuff/common/tools/constants' import { buildArray } from '@codebuff/common/util/array' import { getErrorObject } from '@codebuff/common/util/error' -import { cloneDeep } from 'lodash' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} import { runProgrammaticStep } from './run-programmatic-step' import { processStreamWithTools } from './tools/stream-parser' diff --git a/backend/src/run-programmatic-step.ts b/backend/src/run-programmatic-step.ts index ddbef32dd..f8b01173f 100644 --- a/backend/src/run-programmatic-step.ts +++ b/backend/src/run-programmatic-step.ts @@ -1,7 +1,11 @@ import { SandboxManager } from '@codebuff/agent-runtime/util/quickjs-sandbox' import { getToolCallString } from '@codebuff/common/tools/utils' import { getErrorObject } from '@codebuff/common/util/error' -import { cloneDeep } from 'lodash' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} import { executeToolCall } from './tools/tool-executor' diff --git a/backend/src/tools/stream-parser.ts b/backend/src/tools/stream-parser.ts index 4d9e82d22..98acb69dc 100644 --- a/backend/src/tools/stream-parser.ts +++ b/backend/src/tools/stream-parser.ts @@ -8,7 +8,11 @@ import { } from '@codebuff/common/tools/constants' import { buildArray } from '@codebuff/common/util/array' import { generateCompactId } from '@codebuff/common/util/string' -import { cloneDeep } from 'lodash' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} import { executeCustomToolCall, executeToolCall } from './tool-executor' diff --git a/backend/src/tools/tool-executor.ts b/backend/src/tools/tool-executor.ts index adfdc952a..0eb8655f1 100644 --- a/backend/src/tools/tool-executor.ts +++ b/backend/src/tools/tool-executor.ts @@ -4,8 +4,12 @@ import { codebuffToolDefs } from '@codebuff/agent-runtime/tools/definitions/list import { endsAgentStepParam } from '@codebuff/common/tools/constants' import { generateCompactId } from '@codebuff/common/util/string' import { type ToolCallPart } from 'ai' -import { cloneDeep } from 'lodash' import z from 'zod/v4' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} import { convertJsonSchemaToZod } from 'zod-from-json-schema' import { codebuffToolHandlers } from './handlers/list' diff --git a/backend/src/websockets/server.ts b/backend/src/websockets/server.ts index 267e87416..9cc2e4480 100644 --- a/backend/src/websockets/server.ts +++ b/backend/src/websockets/server.ts @@ -1,6 +1,5 @@ import { setSessionConnected } from '@codebuff/agent-runtime/live-user-inputs' import { CLIENT_MESSAGE_SCHEMA } from '@codebuff/common/websockets/websocket-schema' -import { isError } from 'lodash' import { WebSocketServer } from 'ws' import { Switchboard } from './switchboard' @@ -27,7 +26,7 @@ export class MessageParseError extends Error { } function serializeError(err: unknown) { - return isError(err) ? err.message : 'Unexpected error.' + return err instanceof Error ? err.message : 'Unexpected error.' } async function processMessage(params: { diff --git a/bun.lock b/bun.lock index 70b49a183..90f468c0f 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,6 @@ "devDependencies": { "@tanstack/react-query": "^5.59.16", "@types/bun": "^1.3.0", - "@types/lodash": "4.17.7", "@types/node": "^22.9.0", "@types/node-fetch": "^2.6.12", "@types/parse-path": "^7.1.0", @@ -20,7 +19,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-unused-imports": "^4.1.4", "ignore": "^6.0.2", - "lodash": "4.17.21", "prettier": "3.3.2", "ts-node": "^10.9.2", "ts-pattern": "^5.5.0", @@ -56,7 +54,6 @@ "express": "4.19.2", "gpt-tokenizer": "2.8.1", "ignore": "5.3.2", - "lodash": "*", "micromatch": "^4.0.8", "openai": "^4.78.1", "pino": "9.4.0", @@ -114,7 +111,6 @@ "drizzle-kit": "0.28.1", "drizzle-orm": "0.36.4", "ignore": "5.3.2", - "lodash": "*", "next-auth": "^4.24.7", "partial-json": "^0.1.7", "pg": "^8.14.1", @@ -143,7 +139,6 @@ "@oclif/parser": "^3.8.17", "async": "^3.2.6", "diff": "^8.0.2", - "lodash": "^4.17.21", "p-limit": "^6.2.0", "zod": "3.25.67", }, @@ -172,7 +167,6 @@ "ignore": "7.0.3", "isomorphic-git": "^1.29.0", "jimp": "^1.6.0", - "lodash": "*", "markdown-it": "^14.1.0", "markdown-it-terminal": "^0.4.0", "micromatch": "^4.0.8", @@ -266,11 +260,9 @@ "@codebuff/backend": "workspace:*", "@codebuff/bigquery": "workspace:*", "@codebuff/common": "workspace:*", - "lodash": "^4.17.21", }, "devDependencies": { "@types/bun": "^1.3.0", - "@types/lodash": "^4.14.195", "@types/node": "22", }, }, @@ -1392,8 +1384,6 @@ "@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="], - "@types/lodash": ["@types/lodash@4.17.7", "", {}, "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA=="], - "@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], diff --git a/common/package.json b/common/package.json index e3854e510..84b809e50 100644 --- a/common/package.json +++ b/common/package.json @@ -40,7 +40,6 @@ "drizzle-kit": "0.28.1", "drizzle-orm": "0.36.4", "ignore": "5.3.2", - "lodash": "*", "next-auth": "^4.24.7", "partial-json": "^0.1.7", "pg": "^8.14.1", diff --git a/common/src/project-file-tree.ts b/common/src/project-file-tree.ts index 6e52944ab..0d98c95e2 100644 --- a/common/src/project-file-tree.ts +++ b/common/src/project-file-tree.ts @@ -1,7 +1,6 @@ import path from 'path' import * as ignore from 'ignore' -import { sortBy } from 'lodash' import { DEFAULT_IGNORED_PATHS } from './old-constants' import { isValidProjectRoot } from './util/file' @@ -209,10 +208,9 @@ export function getLastReadFilePaths( flattenedNodes: FileTreeNode[], count: number, ) { - return sortBy( - flattenedNodes.filter((node) => node.lastReadTime), - 'lastReadTime', - ) + return flattenedNodes + .filter((node) => node.lastReadTime) + .sort((a, b) => (a.lastReadTime || 0) - (b.lastReadTime || 0)) .reverse() .slice(0, count) .map((node) => node.filePath) diff --git a/common/src/util/array.ts b/common/src/util/array.ts index 57ac29d4d..b14801ea7 100644 --- a/common/src/util/array.ts +++ b/common/src/util/array.ts @@ -1,5 +1,12 @@ -// TODO: Replace lodash functions with native equivalents (e.g., use array.filter(Boolean) instead of compact, array.flat(Infinity) instead of flattenDeep, JSON.stringify-based deep compare instead of isEqual). -import { compact, flattenDeep, isEqual } from 'lodash' +// Deep equality check using JSON serialization +// Note: Not exported to avoid type recursion issues +function isEqual(a: unknown, b: unknown): boolean { + try { + return JSON.stringify(a) === JSON.stringify(b) + } catch { + return a === b + } +} export function filterDefined(array: (T | null | undefined)[]) { return array.filter((item) => item !== null && item !== undefined) as T[] @@ -8,8 +15,21 @@ export function filterDefined(array: (T | null | undefined)[]) { type Falsey = false | undefined | null | 0 | '' type FalseyValueArray = T | Falsey | FalseyValueArray[] -export function buildArray(...params: FalseyValueArray[]) { - return compact(flattenDeep(params)) as T[] +export function buildArray(...params: FalseyValueArray[]): T[] { + const result: any[] = [] + + function flatten(arr: any): void { + for (const item of arr) { + if (Array.isArray(item)) { + flatten(item) + } else if (item) { + result.push(item) + } + } + } + + flatten(params) + return result as T[] } export function groupConsecutive(xs: T[], key: (x: T) => U) { diff --git a/common/src/util/messages.ts b/common/src/util/messages.ts index 3a3e7f10f..99a5bf4d2 100644 --- a/common/src/util/messages.ts +++ b/common/src/util/messages.ts @@ -1,6 +1,18 @@ -import { cloneDeep, has, isEqual } from 'lodash' - import { buildArray } from './array' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} + +// Deep equality check using JSON serialization +function isEqual(a: unknown, b: unknown): boolean { + try { + return JSON.stringify(a) === JSON.stringify(b) + } catch { + return a === b + } +} import { getToolCallString } from '../tools/utils' import type { @@ -41,8 +53,11 @@ export function withoutCacheControl< T extends { providerOptions?: ProviderMetadata }, >(obj: T): T { const wrapper = cloneDeep(obj) - if (has(wrapper.providerOptions?.anthropic?.cacheControl, 'type')) { - delete wrapper.providerOptions?.anthropic?.cacheControl?.type + if ( + wrapper.providerOptions?.anthropic?.cacheControl && + 'type' in wrapper.providerOptions.anthropic.cacheControl + ) { + delete (wrapper.providerOptions.anthropic.cacheControl as any).type } if ( Object.keys(wrapper.providerOptions?.anthropic?.cacheControl ?? {}) @@ -54,8 +69,11 @@ export function withoutCacheControl< delete wrapper.providerOptions?.anthropic } - if (has(wrapper.providerOptions?.openrouter?.cacheControl, 'type')) { - delete wrapper.providerOptions?.openrouter?.cacheControl?.type + if ( + wrapper.providerOptions?.openrouter?.cacheControl && + 'type' in wrapper.providerOptions.openrouter.cacheControl + ) { + delete (wrapper.providerOptions.openrouter.cacheControl as any).type } if ( Object.keys(wrapper.providerOptions?.openrouter?.cacheControl ?? {}) diff --git a/common/src/util/object.ts b/common/src/util/object.ts index 8a6524de3..45c4246d2 100644 --- a/common/src/util/object.ts +++ b/common/src/util/object.ts @@ -1,5 +1,26 @@ -// TODO: Replace lodash functions isEqual, mapValues, union with native JS equivalents (e.g., use JSON.stringify(a) === JSON.stringify(b) for deep equality, Object.fromEntries(Object.entries(obj).map(([k,v]) => v)) to map values, and Array.from(new Set([...arr1,...arr2])) for union). -import { isEqual, mapValues, union } from 'lodash' +// Deep equality check using JSON serialization +function isEqual(a: unknown, b: unknown): boolean { + try { + return JSON.stringify(a) === JSON.stringify(b) + } catch { + return a === b + } +} + +// Map values of an object +function mapValues( + obj: T, + fn: (value: any, key: keyof T) => R, +): { [K in keyof T]: R } { + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, fn(v, k as keyof T)]), + ) as { [K in keyof T]: R } +} + +// Union of two arrays +function union(arr1: T[], arr2: T[]): T[] { + return Array.from(new Set([...arr1, ...arr2])) +} export const removeUndefinedProps = ( obj: T, @@ -61,7 +82,7 @@ export const subtractObjects = ( export const hasChanges = (obj: T, partial: Partial) => { const currValues = mapValues(partial, (_, key: keyof T) => obj[key]) - return !isEqual(currValues, partial) + return !isEqual(currValues, partial as any) } export const hasSignificantDeepChanges = ( @@ -75,8 +96,8 @@ export const hasSignificantDeepChanges = ( } if (typeof currValue === 'object' && typeof partialValue === 'object') { return hasSignificantDeepChanges( - currValue, - partialValue, + currValue as any, + partialValue as any, epsilonForNumbers, ) } diff --git a/common/src/util/string.ts b/common/src/util/string.ts index 596350738..32e1fdb3a 100644 --- a/common/src/util/string.ts +++ b/common/src/util/string.ts @@ -1,4 +1,7 @@ -import { sumBy } from 'lodash' +// Sum an array by extracting numeric values with a function +function sumBy(arr: T[], fn: (item: T) => number): number { + return arr.reduce((sum, item) => sum + fn(item), 0) +} export const truncateString = (str: string, maxLength: number) => { if (str.length <= maxLength) { diff --git a/evals/git-evals/run-git-evals.ts b/evals/git-evals/run-git-evals.ts index 491029ab0..0af0ffbd2 100644 --- a/evals/git-evals/run-git-evals.ts +++ b/evals/git-evals/run-git-evals.ts @@ -7,9 +7,13 @@ import { promptAiSdkStructured } from '@codebuff/backend/llm-apis/vercel-ai-sdk/ import { getErrorObject } from '@codebuff/common/util/error' import { withTimeout } from '@codebuff/common/util/promise' import { generateCompactId } from '@codebuff/common/util/string' -import { cloneDeep } from 'lodash' import pLimit from 'p-limit' +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} + import { resetRepoToCommit } from '../scaffolding' import { createInitialSessionState } from '../test-setup' import { judgeEvalRun } from './judge-git-eval' diff --git a/evals/package.json b/evals/package.json index a8556049e..e98339784 100644 --- a/evals/package.json +++ b/evals/package.json @@ -41,7 +41,6 @@ "@oclif/parser": "^3.8.17", "async": "^3.2.6", "diff": "^8.0.2", - "lodash": "^4.17.21", "p-limit": "^6.2.0", "zod": "3.25.67" }, diff --git a/npm-app/package.json b/npm-app/package.json index f506a69b2..a7bfca172 100644 --- a/npm-app/package.json +++ b/npm-app/package.json @@ -48,7 +48,6 @@ "ignore": "7.0.3", "isomorphic-git": "^1.29.0", "jimp": "^1.6.0", - "lodash": "*", "markdown-it": "^14.1.0", "markdown-it-terminal": "^0.4.0", "micromatch": "^4.0.8", diff --git a/npm-app/src/cli.ts b/npm-app/src/cli.ts index 13868853f..7fec51204 100644 --- a/npm-app/src/cli.ts +++ b/npm-app/src/cli.ts @@ -12,7 +12,6 @@ import { } from '@codebuff/common/util/agent-name-resolver' import { isDir } from '@codebuff/common/util/file' import { pluralize } from '@codebuff/common/util/string' -import { uniq } from 'lodash' import { blueBright, bold, @@ -526,17 +525,19 @@ export class CLI { return p } - const matchingPaths = uniq( - allFiles - .map((filePath) => { - let candidate = null - while (filePath.includes(searchTerm)) { - candidate = filePath - filePath = path.dirname(filePath) + '/' - } - return candidate - }) - .filter((filePath): filePath is string => !!filePath), + const matchingPaths = Array.from( + new Set( + allFiles + .map((filePath) => { + let candidate = null + while (filePath.includes(searchTerm)) { + candidate = filePath + filePath = path.dirname(filePath) + '/' + } + return candidate + }) + .filter((filePath): filePath is string => !!filePath), + ), ).sort((a, b) => priority(a) - priority(b)) if (matchingPaths.length > 0) { diff --git a/npm-app/src/dev-process-manager.ts b/npm-app/src/dev-process-manager.ts index 8730df8bd..7ef77cb2b 100644 --- a/npm-app/src/dev-process-manager.ts +++ b/npm-app/src/dev-process-manager.ts @@ -1,9 +1,7 @@ -// TODO: Replace lodash has with native property check using optional chaining or Object.prototype.hasOwnProperty.call. import path from 'path' import { codebuffConfigFile } from '@codebuff/common/json-config/constants' import { generateCompactId } from '@codebuff/common/util/string' -import { has } from 'lodash' import { yellow } from 'picocolors' import { runBackgroundCommand } from './terminal/background' @@ -60,7 +58,8 @@ export function startDevProcesses( stderrFile, }, (result) => { - if (has(result[0].value, 'processId')) { + const value = result[0].value as any + if ('processId' in value) { console.log(yellow(`- ${name}: ${command}`)) } else { console.log(yellow(`- ${name}: ${command} — failed to start`)) diff --git a/npm-app/src/json-config/hooks.ts b/npm-app/src/json-config/hooks.ts index cafa99bcb..5e6aca537 100644 --- a/npm-app/src/json-config/hooks.ts +++ b/npm-app/src/json-config/hooks.ts @@ -1,5 +1,4 @@ import { generateCompactId } from '@codebuff/common/util/string' -import { has } from 'lodash' import micromatch from 'micromatch' import { bold, gray } from 'picocolors' @@ -67,7 +66,8 @@ export async function runFileChangeHooks(filesChanged: string[]): Promise<{ undefined, undefined, ) - if (has(result[0].value, 'exitCode') && result[0].value.exitCode !== 0) { + const value = result[0].value as any + if ('exitCode' in value && value.exitCode !== 0) { someHooksFailed = true // Show user this hook failed? // logger.warn( diff --git a/package.json b/package.json index ac7e93dd6..1a2c585a8 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "devDependencies": { "@tanstack/react-query": "^5.59.16", "@types/bun": "^1.3.0", - "@types/lodash": "4.17.7", "@types/node": "^22.9.0", "@types/node-fetch": "^2.6.12", "@types/parse-path": "^7.1.0", @@ -58,7 +57,6 @@ "eslint-plugin-import": "^2.29.1", "eslint-plugin-unused-imports": "^4.1.4", "ignore": "^6.0.2", - "lodash": "4.17.21", "prettier": "3.3.2", "ts-node": "^10.9.2", "ts-pattern": "^5.5.0", diff --git a/packages/agent-runtime/src/find-files/request-files-prompt.ts b/packages/agent-runtime/src/find-files/request-files-prompt.ts index 19156a6c4..82d0e16de 100644 --- a/packages/agent-runtime/src/find-files/request-files-prompt.ts +++ b/packages/agent-runtime/src/find-files/request-files-prompt.ts @@ -6,7 +6,21 @@ import { type FinetunedVertexModel, } from '@codebuff/common/old-constants' import { getAllFilePaths } from '@codebuff/common/project-file-tree' -import { range, shuffle, uniq } from 'lodash' + +// Fisher-Yates shuffle algorithm +function shuffle(array: T[]): T[] { + const result = [...array] + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[result[i], result[j]] = [result[j], result[i]] + } + return result +} + +// Generate a range of numbers +function range(count: number): number[] { + return Array.from({ length: count }, (_, i) => i) +} import { promptFlashWithFallbacks } from '../llm-api/gemini-with-fallbacks' import { @@ -82,7 +96,7 @@ export async function requestRelevantFiles( const candidateFiles = (await keyPromise).files - validateFilePaths(uniq(candidateFiles)) + validateFilePaths(Array.from(new Set(candidateFiles))) // logger.info( // { @@ -149,7 +163,7 @@ export async function requestRelevantFilesForTraining( }) const candidateFiles = [...keyFiles.files, ...nonObviousFiles.files] - const validatedFiles = validateFilePaths(uniq(candidateFiles)) + const validatedFiles = validateFilePaths(Array.from(new Set(candidateFiles))) logger.debug( { keyFiles, nonObviousFiles, validatedFiles }, 'requestRelevantFilesForTraining: results', @@ -322,7 +336,10 @@ function getExampleFileList(params: { selectedDirectories.add(dirname(filePath)) } - return uniq([...selectedFiles, ...randomFilePaths]).slice(0, count) + return Array.from(new Set([...selectedFiles, ...randomFilePaths])).slice( + 0, + count, + ) } function generateNonObviousRequestFilesPrompt( diff --git a/packages/agent-runtime/src/get-file-reading-updates.ts b/packages/agent-runtime/src/get-file-reading-updates.ts index c66bca927..a114ee089 100644 --- a/packages/agent-runtime/src/get-file-reading-updates.ts +++ b/packages/agent-runtime/src/get-file-reading-updates.ts @@ -1,5 +1,3 @@ -import { uniq } from 'lodash' - import type { RequestFilesFn } from '@codebuff/common/types/contracts/client' export async function getFileReadingUpdates(params: { @@ -13,7 +11,7 @@ export async function getFileReadingUpdates(params: { > { const { requestFiles, requestedFiles } = params - const allFilePaths = uniq(requestedFiles) + const allFilePaths = Array.from(new Set(requestedFiles)) const loadedFiles = await requestFiles({ filePaths: allFilePaths }) const addedFiles = allFilePaths diff --git a/packages/agent-runtime/src/tools/handlers/tool/write-file.ts b/packages/agent-runtime/src/tools/handlers/tool/write-file.ts index b50e5a533..fb160435f 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/write-file.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/write-file.ts @@ -1,7 +1,27 @@ -import { partition } from 'lodash' - import { processFileBlock } from '../../../process-file-block' +// Partition an array into two arrays based on a predicate +function partition( + array: T[], + predicate: (value: T) => value is S, +): [S[], Exclude[]] +function partition(array: T[], predicate: (value: T) => boolean): [T[], T[]] +function partition( + array: T[], + predicate: (value: T) => boolean, +): [T[], T[]] { + const truthy: T[] = [] + const falsy: T[] = [] + for (const item of array) { + if (predicate(item)) { + truthy.push(item) + } else { + falsy.push(item) + } + } + return [truthy, falsy] +} + import type { CodebuffToolHandlerFunction } from '../handler-function-type' import type { ClientToolCall, diff --git a/packages/agent-runtime/src/util/messages.ts b/packages/agent-runtime/src/util/messages.ts index 8070ba3f4..7be1cd971 100644 --- a/packages/agent-runtime/src/util/messages.ts +++ b/packages/agent-runtime/src/util/messages.ts @@ -3,7 +3,20 @@ import { AssertionError } from 'assert' import { buildArray } from '@codebuff/common/util/array' import { getErrorObject } from '@codebuff/common/util/error' import { closeXml } from '@codebuff/common/util/xml' -import { cloneDeep, isEqual } from 'lodash' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} + +// Deep equality check using JSON serialization +function isEqual(a: unknown, b: unknown): boolean { + try { + return JSON.stringify(a) === JSON.stringify(b) + } catch { + return a === b + } +} import { simplifyTerminalCommandResults } from './simplify-tool-results' import { countTokensJson } from './token-counter' diff --git a/packages/agent-runtime/src/util/simplify-tool-results.ts b/packages/agent-runtime/src/util/simplify-tool-results.ts index 8ecd1458d..405346d3d 100644 --- a/packages/agent-runtime/src/util/simplify-tool-results.ts +++ b/packages/agent-runtime/src/util/simplify-tool-results.ts @@ -1,5 +1,9 @@ import { getErrorObject } from '@codebuff/common/util/error' -import { cloneDeep } from 'lodash' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} import type { CodebuffToolOutput } from '@codebuff/common/tools/list' import type { Logger } from '@codebuff/common/types/contracts/logger' diff --git a/scripts/analyze-edit-blocks.ts b/scripts/analyze-edit-blocks.ts index b993c58d1..c91ffe99d 100644 --- a/scripts/analyze-edit-blocks.ts +++ b/scripts/analyze-edit-blocks.ts @@ -1,4 +1,3 @@ -// TODO: Replace lodash shuffle with a native shuffle implementation using Fisher-Yates algorithm (e.g., for arrays). import fs from 'fs' import path from 'path' @@ -6,7 +5,16 @@ import { db } from '@codebuff/common/db' import * as schema from '@codebuff/common/db/schema' import { fileRegex } from '@codebuff/common/util/file' import { eq, desc } from 'drizzle-orm' -import { shuffle } from 'lodash' + +// Fisher-Yates shuffle algorithm +function shuffle(array: T[]): T[] { + const result = [...array] + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[result[i], result[j]] = [result[j], result[i]] + } + return result +} interface EditBlock { filePath: string diff --git a/scripts/package.json b/scripts/package.json index 1777df15d..eceb3546f 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -24,12 +24,10 @@ "@ai-sdk/openai-compatible": "^1.0.19", "@codebuff/backend": "workspace:*", "@codebuff/bigquery": "workspace:*", - "@codebuff/common": "workspace:*", - "lodash": "^4.17.21" + "@codebuff/common": "workspace:*" }, "devDependencies": { "@types/bun": "^1.3.0", - "@types/lodash": "^4.14.195", "@types/node": "22" } } diff --git a/sdk/src/run-state.ts b/sdk/src/run-state.ts index ef0f58530..3b7536918 100644 --- a/sdk/src/run-state.ts +++ b/sdk/src/run-state.ts @@ -2,7 +2,11 @@ import * as os from 'os' import path from 'path' import { getFileTokenScores } from '@codebuff/code-map/parse' -import { cloneDeep } from 'lodash' + +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} import { getProjectFileTree, @@ -36,7 +40,7 @@ export type InitialSessionStateOptions = { customToolDefinitions?: CustomToolDefinition[] maxAgentSteps?: number fs?: CodebuffFileSystem -}; +} /** * Processes agent definitions array and converts handleSteps functions to strings @@ -167,11 +171,11 @@ function deriveKnowledgeFiles( export function initialSessionState( options: InitialSessionStateOptions, -): Promise; +): Promise export function initialSessionState( cwd: string, options?: Omit, -): Promise; +): Promise export async function initialSessionState( arg1: string | InitialSessionStateOptions, arg2?: Omit, diff --git a/sdk/src/run.ts b/sdk/src/run.ts index 1187d6ba8..81f8b9bbf 100644 --- a/sdk/src/run.ts +++ b/sdk/src/run.ts @@ -1,6 +1,9 @@ import path from 'path' -import { cloneDeep } from 'lodash' +// Deep clone using JSON serialization (works for serializable objects) +function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) +} import { initialSessionState, applyOverridesToSessionState } from './run-state' import { stripToolCallPayloads } from './tool-xml-buffer' @@ -466,8 +469,7 @@ export async function run({ chunkType !== 'subagent-finish' ) { await emitPendingSection(ROOT_AGENT_KEY) - const pendingAgentId = - 'agentId' in chunk ? chunk.agentId : undefined + const pendingAgentId = 'agentId' in chunk ? chunk.agentId : undefined if (pendingAgentId && pendingAgentId !== ROOT_AGENT_KEY) { await emitPendingSection(pendingAgentId, pendingAgentId) } From 54317166d6e345eaf1c2ea2e431cb698f98efa30 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 21 Oct 2025 15:09:33 -0700 Subject: [PATCH 06/10] refactor: consolidate lodash replacement utilities into a shared module to reduce duplication and standardize usage across backend, sdk, and tooling code. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- backend/src/run-agent-step.ts | 6 +--- backend/src/run-programmatic-step.ts | 6 +--- backend/src/tools/stream-parser.ts | 6 +--- backend/src/tools/tool-executor.ts | 6 +--- common/src/util/array.ts | 10 +----- common/src/util/messages.ts | 36 ++++++------------- common/src/util/object.ts | 26 ++------------ common/src/util/string.ts | 5 +-- evals/git-evals/run-git-evals.ts | 6 +--- .../src/find-files/request-files-prompt.ts | 16 +-------- .../src/tools/handlers/tool/write-file.ts | 24 ++----------- packages/agent-runtime/src/util/messages.ts | 15 +------- .../src/util/simplify-tool-results.ts | 6 +--- scripts/analyze-edit-blocks.ts | 11 +----- sdk/src/run-state.ts | 6 +--- sdk/src/run.ts | 5 +-- 16 files changed, 27 insertions(+), 163 deletions(-) diff --git a/backend/src/run-agent-step.ts b/backend/src/run-agent-step.ts index f7a4a8ea7..180a406ef 100644 --- a/backend/src/run-agent-step.ts +++ b/backend/src/run-agent-step.ts @@ -19,11 +19,7 @@ import { supportsCacheControl } from '@codebuff/common/old-constants' import { TOOLS_WHICH_WONT_FORCE_NEXT_STEP } from '@codebuff/common/tools/constants' import { buildArray } from '@codebuff/common/util/array' import { getErrorObject } from '@codebuff/common/util/error' - -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import { runProgrammaticStep } from './run-programmatic-step' import { processStreamWithTools } from './tools/stream-parser' diff --git a/backend/src/run-programmatic-step.ts b/backend/src/run-programmatic-step.ts index f8b01173f..9286fd647 100644 --- a/backend/src/run-programmatic-step.ts +++ b/backend/src/run-programmatic-step.ts @@ -1,11 +1,7 @@ import { SandboxManager } from '@codebuff/agent-runtime/util/quickjs-sandbox' import { getToolCallString } from '@codebuff/common/tools/utils' import { getErrorObject } from '@codebuff/common/util/error' - -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import { executeToolCall } from './tools/tool-executor' diff --git a/backend/src/tools/stream-parser.ts b/backend/src/tools/stream-parser.ts index 98acb69dc..59e9e381b 100644 --- a/backend/src/tools/stream-parser.ts +++ b/backend/src/tools/stream-parser.ts @@ -7,13 +7,9 @@ import { toolNames, } from '@codebuff/common/tools/constants' import { buildArray } from '@codebuff/common/util/array' +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import { generateCompactId } from '@codebuff/common/util/string' -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} - import { executeCustomToolCall, executeToolCall } from './tool-executor' import type { CustomToolCall, ExecuteToolCallParams } from './tool-executor' diff --git a/backend/src/tools/tool-executor.ts b/backend/src/tools/tool-executor.ts index 0eb8655f1..471cf2959 100644 --- a/backend/src/tools/tool-executor.ts +++ b/backend/src/tools/tool-executor.ts @@ -2,14 +2,10 @@ import { checkLiveUserInput } from '@codebuff/agent-runtime/live-user-inputs' import { getMCPToolData } from '@codebuff/agent-runtime/mcp' import { codebuffToolDefs } from '@codebuff/agent-runtime/tools/definitions/list' import { endsAgentStepParam } from '@codebuff/common/tools/constants' +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import { generateCompactId } from '@codebuff/common/util/string' import { type ToolCallPart } from 'ai' import z from 'zod/v4' - -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} import { convertJsonSchemaToZod } from 'zod-from-json-schema' import { codebuffToolHandlers } from './handlers/list' diff --git a/common/src/util/array.ts b/common/src/util/array.ts index b14801ea7..8cddb831f 100644 --- a/common/src/util/array.ts +++ b/common/src/util/array.ts @@ -1,12 +1,4 @@ -// Deep equality check using JSON serialization -// Note: Not exported to avoid type recursion issues -function isEqual(a: unknown, b: unknown): boolean { - try { - return JSON.stringify(a) === JSON.stringify(b) - } catch { - return a === b - } -} +import { isEqual } from './lodash-replacements' export function filterDefined(array: (T | null | undefined)[]) { return array.filter((item) => item !== null && item !== undefined) as T[] diff --git a/common/src/util/messages.ts b/common/src/util/messages.ts index 99a5bf4d2..ae3a14131 100644 --- a/common/src/util/messages.ts +++ b/common/src/util/messages.ts @@ -1,18 +1,6 @@ import { buildArray } from './array' +import { cloneDeep, isEqual } from './lodash-replacements' -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} - -// Deep equality check using JSON serialization -function isEqual(a: unknown, b: unknown): boolean { - try { - return JSON.stringify(a) === JSON.stringify(b) - } catch { - return a === b - } -} import { getToolCallString } from '../tools/utils' import type { @@ -34,9 +22,9 @@ export function toContentString(msg: ModelMessage): string { export function withCacheControl< T extends { providerOptions?: ProviderMetadata }, >(obj: T): T { - const wrapper = cloneDeep(obj) + const wrapper = cloneDeep(obj) as T if (!wrapper.providerOptions) { - wrapper.providerOptions = {} + wrapper.providerOptions = {} as ProviderMetadata } if (!wrapper.providerOptions.anthropic) { wrapper.providerOptions.anthropic = {} @@ -52,12 +40,10 @@ export function withCacheControl< export function withoutCacheControl< T extends { providerOptions?: ProviderMetadata }, >(obj: T): T { - const wrapper = cloneDeep(obj) - if ( - wrapper.providerOptions?.anthropic?.cacheControl && - 'type' in wrapper.providerOptions.anthropic.cacheControl - ) { - delete (wrapper.providerOptions.anthropic.cacheControl as any).type + const wrapper = cloneDeep(obj) as T + const anthropicCache = wrapper.providerOptions?.anthropic?.cacheControl + if (anthropicCache && typeof anthropicCache === 'object' && 'type' in anthropicCache) { + delete (anthropicCache as any).type } if ( Object.keys(wrapper.providerOptions?.anthropic?.cacheControl ?? {}) @@ -69,11 +55,9 @@ export function withoutCacheControl< delete wrapper.providerOptions?.anthropic } - if ( - wrapper.providerOptions?.openrouter?.cacheControl && - 'type' in wrapper.providerOptions.openrouter.cacheControl - ) { - delete (wrapper.providerOptions.openrouter.cacheControl as any).type + const openrouterCache = wrapper.providerOptions?.openrouter?.cacheControl + if (openrouterCache && typeof openrouterCache === 'object' && 'type' in openrouterCache) { + delete (openrouterCache as any).type } if ( Object.keys(wrapper.providerOptions?.openrouter?.cacheControl ?? {}) diff --git a/common/src/util/object.ts b/common/src/util/object.ts index 45c4246d2..20df14445 100644 --- a/common/src/util/object.ts +++ b/common/src/util/object.ts @@ -1,26 +1,4 @@ -// Deep equality check using JSON serialization -function isEqual(a: unknown, b: unknown): boolean { - try { - return JSON.stringify(a) === JSON.stringify(b) - } catch { - return a === b - } -} - -// Map values of an object -function mapValues( - obj: T, - fn: (value: any, key: keyof T) => R, -): { [K in keyof T]: R } { - return Object.fromEntries( - Object.entries(obj).map(([k, v]) => [k, fn(v, k as keyof T)]), - ) as { [K in keyof T]: R } -} - -// Union of two arrays -function union(arr1: T[], arr2: T[]): T[] { - return Array.from(new Set([...arr1, ...arr2])) -} +import { isEqual, mapValues, union } from './lodash-replacements' export const removeUndefinedProps = ( obj: T, @@ -81,7 +59,7 @@ export const subtractObjects = ( } export const hasChanges = (obj: T, partial: Partial) => { - const currValues = mapValues(partial, (_, key: keyof T) => obj[key]) + const currValues = mapValues(partial as T, (_, key: keyof T) => obj[key]) return !isEqual(currValues, partial as any) } diff --git a/common/src/util/string.ts b/common/src/util/string.ts index 32e1fdb3a..43460f9d7 100644 --- a/common/src/util/string.ts +++ b/common/src/util/string.ts @@ -1,7 +1,4 @@ -// Sum an array by extracting numeric values with a function -function sumBy(arr: T[], fn: (item: T) => number): number { - return arr.reduce((sum, item) => sum + fn(item), 0) -} +import { sumBy } from './lodash-replacements' export const truncateString = (str: string, maxLength: number) => { if (str.length <= maxLength) { diff --git a/evals/git-evals/run-git-evals.ts b/evals/git-evals/run-git-evals.ts index 0af0ffbd2..f30aec54d 100644 --- a/evals/git-evals/run-git-evals.ts +++ b/evals/git-evals/run-git-evals.ts @@ -5,15 +5,11 @@ import path from 'path' import { disableLiveUserInputCheck } from '@codebuff/agent-runtime/live-user-inputs' import { promptAiSdkStructured } from '@codebuff/backend/llm-apis/vercel-ai-sdk/ai-sdk' import { getErrorObject } from '@codebuff/common/util/error' +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import { withTimeout } from '@codebuff/common/util/promise' import { generateCompactId } from '@codebuff/common/util/string' import pLimit from 'p-limit' -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} - import { resetRepoToCommit } from '../scaffolding' import { createInitialSessionState } from '../test-setup' import { judgeEvalRun } from './judge-git-eval' diff --git a/packages/agent-runtime/src/find-files/request-files-prompt.ts b/packages/agent-runtime/src/find-files/request-files-prompt.ts index 82d0e16de..c833bec22 100644 --- a/packages/agent-runtime/src/find-files/request-files-prompt.ts +++ b/packages/agent-runtime/src/find-files/request-files-prompt.ts @@ -6,21 +6,7 @@ import { type FinetunedVertexModel, } from '@codebuff/common/old-constants' import { getAllFilePaths } from '@codebuff/common/project-file-tree' - -// Fisher-Yates shuffle algorithm -function shuffle(array: T[]): T[] { - const result = [...array] - for (let i = result.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)) - ;[result[i], result[j]] = [result[j], result[i]] - } - return result -} - -// Generate a range of numbers -function range(count: number): number[] { - return Array.from({ length: count }, (_, i) => i) -} +import { range, shuffle } from '@codebuff/common/util/lodash-replacements' import { promptFlashWithFallbacks } from '../llm-api/gemini-with-fallbacks' import { diff --git a/packages/agent-runtime/src/tools/handlers/tool/write-file.ts b/packages/agent-runtime/src/tools/handlers/tool/write-file.ts index fb160435f..804b4533a 100644 --- a/packages/agent-runtime/src/tools/handlers/tool/write-file.ts +++ b/packages/agent-runtime/src/tools/handlers/tool/write-file.ts @@ -1,26 +1,6 @@ -import { processFileBlock } from '../../../process-file-block' +import { partition } from '@codebuff/common/util/lodash-replacements' -// Partition an array into two arrays based on a predicate -function partition( - array: T[], - predicate: (value: T) => value is S, -): [S[], Exclude[]] -function partition(array: T[], predicate: (value: T) => boolean): [T[], T[]] -function partition( - array: T[], - predicate: (value: T) => boolean, -): [T[], T[]] { - const truthy: T[] = [] - const falsy: T[] = [] - for (const item of array) { - if (predicate(item)) { - truthy.push(item) - } else { - falsy.push(item) - } - } - return [truthy, falsy] -} +import { processFileBlock } from '../../../process-file-block' import type { CodebuffToolHandlerFunction } from '../handler-function-type' import type { diff --git a/packages/agent-runtime/src/util/messages.ts b/packages/agent-runtime/src/util/messages.ts index 7be1cd971..d02d7a8e7 100644 --- a/packages/agent-runtime/src/util/messages.ts +++ b/packages/agent-runtime/src/util/messages.ts @@ -2,22 +2,9 @@ import { AssertionError } from 'assert' import { buildArray } from '@codebuff/common/util/array' import { getErrorObject } from '@codebuff/common/util/error' +import { cloneDeep, isEqual } from '@codebuff/common/util/lodash-replacements' import { closeXml } from '@codebuff/common/util/xml' -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} - -// Deep equality check using JSON serialization -function isEqual(a: unknown, b: unknown): boolean { - try { - return JSON.stringify(a) === JSON.stringify(b) - } catch { - return a === b - } -} - import { simplifyTerminalCommandResults } from './simplify-tool-results' import { countTokensJson } from './token-counter' diff --git a/packages/agent-runtime/src/util/simplify-tool-results.ts b/packages/agent-runtime/src/util/simplify-tool-results.ts index 405346d3d..8b6dc6ce5 100644 --- a/packages/agent-runtime/src/util/simplify-tool-results.ts +++ b/packages/agent-runtime/src/util/simplify-tool-results.ts @@ -1,9 +1,5 @@ import { getErrorObject } from '@codebuff/common/util/error' - -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import type { CodebuffToolOutput } from '@codebuff/common/tools/list' import type { Logger } from '@codebuff/common/types/contracts/logger' diff --git a/scripts/analyze-edit-blocks.ts b/scripts/analyze-edit-blocks.ts index c91ffe99d..472561a22 100644 --- a/scripts/analyze-edit-blocks.ts +++ b/scripts/analyze-edit-blocks.ts @@ -4,18 +4,9 @@ import path from 'path' import { db } from '@codebuff/common/db' import * as schema from '@codebuff/common/db/schema' import { fileRegex } from '@codebuff/common/util/file' +import { shuffle } from '@codebuff/common/util/lodash-replacements' import { eq, desc } from 'drizzle-orm' -// Fisher-Yates shuffle algorithm -function shuffle(array: T[]): T[] { - const result = [...array] - for (let i = result.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)) - ;[result[i], result[j]] = [result[j], result[i]] - } - return result -} - interface EditBlock { filePath: string content: string diff --git a/sdk/src/run-state.ts b/sdk/src/run-state.ts index 3b7536918..e6c949b58 100644 --- a/sdk/src/run-state.ts +++ b/sdk/src/run-state.ts @@ -2,11 +2,7 @@ import * as os from 'os' import path from 'path' import { getFileTokenScores } from '@codebuff/code-map/parse' - -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import { getProjectFileTree, diff --git a/sdk/src/run.ts b/sdk/src/run.ts index 81f8b9bbf..cbda50509 100644 --- a/sdk/src/run.ts +++ b/sdk/src/run.ts @@ -1,9 +1,6 @@ import path from 'path' -// Deep clone using JSON serialization (works for serializable objects) -function cloneDeep(obj: T): T { - return JSON.parse(JSON.stringify(obj)) -} +import { cloneDeep } from '@codebuff/common/util/lodash-replacements' import { initialSessionState, applyOverridesToSessionState } from './run-state' import { stripToolCallPayloads } from './tool-xml-buffer' From d6e1fd5a42d8bd9f23bff19a6caf48281dde7816 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 21 Oct 2025 15:10:12 -0700 Subject: [PATCH 07/10] Consolidate duplicated lodash replacement utilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created common/src/util/lodash-replacements.ts as a shared module containing all lodash replacement functions (cloneDeep, isEqual, shuffle, range, sumBy, mapValues, union, partition). Updated 13 files to import from shared module instead of duplicating: - common/src/util/{array,object,string,messages}.ts - backend/src/{run-agent-step,run-programmatic-step}.ts - backend/src/tools/{stream-parser,tool-executor}.ts - packages/agent-runtime/src/util/{messages,simplify-tool-results}.ts - packages/agent-runtime/src/tools/handlers/tool/write-file.ts - packages/agent-runtime/src/find-files/request-files-prompt.ts - sdk/src/{run,run-state}.ts - evals/git-evals/run-git-evals.ts - scripts/analyze-edit-blocks.ts Removed ~150 lines of duplicated code. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- common/src/util/lodash-replacements.ts | 76 ++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 common/src/util/lodash-replacements.ts diff --git a/common/src/util/lodash-replacements.ts b/common/src/util/lodash-replacements.ts new file mode 100644 index 000000000..3ac83f57e --- /dev/null +++ b/common/src/util/lodash-replacements.ts @@ -0,0 +1,76 @@ +// Shared lodash replacement utilities +// These functions replace lodash with native JavaScript implementations + +// Deep clone using JSON serialization (works for serializable objects) +export function cloneDeep(obj: T): T { + return JSON.parse(JSON.stringify(obj)) as T +} + +// Deep equality check using JSON serialization +export function isEqual(a: unknown, b: unknown): boolean { + try { + return JSON.stringify(a) === JSON.stringify(b) + } catch { + return a === b + } +} + +// Fisher-Yates shuffle algorithm +export function shuffle(array: T[]): T[] { + const result = [...array] + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[result[i], result[j]] = [result[j], result[i]] + } + return result +} + +// Generate a range of numbers +export function range(count: number): number[] { + return Array.from({ length: count }, (_, i) => i) +} + +// Sum an array by extracting numeric values with a function +export function sumBy(arr: T[], fn: (item: T) => number): number { + return arr.reduce((sum, item) => sum + fn(item), 0) +} + +// Map values of an object +export function mapValues( + obj: T, + fn: (value: any, key: keyof T) => R, +): { [K in keyof T]: R } { + return Object.fromEntries( + Object.entries(obj).map(([k, v]) => [k, fn(v, k as keyof T)]), + ) as { [K in keyof T]: R } +} + +// Union of two arrays +export function union(arr1: T[], arr2: T[]): T[] { + return Array.from(new Set([...arr1, ...arr2])) +} + +// Partition an array into two arrays based on a predicate +export function partition( + array: T[], + predicate: (value: T) => value is S, +): [S[], Exclude[]]; +export function partition( + array: T[], + predicate: (value: T) => boolean, +): [T[], T[]]; +export function partition( + array: T[], + predicate: (value: T) => boolean, +): [T[], T[]] { + const truthy: T[] = [] + const falsy: T[] = [] + for (const item of array) { + if (predicate(item)) { + truthy.push(item) + } else { + falsy.push(item) + } + } + return [truthy, falsy] +} From 18baa895d42c3791c9f799993b486481f38e23cc Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 21 Oct 2025 15:36:35 -0700 Subject: [PATCH 08/10] feat(util): introduce uniq() utility and standardize array dedup across codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralizes array dedup using a new uniq() function to replace scattered Set-based patterns and prepare groundwork for removing lodash replacements. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- common/src/util/lodash-replacements.ts | 9 +++++++-- .../src/find-files/request-files-prompt.ts | 11 ++++------- .../agent-runtime/src/get-file-reading-updates.ts | 3 ++- packages/code-map/src/parse.ts | 6 ++++-- scripts/generate-ci-env.js | 3 ++- web/src/lib/docs.ts | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/common/src/util/lodash-replacements.ts b/common/src/util/lodash-replacements.ts index 3ac83f57e..9a0d77559 100644 --- a/common/src/util/lodash-replacements.ts +++ b/common/src/util/lodash-replacements.ts @@ -54,11 +54,11 @@ export function union(arr1: T[], arr2: T[]): T[] { export function partition( array: T[], predicate: (value: T) => value is S, -): [S[], Exclude[]]; +): [S[], Exclude[]] export function partition( array: T[], predicate: (value: T) => boolean, -): [T[], T[]]; +): [T[], T[]] export function partition( array: T[], predicate: (value: T) => boolean, @@ -74,3 +74,8 @@ export function partition( } return [truthy, falsy] } + +// Remove duplicates from an array +export function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)) +} diff --git a/packages/agent-runtime/src/find-files/request-files-prompt.ts b/packages/agent-runtime/src/find-files/request-files-prompt.ts index c833bec22..457071736 100644 --- a/packages/agent-runtime/src/find-files/request-files-prompt.ts +++ b/packages/agent-runtime/src/find-files/request-files-prompt.ts @@ -6,7 +6,7 @@ import { type FinetunedVertexModel, } from '@codebuff/common/old-constants' import { getAllFilePaths } from '@codebuff/common/project-file-tree' -import { range, shuffle } from '@codebuff/common/util/lodash-replacements' +import { range, shuffle, uniq } from '@codebuff/common/util/lodash-replacements' import { promptFlashWithFallbacks } from '../llm-api/gemini-with-fallbacks' import { @@ -82,7 +82,7 @@ export async function requestRelevantFiles( const candidateFiles = (await keyPromise).files - validateFilePaths(Array.from(new Set(candidateFiles))) + validateFilePaths(uniq(candidateFiles)) // logger.info( // { @@ -149,7 +149,7 @@ export async function requestRelevantFilesForTraining( }) const candidateFiles = [...keyFiles.files, ...nonObviousFiles.files] - const validatedFiles = validateFilePaths(Array.from(new Set(candidateFiles))) + const validatedFiles = validateFilePaths(uniq(candidateFiles)) logger.debug( { keyFiles, nonObviousFiles, validatedFiles }, 'requestRelevantFilesForTraining: results', @@ -322,10 +322,7 @@ function getExampleFileList(params: { selectedDirectories.add(dirname(filePath)) } - return Array.from(new Set([...selectedFiles, ...randomFilePaths])).slice( - 0, - count, - ) + return uniq([...selectedFiles, ...randomFilePaths]).slice(0, count) } function generateNonObviousRequestFilesPrompt( diff --git a/packages/agent-runtime/src/get-file-reading-updates.ts b/packages/agent-runtime/src/get-file-reading-updates.ts index a114ee089..23646fc8f 100644 --- a/packages/agent-runtime/src/get-file-reading-updates.ts +++ b/packages/agent-runtime/src/get-file-reading-updates.ts @@ -1,4 +1,5 @@ import type { RequestFilesFn } from '@codebuff/common/types/contracts/client' +import { uniq } from '@codebuff/common/util/lodash-replacements' export async function getFileReadingUpdates(params: { requestFiles: RequestFilesFn @@ -11,7 +12,7 @@ export async function getFileReadingUpdates(params: { > { const { requestFiles, requestedFiles } = params - const allFilePaths = Array.from(new Set(requestedFiles)) + const allFilePaths = uniq(requestedFiles) const loadedFiles = await requestFiles({ filePaths: allFilePaths }) const addedFiles = allFilePaths diff --git a/packages/code-map/src/parse.ts b/packages/code-map/src/parse.ts index 8c651b80d..000b81d99 100644 --- a/packages/code-map/src/parse.ts +++ b/packages/code-map/src/parse.ts @@ -1,6 +1,8 @@ import * as fs from 'fs' import * as path from 'path' +import { uniq } from '@codebuff/common/util/lodash-replacements' + import { getLanguageConfig, LanguageConfig } from './languages' import type { Parser, Query } from 'web-tree-sitter' @@ -172,8 +174,8 @@ export function parseTokens( throw new Error('Parser or query not found') } const parseResults = parseFile(parser, query, sourceCode) - const identifiers = Array.from(new Set(parseResults.identifier)) - const calls = Array.from(new Set(parseResults['call.identifier'])) + const identifiers = uniq(parseResults.identifier) + const calls = uniq(parseResults['call.identifier']) if (DEBUG_PARSING) { console.log(`\nParsing ${filePath}:`) diff --git a/scripts/generate-ci-env.js b/scripts/generate-ci-env.js index 40e9221b6..91de1020a 100644 --- a/scripts/generate-ci-env.js +++ b/scripts/generate-ci-env.js @@ -7,6 +7,7 @@ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' +import { uniq } from '../common/src/util/lodash-replacements.ts' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) @@ -88,7 +89,7 @@ function generateGitHubEnv() { } else if (scope === 'client') { selected = varsByScope.client } else { - selected = Array.from(new Set([...varsByScope.server, ...varsByScope.client])) + selected = uniq([...varsByScope.server, ...varsByScope.client]) } if (prefix) { diff --git a/web/src/lib/docs.ts b/web/src/lib/docs.ts index fa9a66696..7d4c772ba 100644 --- a/web/src/lib/docs.ts +++ b/web/src/lib/docs.ts @@ -34,5 +34,5 @@ export async function getNewsArticles(): Promise { // export function getAllCategories() { // if (!allDocs) return [] -// return Array.from(new Set((allDocs as Doc[]).map((doc: Doc) => doc.category))) +// return uniq((allDocs as Doc[]).map((doc: Doc) => doc.category)) // } From d2d6f5baeb682fb9eb419b9699fb4aa2194852b3 Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 21 Oct 2025 16:22:45 -0700 Subject: [PATCH 09/10] Merge origin/main and replace lodash/debounce with native implementation - Merged origin/main into remove-lodash-comments branch with no conflicts - Added native debounce function to common/src/util/lodash-replacements.ts - Replaced lodash/debounce imports in web hooks with shared utility - All typechecks pass successfully --- common/src/util/lodash-replacements.ts | 41 ++++++++++++++++++++++++++ web/src/hooks/use-auto-topup.ts | 30 +++++++++---------- web/src/hooks/use-org-auto-topup.ts | 34 ++++++++++----------- 3 files changed, 73 insertions(+), 32 deletions(-) diff --git a/common/src/util/lodash-replacements.ts b/common/src/util/lodash-replacements.ts index 9a0d77559..2d5eec016 100644 --- a/common/src/util/lodash-replacements.ts +++ b/common/src/util/lodash-replacements.ts @@ -79,3 +79,44 @@ export function partition( export function uniq(arr: T[]): T[] { return Array.from(new Set(arr)) } + +// Add debounce implementation export below +export function debounce any>(fn: T, wait = 0) { + let timeout: ReturnType | null = null + let lastArgs: any + let lastThis: any + let result: ReturnType + + const debounced = function (this: any, ...args: Parameters) { + lastArgs = args + lastThis = this + if (timeout) clearTimeout(timeout) + timeout = setTimeout(() => { + timeout = null + result = fn.apply(lastThis, lastArgs) + }, wait) + return result + } as unknown as T & { + cancel: () => void + flush: () => ReturnType | undefined + } + + debounced.cancel = () => { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + } + + debounced.flush = () => { + if (timeout) { + clearTimeout(timeout) + timeout = null + result = fn.apply(lastThis, lastArgs) + return result + } + return result + } + + return debounced +} diff --git a/web/src/hooks/use-auto-topup.ts b/web/src/hooks/use-auto-topup.ts index 7e21b0ad1..5adbcf829 100644 --- a/web/src/hooks/use-auto-topup.ts +++ b/web/src/hooks/use-auto-topup.ts @@ -1,6 +1,6 @@ import { convertStripeGrantAmountToCredits } from '@codebuff/common/util/currency' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import debounce from 'lodash/debounce' +import { debounce } from '@codebuff/common/util/lodash-replacements' import { useState, useCallback, useRef, useEffect } from 'react' import type { AutoTopupState } from '@/components/auto-topup/types' @@ -49,12 +49,12 @@ export function useAutoTopup(): AutoTopupState { auto_topup_threshold: clamp( thresholdCredits, MIN_THRESHOLD_CREDITS, - MAX_THRESHOLD_CREDITS + MAX_THRESHOLD_CREDITS, ), initialTopUpDollars: clamp( topUpDollars > 0 ? topUpDollars : MIN_TOPUP_DOLLARS, MIN_TOPUP_DOLLARS, - MAX_TOPUP_DOLLARS + MAX_TOPUP_DOLLARS, ), } }, @@ -76,7 +76,7 @@ export function useAutoTopup(): AutoTopupState { setIsEnabled(userProfile.auto_topup_enabled ?? false) setThreshold(userProfile.auto_topup_threshold ?? MIN_THRESHOLD_CREDITS) setTopUpAmountDollars( - userProfile.initialTopUpDollars ?? MIN_TOPUP_DOLLARS + userProfile.initialTopUpDollars ?? MIN_TOPUP_DOLLARS, ) setTimeout(() => { isInitialLoad.current = false @@ -91,7 +91,7 @@ export function useAutoTopup(): AutoTopupState { UserProfile, 'auto_topup_enabled' | 'auto_topup_threshold' | 'auto_topup_amount' > - > + >, ) => { const payload = { enabled: settings.auto_topup_enabled, @@ -121,20 +121,20 @@ export function useAutoTopup(): AutoTopupState { const topUpCredits = convertStripeGrantAmountToCredits( payload.amount * 100, - CENTS_PER_CREDIT + CENTS_PER_CREDIT, ) const minTopUpCredits = convertStripeGrantAmountToCredits( MIN_TOPUP_DOLLARS * 100, - CENTS_PER_CREDIT + CENTS_PER_CREDIT, ) const maxTopUpCredits = convertStripeGrantAmountToCredits( MAX_TOPUP_DOLLARS * 100, - CENTS_PER_CREDIT + CENTS_PER_CREDIT, ) if (topUpCredits < minTopUpCredits || topUpCredits > maxTopUpCredits) { throw new Error( - `Top-up amount must result in between ${minTopUpCredits} and ${maxTopUpCredits} credits.` + `Top-up amount must result in between ${minTopUpCredits} and ${maxTopUpCredits} credits.`, ) } } @@ -196,7 +196,7 @@ export function useAutoTopup(): AutoTopupState { setIsEnabled(updatedData.auto_topup_enabled ?? false) setThreshold(updatedData.auto_topup_threshold ?? MIN_THRESHOLD_CREDITS) setTopUpAmountDollars( - updatedData.initialTopUpDollars ?? MIN_TOPUP_DOLLARS + updatedData.initialTopUpDollars ?? MIN_TOPUP_DOLLARS, ) return updatedData @@ -214,7 +214,7 @@ export function useAutoTopup(): AutoTopupState { setIsEnabled(userProfile.auto_topup_enabled ?? false) setThreshold(userProfile.auto_topup_threshold ?? MIN_THRESHOLD_CREDITS) setTopUpAmountDollars( - userProfile.initialTopUpDollars ?? MIN_TOPUP_DOLLARS + userProfile.initialTopUpDollars ?? MIN_TOPUP_DOLLARS, ) } pendingSettings.current = null @@ -246,7 +246,7 @@ export function useAutoTopup(): AutoTopupState { auto_topup_amount: currentTopUpDollars, }) }, 750), - [autoTopupMutation, userProfile] + [autoTopupMutation, userProfile], ) const handleThresholdChange = (rawValue: number) => { @@ -258,7 +258,7 @@ export function useAutoTopup(): AutoTopupState { const validValue = clamp( rawValue, MIN_THRESHOLD_CREDITS, - MAX_THRESHOLD_CREDITS + MAX_THRESHOLD_CREDITS, ) pendingSettings.current = { threshold: validValue, topUpAmountDollars } @@ -335,7 +335,7 @@ export function useAutoTopup(): AutoTopupState { onError: () => { setIsEnabled(false) }, - } + }, ) } else { autoTopupMutation.mutate( @@ -351,7 +351,7 @@ export function useAutoTopup(): AutoTopupState { onError: () => { setIsEnabled(true) }, - } + }, ) } } diff --git a/web/src/hooks/use-org-auto-topup.ts b/web/src/hooks/use-org-auto-topup.ts index 4bb890d98..ca26d6d91 100644 --- a/web/src/hooks/use-org-auto-topup.ts +++ b/web/src/hooks/use-org-auto-topup.ts @@ -1,6 +1,6 @@ import { CREDIT_PRICING } from '@codebuff/common/old-constants' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import debounce from 'lodash/debounce' +import { debounce } from '@codebuff/common/util/lodash-replacements' import { useCallback, useEffect, useRef, useState } from 'react' import { toast } from '@/components/ui/use-toast' @@ -52,7 +52,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { const queryClient = useQueryClient() const [isEnabled, setIsEnabled] = useState(false) const [threshold, setThreshold] = useState( - ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS + ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS, ) const [topUpAmountDollars, setTopUpAmountDollars] = useState(MIN_TOPUP_DOLLARS) @@ -85,18 +85,18 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { autoTopupThreshold: clamp( thresholdCredits, ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS, - ORG_AUTO_TOPUP_CONSTANTS.MAX_THRESHOLD_CREDITS + ORG_AUTO_TOPUP_CONSTANTS.MAX_THRESHOLD_CREDITS, ), autoTopupAmount: clamp( topUpAmount, ORG_AUTO_TOPUP_CONSTANTS.MIN_TOPUP_CREDITS, (ORG_AUTO_TOPUP_CONSTANTS.MAX_TOPUP_DOLLARS * 100) / - ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT + ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT, ), initialTopUpDollars: clamp( topUpDollars > 0 ? topUpDollars : MIN_TOPUP_DOLLARS, MIN_TOPUP_DOLLARS, - ORG_AUTO_TOPUP_CONSTANTS.MAX_TOPUP_DOLLARS + ORG_AUTO_TOPUP_CONSTANTS.MAX_TOPUP_DOLLARS, ), } }, @@ -110,7 +110,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { setIsEnabled(organizationSettings.autoTopupEnabled ?? false) setThreshold( organizationSettings.autoTopupThreshold ?? - ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS + ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS, ) const topUpDollars = (organizationSettings.autoTopupAmount * @@ -129,7 +129,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { autoTopupEnabled: boolean autoTopupThreshold: number autoTopupAmount: number - }> + }>, ) => { const payload = { autoTopupEnabled: settings.autoTopupEnabled, @@ -216,18 +216,18 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { setIsEnabled(updatedData.autoTopupEnabled ?? false) setThreshold( updatedData.autoTopupThreshold ?? - ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS + ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS, ) const topUpDollars = (updatedData.autoTopupAmount * ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT) / 100 setTopUpAmountDollars( - topUpDollars > 0 ? topUpDollars : MIN_TOPUP_DOLLARS + topUpDollars > 0 ? topUpDollars : MIN_TOPUP_DOLLARS, ) return updatedData - } + }, ) pendingSettings.current = null @@ -242,14 +242,14 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { setIsEnabled(organizationSettings.autoTopupEnabled ?? false) setThreshold( organizationSettings.autoTopupThreshold ?? - ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS + ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS, ) const topUpDollars = (organizationSettings.autoTopupAmount * ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT) / 100 setTopUpAmountDollars( - topUpDollars > 0 ? topUpDollars : MIN_TOPUP_DOLLARS + topUpDollars > 0 ? topUpDollars : MIN_TOPUP_DOLLARS, ) } pendingSettings.current = null @@ -265,7 +265,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { topUpAmountDollars: currentTopUpDollars, } = pendingSettings.current const currentTopUpCredits = Math.round( - (currentTopUpDollars * 100) / ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT + (currentTopUpDollars * 100) / ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT, ) if ( @@ -283,7 +283,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { autoTopupAmount: currentTopUpCredits, }) }, 750), - [autoTopupMutation, organizationSettings] + [autoTopupMutation, organizationSettings], ) const handleThresholdChange = (rawValue: number) => { @@ -295,7 +295,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { const validValue = clamp( rawValue, ORG_AUTO_TOPUP_CONSTANTS.MIN_THRESHOLD_CREDITS, - ORG_AUTO_TOPUP_CONSTANTS.MAX_THRESHOLD_CREDITS + ORG_AUTO_TOPUP_CONSTANTS.MAX_THRESHOLD_CREDITS, ) pendingSettings.current = { threshold: validValue, topUpAmountDollars } @@ -318,7 +318,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { const validValue = clamp( rawValue, MIN_TOPUP_DOLLARS, - ORG_AUTO_TOPUP_CONSTANTS.MAX_TOPUP_DOLLARS + ORG_AUTO_TOPUP_CONSTANTS.MAX_TOPUP_DOLLARS, ) pendingSettings.current = { threshold, topUpAmountDollars: validValue } @@ -365,7 +365,7 @@ export function useOrgAutoTopup(organizationId: string): OrgAutoTopupState { } const topUpCredits = Math.round( - (topUpAmountDollars * 100) / ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT + (topUpAmountDollars * 100) / ORG_AUTO_TOPUP_CONSTANTS.CENTS_PER_CREDIT, ) try { From cea19b46613c030ba5a534678495adf01919860d Mon Sep 17 00:00:00 2001 From: brandonkachen Date: Tue, 21 Oct 2025 16:50:49 -0700 Subject: [PATCH 10/10] fix(ci): replace TypeScript import with inline uniq function in generate-ci-env.js Node.js in CI cannot load .ts files directly. Replaced the import from lodash-replacements.ts with an inline native uniq implementation. --- scripts/generate-ci-env.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/generate-ci-env.js b/scripts/generate-ci-env.js index 91de1020a..0f4cbe0a4 100644 --- a/scripts/generate-ci-env.js +++ b/scripts/generate-ci-env.js @@ -7,7 +7,10 @@ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' -import { uniq } from '../common/src/util/lodash-replacements.ts' +// Native uniq implementation (lodash replacement) +function uniq(arr) { + return Array.from(new Set(arr)) +} const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename)